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:
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user