From 200be7790302fde53946b0e6ffa8f8926bf0547b Mon Sep 17 00:00:00 2001 From: 3dtours Date: Mon, 7 Sep 2026 16:48:16 +0700 Subject: [PATCH] feat: full-res screen sharpen on camera-captured exports Lightroom-style 'Screen' output sharpen (unsharp mask, 3x3 convolution via Skia ImageFilter.MakeMatrixConvolution, amount 0.5, kernel sums to 1) applied to final pixels before JPEG encode. Camera-capture branch passes { dpi: 300, sharpen: true }; library-mode export untouched. Export pixels were already full resolution - sharpen is a new final GPU pass, no pixel-count change. --- App.tsx | 2 +- src/utils/exportEngine.ts | 35 +++++++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/App.tsx b/App.tsx index 0cd4244..9eda618 100644 --- a/App.tsx +++ b/App.tsx @@ -294,7 +294,7 @@ export default function App() { selectedFrame, useGeotag, captureGps, - { dpi: 300 } // stamp 300dpi into JPEG header (print-ready metadata) + { dpi: 300, sharpen: true } // 300dpi header + full-res screen sharpen ); if (result) { diff --git a/src/utils/exportEngine.ts b/src/utils/exportEngine.ts index 527de5b..7284750 100644 --- a/src/utils/exportEngine.ts +++ b/src/utils/exportEngine.ts @@ -1,4 +1,4 @@ -import { Skia, ImageFormat, BlendMode, TileMode } from '@shopify/react-native-skia'; +import { Skia, ImageFormat, BlendMode, TileMode, type SkImage } from '@shopify/react-native-skia'; import * as FileSystem from 'expo-file-system/legacy'; import * as MediaLibrary from 'expo-media-library/legacy'; import { Asset } from 'expo-asset'; @@ -16,6 +16,34 @@ 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; +} + +// Lightroom-style "Screen" output sharpen: subtle unsharp mask. A 3x3 +// convolution whose kernel sums to 1 (brightness preserved), amount ~0.5. +// ponytail: fixed subtle preset tuned for full-res desktop/screen output; turn +// into an amount/radius UI control if users ask for adjustable strength. +function screenSharpenImage(image: SkImage, amount = 0.5): SkImage { + const w = image.width(); + const h = image.height(); + const center = 1 + 4 * amount; + const filter = Skia.ImageFilter.MakeMatrixConvolution( + 3, 3, + [0, -amount, 0, -amount, center, -amount, 0, -amount, 0], + 1, 0, 1, 1, TileMode.Clamp, false, null, null + ); + const surf = Skia.Surface.Make(w, h); + if (!surf) return image; // fall back to unsharpened on allocation failure + const paint = Skia.Paint(); + paint.setImageFilter(filter); + const c = surf.getCanvas(); + c.clear(Skia.Color('transparent')); + c.drawImage(image, 0, 0, paint); + const sharpened = surf.makeImageSnapshot(); + if (!sharpened) return image; + return sharpened; } // Fixed filename: every export overwrites the previous one, so temp files @@ -187,7 +215,10 @@ export async function processAndExportPhoto( } // 9. Snapshot & Encode to JPEG (base64 string, no SkData round-trip) - const resultImage = surface.makeImageSnapshot(); + let resultImage = surface.makeImageSnapshot(); + if (options?.sharpen) { + resultImage = screenSharpenImage(resultImage); + } let base64 = resultImage.encodeToBase64(ImageFormat.JPEG, 95); if (!base64) {