cross-image 0.1.5 → 0.2.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 (72) hide show
  1. package/README.md +36 -18
  2. package/esm/mod.d.ts +30 -4
  3. package/esm/mod.js +30 -4
  4. package/esm/src/formats/apng.d.ts +50 -0
  5. package/esm/src/formats/apng.js +364 -0
  6. package/esm/src/formats/bmp.d.ts +0 -6
  7. package/esm/src/formats/bmp.js +24 -47
  8. package/esm/src/formats/dng.d.ts +27 -0
  9. package/esm/src/formats/dng.js +191 -0
  10. package/esm/src/formats/gif.d.ts +0 -2
  11. package/esm/src/formats/gif.js +10 -16
  12. package/esm/src/formats/ico.d.ts +41 -0
  13. package/esm/src/formats/ico.js +214 -0
  14. package/esm/src/formats/pam.d.ts +43 -0
  15. package/esm/src/formats/pam.js +177 -0
  16. package/esm/src/formats/pcx.d.ts +13 -0
  17. package/esm/src/formats/pcx.js +204 -0
  18. package/esm/src/formats/png.d.ts +2 -21
  19. package/esm/src/formats/png.js +5 -429
  20. package/esm/src/formats/png_base.d.ts +108 -0
  21. package/esm/src/formats/png_base.js +487 -0
  22. package/esm/src/formats/tiff.d.ts +7 -7
  23. package/esm/src/formats/webp.d.ts +0 -1
  24. package/esm/src/formats/webp.js +4 -7
  25. package/esm/src/image.d.ts +99 -0
  26. package/esm/src/image.js +204 -2
  27. package/esm/src/utils/byte_utils.d.ts +30 -0
  28. package/esm/src/utils/byte_utils.js +50 -0
  29. package/esm/src/utils/gif_encoder.d.ts +3 -2
  30. package/esm/src/utils/gif_encoder.js +115 -48
  31. package/esm/src/utils/image_processing.d.ts +91 -0
  32. package/esm/src/utils/image_processing.js +231 -0
  33. package/esm/src/utils/webp_decoder.js +47 -12
  34. package/esm/src/utils/webp_encoder.js +97 -39
  35. package/package.json +4 -1
  36. package/script/mod.d.ts +30 -4
  37. package/script/mod.js +36 -6
  38. package/script/src/formats/apng.d.ts +50 -0
  39. package/script/src/formats/apng.js +368 -0
  40. package/script/src/formats/bmp.d.ts +0 -6
  41. package/script/src/formats/bmp.js +24 -47
  42. package/script/src/formats/dng.d.ts +27 -0
  43. package/script/src/formats/dng.js +195 -0
  44. package/script/src/formats/gif.d.ts +0 -2
  45. package/script/src/formats/gif.js +10 -16
  46. package/script/src/formats/ico.d.ts +41 -0
  47. package/script/src/formats/ico.js +218 -0
  48. package/script/src/formats/pam.d.ts +43 -0
  49. package/script/src/formats/pam.js +181 -0
  50. package/script/src/formats/pcx.d.ts +13 -0
  51. package/script/src/formats/pcx.js +208 -0
  52. package/script/src/formats/png.d.ts +2 -21
  53. package/script/src/formats/png.js +5 -429
  54. package/script/src/formats/png_base.d.ts +108 -0
  55. package/script/src/formats/png_base.js +491 -0
  56. package/script/src/formats/tiff.d.ts +7 -7
  57. package/script/src/formats/webp.d.ts +0 -1
  58. package/script/src/formats/webp.js +4 -7
  59. package/script/src/image.d.ts +99 -0
  60. package/script/src/image.js +204 -2
  61. package/script/src/utils/byte_utils.d.ts +30 -0
  62. package/script/src/utils/byte_utils.js +58 -0
  63. package/script/src/utils/gif_encoder.d.ts +3 -2
  64. package/script/src/utils/gif_encoder.js +115 -48
  65. package/script/src/utils/image_processing.d.ts +91 -0
  66. package/script/src/utils/image_processing.js +242 -0
  67. package/script/src/utils/webp_decoder.js +47 -12
  68. package/script/src/utils/webp_encoder.js +97 -39
  69. package/esm/src/formats/raw.d.ts +0 -40
  70. package/esm/src/formats/raw.js +0 -118
  71. package/script/src/formats/raw.d.ts +0 -40
  72. package/script/src/formats/raw.js +0 -122
