Files
RecipesCam/modules/recipescam-export/index.ts
T
admin a85ebf0a81 Sell PRO as an in-app unlock, take shared photos, and let FX run negative
- One free Play app (com.locphamtran.recipescamera) with an expo-iap PRO
  unlock, replacing the paid apk for the store channel
- Android SEND/SEND_MULTIPLE share target feeds photos into the editor,
  on cold start and on resume
- BACK button in the TopBar and press-and-hold PEEK replace the chip row
- NOISE REDUCTION and SHARPENING accept -10: a negative SHARPENING softens
  the frame before the grain pass, a negative NOISE REDUCTION re-grains it,
  in the Skia engine, the live preview and the Kotlin export path alike
2026-09-16 06:48:22 +07:00

127 lines
5.5 KiB
TypeScript

import { requireNativeModule } from 'expo-modules-core';
export interface DecodeEncodeTimings {
decodeMs: number;
encodeWriteMs: number;
totalMs: number;
}
export interface ColorTimings {
decodeMs: number;
toneMs?: number;
cinemaMs?: number;
colorMs: number;
clarityMs?: number;
grainMs?: number;
frameMs?: number;
sharpenMs?: number;
encodeWriteMs: number;
totalMs: number;
}
/** Rect (pixel coords) of the center-largest aspect crop, computed in JS. */
export interface CropRect {
x: number;
y: number;
width: number;
height: number;
}
/** ToneUniforms from toneShader.ts (0/0/0 = skip tone pass). */
export interface ToneUniforms {
dr: number;
hl: number;
sh: number;
}
/** Optional pipeline params passed as a map (native Coroutine arity cap = 8). */
export interface PhotoAdjust {
/** ToneUniforms from toneShader.ts; default 0/0/0 = skip tone pass. */
toneDr?: number;
toneHl?: number;
toneSh?: number;
/** 16 flat floats from cinemaShader.ts getCinemaUniforms(season).flat; null/omitted or flat[0]=0 = skip. */
cinema?: number[] | null;
/** ColorAdjustments.denoise -10..10; >0 blur, <0 re-grain (0 = skip). */
denoise?: number;
/** ColorAdjustments.clarity -10..10; >0 conv sharpen, <0 mist blur (0 = skip). */
clarity?: number;
/** ColorAdjustments.grain 0..10 (0 = skip). */
grain?: number;
/** JPEG quality 0..100; default 95. */
quality?: number;
/** Final screen-sharpen amount (exportEngine #9 screenSharpenImage); >0 applies. */
sharpen?: number;
/** Soften blur radius for a negative sharpening knob (exportEngine #4); >0 applies. */
soften?: number;
/** DPI stamped into the JPEG header after encode (patchJpegDpi port); 0 = skip. */
dpi?: number;
/** Composition aspect-ratio crop computed after decode (center-largest, engine #1b). */
aspect?: '4:3' | '3:2' | 'full';
/** Photo frame preset applied before encode, mirroring frameUtils.ts. Default 'none'. */
frameId?: 'none' | 'classic-white' | 'cinematic' | 'polaroid' | 'wallframe';
/** Base64 PNG art (wallframe only, 4000x3117). Decoded per call. */
frameArtB64?: string;
/** Base64 TTF body font (geo stamp only). Cached after first decode. */
fontB64?: string;
/** Base64 TTF emoji font (geo stamp only). Cached after first decode. */
iconB64?: string;
/** Optional GPS stamp {locality, timestamp}; null/omitted = skip. */
geo?: { locality: string; timestamp: number } | null;
/** Encode PNG instead of JPEG. Probe-only. */
png?: boolean;
/** Probe-only: sample these photo-space pixel coords after frame pass, before encode; logs via samplePixel. */
probeSamples?: Array<{ x: number; y: number }>;
/** Probe-only: log per-row stamp diff/amber/white metrics (see samplePixel) and return them in ColorTimings-adjacent logs. */
probeStampDiff?: boolean;
}
export interface RecipescamExportModule {
/** Decode JPEG/PNG at srcPath (plain absolute path), re-encode JPEG q and write dstPath. All native, background. Returns stage timings in ms. */
decodeEncodeAsync(srcPath: string, dstPath: string, quality: number): Promise<DecodeEncodeTimings>;
/**
* Native pipeline mirroring exportEngine.ts filter order — decode, optional
* center-largest crop, optional tone pass (TONE_SKSL port), optional cinema
* seasonal grade (CINEMA_SKSL port), row-major 4x5 color matrix (translate
* column in 0..1 Skia space), optional denoise blur / clarity conv / mist
* blur / grain Overlay, then JPEG-encode to dstPath. All native, off the
* JS/main threads. Stage order mirrors exportEngine: crop -> tone -> cinema
* -> matrix -> denoise/clarity -> grain -> frame (classic/cinematic/
* polaroid/wallframe) -> GPS stamp (geo, bottom of the composed output)
* -> encode. Timings gain frameMs when a frame/stamp pass runs.
*/
processPhotoAsync(srcPath: string, dstPath: string, matrix: number[], crop: CropRect | null, adjust?: PhotoAdjust): Promise<ColorTimings>;
/** P1 alias: matrix + crop, no tone/cinema/enhance passes. */
processColorAsync(srcPath: string, dstPath: string, matrix: number[], crop: CropRect | null, quality: number): Promise<ColorTimings>;
/**
* Insert an encoded image into DCIM/Camera (MediaStore RELATIVE_PATH) — where
* the stock camera writes. Returns the MediaStore _ID (expo Asset.id shape),
* or '' when the insert/copy fails, so JS falls back to MediaLibrary.
*/
saveToCameraRollAsync(srcPath: string, mimeType: string): Promise<string>;
/**
* Raw device identity the stock Xiaomi camera folds into EXIF 0x889e (the
* watermark blob HyperOS Gallery reads) but CameraX/HAL never supplies:
* codename, marketing name, manufacturer, model. Empty strings when absent.
*/
deviceInfo(): { device: string; marketName: string; manufacturer: string; model: string };
/**
* The image a system share handed this launch (AndroidManifest
* ACTION_SEND — see takeSharedImage in the Kotlin module): the sender's
* stream is copied into the cache and returned as an ordinary file.
* `path` is "" when this launch was not a share, or when the share was
* already taken — the intent is read and cleared exactly once.
*/
takeSharedImage(): Promise<{ path: string; assetId: string; type: string }>;
/** P0 probe helpers — removed before ship. */
ping(): Promise<string>;
materializeBenchAsset(assetName: string): Promise<string>;
materializeSolidAsset(r: number, g: number, b: number): Promise<string>;
materializeRampAsset(): Promise<string>;
materializeStepAsset(): Promise<string>;
}
export default requireNativeModule('RecipesCamExport') as RecipescamExportModule;