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.
This commit is contained in:
2026-09-07 16:48:16 +07:00
parent aa6a74f229
commit 200be77903
2 changed files with 34 additions and 3 deletions
+33 -2
View File
@@ -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) {