{"version":3,"file":"use-image-processing.cjs","names":[],"sources":["../../src/imaging/use-image-processing.ts"],"sourcesContent":["/**\n * React bindings for image work.\n *\n * Two things components get wrong every time: they leak object URLs (an\n * `URL.createObjectURL` without its `revokeObjectURL` holds the whole blob\n * in memory for the page's lifetime), and they set state after unmounting\n * when a slow resize finishes late. Both live here instead of in each\n * component.\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { compressToTarget, type CompressedImage } from \"./compress\";\nimport type { CompressOptions, ImageSource, ProcessedImage, ResizeOptions } from \"./types\";\nimport { resizeImage } from \"./transform\";\n\n/** What {@link useImagePreview} returns. */\nexport interface UseImagePreviewResult {\n    /** Object URL for the current source, or `null`. */\n    readonly url: string | null;\n}\n\n/**\n * Hold an object URL for a blob, and revoke it when it is replaced.\n *\n * @example\n * ```tsx\n * function Preview({ file }: { file: File | null }) {\n *     const { url } = useImagePreview(file);\n *     return url === null ? null : <img src={url} alt=\"\" />;\n * }\n * ```\n *\n * @param source The blob to preview, or `null`.\n * @returns The object URL, valid until the source changes or the component\n *   unmounts.\n */\nexport function useImagePreview(source: Blob | null | undefined): UseImagePreviewResult {\n    const [url, setUrl] = useState<string | null>(null);\n\n    useEffect(() => {\n        if (source === null || source === undefined) {\n            setUrl(null);\n            return;\n        }\n        const created = URL.createObjectURL(source);\n        setUrl(created);\n        return () => {\n            URL.revokeObjectURL(created);\n        };\n    }, [source]);\n\n    return { url };\n}\n\n/** Lifecycle of a processing call. */\nexport type ImageProcessingStatus = \"idle\" | \"working\" | \"done\" | \"error\";\n\n/** What {@link useImageProcessing} returns. */\nexport interface UseImageProcessingResult {\n    /** Resize (and re-encode) an image. */\n    readonly resize: (source: ImageSource, options?: ResizeOptions) => Promise<ProcessedImage>;\n    /** Compress an image into a byte budget. */\n    readonly compress: (source: ImageSource, options: CompressOptions) => Promise<CompressedImage>;\n    /** The most recent result. */\n    readonly result: ProcessedImage | null;\n    /** Where the last call is. */\n    readonly status: ImageProcessingStatus;\n    /** Why the last call failed. */\n    readonly error: Error | null;\n    /** Whether a call is in flight. */\n    readonly isWorking: boolean;\n}\n\n/**\n * Run image operations with status tracking, safe against unmount.\n *\n * @example\n * ```tsx\n * function Upload() {\n *     const { compress, isWorking, result } = useImageProcessing();\n *\n *     async function onPick(file: File) {\n *         const ready = await compress(file, { maxBytes: 1_000_000, width: 1600 });\n *         await fetch(\"/api/photos\", { method: \"POST\", body: ready.blob });\n *     }\n *\n *     return <input type=\"file\" disabled={isWorking} onChange={(e) => onPick(e.target.files![0]!)} />;\n * }\n * ```\n *\n * The returned promises still reject on failure, so a caller can `try`\n * around them; `status` and `error` exist for rendering, not for swallowing\n * the failure.\n *\n * @returns The operations plus their state.\n */\nexport function useImageProcessing(): UseImageProcessingResult {\n    const [result, setResult] = useState<ProcessedImage | null>(null);\n    const [status, setStatus] = useState<ImageProcessingStatus>(\"idle\");\n    const [error, setError] = useState<Error | null>(null);\n    const mounted = useRef(true);\n\n    useEffect(() => {\n        mounted.current = true;\n        return () => {\n            mounted.current = false;\n        };\n    }, []);\n\n    const run = useCallback(\n        async <T extends ProcessedImage>(work: () => Promise<T>): Promise<T> => {\n            if (mounted.current) {\n                setStatus(\"working\");\n                setError(null);\n            }\n            try {\n                const produced = await work();\n                if (mounted.current) {\n                    setResult(produced);\n                    setStatus(\"done\");\n                }\n                return produced;\n            } catch (caught) {\n                const failure = caught instanceof Error ? caught : new Error(String(caught));\n                if (mounted.current) {\n                    setError(failure);\n                    setStatus(\"error\");\n                }\n                throw failure;\n            }\n        },\n        [],\n    );\n\n    const resize = useCallback(\n        (source: ImageSource, options: ResizeOptions = {}) =>\n            run(() => resizeImage(source, options)),\n        [run],\n    );\n\n    const compress = useCallback(\n        (source: ImageSource, options: CompressOptions) =>\n            run(() => compressToTarget(source, options)),\n        [run],\n    );\n\n    return {\n        resize,\n        compress,\n        result,\n        status,\n        error,\n        isWorking: status === \"working\",\n    };\n}\n"],"mappings":"sFAqCA,SAAgB,EAAgB,EAAwD,CACpF,GAAM,CAAC,EAAK,IAAA,EAAU,EAAA,SAAA,CAAwB,IAAI,EAclD,OAZA,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,GAAW,KAA8B,CACzC,EAAO,IAAI,EACX,MACJ,CACA,IAAM,EAAU,IAAI,gBAAgB,CAAM,EAE1C,OADA,EAAO,CAAO,MACD,CACT,IAAI,gBAAgB,CAAO,CAC/B,CACJ,EAAG,CAAC,CAAM,CAAC,EAEJ,CAAE,KAAI,CACjB,CA4CA,SAAgB,GAA+C,CAC3D,GAAM,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAAgC,IAAI,EAC1D,CAAC,EAAQ,IAAA,EAAa,EAAA,SAAA,CAAgC,MAAM,EAC5D,CAAC,EAAO,IAAA,EAAY,EAAA,SAAA,CAAuB,IAAI,EAC/C,GAAA,EAAU,EAAA,OAAA,CAAO,EAAI,GAE3B,EAAA,EAAA,UAAA,MACI,EAAQ,QAAU,OACL,CACT,EAAQ,QAAU,EACtB,GACD,CAAC,CAAC,EAEL,IAAM,GAAA,EAAM,EAAA,YAAA,CACR,KAAiC,IAAuC,CAChE,EAAQ,UACR,EAAU,SAAS,EACnB,EAAS,IAAI,GAEjB,GAAI,CACA,IAAM,EAAW,MAAM,EAAK,EAK5B,OAJI,EAAQ,UACR,EAAU,CAAQ,EAClB,EAAU,MAAM,GAEb,CACX,OAAS,EAAQ,CACb,IAAM,EAAU,aAAkB,MAAQ,EAAa,MAAM,OAAO,CAAM,CAAC,EAK3E,MAJI,EAAQ,UACR,EAAS,CAAO,EAChB,EAAU,OAAO,GAEf,CACV,CACJ,EACA,CAAC,CACL,EAcA,MAAO,CACH,QAAA,EAbW,EAAA,YAAA,EACV,EAAqB,EAAyB,CAAC,IAC5C,MAAU,EAAA,YAAY,EAAQ,CAAO,CAAC,EAC1C,CAAC,CAAG,CAUJ,EACA,UAAA,EARa,EAAA,YAAA,EACZ,EAAqB,IAClB,MAAU,EAAA,iBAAiB,EAAQ,CAAO,CAAC,EAC/C,CAAC,CAAG,CAKJ,EACA,SACA,SACA,QACA,UAAW,IAAW,SAC1B,CACJ"}