weblet-convert 0.0.2

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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 enzox0
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # @weblet-convert
2
+
3
+ Convert **images to WebP** and **videos to WebM** in both **browsers** and **Node.js**.
4
+
5
+ ## What this is for
6
+
7
+ - **Shrink images for the web**: turn PNG/JPEG/etc into WebP, optionally downscale, and (optionally) try to keep output under a byte budget.
8
+ - **Transcode videos for the web**: turn common video formats into WebM.
9
+ - **One entrypoint for uploads**: call `convert()` and it will detect **image vs video** and convert appropriately.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm i weblet-convert
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ### Browser
20
+
21
+ ```ts
22
+ import { convert } from "weblet-convert"
23
+
24
+ const res = await convert(fileOrBlobOrUrl, { returnFile: true })
25
+ console.log(res.kind, res.blob.type, res.blob.size)
26
+ ```
27
+
28
+ - **Input types (browser)**: `Blob | File | string`
29
+ - If `string`, it is fetched as a URL.
30
+ - **Output**: always includes `blob`. If `returnFile: true` and the environment has `File`, it also includes `file`.
31
+ - **Browser video note**: video transcoding uses ffmpeg.wasm, so large videos can be slow and memory-heavy.
32
+
33
+ ### Node.js
34
+
35
+ ```ts
36
+ import { convert } from "weblet-convert/node"
37
+ import { readFile } from "node:fs/promises"
38
+
39
+ const bytes = await readFile("input.png")
40
+ const res = await convert(bytes)
41
+ const outArrayBuffer = await res.blob.arrayBuffer()
42
+ console.log(res.kind, res.blob.type, outArrayBuffer.byteLength)
43
+ ```
44
+
45
+ - **Input types (node)**: `Buffer | Uint8Array | ArrayBuffer | string`
46
+ - If `string` starts with `http://` or `https://`, it is fetched.
47
+ - Otherwise it is treated as a local file path.
48
+ - **Node requirement**: Node.js **18+** (uses global `fetch` / `Blob`).
49
+
50
+ ## API
51
+
52
+ ### `convert(input, options?)`
53
+
54
+ Converts based on detected asset type:
55
+
56
+ - Images → WebP (`imageToWebp()`)
57
+ - Videos → WebM (`videoToWebm()`)
58
+
59
+ ## Supported formats
60
+
61
+ ### Outputs
62
+ - **Images**: WebP (`image/webp`)
63
+ - **Videos**: WebM (`video/webm`, VP9 video + Opus audio)
64
+
65
+ ### Inputs (auto-detected)
66
+ - **Images (typical)**: `png`, `jpg` / `jpeg`, `gif`, `bmp`, `tif` / `tiff`, `webp`, `avif`, `svg`
67
+ - **Videos (typical)**: `mp4`, `mov`, `m4v`, `mkv`, `webm`, `avi`, `wmv`, `flv`
68
+
69
+ Notes:
70
+ - **Detection** prefers MIME (`File.type` / `Blob.type`) and falls back to magic-byte sniffing.
71
+ - **Actual decodability depends on the environment**:
72
+ - **Node** image decoding is handled by `sharp` (supports many formats).
73
+ - **Browser** image decoding depends on the browser’s built-in codecs.
74
+ - **Video transcoding** in the browser uses ffmpeg.wasm and can generally read many formats, but performance varies.
75
+
76
+ ### `imageToWebp(input, options?)`
77
+
78
+ #### Options
79
+
80
+ - **`maxWidth` / `maxHeight`**: hard cap for output dimensions (keeps aspect ratio). Default `2048`.
81
+ - **`targetBytes`**: if set, tries to encode to **\(\le\)** this size by lowering quality (binary search within the range).
82
+ - **`maxQuality` / `minQuality`**: quality range, from `0` to `1`. Defaults `0.82` / `0.45`.
83
+ - **`returnFile`**: if `true`, also returns `file` when `File` exists. Default `false`.
84
+ - **`fileName`**: output file name (only used when returning a `File`).
85
+ - **`force`**: when WebP encoding isn’t supported in the browser, controls whether the original input can be returned instead of WebP. (In Node, WebP is always produced because `sharp` encodes it.)
86
+
87
+ #### Result
88
+
89
+ - **`blob`**: output `Blob` (normally `type === "image/webp"`).
90
+ - **`file?`**: only present when `returnFile: true` *and* `File` exists.
91
+ - **`width` / `height`**: output dimensions.
92
+ - **`quality`**: selected quality in the `0..1` range.
93
+ - **`isWebp`**: whether the output is actually WebP.
94
+
95
+ ### `videoToWebm(input, options?)`
96
+
97
+ Transcodes videos to WebM.
98
+
99
+ ## Notes and limitations
100
+
101
+ - **Browser WebP support varies**: the browser build relies on Canvas/OffscreenCanvas WebP encoding support. When encoding isn’t available, the function can return the original input and set `isWebp: false`.
102
+ - **Metadata**: EXIF/IPTC metadata is not preserved.
103
+ - **Not a perfect “max bytes” guarantee**: `targetBytes` is a best-effort search within the provided quality range.
104
+
105
+ ## Import paths
106
+
107
+ - **Browser**: `import { convert, imageToWebp, videoToWebm } from "@weblet/convert"`
108
+ - **Node**: `import { convert, imageToWebp, videoToWebm } from "weblet-convert/node"`
109
+
110
+ ## Build (contributors)
111
+
112
+ ```bash
113
+ npm run build
114
+ npm run typecheck
115
+ npm test
116
+ ```
117
+
118
+ ## License
119
+
120
+ MIT. See `LICENSE`.
121
+
@@ -0,0 +1,254 @@
1
+ 'use strict';
2
+
3
+ var ffmpeg = require('@ffmpeg/ffmpeg');
4
+ var util = require('@ffmpeg/util');
5
+ var fileType = require('file-type');
6
+
7
+ // src/browser/index.ts
8
+ var DEFAULTS = {
9
+ maxWidth: 2048,
10
+ maxHeight: 2048,
11
+ maxQuality: 0.82,
12
+ minQuality: 0.45,
13
+ force: true,
14
+ returnFile: false
15
+ };
16
+ function clamp01(n) {
17
+ return Math.max(0, Math.min(1, n));
18
+ }
19
+ function toWebpName(name) {
20
+ const base = name.replace(/\.[^/.]+$/, "");
21
+ return `${base || "image"}.webp`;
22
+ }
23
+ function getInputName(input) {
24
+ if (typeof input === "string") return "image.webp";
25
+ if (input instanceof File && input.name) return toWebpName(input.name);
26
+ return "image.webp";
27
+ }
28
+ async function inputToBlob(input) {
29
+ if (typeof input === "string") {
30
+ const res = await fetch(input);
31
+ if (!res.ok) throw new Error(`Failed to fetch: ${res.status} ${res.statusText}`);
32
+ return await res.blob();
33
+ }
34
+ return input;
35
+ }
36
+ function toWebmName(name) {
37
+ const base = name.replace(/\.[^/.]+$/, "");
38
+ return `${base || "video"}.webm`;
39
+ }
40
+ function defaultVideoName(input, fileName) {
41
+ if (fileName && fileName.trim().length > 0) return fileName;
42
+ if (typeof input === "string") return "video.webm";
43
+ if (input instanceof File && input.name) return toWebmName(input.name);
44
+ return "video.webm";
45
+ }
46
+ var ffmpegSingleton = null;
47
+ var ffmpegLoadPromise = null;
48
+ async function getFfmpeg() {
49
+ if (ffmpegSingleton) return ffmpegSingleton;
50
+ if (ffmpegLoadPromise) return ffmpegLoadPromise;
51
+ const ff = new ffmpeg.FFmpeg();
52
+ ffmpegLoadPromise = (async () => {
53
+ await ff.load();
54
+ ffmpegSingleton = ff;
55
+ return ff;
56
+ })();
57
+ return ffmpegLoadPromise;
58
+ }
59
+ var VIDEO_DEFAULTS = {
60
+ crf: 32,
61
+ deadline: "good",
62
+ returnFile: false
63
+ };
64
+ async function videoToWebm(input, options = {}) {
65
+ const opts = { ...VIDEO_DEFAULTS, ...options };
66
+ const blob = await inputToBlob(input);
67
+ const ffmpeg = await getFfmpeg();
68
+ const inName = "input";
69
+ const outName = "output.webm";
70
+ await ffmpeg.writeFile(inName, await util.fetchFile(blob));
71
+ const args = [
72
+ ...opts.maxDurationSeconds && opts.maxDurationSeconds > 0 ? ["-t", String(opts.maxDurationSeconds)] : [],
73
+ "-i",
74
+ inName,
75
+ "-map",
76
+ "0:v:0",
77
+ "-map",
78
+ "0:a?",
79
+ "-c:v",
80
+ "libvpx-vp9",
81
+ "-b:v",
82
+ "0",
83
+ "-crf",
84
+ String(opts.crf),
85
+ "-deadline",
86
+ opts.deadline,
87
+ "-row-mt",
88
+ "1",
89
+ "-c:a",
90
+ "libopus",
91
+ outName
92
+ ];
93
+ await ffmpeg.exec(args);
94
+ const outData = await ffmpeg.readFile(outName);
95
+ const outBytes = outData instanceof Uint8Array ? outData : new Uint8Array(outData);
96
+ const outBlob = new Blob([outBytes], { type: "video/webm" });
97
+ const file = opts.returnFile ? new File([outBlob], defaultVideoName(input, opts.fileName), { type: outBlob.type }) : void 0;
98
+ return { blob: outBlob, file, isWebm: true };
99
+ }
100
+ async function detectKindBrowser(input) {
101
+ if (typeof input !== "string") {
102
+ const t = (input.type || "").toLowerCase();
103
+ if (t.startsWith("image/")) return "image";
104
+ if (t.startsWith("video/")) return "video";
105
+ }
106
+ const blob = await inputToBlob(input);
107
+ const buf = new Uint8Array(await blob.slice(0, 4100).arrayBuffer());
108
+ const ft = await fileType.fileTypeFromBuffer(buf);
109
+ if (!ft) return "unknown";
110
+ if (ft.mime.startsWith("image/")) return "image";
111
+ if (ft.mime.startsWith("video/")) return "video";
112
+ return "unknown";
113
+ }
114
+ async function convert(input, options = {}) {
115
+ const kind = await detectKindBrowser(input);
116
+ const returnFile = options.returnFile ?? false;
117
+ if (kind === "image") {
118
+ const res = await imageToWebp(input, { ...options.image, returnFile, fileName: options.fileName });
119
+ return { kind: "image", ...res };
120
+ }
121
+ if (kind === "video") {
122
+ const res = await videoToWebm(input, { ...options.video, returnFile, fileName: options.fileName });
123
+ return { kind: "video", ...res };
124
+ }
125
+ throw new Error("Unsupported input type. Expected an image/* or video/* asset.");
126
+ }
127
+ function computeTargetSize(srcW, srcH, maxW, maxH) {
128
+ if (srcW <= 0 || srcH <= 0) return { width: srcW, height: srcH };
129
+ const scale = Math.min(1, maxW / srcW, maxH / srcH);
130
+ return {
131
+ width: Math.max(1, Math.round(srcW * scale)),
132
+ height: Math.max(1, Math.round(srcH * scale))
133
+ };
134
+ }
135
+ async function decodeImage(blob) {
136
+ if (typeof createImageBitmap === "function") {
137
+ const bitmap = await createImageBitmap(blob, { imageOrientation: "from-image" });
138
+ return { bitmap, width: bitmap.width, height: bitmap.height };
139
+ }
140
+ const url = URL.createObjectURL(blob);
141
+ try {
142
+ const img = await new Promise((resolve, reject) => {
143
+ const el = new Image();
144
+ el.onload = () => resolve(el);
145
+ el.onerror = () => reject(new Error("Failed to decode image"));
146
+ el.src = url;
147
+ });
148
+ const canvas = document.createElement("canvas");
149
+ canvas.width = img.naturalWidth || img.width;
150
+ canvas.height = img.naturalHeight || img.height;
151
+ const ctx = canvas.getContext("2d");
152
+ if (!ctx) throw new Error("Canvas 2D context not available");
153
+ ctx.drawImage(img, 0, 0);
154
+ const dataUrl = canvas.toDataURL("image/png");
155
+ const pngBlob = await (await fetch(dataUrl)).blob();
156
+ const bitmap = await createImageBitmap(pngBlob);
157
+ return { bitmap, width: bitmap.width, height: bitmap.height };
158
+ } finally {
159
+ URL.revokeObjectURL(url);
160
+ }
161
+ }
162
+ async function encodeWebpFromBitmap(bitmap, width, height, quality) {
163
+ const q = clamp01(quality);
164
+ const hasOffscreen = typeof OffscreenCanvas !== "undefined";
165
+ if (hasOffscreen) {
166
+ const canvas2 = new OffscreenCanvas(width, height);
167
+ const ctx2 = canvas2.getContext("2d");
168
+ if (!ctx2) return null;
169
+ ctx2.drawImage(bitmap, 0, 0, width, height);
170
+ try {
171
+ return await canvas2.convertToBlob({ type: "image/webp", quality: q });
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+ const canvas = document.createElement("canvas");
177
+ canvas.width = width;
178
+ canvas.height = height;
179
+ const ctx = canvas.getContext("2d");
180
+ if (!ctx) return null;
181
+ ctx.drawImage(bitmap, 0, 0, width, height);
182
+ const blob = await new Promise((resolve) => {
183
+ canvas.toBlob((b) => resolve(b), "image/webp", q);
184
+ });
185
+ return blob;
186
+ }
187
+ async function imageToWebp(input, options = {}) {
188
+ const opts = { ...DEFAULTS, ...options };
189
+ const blob = await inputToBlob(input);
190
+ const { bitmap, width: srcW, height: srcH } = await decodeImage(blob);
191
+ try {
192
+ const { width, height } = computeTargetSize(srcW, srcH, opts.maxWidth, opts.maxHeight);
193
+ if (!opts.targetBytes || opts.targetBytes <= 0) {
194
+ const out = await encodeWebpFromBitmap(bitmap, width, height, opts.maxQuality);
195
+ if (!out) {
196
+ if (!opts.force) return { blob, width: srcW, height: srcH, quality: 1, isWebp: false };
197
+ return { blob, width: srcW, height: srcH, quality: 1, isWebp: false };
198
+ }
199
+ const fileName2 = opts.fileName ?? getInputName(input);
200
+ const file2 = opts.returnFile ? new File([out], fileName2, { type: out.type }) : void 0;
201
+ return { blob: out, file: file2, width, height, quality: opts.maxQuality, isWebp: true };
202
+ }
203
+ const maxQ = clamp01(opts.maxQuality);
204
+ const minQ = clamp01(Math.min(opts.minQuality, maxQ));
205
+ const atMax = await encodeWebpFromBitmap(bitmap, width, height, maxQ);
206
+ if (!atMax) {
207
+ if (!opts.force) return { blob, width: srcW, height: srcH, quality: 1, isWebp: false };
208
+ return { blob, width: srcW, height: srcH, quality: 1, isWebp: false };
209
+ }
210
+ if (atMax.size <= opts.targetBytes) {
211
+ const fileName2 = opts.fileName ?? getInputName(input);
212
+ const file2 = opts.returnFile ? new File([atMax], fileName2, { type: atMax.type }) : void 0;
213
+ return { blob: atMax, file: file2, width, height, quality: maxQ, isWebp: true };
214
+ }
215
+ const atMin = await encodeWebpFromBitmap(bitmap, width, height, minQ);
216
+ if (!atMin) {
217
+ const fileName2 = opts.fileName ?? getInputName(input);
218
+ const file2 = opts.returnFile ? new File([atMax], fileName2, { type: atMax.type }) : void 0;
219
+ return { blob: atMax, file: file2, width, height, quality: maxQ, isWebp: true };
220
+ }
221
+ if (atMin.size > opts.targetBytes) {
222
+ const fileName2 = opts.fileName ?? getInputName(input);
223
+ const file2 = opts.returnFile ? new File([atMin], fileName2, { type: atMin.type }) : void 0;
224
+ return { blob: atMin, file: file2, width, height, quality: minQ, isWebp: true };
225
+ }
226
+ let lo = minQ;
227
+ let hi = maxQ;
228
+ let bestBlob = atMin;
229
+ let bestQ = minQ;
230
+ for (let i = 0; i < 7; i++) {
231
+ const mid = (lo + hi) / 2;
232
+ const enc = await encodeWebpFromBitmap(bitmap, width, height, mid);
233
+ if (!enc) break;
234
+ if (enc.size <= opts.targetBytes) {
235
+ bestBlob = enc;
236
+ bestQ = mid;
237
+ lo = mid;
238
+ } else {
239
+ hi = mid;
240
+ }
241
+ }
242
+ const fileName = opts.fileName ?? getInputName(input);
243
+ const file = opts.returnFile ? new File([bestBlob], fileName, { type: bestBlob.type }) : void 0;
244
+ return { blob: bestBlob, file, width, height, quality: bestQ, isWebp: true };
245
+ } finally {
246
+ bitmap.close();
247
+ }
248
+ }
249
+
250
+ exports.convert = convert;
251
+ exports.imageToWebp = imageToWebp;
252
+ exports.videoToWebm = videoToWebm;
253
+ //# sourceMappingURL=index.cjs.map
254
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/browser/index.ts"],"names":["FFmpeg","fetchFile","fileTypeFromBuffer","canvas","ctx","fileName","file"],"mappings":"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","file":"index.cjs","sourcesContent":["import { FFmpeg } from \"@ffmpeg/ffmpeg\"\r\nimport { fetchFile } from \"@ffmpeg/util\"\r\nimport { fileTypeFromBuffer } from \"file-type\"\r\n\r\nimport type { ConvertResult } from \"../shared/types.js\"\r\n\r\nexport type ImageToWebpInput = Blob | File | string\r\n\r\nexport type ImageToWebpOptions = {\r\n /**\r\n * Hard cap for output dimensions, preserving aspect ratio.\r\n * If the input is larger, it will be downscaled.\r\n */\r\n maxWidth?: number\r\n maxHeight?: number\r\n\r\n /**\r\n * Target maximum output size in bytes.\r\n * If provided, the encoder will try to reach <= targetBytes by reducing quality.\r\n */\r\n targetBytes?: number\r\n\r\n /**\r\n * Quality search range (0..1). Higher is better quality, larger file.\r\n */\r\n maxQuality?: number\r\n minQuality?: number\r\n\r\n /**\r\n * If true, always return WebP even if it can't reach targetBytes.\r\n * If false, will return the original input when WebP isn't supported.\r\n */\r\n force?: boolean\r\n\r\n /**\r\n * Optional output filename (only used when returning a File).\r\n * If omitted and input is a File, the name is derived from it.\r\n */\r\n fileName?: string\r\n\r\n /**\r\n * Return a File instead of a Blob.\r\n */\r\n returnFile?: boolean\r\n}\r\n\r\nexport type ImageToWebpResult = {\r\n blob: Blob\r\n file?: File\r\n width: number\r\n height: number\r\n quality: number\r\n /**\r\n * True if output is `image/webp`.\r\n */\r\n isWebp: boolean\r\n}\r\n\r\nconst DEFAULTS: Required<\r\n Pick<\r\n ImageToWebpOptions,\r\n \"maxWidth\" | \"maxHeight\" | \"maxQuality\" | \"minQuality\" | \"force\" | \"returnFile\"\r\n >\r\n> = {\r\n maxWidth: 2048,\r\n maxHeight: 2048,\r\n maxQuality: 0.82,\r\n minQuality: 0.45,\r\n force: true,\r\n returnFile: false,\r\n}\r\n\r\nfunction clamp01(n: number) {\r\n return Math.max(0, Math.min(1, n))\r\n}\r\n\r\nfunction toWebpName(name: string) {\r\n const base = name.replace(/\\.[^/.]+$/, \"\")\r\n return `${base || \"image\"}.webp`\r\n}\r\n\r\nfunction getInputName(input: ImageToWebpInput) {\r\n if (typeof input === \"string\") return \"image.webp\"\r\n if (input instanceof File && input.name) return toWebpName(input.name)\r\n return \"image.webp\"\r\n}\r\n\r\nasync function inputToBlob(input: ImageToWebpInput): Promise<Blob> {\r\n if (typeof input === \"string\") {\r\n const res = await fetch(input)\r\n if (!res.ok) throw new Error(`Failed to fetch: ${res.status} ${res.statusText}`)\r\n return await res.blob()\r\n }\r\n return input\r\n}\r\n\r\nfunction toWebmName(name: string) {\r\n const base = name.replace(/\\.[^/.]+$/, \"\")\r\n return `${base || \"video\"}.webm`\r\n}\r\n\r\nfunction defaultVideoName(input: Blob | File | string, fileName?: string) {\r\n if (fileName && fileName.trim().length > 0) return fileName\r\n if (typeof input === \"string\") return \"video.webm\"\r\n if (input instanceof File && input.name) return toWebmName(input.name)\r\n return \"video.webm\"\r\n}\r\n\r\nlet ffmpegSingleton: FFmpeg | null = null\r\nlet ffmpegLoadPromise: Promise<FFmpeg> | null = null\r\n\r\nasync function getFfmpeg(): Promise<FFmpeg> {\r\n if (ffmpegSingleton) return ffmpegSingleton\r\n if (ffmpegLoadPromise) return ffmpegLoadPromise\r\n const ff = new FFmpeg()\r\n ffmpegLoadPromise = (async () => {\r\n await ff.load()\r\n ffmpegSingleton = ff\r\n return ff\r\n })()\r\n return ffmpegLoadPromise\r\n}\r\n\r\nexport type VideoToWebmInput = Blob | File | string\r\n\r\nexport type VideoToWebmOptions = {\r\n crf?: number\r\n deadline?: \"good\" | \"best\" | \"realtime\"\r\n maxDurationSeconds?: number\r\n fileName?: string\r\n returnFile?: boolean\r\n}\r\n\r\nexport type VideoToWebmResult = {\r\n blob: Blob\r\n file?: File\r\n isWebm: boolean\r\n}\r\n\r\nconst VIDEO_DEFAULTS: Required<Pick<VideoToWebmOptions, \"crf\" | \"deadline\" | \"returnFile\">> = {\r\n crf: 32,\r\n deadline: \"good\",\r\n returnFile: false,\r\n}\r\n\r\nexport async function videoToWebm(\r\n input: VideoToWebmInput,\r\n options: VideoToWebmOptions = {}\r\n): Promise<VideoToWebmResult> {\r\n const opts = { ...VIDEO_DEFAULTS, ...options }\r\n const blob = await inputToBlob(input)\r\n const ffmpeg = await getFfmpeg()\r\n\r\n const inName = \"input\"\r\n const outName = \"output.webm\"\r\n\r\n await ffmpeg.writeFile(inName, await fetchFile(blob))\r\n\r\n const args: string[] = [\r\n ...(opts.maxDurationSeconds && opts.maxDurationSeconds > 0 ? [\"-t\", String(opts.maxDurationSeconds)] : []),\r\n \"-i\",\r\n inName,\r\n \"-map\",\r\n \"0:v:0\",\r\n \"-map\",\r\n \"0:a?\",\r\n \"-c:v\",\r\n \"libvpx-vp9\",\r\n \"-b:v\",\r\n \"0\",\r\n \"-crf\",\r\n String(opts.crf),\r\n \"-deadline\",\r\n opts.deadline,\r\n \"-row-mt\",\r\n \"1\",\r\n \"-c:a\",\r\n \"libopus\",\r\n outName,\r\n ]\r\n\r\n await ffmpeg.exec(args)\r\n const outData = await ffmpeg.readFile(outName)\r\n const outBytes = outData instanceof Uint8Array ? outData : new Uint8Array(outData as any)\r\n\r\n const outBlob = new Blob([outBytes], { type: \"video/webm\" })\r\n const file = opts.returnFile ? new File([outBlob], defaultVideoName(input, opts.fileName), { type: outBlob.type }) : undefined\r\n return { blob: outBlob, file, isWebm: true }\r\n}\r\n\r\nexport type ConvertInput = ImageToWebpInput\r\n\r\nexport type ConvertOptions = {\r\n image?: ImageToWebpOptions\r\n video?: VideoToWebmOptions\r\n returnFile?: boolean\r\n fileName?: string\r\n}\r\n\r\nasync function detectKindBrowser(input: ConvertInput): Promise<\"image\" | \"video\" | \"unknown\"> {\r\n if (typeof input !== \"string\") {\r\n const t = (input.type || \"\").toLowerCase()\r\n if (t.startsWith(\"image/\")) return \"image\"\r\n if (t.startsWith(\"video/\")) return \"video\"\r\n }\r\n\r\n const blob = await inputToBlob(input)\r\n const buf = new Uint8Array(await blob.slice(0, 4100).arrayBuffer())\r\n const ft = await fileTypeFromBuffer(buf)\r\n if (!ft) return \"unknown\"\r\n if (ft.mime.startsWith(\"image/\")) return \"image\"\r\n if (ft.mime.startsWith(\"video/\")) return \"video\"\r\n return \"unknown\"\r\n}\r\n\r\nexport async function convert(input: ConvertInput, options: ConvertOptions = {}): Promise<ConvertResult> {\r\n const kind = await detectKindBrowser(input)\r\n const returnFile = options.returnFile ?? false\r\n\r\n if (kind === \"image\") {\r\n const res = await imageToWebp(input, { ...options.image, returnFile, fileName: options.fileName })\r\n return { kind: \"image\", ...res }\r\n }\r\n\r\n if (kind === \"video\") {\r\n const res = await videoToWebm(input, { ...options.video, returnFile, fileName: options.fileName })\r\n return { kind: \"video\", ...res }\r\n }\r\n\r\n throw new Error(\"Unsupported input type. Expected an image/* or video/* asset.\")\r\n}\r\n\r\nfunction computeTargetSize(\r\n srcW: number,\r\n srcH: number,\r\n maxW: number,\r\n maxH: number\r\n): { width: number; height: number } {\r\n if (srcW <= 0 || srcH <= 0) return { width: srcW, height: srcH }\r\n const scale = Math.min(1, maxW / srcW, maxH / srcH)\r\n return {\r\n width: Math.max(1, Math.round(srcW * scale)),\r\n height: Math.max(1, Math.round(srcH * scale)),\r\n }\r\n}\r\n\r\nasync function decodeImage(blob: Blob): Promise<{ bitmap: ImageBitmap; width: number; height: number }> {\r\n if (typeof createImageBitmap === \"function\") {\r\n const bitmap = await createImageBitmap(blob, { imageOrientation: \"from-image\" as any })\r\n return { bitmap, width: bitmap.width, height: bitmap.height }\r\n }\r\n\r\n const url = URL.createObjectURL(blob)\r\n try {\r\n const img = await new Promise<HTMLImageElement>((resolve, reject) => {\r\n const el = new Image()\r\n el.onload = () => resolve(el)\r\n el.onerror = () => reject(new Error(\"Failed to decode image\"))\r\n el.src = url\r\n })\r\n\r\n const canvas = document.createElement(\"canvas\")\r\n canvas.width = img.naturalWidth || img.width\r\n canvas.height = img.naturalHeight || img.height\r\n const ctx = canvas.getContext(\"2d\")\r\n if (!ctx) throw new Error(\"Canvas 2D context not available\")\r\n ctx.drawImage(img, 0, 0)\r\n\r\n const dataUrl = canvas.toDataURL(\"image/png\")\r\n const pngBlob = await (await fetch(dataUrl)).blob()\r\n const bitmap = await createImageBitmap(pngBlob)\r\n return { bitmap, width: bitmap.width, height: bitmap.height }\r\n } finally {\r\n URL.revokeObjectURL(url)\r\n }\r\n}\r\n\r\nasync function encodeWebpFromBitmap(\r\n bitmap: ImageBitmap,\r\n width: number,\r\n height: number,\r\n quality: number\r\n): Promise<Blob | null> {\r\n const q = clamp01(quality)\r\n\r\n const hasOffscreen = typeof OffscreenCanvas !== \"undefined\"\r\n if (hasOffscreen) {\r\n const canvas = new OffscreenCanvas(width, height)\r\n const ctx = canvas.getContext(\"2d\")\r\n if (!ctx) return null\r\n ctx.drawImage(bitmap, 0, 0, width, height)\r\n try {\r\n return await canvas.convertToBlob({ type: \"image/webp\", quality: q })\r\n } catch {\r\n return null\r\n }\r\n }\r\n\r\n const canvas = document.createElement(\"canvas\")\r\n canvas.width = width\r\n canvas.height = height\r\n const ctx = canvas.getContext(\"2d\")\r\n if (!ctx) return null\r\n ctx.drawImage(bitmap, 0, 0, width, height)\r\n\r\n const blob = await new Promise<Blob | null>((resolve) => {\r\n canvas.toBlob((b) => resolve(b), \"image/webp\", q)\r\n })\r\n return blob\r\n}\r\n\r\n/**\r\n * Convert an image (png/jpg/etc) to WebP and shrink size while keeping similar quality.\r\n *\r\n * - Downscales to `maxWidth`/`maxHeight` (keeps aspect ratio).\r\n * - Encodes as WebP with a quality search to try meeting `targetBytes` (if provided).\r\n */\r\nexport async function imageToWebp(\r\n input: ImageToWebpInput,\r\n options: ImageToWebpOptions = {}\r\n): Promise<ImageToWebpResult> {\r\n const opts = { ...DEFAULTS, ...options }\r\n const blob = await inputToBlob(input)\r\n\r\n const { bitmap, width: srcW, height: srcH } = await decodeImage(blob)\r\n try {\r\n const { width, height } = computeTargetSize(srcW, srcH, opts.maxWidth, opts.maxHeight)\r\n\r\n if (!opts.targetBytes || opts.targetBytes <= 0) {\r\n const out = await encodeWebpFromBitmap(bitmap, width, height, opts.maxQuality)\r\n if (!out) {\r\n if (!opts.force) return { blob, width: srcW, height: srcH, quality: 1, isWebp: false }\r\n return { blob, width: srcW, height: srcH, quality: 1, isWebp: false }\r\n }\r\n\r\n const fileName = opts.fileName ?? getInputName(input)\r\n const file = opts.returnFile ? new File([out], fileName, { type: out.type }) : undefined\r\n return { blob: out, file, width, height, quality: opts.maxQuality, isWebp: true }\r\n }\r\n\r\n const maxQ = clamp01(opts.maxQuality)\r\n const minQ = clamp01(Math.min(opts.minQuality, maxQ))\r\n\r\n const atMax = await encodeWebpFromBitmap(bitmap, width, height, maxQ)\r\n if (!atMax) {\r\n if (!opts.force) return { blob, width: srcW, height: srcH, quality: 1, isWebp: false }\r\n return { blob, width: srcW, height: srcH, quality: 1, isWebp: false }\r\n }\r\n if (atMax.size <= opts.targetBytes) {\r\n const fileName = opts.fileName ?? getInputName(input)\r\n const file = opts.returnFile ? new File([atMax], fileName, { type: atMax.type }) : undefined\r\n return { blob: atMax, file, width, height, quality: maxQ, isWebp: true }\r\n }\r\n\r\n const atMin = await encodeWebpFromBitmap(bitmap, width, height, minQ)\r\n if (!atMin) {\r\n const fileName = opts.fileName ?? getInputName(input)\r\n const file = opts.returnFile ? new File([atMax], fileName, { type: atMax.type }) : undefined\r\n return { blob: atMax, file, width, height, quality: maxQ, isWebp: true }\r\n }\r\n if (atMin.size > opts.targetBytes) {\r\n const fileName = opts.fileName ?? getInputName(input)\r\n const file = opts.returnFile ? new File([atMin], fileName, { type: atMin.type }) : undefined\r\n return { blob: atMin, file, width, height, quality: minQ, isWebp: true }\r\n }\r\n\r\n let lo = minQ\r\n let hi = maxQ\r\n let bestBlob: Blob = atMin\r\n let bestQ = minQ\r\n\r\n for (let i = 0; i < 7; i++) {\r\n const mid = (lo + hi) / 2\r\n const enc = await encodeWebpFromBitmap(bitmap, width, height, mid)\r\n if (!enc) break\r\n\r\n if (enc.size <= opts.targetBytes) {\r\n bestBlob = enc\r\n bestQ = mid\r\n lo = mid\r\n } else {\r\n hi = mid\r\n }\r\n }\r\n\r\n const fileName = opts.fileName ?? getInputName(input)\r\n const file = opts.returnFile ? new File([bestBlob], fileName, { type: bestBlob.type }) : undefined\r\n return { blob: bestBlob, file, width, height, quality: bestQ, isWebp: true }\r\n } finally {\r\n bitmap.close()\r\n }\r\n}\r\n\r\n"]}
@@ -0,0 +1,94 @@
1
+ type ConvertKind = "image" | "video";
2
+ type ConvertResultCommon = {
3
+ kind: ConvertKind;
4
+ blob: Blob;
5
+ file?: File;
6
+ };
7
+ type ConvertImageResult = ConvertResultCommon & {
8
+ kind: "image";
9
+ width: number;
10
+ height: number;
11
+ quality: number;
12
+ isWebp: boolean;
13
+ };
14
+ type ConvertVideoResult = ConvertResultCommon & {
15
+ kind: "video";
16
+ isWebm: boolean;
17
+ };
18
+ type ConvertResult = ConvertImageResult | ConvertVideoResult;
19
+
20
+ type ImageToWebpInput = Blob | File | string;
21
+ type ImageToWebpOptions = {
22
+ /**
23
+ * Hard cap for output dimensions, preserving aspect ratio.
24
+ * If the input is larger, it will be downscaled.
25
+ */
26
+ maxWidth?: number;
27
+ maxHeight?: number;
28
+ /**
29
+ * Target maximum output size in bytes.
30
+ * If provided, the encoder will try to reach <= targetBytes by reducing quality.
31
+ */
32
+ targetBytes?: number;
33
+ /**
34
+ * Quality search range (0..1). Higher is better quality, larger file.
35
+ */
36
+ maxQuality?: number;
37
+ minQuality?: number;
38
+ /**
39
+ * If true, always return WebP even if it can't reach targetBytes.
40
+ * If false, will return the original input when WebP isn't supported.
41
+ */
42
+ force?: boolean;
43
+ /**
44
+ * Optional output filename (only used when returning a File).
45
+ * If omitted and input is a File, the name is derived from it.
46
+ */
47
+ fileName?: string;
48
+ /**
49
+ * Return a File instead of a Blob.
50
+ */
51
+ returnFile?: boolean;
52
+ };
53
+ type ImageToWebpResult = {
54
+ blob: Blob;
55
+ file?: File;
56
+ width: number;
57
+ height: number;
58
+ quality: number;
59
+ /**
60
+ * True if output is `image/webp`.
61
+ */
62
+ isWebp: boolean;
63
+ };
64
+ type VideoToWebmInput = Blob | File | string;
65
+ type VideoToWebmOptions = {
66
+ crf?: number;
67
+ deadline?: "good" | "best" | "realtime";
68
+ maxDurationSeconds?: number;
69
+ fileName?: string;
70
+ returnFile?: boolean;
71
+ };
72
+ type VideoToWebmResult = {
73
+ blob: Blob;
74
+ file?: File;
75
+ isWebm: boolean;
76
+ };
77
+ declare function videoToWebm(input: VideoToWebmInput, options?: VideoToWebmOptions): Promise<VideoToWebmResult>;
78
+ type ConvertInput = ImageToWebpInput;
79
+ type ConvertOptions = {
80
+ image?: ImageToWebpOptions;
81
+ video?: VideoToWebmOptions;
82
+ returnFile?: boolean;
83
+ fileName?: string;
84
+ };
85
+ declare function convert(input: ConvertInput, options?: ConvertOptions): Promise<ConvertResult>;
86
+ /**
87
+ * Convert an image (png/jpg/etc) to WebP and shrink size while keeping similar quality.
88
+ *
89
+ * - Downscales to `maxWidth`/`maxHeight` (keeps aspect ratio).
90
+ * - Encodes as WebP with a quality search to try meeting `targetBytes` (if provided).
91
+ */
92
+ declare function imageToWebp(input: ImageToWebpInput, options?: ImageToWebpOptions): Promise<ImageToWebpResult>;
93
+
94
+ export { type ConvertInput, type ConvertOptions, type ImageToWebpInput, type ImageToWebpOptions, type ImageToWebpResult, type VideoToWebmInput, type VideoToWebmOptions, type VideoToWebmResult, convert, imageToWebp, videoToWebm };
@@ -0,0 +1,94 @@
1
+ type ConvertKind = "image" | "video";
2
+ type ConvertResultCommon = {
3
+ kind: ConvertKind;
4
+ blob: Blob;
5
+ file?: File;
6
+ };
7
+ type ConvertImageResult = ConvertResultCommon & {
8
+ kind: "image";
9
+ width: number;
10
+ height: number;
11
+ quality: number;
12
+ isWebp: boolean;
13
+ };
14
+ type ConvertVideoResult = ConvertResultCommon & {
15
+ kind: "video";
16
+ isWebm: boolean;
17
+ };
18
+ type ConvertResult = ConvertImageResult | ConvertVideoResult;
19
+
20
+ type ImageToWebpInput = Blob | File | string;
21
+ type ImageToWebpOptions = {
22
+ /**
23
+ * Hard cap for output dimensions, preserving aspect ratio.
24
+ * If the input is larger, it will be downscaled.
25
+ */
26
+ maxWidth?: number;
27
+ maxHeight?: number;
28
+ /**
29
+ * Target maximum output size in bytes.
30
+ * If provided, the encoder will try to reach <= targetBytes by reducing quality.
31
+ */
32
+ targetBytes?: number;
33
+ /**
34
+ * Quality search range (0..1). Higher is better quality, larger file.
35
+ */
36
+ maxQuality?: number;
37
+ minQuality?: number;
38
+ /**
39
+ * If true, always return WebP even if it can't reach targetBytes.
40
+ * If false, will return the original input when WebP isn't supported.
41
+ */
42
+ force?: boolean;
43
+ /**
44
+ * Optional output filename (only used when returning a File).
45
+ * If omitted and input is a File, the name is derived from it.
46
+ */
47
+ fileName?: string;
48
+ /**
49
+ * Return a File instead of a Blob.
50
+ */
51
+ returnFile?: boolean;
52
+ };
53
+ type ImageToWebpResult = {
54
+ blob: Blob;
55
+ file?: File;
56
+ width: number;
57
+ height: number;
58
+ quality: number;
59
+ /**
60
+ * True if output is `image/webp`.
61
+ */
62
+ isWebp: boolean;
63
+ };
64
+ type VideoToWebmInput = Blob | File | string;
65
+ type VideoToWebmOptions = {
66
+ crf?: number;
67
+ deadline?: "good" | "best" | "realtime";
68
+ maxDurationSeconds?: number;
69
+ fileName?: string;
70
+ returnFile?: boolean;
71
+ };
72
+ type VideoToWebmResult = {
73
+ blob: Blob;
74
+ file?: File;
75
+ isWebm: boolean;
76
+ };
77
+ declare function videoToWebm(input: VideoToWebmInput, options?: VideoToWebmOptions): Promise<VideoToWebmResult>;
78
+ type ConvertInput = ImageToWebpInput;
79
+ type ConvertOptions = {
80
+ image?: ImageToWebpOptions;
81
+ video?: VideoToWebmOptions;
82
+ returnFile?: boolean;
83
+ fileName?: string;
84
+ };
85
+ declare function convert(input: ConvertInput, options?: ConvertOptions): Promise<ConvertResult>;
86
+ /**
87
+ * Convert an image (png/jpg/etc) to WebP and shrink size while keeping similar quality.
88
+ *
89
+ * - Downscales to `maxWidth`/`maxHeight` (keeps aspect ratio).
90
+ * - Encodes as WebP with a quality search to try meeting `targetBytes` (if provided).
91
+ */
92
+ declare function imageToWebp(input: ImageToWebpInput, options?: ImageToWebpOptions): Promise<ImageToWebpResult>;
93
+
94
+ export { type ConvertInput, type ConvertOptions, type ImageToWebpInput, type ImageToWebpOptions, type ImageToWebpResult, type VideoToWebmInput, type VideoToWebmOptions, type VideoToWebmResult, convert, imageToWebp, videoToWebm };