cross-image 0.4.2 → 0.4.3

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.
@@ -48,6 +48,7 @@ export declare class GIFFormat implements ImageFormat {
48
48
  * Encode multi-frame image data to animated GIF
49
49
  */
50
50
  encodeFrames(imageData: MultiFrameImageData, options?: GIFEncoderOptions): Promise<Uint8Array>;
51
+ private mapDisposalMethodToNumber;
51
52
  private mapDisposalMethod;
52
53
  private decodeUsingRuntime;
53
54
  private readDataSubBlocks;
@@ -243,12 +243,29 @@ export class GIFFormat {
243
243
  }
244
244
  const encoder = new GIFEncoder(imageData.width, imageData.height);
245
245
  for (const frame of imageData.frames) {
246
- // Get delay from metadata (default to 100ms if not set)
247
246
  const delay = frame.frameMetadata?.delay ?? 100;
248
- encoder.addFrame(frame.data, delay);
247
+ encoder.addFrame(frame.data, delay, {
248
+ left: frame.frameMetadata?.left ?? 0,
249
+ top: frame.frameMetadata?.top ?? 0,
250
+ width: frame.width,
251
+ height: frame.height,
252
+ disposal: this.mapDisposalMethodToNumber(frame.frameMetadata?.disposal),
253
+ });
249
254
  }
250
255
  return Promise.resolve(encoder.encode(options));
251
256
  }
