diff --git a/App.tsx b/App.tsx index ec1f6a3..b94484a 100644 --- a/App.tsx +++ b/App.tsx @@ -1555,12 +1555,14 @@ export default function App() { // shutter instant as the EXIF capture date. exported: false, capturedAt, - // 300dpi header + full-res screen sharpen. No evFromCamera: the camera - // AE bias is never written, so the export applies the same software EV - // gain the preview shows. + // 300dpi header. No evFromCamera: the camera AE bias is never written, + // so the export applies the same software EV gain the preview shows. + // No forced output sharpen either: the 0.5 unsharp mask multiplied + // sensor noise ~3.2x (5x at Nyquist), so dim/high-ISO captures came + // back far noisier than the preview. The SHARPENING knob still + // sharpens when the user dials it above 0. // aspect: crop to the on-screen composition ratio (full = keep 4:3 native). dpi: 300, - sharpen: true, aspect: aspectRatio === 'full' ? undefined : aspectRatio, watermark: wmForExport(), liteMark, diff --git a/src/utils/exportEngine.ts b/src/utils/exportEngine.ts index 617c3c4..273e189 100644 --- a/src/utils/exportEngine.ts +++ b/src/utils/exportEngine.ts @@ -28,9 +28,6 @@ export interface ExportResult { export interface ExportOptions { // DPI to stamp into the JPEG header (metadata only — pixel size never changes). dpi?: number; - // Apply a subtle Lightroom-style "Screen" output sharpen (unsharp mask) to the - // final pixels before encoding. Pixels already export at full resolution. - sharpen?: boolean; // True for photos captured through the live camera: exposure compensation was // already applied as a hardware AE bias at capture, so no 2^EV matrix gain // should be added on top. Library stills omit this and get the matrix gain. @@ -149,7 +146,7 @@ function release(owned: SkDisposable[], item: SkDisposable | null): void { disposeAll([item]); } -function screenSharpenImage(image: SkImage, amount = 0.5): SkImage { +function screenSharpenImage(image: SkImage, amount: number): SkImage { const w = image.width(); const h = image.height(); const center = 1 + 4 * amount; @@ -894,12 +891,11 @@ export async function processAndExportPhoto( // 9. Snapshot & Encode to JPEG (base64 string, no SkData round-trip) let resultImage = own(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. - // A negative knob means "no sharpening at all" (its blur already ran at - // step 4, under the grain), so the legacy 0.5 fallback must not fire. - const sharpenAmount = - sharpKnob > 0 ? (sharpKnob / 10) * 0.8 : sharpKnob < 0 ? 0 : options?.sharpen ? 0.5 : 0; + // Sharpening knob (0..10) is the only source: the old 0.5 fallback that + // fired on every camera capture multiplied sensor noise ~3.2x (5x at + // Nyquist) and made dim/high-ISO shots unusable. A negative or zero knob + // sharpens not at all (a negative one blurs at step 4, under the grain). + const sharpenAmount = sharpKnob > 0 ? (sharpKnob / 10) * 0.8 : 0; if (sharpenAmount > 0) { const sharpened = screenSharpenImage(resultImage, sharpenAmount); // The helper returns its own input when it cannot allocate a surface. diff --git a/src/utils/nativeExport.ts b/src/utils/nativeExport.ts index 62d8507..7c52e79 100644 --- a/src/utils/nativeExport.ts +++ b/src/utils/nativeExport.ts @@ -24,9 +24,6 @@ export interface NativeExportResult { export interface NativeExportOptions { // DPI to stamp into the JPEG header (metadata only — pixel size never changes). dpi?: number; - // Apply the subtle "Screen" output sharpen (exportEngine #9) to the final - // pixels before encoding (legacy camera captures pass sharpen: true → 0.5). - sharpen?: boolean; // True for photos captured through the live camera: the AE bias already // applied exposure compensation at capture, so no 2^EV matrix gain is added. evFromCamera?: boolean; @@ -73,11 +70,11 @@ export async function processAndExportPhotoNative( // (EXPO_PUBLIC_NATIVE_EXPORT is not inlined into a release HBC bundle). const tone = getToneUniforms(adjustments, recipe.baseFilter); const cinema = getCinemaUniforms(recipe.cinema); - // Final screen sharpen (engine #9): the sharpening knob (-10..10) overrides - // the legacy boolean sharpen flag (camera captures keep it true → 0.5). + // Final screen sharpen (engine #9): the sharpening knob (-10..10) is the + // only source — the old 0.5 fallback on every camera capture multiplied + // sensor noise ~3.2x and made dim/high-ISO shots unusable. const sharpKnob = adjustments.sharpening ?? 0; - const sharpenAmount = - sharpKnob > 0 ? (sharpKnob / 10) * 0.8 : sharpKnob < 0 ? 0 : options?.sharpen ? 0.5 : 0; + const sharpenAmount = sharpKnob > 0 ? (sharpKnob / 10) * 0.8 : 0; const aspect: '4:3' | '3:2' | undefined = options?.aspect && options.aspect !== 'full' && frameId !== 'wallframe' ? options.aspect