Files
RecipesCam/src/utils/skiaImage.ts
T
admin 76cf4c6d7e Cut 1.2.3 with a shutter that never waits on GPS
Bound the geotag fix with a 2 s deadline and prewarm it at launch, queue a
shot pressed before the camera session binds and replay it, pass the options
argument capturePhotoToFile requires, save through the native path so the
media permission is not needed, and release the Skia surface and bitmap after
every export. Store the JS bundle uncompressed and load the export pipeline on
demand. Note in RELEASE_NOTES.md that none of this moved the tap threshold.
2026-09-17 10:02:35 +07:00

90 lines
3.8 KiB
TypeScript

import { Skia, type SkImage } from '@shopify/react-native-skia';
// Turn a decoded image 90deg clockwise into a fresh SkImage. Used by the FRAME
// tab so a library photo can go landscape <-> portrait before it enters a frame:
// every consumer reads width()/height(), so the pixel-dimension swap carries the
// new orientation through the polaroid window aspect, the cover crop and the
// watermark areas without a single extra layout branch.
// ponytail: offscreen surface, same fallback as exportEngine.createSurface; a
// matrix-based draw would avoid the copy if this ever shows up in a profile.
export function rotateSkImage90(image: SkImage | null): SkImage | null {
if (!image) return null;
const w = image.width();
const h = image.height();
if (!(w > 0 && h > 0)) return null;
// CPU-backed on purpose: a GPU (MakeOffscreen) snapshot keeps its pixels in a
// context texture that is gone by the time the <Canvas> draws a frame later —
// the preview then renders the photo completely black. Raster pixels survive
// the trip to the render thread.
const surface = Skia.Surface.Make(h, w);
if (!surface) return image; // keep the unrotated photo on allocation failure
try {
const canvas = surface.getCanvas();
canvas.clear(Skia.Color('transparent'));
canvas.translate(h, 0);
canvas.rotate(90, 0, 0);
canvas.drawImage(image, 0, 0);
return surface.makeImageSnapshot() ?? image;
} finally {
// The raster snapshot refcounts the pixels, so the surface can go now
// instead of leaving one full-size copy behind per rotation.
surface.dispose();
}
}
// Fine rotation about the centre (the FRAME tab's STRAIGHTEN), keeping the
// original pixel dimensions: the content is scaled up just enough that the
// rotated rectangle still covers the frame, so no empty corner shows. The
// scale is the axis-aligned wrap of the counter-rotated frame — slightly
// conservative, which crops a hair more rather than leaking a corner.
// ponytail: one offscreen surface per call, same trade as rotateSkImage90.
export function rotateSkImageBy(image: SkImage | null, degrees: number): SkImage | null {
if (!image) return null;
const w = image.width();
const h = image.height();
if (!(w > 0 && h > 0)) return null;
if (!degrees) return image;
const rad = (degrees * Math.PI) / 180;
const cos = Math.abs(Math.cos(rad));
const sin = Math.abs(Math.sin(rad));
const k = Math.max((w * cos + h * sin) / w, (w * sin + h * cos) / h);
const surface = Skia.Surface.Make(w, h);
if (!surface) return image; // keep the unrotated photo on allocation failure
try {
const canvas = surface.getCanvas();
canvas.clear(Skia.Color('transparent'));
canvas.translate(w / 2, h / 2);
canvas.rotate(degrees, 0, 0);
canvas.scale(k, k);
canvas.drawImage(image, -w / 2, -h / 2);
return surface.makeImageSnapshot() ?? image;
} finally {
surface.dispose(); // see rotateSkImage90
}
}
// The whole FRAME-tab photo orientation in one call: the quarter turn first
// (rotateSkImage90 swaps the pixel dimensions, which then propagates through
// every consumer unchanged), then the fine STRAIGHTEN on top of it.
export function applyPhotoRotation(
image: SkImage | null,
quarter = 0,
straighten = 0
): SkImage | null {
let out = image;
const turns = Math.round(quarter / 90) % 4;
for (let i = 0; i < turns; i++) {
const next = rotateSkImage90(out);
// A 180/270 turn allocates one full-size copy per turn: the intermediate is
// dead as soon as the next one exists (never the caller's input image).
if (next && next !== out && i > 0) out.dispose();
out = next ?? out;
}
if (straighten) {
const s = rotateSkImageBy(out, straighten);
if (s && s !== out && out !== image) out.dispose(); // see above
out = s ?? out;
}
return out;
}