cross-image 0.2.4 → 0.4.1
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 +21 -21
- package/README.md +615 -333
- package/esm/mod.d.ts +6 -4
- package/esm/mod.js +4 -2
- package/esm/src/formats/apng.d.ts +7 -5
- package/esm/src/formats/apng.js +15 -10
- package/esm/src/formats/ascii.d.ts +3 -3
- package/esm/src/formats/ascii.js +1 -1
- package/esm/src/formats/avif.d.ts +3 -3
- package/esm/src/formats/avif.js +17 -7
- package/esm/src/formats/bmp.d.ts +3 -3
- package/esm/src/formats/bmp.js +2 -2
- package/esm/src/formats/dng.d.ts +1 -1
- package/esm/src/formats/dng.js +1 -1
- package/esm/src/formats/gif.d.ts +5 -5
- package/esm/src/formats/gif.js +17 -13
- package/esm/src/formats/heic.d.ts +3 -3
- package/esm/src/formats/heic.js +17 -7
- package/esm/src/formats/ico.d.ts +3 -3
- package/esm/src/formats/ico.js +4 -4
- package/esm/src/formats/jpeg.d.ts +3 -3
- package/esm/src/formats/jpeg.js +23 -11
- package/esm/src/formats/pam.d.ts +3 -3
- package/esm/src/formats/pam.js +2 -2
- package/esm/src/formats/pcx.d.ts +3 -3
- package/esm/src/formats/pcx.js +2 -2
- package/esm/src/formats/png.d.ts +4 -3
- package/esm/src/formats/png.js +9 -3
- package/esm/src/formats/png_base.d.ts +42 -1
- package/esm/src/formats/png_base.js +200 -10
- package/esm/src/formats/ppm.d.ts +3 -3
- package/esm/src/formats/ppm.js +2 -2
- package/esm/src/formats/tiff.d.ts +7 -18
- package/esm/src/formats/tiff.js +162 -27
- package/esm/src/formats/webp.d.ts +3 -3
- package/esm/src/formats/webp.js +11 -8
- package/esm/src/image.d.ts +26 -3
- package/esm/src/image.js +66 -22
- package/esm/src/types.d.ts +122 -4
- package/esm/src/utils/base64.d.ts +32 -0
- package/esm/src/utils/base64.js +173 -0
- package/esm/src/utils/gif_decoder.d.ts +4 -1
- package/esm/src/utils/gif_decoder.js +91 -65
- package/esm/src/utils/gif_encoder.d.ts +3 -1
- package/esm/src/utils/gif_encoder.js +4 -2
- package/esm/src/utils/image_processing.d.ts +31 -0
- package/esm/src/utils/image_processing.js +232 -70
- package/esm/src/utils/jpeg_decoder.d.ts +17 -4
- package/esm/src/utils/jpeg_decoder.js +448 -83
- package/esm/src/utils/jpeg_encoder.d.ts +15 -1
- package/esm/src/utils/jpeg_encoder.js +263 -24
- package/esm/src/utils/resize.js +51 -20
- package/esm/src/utils/tiff_deflate.d.ts +18 -0
- package/esm/src/utils/tiff_deflate.js +27 -0
- package/esm/src/utils/tiff_packbits.d.ts +24 -0
- package/esm/src/utils/tiff_packbits.js +90 -0
- package/esm/src/utils/webp_decoder.d.ts +3 -1
- package/esm/src/utils/webp_decoder.js +144 -63
- package/esm/src/utils/webp_encoder.js +5 -11
- package/package.json +1 -1
- package/script/mod.d.ts +6 -4
- package/script/mod.js +13 -3
- package/script/src/formats/apng.d.ts +7 -5
- package/script/src/formats/apng.js +15 -10
- package/script/src/formats/ascii.d.ts +3 -3
- package/script/src/formats/ascii.js +1 -1
- package/script/src/formats/avif.d.ts +3 -3
- package/script/src/formats/avif.js +17 -7
- package/script/src/formats/bmp.d.ts +3 -3
- package/script/src/formats/bmp.js +2 -2
- package/script/src/formats/dng.d.ts +1 -1
- package/script/src/formats/dng.js +1 -1
- package/script/src/formats/gif.d.ts +5 -5
- package/script/src/formats/gif.js +17 -13
- package/script/src/formats/heic.d.ts +3 -3
- package/script/src/formats/heic.js +17 -7
- package/script/src/formats/ico.d.ts +3 -3
- package/script/src/formats/ico.js +4 -4
- package/script/src/formats/jpeg.d.ts +3 -3
- package/script/src/formats/jpeg.js +23 -11
- package/script/src/formats/pam.d.ts +3 -3
- package/script/src/formats/pam.js +2 -2
- package/script/src/formats/pcx.d.ts +3 -3
- package/script/src/formats/pcx.js +2 -2
- package/script/src/formats/png.d.ts +4 -3
- package/script/src/formats/png.js +9 -3
- package/script/src/formats/png_base.d.ts +42 -1
- package/script/src/formats/png_base.js +200 -10
- package/script/src/formats/ppm.d.ts +3 -3
- package/script/src/formats/ppm.js +2 -2
- package/script/src/formats/tiff.d.ts +7 -18
- package/script/src/formats/tiff.js +162 -27
- package/script/src/formats/webp.d.ts +3 -3
- package/script/src/formats/webp.js +11 -8
- package/script/src/image.d.ts +26 -3
- package/script/src/image.js +64 -20
- package/script/src/types.d.ts +122 -4
- package/script/src/utils/base64.d.ts +32 -0
- package/script/src/utils/base64.js +179 -0
- package/script/src/utils/gif_decoder.d.ts +4 -1
- package/script/src/utils/gif_decoder.js +91 -65
- package/script/src/utils/gif_encoder.d.ts +3 -1
- package/script/src/utils/gif_encoder.js +4 -2
- package/script/src/utils/image_processing.d.ts +31 -0
- package/script/src/utils/image_processing.js +236 -70
- package/script/src/utils/jpeg_decoder.d.ts +17 -4
- package/script/src/utils/jpeg_decoder.js +448 -83
- package/script/src/utils/jpeg_encoder.d.ts +15 -1
- package/script/src/utils/jpeg_encoder.js +263 -24
- package/script/src/utils/resize.js +51 -20
- package/script/src/utils/tiff_deflate.d.ts +18 -0
- package/script/src/utils/tiff_deflate.js +31 -0
- package/script/src/utils/tiff_packbits.d.ts +24 -0
- package/script/src/utils/tiff_packbits.js +94 -0
- package/script/src/utils/webp_decoder.d.ts +3 -1
- package/script/src/utils/webp_decoder.js +144 -63
- package/script/src/utils/webp_encoder.js +5 -11
package/script/src/types.d.ts
CHANGED
|
@@ -135,10 +135,46 @@ export interface ResizeOptions {
|
|
|
135
135
|
*/
|
|
136
136
|
fit?: "stretch" | "fit" | "fill" | "cover" | "contain";
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Options for PNG encoding.
|
|
140
|
+
*/
|
|
141
|
+
export interface PNGEncoderOptions {
|
|
142
|
+
/**
|
|
143
|
+
* Compression level (0-9)
|
|
144
|
+
* - 0-2: No filtering (fastest)
|
|
145
|
+
* - 3-6: Sub filter (balanced, default is 6)
|
|
146
|
+
* - 7-9: Adaptive filtering per scanline (best compression)
|
|
147
|
+
*
|
|
148
|
+
* Default: 6 (balanced)
|
|
149
|
+
*
|
|
150
|
+
* Note: Affects PNG filter selection. The native deflate compression
|
|
151
|
+
* is used regardless of level.
|
|
152
|
+
*/
|
|
153
|
+
compressionLevel?: number;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Options for APNG (Animated PNG) encoding.
|
|
157
|
+
*
|
|
158
|
+
* APNG uses PNG encoding for each frame.
|
|
159
|
+
*/
|
|
160
|
+
export interface APNGEncoderOptions extends PNGEncoderOptions {
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Options for GIF encoding.
|
|
164
|
+
*/
|
|
165
|
+
export interface GIFEncoderOptions {
|
|
166
|
+
/**
|
|
167
|
+
* Loop count for animated GIFs.
|
|
168
|
+
* - 0 (default): Loop infinitely
|
|
169
|
+
* - 1+: Loop a specific number of times
|
|
170
|
+
* - undefined or not set: Loop infinitely (same as 0)
|
|
171
|
+
*/
|
|
172
|
+
loop?: number;
|
|
173
|
+
}
|
|
138
174
|
/**
|
|
139
175
|
* Options for ASCII art encoding
|
|
140
176
|
*/
|
|
141
|
-
export interface
|
|
177
|
+
export interface ASCIIEncoderOptions {
|
|
142
178
|
/** Target width in characters (default: 80) */
|
|
143
179
|
width?: number;
|
|
144
180
|
/** Character set to use (default: "simple") */
|
|
@@ -148,10 +184,23 @@ export interface ASCIIOptions {
|
|
|
148
184
|
/** Whether to invert brightness (default: false) */
|
|
149
185
|
invert?: boolean;
|
|
150
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Options for TIFF encoding.
|
|
189
|
+
*/
|
|
190
|
+
export interface TIFFEncoderOptions {
|
|
191
|
+
/** Compression method: "none" (default), "lzw", "packbits", or "deflate" */
|
|
192
|
+
compression?: "none" | "lzw" | "packbits" | "deflate";
|
|
193
|
+
/** Encode as grayscale instead of RGB/RGBA (default: false) */
|
|
194
|
+
grayscale?: boolean;
|
|
195
|
+
/** Encode as RGB without alpha channel (default: false, ignored if grayscale is true) */
|
|
196
|
+
rgb?: boolean;
|
|
197
|
+
/** Encode as CMYK color space (default: false, ignored if grayscale is true) */
|
|
198
|
+
cmyk?: boolean;
|
|
199
|
+
}
|
|
151
200
|
/**
|
|
152
201
|
* Options for WebP encoding
|
|
153
202
|
*/
|
|
154
|
-
export interface
|
|
203
|
+
export interface WebPEncoderOptions {
|
|
155
204
|
/**
|
|
156
205
|
* Encoding quality (1-100, default: 90)
|
|
157
206
|
* - 100 = lossless (VP8L)
|
|
@@ -164,6 +213,75 @@ export interface WebPEncodeOptions {
|
|
|
164
213
|
*/
|
|
165
214
|
lossless?: boolean;
|
|
166
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Options for JPEG encoding.
|
|
218
|
+
*/
|
|
219
|
+
export interface JPEGEncoderOptions {
|
|
220
|
+
/**
|
|
221
|
+
* Encoding quality (1-100, default depends on encoder backend).
|
|
222
|
+
*/
|
|
223
|
+
quality?: number;
|
|
224
|
+
/**
|
|
225
|
+
* Enable progressive JPEG output when using the pure-JS encoder.
|
|
226
|
+
* Runtime encoders do not currently expose a progressive toggle.
|
|
227
|
+
*/
|
|
228
|
+
progressive?: boolean;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Options for AVIF encoding.
|
|
232
|
+
*
|
|
233
|
+
* Note: AVIF encoding is currently delegated to runtime APIs (OffscreenCanvas).
|
|
234
|
+
* Many runtimes ignore `quality` for AVIF, or may not support AVIF encoding at all.
|
|
235
|
+
*/
|
|
236
|
+
export interface AVIFEncoderOptions {
|
|
237
|
+
/**
|
|
238
|
+
* Best-effort encoding quality.
|
|
239
|
+
*
|
|
240
|
+
* Accepts either 0-1 (canvas-style) or 1-100 (library-style).
|
|
241
|
+
*/
|
|
242
|
+
quality?: number;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Options for HEIC encoding.
|
|
246
|
+
*
|
|
247
|
+
* Note: HEIC encoding is currently delegated to runtime APIs (OffscreenCanvas).
|
|
248
|
+
* Many runtimes do not support HEIC encoding.
|
|
249
|
+
*/
|
|
250
|
+
export interface HEICEncoderOptions {
|
|
251
|
+
/**
|
|
252
|
+
* Best-effort encoding quality.
|
|
253
|
+
*
|
|
254
|
+
* Accepts either 0-1 (canvas-style) or 1-100 (library-style).
|
|
255
|
+
*/
|
|
256
|
+
quality?: number;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Common options for decode APIs.
|
|
260
|
+
*
|
|
261
|
+
* These options are runtime-agnostic and can be passed to `Image.decode()` and
|
|
262
|
+
* `Image.decodeFrames()`.
|
|
263
|
+
*/
|
|
264
|
+
export interface ImageDecoderOptions {
|
|
265
|
+
/**
|
|
266
|
+
* Controls tolerant decoding in the pure-JS decoders.
|
|
267
|
+
*
|
|
268
|
+
* - true (default): try to recover from corruption and continue
|
|
269
|
+
* - false: strict mode (fail fast)
|
|
270
|
+
*/
|
|
271
|
+
tolerantDecoding?: boolean;
|
|
272
|
+
/**
|
|
273
|
+
* Optional warning callback used by pure-JS decoders when non-fatal issues are
|
|
274
|
+
* encountered.
|
|
275
|
+
*/
|
|
276
|
+
onWarning?: (message: string, details?: unknown) => void;
|
|
277
|
+
/**
|
|
278
|
+
* Runtime decoder strategy.
|
|
279
|
+
*
|
|
280
|
+
* - "prefer" (default): try runtime decoders first (ImageDecoder/Canvas), then fall back to pure JS
|
|
281
|
+
* - "never": skip runtime decoders and use pure JS when available
|
|
282
|
+
*/
|
|
283
|
+
runtimeDecoding?: "prefer" | "never";
|
|
284
|
+
}
|
|
167
285
|
/**
|
|
168
286
|
* Image format handler interface
|
|
169
287
|
*/
|
|
@@ -177,7 +295,7 @@ export interface ImageFormat {
|
|
|
177
295
|
* @param data Raw image data
|
|
178
296
|
* @returns Decoded image data
|
|
179
297
|
*/
|
|
180
|
-
decode(data: Uint8Array): Promise<ImageData>;
|
|
298
|
+
decode(data: Uint8Array, options?: ImageDecoderOptions): Promise<ImageData>;
|
|
181
299
|
/**
|
|
182
300
|
* Encode image data to bytes
|
|
183
301
|
* @param imageData Image data to encode
|
|
@@ -196,7 +314,7 @@ export interface ImageFormat {
|
|
|
196
314
|
* @param data Raw image data
|
|
197
315
|
* @returns Decoded multi-frame image data
|
|
198
316
|
*/
|
|
199
|
-
decodeFrames?(data: Uint8Array): Promise<MultiFrameImageData>;
|
|
317
|
+
decodeFrames?(data: Uint8Array, options?: ImageDecoderOptions): Promise<MultiFrameImageData>;
|
|
200
318
|
/**
|
|
201
319
|
* Encode multi-frame image data to bytes (optional)
|
|
202
320
|
* @param imageData Multi-frame image data to encode
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64 utilities.
|
|
3
|
+
*
|
|
4
|
+
* Designed to work across Deno, Node.js, and Bun without external dependencies.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Encode bytes into a standard Base64 string.
|
|
8
|
+
*/
|
|
9
|
+
export declare function encodeBase64(bytes: Uint8Array): string;
|
|
10
|
+
/**
|
|
11
|
+
* Decode a Base64 (or Base64URL) string into bytes.
|
|
12
|
+
*
|
|
13
|
+
* - Whitespace is ignored.
|
|
14
|
+
* - Missing padding is tolerated.
|
|
15
|
+
* - `-`/`_` are accepted as URL-safe variants.
|
|
16
|
+
*/
|
|
17
|
+
export declare function decodeBase64(base64: string): Uint8Array;
|
|
18
|
+
export interface ParsedDataUrl {
|
|
19
|
+
mime: string;
|
|
20
|
+
bytes: Uint8Array;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create a base64 data URL.
|
|
24
|
+
*/
|
|
25
|
+
export declare function toDataUrl(mime: string, bytes: Uint8Array): string;
|
|
26
|
+
/**
|
|
27
|
+
* Parse a base64 data URL.
|
|
28
|
+
*
|
|
29
|
+
* Supports `data:<mime>;base64,<payload>`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseDataUrl(url: string): ParsedDataUrl;
|
|
32
|
+
//# sourceMappingURL=base64.d.ts.map
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Base64 utilities.
|
|
4
|
+
*
|
|
5
|
+
* Designed to work across Deno, Node.js, and Bun without external dependencies.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.encodeBase64 = encodeBase64;
|
|
9
|
+
exports.decodeBase64 = decodeBase64;
|
|
10
|
+
exports.toDataUrl = toDataUrl;
|
|
11
|
+
exports.parseDataUrl = parseDataUrl;
|
|
12
|
+
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
13
|
+
const DECODE_TABLE = (() => {
|
|
14
|
+
const table = new Uint8Array(256);
|
|
15
|
+
table.fill(0xff);
|
|
16
|
+
for (let i = 0; i < BASE64_ALPHABET.length; i++) {
|
|
17
|
+
table[BASE64_ALPHABET.charCodeAt(i)] = i;
|
|
18
|
+
}
|
|
19
|
+
return table;
|
|
20
|
+
})();
|
|
21
|
+
function stripWhitespace(input) {
|
|
22
|
+
// Avoid regex to keep this fast and allocation-light.
|
|
23
|
+
let out = "";
|
|
24
|
+
for (let i = 0; i < input.length; i++) {
|
|
25
|
+
const code = input.charCodeAt(i);
|
|
26
|
+
// Space, tab, CR, LF
|
|
27
|
+
if (code === 0x20 || code === 0x09 || code === 0x0d || code === 0x0a)
|
|
28
|
+
continue;
|
|
29
|
+
out += input[i];
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function normalizeBase64(input) {
|
|
34
|
+
// Accept URL-safe alphabet in decode paths.
|
|
35
|
+
let s = stripWhitespace(input.trim());
|
|
36
|
+
if (s.length === 0)
|
|
37
|
+
return s;
|
|
38
|
+
// Map base64url to base64.
|
|
39
|
+
if (s.includes("-") || s.includes("_")) {
|
|
40
|
+
s = s.replaceAll("-", "+").replaceAll("_", "/");
|
|
41
|
+
}
|
|
42
|
+
const remainder = s.length % 4;
|
|
43
|
+
if (remainder === 1) {
|
|
44
|
+
throw new Error("Invalid base64: length must not be 1 (mod 4)");
|
|
45
|
+
}
|
|
46
|
+
if (remainder === 2)
|
|
47
|
+
s += "==";
|
|
48
|
+
if (remainder === 3)
|
|
49
|
+
s += "=";
|
|
50
|
+
return s;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Encode bytes into a standard Base64 string.
|
|
54
|
+
*/
|
|
55
|
+
function encodeBase64(bytes) {
|
|
56
|
+
if (bytes.length === 0)
|
|
57
|
+
return "";
|
|
58
|
+
const outLen = Math.ceil(bytes.length / 3) * 4;
|
|
59
|
+
const out = new Array(outLen);
|
|
60
|
+
let i = 0;
|
|
61
|
+
let o = 0;
|
|
62
|
+
for (; i + 2 < bytes.length; i += 3) {
|
|
63
|
+
const n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
|
|
64
|
+
out[o++] = BASE64_ALPHABET[(n >> 18) & 63];
|
|
65
|
+
out[o++] = BASE64_ALPHABET[(n >> 12) & 63];
|
|
66
|
+
out[o++] = BASE64_ALPHABET[(n >> 6) & 63];
|
|
67
|
+
out[o++] = BASE64_ALPHABET[n & 63];
|
|
68
|
+
}
|
|
69
|
+
const remaining = bytes.length - i;
|
|
70
|
+
if (remaining === 1) {
|
|
71
|
+
const n = bytes[i] << 16;
|
|
72
|
+
out[o++] = BASE64_ALPHABET[(n >> 18) & 63];
|
|
73
|
+
out[o++] = BASE64_ALPHABET[(n >> 12) & 63];
|
|
74
|
+
out[o++] = "=";
|
|
75
|
+
out[o++] = "=";
|
|
76
|
+
}
|
|
77
|
+
else if (remaining === 2) {
|
|
78
|
+
const n = (bytes[i] << 16) | (bytes[i + 1] << 8);
|
|
79
|
+
out[o++] = BASE64_ALPHABET[(n >> 18) & 63];
|
|
80
|
+
out[o++] = BASE64_ALPHABET[(n >> 12) & 63];
|
|
81
|
+
out[o++] = BASE64_ALPHABET[(n >> 6) & 63];
|
|
82
|
+
out[o++] = "=";
|
|
83
|
+
}
|
|
84
|
+
return out.join("");
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Decode a Base64 (or Base64URL) string into bytes.
|
|
88
|
+
*
|
|
89
|
+
* - Whitespace is ignored.
|
|
90
|
+
* - Missing padding is tolerated.
|
|
91
|
+
* - `-`/`_` are accepted as URL-safe variants.
|
|
92
|
+
*/
|
|
93
|
+
function decodeBase64(base64) {
|
|
94
|
+
const s = normalizeBase64(base64);
|
|
95
|
+
if (s.length === 0)
|
|
96
|
+
return new Uint8Array(0);
|
|
97
|
+
let padding = 0;
|
|
98
|
+
if (s.endsWith("=="))
|
|
99
|
+
padding = 2;
|
|
100
|
+
else if (s.endsWith("="))
|
|
101
|
+
padding = 1;
|
|
102
|
+
const quadCount = s.length / 4;
|
|
103
|
+
const outLen = quadCount * 3 - padding;
|
|
104
|
+
const out = new Uint8Array(outLen);
|
|
105
|
+
let o = 0;
|
|
106
|
+
for (let i = 0; i < s.length; i += 4) {
|
|
107
|
+
const c0 = s.charCodeAt(i);
|
|
108
|
+
const c1 = s.charCodeAt(i + 1);
|
|
109
|
+
const c2 = s.charCodeAt(i + 2);
|
|
110
|
+
const c3 = s.charCodeAt(i + 3);
|
|
111
|
+
const v0 = DECODE_TABLE[c0];
|
|
112
|
+
const v1 = DECODE_TABLE[c1];
|
|
113
|
+
if (v0 === 0xff || v1 === 0xff) {
|
|
114
|
+
throw new Error("Invalid base64: invalid character");
|
|
115
|
+
}
|
|
116
|
+
const isPad2 = c2 === 0x3d; // '='
|
|
117
|
+
const isPad3 = c3 === 0x3d;
|
|
118
|
+
const v2 = isPad2 ? 0 : DECODE_TABLE[c2];
|
|
119
|
+
const v3 = isPad3 ? 0 : DECODE_TABLE[c3];
|
|
120
|
+
if ((!isPad2 && v2 === 0xff) || (!isPad3 && v3 === 0xff)) {
|
|
121
|
+
throw new Error("Invalid base64: invalid character");
|
|
122
|
+
}
|
|
123
|
+
const n = (v0 << 18) | (v1 << 12) | (v2 << 6) | v3;
|
|
124
|
+
out[o++] = (n >> 16) & 0xff;
|
|
125
|
+
if (!isPad2) {
|
|
126
|
+
if (o >= out.length)
|
|
127
|
+
break;
|
|
128
|
+
out[o++] = (n >> 8) & 0xff;
|
|
129
|
+
}
|
|
130
|
+
if (!isPad3) {
|
|
131
|
+
if (o >= out.length)
|
|
132
|
+
break;
|
|
133
|
+
out[o++] = n & 0xff;
|
|
134
|
+
}
|
|
135
|
+
if (isPad2 || isPad3) {
|
|
136
|
+
// Padding is only valid in the final quartet.
|
|
137
|
+
if (i + 4 !== s.length) {
|
|
138
|
+
throw new Error("Invalid base64: padding can only appear at the end");
|
|
139
|
+
}
|
|
140
|
+
// If third char is padding, fourth must also be padding.
|
|
141
|
+
if (isPad2 && !isPad3) {
|
|
142
|
+
throw new Error("Invalid base64: invalid padding");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Create a base64 data URL.
|
|
150
|
+
*/
|
|
151
|
+
function toDataUrl(mime, bytes) {
|
|
152
|
+
if (!mime)
|
|
153
|
+
throw new Error("mime is required");
|
|
154
|
+
return `data:${mime};base64,${encodeBase64(bytes)}`;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Parse a base64 data URL.
|
|
158
|
+
*
|
|
159
|
+
* Supports `data:<mime>;base64,<payload>`.
|
|
160
|
+
*/
|
|
161
|
+
function parseDataUrl(url) {
|
|
162
|
+
if (!url.startsWith("data:")) {
|
|
163
|
+
throw new Error("Invalid data URL: must start with 'data:'");
|
|
164
|
+
}
|
|
165
|
+
const commaIndex = url.indexOf(",");
|
|
166
|
+
if (commaIndex === -1) {
|
|
167
|
+
throw new Error("Invalid data URL: missing ',' separator");
|
|
168
|
+
}
|
|
169
|
+
const meta = url.slice(5, commaIndex);
|
|
170
|
+
const payload = url.slice(commaIndex + 1);
|
|
171
|
+
// We only support base64 payloads.
|
|
172
|
+
const metaParts = meta.split(";");
|
|
173
|
+
const mime = metaParts[0] || "application/octet-stream";
|
|
174
|
+
const isBase64 = metaParts.some((p) => p.toLowerCase() === "base64");
|
|
175
|
+
if (!isBase64) {
|
|
176
|
+
throw new Error("Invalid data URL: only base64 payloads are supported");
|
|
177
|
+
}
|
|
178
|
+
return { mime, bytes: decodeBase64(payload) };
|
|
179
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Pure JavaScript GIF decoder implementation
|
|
3
3
|
* Supports GIF87a and GIF89a formats with LZW decompression
|
|
4
4
|
*/
|
|
5
|
+
import type { ImageDecoderOptions } from "../types.js";
|
|
5
6
|
interface GIFImage {
|
|
6
7
|
width: number;
|
|
7
8
|
height: number;
|
|
@@ -19,7 +20,8 @@ interface GIFFrame {
|
|
|
19
20
|
export declare class GIFDecoder {
|
|
20
21
|
private data;
|
|
21
22
|
private pos;
|
|
22
|
-
|
|
23
|
+
private options;
|
|
24
|
+
constructor(data: Uint8Array, settings?: ImageDecoderOptions);
|
|
23
25
|
private readByte;
|
|
24
26
|
private readUint16LE;
|
|
25
27
|
private readBytes;
|
|
@@ -36,6 +38,7 @@ export declare class GIFDecoder {
|
|
|
36
38
|
frames: GIFFrame[];
|
|
37
39
|
};
|
|
38
40
|
private indexedToRGBA;
|
|
41
|
+
private decodeFrame;
|
|
39
42
|
private deinterlace;
|
|
40
43
|
}
|
|
41
44
|
export {};
|
|
@@ -7,7 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.GIFDecoder = void 0;
|
|
8
8
|
const lzw_js_1 = require("./lzw.js");
|
|
9
9
|
class GIFDecoder {
|
|
10
|
-
constructor(data) {
|
|
10
|
+
constructor(data, settings = {}) {
|
|
11
11
|
Object.defineProperty(this, "data", {
|
|
12
12
|
enumerable: true,
|
|
13
13
|
configurable: true,
|
|
@@ -20,8 +20,18 @@ class GIFDecoder {
|
|
|
20
20
|
writable: true,
|
|
21
21
|
value: void 0
|
|
22
22
|
});
|
|
23
|
+
Object.defineProperty(this, "options", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: void 0
|
|
28
|
+
});
|
|
23
29
|
this.data = data;
|
|
24
30
|
this.pos = 0;
|
|
31
|
+
this.options = {
|
|
32
|
+
tolerantDecoding: settings.tolerantDecoding ?? true,
|
|
33
|
+
onWarning: settings.onWarning,
|
|
34
|
+
};
|
|
25
35
|
}
|
|
26
36
|
readByte() {
|
|
27
37
|
if (this.pos >= this.data.length) {
|
|
@@ -210,72 +220,20 @@ class GIFDecoder {
|
|
|
210
220
|
}
|
|
211
221
|
else if (separator === 0x2c) {
|
|
212
222
|
// Image Descriptor
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const localColorTableSize = 2 << (packed & 0x07);
|
|
222
|
-
let localColorTable = null;
|
|
223
|
-
if (hasLocalColorTable) {
|
|
224
|
-
localColorTable = this.readColorTable(localColorTableSize);
|
|
225
|
-
}
|
|
226
|
-
// Read image data
|
|
227
|
-
const minCodeSize = this.readByte();
|
|
228
|
-
const compressedData = this.readDataSubBlocks();
|
|
229
|
-
// Decompress using LZW
|
|
230
|
-
const decoder = new lzw_js_1.LZWDecoder(minCodeSize, compressedData);
|
|
231
|
-
const indexedData = decoder.decompress();
|
|
232
|
-
// Convert indexed to RGBA
|
|
233
|
-
const colorTable = localColorTable || globalColorTable;
|
|
234
|
-
if (!colorTable) {
|
|
235
|
-
throw new Error("No color table available");
|
|
236
|
-
}
|
|
237
|
-
// Deinterlace if necessary
|
|
238
|
-
const deinterlaced = interlaced
|
|
239
|
-
? this.deinterlace(indexedData, imageWidth, imageHeight)
|
|
240
|
-
: indexedData;
|
|
241
|
-
// Create frame with just the image data (not full canvas)
|
|
242
|
-
const frameData = new Uint8Array(imageWidth * imageHeight * 4);
|
|
243
|
-
for (let y = 0; y < imageHeight; y++) {
|
|
244
|
-
for (let x = 0; x < imageWidth; x++) {
|
|
245
|
-
const srcIdx = y * imageWidth + x;
|
|
246
|
-
if (srcIdx >= deinterlaced.length)
|
|
247
|
-
continue;
|
|
248
|
-
const colorIndex = deinterlaced[srcIdx];
|
|
249
|
-
const dstIdx = (y * imageWidth + x) * 4;
|
|
250
|
-
if (transparentColorIndex !== null &&
|
|
251
|
-
colorIndex === transparentColorIndex) {
|
|
252
|
-
// Transparent pixel
|
|
253
|
-
frameData[dstIdx] = 0;
|
|
254
|
-
frameData[dstIdx + 1] = 0;
|
|
255
|
-
frameData[dstIdx + 2] = 0;
|
|
256
|
-
frameData[dstIdx + 3] = 0;
|
|
257
|
-
}
|
|
258
|
-
else {
|
|
259
|
-
// Copy color from color table
|
|
260
|
-
const colorOffset = colorIndex * 3;
|
|
261
|
-
if (colorOffset + 2 < colorTable.length) {
|
|
262
|
-
frameData[dstIdx] = colorTable[colorOffset];
|
|
263
|
-
frameData[dstIdx + 1] = colorTable[colorOffset + 1];
|
|
264
|
-
frameData[dstIdx + 2] = colorTable[colorOffset + 2];
|
|
265
|
-
frameData[dstIdx + 3] = 255;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
223
|
+
if (this.options.tolerantDecoding) {
|
|
224
|
+
try {
|
|
225
|
+
this.decodeFrame(frames, globalColorTable, transparentColorIndex, delayTime, disposalMethod);
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
// Tolerant decoding: skip corrupted frames and continue
|
|
229
|
+
// This allows partial decoding of multi-frame GIFs with some bad frames
|
|
230
|
+
this.options.onWarning?.("GIF: Skipping corrupted frame", e);
|
|
268
231
|
}
|
|
269
232
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
top: imageTop,
|
|
275
|
-
data: frameData,
|
|
276
|
-
delay: delayTime,
|
|
277
|
-
disposal: disposalMethod,
|
|
278
|
-
});
|
|
233
|
+
else {
|
|
234
|
+
// Non-tolerant mode: throw on first error
|
|
235
|
+
this.decodeFrame(frames, globalColorTable, transparentColorIndex, delayTime, disposalMethod);
|
|
236
|
+
}
|
|
279
237
|
// Reset graphic control extension state
|
|
280
238
|
transparentColorIndex = null;
|
|
281
239
|
delayTime = 0;
|
|
@@ -353,6 +311,74 @@ class GIFDecoder {
|
|
|
353
311
|
data: rgba,
|
|
354
312
|
};
|
|
355
313
|
}
|
|
314
|
+
decodeFrame(frames, globalColorTable, transparentColorIndex, delayTime, disposalMethod) {
|
|
315
|
+
const imageLeft = this.readUint16LE();
|
|
316
|
+
const imageTop = this.readUint16LE();
|
|
317
|
+
const imageWidth = this.readUint16LE();
|
|
318
|
+
const imageHeight = this.readUint16LE();
|
|
319
|
+
const packed = this.readByte();
|
|
320
|
+
const hasLocalColorTable = (packed & 0x80) !== 0;
|
|
321
|
+
const interlaced = (packed & 0x40) !== 0;
|
|
322
|
+
// Color table size: 2^(n+1) where n is the 3 least significant bits
|
|
323
|
+
const localColorTableSize = 2 << (packed & 0x07);
|
|
324
|
+
let localColorTable = null;
|
|
325
|
+
if (hasLocalColorTable) {
|
|
326
|
+
localColorTable = this.readColorTable(localColorTableSize);
|
|
327
|
+
}
|
|
328
|
+
// Read image data
|
|
329
|
+
const minCodeSize = this.readByte();
|
|
330
|
+
const compressedData = this.readDataSubBlocks();
|
|
331
|
+
// Decompress using LZW
|
|
332
|
+
const decoder = new lzw_js_1.LZWDecoder(minCodeSize, compressedData);
|
|
333
|
+
const indexedData = decoder.decompress();
|
|
334
|
+
// Convert indexed to RGBA
|
|
335
|
+
const colorTable = localColorTable || globalColorTable;
|
|
336
|
+
if (!colorTable) {
|
|
337
|
+
throw new Error("No color table available");
|
|
338
|
+
}
|
|
339
|
+
// Deinterlace if necessary
|
|
340
|
+
const deinterlaced = interlaced
|
|
341
|
+
? this.deinterlace(indexedData, imageWidth, imageHeight)
|
|
342
|
+
: indexedData;
|
|
343
|
+
// Create frame with just the image data (not full canvas)
|
|
344
|
+
const frameData = new Uint8Array(imageWidth * imageHeight * 4);
|
|
345
|
+
for (let y = 0; y < imageHeight; y++) {
|
|
346
|
+
for (let x = 0; x < imageWidth; x++) {
|
|
347
|
+
const srcIdx = y * imageWidth + x;
|
|
348
|
+
if (srcIdx >= deinterlaced.length)
|
|
349
|
+
continue;
|
|
350
|
+
const colorIndex = deinterlaced[srcIdx];
|
|
351
|
+
const dstIdx = (y * imageWidth + x) * 4;
|
|
352
|
+
if (transparentColorIndex !== null &&
|
|
353
|
+
colorIndex === transparentColorIndex) {
|
|
354
|
+
// Transparent pixel
|
|
355
|
+
frameData[dstIdx] = 0;
|
|
356
|
+
frameData[dstIdx + 1] = 0;
|
|
357
|
+
frameData[dstIdx + 2] = 0;
|
|
358
|
+
frameData[dstIdx + 3] = 0;
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
// Copy color from color table
|
|
362
|
+
const colorOffset = colorIndex * 3;
|
|
363
|
+
if (colorOffset + 2 < colorTable.length) {
|
|
364
|
+
frameData[dstIdx] = colorTable[colorOffset];
|
|
365
|
+
frameData[dstIdx + 1] = colorTable[colorOffset + 1];
|
|
366
|
+
frameData[dstIdx + 2] = colorTable[colorOffset + 2];
|
|
367
|
+
frameData[dstIdx + 3] = 255;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
frames.push({
|
|
373
|
+
width: imageWidth,
|
|
374
|
+
height: imageHeight,
|
|
375
|
+
left: imageLeft,
|
|
376
|
+
top: imageTop,
|
|
377
|
+
data: frameData,
|
|
378
|
+
delay: delayTime,
|
|
379
|
+
disposal: disposalMethod,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
356
382
|
deinterlace(data, width, height) {
|
|
357
383
|
const deinterlaced = new Uint8Array(data.length);
|
|
358
384
|
const passes = [
|
|
@@ -168,11 +168,13 @@ class GIFEncoder {
|
|
|
168
168
|
}
|
|
169
169
|
return Math.max(2, bits);
|
|
170
170
|
}
|
|
171
|
-
encode() {
|
|
171
|
+
encode(options) {
|
|
172
172
|
if (this.frames.length === 0) {
|
|
173
173
|
throw new Error("No frames to encode");
|
|
174
174
|
}
|
|
175
175
|
const output = [];
|
|
176
|
+
// Get loop count from options (default to 0 = infinite)
|
|
177
|
+
const loopCount = options?.loop ?? 0;
|
|
176
178
|
// Quantize first frame for Global Color Table
|
|
177
179
|
const firstFrame = this.frames[0];
|
|
178
180
|
const { palette: globalPalette, indexed: firstIndexed } = this.quantize(firstFrame.data);
|
|
@@ -209,7 +211,7 @@ class GIFEncoder {
|
|
|
209
211
|
this.writeString(output, "NETSCAPE2.0");
|
|
210
212
|
output.push(3); // Sub-block Size
|
|
211
213
|
output.push(1); // Loop Indicator (1 = loop)
|
|
212
|
-
this.writeUint16LE(output,
|
|
214
|
+
this.writeUint16LE(output, loopCount); // Loop Count (0 = infinite, 1+ = specific count)
|
|
213
215
|
output.push(0); // Block Terminator
|
|
214
216
|
}
|
|
215
217
|
// Encode frames
|
|
@@ -186,4 +186,35 @@ export declare function flipHorizontal(data: Uint8Array, width: number, height:
|
|
|
186
186
|
* @returns Flipped image data
|
|
187
187
|
*/
|
|
188
188
|
export declare function flipVertical(data: Uint8Array, width: number, height: number): Uint8Array;
|
|
189
|
+
/**
|
|
190
|
+
* Convert RGB color to CMYK color space
|
|
191
|
+
* @param r Red component (0-255)
|
|
192
|
+
* @param g Green component (0-255)
|
|
193
|
+
* @param b Blue component (0-255)
|
|
194
|
+
* @returns CMYK values: [c (0-1), m (0-1), y (0-1), k (0-1)]
|
|
195
|
+
*/
|
|
196
|
+
export declare function rgbToCmyk(r: number, g: number, b: number): [number, number, number, number];
|
|
197
|
+
/**
|
|
198
|
+
* Convert CMYK color to RGB color space
|
|
199
|
+
* @param c Cyan component (0-1)
|
|
200
|
+
* @param m Magenta component (0-1)
|
|
201
|
+
* @param y Yellow component (0-1)
|
|
202
|
+
* @param k Key/Black component (0-1)
|
|
203
|
+
* @returns RGB values: [r (0-255), g (0-255), b (0-255)]
|
|
204
|
+
*/
|
|
205
|
+
export declare function cmykToRgb(c: number, m: number, y: number, k: number): [number, number, number];
|
|
206
|
+
/**
|
|
207
|
+
* Convert RGBA image data to CMYK representation
|
|
208
|
+
* Returns a Float32Array with 4 values per pixel (C, M, Y, K) in 0-1 range
|
|
209
|
+
* @param data Image data (RGBA)
|
|
210
|
+
* @returns CMYK image data as Float32Array (4 values per pixel)
|
|
211
|
+
*/
|
|
212
|
+
export declare function rgbaToCmyk(data: Uint8Array): Float32Array;
|
|
213
|
+
/**
|
|
214
|
+
* Convert CMYK image data to RGBA representation
|
|
215
|
+
* @param cmykData CMYK image data (4 values per pixel in 0-1 range)
|
|
216
|
+
* @param alpha Optional alpha value for all pixels (0-255, default: 255)
|
|
217
|
+
* @returns RGBA image data
|
|
218
|
+
*/
|
|
219
|
+
export declare function cmykToRgba(cmykData: Float32Array, alpha?: number): Uint8Array;
|
|
189
220
|
//# sourceMappingURL=image_processing.d.ts.map
|