- 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
- FRAME tab now pinches the whole view (frame + photo + watermark) in both
camera and library; one finger pans that view. Preview only: the exported
file is identical at 1x and while the view is magnified.
- Watermark pinch/drag stay relative and keep priority while the WATERMARK
row is open, so they still work after returning from the FRAME tab.
- Exports go through a native MediaStore insert into DCIM/Camera, falling
back to expo-media-library when the native save fails.
HyperOS Gallery reads EXIF 0x889e (a JSON blob) to build its watermark
frame; without it a photo fails with "cannot recognize the parameters".
CameraX/HAL never supplies that vendor tag, so a capture from this app
lacked it while a stock-camera photo carried it.
Read back the same props the stock camera derives the blob from
(ro.product.device / ro.product.marketname / Build.MANUFACTURER) via the
native module, synthesize the blob in exifWrite, and write it only when
the source has none and the device is Xiaomi/Redmi/POCO - a source blob
is never overwritten.
The highlight knee opened at 0.45, so dropping HIGHLIGHT pulled a mid-grey
down with the true highlights. It now opens at 0.65 (and the coefficient
drops 0.32 -> 0.22 to stay monotonic), so the slider leaves everything up to
a light grey untouched and still rolls the bright end off. Shadow is
untouched — its 0.00..0.55 knee was already right.
Switching between the library and the camera now drops the edits back to
the recipe's own values, no frame and no custom mark. A frame was picked
for the still it sat on and the mark was placed against that very photo, so
neither may follow the user across the switch (or into a live capture),
exactly like the sliders that were tuned on the photo in front of them.
Highlight/Shadow scaled R, G and B by one luma gain. That keeps the ratio
but crushes absolute chroma, so -SH turned a saturated blue into near-black
and -HL turned bright colours grey, and both sliders only bit at the very
ends of the range (knees 0.80..1.00 / 0.00..0.30) — they read as dead on
any photo without true whites or blacks.
The curve now moves the luma and carries the colour difference (rgb - luma)
along at clamp(gain, 0.55, 1.35), so hue survives darkening and lifting
instead of collapsing to black or white. Both knobs are pure additive luma
shifts with soft knees over the upper/lower half: the 0.50 midpoint moves
under 3%, and the curve stays monotonic (the old multiplicative form was
not — with hl=-1 a grey 0.73 came out darker than 0.80).
Kotlin applyTone port and the native bench probe gates follow.
Verified on device (4200x2800 probe: grey ramp + colour patches), library
export at ratio 4:3: -10 highlight leaves the darks alone and drops white
243->189 with the sky still blue; -10 shadow keeps blue as dark blue
(0,0,254)->(0,3,146), never black, ramp stays monotonic. Export keeps the
source 4200x2800 as well, so nothing is cropped.
Local Kotlin module modules/recipescam-export (autolinked via expo ./modules)
exposes decodeEncodeAsync — BitmapFactory decode -> JPEG q95 encode -> file,
all on Dispatchers.IO off the JS/main threads. EXPO_PUBLIC_BENCH=1 builds run
src/dev/nativeBenchProbe.ts: 3 timed decode+encode passes on the bundled
wallframe artwork plus a JS-timer gap check proving the JS thread never
blocks during native rendering. Legacy Skia engine untouched (reference).