feat: v1.1 — highlight-weighted metering, settings modal, photo viewer, shutter sound control, cleanup

This commit is contained in:
2026-09-09 09:41:55 +07:00
parent e9a9005ef2
commit 09dbc72cce
46 changed files with 1684 additions and 2491 deletions
+16 -37
View File
@@ -13,6 +13,9 @@ import { patchJpegDpi } from './jpegDpi';
export interface ExportResult {
uri: string;
savedToLibrary: boolean;
// MediaLibrary asset id when the photo was saved to the gallery — lets the
// photo viewer's trash button remove the gallery copy too.
assetId?: string | null;
}
export interface ExportOptions {
@@ -69,9 +72,7 @@ function screenSharpenImage(image: SkImage, amount = 0.5): SkImage {
// 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. Previous export file is deleted after the new one is written and
// gallery-saved, so at most one stale temp file can ever exist.
let lastExportPath: string | null = null;
// that cache.
const nextExportFile = () => `${FileSystem.cacheDirectory}camrecipe_pro_export_${Date.now()}.jpg`;
export async function processAndExportPhoto(
@@ -82,10 +83,6 @@ export async function processAndExportPhoto(
gpsInfo: GPSInfo | null,
options?: ExportOptions
): Promise<ExportResult | null> {
const t0 = Date.now();
// Direct console.error templates (user log captures are E-level only). Steps
// use full static prefixes — release minification dropped step literals when
// they were passed through a wrapper, so no indirection here.
try {
// 1. Read source image file into Skia
const skiaData = await Skia.Data.fromURI(sourceUri);
@@ -99,13 +96,9 @@ export async function processAndExportPhoto(
console.error('Failed to parse image from URI');
return null;
}
console.error(`[export] decode ${Date.now() - t0}ms`);
let skImage = skImage0;
let width = skImage.width();
let height = skImage.height();
console.error(`[export] BEGIN ${width}x${height} ev=${recipe.adjustments.exposureCompensation ?? 0} evFromCamera=${options?.evFromCamera ?? false}`);
// 1b. Aspect-ratio crop (composition): center-largest upright crop of the
// requested ratio. Runs BEFORE any processing so frame/GPS/grain/sharpen
// all apply to exactly the pixels the user framed. Wall Frame is exempt —
@@ -144,7 +137,6 @@ export async function processAndExportPhoto(
skImage = cropped;
width = cropW;
height = cropH;
console.error(`[export] aspect-crop ${width}x${height}`);
}
// 2. Create offscreen canvas (GPU-backed when available — see createSurface)
@@ -181,7 +173,7 @@ export async function processAndExportPhoto(
try {
toneEffect = Skia.RuntimeEffect.Make(TONE_SKSL);
} catch (e) {
console.error('[export] TONE_SKSL compile failed - falling back to matrix-only: ' + e);
console.error('TONE_SKSL compile failed - falling back to matrix-only: ' + e);
}
if (toneEffect) {
const imageShader = skImage.makeShaderOptions(
@@ -194,10 +186,10 @@ export async function processAndExportPhoto(
if (toneShader) {
paint.setShader(toneShader);
} else {
console.error('[export] tone shader build failed - falling back to matrix-only');
console.error('Tone shader build failed - falling back to matrix-only');
}
} else {
console.error('[export] TONE_SKSL compile failed - falling back to matrix-only');
console.error('TONE_SKSL compile failed - falling back to matrix-only');
}
}
@@ -222,7 +214,7 @@ export async function processAndExportPhoto(
}
}
} catch (e) {
console.error('[export] CINEMA_SKSL failed - skipped: ' + e);
console.error('CINEMA_SKSL failed - skipped: ' + e);
}
}
@@ -462,7 +454,6 @@ export async function processAndExportPhoto(
}
// 9. Snapshot & Encode to JPEG (base64 string, no SkData round-trip)
console.error(`[export] composite ${Date.now() - t0}ms`);
let resultImage = outputSurface.makeImageSnapshot();
// Sharpening knob (0..10) overrides the legacy boolean sharpen flag:
// legacy camera captures keep sharpen:true → 0.5 fallback when knob is 0.
@@ -471,7 +462,6 @@ export async function processAndExportPhoto(
if (sharpenAmount > 0) {
resultImage = screenSharpenImage(resultImage, sharpenAmount);
}
console.error(`[export] sharpen ${Date.now() - t0}ms`);
// Encode straight to raw JPEG bytes — no base64. The old path round-tripped
// the whole 12MP frame through JS base64 decode/encode for the DPI patch and
// then writeAsStringAsync, which alone took ~6s of the render.
@@ -481,8 +471,6 @@ export async function processAndExportPhoto(
console.error('Failed to encode image to JPEG');
return null;
}
console.error(`[export] encode ${Date.now() - t0}ms`);
// 9b. Patch DPI metadata (JFIF density / EXIF resolution) when requested —
// in-place Uint8Array work, no base64 round-trip.
if (options?.dpi && options.dpi > 0) {
@@ -499,32 +487,23 @@ export async function processAndExportPhoto(
console.error('Failed to write export file: ' + e);
return null;
}
console.error(`[export] write ${Date.now() - t0}ms`);
console.error(`[export] total-done ${Date.now() - t0}ms`);
// 11. Request Media Library permission & Save to device gallery
// 11. Request Media Library permission & Save to device gallery.
// createAssetAsync (not saveToLibraryAsync) so the caller learns the
// gallery asset id — the photo viewer's trash button needs it to remove
// the saved copy. ponytail: old exports are NOT retired here anymore — the
// in-app photo history still references their cache files for the swipe
// viewer; the OS cache purge reclaims them eventually.
const mediaPermission = await MediaLibrary.requestPermissionsAsync();
if (mediaPermission.granted) {
try {
await MediaLibrary.saveToLibraryAsync(exportFile);
// New file is safely in the gallery — retire the previous export temp.
if (lastExportPath && lastExportPath !== exportFile) {
try {
await FileSystem.deleteAsync(lastExportPath, { idempotent: true });
} catch (e) {
console.warn('Cleanup of previous export temp failed:', e);
}
}
lastExportPath = exportFile;
return { uri: exportFile, savedToLibrary: true };
const asset = await MediaLibrary.createAssetAsync(exportFile);
return { uri: exportFile, savedToLibrary: true, assetId: asset.id };
} catch (e) {
console.warn('Save to gallery failed:', e);
}
} else {
console.warn('Media Library permission denied. Image saved to temporary cache only.');
}
console.error(`[export] gallery-save ${Date.now() - t0}ms`);
return { uri: exportFile, savedToLibrary: false };
} catch (error) {
console.error('Error during photo processing and export:', error);