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 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; }