feat(exif): stamp RC_<ts> filenames + EXIF on capture and export

This commit is contained in:
2026-09-11 17:59:44 +07:00
parent 6faef4aef7
commit 62b9735867
6 changed files with 478 additions and 14 deletions
+17 -6
View File
@@ -7,12 +7,12 @@
// Deliberately does NOT import exportEngine or Skia: when the native path is
// active the Skia engine must not even load.
import RecipescamExport, { PhotoAdjust } from '../../modules/recipescam-export';
import * as FileSystem from 'expo-file-system/legacy';
import * as MediaLibrary from 'expo-media-library/legacy';
import { Recipe, GPSInfo, FrameId, AspectRatio, ColorAdjustments } from '../types';
import { getSkiaColorMatrix, applyExposureGain } from './colorUtils';
import { getToneUniforms } from './toneShader';
import { getCinemaUniforms } from './cinemaShader';
import { nextPhotoPath, stampExifOnFile } from './photoMeta';
export interface NativeExportResult {
uri: string;
@@ -37,15 +37,15 @@ export interface NativeExportOptions {
// module renders geo captions but has no text-watermark draw (ponytail: add
// it when native parity with the Skia engine is completed).
watermark?: { text: string; x: number; y: number } | null;
// Gallery filename + EXIF, same contract as ExportOptions.exported /
// .capturedAt: RC_<ts>_exported.jpg keeps the loaded photo's own EXIF.
exported?: boolean;
capturedAt?: number | null;
}
// The Kotlin module reads plain absolute paths, not file:// URIs.
const stripScheme = (uri: string) => uri.replace(/^file:\/\//, '');
// Unique filename per export (same cache-bust rationale as exportEngine: a fixed
// path would serve stale <Image> cache entries).
const nextExportFile = () => `${FileSystem.cacheDirectory}camrecipe_pro_export_${Date.now()}.jpg`;
export async function processAndExportPhotoNative(
sourceUri: string,
recipe: Recipe,
@@ -99,9 +99,20 @@ export async function processAndExportPhotoNative(
aspect,
};
const exportFile = nextExportFile(); // file:// URI (cacheDirectory)
const exportFile = nextPhotoPath(options?.exported === true); // file:// URI (cacheDirectory)
const dstPath = stripScheme(exportFile);
await RecipescamExport.processPhotoAsync(stripScheme(sourceUri), dstPath, matrix, null, adjust);
// Same EXIF contract as the Skia engine (step 9c). ponytail: pixel
// dimensions are left out here — Kotlin knows the rendered size, JS does not.
await stampExifOnFile(
exportFile,
{
dateTime: options?.capturedAt ? new Date(options.capturedAt) : null,
gps: useGeotag && gpsInfo ? { latitude: gpsInfo.latitude, longitude: gpsInfo.longitude } : null,
dpi: options?.dpi ?? null,
},
sourceUri
);
const mediaPermission = await MediaLibrary.requestPermissionsAsync();
if (mediaPermission.granted) {