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,173 @@
1
+ /**
2
+ * Base64 utilities.
3
+ *
4
+ * Designed to work across Deno, Node.js, and Bun without external dependencies.
5
+ */
6
+ const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
7
+ const DECODE_TABLE = (() => {
8
+ const table = new Uint8Array(256);
9
+ table.fill(0xff);
10
+ for (let i = 0; i < BASE64_ALPHABET.length; i++) {
11
+ table[BASE64_ALPHABET.charCodeAt(i)] = i;
12
+ }
13
+ return table;
14
+ })();
15
+ function stripWhitespace(input) {
16
+ // Avoid regex to keep this fast and allocation-light.
17
+ let out = "";
18
+ for (let i = 0; i < input.length; i++) {
19
+ const code = input.charCodeAt(i);
20
+ // Space, tab, CR, LF
21
+ if (code === 0x20 || code === 0x09 || code === 0x0d || code === 0x0a)
22
+ continue;
23
+ out += input[i];
24
+ }
25
+ return out;
26
+ }
27
+ function normalizeBase64(input) {
28
+ // Accept URL-safe alphabet in decode paths.
29
+ let s = stripWhitespace(input.trim());
30
+ if (s.length === 0)
31
+ return s;
32
+ // Map base64url to base64.
33
+ if (s.includes("-") || s.includes("_")) {
34
+ s = s.replaceAll("-", "+").replaceAll("_", "/");
35
+ }
36
+ const remainder = s.length % 4;
37
+ if (remainder === 1) {
38
+ throw new Error("Invalid base64: length must not be 1 (mod 4)");
39
+ }
40
+ if (remainder === 2)
41
+ s += "==";
42
+ if (remainder === 3)
43
+ s += "=";
44
+ return s;
45
+ }
46
+ /**
47
+ * Encode bytes into a standard Base64 string.
48
+ */
49
+ export function encodeBase64(bytes) {
50
+ if (bytes.length === 0)
51
+ return "";
52
+ const outLen = Math.ceil(bytes.length / 3) * 4;
53
+ const out = new Array(outLen);
54
+ let i = 0;
55
+ let o = 0;
56
+ for (; i + 2 < bytes.length; i += 3) {
57
+ const n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
58
+ out[o++] = BASE64_ALPHABET[(n >> 18) & 63];
59
+ out[o++] = BASE64_ALPHABET[(n >> 12) & 63];
60
+ out[o++] = BASE64_ALPHABET[(n >> 6) & 63];
61
+ out[o++] = BASE64_ALPHABET[n & 63];
62
+ }
63
+ const remaining = bytes.length - i;
64
+ if (remaining === 1) {
65
+ const n = bytes[i] << 16;
66
+ out[o++] = BASE64_ALPHABET[(n >> 18) & 63];
67
+ out[o++] = BASE64_ALPHABET[(n >> 12) & 63];
68
+ out[o++] = "=";
69
+ out[o++] = "=";
70
+ }
71
+ else if (remaining === 2) {
72
+ const n = (bytes[i] << 16) | (bytes[i + 1] << 8);
73
+ out[o++] = BASE64_ALPHABET[(n >> 18) & 63];
74
+ out[o++] = BASE64_ALPHABET[(n >> 12) & 63];
75
+ out[o++] = BASE64_ALPHABET[(n >> 6) & 63];
76
+ out[o++] = "=";
77
+ }
78
+ return out.join("");
79
+ }
80
+ /**
81
+ * Decode a Base64 (or Base64URL) string into bytes.
82
+ *
83
+ * - Whitespace is ignored.
84
+ * - Missing padding is tolerated.
85
+ * - `-`/`_` are accepted as URL-safe variants.
86
+ */
87
+ export function decodeBase64(base64) {
88
+ const s = normalizeBase64(base64);
89
+ if (s.length === 0)
90
+ return new Uint8Array(0);
91
+ let padding = 0;
92
+ if (s.endsWith("=="))
93
+ padding = 2;
94
+ else if (s.endsWith("="))
95
+ padding = 1;
96
+ const quadCount = s.length / 4;
97
+ const outLen = quadCount * 3 - padding;
98
+ const out = new Uint8Array(outLen);
99
+ let o = 0;
100
+ for (let i = 0; i < s.length; i += 4) {
101
+ const c0 = s.charCodeAt(i);
102
+ const c1 = s.charCodeAt(i + 1);
103
+ const c2 = s.charCodeAt(i + 2);
104
+ const c3 = s.charCodeAt(i + 3);
105
+ const v0 = DECODE_TABLE[c0];
106
+ const v1 = DECODE_TABLE[c1];
107
+ if (v0 === 0xff || v1 === 0xff) {
108
+ throw new Error("Invalid base64: invalid character");
109
+ }
110
+ const isPad2 = c2 === 0x3d; // '='
111
+ const isPad3 = c3 === 0x3d;
112
+ const v2 = isPad2 ? 0 : DECODE_TABLE[c2];
113
+ const v3 = isPad3 ? 0 : DECODE_TABLE[c3];
114
+ if ((!isPad2 && v2 === 0xff) || (!isPad3 && v3 === 0xff)) {
115
+ throw new Error("Invalid base64: invalid character");
116
+ }
117
+ const n = (v0 << 18) | (v1 << 12) | (v2 << 6) | v3;
118
+ out[o++] = (n >> 16) & 0xff;
119
+ if (!isPad2) {
120
+ if (o >= out.length)
121
+ break;
122
+ out[o++] = (n >> 8) & 0xff;
123
+ }
124
+ if (!isPad3) {
125
+ if (o >= out.length)
126
+ break;
127
+ out[o++] = n & 0xff;
128
+ }
129
+ if (isPad2 || isPad3) {
130
+ // Padding is only valid in the final quartet.
131
+ if (i + 4 !== s.length) {
132
+ throw new Error("Invalid base64: padding can only appear at the end");
133
+ }
134
+ // If third char is padding, fourth must also be padding.
135
+ if (isPad2 && !isPad3) {
136
+ throw new Error("Invalid base64: invalid padding");
137
+ }
138
+ }
139
+ }
140
+ return out;
141
+ }
142
+ /**
143
+ * Create a base64 data URL.
144
+ */
145
+ export function toDataUrl(mime, bytes) {
146
+ if (!mime)
147
+ throw new Error("mime is required");
148
+ return `data:${mime};base64,${encodeBase64(bytes)}`;
149
+ }
150
+ /**
151
+ * Parse a base64 data URL.
152
+ *
153
+ * Supports `data:<mime>;base64,<payload>`.
154
+ */
155
+ export function parseDataUrl(url) {
156
+ if (!url.startsWith("data:")) {
157
+ throw new Error("Invalid data URL: must start with 'data:'");
158
+ }
159
+ const commaIndex = url.indexOf(",");
160
+ if (commaIndex === -1) {
161
+ throw new Error("Invalid data URL: missing ',' separator");
162
+ }
163
+ const meta = url.slice(5, commaIndex);
164
+ const payload = url.slice(commaIndex + 1);
165
+ // We only support base64 payloads.
166
+ const metaParts = meta.split(";");
167
+ const mime = metaParts[0] || "application/octet-stream";
168
+ const isBase64 = metaParts.some((p) => p.toLowerCase() === "base64");
169
+ if (!isBase64) {
170
+ throw new Error("Invalid data URL: only base64 payloads are supported");
171
+ }
172
+ return { mime, bytes: decodeBase64(payload) };
173
+ }
@@ -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 {};
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { LZWDecoder } from "./lzw.js";
6
6
  export class GIFDecoder {
7
- constructor(data) {
7
+ constructor(data, settings = {}) {
8
8
  Object.defineProperty(this, "data", {
9
9
  enumerable: true,
10
10
  configurable: true,
@@ -17,8 +17,18 @@ export class GIFDecoder {
17
17
  writable: true,
18
18
  value: void 0
19
19
  });
20
+ Object.defineProperty(this, "options", {
21
+ enumerable: true,
22
+ configurable: true,
23
+ writable: true,
24
+ value: void 0
25
+ });
20
26
  this.data = data;
21
27
  this.pos = 0;
28
+ this.options = {
29
+ tolerantDecoding: settings.tolerantDecoding ?? true,
30
+ onWarning: settings.onWarning,
31
+ };
22
32
  }
23
33
  readByte() {
24
34
  if (this.pos >= this.data.length) {
@@ -207,72 +217,20 @@ export class GIFDecoder {
207
217
  }
208
218
  else if (separator === 0x2c) {
209
219
  // Image Descriptor
210
- const imageLeft = this.readUint16LE();
211
- const imageTop = this.readUint16LE();
212
- const imageWidth = this.readUint16LE();
213
- const imageHeight = this.readUint16LE();
214
- const packed = this.readByte();
215
- const hasLocalColorTable = (packed & 0x80) !== 0;
216
- const interlaced = (packed & 0x40) !== 0;
217
- // Color table size: 2^(n+1) where n is the 3 least significant bits
218
- const localColorTableSize = 2 << (packed & 0x07);
219
- let localColorTable = null;
220
- if (hasLocalColorTable) {
221
- localColorTable = this.readColorTable(localColorTableSize);
222
- }
223
- // Read image data
224
- const minCodeSize = this.readByte();
225
- const compressedData = this.readDataSubBlocks();
226
- // Decompress using LZW
227
- const decoder = new LZWDecoder(minCodeSize, compressedData);
228
- const indexedData = decoder.decompress();
229
- // Convert indexed to RGBA
230
- const colorTable = localColorTable || globalColorTable;
231
- if (!colorTable) {
232
- throw new Error("No color table available");
233
- }
234
- // Deinterlace if necessary
235
- const deinterlaced = interlaced
236
- ? this.deinterlace(indexedData, imageWidth, imageHeight)
237
- : indexedData;
238
- // Create frame with just the image data (not full canvas)
239
- const frameData = new Uint8Array(imageWidth * imageHeight * 4);
240
- for (let y = 0; y < imageHeight; y++) {
241
- for (let x = 0; x < imageWidth; x++) {
242
- const srcIdx = y * imageWidth + x;
243
- if (srcIdx >= deinterlaced.length)
244
- continue;
245
- const colorIndex = deinterlaced[srcIdx];
246
- const dstIdx = (y * imageWidth + x) * 4;
247
- if (transparentColorIndex !== null &&
248
- colorIndex === transparentColorIndex) {
249
- // Transparent pixel
250
- frameData[dstIdx] = 0;
251
- frameData[dstIdx + 1] = 0;
252
- frameData[dstIdx + 2] = 0;
253
- frameData[dstIdx + 3] = 0;
254
- }
255
- else {
256
- // Copy color from color table
257
- const colorOffset = colorIndex * 3;
258
- if (colorOffset + 2 < colorTable.length) {
259
- frameData[dstIdx] = colorTable[colorOffset];
260
- frameData[dstIdx + 1] = colorTable[colorOffset + 1];
261
- frameData[dstIdx + 2] = colorTable[colorOffset + 2];
262
- frameData[dstIdx + 3] = 255;
263
- }
264
- }
220
+ if (this.options.tolerantDecoding) {
221
+ try {
222
+ this.decodeFrame(frames, globalColorTable, transparentColorIndex, delayTime, disposalMethod);
223
+ }
224
+ catch (e) {
225
+ // Tolerant decoding: skip corrupted frames and continue
226
+ // This allows partial decoding of multi-frame GIFs with some bad frames
227
+ this.options.onWarning?.("GIF: Skipping corrupted frame", e);
265
228
  }
266
229
  }
267
- frames.push({
268
- width: imageWidth,
269
- height: imageHeight,
270
- left: imageLeft,
271
- top: imageTop,
272
- data: frameData,
273
- delay: delayTime,
274
- disposal: disposalMethod,
275
- });
230
+ else {
231
+ // Non-tolerant mode: throw on first error
232
+ this.decodeFrame(frames, globalColorTable, transparentColorIndex, delayTime, disposalMethod);
233
+ }
276
234
  // Reset graphic control extension state
277
235
  transparentColorIndex = null;
278
236
  delayTime = 0;
@@ -350,6 +308,74 @@ export class GIFDecoder {
350
308
  data: rgba,
351
309
  };
352
310
  }
311
+ decodeFrame(frames, globalColorTable, transparentColorIndex, delayTime, disposalMethod) {
312
+ const imageLeft = this.readUint16LE();
313
+ const imageTop = this.readUint16LE();
314
+ const imageWidth = this.readUint16LE();
315
+ const imageHeight = this.readUint16LE();
316
+ const packed = this.readByte();
317
+ const hasLocalColorTable = (packed & 0x80) !== 0;
318
+ const interlaced = (packed & 0x40) !== 0;
319
+ // Color table size: 2^(n+1) where n is the 3 least significant bits
320
+ const localColorTableSize = 2 << (packed & 0x07);
321
+ let localColorTable = null;
322
+ if (hasLocalColorTable) {
323
+ localColorTable = this.readColorTable(localColorTableSize);
324
+ }
325
+ // Read image data
326
+ const minCodeSize = this.readByte();
327
+ const compressedData = this.readDataSubBlocks();
328
+ // Decompress using LZW
329
+ const decoder = new LZWDecoder(minCodeSize, compressedData);
330
+ const indexedData = decoder.decompress();
331
+ // Convert indexed to RGBA
332
+ const colorTable = localColorTable || globalColorTable;
333
+ if (!colorTable) {
334
+ throw new Error("No color table available");
335
+ }
336
+ // Deinterlace if necessary
337
+ const deinterlaced = interlaced
338
+ ? this.deinterlace(indexedData, imageWidth, imageHeight)
339
+ : indexedData;
340
+ // Create frame with just the image data (not full canvas)
341
+ const frameData = new Uint8Array(imageWidth * imageHeight * 4);
342
+ for (let y = 0; y < imageHeight; y++) {
343
+ for (let x = 0; x < imageWidth; x++) {
344
+ const srcIdx = y * imageWidth + x;
345
+ if (srcIdx >= deinterlaced.length)
346
+ continue;
347
+ const colorIndex = deinterlaced[srcIdx];
348
+ const dstIdx = (y * imageWidth + x) * 4;
349
+ if (transparentColorIndex !== null &&
350
+ colorIndex === transparentColorIndex) {
351
+ // Transparent pixel
352
+ frameData[dstIdx] = 0;
353
+ frameData[dstIdx + 1] = 0;
354
+ frameData[dstIdx + 2] = 0;
355
+ frameData[dstIdx + 3] = 0;
356
+ }
357
+ else {
358
+ // Copy color from color table
359
+ const colorOffset = colorIndex * 3;
360
+ if (colorOffset + 2 < colorTable.length) {
361
+ frameData[dstIdx] = colorTable[colorOffset];
362
+ frameData[dstIdx + 1] = colorTable[colorOffset + 1];
363
+ frameData[dstIdx + 2] = colorTable[colorOffset + 2];
364
+ frameData[dstIdx + 3] = 255;
365
+ }
366
+ }
367
+ }
368
+ }
369
+ frames.push({
370
+ width: imageWidth,
371
+ height: imageHeight,
372
+ left: imageLeft,
373
+ top: imageTop,
374
+ data: frameData,
375
+ delay: delayTime,
376
+ disposal: disposalMethod,
377
+ });
378
+ }
353
379
  deinterlace(data, width, height) {
354
380
  const deinterlaced = new Uint8Array(data.length);
355
381
  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
@@ -165,11 +165,13 @@ export class GIFEncoder {
165
165
  }
166
166
  return Math.max(2, bits);
167
167
  }
168
- encode() {
168
+ encode(options) {
169
169
  if (this.frames.length === 0) {
170
170
  throw new Error("No frames to encode");
171
171
  }
172
172
  const output = [];
173
+ // Get loop count from options (default to 0 = infinite)
174
+ const loopCount = options?.loop ?? 0;
173
175
  // Quantize first frame for Global Color Table
174
176
  const firstFrame = this.frames[0];
175
177
  const { palette: globalPalette, indexed: firstIndexed } = this.quantize(firstFrame.data);
@@ -206,7 +208,7 @@ export class GIFEncoder {
206
208
  this.writeString(output, "NETSCAPE2.0");
207
209
  output.push(3); // Sub-block Size
208
210
  output.push(1); // Loop Indicator (1 = loop)
209
- this.writeUint16LE(output, 0); // Loop Count (0 = infinite)
211
+ this.writeUint16LE(output, loopCount); // Loop Count (0 = infinite, 1+ = specific count)
210
212
  output.push(0); // Block Terminator
211
213
  }
212
214
  // 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