P5 native export hooked (camera+library) — real photo to gallery on Xiaomi
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
// P5 native export path: mirrors exportEngine.processAndExportPhoto's pipeline
|
||||
// contract (same recipe math, same output file/gallery behavior) but renders
|
||||
// inside the RecipesCamExport Kotlin module — decode/crop/tone/cinema/matrix/
|
||||
// denoise/clarity/grain/frame/GPS/sharpen run off the JS thread and the JPEG is
|
||||
// encoded straight to disk (DPI header patched natively). JS only computes the
|
||||
// parameter set with the same helpers exportEngine uses, then saves to gallery.
|
||||
// Deliberately does NOT import exportEngine or Skia: when the native path is
|
||||
// active the Skia engine must not even load.
|
||||
import RecipescamExport, { PhotoAdjust } from '../../modules/recipescam-export';
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
import * as MediaLibrary from 'expo-media-library/legacy';
|
||||
import { Recipe, GPSInfo, FrameId, AspectRatio, ColorAdjustments } from '../types';
|
||||
import { getSkiaColorMatrix, applyExposureGain } from './colorUtils';
|
||||
import { getToneUniforms } from './toneShader';
|
||||
import { getCinemaUniforms } from './cinemaShader';
|
||||
|
||||
export interface NativeExportResult {
|
||||
uri: string;
|
||||
savedToLibrary: boolean;
|
||||
assetId?: string | null;
|
||||
}
|
||||
|
||||
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;
|
||||
// Composition aspect-ratio crop ('full'/undefined = none). Native computes the
|
||||
// center-largest rect after decode because the pixel size is only known there.
|
||||
aspect?: AspectRatio;
|
||||
}
|
||||
|
||||
// The Kotlin module reads plain absolute paths, not file:// URIs.
|
||||
const stripScheme = (uri: string) => uri.replace(/^file:\/\//, '');
|
||||
|
||||
// Unique filename per export (same cache-bust rationale as exportEngine: a fixed
|
||||
// path would serve stale <Image> cache entries).
|
||||
const nextExportFile = () => `${FileSystem.cacheDirectory}camrecipe_pro_export_${Date.now()}.jpg`;
|
||||
|
||||
export async function processAndExportPhotoNative(
|
||||
sourceUri: string,
|
||||
recipe: Recipe,
|
||||
frameId: FrameId,
|
||||
useGeotag: boolean,
|
||||
gpsInfo: GPSInfo | null,
|
||||
options?: NativeExportOptions
|
||||
): Promise<NativeExportResult | null> {
|
||||
try {
|
||||
const adjustments: ColorAdjustments = recipe.adjustments;
|
||||
// Same matrix rule as exportEngine #3: camera captures skip 2^EV (hardware
|
||||
// AE bias already applied it), library stills get the matrix gain.
|
||||
const matrix = options?.evFromCamera
|
||||
? getSkiaColorMatrix(recipe.baseFilter, adjustments)
|
||||
: applyExposureGain(
|
||||
getSkiaColorMatrix(recipe.baseFilter, adjustments),
|
||||
adjustments.exposureCompensation ?? 0
|
||||
);
|
||||
const tone = getToneUniforms(adjustments);
|
||||
const cinema = getCinemaUniforms(recipe.cinema);
|
||||
// Final screen sharpen (engine #9): the sharpening knob (0..10) overrides
|
||||
// the legacy boolean sharpen flag (camera captures keep it true → 0.5).
|
||||
const sharpKnob = adjustments.sharpening ?? 0;
|
||||
const sharpenAmount = sharpKnob > 0 ? (sharpKnob / 10) * 0.8 : options?.sharpen ? 0.5 : 0;
|
||||
const aspect: '4:3' | '3:2' | undefined =
|
||||
options?.aspect && options.aspect !== 'full' && frameId !== 'wallframe'
|
||||
? options.aspect
|
||||
: undefined;
|
||||
|
||||
const adjust: PhotoAdjust = {
|
||||
toneDr: tone.dr,
|
||||
toneHl: tone.hl,
|
||||
toneSh: tone.sh,
|
||||
cinema: cinema ? cinema.flat : null,
|
||||
denoise: adjustments.denoise,
|
||||
clarity: adjustments.clarity,
|
||||
grain: adjustments.grain,
|
||||
quality: 95, // engine encodes at fixed JPEG 95
|
||||
frameId,
|
||||
// Fonts/artwork are bundled in the APK (res/raw assets_*, drawable
|
||||
// wallframe) — native reads them directly; no base64 bridge needed.
|
||||
geo:
|
||||
useGeotag && gpsInfo
|
||||
? { locality: gpsInfo.locality || 'STREET VIEW', timestamp: gpsInfo.timestamp }
|
||||
: null,
|
||||
sharpen: sharpenAmount > 0 ? sharpenAmount : undefined,
|
||||
dpi: options?.dpi && options.dpi > 0 ? options.dpi : undefined,
|
||||
aspect,
|
||||
};
|
||||
|
||||
const exportFile = nextExportFile(); // file:// URI (cacheDirectory)
|
||||
const dstPath = stripScheme(exportFile);
|
||||
await RecipescamExport.processPhotoAsync(stripScheme(sourceUri), dstPath, matrix, null, adjust);
|
||||
|
||||
const mediaPermission = await MediaLibrary.requestPermissionsAsync();
|
||||
if (mediaPermission.granted) {
|
||||
try {
|
||||
const asset = await MediaLibrary.createAssetAsync(exportFile);
|
||||
return { uri: exportFile, savedToLibrary: true, assetId: asset.id };
|
||||
} catch (e) {
|
||||
console.warn('Save to gallery failed:', e);
|
||||
}
|
||||
} else {
|
||||
console.warn('Media Library permission denied. Image saved to temporary cache only.');
|
||||
}
|
||||
return { uri: exportFile, savedToLibrary: false };
|
||||
} catch (error) {
|
||||
console.error('Error during native photo processing and export:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user