{"version":3,"file":"transform.cjs","names":[],"sources":["../../src/imaging/transform.ts"],"sourcesContent":["/**\n * Resize, crop, rotate — the operations a PWA runs before an upload.\n *\n * Every one of them ends in a re-encode, which has a consequence worth\n * knowing: **the output carries no EXIF.** Location, camera serial and\n * timestamp do not survive a canvas round trip. For an app handling user\n * photos that is usually the point, and it is the reason `resizeImage` is\n * also the privacy step, not only the bandwidth step.\n */\n\nimport { createSurface, drawScaled, getContext } from \"./canvas\";\nimport { decodeImage } from \"./decode\";\nimport type {\n    CropRect,\n    EncodeOptions,\n    ImageSource,\n    ProcessedImage,\n    ResizeFit,\n    ResizeOptions,\n} from \"./types\";\nimport { encodeImage } from \"./encode\";\n\n/** Background used when a format cannot carry transparency. */\nexport const DEFAULT_BACKGROUND = \"#ffffff\";\n\n/** Formats that have no alpha channel. */\nconst OPAQUE_TYPES = new Set([\"image/jpeg\", \"image/jpg\"]);\n\n/**\n * Compute the drawing geometry for a fit mode.\n *\n * @param source Source dimensions.\n * @param box Requested box; a missing side is derived from the aspect ratio.\n * @param fit How to fit the box.\n * @param withoutEnlargement Never scale up.\n * @returns Surface size and where the image lands inside it.\n */\nfunction layout(\n    source: { width: number; height: number },\n    box: { width?: number; height?: number },\n    fit: ResizeFit,\n    withoutEnlargement: boolean,\n): {\n    surface: { width: number; height: number };\n    draw: { x: number; y: number; width: number; height: number };\n} {\n    const ratio = source.width / source.height;\n    let targetWidth = box.width ?? (box.height !== undefined ? box.height * ratio : source.width);\n    let targetHeight = box.height ?? (box.width !== undefined ? box.width / ratio : source.height);\n\n    if (withoutEnlargement) {\n        const scale = Math.min(1, source.width / targetWidth, source.height / targetHeight);\n        targetWidth *= scale;\n        targetHeight *= scale;\n    }\n\n    targetWidth = Math.max(1, Math.round(targetWidth));\n    targetHeight = Math.max(1, Math.round(targetHeight));\n\n    if (fit === \"fill\") {\n        return {\n            surface: { width: targetWidth, height: targetHeight },\n            draw: { x: 0, y: 0, width: targetWidth, height: targetHeight },\n        };\n    }\n\n    const scale =\n        fit === \"cover\"\n            ? Math.max(targetWidth / source.width, targetHeight / source.height)\n            : Math.min(targetWidth / source.width, targetHeight / source.height);\n\n    const drawWidth = Math.max(1, Math.round(source.width * scale));\n    const drawHeight = Math.max(1, Math.round(source.height * scale));\n\n    if (fit === \"contain\") {\n        return {\n            surface: { width: drawWidth, height: drawHeight },\n            draw: { x: 0, y: 0, width: drawWidth, height: drawHeight },\n        };\n    }\n\n    return {\n        surface: { width: targetWidth, height: targetHeight },\n        draw: {\n            x: Math.round((targetWidth - drawWidth) / 2),\n            y: Math.round((targetHeight - drawHeight) / 2),\n            width: drawWidth,\n            height: drawHeight,\n        },\n    };\n}\n\n/**\n * Resolve the background to paint before drawing.\n *\n * @param options The caller's options.\n * @returns A colour, or `undefined` to keep transparency.\n */\nfunction backgroundFor(options: ResizeOptions): string | undefined {\n    if (options.background !== undefined) return options.background;\n    const type = options.type ?? \"image/jpeg\";\n    if (OPAQUE_TYPES.has(type)) return DEFAULT_BACKGROUND;\n    return options.fit === \"pad\" ? DEFAULT_BACKGROUND : undefined;\n}\n\n/**\n * Resize an image, re-encoding it.\n *\n * @example\n * ```ts\n * const resized = await resizeImage(file, { width: 1600, type: \"image/webp\" });\n * console.log(resized.width, resized.bytes, resized.type);\n * ```\n *\n * @param source Anything decodable.\n * @param options Target box, fit, format and quality.\n * @returns The encoded result.\n * @throws {@link ImageDecodeError} when the source cannot be decoded.\n * @throws {@link ImageEncodeError} when the canvas produces no bytes.\n */\nexport async function resizeImage(\n    source: ImageSource,\n    options: ResizeOptions = {},\n): Promise<ProcessedImage> {\n    const { bitmap } = await decodeImage(source);\n    try {\n        const geometry = layout(\n            bitmap,\n            { width: options.width, height: options.height },\n            options.fit ?? \"contain\",\n            options.withoutEnlargement !== false,\n        );\n        const surface = createSurface(geometry.surface.width, geometry.surface.height);\n        drawScaled(bitmap, surface, geometry.draw, backgroundFor(options));\n        return await encodeImage(surface, options);\n    } finally {\n        bitmap.close?.();\n    }\n}\n\n/**\n * Crop a rectangle out of an image, in source pixels.\n *\n * The rectangle is clamped to the image, so a crop dragged past the edge\n * produces a smaller result instead of transparent padding.\n *\n * @example\n * ```ts\n * const badge = await cropImage(file, { x: 120, y: 80, width: 400, height: 400 });\n * ```\n *\n * @param source Anything decodable.\n * @param rect The region to keep.\n * @param options Format and quality.\n * @returns The encoded crop.\n * @throws {@link ImageDecodeError} when the source cannot be decoded.\n */\nexport async function cropImage(\n    source: ImageSource,\n    rect: CropRect,\n    options: EncodeOptions = {},\n): Promise<ProcessedImage> {\n    const { bitmap } = await decodeImage(source);\n    try {\n        const x = Math.max(0, Math.min(Math.round(rect.x), bitmap.width - 1));\n        const y = Math.max(0, Math.min(Math.round(rect.y), bitmap.height - 1));\n        const width = Math.max(1, Math.min(Math.round(rect.width), bitmap.width - x));\n        const height = Math.max(1, Math.min(Math.round(rect.height), bitmap.height - y));\n\n        const surface = createSurface(width, height);\n        const context = getContext(\n            surface,\n            OPAQUE_TYPES.has(options.type ?? \"image/jpeg\") ? DEFAULT_BACKGROUND : undefined,\n        );\n        context.drawImage(bitmap, x, y, width, height, 0, 0, width, height);\n        return await encodeImage(surface, options);\n    } finally {\n        bitmap.close?.();\n    }\n}\n\n/**\n * Rotate an image by a multiple of 90 degrees.\n *\n * Restricted to right angles on purpose: an arbitrary angle needs a\n * decision about the corners (crop, pad, or grow the canvas) that belongs\n * to the caller's design, not to a utility default.\n *\n * @example\n * ```ts\n * const upright = await rotateImage(file, 90);\n * ```\n *\n * @param source Anything decodable.\n * @param degrees `90`, `180`, `270` — or any multiple, normalised.\n * @param options Format and quality.\n * @returns The rotated image.\n * @throws {@link ImageDecodeError} when the source cannot be decoded.\n * @throws {@link RangeError} when the angle is not a multiple of 90.\n */\nexport async function rotateImage(\n    source: ImageSource,\n    degrees: number,\n    options: EncodeOptions = {},\n): Promise<ProcessedImage> {\n    if (degrees % 90 !== 0) {\n        throw new RangeError(\n            `rotateImage takes multiples of 90 degrees; got ${degrees}. For an ` +\n                \"arbitrary angle, draw it yourself: the corner handling is a design \" +\n                \"decision, not a default.\",\n        );\n    }\n    const turns = (((degrees / 90) % 4) + 4) % 4;\n    const { bitmap } = await decodeImage(source);\n    try {\n        const swapped = turns % 2 === 1;\n        const width = swapped ? bitmap.height : bitmap.width;\n        const height = swapped ? bitmap.width : bitmap.height;\n\n        const surface = createSurface(width, height);\n        const context = getContext(\n            surface,\n            OPAQUE_TYPES.has(options.type ?? \"image/jpeg\") ? DEFAULT_BACKGROUND : undefined,\n        );\n        context.translate(width / 2, height / 2);\n        context.rotate((turns * Math.PI) / 2);\n        context.drawImage(bitmap, -bitmap.width / 2, -bitmap.height / 2);\n        return await encodeImage(surface, options);\n    } finally {\n        bitmap.close?.();\n    }\n}\n\n/**\n * Mirror an image horizontally, vertically, or both.\n *\n * @example\n * ```ts\n * const selfie = await flipImage(capture, { horizontal: true });\n * ```\n *\n * @param source Anything decodable.\n * @param axes Which axes to mirror.\n * @param options Format and quality.\n * @returns The flipped image.\n * @throws {@link ImageDecodeError} when the source cannot be decoded.\n */\nexport async function flipImage(\n    source: ImageSource,\n    axes: { horizontal?: boolean; vertical?: boolean },\n    options: EncodeOptions = {},\n): Promise<ProcessedImage> {\n    const { bitmap } = await decodeImage(source);\n    try {\n        const surface = createSurface(bitmap.width, bitmap.height);\n        const context = getContext(\n            surface,\n            OPAQUE_TYPES.has(options.type ?? \"image/jpeg\") ? DEFAULT_BACKGROUND : undefined,\n        );\n        context.translate(\n            axes.horizontal === true ? bitmap.width : 0,\n            axes.vertical === true ? bitmap.height : 0,\n        );\n        context.scale(axes.horizontal === true ? -1 : 1, axes.vertical === true ? -1 : 1);\n        context.drawImage(bitmap, 0, 0);\n        return await encodeImage(surface, options);\n    } finally {\n        bitmap.close?.();\n    }\n}\n"],"mappings":"oFAuBA,IAAa,EAAqB,UAG5B,EAAe,IAAI,IAAI,CAAC,aAAc,WAAW,CAAC,EAWxD,SAAS,EACL,EACA,EACA,EACA,EAIF,CACE,IAAM,EAAQ,EAAO,MAAQ,EAAO,OAChC,EAAc,EAAI,QAAU,EAAI,SAAW,IAAA,GAAiC,EAAO,MAA5B,EAAI,OAAS,GACpE,EAAe,EAAI,SAAW,EAAI,QAAU,IAAA,GAAgC,EAAO,OAA3B,EAAI,MAAQ,GAExE,GAAI,EAAoB,CACpB,IAAM,EAAQ,KAAK,IAAI,EAAG,EAAO,MAAQ,EAAa,EAAO,OAAS,CAAY,EAClF,GAAe,EACf,GAAgB,CACpB,CAKA,GAHA,EAAc,KAAK,IAAI,EAAG,KAAK,MAAM,CAAW,CAAC,EACjD,EAAe,KAAK,IAAI,EAAG,KAAK,MAAM,CAAY,CAAC,EAE/C,IAAQ,OACR,MAAO,CACH,QAAS,CAAE,MAAO,EAAa,OAAQ,CAAa,EACpD,KAAM,CAAE,EAAG,EAAG,EAAG,EAAG,MAAO,EAAa,OAAQ,CAAa,CACjE,EAGJ,IAAM,EACF,IAAQ,QACF,KAAK,IAAI,EAAc,EAAO,MAAO,EAAe,EAAO,MAAM,EACjE,KAAK,IAAI,EAAc,EAAO,MAAO,EAAe,EAAO,MAAM,EAErE,EAAY,KAAK,IAAI,EAAG,KAAK,MAAM,EAAO,MAAQ,CAAK,CAAC,EACxD,EAAa,KAAK,IAAI,EAAG,KAAK,MAAM,EAAO,OAAS,CAAK,CAAC,EAShE,OAPI,IAAQ,UACD,CACH,QAAS,CAAE,MAAO,EAAW,OAAQ,CAAW,EAChD,KAAM,CAAE,EAAG,EAAG,EAAG,EAAG,MAAO,EAAW,OAAQ,CAAW,CAC7D,EAGG,CACH,QAAS,CAAE,MAAO,EAAa,OAAQ,CAAa,EACpD,KAAM,CACF,EAAG,KAAK,OAAO,EAAc,GAAa,CAAC,EAC3C,EAAG,KAAK,OAAO,EAAe,GAAc,CAAC,EAC7C,MAAO,EACP,OAAQ,CACZ,CACJ,CACJ,CAQA,SAAS,EAAc,EAA4C,CAC/D,GAAI,EAAQ,aAAe,IAAA,GAAW,OAAO,EAAQ,WACrD,IAAM,EAAO,EAAQ,MAAQ,aAE7B,OADI,EAAa,IAAI,CAAI,GAClB,EAAQ,MAAQ,MADY,EACiB,IAAA,EACxD,CAiBA,eAAsB,EAClB,EACA,EAAyB,CAAC,EACH,CACvB,GAAM,CAAE,UAAW,MAAM,EAAA,YAAY,CAAM,EAC3C,GAAI,CACA,IAAM,EAAW,EACb,EACA,CAAE,MAAO,EAAQ,MAAO,OAAQ,EAAQ,MAAO,EAC/C,EAAQ,KAAO,UACf,EAAQ,qBAAuB,EACnC,EACM,EAAU,EAAA,cAAc,EAAS,QAAQ,MAAO,EAAS,QAAQ,MAAM,EAE7E,OADA,EAAA,WAAW,EAAQ,EAAS,EAAS,KAAM,EAAc,CAAO,CAAC,EAC1D,MAAM,EAAA,YAAY,EAAS,CAAO,CAC7C,QAAU,CACN,EAAO,QAAQ,CACnB,CACJ,CAmBA,eAAsB,EAClB,EACA,EACA,EAAyB,CAAC,EACH,CACvB,GAAM,CAAE,UAAW,MAAM,EAAA,YAAY,CAAM,EAC3C,GAAI,CACA,IAAM,EAAI,KAAK,IAAI,EAAG,KAAK,IAAI,KAAK,MAAM,EAAK,CAAC,EAAG,EAAO,MAAQ,CAAC,CAAC,EAC9D,EAAI,KAAK,IAAI,EAAG,KAAK,IAAI,KAAK,MAAM,EAAK,CAAC,EAAG,EAAO,OAAS,CAAC,CAAC,EAC/D,EAAQ,KAAK,IAAI,EAAG,KAAK,IAAI,KAAK,MAAM,EAAK,KAAK,EAAG,EAAO,MAAQ,CAAC,CAAC,EACtE,EAAS,KAAK,IAAI,EAAG,KAAK,IAAI,KAAK,MAAM,EAAK,MAAM,EAAG,EAAO,OAAS,CAAC,CAAC,EAEzE,EAAU,EAAA,cAAc,EAAO,CAAM,EAM3C,OADA,EAJgB,WACZ,EACA,EAAa,IAAI,EAAQ,MAAQ,YAAY,EAAI,EAAqB,IAAA,EAE1E,CAAA,CAAQ,UAAU,EAAQ,EAAG,EAAG,EAAO,EAAQ,EAAG,EAAG,EAAO,CAAM,EAC3D,MAAM,EAAA,YAAY,EAAS,CAAO,CAC7C,QAAU,CACN,EAAO,QAAQ,CACnB,CACJ,CAqBA,eAAsB,EAClB,EACA,EACA,EAAyB,CAAC,EACH,CACvB,GAAI,EAAU,IAAO,EACjB,MAAU,WACN,kDAAkD,EAAQ,qGAG9D,EAEJ,IAAM,GAAW,EAAU,GAAM,EAAK,GAAK,EACrC,CAAE,UAAW,MAAM,EAAA,YAAY,CAAM,EAC3C,GAAI,CACA,IAAM,EAAU,EAAQ,GAAM,EACxB,EAAQ,EAAU,EAAO,OAAS,EAAO,MACzC,EAAS,EAAU,EAAO,MAAQ,EAAO,OAEzC,EAAU,EAAA,cAAc,EAAO,CAAM,EACrC,EAAU,EAAA,WACZ,EACA,EAAa,IAAI,EAAQ,MAAQ,YAAY,EAAI,EAAqB,IAAA,EAC1E,EAIA,OAHA,EAAQ,UAAU,EAAQ,EAAG,EAAS,CAAC,EACvC,EAAQ,OAAQ,EAAQ,KAAK,GAAM,CAAC,EACpC,EAAQ,UAAU,EAAQ,CAAC,EAAO,MAAQ,EAAG,CAAC,EAAO,OAAS,CAAC,EACxD,MAAM,EAAA,YAAY,EAAS,CAAO,CAC7C,QAAU,CACN,EAAO,QAAQ,CACnB,CACJ,CAgBA,eAAsB,EAClB,EACA,EACA,EAAyB,CAAC,EACH,CACvB,GAAM,CAAE,UAAW,MAAM,EAAA,YAAY,CAAM,EAC3C,GAAI,CACA,IAAM,EAAU,EAAA,cAAc,EAAO,MAAO,EAAO,MAAM,EACnD,EAAU,EAAA,WACZ,EACA,EAAa,IAAI,EAAQ,MAAQ,YAAY,EAAI,EAAqB,IAAA,EAC1E,EAOA,OANA,EAAQ,UACJ,EAAK,aAAe,GAAO,EAAO,MAAQ,EAC1C,EAAK,WAAa,GAAO,EAAO,OAAS,CAC7C,EACA,EAAQ,MAAM,EAAK,aAAe,GAAO,GAAK,EAAG,EAAK,WAAa,GAAO,GAAK,CAAC,EAChF,EAAQ,UAAU,EAAQ,EAAG,CAAC,EACvB,MAAM,EAAA,YAAY,EAAS,CAAO,CAC7C,QAAU,CACN,EAAO,QAAQ,CACnB,CACJ"}