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.
Files changed (117) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +615 -333
  3. package/esm/mod.d.ts +6 -4
  4. package/esm/mod.js +4 -2
  5. package/esm/src/formats/apng.d.ts +7 -5
  6. package/esm/src/formats/apng.js +15 -10
  7. package/esm/src/formats/ascii.d.ts +3 -3
  8. package/esm/src/formats/ascii.js +1 -1
  9. package/esm/src/formats/avif.d.ts +3 -3
  10. package/esm/src/formats/avif.js +17 -7
  11. package/esm/src/formats/bmp.d.ts +3 -3
  12. package/esm/src/formats/bmp.js +2 -2
  13. package/esm/src/formats/dng.d.ts +1 -1
  14. package/esm/src/formats/dng.js +1 -1
  15. package/esm/src/formats/gif.d.ts +5 -5
  16. package/esm/src/formats/gif.js +17 -13
  17. package/esm/src/formats/heic.d.ts +3 -3
  18. package/esm/src/formats/heic.js +17 -7
  19. package/esm/src/formats/ico.d.ts +3 -3
  20. package/esm/src/formats/ico.js +4 -4
  21. package/esm/src/formats/jpeg.d.ts +3 -3
  22. package/esm/src/formats/jpeg.js +23 -11
  23. package/esm/src/formats/pam.d.ts +3 -3
  24. package/esm/src/formats/pam.js +2 -2
  25. package/esm/src/formats/pcx.d.ts +3 -3
  26. package/esm/src/formats/pcx.js +2 -2
  27. package/esm/src/formats/png.d.ts +4 -3
  28. package/esm/src/formats/png.js +9 -3
  29. package/esm/src/formats/png_base.d.ts +42 -1
  30. package/esm/src/formats/png_base.js +200 -10
  31. package/esm/src/formats/ppm.d.ts +3 -3
  32. package/esm/src/formats/ppm.js +2 -2
  33. package/esm/src/formats/tiff.d.ts +7 -18
  34. package/esm/src/formats/tiff.js +162 -27
  35. package/esm/src/formats/webp.d.ts +3 -3
  36. package/esm/src/formats/webp.js +11 -8
  37. package/esm/src/image.d.ts +26 -3
  38. package/esm/src/image.js +66 -22
  39. package/esm/src/types.d.ts +122 -4
  40. package/esm/src/utils/base64.d.ts +32 -0
  41. package/esm/src/utils/base64.js +173 -0
  42. package/esm/src/utils/gif_decoder.d.ts +4 -1
  43. package/esm/src/utils/gif_decoder.js +91 -65
  44. package/esm/src/utils/gif_encoder.d.ts +3 -1
  45. package/esm/src/utils/gif_encoder.js +4 -2
  46. package/esm/src/utils/image_processing.d.ts +31 -0
  47. package/esm/src/utils/image_processing.js +232 -70
  48. package/esm/src/utils/jpeg_decoder.d.ts +17 -4
  49. package/esm/src/utils/jpeg_decoder.js +448 -83
  50. package/esm/src/utils/jpeg_encoder.d.ts +15 -1
  51. package/esm/src/utils/jpeg_encoder.js +263 -24
  52. package/esm/src/utils/resize.js +51 -20
  53. package/esm/src/utils/tiff_deflate.d.ts +18 -0
  54. package/esm/src/utils/tiff_deflate.js +27 -0
  55. package/esm/src/utils/tiff_packbits.d.ts +24 -0
  56. package/esm/src/utils/tiff_packbits.js +90 -0
  57. package/esm/src/utils/webp_decoder.d.ts +3 -1
  58. package/esm/src/utils/webp_decoder.js +144 -63
  59. package/esm/src/utils/webp_encoder.js +5 -11
  60. package/package.json +1 -1
  61. package/script/mod.d.ts +6 -4
  62. package/script/mod.js +13 -3
  63. package/script/src/formats/apng.d.ts +7 -5
  64. package/script/src/formats/apng.js +15 -10
  65. package/script/src/formats/ascii.d.ts +3 -3
  66. package/script/src/formats/ascii.js +1 -1
  67. package/script/src/formats/avif.d.ts +3 -3
  68. package/script/src/formats/avif.js +17 -7
  69. package/script/src/formats/bmp.d.ts +3 -3
  70. package/script/src/formats/bmp.js +2 -2
  71. package/script/src/formats/dng.d.ts +1 -1
  72. package/script/src/formats/dng.js +1 -1
  73. package/script/src/formats/gif.d.ts +5 -5
  74. package/script/src/formats/gif.js +17 -13
  75. package/script/src/formats/heic.d.ts +3 -3
  76. package/script/src/formats/heic.js +17 -7
  77. package/script/src/formats/ico.d.ts +3 -3
  78. package/script/src/formats/ico.js +4 -4
  79. package/script/src/formats/jpeg.d.ts +3 -3
  80. package/script/src/formats/jpeg.js +23 -11
  81. package/script/src/formats/pam.d.ts +3 -3
  82. package/script/src/formats/pam.js +2 -2
  83. package/script/src/formats/pcx.d.ts +3 -3
  84. package/script/src/formats/pcx.js +2 -2
  85. package/script/src/formats/png.d.ts +4 -3
  86. package/script/src/formats/png.js +9 -3
  87. package/script/src/formats/png_base.d.ts +42 -1
  88. package/script/src/formats/png_base.js +200 -10
  89. package/script/src/formats/ppm.d.ts +3 -3
  90. package/script/src/formats/ppm.js +2 -2
  91. package/script/src/formats/tiff.d.ts +7 -18
  92. package/script/src/formats/tiff.js +162 -27
  93. package/script/src/formats/webp.d.ts +3 -3
  94. package/script/src/formats/webp.js +11 -8
  95. package/script/src/image.d.ts +26 -3
  96. package/script/src/image.js +64 -20
  97. package/script/src/types.d.ts +122 -4
  98. package/script/src/utils/base64.d.ts +32 -0
  99. package/script/src/utils/base64.js +179 -0
  100. package/script/src/utils/gif_decoder.d.ts +4 -1
  101. package/script/src/utils/gif_decoder.js +91 -65
  102. package/script/src/utils/gif_encoder.d.ts +3 -1
  103. package/script/src/utils/gif_encoder.js +4 -2
  104. package/script/src/utils/image_processing.d.ts +31 -0
  105. package/script/src/utils/image_processing.js +236 -70
  106. package/script/src/utils/jpeg_decoder.d.ts +17 -4
  107. package/script/src/utils/jpeg_decoder.js +448 -83
  108. package/script/src/utils/jpeg_encoder.d.ts +15 -1
  109. package/script/src/utils/jpeg_encoder.js +263 -24
  110. package/script/src/utils/resize.js +51 -20
  111. package/script/src/utils/tiff_deflate.d.ts +18 -0
  112. package/script/src/utils/tiff_deflate.js +31 -0
  113. package/script/src/utils/tiff_packbits.d.ts +24 -0
  114. package/script/src/utils/tiff_packbits.js +94 -0
  115. package/script/src/utils/webp_decoder.d.ts +3 -1
  116. package/script/src/utils/webp_decoder.js +144 -63
  117. package/script/src/utils/webp_encoder.js +5 -11
@@ -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 ASCIIOptions {
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 WebPEncodeOptions {
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
- constructor(data: Uint8Array);
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
- const imageLeft = this.readUint16LE();
214
- const imageTop = this.readUint16LE();
215
- const imageWidth = this.readUint16LE();
216
- const imageHeight = this.readUint16LE();
217
- const packed = this.readByte();
218
- const hasLocalColorTable = (packed & 0x80) !== 0;
219
- const interlaced = (packed & 0x40) !== 0;
220
- // Color table size: 2^(n+1) where n is the 3 least significant bits
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
- frames.push({
271
- width: imageWidth,
272
- height: imageHeight,
273
- left: imageLeft,
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 = [
@@ -25,6 +25,8 @@ export declare class GIFEncoder {
25
25
  private quantize;
26
26
  private nextPowerOf2;
27
27
  private getBitsPerColor;
28
- encode(): Uint8Array;
28
+ encode(options?: {
29
+ loop?: number;
30
+ }): Uint8Array;
29
31
  }
30
32
  //# sourceMappingURL=gif_encoder.d.ts.map
@@ -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, 0); // Loop Count (0 = infinite)
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