{"version":3,"file":"compress.cjs","names":[],"sources":["../../src/imaging/compress.ts"],"sourcesContent":["/**\n * Fitting an image into a byte budget.\n *\n * The upload endpoint takes 2 MB. The user's phone produces 8. Guessing a\n * quality that \"usually works\" fails on exactly the photos that matter —\n * a detailed scene compresses worse than a flat one, so a fixed 0.7 lands\n * anywhere between 400 KB and 4 MB depending on the picture.\n *\n * Binary search on quality lands on the budget in a bounded number of\n * encodes, and reports what it settled on so the caller can log it.\n */\n\nimport type { CompressOptions, ImageSource, ProcessedImage } from \"./types\";\nimport { resizeImage } from \"./transform\";\n\n/** Search steps when the caller does not choose. */\nexport const DEFAULT_COMPRESS_STEPS = 6;\n\n/** Lowest quality worth producing by default. */\nexport const DEFAULT_MIN_QUALITY = 0.4;\n\n/** Highest quality to start from by default. */\nexport const DEFAULT_MAX_QUALITY = 0.92;\n\n/** What {@link compressToTarget} produced. */\nexport interface CompressedImage extends ProcessedImage {\n    /** The quality it settled on. */\n    readonly quality: number;\n    /** How many encodes it took. */\n    readonly attempts: number;\n    /**\n     * Whether the result actually fits the budget.\n     *\n     * `false` means the image could not reach it even at `minQuality` —\n     * reported rather than thrown, because a 2.1 MB result against a 2 MB\n     * budget is usually still worth uploading, and that call is the\n     * caller's.\n     */\n    readonly withinBudget: boolean;\n}\n\n/**\n * Compress an image until it fits a byte budget.\n *\n * @example\n * ```ts\n * const upload = await compressToTarget(file, {\n *     maxBytes: 2 * 1024 * 1024,\n *     width: 2000,\n *     type: \"image/webp\",\n * });\n *\n * if (!upload.withinBudget) {\n *     console.warn(`still ${upload.bytes} bytes at quality ${upload.quality}`);\n * }\n * ```\n *\n * Resizing first is what usually does the work: halving the long edge\n * removes three quarters of the pixels, which no quality setting matches.\n * Pass `width`/`height` when the source is a full-resolution photo.\n *\n * @param source Anything decodable.\n * @param options Byte budget plus the usual resize and format options.\n * @returns The best result found, and whether it fits.\n * @throws {@link ImageDecodeError} when the source cannot be decoded.\n */\nexport async function compressToTarget(\n    source: ImageSource,\n    options: CompressOptions,\n): Promise<CompressedImage> {\n    const minQuality = options.minQuality ?? DEFAULT_MIN_QUALITY;\n    const maxQuality = options.maxQuality ?? DEFAULT_MAX_QUALITY;\n    const steps = Math.max(1, options.steps ?? DEFAULT_COMPRESS_STEPS);\n\n    let low = minQuality;\n    let high = maxQuality;\n    let attempts = 0;\n\n    let best = await resizeImage(source, { ...options, quality: maxQuality });\n    attempts += 1;\n    let bestQuality = maxQuality;\n\n    if (best.bytes <= options.maxBytes) {\n        return { ...best, quality: bestQuality, attempts, withinBudget: true };\n    }\n\n    let fitting: ProcessedImage | null = null;\n    let fittingQuality = minQuality;\n\n    for (let step = 0; step < steps; step += 1) {\n        const quality = (low + high) / 2;\n        const candidate = await resizeImage(source, { ...options, quality });\n        attempts += 1;\n\n        if (candidate.bytes <= options.maxBytes) {\n            fitting = candidate;\n            fittingQuality = quality;\n            low = quality;\n        } else {\n            high = quality;\n        }\n    }\n\n    if (fitting !== null) {\n        return { ...fitting, quality: fittingQuality, attempts, withinBudget: true };\n    }\n\n    best = await resizeImage(source, { ...options, quality: minQuality });\n    attempts += 1;\n    bestQuality = minQuality;\n    return {\n        ...best,\n        quality: bestQuality,\n        attempts,\n        withinBudget: best.bytes <= options.maxBytes,\n    };\n}\n"],"mappings":"mCAgBA,IAAa,EAAyB,EAGzB,EAAsB,GAGtB,EAAsB,IA4CnC,eAAsB,EAClB,EACA,EACwB,CACxB,IAAM,EAAa,EAAQ,YAAA,GACrB,EAAa,EAAQ,YAAA,IACrB,EAAQ,KAAK,IAAI,EAAG,EAAQ,OAAA,CAA+B,EAE7D,EAAM,EACN,EAAO,EACP,EAAW,EAEX,EAAO,MAAM,EAAA,YAAY,EAAQ,CAAE,GAAG,EAAS,QAAS,CAAW,CAAC,EACxE,GAAY,EACZ,IAAI,EAAc,EAElB,GAAI,EAAK,OAAS,EAAQ,SACtB,MAAO,CAAE,GAAG,EAAM,QAAS,EAAa,WAAU,aAAc,EAAK,EAGzE,IAAI,EAAiC,KACjC,EAAiB,EAErB,IAAK,IAAI,EAAO,EAAG,EAAO,EAAO,GAAQ,EAAG,CACxC,IAAM,GAAW,EAAM,GAAQ,EACzB,EAAY,MAAM,EAAA,YAAY,EAAQ,CAAE,GAAG,EAAS,SAAQ,CAAC,EACnE,GAAY,EAER,EAAU,OAAS,EAAQ,UAC3B,EAAU,EACV,EAAiB,EACjB,EAAM,GAEN,EAAO,CAEf,CASA,OAPI,IAAY,MAIhB,EAAO,MAAM,EAAA,YAAY,EAAQ,CAAE,GAAG,EAAS,QAAS,CAAW,CAAC,EACpE,GAAY,EACZ,EAAc,EACP,CACH,GAAG,EACH,QAAS,EACT,WACA,aAAc,EAAK,OAAS,EAAQ,QACxC,GAXW,CAAE,GAAG,EAAS,QAAS,EAAgB,WAAU,aAAc,EAAK,CAYnF"}