{"version":3,"file":"encode.cjs","names":[],"sources":["../../src/imaging/encode.ts"],"sourcesContent":["/**\n * Getting bytes back out, and knowing which bytes you got.\n *\n * The trap here is silent: `canvas.toBlob(cb, \"image/avif\")` does not throw\n * on a browser without an AVIF encoder. It hands back a **PNG** with the\n * requested type ignored, and an app that trusted the request uploads\n * several megabytes where it planned for a few hundred kilobytes. Every\n * function here reports the type actually produced, and\n * :func:`supportsImageType` answers the question up front.\n */\n\nimport { getContext, type Surface } from \"./canvas\";\nimport { ImageEncodeError } from \"./exceptions\";\nimport type { EncodeOptions, ImageType, ProcessedImage } from \"./types\";\n\n/** Quality used when the caller does not choose. */\nexport const DEFAULT_QUALITY = 0.85;\n\n/** Format used when the caller does not choose. */\nexport const DEFAULT_TYPE: ImageType = \"image/jpeg\";\n\nconst supportCache = new Map<string, boolean>();\n\n/**\n * Encode a surface into image bytes.\n *\n * @param surface The canvas to encode.\n * @param options Format and quality.\n * @returns The blob plus the dimensions and the type actually produced.\n * @throws {@link ImageEncodeError} when the canvas produces nothing.\n */\nexport async function encodeImage(\n    surface: Surface,\n    options: EncodeOptions = {},\n): Promise<ProcessedImage> {\n    const type = options.type ?? DEFAULT_TYPE;\n    const quality = options.quality ?? DEFAULT_QUALITY;\n\n    const blob = await surfaceToBlob(surface, type, quality);\n    if (blob === null) {\n        throw new ImageEncodeError(\n            `The canvas produced no bytes for ${type}. The image may be too large ` +\n                \"for this browser's canvas limits.\",\n        );\n    }\n\n    return {\n        blob,\n        width: surface.width,\n        height: surface.height,\n        type: blob.type || type,\n        bytes: blob.size,\n    };\n}\n\n/**\n * Encode a surface, whichever kind it is.\n *\n * `OffscreenCanvas` and `HTMLCanvasElement` disagree on how to hand back\n * bytes — one returns a promise, the other takes a callback.\n *\n * @param surface The canvas.\n * @param type MIME type to request.\n * @param quality Quality for lossy formats.\n * @returns The blob, or `null` when the canvas produced nothing.\n */\nasync function surfaceToBlob(\n    surface: Surface,\n    type: string,\n    quality: number,\n): Promise<Blob | null> {\n    if (\"convertToBlob\" in surface) {\n        return await surface.convertToBlob({ type, quality });\n    }\n    return await new Promise<Blob | null>((resolve) => {\n        surface.toBlob((blob) => resolve(blob), type, quality);\n    });\n}\n\n/**\n * Whether this browser can actually encode a format.\n *\n * Asks for a 1x1 image in that type and checks what came back, because\n * that is the only answer that counts: a browser that \"supports\" WebP for\n * display may still not encode it.\n *\n * @example\n * ```ts\n * const type = (await supportsImageType(\"image/webp\")) ? \"image/webp\" : \"image/jpeg\";\n * const resized = await resizeImage(file, { width: 1200, type });\n * ```\n *\n * @param type The format to test.\n * @returns Whether encoding produces that type. Cached per type.\n */\nexport async function supportsImageType(type: ImageType): Promise<boolean> {\n    const cached = supportCache.get(type);\n    if (cached !== undefined) return cached;\n\n    let supported: boolean;\n    try {\n        const { createSurface } = await import(\"./canvas\");\n        const surface = createSurface(1, 1);\n        getContext(surface);\n        const blob = await surfaceToBlob(surface, type, 0.5);\n        supported = blob !== null && blob.type === type;\n    } catch {\n        supported = false;\n    }\n\n    supportCache.set(type, supported);\n    return supported;\n}\n\n/**\n * Pick the smallest format this browser can actually produce.\n *\n * @example\n * ```ts\n * const type = await bestSupportedType([\"image/avif\", \"image/webp\", \"image/jpeg\"]);\n * ```\n *\n * @param preferences Formats in preference order.\n * @returns The first supported one, falling back to `image/jpeg`, which\n *   every canvas implementation encodes.\n */\nexport async function bestSupportedType(\n    preferences: readonly ImageType[] = [\"image/webp\", \"image/jpeg\"],\n): Promise<ImageType> {\n    for (const type of preferences) {\n        if (await supportsImageType(type)) return type;\n    }\n    return \"image/jpeg\";\n}\n\n/**\n * Clear the format-support cache.\n *\n * Only useful in tests: browser support does not change at runtime.\n */\nexport function resetImageTypeSupportCache(): void {\n    supportCache.clear();\n}\n"],"mappings":"8DAgBA,IAAa,EAAkB,IAGlB,EAA0B,aAEjC,EAAe,IAAI,IAUzB,eAAsB,EAClB,EACA,EAAyB,CAAC,EACH,CACvB,IAAM,EAAO,EAAQ,MAAA,aAGf,EAAO,MAAM,EAAc,EAAS,EAF1B,EAAQ,SAAA,GAE+B,EACvD,GAAI,IAAS,KACT,MAAM,IAAI,EAAA,iBACN,oCAAoC,EAAK,+DAE7C,EAGJ,MAAO,CACH,OACA,MAAO,EAAQ,MACf,OAAQ,EAAQ,OAChB,KAAM,EAAK,MAAQ,EACnB,MAAO,EAAK,IAChB,CACJ,CAaA,eAAe,EACX,EACA,EACA,EACoB,CAIpB,MAHI,kBAAmB,EACZ,MAAM,EAAQ,cAAc,CAAE,OAAM,SAAQ,CAAC,EAEjD,MAAM,IAAI,QAAsB,GAAY,CAC/C,EAAQ,OAAQ,GAAS,EAAQ,CAAI,EAAG,EAAM,CAAO,CACzD,CAAC,CACL,CAkBA,eAAsB,EAAkB,EAAmC,CACvE,IAAM,EAAS,EAAa,IAAI,CAAI,EACpC,GAAI,IAAW,IAAA,GAAW,OAAO,EAEjC,IAAI,EACJ,GAAI,CACA,GAAM,CAAE,iBAAkB,MAAA,QAAA,QAAA,CAAA,CAAA,SAAA,QAAM,cAAA,CAAA,EAC1B,EAAU,EAAc,EAAG,CAAC,EAClC,EAAA,WAAW,CAAO,EAClB,IAAM,EAAO,MAAM,EAAc,EAAS,EAAM,EAAG,EACnD,EAAY,IAAS,MAAQ,EAAK,OAAS,CAC/C,MAAQ,CACJ,EAAY,EAChB,CAGA,OADA,EAAa,IAAI,EAAM,CAAS,EACzB,CACX,CAcA,eAAsB,EAClB,EAAoC,CAAC,aAAc,YAAY,EAC7C,CAClB,IAAK,IAAM,KAAQ,EACf,GAAI,MAAM,EAAkB,CAAI,EAAG,OAAO,EAE9C,MAAO,YACX"}