Add aspect-ratio composition band and RAW DNG sidecar capture

This commit is contained in:
2026-09-08 18:43:02 +07:00
parent a16c8734a8
commit d43405a4de
6 changed files with 371 additions and 65 deletions
+53 -5
View File
@@ -3,7 +3,7 @@ import { File } from 'expo-file-system';
import * as FileSystem from 'expo-file-system/legacy';
import * as MediaLibrary from 'expo-media-library/legacy';
import { Asset } from 'expo-asset';
import { Recipe, GPSInfo, FrameId } from '../types';
import { Recipe, GPSInfo, FrameId, AspectRatio, ASPECT_RATIO_W_H } from '../types';
import { getSkiaColorMatrix, applyExposureGain } from './colorUtils';
import { TONE_SKSL, getToneUniforms, toneIsActive } from './toneShader';
import { CINEMA_SKSL, getCinemaUniforms, cinemaIsActive } from './cinemaShader';
@@ -25,6 +25,12 @@ export interface ExportOptions {
// 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.
evFromCamera?: boolean;
// Composition aspect-ratio crop (upright 4:3 or 3:2). Center-largest crop
// applied right after decode — every later step (frame/GPS/sharpen/size)
// then runs on the cropped pixels, so the export matches the ratio the
// viewfinder displayed. WALL FRAME ignores this (frame keeps its fixed
// opening and export size). 'full'/undefined = no crop.
aspect?: AspectRatio;
}
// Lightroom-style "Screen" output sharpen: subtle unsharp mask. A 3x3
@@ -87,18 +93,60 @@ export async function processAndExportPhoto(
console.error('Failed to load image data from URI');
return null;
}
const skImage = Skia.Image.MakeImageFromEncoded(skiaData);
const skImage0 = Skia.Image.MakeImageFromEncoded(skiaData);
if (!skImage) {
if (!skImage0) {
console.error('Failed to parse image from URI');
return null;
}
console.error(`[export] decode ${Date.now() - t0}ms`);
const width = skImage.width();
const height = skImage.height();
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 —
// its export is the fixed frame artwork whose opening cover-crops anyway.
const ratioWH = options?.aspect && options.aspect !== 'full' ? ASPECT_RATIO_W_H[options.aspect] : null;
if (ratioWH && frameId !== 'wallframe') {
const curAspect = width / height;
let cropW = width;
let cropH = height;
let srcX = 0;
let srcY = 0;
if (curAspect > ratioWH) {
cropW = Math.round(height * ratioWH);
srcX = Math.round((width - cropW) / 2);
} else {
cropH = Math.round(width / ratioWH);
srcY = Math.round((height - cropH) / 2);
}
const cropSurface = createSurface(cropW, cropH);
if (!cropSurface) {
console.error('Failed to create aspect-crop surface');
return null;
}
const cropCanvas = cropSurface.getCanvas();
cropCanvas.drawImageRect(
skImage,
Skia.XYWHRect(srcX, srcY, cropW, cropH),
Skia.XYWHRect(0, 0, cropW, cropH),
Skia.Paint()
);
const cropped = cropSurface.makeImageSnapshot();
if (!cropped) {
console.error('Failed to snapshot aspect crop');
return null;
}
skImage = cropped;
width = cropW;
height = cropH;
console.error(`[export] aspect-crop ${width}x${height}`);
}
// 2. Create offscreen canvas (GPU-backed when available — see createSurface)
const surface = createSurface(width, height);
if (!surface) {