package/esm/src/image.js CHANGED
@@ -1,11 +1,16 @@
1
1
  import { resizeBilinear, resizeNearest } from "./utils/resize.js";
2
+ import { adjustBrightness, adjustContrast, adjustExposure, adjustSaturation, composite, crop, fillRect, grayscale, invert, } from "./utils/image_processing.js";
2
3
  import { PNGFormat } from "./formats/png.js";
4
+ import { APNGFormat } from "./formats/apng.js";
3
5
  import { JPEGFormat } from "./formats/jpeg.js";
4
6
  import { WebPFormat } from "./formats/webp.js";
5
7
  import { GIFFormat } from "./formats/gif.js";
6
8
  import { TIFFFormat } from "./formats/tiff.js";
7
9
  import { BMPFormat } from "./formats/bmp.js";
8
- import { RAWFormat } from "./formats/raw.js";
10
+ import { ICOFormat } from "./formats/ico.js";
11
+ import { DNGFormat } from "./formats/dng.js";
12
+ import { PAMFormat } from "./formats/pam.js";
13
+ import { PCXFormat } from "./formats/pcx.js";
9
14
  import { ASCIIFormat } from "./formats/ascii.js";
10
15
  import { validateImageDimensions } from "./utils/security.js";
11
16
  /**
@@ -262,6 +267,31 @@ export class Image {
262
267
  image.imageData = { width, height, data: new Uint8Array(data) };
263
268
  return image;
264
269
  }
270
+ /**
271
+ * Create a blank image with the specified dimensions and color
272
+ * @param width Image width
273
+ * @param height Image height
274
+ * @param r Red component (0-255, default: 0)
275
+ * @param g Green component (0-255, default: 0)
276
+ * @param b Blue component (0-255, default: 0)
277
+ * @param a Alpha component (0-255, default: 255)
278
+ * @returns Image instance
279
+ */
280
+ static create(width, height, r = 0, g = 0, b = 0, a = 255) {
281
+ // Validate dimensions for security (prevent integer overflow and heap exhaustion)
282
+ validateImageDimensions(width, height);
283
+ const data = new Uint8Array(width * height * 4);
284
+ // Fill with the specified color
285
+ for (let i = 0; i < data.length; i += 4) {
286
+ data[i] = r;
287
+ data[i + 1] = g;
288
+ data[i + 2] = b;
289
+ data[i + 3] = a;
290
+ }
291
+ const image = new Image();
292
+ image.imageData = { width, height, data };
293
+ return image;
294
+ }
265
295
  /**
266
296
  * Resize the image
267
297
  * @param options Resize options
@@ -348,6 +378,174 @@ export class Image {
348
378
  };
349
379
  return image;
350
380
  }
381
+ /**
382
+ * Composite another image on top of this image at the specified position
383
+ * @param overlay Image to place on top
384
+ * @param x X position (can be negative)
385
+ * @param y Y position (can be negative)
386
+ * @param opacity Opacity of overlay (0-1, default: 1)
387
+ * @returns This image instance for chaining
388
+ */
389
+ composite(overlay, x, y, opacity = 1) {
390
+ if (!this.imageData)
391
+ throw new Error("No image loaded");
392
+ if (!overlay.imageData)
393
+ throw new Error("Overlay has no image loaded");
394
+ this.imageData.data = composite(this.imageData.data, this.imageData.width, this.imageData.height, overlay.imageData.data, overlay.imageData.width, overlay.imageData.height, x, y, opacity);
395
+ return this;
396
+ }
397
+ /**
398
+ * Adjust brightness of the image
399
+ * @param amount Brightness adjustment (-1 to 1, where 0 is no change)
400
+ * @returns This image instance for chaining
401
+ */
402
+ brightness(amount) {
403
+ if (!this.imageData)
404
+ throw new Error("No image loaded");
405
+ this.imageData.data = adjustBrightness(this.imageData.data, amount);
406
+ return this;
407
+ }
408
+ /**
409
+ * Adjust contrast of the image
410
+ * @param amount Contrast adjustment (-1 to 1, where 0 is no change)
411
+ * @returns This image instance for chaining
412
+ */
413
+ contrast(amount) {
414
+ if (!this.imageData)
415
+ throw new Error("No image loaded");
416
+ this.imageData.data = adjustContrast(this.imageData.data, amount);
417
+ return this;
418
+ }
419
+ /**
420
+ * Adjust exposure of the image
421
+ * @param amount Exposure adjustment in stops (-3 to 3, where 0 is no change)
422
+ * @returns This image instance for chaining
423
+ */
424
+ exposure(amount) {
425
+ if (!this.imageData)
426
+ throw new Error("No image loaded");
427
+ this.imageData.data = adjustExposure(this.imageData.data, amount);
428
+ return this;
429
+ }
430
+ /**
431
+ * Adjust saturation of the image
432
+ * @param amount Saturation adjustment (-1 to 1, where 0 is no change)
433
+ * @returns This image instance for chaining
434
+ */
435
+ saturation(amount) {
436
+ if (!this.imageData)
437
+ throw new Error("No image loaded");
438
+ this.imageData.data = adjustSaturation(this.imageData.data, amount);
439
+ return this;
440
+ }
441
+ /**
442
+ * Invert colors of the image
443
+ * @returns This image instance for chaining
444
+ */
445
+ invert() {
446
+ if (!this.imageData)
447
+ throw new Error("No image loaded");
448
+ this.imageData.data = invert(this.imageData.data);
449
+ return this;
450
+ }
451
+ /**
452
+ * Convert the image to grayscale
453
+ * @returns This image instance for chaining
454
+ */
455
+ grayscale() {
456
+ if (!this.imageData)
457
+ throw new Error("No image loaded");
458
+ this.imageData.data = grayscale(this.imageData.data);
459
+ return this;
460
+ }
461
+ /**
462
+ * Fill a rectangular region with a color
463
+ * @param x Starting X position
464
+ * @param y Starting Y position
465
+ * @param width Width of the fill region
466
+ * @param height Height of the fill region
467
+ * @param r Red component (0-255)
468
+ * @param g Green component (0-255)
469
+ * @param b Blue component (0-255)
470
+ * @param a Alpha component (0-255, default: 255)
471
+ * @returns This image instance for chaining
472
+ */
473
+ fillRect(x, y, width, height, r, g, b, a = 255) {
474
+ if (!this.imageData)
475
+ throw new Error("No image loaded");
476
+ this.imageData.data = fillRect(this.imageData.data, this.imageData.width, this.imageData.height, x, y, width, height, r, g, b, a);
477
+ return this;
478
+ }
479
+ /**
480
+ * Crop the image to a rectangular region
481
+ * @param x Starting X position
482
+ * @param y Starting Y position
483
+ * @param width Width of the crop region
484
+ * @param height Height of the crop region
485
+ * @returns This image instance for chaining
486
+ */
487
+ crop(x, y, width, height) {
488
+ if (!this.imageData)
489
+ throw new Error("No image loaded");
490
+ const result = crop(this.imageData.data, this.imageData.width, this.imageData.height, x, y, width, height);
491
+ this.imageData.width = result.width;
492
+ this.imageData.height = result.height;
493
+ this.imageData.data = result.data;
494
+ // Update physical dimensions if DPI is set
495
+ if (this.imageData.metadata) {
496
+ const metadata = this.imageData.metadata;
497
+ if (metadata.dpiX) {
498
+ this.imageData.metadata.physicalWidth = result.width / metadata.dpiX;
499
+ }
500
+ if (metadata.dpiY) {
501
+ this.imageData.metadata.physicalHeight = result.height / metadata.dpiY;
502
+ }
503
+ }
504
+ return this;
505
+ }
506
+ /**
507
+ * Get the pixel color at the specified position
508
+ * @param x X position
509
+ * @param y Y position
510
+ * @returns Object with r, g, b, a components (0-255) or undefined if out of bounds
511
+ */
512
+ getPixel(x, y) {
513
+ if (!this.imageData)
514
+ throw new Error("No image loaded");
515
+ if (x < 0 || x >= this.imageData.width || y < 0 || y >= this.imageData.height) {
516
+ return undefined;
517
+ }
518
+ const idx = (y * this.imageData.width + x) * 4;
519
+ return {
520
+ r: this.imageData.data[idx],
521
+ g: this.imageData.data[idx + 1],
522
+ b: this.imageData.data[idx + 2],
523
+ a: this.imageData.data[idx + 3],
524
+ };
525
+ }
526
+ /**
527
+ * Set the pixel color at the specified position
528
+ * @param x X position
529
+ * @param y Y position
530
+ * @param r Red component (0-255)
531
+ * @param g Green component (0-255)
532
+ * @param b Blue component (0-255)
533
+ * @param a Alpha component (0-255, default: 255)
534
+ * @returns This image instance for chaining
535
+ */
536
+ setPixel(x, y, r, g, b, a = 255) {
537
+ if (!this.imageData)
538
+ throw new Error("No image loaded");
539
+ if (x < 0 || x >= this.imageData.width || y < 0 || y >= this.imageData.height) {
540
+ return this;
541
+ }
542
+ const idx = (y * this.imageData.width + x) * 4;
543
+ this.imageData.data[idx] = r;
544
+ this.imageData.data[idx + 1] = g;
545
+ this.imageData.data[idx + 2] = b;
546
+ this.imageData.data[idx + 3] = a;
547
+ return this;
548
+ }
351
549
  }
