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
+28 -8
View File
@@ -9,6 +9,8 @@ import { TONE_SKSL, getToneUniforms, toneIsActive, toneUniformArray } from './to
import { CINEMA_SKSL, getCinemaUniforms, cinemaIsActive } from './cinemaShader';
import { drawFrameOnCanvas, polaroidLayout, POLAROID_CARD, POLAROID_WIN_W, wallframeLayout, WALLFRAME_W, WALLFRAME_H, WALLFRAME_LAND_W, WALLFRAME_LAND_H } from './frameUtils';
import { patchJpegDpi } from './jpegDpi';
import { writeJpegExif } from './exifWrite';
import { nextPhotoPath, readPhotoBytes } from './photoMeta';
import { applyPhotoRotation } from './skiaImage';
export interface ExportResult {
@@ -76,6 +78,13 @@ export interface ExportOptions {
// see exactly the orientation the preview showed.
photoRotation?: 0 | 90 | 180 | 270;
photoStraighten?: number;
// This export came from a photo loaded out of the library (not a live
// capture): the file is named RC_<ts>_exported.jpg and the loaded photo's
// own EXIF lives on in the output.
exported?: boolean;
// Shutter instant of a live capture (ms). Absent = keep the loaded photo's
// own DateTimeOriginal.
capturedAt?: number | null;
}
// Lightroom-style "Screen" output sharpen: subtle unsharp mask. A 3x3
@@ -111,12 +120,6 @@ function screenSharpenImage(image: SkImage, amount = 0.5): SkImage {
return sharpened;
}
// Unique filename per export. A fixed path made every export share one URI, and
// RN's <Image> cache (keyed by URI) then kept showing the previous photo in the
// thumbnail after a new capture overwrote the file. A fresh URI per export busts
// that cache.
const nextExportFile = () => `${FileSystem.cacheDirectory}camrecipe_pro_export_${Date.now()}.jpg`;
export async function processAndExportPhoto(
sourceUri: string,
recipe: Recipe,
@@ -706,10 +709,27 @@ export async function processAndExportPhoto(
bytes = patchJpegDpi(bytes, options.dpi);
}
// 9c. EXIF. Skia's re-encode dropped the loaded photo's metadata block, so
// rebuild it here: the source photo donates every tag this app does not
// own (camera, lens, exposure) and the fields it is responsible for are
// stamped on top — software + version, capture date, GPS, pixel size.
bytes = writeJpegExif(
bytes,
{
dateTime: options?.capturedAt ? new Date(options.capturedAt) : null,
gps: useGeotag && gpsInfo ? { latitude: gpsInfo.latitude, longitude: gpsInfo.longitude } : null,
width: resultImage.width(),
height: resultImage.height(),
dpi: options?.dpi ?? null,
},
await readPhotoBytes(sourceUri)
);
// 10. Write binary bytes to a unique temporary local file. The unique name
// is load-bearing: a fixed URI would be served from RN's <Image> cache and
// the thumbnail/preview would keep showing the previous export.
const exportFile = nextExportFile();
// the thumbnail/preview would keep showing the previous export (and the
// gallery shows this basename).
const exportFile = nextPhotoPath(options?.exported === true);
try {
new File(exportFile).write(bytes);
} catch (e) {