// Gallery-facing photo metadata: the RC_ filename convention and the file-level // EXIF helpers both export engines share (the codec itself lives in exifWrite.ts). import { File } from 'expo-file-system'; import * as FileSystem from 'expo-file-system/legacy'; import RecipescamExport from '../../modules/recipescam-export'; import { isFloorDevice } from './deviceFloor'; import { writeJpegExif, SOFTWARE, type DeviceIdentity, type ExifStamp } from './exifWrite'; // RC_[_exported].jpg — the name the gallery shows for a saved // photo. The exported variant marks a photo re-exported from the library. export function rcPhotoName(exported: boolean, date = new Date()): string { const p = (n: number) => String(n).padStart(2, '0'); const stamp = `${date.getFullYear()}${p(date.getMonth() + 1)}${p(date.getDate())}_${p(date.getHours())}${p(date.getMinutes())}${p(date.getSeconds())}`; return `RC_${stamp}${exported ? '_exported' : ''}.jpg`; } // Cache path for the next export. Two renders landing in the same second would // collide on the name AND on RN's cache (keyed by URI), so the // timestamp walks forward a second at a time instead of taking a suffix. export function nextPhotoPath(exported: boolean): string { let ms = Date.now(); for (;;) { const path = `${FileSystem.cacheDirectory}${rcPhotoName(exported, new Date(ms))}`; if (!new File(path).exists) return path; ms += 1000; } } export async function readPhotoBytes(uri: string): Promise { try { return await new File(uri).bytes(); } catch (e) { console.warn('EXIF source read failed:', e); return null; } } // Device identity for the Gallery watermark blob (exifWrite.watermarkBlob). // Read once — the props cannot change while the app runs. null when the native // module is older than this call, so stamping still works without it. let deviceIdentity: DeviceIdentity | null | undefined; export function getDeviceIdentity(): DeviceIdentity | null { if (deviceIdentity === undefined) { try { deviceIdentity = RecipescamExport.deviceInfo(); } catch (e) { console.warn('deviceInfo unavailable:', e); deviceIdentity = null; } } return deviceIdentity; } // Device facts the capture and render paths budget against (deviceFloor.ts): // total RAM and CPU cores. Lives next to deviceInfo because that is where the // native handle already is; read once, the numbers cannot change while the app // runs. ramMb 0 = the platform refused, which deviceFloor reads as "not a floor // device" so an unmeasurable phone keeps the full-quality path. let devicePerfCache: { ramMb: number; cores: number } | null = null; export function devicePerf(): { ramMb: number; cores: number } { if (!devicePerfCache) { try { devicePerfCache = RecipescamExport.devicePerf(); } catch (e) { console.warn('devicePerf unavailable, full-quality path:', e); devicePerfCache = { ramMb: 0, cores: 0 }; } // One line, so a floor device in the field can be confirmed from logcat: // "adb logcat -s ReactNativeJS | grep floor". const { ramMb, cores } = devicePerfCache; console.info(`devicePerf: ramMb=${ramMb} cores=${cores} floor=${isFloorDevice(ramMb)}`); } return devicePerfCache; } // Stamp EXIF into a file already on disk (the native engine writes the JPEG // itself, so those bytes never pass through JS). Never throws — a metadata // failure must not cost the user the photo. export async function stampExifOnFile(fileUri: string, stamp: ExifStamp, sourceUri?: string | null): Promise { try { const target = new File(fileUri); const source = sourceUri ? await readPhotoBytes(sourceUri) : null; target.write(writeJpegExif(await target.bytes(), { software: SOFTWARE, device: getDeviceIdentity(), ...stamp }, source)); } catch (e) { console.warn('EXIF stamp failed:', e); } }