352
550
  Object.defineProperty(Image, "formats", {
353
551
  enumerable: true,
@@ -355,12 +553,16 @@ Object.defineProperty(Image, "formats", {
355
553
  writable: true,
356
554
  value: [
357
555
  new PNGFormat(),
556
+ new APNGFormat(),
358
557
  new JPEGFormat(),
359
558
  new WebPFormat(),
360
559
  new GIFFormat(),
361
560
  new TIFFFormat(),
362
561
  new BMPFormat(),
363
- new RAWFormat(),
562
+ new ICOFormat(),
563
+ new DNGFormat(),
564
+ new PAMFormat(),
565
+ new PCXFormat(),
364
566
  new ASCIIFormat(),
365
567
  ]
366
568
  });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shared byte-level read/write utilities for image formats
3
+ * These functions handle reading and writing multi-byte integers
4
+ * in little-endian byte order, commonly used in BMP, ICO, GIF, and other formats.
5
+ */
6
+ /**
7
+ * Read a 16-bit unsigned integer in little-endian format
8
+ */
9
+ export declare function readUint16LE(data: Uint8Array, offset: number): number;
10
+ /**
11
+ * Read a 32-bit unsigned integer in little-endian format
12
+ */
13
+ export declare function readUint32LE(data: Uint8Array, offset: number): number;
14
+ /**
15
+ * Read a 32-bit signed integer in little-endian format
16
+ */
17
+ export declare function readInt32LE(data: Uint8Array, offset: number): number;
18
+ /**
19
+ * Write a 16-bit unsigned integer in little-endian format
20
+ */
21
+ export declare function writeUint16LE(data: Uint8Array, offset: number, value: number): void;
22
+ /**
23
+ * Write a 32-bit unsigned integer in little-endian format
24
+ */
25
+ export declare function writeUint32LE(data: Uint8Array, offset: number, value: number): void;
26
+ /**
27
+ * Write a 32-bit signed integer in little-endian format
28
+ */
29
+ export declare function writeInt32LE(data: Uint8Array, offset: number, value: number): void;
30
+ //# sourceMappingURL=byte_utils.d.ts.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Shared byte-level read/write utilities for image formats
3
+ * These functions handle reading and writing multi-byte integers
4
+ * in little-endian byte order, commonly used in BMP, ICO, GIF, and other formats.
5
+ */
6
+ // Constants for signed/unsigned integer conversion
7
+ const INT32_MAX = 0x7fffffff;
8
+ const UINT32_RANGE = 0x100000000;
9
+ /**
10
+ * Read a 16-bit unsigned integer in little-endian format
11
+ */
12
+ export function readUint16LE(data, offset) {
13
+ return data[offset] | (data[offset + 1] << 8);
14
+ }
15
+ /**
16
+ * Read a 32-bit unsigned integer in little-endian format
17
+ */
18
+ export function readUint32LE(data, offset) {
19
+ return data[offset] | (data[offset + 1] << 8) |
20
+ (data[offset + 2] << 16) | (data[offset + 3] << 24);
21
+ }
22
+ /**
23
+ * Read a 32-bit signed integer in little-endian format
24
+ */
25
+ export function readInt32LE(data, offset) {
26
+ const value = readUint32LE(data, offset);
27
+ return value > INT32_MAX ? value - UINT32_RANGE : value;
28
+ }
29
+ /**
30
+ * Write a 16-bit unsigned integer in little-endian format
31
+ */
32
+ export function writeUint16LE(data, offset, value) {
33
+ data[offset] = value & 0xff;
34
+ data[offset + 1] = (value >>> 8) & 0xff;
35
+ }
36
+ /**
37
+ * Write a 32-bit unsigned integer in little-endian format
38
+ */
39
+ export function writeUint32LE(data, offset, value) {
40
+ data[offset] = value & 0xff;
41
+ data[offset + 1] = (value >>> 8) & 0xff;
42
+ data[offset + 2] = (value >>> 16) & 0xff;
43
+ data[offset + 3] = (value >>> 24) & 0xff;
44
+ }
45
+ /**
46
+ * Write a 32-bit signed integer in little-endian format
47
+ */
48
+ export function writeInt32LE(data, offset, value) {
49
+ writeUint32LE(data, offset, value < 0 ? value + UINT32_RANGE : value);
50
+ }
@@ -5,8 +5,9 @@
5
5
  export declare class GIFEncoder {
6
6
  private width;
7
7
  private height;
8
- private data;
9
- constructor(width: number, height: number, data: Uint8Array);
8
+ private frames;
9
+ constructor(width: number, height: number, data?: Uint8Array);
10
+ addFrame(data: Uint8Array, delay?: number): void;
10
11
  private writeBytes;
11
12
  private writeUint16LE;
12
13
  private writeString;
@@ -17,15 +17,20 @@ export class GIFEncoder {
17
17
  writable: true,
18
18
  value: void 0
19
19
  });
20
- Object.defineProperty(this, "data", {
20
+ Object.defineProperty(this, "frames", {
21
21
  enumerable: true,
22
22
  configurable: true,
23
23
  writable: true,
24
- value: void 0
24
+ value: []
25
25
  });
26
26
  this.width = width;
27
27
  this.height = height;
28
- this.data = data;
28
+ if (data) {
29
+ this.addFrame(data);
30
+ }
31
+ }
32
+ addFrame(data, delay = 0) {
33
+ this.frames.push({ data, delay });
29
34
  }
30
35
  writeBytes(output, bytes) {
31
36
  output.push(...bytes);
@@ -54,7 +59,7 @@ export class GIFEncoder {
54
59
  /**
55
60
  * Quantize RGBA image to 256 colors using median cut algorithm
56
61
  */
57
- quantize() {
62
+ quantize(data) {
58
63
  // Simple quantization: collect unique colors and build palette
59
64
  const colorMap = new Map();
60
65
  const colors = [];
@@ -65,10 +70,10 @@ export class GIFEncoder {
65
70
  const rgStep = 255 / RG_LEVELS; // Step size for R/G quantization
66
71
  const bStep = 255 / B_LEVELS; // Step size for B quantization
67
72
  // Collect unique colors
68
- for (let i = 0; i < this.data.length; i += 4) {
69
- const r = this.data[i];
70
- const g = this.data[i + 1];
71
- const b = this.data[i + 2];
73
+ for (let i = 0; i < data.length; i += 4) {
74
+ const r = data[i];
75
+ const g = data[i + 1];
76
+ const b = data[i + 2];
72
77
  const key = `${r},${g},${b}`;
73
78
  if (!colorMap.has(key) && colors.length < 256) {
74
79
  colorMap.set(key, colors.length);
@@ -83,12 +88,12 @@ export class GIFEncoder {
83
88
  colorMap.clear();
84
89
  colors.length = 0;
85
90
  useColorReduction = true;
86
- for (let i = 0; i < this.data.length; i += 4) {
91
+ for (let i = 0; i < data.length; i += 4) {
87
92
  // Reduce color depth: 3 bits for R/G channels, 2 bits for B channel
88
93
  // This gives us 8 bits total = 256 possible colors
89
- const r = this.quantizeChannel(this.data[i], RG_LEVELS, rgStep);
90
- const g = this.quantizeChannel(this.data[i + 1], RG_LEVELS, rgStep);
91
- const b = this.quantizeChannel(this.data[i + 2], B_LEVELS, bStep);
94
+ const r = this.quantizeChannel(data[i], RG_LEVELS, rgStep);
95
+ const g = this.quantizeChannel(data[i + 1], RG_LEVELS, rgStep);
96
+ const b = this.quantizeChannel(data[i + 2], B_LEVELS, bStep);
92
97
  const key = `${r},${g},${b}`;
93
98
  if (!colorMap.has(key)) {
94
99
  if (colors.length < 256) {
@@ -112,10 +117,10 @@ export class GIFEncoder {
112
117
  }
113
118
  // Create indexed data
114
119
  const indexed = new Uint8Array(this.width * this.height);
115
- for (let i = 0, j = 0; i < this.data.length; i += 4, j++) {
116
- let r = this.data[i];
117
- let g = this.data[i + 1];
118
- let b = this.data[i + 2];
120
+ for (let i = 0, j = 0; i < data.length; i += 4, j++) {
121
+ let r = data[i];
122
+ let g = data[i + 1];
123
+ let b = data[i + 2];
119
124
  // Apply color reduction if it was used for building the palette
120
125
  if (useColorReduction) {
121
126
  r = this.quantizeChannel(r, RG_LEVELS, rgStep);
@@ -161,10 +166,14 @@ export class GIFEncoder {
161
166
  return Math.max(2, bits);
162
167
  }
163
168
  encode() {
169
+ if (this.frames.length === 0) {
170
+ throw new Error("No frames to encode");
171
+ }
164
172
  const output = [];
165
- // Quantize image
166
- const { palette, indexed } = this.quantize();
167
- const paletteSize = palette.length / 3;
173
+ // Quantize first frame for Global Color Table
174
+ const firstFrame = this.frames[0];
175
+ const { palette: globalPalette, indexed: firstIndexed } = this.quantize(firstFrame.data);
176
+ const paletteSize = globalPalette.length / 3;
168
177
  const bitsPerColor = this.getBitsPerColor(paletteSize);
169
178
  // Header
170
179
  this.writeString(output, "GIF89a");
@@ -187,38 +196,96 @@ export class GIFEncoder {
187
196
  // So we need to write that many colors, padding if necessary
188
197
  const gctSize = 1 << bitsPerColor;
189
198
  const paddedPalette = new Uint8Array(gctSize * 3);
190
- paddedPalette.set(palette);
199
+ paddedPalette.set(globalPalette);
191
200
  this.writeBytes(output, paddedPalette);
192
- // Image Descriptor
193
- output.push(0x2c); // Image Separator
194
- // Image position and dimensions
195
- this.writeUint16LE(output, 0); // Left
196
- this.writeUint16LE(output, 0); // Top
197
- this.writeUint16LE(output, this.width);
198
- this.writeUint16LE(output, this.height);
199
- // Packed field:
200
- // - Local Color Table Flag (1 bit): 0
201
- // - Interlace Flag (1 bit): 0
202
- // - Sort Flag (1 bit): 0
203
- // - Reserved (2 bits): 0
204
- // - Size of Local Color Table (3 bits): 0
205
- output.push(0);
206
- // LZW Minimum Code Size
207
- const minCodeSize = Math.max(2, bitsPerColor);
208
- output.push(minCodeSize);
209
- // Compress image data with LZW
210
- const encoder = new LZWEncoder(minCodeSize);
211
- const compressed = encoder.compress(indexed);
212
- // Write compressed data in sub-blocks (max 255 bytes per block)
213
- for (let i = 0; i < compressed.length; i += 255) {
214
- const blockSize = Math.min(255, compressed.length - i);
215
- output.push(blockSize);
216
- for (let j = 0; j < blockSize; j++) {
217
- output.push(compressed[i + j]);
201
+ // Netscape Application Extension (Looping)
202
+ if (this.frames.length > 1) {
203
+ output.push(0x21); // Extension Introducer
204
+ output.push(0xff); // Application Extension Label
205
+ output.push(11); // Block Size
206
+ this.writeString(output, "NETSCAPE2.0");
207
+ output.push(3); // Sub-block Size
208
+ output.push(1); // Loop Indicator (1 = loop)
209
+ this.writeUint16LE(output, 0); // Loop Count (0 = infinite)
210
+ output.push(0); // Block Terminator
211
+ }
212
+ // Encode frames
213
+ for (let i = 0; i < this.frames.length; i++) {
214
+ const frame = this.frames[i];
215
+ let indexed;
216
+ let useLocalPalette = false;
217
+ let localPalette = null;
218
+ let localBitsPerColor = bitsPerColor;
219
+ if (i === 0) {
220
+ indexed = firstIndexed;
221
+ }
222
+ else {
223
+ // Quantize subsequent frames
224
+ // For simplicity, we use a Local Color Table for each frame to ensure colors are correct
225
+ const result = this.quantize(frame.data);
226
+ indexed = result.indexed;
227
+ localPalette = result.palette;
228
+ useLocalPalette = true;
229
+ const localPaletteSize = localPalette.length / 3;
230
+ localBitsPerColor = this.getBitsPerColor(localPaletteSize);
218
231
  }
232
+ // Graphic Control Extension
233
+ output.push(0x21); // Extension Introducer
234
+ output.push(0xf9); // Graphic Control Label
235
+ output.push(4); // Byte Size
236
+ // Packed Field
237
+ // Reserved (3 bits)
238
+ // Disposal Method (3 bits): 2 (Restore to background) - usually safe for animation
239
+ // User Input Flag (1 bit): 0
240
+ // Transparent Color Flag (1 bit): 0
241
+ output.push(0x08); // Disposal method 2 (Restore to background)
242
+ // Delay Time (1/100ths of a second)
243
+ // Default to 10 (100ms) if not specified
244
+ const delay = frame.delay > 0 ? Math.round(frame.delay / 10) : 10;
245
+ this.writeUint16LE(output, delay);
246
+ // Transparent Color Index
247
+ output.push(0);
248
+ output.push(0); // Block Terminator
249
+ // Image Descriptor
250
+ output.push(0x2c); // Image Separator
251
+ this.writeUint16LE(output, 0); // Left
252
+ this.writeUint16LE(output, 0); // Top
253
+ this.writeUint16LE(output, this.width);
254
+ this.writeUint16LE(output, this.height);
255
+ // Packed Field
256
+ if (useLocalPalette && localPalette) {
257
+ // LCT Flag: 1
258
+ // Interlace: 0
259
+ // Sort: 0
260
+ // Reserved: 0
261
+ // Size of LCT: localBitsPerColor - 1
262
+ const lctPacked = 0x80 | (localBitsPerColor - 1);
263
+ output.push(lctPacked);
264
+ // Write Local Color Table
265
+ const lctSize = 1 << localBitsPerColor;
266
+ const paddedLct = new Uint8Array(lctSize * 3);
267
+ paddedLct.set(localPalette);
268
+ this.writeBytes(output, paddedLct);
269
+ }
270
+ else {
271
+ output.push(0); // No LCT
272
+ }
273
+ // LZW Minimum Code Size
274
+ const minCodeSize = Math.max(2, useLocalPalette ? localBitsPerColor : bitsPerColor);
275
+ output.push(minCodeSize);
276
+ // Compress image data with LZW
277
+ const encoder = new LZWEncoder(minCodeSize);
278
+ const compressed = encoder.compress(indexed);
279
+ // Write compressed data in sub-blocks (max 255 bytes per block)
280
+ for (let k = 0; k < compressed.length; k += 255) {
281
+ const blockSize = Math.min(255, compressed.length - k);
282
+ output.push(blockSize);
283
+ for (let j = 0; j < blockSize; j++) {
284
+ output.push(compressed[k + j]);
285
+ }
286
+ }
287
+ output.push(0); // Block Terminator
219
288
  }
220
- // Block Terminator
221
- output.push(0);
222
289
  // Trailer
223
290
  output.push(0x3b);
224
291
  return new Uint8Array(output);
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Image processing utilities for common operations like compositing,
3
+ * level adjustments, and color manipulations.
4
+ */
5
+ /**
6
+ * Composite one image on top of another at a specified position
7
+ * @param base Base image data (RGBA)
8
+ * @param baseWidth Base image width
9
+ * @param baseHeight Base image height
10
+ * @param overlay Overlay image data (RGBA)
11
+ * @param overlayWidth Overlay image width
12
+ * @param overlayHeight Overlay image height
13
+ * @param x X position to place overlay (can be negative)
14
+ * @param y Y position to place overlay (can be negative)
15
+ * @param opacity Opacity of overlay (0-1, default: 1)
16
+ * @returns New image data with overlay composited on base
17
+ */
18
+ export declare function composite(base: Uint8Array, baseWidth: number, baseHeight: number, overlay: Uint8Array, overlayWidth: number, overlayHeight: number, x: number, y: number, opacity?: number): Uint8Array;
19
+ /**
20
+ * Adjust brightness of an image
21
+ * @param data Image data (RGBA)
22
+ * @param amount Brightness adjustment (-1 to 1, where 0 is no change)
23
+ * @returns New image data with adjusted brightness
24
+ */
25
+ export declare function adjustBrightness(data: Uint8Array, amount: number): Uint8Array;
26
+ /**
27
+ * Adjust contrast of an image
28
+ * @param data Image data (RGBA)
29
+ * @param amount Contrast adjustment (-1 to 1, where 0 is no change)
30
+ * @returns New image data with adjusted contrast
31
+ */
32
+ export declare function adjustContrast(data: Uint8Array, amount: number): Uint8Array;
33
+ /**
34
+ * Adjust exposure of an image
35
+ * @param data Image data (RGBA)
36
+ * @param amount Exposure adjustment in stops (-3 to 3, where 0 is no change)
37
+ * @returns New image data with adjusted exposure
38
+ */
39
+ export declare function adjustExposure(data: Uint8Array, amount: number): Uint8Array;
40
+ /**
41
+ * Adjust saturation of an image
42
+ * @param data Image data (RGBA)
43
+ * @param amount Saturation adjustment (-1 to 1, where 0 is no change)
44
+ * @returns New image data with adjusted saturation
45
+ */
46
+ export declare function adjustSaturation(data: Uint8Array, amount: number): Uint8Array;
47
+ /**
48
+ * Invert colors of an image
49
+ * @param data Image data (RGBA)
50
+ * @returns New image data with inverted colors
51
+ */
52
+ export declare function invert(data: Uint8Array): Uint8Array;
53
+ /**
54
+ * Convert image to grayscale
55
+ * @param data Image data (RGBA)
56
+ * @returns New image data in grayscale
57
+ */
58
+ export declare function grayscale(data: Uint8Array): Uint8Array;
59
+ /**
60
+ * Fill a rectangular region with a color
61
+ * @param data Image data (RGBA)
62
+ * @param width Image width
63
+ * @param height Image height
64
+ * @param x Starting X position
65
+ * @param y Starting Y position
66
+ * @param fillWidth Width of the fill region
67
+ * @param fillHeight Height of the fill region
68
+ * @param r Red component (0-255)
69
+ * @param g Green component (0-255)
70
+ * @param b Blue component (0-255)
71
+ * @param a Alpha component (0-255)
72
+ * @returns Modified image data
73
+ */
74
+ export declare function fillRect(data: Uint8Array, width: number, height: number, x: number, y: number, fillWidth: number, fillHeight: number, r: number, g: number, b: number, a: number): Uint8Array;
75
+ /**
76
+ * Crop an image to a rectangular region
77
+ * @param data Image data (RGBA)
78
+ * @param width Image width
79
+ * @param height Image height
80
+ * @param x Starting X position
81
+ * @param y Starting Y position
82
+ * @param cropWidth Width of the crop region
83
+ * @param cropHeight Height of the crop region
84
+ * @returns Cropped image data and dimensions
85
+ */
86
+ export declare function crop(data: Uint8Array, width: number, height: number, x: number, y: number, cropWidth: number, cropHeight: number): {
87
+ data: Uint8Array;
88
+ width: number;
89
+ height: number;
90
+ };
91
+ //# sourceMappingURL=image_processing.d.ts.map