tempest-react-sdk 0.31.1 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/bin/lib/design/collect.mjs +103 -0
- package/bin/lib/design/findings.mjs +70 -0
- package/bin/lib/design/functions.mjs +188 -0
- package/bin/lib/design/functions.test.mjs +127 -0
- package/bin/lib/design/index.mjs +80 -0
- package/bin/lib/design/index.test.mjs +129 -0
- package/bin/lib/design/markers.mjs +76 -0
- package/bin/lib/design/markers.test.mjs +60 -0
- package/bin/lib/design/mask.mjs +209 -0
- package/bin/lib/design/mask.test.mjs +69 -0
- package/bin/lib/design/scan.mjs +229 -0
- package/bin/lib/design/scan.test.mjs +218 -0
- package/bin/lib/doctor/doctor.e2e.test.mjs +79 -2
- package/bin/tempest.mjs +83 -3
- package/dist/imaging/canvas.cjs +2 -0
- package/dist/imaging/canvas.cjs.map +1 -0
- package/dist/imaging/canvas.js +23 -0
- package/dist/imaging/canvas.js.map +1 -0
- package/dist/imaging/compress.cjs +2 -0
- package/dist/imaging/compress.cjs.map +1 -0
- package/dist/imaging/compress.js +43 -0
- package/dist/imaging/compress.js.map +1 -0
- package/dist/imaging/decode.cjs +2 -0
- package/dist/imaging/decode.cjs.map +1 -0
- package/dist/imaging/decode.js +42 -0
- package/dist/imaging/decode.js.map +1 -0
- package/dist/imaging/encode.cjs +2 -0
- package/dist/imaging/encode.cjs.map +1 -0
- package/dist/imaging/encode.js +48 -0
- package/dist/imaging/encode.js.map +1 -0
- package/dist/imaging/exceptions.cjs +2 -0
- package/dist/imaging/exceptions.cjs.map +1 -0
- package/dist/imaging/exceptions.js +26 -0
- package/dist/imaging/exceptions.js.map +1 -0
- package/dist/imaging/thumbnails.cjs +2 -0
- package/dist/imaging/thumbnails.cjs.map +1 -0
- package/dist/imaging/thumbnails.js +34 -0
- package/dist/imaging/thumbnails.js.map +1 -0
- package/dist/imaging/transform.cjs +2 -0
- package/dist/imaging/transform.cjs.map +1 -0
- package/dist/imaging/transform.js +97 -0
- package/dist/imaging/transform.js.map +1 -0
- package/dist/imaging/use-image-processing.cjs +2 -0
- package/dist/imaging/use-image-processing.cjs.map +1 -0
- package/dist/imaging/use-image-processing.js +45 -0
- package/dist/imaging/use-image-processing.js.map +1 -0
- package/dist/imaging.cjs +1 -0
- package/dist/imaging.d.ts +541 -0
- package/dist/imaging.js +9 -0
- package/dist/tabular/assets.cjs +2 -0
- package/dist/tabular/assets.cjs.map +1 -0
- package/dist/tabular/assets.js +19 -0
- package/dist/tabular/assets.js.map +1 -0
- package/dist/tabular/cache.cjs +2 -0
- package/dist/tabular/cache.cjs.map +1 -0
- package/dist/tabular/cache.js +48 -0
- package/dist/tabular/cache.js.map +1 -0
- package/dist/tabular/exceptions.cjs +2 -0
- package/dist/tabular/exceptions.cjs.map +1 -0
- package/dist/tabular/exceptions.js +30 -0
- package/dist/tabular/exceptions.js.map +1 -0
- package/dist/tabular/manifest.cjs +2 -0
- package/dist/tabular/manifest.cjs.map +1 -0
- package/dist/tabular/manifest.js +42 -0
- package/dist/tabular/manifest.js.map +1 -0
- package/dist/tabular/predictor.cjs +2 -0
- package/dist/tabular/predictor.cjs.map +1 -0
- package/dist/tabular/predictor.js +111 -0
- package/dist/tabular/predictor.js.map +1 -0
- package/dist/tabular/use-tabular-predictor.cjs +2 -0
- package/dist/tabular/use-tabular-predictor.cjs.map +1 -0
- package/dist/tabular/use-tabular-predictor.js +51 -0
- package/dist/tabular/use-tabular-predictor.js.map +1 -0
- package/dist/tabular.cjs +1 -0
- package/dist/tabular.d.ts +489 -0
- package/dist/tabular.js +7 -0
- package/package.json +11 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ImagingUnavailableError as e } from "./exceptions.js";
|
|
2
|
+
//#region src/imaging/canvas.ts
|
|
3
|
+
function t(t, n) {
|
|
4
|
+
let r = Math.max(1, Math.round(t)), i = Math.max(1, Math.round(n));
|
|
5
|
+
if (typeof OffscreenCanvas < "u") return new OffscreenCanvas(r, i);
|
|
6
|
+
if (typeof document < "u") {
|
|
7
|
+
let e = document.createElement("canvas");
|
|
8
|
+
return e.width = r, e.height = i, e;
|
|
9
|
+
}
|
|
10
|
+
throw new e("No canvas is available here. This module needs a browser (or a worker with OffscreenCanvas); it does not run under plain Node.");
|
|
11
|
+
}
|
|
12
|
+
function n(t, n) {
|
|
13
|
+
let r = t.getContext("2d");
|
|
14
|
+
if (r === null) throw new e("Could not get a 2-D context from the canvas.");
|
|
15
|
+
return r.imageSmoothingEnabled = !0, r.imageSmoothingQuality = "high", n !== void 0 && (r.fillStyle = n, r.fillRect(0, 0, t.width, t.height)), r;
|
|
16
|
+
}
|
|
17
|
+
function r(e, t, r, i) {
|
|
18
|
+
n(t, i).drawImage(e, r.x, r.y, r.width, r.height);
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { t as createSurface, r as drawScaled, n as getContext };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=canvas.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvas.js","names":[],"sources":["../../src/imaging/canvas.ts"],"sourcesContent":["/**\n * The drawing surface, and the one thing everyone gets wrong on it.\n *\n * **JPEG has no alpha.** Encoding a transparent PNG as JPEG paints the\n * transparent pixels black. Filling the surface first is the difference\n * between a photo on a white background and one with a black hole in it.\n *\n * What is *not* here is worth recording. The received wisdom for downscaling\n * on a canvas is to halve repeatedly, because a single `drawImage` into a\n * much smaller box was said to alias. That was implemented here, and then\n * measured: on a 512 px checkerboard reduced to 32 px, the stepwise result\n * and the single high-quality draw were **pixel-identical** (standard\n * deviation 0.0 on both) in Chromium and Firefox — while stepwise cost\n * **39.19 ms against 0.13 ms** on a 4000x3000 photo, 300 times more, and\n * allocated three intermediate canvases on a device that may not have the\n * memory. Modern engines honour `imageSmoothingQuality = \"high\"`, which is\n * what this module sets. The halving was deleted rather than kept \"just in\n * case\": unmeasurable benefit at 300x the cost is not insurance, it is\n * ballast.\n */\n\nimport { ImagingUnavailableError } from \"./exceptions\";\n\n/** A canvas this module can draw on, on the main thread or in a worker. */\nexport type Surface = OffscreenCanvas | HTMLCanvasElement;\n\n/** A 2-D context from either surface kind. */\nexport type SurfaceContext = OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;\n\n/**\n * Create a drawing surface, preferring `OffscreenCanvas`.\n *\n * `OffscreenCanvas` works inside a worker, which is where a PWA wants this\n * running: resizing a 12-megapixel photo on the main thread blocks the UI\n * for tens of milliseconds per image.\n *\n * @param width Surface width in pixels.\n * @param height Surface height in pixels.\n * @returns The surface.\n * @throws {@link ImagingUnavailableError} when neither kind exists — a\n * server render, or a test environment without a canvas.\n */\nexport function createSurface(width: number, height: number): Surface {\n const safeWidth = Math.max(1, Math.round(width));\n const safeHeight = Math.max(1, Math.round(height));\n\n if (typeof OffscreenCanvas !== \"undefined\") {\n return new OffscreenCanvas(safeWidth, safeHeight);\n }\n if (typeof document !== \"undefined\") {\n const canvas = document.createElement(\"canvas\");\n canvas.width = safeWidth;\n canvas.height = safeHeight;\n return canvas;\n }\n throw new ImagingUnavailableError(\n \"No canvas is available here. This module needs a browser (or a worker \" +\n \"with OffscreenCanvas); it does not run under plain Node.\",\n );\n}\n\n/**\n * Get a 2-D context configured for image work.\n *\n * `imageSmoothingQuality = \"high\"` is the setting that makes a steep\n * downscale average its source pixels instead of sampling them sparsely.\n *\n * @param surface The surface to draw on.\n * @param background Optional fill painted before anything else.\n * @returns The context.\n * @throws {@link ImagingUnavailableError} when the context cannot be created.\n */\nexport function getContext(surface: Surface, background?: string): SurfaceContext {\n const context = surface.getContext(\"2d\") as SurfaceContext | null;\n if (context === null) {\n throw new ImagingUnavailableError(\"Could not get a 2-D context from the canvas.\");\n }\n context.imageSmoothingEnabled = true;\n context.imageSmoothingQuality = \"high\";\n if (background !== undefined) {\n context.fillStyle = background;\n context.fillRect(0, 0, surface.width, surface.height);\n }\n return context;\n}\n\n/**\n * Draw a bitmap into a surface with high-quality filtering.\n *\n * @param bitmap The source pixels.\n * @param target Destination surface.\n * @param box Where to draw inside the destination.\n * @param background Fill painted before drawing.\n */\nexport function drawScaled(\n bitmap: ImageBitmap,\n target: Surface,\n box: { x: number; y: number; width: number; height: number },\n background?: string,\n): void {\n const context = getContext(target, background);\n context.drawImage(bitmap, box.x, box.y, box.width, box.height);\n}\n"],"mappings":";;AA0CA,SAAgB,EAAc,GAAe,GAAyB;CAClE,IAAM,IAAY,KAAK,IAAI,GAAG,KAAK,MAAM,CAAK,CAAC,GACzC,IAAa,KAAK,IAAI,GAAG,KAAK,MAAM,CAAM,CAAC;CAEjD,IAAI,OAAO,kBAAoB,KAC3B,OAAO,IAAI,gBAAgB,GAAW,CAAU;CAEpD,IAAI,OAAO,WAAa,KAAa;EACjC,IAAM,IAAS,SAAS,cAAc,QAAQ;EAG9C,OAFA,EAAO,QAAQ,GACf,EAAO,SAAS,GACT;CACX;CACA,MAAM,IAAI,EACN,gIAEJ;AACJ;AAaA,SAAgB,EAAW,GAAkB,GAAqC;CAC9E,IAAM,IAAU,EAAQ,WAAW,IAAI;CACvC,IAAI,MAAY,MACZ,MAAM,IAAI,EAAwB,8CAA8C;CAQpF,OANA,EAAQ,wBAAwB,IAChC,EAAQ,wBAAwB,QAC5B,MAAe,KAAA,MACf,EAAQ,YAAY,GACpB,EAAQ,SAAS,GAAG,GAAG,EAAQ,OAAO,EAAQ,MAAM,IAEjD;AACX;AAUA,SAAgB,EACZ,GACA,GACA,GACA,GACI;CAEJ,EAD2B,GAAQ,CACnC,CAAA,CAAQ,UAAU,GAAQ,EAAI,GAAG,EAAI,GAAG,EAAI,OAAO,EAAI,MAAM;AACjE"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./transform.cjs");var t=6,n=.4,r=.92;async function i(t,n){let r=n.minQuality??.4,i=n.maxQuality??.92,a=Math.max(1,n.steps??6),o=r,s=i,c=0,l=await e.resizeImage(t,{...n,quality:i});c+=1;let u=i;if(l.bytes<=n.maxBytes)return{...l,quality:u,attempts:c,withinBudget:!0};let d=null,f=r;for(let r=0;r<a;r+=1){let r=(o+s)/2,i=await e.resizeImage(t,{...n,quality:r});c+=1,i.bytes<=n.maxBytes?(d=i,f=r,o=r):s=r}return d===null?(l=await e.resizeImage(t,{...n,quality:r}),c+=1,u=r,{...l,quality:u,attempts:c,withinBudget:l.bytes<=n.maxBytes}):{...d,quality:f,attempts:c,withinBudget:!0}}exports.DEFAULT_COMPRESS_STEPS=t,exports.DEFAULT_MAX_QUALITY=r,exports.DEFAULT_MIN_QUALITY=n,exports.compressToTarget=i;
|
|
2
|
+
//# sourceMappingURL=compress.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { resizeImage as e } from "./transform.js";
|
|
2
|
+
//#region src/imaging/compress.ts
|
|
3
|
+
var t = 6, n = .4, r = .92;
|
|
4
|
+
async function i(t, n) {
|
|
5
|
+
let r = n.minQuality ?? .4, i = n.maxQuality ?? .92, a = Math.max(1, n.steps ?? 6), o = r, s = i, c = 0, l = await e(t, {
|
|
6
|
+
...n,
|
|
7
|
+
quality: i
|
|
8
|
+
});
|
|
9
|
+
c += 1;
|
|
10
|
+
let u = i;
|
|
11
|
+
if (l.bytes <= n.maxBytes) return {
|
|
12
|
+
...l,
|
|
13
|
+
quality: u,
|
|
14
|
+
attempts: c,
|
|
15
|
+
withinBudget: !0
|
|
16
|
+
};
|
|
17
|
+
let d = null, f = r;
|
|
18
|
+
for (let r = 0; r < a; r += 1) {
|
|
19
|
+
let r = (o + s) / 2, i = await e(t, {
|
|
20
|
+
...n,
|
|
21
|
+
quality: r
|
|
22
|
+
});
|
|
23
|
+
c += 1, i.bytes <= n.maxBytes ? (d = i, f = r, o = r) : s = r;
|
|
24
|
+
}
|
|
25
|
+
return d === null ? (l = await e(t, {
|
|
26
|
+
...n,
|
|
27
|
+
quality: r
|
|
28
|
+
}), c += 1, u = r, {
|
|
29
|
+
...l,
|
|
30
|
+
quality: u,
|
|
31
|
+
attempts: c,
|
|
32
|
+
withinBudget: l.bytes <= n.maxBytes
|
|
33
|
+
}) : {
|
|
34
|
+
...d,
|
|
35
|
+
quality: f,
|
|
36
|
+
attempts: c,
|
|
37
|
+
withinBudget: !0
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { t as DEFAULT_COMPRESS_STEPS, r as DEFAULT_MAX_QUALITY, n as DEFAULT_MIN_QUALITY, i as compressToTarget };
|
|
42
|
+
|
|
43
|
+
//# sourceMappingURL=compress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compress.js","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":";;AAgBA,IAAa,IAAyB,GAGzB,IAAsB,IAGtB,IAAsB;AA4CnC,eAAsB,EAClB,GACA,GACwB;CACxB,IAAM,IAAa,EAAQ,cAAA,IACrB,IAAa,EAAQ,cAAA,KACrB,IAAQ,KAAK,IAAI,GAAG,EAAQ,SAAA,CAA+B,GAE7D,IAAM,GACN,IAAO,GACP,IAAW,GAEX,IAAO,MAAM,EAAY,GAAQ;EAAE,GAAG;EAAS,SAAS;CAAW,CAAC;CACxE,KAAY;CACZ,IAAI,IAAc;CAElB,IAAI,EAAK,SAAS,EAAQ,UACtB,OAAO;EAAE,GAAG;EAAM,SAAS;EAAa;EAAU,cAAc;CAAK;CAGzE,IAAI,IAAiC,MACjC,IAAiB;CAErB,KAAK,IAAI,IAAO,GAAG,IAAO,GAAO,KAAQ,GAAG;EACxC,IAAM,KAAW,IAAM,KAAQ,GACzB,IAAY,MAAM,EAAY,GAAQ;GAAE,GAAG;GAAS;EAAQ,CAAC;EAGnE,AAFA,KAAY,GAER,EAAU,SAAS,EAAQ,YAC3B,IAAU,GACV,IAAiB,GACjB,IAAM,KAEN,IAAO;CAEf;CASA,OAPI,MAAY,QAIhB,IAAO,MAAM,EAAY,GAAQ;EAAE,GAAG;EAAS,SAAS;CAAW,CAAC,GACpE,KAAY,GACZ,IAAc,GACP;EACH,GAAG;EACH,SAAS;EACT;EACA,cAAc,EAAK,SAAS,EAAQ;CACxC,KAXW;EAAE,GAAG;EAAS,SAAS;EAAgB;EAAU,cAAc;CAAK;AAYnF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./exceptions.cjs");async function t(e){let t=await n(e);return{bitmap:t,width:t.width,height:t.height}}async function n(t){if(typeof ImageBitmap<`u`&&t instanceof ImageBitmap)return t;if(typeof t==`string`){let r;try{r=await fetch(t)}catch(n){throw new e.ImageDecodeError(`Could not fetch the image at ${t}`,{cause:n})}if(!r.ok)throw new e.ImageDecodeError(`Could not fetch the image at ${t}: ${r.status} ${r.statusText}`);return await n(await r.blob())}try{return await createImageBitmap(t,{imageOrientation:`from-image`})}catch(t){throw new e.ImageDecodeError(`Could not decode the image: ${t instanceof Error?t.message:String(t)}`,{cause:t})}}async function r(e){let{bitmap:n}=await t(e),r={width:n.width,height:n.height,type:e.type,bytes:e.size,aspectRatio:n.height===0?0:n.width/n.height};return n.close?.(),r}exports.decodeImage=t,exports.readImageInfo=r;
|
|
2
|
+
//# sourceMappingURL=decode.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decode.cjs","names":[],"sources":["../../src/imaging/decode.ts"],"sourcesContent":["/**\n * Turning whatever the app has into pixels.\n *\n * A PWA receives images from a file input, a camera capture, a paste, a\n * fetch, or a canvas it drew itself. They all become an `ImageBitmap` here,\n * with the one correction that matters on phones applied: **EXIF\n * orientation**.\n *\n * A photo taken in portrait is usually stored landscape plus an orientation\n * tag. Decode it without honouring the tag and every user sees their photo\n * sideways — the single most reported bug in upload flows. `createImageBitmap`\n * accepts `imageOrientation: \"from-image\"`, and that is what this module\n * asks for.\n */\n\nimport { ImageDecodeError } from \"./exceptions\";\nimport type { DecodedImage, ImageInfo, ImageSource } from \"./types\";\n\n/**\n * Decode any supported source into a bitmap, oriented as the photographer\n * held the camera.\n *\n * @example\n * ```ts\n * const { bitmap, width, height } = await decodeImage(file);\n * ```\n *\n * @param source A `Blob`/`File`, URL string, `ImageBitmap`, `ImageData`,\n * `HTMLImageElement`, or a canvas.\n * @returns The decoded pixels and their dimensions.\n * @throws {@link ImageDecodeError} when the bytes are not a decodable image,\n * or the URL cannot be fetched.\n */\nexport async function decodeImage(source: ImageSource): Promise<DecodedImage> {\n const bitmap = await toBitmap(source);\n return { bitmap, width: bitmap.width, height: bitmap.height };\n}\n\n/**\n * Decode a source into an `ImageBitmap`.\n *\n * @param source The image source.\n * @returns The bitmap.\n * @throws {@link ImageDecodeError} when decoding fails.\n */\nasync function toBitmap(source: ImageSource): Promise<ImageBitmap> {\n if (typeof ImageBitmap !== \"undefined\" && source instanceof ImageBitmap) {\n return source;\n }\n\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source);\n } catch (error) {\n throw new ImageDecodeError(`Could not fetch the image at ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ImageDecodeError(\n `Could not fetch the image at ${source}: ${response.status} ${response.statusText}`,\n );\n }\n return await toBitmap(await response.blob());\n }\n\n try {\n return await createImageBitmap(source as ImageBitmapSource, {\n imageOrientation: \"from-image\",\n });\n } catch (error) {\n throw new ImageDecodeError(\n `Could not decode the image: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n}\n\n/**\n * Read an image's dimensions, type and size.\n *\n * Decodes to measure, which is the only reliable way in a browser — there\n * is no header parser here, and guessing dimensions from a MIME type is not\n * a thing. Cheap enough for a preview, not for a thousand files in a loop.\n *\n * @example\n * ```ts\n * const info = await readImageInfo(file);\n * if (info.bytes > 5_000_000) {\n * // ask before uploading\n * }\n * ```\n *\n * @param blob The image file.\n * @returns Its dimensions, MIME type, byte size and aspect ratio.\n * @throws {@link ImageDecodeError} when the blob is not a decodable image.\n */\nexport async function readImageInfo(blob: Blob): Promise<ImageInfo> {\n const { bitmap } = await decodeImage(blob);\n const info: ImageInfo = {\n width: bitmap.width,\n height: bitmap.height,\n type: blob.type,\n bytes: blob.size,\n aspectRatio: bitmap.height === 0 ? 0 : bitmap.width / bitmap.height,\n };\n bitmap.close?.();\n return info;\n}\n"],"mappings":"oCAiCA,eAAsB,EAAY,EAA4C,CAC1E,IAAM,EAAS,MAAM,EAAS,CAAM,EACpC,MAAO,CAAE,SAAQ,MAAO,EAAO,MAAO,OAAQ,EAAO,MAAO,CAChE,CASA,eAAe,EAAS,EAA2C,CAC/D,GAAI,OAAO,YAAgB,KAAe,aAAkB,YACxD,OAAO,EAGX,GAAI,OAAO,GAAW,SAAU,CAC5B,IAAI,EACJ,GAAI,CACA,EAAW,MAAM,MAAM,CAAM,CACjC,OAAS,EAAO,CACZ,MAAM,IAAI,EAAA,iBAAiB,gCAAgC,IAAU,CACjE,MAAO,CACX,CAAC,CACL,CACA,GAAI,CAAC,EAAS,GACV,MAAM,IAAI,EAAA,iBACN,gCAAgC,EAAO,IAAI,EAAS,OAAO,GAAG,EAAS,YAC3E,EAEJ,OAAO,MAAM,EAAS,MAAM,EAAS,KAAK,CAAC,CAC/C,CAEA,GAAI,CACA,OAAO,MAAM,kBAAkB,EAA6B,CACxD,iBAAkB,YACtB,CAAC,CACL,OAAS,EAAO,CACZ,MAAM,IAAI,EAAA,iBACN,+BAA+B,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,IACpF,CAAE,MAAO,CAAM,CACnB,CACJ,CACJ,CAqBA,eAAsB,EAAc,EAAgC,CAChE,GAAM,CAAE,UAAW,MAAM,EAAY,CAAI,EACnC,EAAkB,CACpB,MAAO,EAAO,MACd,OAAQ,EAAO,OACf,KAAM,EAAK,KACX,MAAO,EAAK,KACZ,YAAa,EAAO,SAAW,EAAI,EAAI,EAAO,MAAQ,EAAO,MACjE,EAEA,OADA,EAAO,QAAQ,EACR,CACX"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ImageDecodeError as e } from "./exceptions.js";
|
|
2
|
+
//#region src/imaging/decode.ts
|
|
3
|
+
async function t(e) {
|
|
4
|
+
let t = await n(e);
|
|
5
|
+
return {
|
|
6
|
+
bitmap: t,
|
|
7
|
+
width: t.width,
|
|
8
|
+
height: t.height
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
async function n(t) {
|
|
12
|
+
if (typeof ImageBitmap < "u" && t instanceof ImageBitmap) return t;
|
|
13
|
+
if (typeof t == "string") {
|
|
14
|
+
let r;
|
|
15
|
+
try {
|
|
16
|
+
r = await fetch(t);
|
|
17
|
+
} catch (n) {
|
|
18
|
+
throw new e(`Could not fetch the image at ${t}`, { cause: n });
|
|
19
|
+
}
|
|
20
|
+
if (!r.ok) throw new e(`Could not fetch the image at ${t}: ${r.status} ${r.statusText}`);
|
|
21
|
+
return await n(await r.blob());
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return await createImageBitmap(t, { imageOrientation: "from-image" });
|
|
25
|
+
} catch (t) {
|
|
26
|
+
throw new e(`Could not decode the image: ${t instanceof Error ? t.message : String(t)}`, { cause: t });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function r(e) {
|
|
30
|
+
let { bitmap: n } = await t(e), r = {
|
|
31
|
+
width: n.width,
|
|
32
|
+
height: n.height,
|
|
33
|
+
type: e.type,
|
|
34
|
+
bytes: e.size,
|
|
35
|
+
aspectRatio: n.height === 0 ? 0 : n.width / n.height
|
|
36
|
+
};
|
|
37
|
+
return n.close?.(), r;
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { t as decodeImage, r as readImageInfo };
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=decode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decode.js","names":[],"sources":["../../src/imaging/decode.ts"],"sourcesContent":["/**\n * Turning whatever the app has into pixels.\n *\n * A PWA receives images from a file input, a camera capture, a paste, a\n * fetch, or a canvas it drew itself. They all become an `ImageBitmap` here,\n * with the one correction that matters on phones applied: **EXIF\n * orientation**.\n *\n * A photo taken in portrait is usually stored landscape plus an orientation\n * tag. Decode it without honouring the tag and every user sees their photo\n * sideways — the single most reported bug in upload flows. `createImageBitmap`\n * accepts `imageOrientation: \"from-image\"`, and that is what this module\n * asks for.\n */\n\nimport { ImageDecodeError } from \"./exceptions\";\nimport type { DecodedImage, ImageInfo, ImageSource } from \"./types\";\n\n/**\n * Decode any supported source into a bitmap, oriented as the photographer\n * held the camera.\n *\n * @example\n * ```ts\n * const { bitmap, width, height } = await decodeImage(file);\n * ```\n *\n * @param source A `Blob`/`File`, URL string, `ImageBitmap`, `ImageData`,\n * `HTMLImageElement`, or a canvas.\n * @returns The decoded pixels and their dimensions.\n * @throws {@link ImageDecodeError} when the bytes are not a decodable image,\n * or the URL cannot be fetched.\n */\nexport async function decodeImage(source: ImageSource): Promise<DecodedImage> {\n const bitmap = await toBitmap(source);\n return { bitmap, width: bitmap.width, height: bitmap.height };\n}\n\n/**\n * Decode a source into an `ImageBitmap`.\n *\n * @param source The image source.\n * @returns The bitmap.\n * @throws {@link ImageDecodeError} when decoding fails.\n */\nasync function toBitmap(source: ImageSource): Promise<ImageBitmap> {\n if (typeof ImageBitmap !== \"undefined\" && source instanceof ImageBitmap) {\n return source;\n }\n\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source);\n } catch (error) {\n throw new ImageDecodeError(`Could not fetch the image at ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ImageDecodeError(\n `Could not fetch the image at ${source}: ${response.status} ${response.statusText}`,\n );\n }\n return await toBitmap(await response.blob());\n }\n\n try {\n return await createImageBitmap(source as ImageBitmapSource, {\n imageOrientation: \"from-image\",\n });\n } catch (error) {\n throw new ImageDecodeError(\n `Could not decode the image: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error },\n );\n }\n}\n\n/**\n * Read an image's dimensions, type and size.\n *\n * Decodes to measure, which is the only reliable way in a browser — there\n * is no header parser here, and guessing dimensions from a MIME type is not\n * a thing. Cheap enough for a preview, not for a thousand files in a loop.\n *\n * @example\n * ```ts\n * const info = await readImageInfo(file);\n * if (info.bytes > 5_000_000) {\n * // ask before uploading\n * }\n * ```\n *\n * @param blob The image file.\n * @returns Its dimensions, MIME type, byte size and aspect ratio.\n * @throws {@link ImageDecodeError} when the blob is not a decodable image.\n */\nexport async function readImageInfo(blob: Blob): Promise<ImageInfo> {\n const { bitmap } = await decodeImage(blob);\n const info: ImageInfo = {\n width: bitmap.width,\n height: bitmap.height,\n type: blob.type,\n bytes: blob.size,\n aspectRatio: bitmap.height === 0 ? 0 : bitmap.width / bitmap.height,\n };\n bitmap.close?.();\n return info;\n}\n"],"mappings":";;AAiCA,eAAsB,EAAY,GAA4C;CAC1E,IAAM,IAAS,MAAM,EAAS,CAAM;CACpC,OAAO;EAAE;EAAQ,OAAO,EAAO;EAAO,QAAQ,EAAO;CAAO;AAChE;AASA,eAAe,EAAS,GAA2C;CAC/D,IAAI,OAAO,cAAgB,OAAe,aAAkB,aACxD,OAAO;CAGX,IAAI,OAAO,KAAW,UAAU;EAC5B,IAAI;EACJ,IAAI;GACA,IAAW,MAAM,MAAM,CAAM;EACjC,SAAS,GAAO;GACZ,MAAM,IAAI,EAAiB,gCAAgC,KAAU,EACjE,OAAO,EACX,CAAC;EACL;EACA,IAAI,CAAC,EAAS,IACV,MAAM,IAAI,EACN,gCAAgC,EAAO,IAAI,EAAS,OAAO,GAAG,EAAS,YAC3E;EAEJ,OAAO,MAAM,EAAS,MAAM,EAAS,KAAK,CAAC;CAC/C;CAEA,IAAI;EACA,OAAO,MAAM,kBAAkB,GAA6B,EACxD,kBAAkB,aACtB,CAAC;CACL,SAAS,GAAO;EACZ,MAAM,IAAI,EACN,+BAA+B,aAAiB,QAAQ,EAAM,UAAU,OAAO,CAAK,KACpF,EAAE,OAAO,EAAM,CACnB;CACJ;AACJ;AAqBA,eAAsB,EAAc,GAAgC;CAChE,IAAM,EAAE,cAAW,MAAM,EAAY,CAAI,GACnC,IAAkB;EACpB,OAAO,EAAO;EACd,QAAQ,EAAO;EACf,MAAM,EAAK;EACX,OAAO,EAAK;EACZ,aAAa,EAAO,WAAW,IAAI,IAAI,EAAO,QAAQ,EAAO;CACjE;CAEA,OADA,EAAO,QAAQ,GACR;AACX"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./exceptions.cjs"),t=require("./canvas.cjs");var n=.85,r=`image/jpeg`,i=new Map;async function a(t,n={}){let r=n.type??`image/jpeg`,i=await o(t,r,n.quality??.85);if(i===null)throw new e.ImageEncodeError(`The canvas produced no bytes for ${r}. The image may be too large for this browser's canvas limits.`);return{blob:i,width:t.width,height:t.height,type:i.type||r,bytes:i.size}}async function o(e,t,n){return`convertToBlob`in e?await e.convertToBlob({type:t,quality:n}):await new Promise(r=>{e.toBlob(e=>r(e),t,n)})}async function s(e){let n=i.get(e);if(n!==void 0)return n;let r;try{let{createSurface:n}=await Promise.resolve().then(()=>require("./canvas.cjs")),i=n(1,1);t.getContext(i);let a=await o(i,e,.5);r=a!==null&&a.type===e}catch{r=!1}return i.set(e,r),r}async function c(e=[`image/webp`,`image/jpeg`]){for(let t of e)if(await s(t))return t;return`image/jpeg`}function l(){i.clear()}exports.DEFAULT_QUALITY=n,exports.DEFAULT_TYPE=r,exports.bestSupportedType=c,exports.encodeImage=a,exports.resetImageTypeSupportCache=l,exports.supportsImageType=s;
|
|
2
|
+
//# sourceMappingURL=encode.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"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,CAOA,SAAgB,GAAmC,CAC/C,EAAa,MAAM,CACvB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ImageEncodeError as e } from "./exceptions.js";
|
|
2
|
+
import { getContext as t } from "./canvas.js";
|
|
3
|
+
//#region src/imaging/encode.ts
|
|
4
|
+
var n = .85, r = "image/jpeg", i = /* @__PURE__ */ new Map();
|
|
5
|
+
async function a(t, n = {}) {
|
|
6
|
+
let r = n.type ?? "image/jpeg", i = await o(t, r, n.quality ?? .85);
|
|
7
|
+
if (i === null) throw new e(`The canvas produced no bytes for ${r}. The image may be too large for this browser's canvas limits.`);
|
|
8
|
+
return {
|
|
9
|
+
blob: i,
|
|
10
|
+
width: t.width,
|
|
11
|
+
height: t.height,
|
|
12
|
+
type: i.type || r,
|
|
13
|
+
bytes: i.size
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
async function o(e, t, n) {
|
|
17
|
+
return "convertToBlob" in e ? await e.convertToBlob({
|
|
18
|
+
type: t,
|
|
19
|
+
quality: n
|
|
20
|
+
}) : await new Promise((r) => {
|
|
21
|
+
e.toBlob((e) => r(e), t, n);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
async function s(e) {
|
|
25
|
+
let n = i.get(e);
|
|
26
|
+
if (n !== void 0) return n;
|
|
27
|
+
let r;
|
|
28
|
+
try {
|
|
29
|
+
let { createSurface: n } = await import("./canvas.js"), i = n(1, 1);
|
|
30
|
+
t(i);
|
|
31
|
+
let a = await o(i, e, .5);
|
|
32
|
+
r = a !== null && a.type === e;
|
|
33
|
+
} catch {
|
|
34
|
+
r = !1;
|
|
35
|
+
}
|
|
36
|
+
return i.set(e, r), r;
|
|
37
|
+
}
|
|
38
|
+
async function c(e = ["image/webp", "image/jpeg"]) {
|
|
39
|
+
for (let t of e) if (await s(t)) return t;
|
|
40
|
+
return "image/jpeg";
|
|
41
|
+
}
|
|
42
|
+
function l() {
|
|
43
|
+
i.clear();
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { n as DEFAULT_QUALITY, r as DEFAULT_TYPE, c as bestSupportedType, a as encodeImage, l as resetImageTypeSupportCache, s as supportsImageType };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=encode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encode.js","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":";;;AAgBA,IAAa,IAAkB,KAGlB,IAA0B,cAEjC,oBAAe,IAAI,IAAqB;AAU9C,eAAsB,EAClB,GACA,IAAyB,CAAC,GACH;CACvB,IAAM,IAAO,EAAQ,QAAA,cAGf,IAAO,MAAM,EAAc,GAAS,GAF1B,EAAQ,WAAA,GAE+B;CACvD,IAAI,MAAS,MACT,MAAM,IAAI,EACN,oCAAoC,EAAK,+DAE7C;CAGJ,OAAO;EACH;EACA,OAAO,EAAQ;EACf,QAAQ,EAAQ;EAChB,MAAM,EAAK,QAAQ;EACnB,OAAO,EAAK;CAChB;AACJ;AAaA,eAAe,EACX,GACA,GACA,GACoB;CAIpB,OAHI,mBAAmB,IACZ,MAAM,EAAQ,cAAc;EAAE;EAAM;CAAQ,CAAC,IAEjD,MAAM,IAAI,SAAsB,MAAY;EAC/C,EAAQ,QAAQ,MAAS,EAAQ,CAAI,GAAG,GAAM,CAAO;CACzD,CAAC;AACL;AAkBA,eAAsB,EAAkB,GAAmC;CACvE,IAAM,IAAS,EAAa,IAAI,CAAI;CACpC,IAAI,MAAW,KAAA,GAAW,OAAO;CAEjC,IAAI;CACJ,IAAI;EACA,IAAM,EAAE,qBAAkB,MAAM,OAAO,gBACjC,IAAU,EAAc,GAAG,CAAC;EAClC,EAAW,CAAO;EAClB,IAAM,IAAO,MAAM,EAAc,GAAS,GAAM,EAAG;EACnD,IAAY,MAAS,QAAQ,EAAK,SAAS;CAC/C,QAAQ;EACJ,IAAY;CAChB;CAGA,OADA,EAAa,IAAI,GAAM,CAAS,GACzB;AACX;AAcA,eAAsB,EAClB,IAAoC,CAAC,cAAc,YAAY,GAC7C;CAClB,KAAK,IAAM,KAAQ,GACf,IAAI,MAAM,EAAkB,CAAI,GAAG,OAAO;CAE9C,OAAO;AACX;AAOA,SAAgB,IAAmC;CAC/C,EAAa,MAAM;AACvB"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=class extends Error{constructor(e,t){super(e,t),this.name=`ImagingError`}},t=class extends e{constructor(e,t){super(e,t),this.name=`ImageDecodeError`}},n=class extends e{constructor(e,t){super(e,t),this.name=`ImageEncodeError`}},r=class extends e{constructor(e,t){super(e,t),this.name=`UnsupportedImageTypeError`}},i=class extends e{constructor(e,t){super(e,t),this.name=`ImagingUnavailableError`}};exports.ImageDecodeError=t,exports.ImageEncodeError=n,exports.ImagingError=e,exports.ImagingUnavailableError=i,exports.UnsupportedImageTypeError=r;
|
|
2
|
+
//# sourceMappingURL=exceptions.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exceptions.cjs","names":[],"sources":["../../src/imaging/exceptions.ts"],"sourcesContent":["/**\n * Errors thrown by the imaging module.\n *\n * `name` is a literal string on every subclass: minifiers rename classes,\n * and a derived `new.target.name` ships as `error.name === \"t\"`.\n */\n\n/** Base class for every error this module throws. */\nexport class ImagingError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ImagingError\";\n }\n}\n\n/** The source could not be decoded into pixels. */\nexport class ImageDecodeError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ImageDecodeError\";\n }\n}\n\n/** The canvas could not produce encoded bytes. */\nexport class ImageEncodeError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ImageEncodeError\";\n }\n}\n\n/**\n * The browser cannot encode the requested format.\n *\n * Worth its own class because the failure is otherwise silent: asking a\n * canvas for an unsupported type does not throw — it hands back a PNG,\n * and an app that trusted the request ships 4 MB where it expected 300 KB.\n */\nexport class UnsupportedImageTypeError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"UnsupportedImageTypeError\";\n }\n}\n\n/** The environment has no canvas to draw on. */\nexport class ImagingUnavailableError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ImagingUnavailableError\";\n }\n}\n"],"mappings":"AAQA,IAAa,EAAb,cAAkC,KAAM,CACpC,YAAY,EAAiB,EAAwB,CACjD,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,cAChB,CACJ,EAGa,EAAb,cAAsC,CAAa,CAC/C,YAAY,EAAiB,EAAwB,CACjD,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,kBAChB,CACJ,EAGa,EAAb,cAAsC,CAAa,CAC/C,YAAY,EAAiB,EAAwB,CACjD,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,kBAChB,CACJ,EASa,EAAb,cAA+C,CAAa,CACxD,YAAY,EAAiB,EAAwB,CACjD,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,2BAChB,CACJ,EAGa,EAAb,cAA6C,CAAa,CACtD,YAAY,EAAiB,EAAwB,CACjD,MAAM,EAAS,CAAO,EACtB,KAAK,KAAO,yBAChB,CACJ"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/imaging/exceptions.ts
|
|
2
|
+
var e = class extends Error {
|
|
3
|
+
constructor(e, t) {
|
|
4
|
+
super(e, t), this.name = "ImagingError";
|
|
5
|
+
}
|
|
6
|
+
}, t = class extends e {
|
|
7
|
+
constructor(e, t) {
|
|
8
|
+
super(e, t), this.name = "ImageDecodeError";
|
|
9
|
+
}
|
|
10
|
+
}, n = class extends e {
|
|
11
|
+
constructor(e, t) {
|
|
12
|
+
super(e, t), this.name = "ImageEncodeError";
|
|
13
|
+
}
|
|
14
|
+
}, r = class extends e {
|
|
15
|
+
constructor(e, t) {
|
|
16
|
+
super(e, t), this.name = "UnsupportedImageTypeError";
|
|
17
|
+
}
|
|
18
|
+
}, i = class extends e {
|
|
19
|
+
constructor(e, t) {
|
|
20
|
+
super(e, t), this.name = "ImagingUnavailableError";
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { t as ImageDecodeError, n as ImageEncodeError, e as ImagingError, i as ImagingUnavailableError, r as UnsupportedImageTypeError };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=exceptions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exceptions.js","names":[],"sources":["../../src/imaging/exceptions.ts"],"sourcesContent":["/**\n * Errors thrown by the imaging module.\n *\n * `name` is a literal string on every subclass: minifiers rename classes,\n * and a derived `new.target.name` ships as `error.name === \"t\"`.\n */\n\n/** Base class for every error this module throws. */\nexport class ImagingError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ImagingError\";\n }\n}\n\n/** The source could not be decoded into pixels. */\nexport class ImageDecodeError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ImageDecodeError\";\n }\n}\n\n/** The canvas could not produce encoded bytes. */\nexport class ImageEncodeError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ImageEncodeError\";\n }\n}\n\n/**\n * The browser cannot encode the requested format.\n *\n * Worth its own class because the failure is otherwise silent: asking a\n * canvas for an unsupported type does not throw — it hands back a PNG,\n * and an app that trusted the request ships 4 MB where it expected 300 KB.\n */\nexport class UnsupportedImageTypeError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"UnsupportedImageTypeError\";\n }\n}\n\n/** The environment has no canvas to draw on. */\nexport class ImagingUnavailableError extends ImagingError {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"ImagingUnavailableError\";\n }\n}\n"],"mappings":";AAQA,IAAa,IAAb,cAAkC,MAAM;CACpC,YAAY,GAAiB,GAAwB;EAEjD,AADA,MAAM,GAAS,CAAO,GACtB,KAAK,OAAO;CAChB;AACJ,GAGa,IAAb,cAAsC,EAAa;CAC/C,YAAY,GAAiB,GAAwB;EAEjD,AADA,MAAM,GAAS,CAAO,GACtB,KAAK,OAAO;CAChB;AACJ,GAGa,IAAb,cAAsC,EAAa;CAC/C,YAAY,GAAiB,GAAwB;EAEjD,AADA,MAAM,GAAS,CAAO,GACtB,KAAK,OAAO;CAChB;AACJ,GASa,IAAb,cAA+C,EAAa;CACxD,YAAY,GAAiB,GAAwB;EAEjD,AADA,MAAM,GAAS,CAAO,GACtB,KAAK,OAAO;CAChB;AACJ,GAGa,IAAb,cAA6C,EAAa;CACtD,YAAY,GAAiB,GAAwB;EAEjD,AADA,MAAM,GAAS,CAAO,GACtB,KAAK,OAAO;CAChB;AACJ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./canvas.cjs"),t=require("./decode.cjs"),n=require("./encode.cjs");async function r(r,i,a={}){let{bitmap:o}=await t.decodeImage(r);try{let t=[];for(let r of i){let i=Math.min(1,r.size/Math.max(o.width,o.height)),s=Math.max(1,Math.round(o.width*i)),c=Math.max(1,Math.round(o.height*i)),l=e.createSurface(s,c);e.drawScaled(o,l,{x:0,y:0,width:s,height:c});let u=await n.encodeImage(l,{...a,...r.encode});t.push({...u,name:r.name})}return t}finally{o.close?.()}}exports.createThumbnails=r;
|
|
2
|
+
//# sourceMappingURL=thumbnails.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnails.cjs","names":[],"sources":["../../src/imaging/thumbnails.ts"],"sourcesContent":["/**\n * Producing several sizes from one decode.\n *\n * A gallery needs a grid thumbnail, a list avatar and a detail view. Doing\n * that with three `resizeImage` calls decodes the same photo three times,\n * which on a 12-megapixel picture is most of the cost — the decode dominates,\n * not the scaling.\n */\n\nimport { createSurface, drawScaled } from \"./canvas\";\nimport { decodeImage } from \"./decode\";\nimport { encodeImage } from \"./encode\";\nimport type { EncodeOptions, ImageSource, ProcessedImage } from \"./types\";\n\n/** One requested size. */\nexport interface ThumbnailSpec {\n /** Name to find it by in the result. */\n readonly name: string;\n /** Longest edge in pixels. */\n readonly size: number;\n /** Format and quality, overriding the shared options. */\n readonly encode?: EncodeOptions;\n}\n\n/** A produced thumbnail. */\nexport interface Thumbnail extends ProcessedImage {\n readonly name: string;\n}\n\n/**\n * Produce several sizes from a single decode.\n *\n * @example\n * ```ts\n * const [thumb, card] = await createThumbnails(file, [\n * { name: \"thumb\", size: 96 },\n * { name: \"card\", size: 480 },\n * ]);\n * ```\n *\n * Sizes are the **longest edge**, and the aspect ratio is kept, so a\n * portrait and a landscape photo both fit the same grid cell without a\n * separate calculation per orientation.\n *\n * @param source Anything decodable.\n * @param specs The sizes to produce.\n * @param options Shared format and quality.\n * @returns One result per spec, in the order requested.\n * @throws {@link ImageDecodeError} when the source cannot be decoded.\n */\nexport async function createThumbnails(\n source: ImageSource,\n specs: readonly ThumbnailSpec[],\n options: EncodeOptions = {},\n): Promise<Thumbnail[]> {\n const { bitmap } = await decodeImage(source);\n try {\n const results: Thumbnail[] = [];\n for (const spec of specs) {\n const scale = Math.min(1, spec.size / Math.max(bitmap.width, bitmap.height));\n const width = Math.max(1, Math.round(bitmap.width * scale));\n const height = Math.max(1, Math.round(bitmap.height * scale));\n\n const surface = createSurface(width, height);\n drawScaled(bitmap, surface, { x: 0, y: 0, width, height });\n const encoded = await encodeImage(surface, { ...options, ...spec.encode });\n results.push({ ...encoded, name: spec.name });\n }\n return results;\n } finally {\n bitmap.close?.();\n }\n}\n"],"mappings":"oFAkDA,eAAsB,EAClB,EACA,EACA,EAAyB,CAAC,EACN,CACpB,GAAM,CAAE,UAAW,MAAM,EAAA,YAAY,CAAM,EAC3C,GAAI,CACA,IAAM,EAAuB,CAAC,EAC9B,IAAK,IAAM,KAAQ,EAAO,CACtB,IAAM,EAAQ,KAAK,IAAI,EAAG,EAAK,KAAO,KAAK,IAAI,EAAO,MAAO,EAAO,MAAM,CAAC,EACrE,EAAQ,KAAK,IAAI,EAAG,KAAK,MAAM,EAAO,MAAQ,CAAK,CAAC,EACpD,EAAS,KAAK,IAAI,EAAG,KAAK,MAAM,EAAO,OAAS,CAAK,CAAC,EAEtD,EAAU,EAAA,cAAc,EAAO,CAAM,EAC3C,EAAA,WAAW,EAAQ,EAAS,CAAE,EAAG,EAAG,EAAG,EAAG,QAAO,QAAO,CAAC,EACzD,IAAM,EAAU,MAAM,EAAA,YAAY,EAAS,CAAE,GAAG,EAAS,GAAG,EAAK,MAAO,CAAC,EACzE,EAAQ,KAAK,CAAE,GAAG,EAAS,KAAM,EAAK,IAAK,CAAC,CAChD,CACA,OAAO,CACX,QAAU,CACN,EAAO,QAAQ,CACnB,CACJ"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createSurface as e, drawScaled as t } from "./canvas.js";
|
|
2
|
+
import { decodeImage as n } from "./decode.js";
|
|
3
|
+
import { encodeImage as r } from "./encode.js";
|
|
4
|
+
//#region src/imaging/thumbnails.ts
|
|
5
|
+
async function i(i, a, o = {}) {
|
|
6
|
+
let { bitmap: s } = await n(i);
|
|
7
|
+
try {
|
|
8
|
+
let n = [];
|
|
9
|
+
for (let i of a) {
|
|
10
|
+
let a = Math.min(1, i.size / Math.max(s.width, s.height)), c = Math.max(1, Math.round(s.width * a)), l = Math.max(1, Math.round(s.height * a)), u = e(c, l);
|
|
11
|
+
t(s, u, {
|
|
12
|
+
x: 0,
|
|
13
|
+
y: 0,
|
|
14
|
+
width: c,
|
|
15
|
+
height: l
|
|
16
|
+
});
|
|
17
|
+
let d = await r(u, {
|
|
18
|
+
...o,
|
|
19
|
+
...i.encode
|
|
20
|
+
});
|
|
21
|
+
n.push({
|
|
22
|
+
...d,
|
|
23
|
+
name: i.name
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return n;
|
|
27
|
+
} finally {
|
|
28
|
+
s.close?.();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { i as createThumbnails };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=thumbnails.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnails.js","names":[],"sources":["../../src/imaging/thumbnails.ts"],"sourcesContent":["/**\n * Producing several sizes from one decode.\n *\n * A gallery needs a grid thumbnail, a list avatar and a detail view. Doing\n * that with three `resizeImage` calls decodes the same photo three times,\n * which on a 12-megapixel picture is most of the cost — the decode dominates,\n * not the scaling.\n */\n\nimport { createSurface, drawScaled } from \"./canvas\";\nimport { decodeImage } from \"./decode\";\nimport { encodeImage } from \"./encode\";\nimport type { EncodeOptions, ImageSource, ProcessedImage } from \"./types\";\n\n/** One requested size. */\nexport interface ThumbnailSpec {\n /** Name to find it by in the result. */\n readonly name: string;\n /** Longest edge in pixels. */\n readonly size: number;\n /** Format and quality, overriding the shared options. */\n readonly encode?: EncodeOptions;\n}\n\n/** A produced thumbnail. */\nexport interface Thumbnail extends ProcessedImage {\n readonly name: string;\n}\n\n/**\n * Produce several sizes from a single decode.\n *\n * @example\n * ```ts\n * const [thumb, card] = await createThumbnails(file, [\n * { name: \"thumb\", size: 96 },\n * { name: \"card\", size: 480 },\n * ]);\n * ```\n *\n * Sizes are the **longest edge**, and the aspect ratio is kept, so a\n * portrait and a landscape photo both fit the same grid cell without a\n * separate calculation per orientation.\n *\n * @param source Anything decodable.\n * @param specs The sizes to produce.\n * @param options Shared format and quality.\n * @returns One result per spec, in the order requested.\n * @throws {@link ImageDecodeError} when the source cannot be decoded.\n */\nexport async function createThumbnails(\n source: ImageSource,\n specs: readonly ThumbnailSpec[],\n options: EncodeOptions = {},\n): Promise<Thumbnail[]> {\n const { bitmap } = await decodeImage(source);\n try {\n const results: Thumbnail[] = [];\n for (const spec of specs) {\n const scale = Math.min(1, spec.size / Math.max(bitmap.width, bitmap.height));\n const width = Math.max(1, Math.round(bitmap.width * scale));\n const height = Math.max(1, Math.round(bitmap.height * scale));\n\n const surface = createSurface(width, height);\n drawScaled(bitmap, surface, { x: 0, y: 0, width, height });\n const encoded = await encodeImage(surface, { ...options, ...spec.encode });\n results.push({ ...encoded, name: spec.name });\n }\n return results;\n } finally {\n bitmap.close?.();\n }\n}\n"],"mappings":";;;;AAkDA,eAAsB,EAClB,GACA,GACA,IAAyB,CAAC,GACN;CACpB,IAAM,EAAE,cAAW,MAAM,EAAY,CAAM;CAC3C,IAAI;EACA,IAAM,IAAuB,CAAC;EAC9B,KAAK,IAAM,KAAQ,GAAO;GACtB,IAAM,IAAQ,KAAK,IAAI,GAAG,EAAK,OAAO,KAAK,IAAI,EAAO,OAAO,EAAO,MAAM,CAAC,GACrE,IAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,EAAO,QAAQ,CAAK,CAAC,GACpD,IAAS,KAAK,IAAI,GAAG,KAAK,MAAM,EAAO,SAAS,CAAK,CAAC,GAEtD,IAAU,EAAc,GAAO,CAAM;GAC3C,EAAW,GAAQ,GAAS;IAAE,GAAG;IAAG,GAAG;IAAG;IAAO;GAAO,CAAC;GACzD,IAAM,IAAU,MAAM,EAAY,GAAS;IAAE,GAAG;IAAS,GAAG,EAAK;GAAO,CAAC;GACzE,EAAQ,KAAK;IAAE,GAAG;IAAS,MAAM,EAAK;GAAK,CAAC;EAChD;EACA,OAAO;CACX,UAAU;EACN,EAAO,QAAQ;CACnB;AACJ"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./canvas.cjs"),t=require("./decode.cjs"),n=require("./encode.cjs");var r=`#ffffff`,i=new Set([`image/jpeg`,`image/jpg`]);function a(e,t,n,r){let i=e.width/e.height,a=t.width??(t.height===void 0?e.width:t.height*i),o=t.height??(t.width===void 0?e.height:t.width/i);if(r){let t=Math.min(1,e.width/a,e.height/o);a*=t,o*=t}if(a=Math.max(1,Math.round(a)),o=Math.max(1,Math.round(o)),n===`fill`)return{surface:{width:a,height:o},draw:{x:0,y:0,width:a,height:o}};let s=n===`cover`?Math.max(a/e.width,o/e.height):Math.min(a/e.width,o/e.height),c=Math.max(1,Math.round(e.width*s)),l=Math.max(1,Math.round(e.height*s));return n===`contain`?{surface:{width:c,height:l},draw:{x:0,y:0,width:c,height:l}}:{surface:{width:a,height:o},draw:{x:Math.round((a-c)/2),y:Math.round((o-l)/2),width:c,height:l}}}function o(e){if(e.background!==void 0)return e.background;let t=e.type??`image/jpeg`;return i.has(t)||e.fit===`pad`?r:void 0}async function s(r,i={}){let{bitmap:s}=await t.decodeImage(r);try{let t=a(s,{width:i.width,height:i.height},i.fit??`contain`,i.withoutEnlargement!==!1),r=e.createSurface(t.surface.width,t.surface.height);return e.drawScaled(s,r,t.draw,o(i)),await n.encodeImage(r,i)}finally{s.close?.()}}async function c(a,o,s={}){let{bitmap:c}=await t.decodeImage(a);try{let t=Math.max(0,Math.min(Math.round(o.x),c.width-1)),a=Math.max(0,Math.min(Math.round(o.y),c.height-1)),l=Math.max(1,Math.min(Math.round(o.width),c.width-t)),u=Math.max(1,Math.min(Math.round(o.height),c.height-a)),d=e.createSurface(l,u);return e.getContext(d,i.has(s.type??`image/jpeg`)?r:void 0).drawImage(c,t,a,l,u,0,0,l,u),await n.encodeImage(d,s)}finally{c.close?.()}}async function l(a,o,s={}){if(o%90!=0)throw RangeError(`rotateImage takes multiples of 90 degrees; got ${o}. For an arbitrary angle, draw it yourself: the corner handling is a design decision, not a default.`);let c=(o/90%4+4)%4,{bitmap:l}=await t.decodeImage(a);try{let t=c%2==1,a=t?l.height:l.width,o=t?l.width:l.height,u=e.createSurface(a,o),d=e.getContext(u,i.has(s.type??`image/jpeg`)?r:void 0);return d.translate(a/2,o/2),d.rotate(c*Math.PI/2),d.drawImage(l,-l.width/2,-l.height/2),await n.encodeImage(u,s)}finally{l.close?.()}}async function u(a,o,s={}){let{bitmap:c}=await t.decodeImage(a);try{let t=e.createSurface(c.width,c.height),a=e.getContext(t,i.has(s.type??`image/jpeg`)?r:void 0);return a.translate(o.horizontal===!0?c.width:0,o.vertical===!0?c.height:0),a.scale(o.horizontal===!0?-1:1,o.vertical===!0?-1:1),a.drawImage(c,0,0),await n.encodeImage(t,s)}finally{c.close?.()}}exports.DEFAULT_BACKGROUND=r,exports.cropImage=c,exports.flipImage=u,exports.resizeImage=s,exports.rotateImage=l;
|
|
2
|
+
//# sourceMappingURL=transform.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"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":"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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createSurface as e, drawScaled as t, getContext as n } from "./canvas.js";
|
|
2
|
+
import { decodeImage as r } from "./decode.js";
|
|
3
|
+
import { encodeImage as i } from "./encode.js";
|
|
4
|
+
//#region src/imaging/transform.ts
|
|
5
|
+
var a = "#ffffff", o = /* @__PURE__ */ new Set(["image/jpeg", "image/jpg"]);
|
|
6
|
+
function s(e, t, n, r) {
|
|
7
|
+
let i = e.width / e.height, a = t.width ?? (t.height === void 0 ? e.width : t.height * i), o = t.height ?? (t.width === void 0 ? e.height : t.width / i);
|
|
8
|
+
if (r) {
|
|
9
|
+
let t = Math.min(1, e.width / a, e.height / o);
|
|
10
|
+
a *= t, o *= t;
|
|
11
|
+
}
|
|
12
|
+
if (a = Math.max(1, Math.round(a)), o = Math.max(1, Math.round(o)), n === "fill") return {
|
|
13
|
+
surface: {
|
|
14
|
+
width: a,
|
|
15
|
+
height: o
|
|
16
|
+
},
|
|
17
|
+
draw: {
|
|
18
|
+
x: 0,
|
|
19
|
+
y: 0,
|
|
20
|
+
width: a,
|
|
21
|
+
height: o
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
let s = n === "cover" ? Math.max(a / e.width, o / e.height) : Math.min(a / e.width, o / e.height), c = Math.max(1, Math.round(e.width * s)), l = Math.max(1, Math.round(e.height * s));
|
|
25
|
+
return n === "contain" ? {
|
|
26
|
+
surface: {
|
|
27
|
+
width: c,
|
|
28
|
+
height: l
|
|
29
|
+
},
|
|
30
|
+
draw: {
|
|
31
|
+
x: 0,
|
|
32
|
+
y: 0,
|
|
33
|
+
width: c,
|
|
34
|
+
height: l
|
|
35
|
+
}
|
|
36
|
+
} : {
|
|
37
|
+
surface: {
|
|
38
|
+
width: a,
|
|
39
|
+
height: o
|
|
40
|
+
},
|
|
41
|
+
draw: {
|
|
42
|
+
x: Math.round((a - c) / 2),
|
|
43
|
+
y: Math.round((o - l) / 2),
|
|
44
|
+
width: c,
|
|
45
|
+
height: l
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function c(e) {
|
|
50
|
+
if (e.background !== void 0) return e.background;
|
|
51
|
+
let t = e.type ?? "image/jpeg";
|
|
52
|
+
return o.has(t) || e.fit === "pad" ? a : void 0;
|
|
53
|
+
}
|
|
54
|
+
async function l(n, a = {}) {
|
|
55
|
+
let { bitmap: o } = await r(n);
|
|
56
|
+
try {
|
|
57
|
+
let n = s(o, {
|
|
58
|
+
width: a.width,
|
|
59
|
+
height: a.height
|
|
60
|
+
}, a.fit ?? "contain", a.withoutEnlargement !== !1), r = e(n.surface.width, n.surface.height);
|
|
61
|
+
return t(o, r, n.draw, c(a)), await i(r, a);
|
|
62
|
+
} finally {
|
|
63
|
+
o.close?.();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function u(t, s, c = {}) {
|
|
67
|
+
let { bitmap: l } = await r(t);
|
|
68
|
+
try {
|
|
69
|
+
let t = Math.max(0, Math.min(Math.round(s.x), l.width - 1)), r = Math.max(0, Math.min(Math.round(s.y), l.height - 1)), u = Math.max(1, Math.min(Math.round(s.width), l.width - t)), d = Math.max(1, Math.min(Math.round(s.height), l.height - r)), f = e(u, d);
|
|
70
|
+
return n(f, o.has(c.type ?? "image/jpeg") ? a : void 0).drawImage(l, t, r, u, d, 0, 0, u, d), await i(f, c);
|
|
71
|
+
} finally {
|
|
72
|
+
l.close?.();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function d(t, s, c = {}) {
|
|
76
|
+
if (s % 90 != 0) throw RangeError(`rotateImage takes multiples of 90 degrees; got ${s}. For an arbitrary angle, draw it yourself: the corner handling is a design decision, not a default.`);
|
|
77
|
+
let l = (s / 90 % 4 + 4) % 4, { bitmap: u } = await r(t);
|
|
78
|
+
try {
|
|
79
|
+
let t = l % 2 == 1, r = t ? u.height : u.width, s = t ? u.width : u.height, d = e(r, s), f = n(d, o.has(c.type ?? "image/jpeg") ? a : void 0);
|
|
80
|
+
return f.translate(r / 2, s / 2), f.rotate(l * Math.PI / 2), f.drawImage(u, -u.width / 2, -u.height / 2), await i(d, c);
|
|
81
|
+
} finally {
|
|
82
|
+
u.close?.();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function f(t, s, c = {}) {
|
|
86
|
+
let { bitmap: l } = await r(t);
|
|
87
|
+
try {
|
|
88
|
+
let t = e(l.width, l.height), r = n(t, o.has(c.type ?? "image/jpeg") ? a : void 0);
|
|
89
|
+
return r.translate(s.horizontal === !0 ? l.width : 0, s.vertical === !0 ? l.height : 0), r.scale(s.horizontal === !0 ? -1 : 1, s.vertical === !0 ? -1 : 1), r.drawImage(l, 0, 0), await i(t, c);
|
|
90
|
+
} finally {
|
|
91
|
+
l.close?.();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
export { a as DEFAULT_BACKGROUND, u as cropImage, f as flipImage, l as resizeImage, d as rotateImage };
|
|
96
|
+
|
|
97
|
+
//# sourceMappingURL=transform.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transform.js","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":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./transform.cjs"),t=require("./compress.cjs");let n=require("react");function r(e){let[t,r]=(0,n.useState)(null);return(0,n.useEffect)(()=>{if(e==null){r(null);return}let t=URL.createObjectURL(e);return r(t),()=>{URL.revokeObjectURL(t)}},[e]),{url:t}}function i(){let[r,i]=(0,n.useState)(null),[a,o]=(0,n.useState)(`idle`),[s,c]=(0,n.useState)(null),l=(0,n.useRef)(!0);(0,n.useEffect)(()=>(l.current=!0,()=>{l.current=!1}),[]);let u=(0,n.useCallback)(async e=>{l.current&&(o(`working`),c(null));try{let t=await e();return l.current&&(i(t),o(`done`)),t}catch(e){let t=e instanceof Error?e:Error(String(e));throw l.current&&(c(t),o(`error`)),t}},[]);return{resize:(0,n.useCallback)((t,n={})=>u(()=>e.resizeImage(t,n)),[u]),compress:(0,n.useCallback)((e,n)=>u(()=>t.compressToTarget(e,n)),[u]),result:r,status:a,error:s,isWorking:a===`working`}}exports.useImagePreview=r,exports.useImageProcessing=i;
|
|
2
|
+
//# sourceMappingURL=use-image-processing.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"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,EAAA,EAAA,SAAA,CAAkC,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,CAA6C,IAAI,EAC1D,CAAC,EAAQ,IAAA,EAAA,EAAA,SAAA,CAA6C,MAAM,EAC5D,CAAC,EAAO,IAAA,EAAA,EAAA,SAAA,CAAmC,IAAI,EAC/C,GAAA,EAAA,EAAA,OAAA,CAAiB,EAAI,GAE3B,EAAA,EAAA,UAAA,MACI,EAAQ,QAAU,OACL,CACT,EAAQ,QAAU,EACtB,GACD,CAAC,CAAC,EAEL,IAAM,GAAA,EAAA,EAAA,YAAA,CACF,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,EAAA,EAAA,YAAA,EAZC,EAAqB,EAAyB,CAAC,IAC5C,MAAU,EAAA,YAAY,EAAQ,CAAO,CAAC,EAC1C,CAAC,CAAG,CAUJ,EACA,UAAA,EAAA,EAAA,YAAA,EAPC,EAAqB,IAClB,MAAU,EAAA,iBAAiB,EAAQ,CAAO,CAAC,EAC/C,CAAC,CAAG,CAKJ,EACA,SACA,SACA,QACA,UAAW,IAAW,SAC1B,CACJ"}
|