257
+ mapDisposalMethodToNumber(disposal) {
258
+ switch (disposal) {
259
+ case "none":
260
+ return 1;
261
+ case "background":
262
+ return 2;
263
+ case "previous":
264
+ return 3;
265
+ default:
266
+ return 0;
267
+ }
268
+ }
252
269
  mapDisposalMethod(disposal) {
253
270
  switch (disposal) {
254
271
  case 0:
@@ -3,11 +3,20 @@
3
3
  * Supports GIF89a format with LZW compression
4
4
  */
5
5
  export declare class GIFEncoder {
6
- private width;
7
- private height;
6
+ private canvasWidth;
7
+ private canvasHeight;
8
8
  private frames;
9
9
  constructor(width: number, height: number, data?: Uint8Array);
10
- addFrame(data: Uint8Array, delay?: number): void;
10
+ /**
11
+ * Add a frame with optional metadata for partial frame support
12
+ */
13
+ addFrame(data: Uint8Array, delay?: number, options?: {
14
+ left?: number;
15
+ top?: number;
16
+ width?: number;
17
+ height?: number;
18
+ disposal?: number;
19
+ }): void;
11
20
  private writeBytes;
12
21
  private writeUint16LE;
13
22
  private writeString;
@@ -20,7 +29,7 @@ export declare class GIFEncoder {
20
29
  */
21
30
  private quantizeChannel;
22
31
  /**
23
- * Quantize RGBA image to 256 colors using median cut algorithm
32
+ * Quantize RGBA image to 256 colors with transparency support
24
33
  */
25
34
  private quantize;
26
35
  private nextPowerOf2;
@@ -5,13 +5,13 @@
5
5
  import { LZWEncoder } from "./lzw.js";
6
6
  export class GIFEncoder {
7
7
  constructor(width, height, data) {
8
- Object.defineProperty(this, "width", {
8
+ Object.defineProperty(this, "canvasWidth", {
9
9
  enumerable: true,
10
10
  configurable: true,
11
11
  writable: true,
12
12
  value: void 0
13
13
  });
14
- Object.defineProperty(this, "height", {
14
+ Object.defineProperty(this, "canvasHeight", {
15
15
  enumerable: true,
16
16
  configurable: true,
17
17
  writable: true,
@@ -23,14 +23,28 @@ export class GIFEncoder {
23
23
  writable: true,
24
24
  value: []
25
25
  });
26
- this.width = width;
27
- this.height = height;
26
+ this.canvasWidth = width;
27
+ this.canvasHeight = height;
28
28
  if (data) {
29
29
  this.addFrame(data);
30
30
  }
31
31
  }
32
- addFrame(data, delay = 0) {
33
- this.frames.push({ data, delay });
32
+ /**
33
+ * Add a frame with optional metadata for partial frame support
34
+ */
35
+ addFrame(data, delay = 0, options) {
36
+ const pixelCount = data.length / 4;
37
+ const frameWidth = options?.width ?? this.canvasWidth;
38
+ const frameHeight = options?.height ?? Math.ceil(pixelCount / frameWidth);
39
+ this.frames.push({
40
+ data,
41
+ delay,
42
+ left: options?.left ?? 0,
43
+ top: options?.top ?? 0,
44
+ width: frameWidth,
45
+ height: frameHeight,
46
+ disposal: options?.disposal ?? 0,
47
+ });
34
48
  }
35
49
  writeBytes(output, bytes) {
36
50
  output.push(...bytes);
@@ -57,49 +71,59 @@ export class GIFEncoder {
57
71
  return Math.round(Math.round(value * levels / 255) * step);
58
72
  }
59
73
  /**
60
- * Quantize RGBA image to 256 colors using median cut algorithm
74
+ * Quantize RGBA image to 256 colors with transparency support
61
75
  */
62
- quantize(data) {
63
- // Simple quantization: collect unique colors and build palette
76
+ quantize(data, frameWidth, frameHeight) {
77
+ // Check for transparent pixels (alpha < 128)
78
+ let hasTransparency = false;
79
+ for (let i = 3; i < data.length; i += 4) {
80
+ if (data[i] < 128) {
81
+ hasTransparency = true;
82
+ break;
83
+ }
84
+ }
64
85
  const colorMap = new Map();
65
86
  const colors = [];
66
- // Color quantization parameters for 8-bit palette (256 colors)
67
- // R/G: 8 levels (0-7) using 3 bits, B: 4 levels (0-3) using 2 bits (8*8*4=256)
68
- const RG_LEVELS = 7; // Max level for R/G (8 total levels: 0-7)
69
- const B_LEVELS = 3; // Max level for B (4 total levels: 0-3)
70
- const rgStep = 255 / RG_LEVELS; // Step size for R/G quantization
71
- const bStep = 255 / B_LEVELS; // Step size for B quantization
72
- // Collect unique colors
87
+ const transparentIndex = 0;
88
+ // Reserve index 0 for transparency if needed
89
+ if (hasTransparency) {
90
+ colors.push({ r: 0, g: 0, b: 0 });
91
+ }
92
+ // Color quantization parameters
93
+ const RG_LEVELS = 7;
94
+ const B_LEVELS = 3;
95
+ const rgStep = 255 / RG_LEVELS;
96
+ const bStep = 255 / B_LEVELS;
97
+ const maxColors = hasTransparency ? 255 : 256;
98
+ // Collect unique colors from opaque pixels only
73
99
  for (let i = 0; i < data.length; i += 4) {
100
+ if (data[i + 3] < 128)
101
+ continue; // Skip transparent
74
102
  const r = data[i];
75
103
  const g = data[i + 1];
76
104
  const b = data[i + 2];
77
105
  const key = `${r},${g},${b}`;
78
- if (!colorMap.has(key) && colors.length < 256) {
106
+ if (!colorMap.has(key) && colors.length < maxColors) {
79
107
  colorMap.set(key, colors.length);
80
108
  colors.push({ r, g, b });
81
109
  }
82
110
  }
83
- // Track if color reduction was applied
84
111
  let useColorReduction = false;
85
- // If we have too many colors, use simple color reduction
86
- if (colors.length >= 256) {
87
- // Downsample colors to 256 by reducing color depth
112
+ // If too many colors, reduce color depth
113
+ if (colors.length >= maxColors) {
88
114
  colorMap.clear();
89
- colors.length = 0;
115
+ colors.length = hasTransparency ? 1 : 0;
90
116
  useColorReduction = true;
91
117
  for (let i = 0; i < data.length; i += 4) {
92
- // Reduce color depth: 3 bits for R/G channels, 2 bits for B channel
93
- // This gives us 8 bits total = 256 possible colors
118
+ if (data[i + 3] < 128)
119
+ continue;
94
120
  const r = this.quantizeChannel(data[i], RG_LEVELS, rgStep);
95
121
  const g = this.quantizeChannel(data[i + 1], RG_LEVELS, rgStep);
96
122
  const b = this.quantizeChannel(data[i + 2], B_LEVELS, bStep);
97
123
  const key = `${r},${g},${b}`;
98
- if (!colorMap.has(key)) {
99
- if (colors.length < 256) {
100
- colorMap.set(key, colors.length);
101
- colors.push({ r, g, b });
102
- }
124
+ if (!colorMap.has(key) && colors.length < maxColors) {
125
+ colorMap.set(key, colors.length);
126
+ colors.push({ r, g, b });
103
127
  }
104
128
  }
105
129
  }
@@ -115,28 +139,31 @@ export class GIFEncoder {
115
139
  palette[i * 3 + 1] = colors[i].g;
116
140
  palette[i * 3 + 2] = colors[i].b;
117
141
  }
118
- // Create indexed data
119
- const indexed = new Uint8Array(this.width * this.height);
120
- for (let i = 0, j = 0; i < data.length; i += 4, j++) {
142
+ // Create indexed data using frame dimensions
143
+ const indexed = new Uint8Array(frameWidth * frameHeight);
144
+ for (let i = 0, j = 0; i < data.length && j < indexed.length; i += 4, j++) {
145
+ // Handle transparent pixels
146
+ if (data[i + 3] < 128) {
147
+ indexed[j] = transparentIndex;
148
+ continue;
149
+ }
121
150
  let r = data[i];
122
151
  let g = data[i + 1];
123
152
  let b = data[i + 2];
124
- // Apply color reduction if it was used for building the palette
125
153
  if (useColorReduction) {
126
154
  r = this.quantizeChannel(r, RG_LEVELS, rgStep);
127
155
  g = this.quantizeChannel(g, RG_LEVELS, rgStep);
128
156
  b = this.quantizeChannel(b, B_LEVELS, bStep);
129
157
  }
130
158
  const key = `${r},${g},${b}`;
131
- // Try fast O(1) lookup first
132
159
  if (colorMap.has(key)) {
133
160
  indexed[j] = colorMap.get(key);
134
161
  }
135
162
  else {
136
- // Fallback: find closest color in palette (shouldn't happen often)
163
+ // Find closest color
137
164
  let minDist = Infinity;
138
- let bestIdx = 0;
139
- for (let k = 0; k < colors.length; k++) {
165
+ let bestIdx = hasTransparency ? 1 : 0;
166
+ for (let k = hasTransparency ? 1 : 0; k < colors.length; k++) {
140
167
  const dr = r - colors[k].r;
141
168
  const dg = g - colors[k].g;
142
169
  const db = b - colors[k].b;
@@ -149,7 +176,7 @@ export class GIFEncoder {
149
176
  indexed[j] = bestIdx;
150
177
  }
151
178
  }
152
- return { palette, indexed };
179
+ return { palette, indexed, hasTransparency, transparentIndex };
153
180
  }
154
181
  nextPowerOf2(n) {
155
182
  let power = 1;
@@ -170,46 +197,34 @@ export class GIFEncoder {
170
197
  throw new Error("No frames to encode");
171
198
  }
172
199
  const output = [];
173
- // Get loop count from options (default to 0 = infinite)
174
200
  const loopCount = options?.loop ?? 0;
175
201
  // Quantize first frame for Global Color Table
176
202
  const firstFrame = this.frames[0];
177
- const { palette: globalPalette, indexed: firstIndexed } = this.quantize(firstFrame.data);
203
+ const firstResult = this.quantize(firstFrame.data, firstFrame.width, firstFrame.height);
204
+ const globalPalette = firstResult.palette;
178
205
  const paletteSize = globalPalette.length / 3;
179
206
  const bitsPerColor = this.getBitsPerColor(paletteSize);
180
207
  // Header
181
208
  this.writeString(output, "GIF89a");
182
209
  // Logical Screen Descriptor
183
- this.writeUint16LE(output, this.width);
184
- this.writeUint16LE(output, this.height);
185
- // Packed field:
186
- // - Global Color Table Flag (1 bit): 1
187
- // - Color Resolution (3 bits): bitsPerColor - 1
188
- // - Sort Flag (1 bit): 0
189
- // - Size of Global Color Table (3 bits): bitsPerColor - 1
210
+ this.writeUint16LE(output, this.canvasWidth);
211
+ this.writeUint16LE(output, this.canvasHeight);
190
212
  const packed = 0x80 | ((bitsPerColor - 1) << 4) | (bitsPerColor - 1);
191
213
  output.push(packed);
192
- // Background Color Index
193
- output.push(0);
194
- // Pixel Aspect Ratio
195
- output.push(0);
214
+ output.push(0); // Background Color Index
215
+ output.push(0); // Pixel Aspect Ratio
196
216
  // Global Color Table
197
- // The GCT size is 2^(n+1) where n is the value in the packed field
198
- // So we need to write that many colors, padding if necessary
199
217
  const gctSize = 1 << bitsPerColor;
200
218
  const paddedPalette = new Uint8Array(gctSize * 3);
201
219
  paddedPalette.set(globalPalette);
202
220
  this.writeBytes(output, paddedPalette);
203
221
  // Netscape Application Extension (Looping)
204
222
  if (this.frames.length > 1) {
205
- output.push(0x21); // Extension Introducer
206
- output.push(0xff); // Application Extension Label
207
- output.push(11); // Block Size
223
+ output.push(0x21, 0xff, 11);
208
224
  this.writeString(output, "NETSCAPE2.0");
209
- output.push(3); // Sub-block Size
210
- output.push(1); // Loop Indicator (1 = loop)
211
- this.writeUint16LE(output, loopCount); // Loop Count (0 = infinite, 1+ = specific count)
212
- output.push(0); // Block Terminator
225
+ output.push(3, 1);
226
+ this.writeUint16LE(output, loopCount);
227
+ output.push(0);
213
228
  }
214
229
  // Encode frames
215
230
  for (let i = 0; i < this.frames.length; i++) {
@@ -218,67 +233,56 @@ export class GIFEncoder {
218
233
  let useLocalPalette = false;
219
234
  let localPalette = null;
220
235
  let localBitsPerColor = bitsPerColor;
236
+ let hasTransparency;
237
+ let transparentIndex;
221
238
  if (i === 0) {
222
- indexed = firstIndexed;
239
+ indexed = firstResult.indexed;
240
+ hasTransparency = firstResult.hasTransparency;
241
+ transparentIndex = firstResult.transparentIndex;
223
242
  }
224
243
  else {
225
- // Quantize subsequent frames
226
- // For simplicity, we use a Local Color Table for each frame to ensure colors are correct
227
- const result = this.quantize(frame.data);
244
+ const result = this.quantize(frame.data, frame.width, frame.height);
228
245
  indexed = result.indexed;
229
246
  localPalette = result.palette;
230
247
  useLocalPalette = true;
231
- const localPaletteSize = localPalette.length / 3;
232
- localBitsPerColor = this.getBitsPerColor(localPaletteSize);
248
+ hasTransparency = result.hasTransparency;
249
+ transparentIndex = result.transparentIndex;
250
+ localBitsPerColor = this.getBitsPerColor(localPalette.length / 3);
233
251
  }
234
252
  // Graphic Control Extension
235
- output.push(0x21); // Extension Introducer
236
- output.push(0xf9); // Graphic Control Label
237
- output.push(4); // Byte Size
238
- // Packed Field
239
- // Reserved (3 bits)
240
- // Disposal Method (3 bits): 2 (Restore to background) - usually safe for animation
241
- // User Input Flag (1 bit): 0
242
- // Transparent Color Flag (1 bit): 0
243
- output.push(0x08); // Disposal method 2 (Restore to background)
244
- // Delay Time (1/100ths of a second)
245
- // Default to 10 (100ms) if not specified
253
+ output.push(0x21, 0xf9, 4);
254
+ // Packed: disposal method + transparency flag
255
+ const disposal = frame.disposal & 0x07;
256
+ const gcePacked = (disposal << 2) | (hasTransparency ? 1 : 0);
257
+ output.push(gcePacked);
258
+ // Delay time
246
259
  const delay = frame.delay > 0 ? Math.round(frame.delay / 10) : 10;
247
260
  this.writeUint16LE(output, delay);
248
- // Transparent Color Index
249
- output.push(0);
261
+ // Transparent color index
262
+ output.push(hasTransparency ? transparentIndex : 0);
250
263
  output.push(0); // Block Terminator
251
264
  // Image Descriptor
252
- output.push(0x2c); // Image Separator
253
- this.writeUint16LE(output, 0); // Left
254
- this.writeUint16LE(output, 0); // Top
255
- this.writeUint16LE(output, this.width);
256
- this.writeUint16LE(output, this.height);
257
- // Packed Field
265
+ output.push(0x2c);
266
+ this.writeUint16LE(output, frame.left);
267
+ this.writeUint16LE(output, frame.top);
268
+ this.writeUint16LE(output, frame.width);
269
+ this.writeUint16LE(output, frame.height);
258
270
  if (useLocalPalette && localPalette) {
259
- // LCT Flag: 1
260
- // Interlace: 0
261
- // Sort: 0
262
- // Reserved: 0
263
- // Size of LCT: localBitsPerColor - 1
264
271
  const lctPacked = 0x80 | (localBitsPerColor - 1);
265
272
  output.push(lctPacked);
266
- // Write Local Color Table
267
273
  const lctSize = 1 << localBitsPerColor;
268
274
  const paddedLct = new Uint8Array(lctSize * 3);
269
275
  paddedLct.set(localPalette);
270
276
  this.writeBytes(output, paddedLct);
271
277
  }
272
278
  else {
273
- output.push(0); // No LCT
279
+ output.push(0);
274
280
  }
275
- // LZW Minimum Code Size
281
+ // LZW compression
276
282
  const minCodeSize = Math.max(2, useLocalPalette ? localBitsPerColor : bitsPerColor);
277
283
  output.push(minCodeSize);
278
- // Compress image data with LZW
279
284
  const encoder = new LZWEncoder(minCodeSize);
280
285
  const compressed = encoder.compress(indexed);
281
- // Write compressed data in sub-blocks (max 255 bytes per block)
282
286
  for (let k = 0; k < compressed.length; k += 255) {
283
287
  const blockSize = Math.min(255, compressed.length - k);
284
288
  output.push(blockSize);
@@ -288,8 +292,7 @@ export class GIFEncoder {
288
292
  }
289
293
  output.push(0); // Block Terminator
290
294
  }
291
- // Trailer
292
- output.push(0x3b);
295
+ output.push(0x3b); // Trailer
293
296
  return new Uint8Array(output);
294
297
  }
295
298
  }
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Deflate compression/decompression for TIFF
3
- * Uses native JavaScript CompressionStream/DecompressionStream APIs
4
3
  * Compression code: 8 (Adobe-style Deflate)
5
4
  */
6
5
  /**
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Deflate compression/decompression for TIFF
3
- * Uses native JavaScript CompressionStream/DecompressionStream APIs
4
3
  * Compression code: 8 (Adobe-style Deflate)
5
4
  */
6
5
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cross-image",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "A pure JavaScript, dependency-free, cross-runtime image processing library for Deno, Node.js, and Bun.",
5
5
  "keywords": [
6
6
  "image",
@@ -48,6 +48,7 @@ export declare class GIFFormat implements ImageFormat {
48
48
  * Encode multi-frame image data to animated GIF
49
49
  */
50
50
  encodeFrames(imageData: MultiFrameImageData, options?: GIFEncoderOptions): Promise<Uint8Array>;
51
+ private mapDisposalMethodToNumber;
51
52
  private mapDisposalMethod;
52
53
  private decodeUsingRuntime;
53
54
  private readDataSubBlocks;
@@ -246,12 +246,29 @@ class GIFFormat {
246
246
  }
247
247
  const encoder = new gif_encoder_js_1.GIFEncoder(imageData.width, imageData.height);
248
248
  for (const frame of imageData.frames) {
249
- // Get delay from metadata (default to 100ms if not set)
250
249
  const delay = frame.frameMetadata?.delay ?? 100;
251
- encoder.addFrame(frame.data, delay);
250
+ encoder.addFrame(frame.data, delay, {
251
+ left: frame.frameMetadata?.left ?? 0,
252
+ top: frame.frameMetadata?.top ?? 0,
253
+ width: frame.width,
254
+ height: frame.height,
255
+ disposal: this.mapDisposalMethodToNumber(frame.frameMetadata?.disposal),
256
+ });
252
257
  }
253
258
  return Promise.resolve(encoder.encode(options));
254
259
  }
260
+ mapDisposalMethodToNumber(disposal) {
261
+ switch (disposal) {
262
+ case "none":
263
+ return 1;
264
+ case "background":
265
+ return 2;
266
+ case "previous":
267
+ return 3;
268
+ default:
269
+ return 0;
270
+ }
271
+ }
255
272
  mapDisposalMethod(disposal) {
256
273
  switch (disposal) {
257
274
  case 0:
@@ -3,11 +3,20 @@
3
3
  * Supports GIF89a format with LZW compression
4
4
  */
5
5
  export declare class GIFEncoder {
6
- private width;
7
- private height;
6
+ private canvasWidth;
7
+ private canvasHeight;
8
8
  private frames;
9
9
  constructor(width: number, height: number, data?: Uint8Array);
10
- addFrame(data: Uint8Array, delay?: number): void;
10
+ /**
11
+ * Add a frame with optional metadata for partial frame support
12
+ */
13
+ addFrame(data: Uint8Array, delay?: number, options?: {
14
+ left?: number;
15
+ top?: number;
16
+ width?: number;
17
+ height?: number;
18
+ disposal?: number;
19
+ }): void;
11
20
  private writeBytes;
12
21
  private writeUint16LE;
13
22
  private writeString;
@@ -20,7 +29,7 @@ export declare class GIFEncoder {
20
29
  */
21
30
  private quantizeChannel;
22
31
  /**
23
- * Quantize RGBA image to 256 colors using median cut algorithm
32
+ * Quantize RGBA image to 256 colors with transparency support
24
33
  */
25
34
  private quantize;
26
35
  private nextPowerOf2;
@@ -8,13 +8,13 @@ exports.GIFEncoder = void 0;
8
8
  const lzw_js_1 = require("./lzw.js");
9
9
  class GIFEncoder {
10
10
  constructor(width, height, data) {
11
- Object.defineProperty(this, "width", {
11
+ Object.defineProperty(this, "canvasWidth", {
12
12
  enumerable: true,
13
13
  configurable: true,
14
14
  writable: true,
15
15
  value: void 0
16
16
  });
17
- Object.defineProperty(this, "height", {
17
+ Object.defineProperty(this, "canvasHeight", {
18
18
  enumerable: true,
19
19
  configurable: true,
20
20
  writable: true,
@@ -26,14 +26,28 @@ class GIFEncoder {
26
26
  writable: true,
27
27
  value: []
28
28
  });
29
- this.width = width;
30
- this.height = height;
29
+ this.canvasWidth = width;
30
+ this.canvasHeight = height;
31
31
  if (data) {
32
32
  this.addFrame(data);
33
33
  }
34
34
  }
35
- addFrame(data, delay = 0) {
36
- this.frames.push({ data, delay });
35
+ /**
36
+ * Add a frame with optional metadata for partial frame support
37
+ */
38
+ addFrame(data, delay = 0, options) {
39
+ const pixelCount = data.length / 4;
40
+ const frameWidth = options?.width ?? this.canvasWidth;
41
+ const frameHeight = options?.height ?? Math.ceil(pixelCount / frameWidth);
42
+ this.frames.push({
43
+ data,
44
+ delay,
45
+ left: options?.left ?? 0,
46
+ top: options?.top ?? 0,
47
+ width: frameWidth,
48
+ height: frameHeight,
49
+ disposal: options?.disposal ?? 0,
50
+ });
37
51
  }
38
52
  writeBytes(output, bytes) {
39
53
  output.push(...bytes);
@@ -60,49 +74,59 @@ class GIFEncoder {
60
74
  return Math.round(Math.round(value * levels / 255) * step);
61
75
  }
62
76
  /**
63
- * Quantize RGBA image to 256 colors using median cut algorithm
77
+ * Quantize RGBA image to 256 colors with transparency support
64
78
  */
65
- quantize(data) {
66
- // Simple quantization: collect unique colors and build palette
79
+ quantize(data, frameWidth, frameHeight) {
80
+ // Check for transparent pixels (alpha < 128)
81
+ let hasTransparency = false;
82
+ for (let i = 3; i < data.length; i += 4) {
83
+ if (data[i] < 128) {
84
+ hasTransparency = true;
85
+ break;
86
+ }
87
+ }
67
88
  const colorMap = new Map();
68
89
  const colors = [];
69
- // Color quantization parameters for 8-bit palette (256 colors)
70
- // R/G: 8 levels (0-7) using 3 bits, B: 4 levels (0-3) using 2 bits (8*8*4=256)
71
- const RG_LEVELS = 7; // Max level for R/G (8 total levels: 0-7)
72
- const B_LEVELS = 3; // Max level for B (4 total levels: 0-3)
73
- const rgStep = 255 / RG_LEVELS; // Step size for R/G quantization
74
- const bStep = 255 / B_LEVELS; // Step size for B quantization
75
- // Collect unique colors
90
+ const transparentIndex = 0;
91
+ // Reserve index 0 for transparency if needed
92
+ if (hasTransparency) {
93
+ colors.push({ r: 0, g: 0, b: 0 });
94
+ }
95
+ // Color quantization parameters
96
+ const RG_LEVELS = 7;
97
+ const B_LEVELS = 3;
98
+ const rgStep = 255 / RG_LEVELS;
99
+ const bStep = 255 / B_LEVELS;
100
+ const maxColors = hasTransparency ? 255 : 256;
101
+ // Collect unique colors from opaque pixels only
76
102
  for (let i = 0; i < data.length; i += 4) {
103
+ if (data[i + 3] < 128)
104
+ continue; // Skip transparent
77
105
  const r = data[i];
78
106
  const g = data[i + 1];
79
107
  const b = data[i + 2];
80
108
  const key = `${r},${g},${b}`;
81
- if (!colorMap.has(key) && colors.length < 256) {
109
+ if (!colorMap.has(key) && colors.length < maxColors) {
82
110
  colorMap.set(key, colors.length);
83
111
  colors.push({ r, g, b });
84
112
  }
85
113
  }
86
- // Track if color reduction was applied
87
114
  let useColorReduction = false;
88
- // If we have too many colors, use simple color reduction
89
- if (colors.length >= 256) {
90
- // Downsample colors to 256 by reducing color depth
115
+ // If too many colors, reduce color depth
116
+ if (colors.length >= maxColors) {
91
117
  colorMap.clear();
92
- colors.length = 0;
118
+ colors.length = hasTransparency ? 1 : 0;
93
119
  useColorReduction = true;
94
120
  for (let i = 0; i < data.length; i += 4) {
95
- // Reduce color depth: 3 bits for R/G channels, 2 bits for B channel
96
- // This gives us 8 bits total = 256 possible colors
121
+ if (data[i + 3] < 128)
122
+ continue;
97
123
  const r = this.quantizeChannel(data[i], RG_LEVELS, rgStep);
98
124
  const g = this.quantizeChannel(data[i + 1], RG_LEVELS, rgStep);
99
125
  const b = this.quantizeChannel(data[i + 2], B_LEVELS, bStep);
100
126
  const key = `${r},${g},${b}`;
101
- if (!colorMap.has(key)) {
102
- if (colors.length < 256) {
103
- colorMap.set(key, colors.length);
104
- colors.push({ r, g, b });
105
- }
127
+ if (!colorMap.has(key) && colors.length < maxColors) {
128
+ colorMap.set(key, colors.length);
129
+ colors.push({ r, g, b });
106
130
  }
107
131
  }
108
132
  }
@@ -118,28 +142,31 @@ class GIFEncoder {
118
142
  palette[i * 3 + 1] = colors[i].g;
119
143
  palette[i * 3 + 2] = colors[i].b;
120
144
  }
121
- // Create indexed data
122
- const indexed = new Uint8Array(this.width * this.height);
123
- for (let i = 0, j = 0; i < data.length; i += 4, j++) {
145
+ // Create indexed data using frame dimensions
146
+ const indexed = new Uint8Array(frameWidth * frameHeight);
147
+ for (let i = 0, j = 0; i < data.length && j < indexed.length; i += 4, j++) {
148
+ // Handle transparent pixels
149
+ if (data[i + 3] < 128) {
150
+ indexed[j] = transparentIndex;
151
+ continue;
152
+ }
124
153
  let r = data[i];
125
154
  let g = data[i + 1];
126
155
  let b = data[i + 2];
127
- // Apply color reduction if it was used for building the palette
128
156
  if (useColorReduction) {
129
157
  r = this.quantizeChannel(r, RG_LEVELS, rgStep);
130
158
  g = this.quantizeChannel(g, RG_LEVELS, rgStep);
131
159
  b = this.quantizeChannel(b, B_LEVELS, bStep);
132
160
  }
133
161
  const key = `${r},${g},${b}`;
134
- // Try fast O(1) lookup first
135
162
  if (colorMap.has(key)) {
136
163
  indexed[j] = colorMap.get(key);
137
164
  }
138
165
  else {
139
- // Fallback: find closest color in palette (shouldn't happen often)
166
+ // Find closest color
140
167
  let minDist = Infinity;
141
- let bestIdx = 0;
142
- for (let k = 0; k < colors.length; k++) {
168
+ let bestIdx = hasTransparency ? 1 : 0;
169
+ for (let k = hasTransparency ? 1 : 0; k < colors.length; k++) {
143
170
  const dr = r - colors[k].r;
144
171
  const dg = g - colors[k].g;
145
172
  const db = b - colors[k].b;
@@ -152,7 +179,7 @@ class GIFEncoder {
152
179
  indexed[j] = bestIdx;
153
180
  }
154
181
  }
155
- return { palette, indexed };
182
+ return { palette, indexed, hasTransparency, transparentIndex };
156
183
  }
157
184
  nextPowerOf2(n) {
158
185
  let power = 1;
@@ -173,46 +200,34 @@ class GIFEncoder {
173
200
  throw new Error("No frames to encode");
174
201
  }
175
202
  const output = [];
176
- // Get loop count from options (default to 0 = infinite)
177
203
  const loopCount = options?.loop ?? 0;
178
204
  // Quantize first frame for Global Color Table
179
205
  const firstFrame = this.frames[0];
180
- const { palette: globalPalette, indexed: firstIndexed } = this.quantize(firstFrame.data);
206
+ const firstResult = this.quantize(firstFrame.data, firstFrame.width, firstFrame.height);
207
+ const globalPalette = firstResult.palette;
181
208
  const paletteSize = globalPalette.length / 3;
182
209
  const bitsPerColor = this.getBitsPerColor(paletteSize);
183
210
  // Header
184
211
  this.writeString(output, "GIF89a");
185
212
  // Logical Screen Descriptor
186
- this.writeUint16LE(output, this.width);
187
- this.writeUint16LE(output, this.height);
188
- // Packed field:
189
- // - Global Color Table Flag (1 bit): 1
190
- // - Color Resolution (3 bits): bitsPerColor - 1
191
- // - Sort Flag (1 bit): 0
192
- // - Size of Global Color Table (3 bits): bitsPerColor - 1
213
+ this.writeUint16LE(output, this.canvasWidth);
214
+ this.writeUint16LE(output, this.canvasHeight);
193
215
  const packed = 0x80 | ((bitsPerColor - 1) << 4) | (bitsPerColor - 1);
194
216
  output.push(packed);
195
- // Background Color Index
196
- output.push(0);
197
- // Pixel Aspect Ratio
198
- output.push(0);
217
+ output.push(0); // Background Color Index
218
+ output.push(0); // Pixel Aspect Ratio
199
219
  // Global Color Table
200
- // The GCT size is 2^(n+1) where n is the value in the packed field
201
- // So we need to write that many colors, padding if necessary
202
220
  const gctSize = 1 << bitsPerColor;
203
221
  const paddedPalette = new Uint8Array(gctSize * 3);
204
222
  paddedPalette.set(globalPalette);
205
223
  this.writeBytes(output, paddedPalette);
206
224
  // Netscape Application Extension (Looping)
207
225
  if (this.frames.length > 1) {
208
- output.push(0x21); // Extension Introducer
209
- output.push(0xff); // Application Extension Label
210
- output.push(11); // Block Size
226
+ output.push(0x21, 0xff, 11);
211
227
  this.writeString(output, "NETSCAPE2.0");
212
- output.push(3); // Sub-block Size
213
- output.push(1); // Loop Indicator (1 = loop)
214
- this.writeUint16LE(output, loopCount); // Loop Count (0 = infinite, 1+ = specific count)
215
- output.push(0); // Block Terminator
228
+ output.push(3, 1);
229
+ this.writeUint16LE(output, loopCount);
230
+ output.push(0);
216
231
  }
217
232
  // Encode frames
218
233
  for (let i = 0; i < this.frames.length; i++) {
@@ -221,67 +236,56 @@ class GIFEncoder {
221
236
  let useLocalPalette = false;
222
237
  let localPalette = null;
223
238
  let localBitsPerColor = bitsPerColor;
239
+ let hasTransparency;
240
+ let transparentIndex;
224
241
  if (i === 0) {
225
- indexed = firstIndexed;
242
+ indexed = firstResult.indexed;
243
+ hasTransparency = firstResult.hasTransparency;
244
+ transparentIndex = firstResult.transparentIndex;
226
245
  }
227
246
  else {
228
- // Quantize subsequent frames
229
- // For simplicity, we use a Local Color Table for each frame to ensure colors are correct
230
- const result = this.quantize(frame.data);
247
+ const result = this.quantize(frame.data, frame.width, frame.height);
231
248
  indexed = result.indexed;
232
249
  localPalette = result.palette;
233
250
  useLocalPalette = true;
234
- const localPaletteSize = localPalette.length / 3;
235
- localBitsPerColor = this.getBitsPerColor(localPaletteSize);
251
+ hasTransparency = result.hasTransparency;
252
+ transparentIndex = result.transparentIndex;
253
+ localBitsPerColor = this.getBitsPerColor(localPalette.length / 3);
236
254
  }
237
255
  // Graphic Control Extension
238
- output.push(0x21); // Extension Introducer
239
- output.push(0xf9); // Graphic Control Label
240
- output.push(4); // Byte Size
241
- // Packed Field
242
- // Reserved (3 bits)
243
- // Disposal Method (3 bits): 2 (Restore to background) - usually safe for animation
244
- // User Input Flag (1 bit): 0
245
- // Transparent Color Flag (1 bit): 0
246
- output.push(0x08); // Disposal method 2 (Restore to background)
247
- // Delay Time (1/100ths of a second)
248
- // Default to 10 (100ms) if not specified
256
+ output.push(0x21, 0xf9, 4);
257
+ // Packed: disposal method + transparency flag
258
+ const disposal = frame.disposal & 0x07;
259
+ const gcePacked = (disposal << 2) | (hasTransparency ? 1 : 0);
260
+ output.push(gcePacked);
261
+ // Delay time
249
262
  const delay = frame.delay > 0 ? Math.round(frame.delay / 10) : 10;
250
263
  this.writeUint16LE(output, delay);
251
- // Transparent Color Index
252
- output.push(0);
264
+ // Transparent color index
265
+ output.push(hasTransparency ? transparentIndex : 0);
253
266
  output.push(0); // Block Terminator
254
267
  // Image Descriptor
255
- output.push(0x2c); // Image Separator
256
- this.writeUint16LE(output, 0); // Left
257
- this.writeUint16LE(output, 0); // Top
258
- this.writeUint16LE(output, this.width);
259
- this.writeUint16LE(output, this.height);
260
- // Packed Field
268
+ output.push(0x2c);
269
+ this.writeUint16LE(output, frame.left);
270
+ this.writeUint16LE(output, frame.top);
271
+ this.writeUint16LE(output, frame.width);
272
+ this.writeUint16LE(output, frame.height);
261
273
  if (useLocalPalette && localPalette) {
262
- // LCT Flag: 1
263
- // Interlace: 0
264
- // Sort: 0
265
- // Reserved: 0
266
- // Size of LCT: localBitsPerColor - 1
267
274
  const lctPacked = 0x80 | (localBitsPerColor - 1);
268
275
  output.push(lctPacked);
269
- // Write Local Color Table
270
276
  const lctSize = 1 << localBitsPerColor;
271
277
  const paddedLct = new Uint8Array(lctSize * 3);
272
278
  paddedLct.set(localPalette);
273
279
  this.writeBytes(output, paddedLct);
274
280
  }
275
281
  else {
276
- output.push(0); // No LCT
282
+ output.push(0);
277
283
  }
278
- // LZW Minimum Code Size
284
+ // LZW compression
279
285
  const minCodeSize = Math.max(2, useLocalPalette ? localBitsPerColor : bitsPerColor);
280
286
  output.push(minCodeSize);
281
- // Compress image data with LZW
282
287
  const encoder = new lzw_js_1.LZWEncoder(minCodeSize);
283
288
  const compressed = encoder.compress(indexed);
284
- // Write compressed data in sub-blocks (max 255 bytes per block)
285
289
  for (let k = 0; k < compressed.length; k += 255) {
286
290
  const blockSize = Math.min(255, compressed.length - k);
287
291
  output.push(blockSize);
@@ -291,8 +295,7 @@ class GIFEncoder {
291
295
  }
292
296
  output.push(0); // Block Terminator
293
297
  }
294
- // Trailer
295
- output.push(0x3b);
298
+ output.push(0x3b); // Trailer
296
299
  return new Uint8Array(output);
297
300
  }
298
301
  }
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Deflate compression/decompression for TIFF
3
- * Uses native JavaScript CompressionStream/DecompressionStream APIs
4
3
  * Compression code: 8 (Adobe-style Deflate)
5
4
  */
6
5
  /**
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Deflate compression/decompression for TIFF
4
- * Uses native JavaScript CompressionStream/DecompressionStream APIs
5
4
  * Compression code: 8 (Adobe-style Deflate)
6
5
  */
7
6
  Object.defineProperty(exports, "__esModule", { value: true });