cross-image 0.2.0 → 0.2.2

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 (62) hide show
  1. package/README.md +41 -28
  2. package/esm/mod.d.ts +4 -1
  3. package/esm/mod.js +4 -1
  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.js +4 -4
  9. package/esm/src/formats/gif.d.ts +0 -2
  10. package/esm/src/formats/gif.js +10 -16
  11. package/esm/src/formats/ico.d.ts +41 -0
  12. package/esm/src/formats/ico.js +214 -0
  13. package/esm/src/formats/pcx.js +1 -1
  14. package/esm/src/formats/png.d.ts +2 -21
  15. package/esm/src/formats/png.js +5 -429
  16. package/esm/src/formats/png_base.d.ts +108 -0
  17. package/esm/src/formats/png_base.js +487 -0
  18. package/esm/src/formats/ppm.d.ts +50 -0
  19. package/esm/src/formats/ppm.js +242 -0
  20. package/esm/src/formats/tiff.d.ts +4 -0
  21. package/esm/src/formats/tiff.js +163 -44
  22. package/esm/src/formats/webp.d.ts +0 -1
  23. package/esm/src/formats/webp.js +4 -7
  24. package/esm/src/image.d.ts +30 -0
  25. package/esm/src/image.js +62 -1
  26. package/esm/src/utils/byte_utils.d.ts +30 -0
  27. package/esm/src/utils/byte_utils.js +50 -0
  28. package/esm/src/utils/gif_encoder.d.ts +3 -2
  29. package/esm/src/utils/gif_encoder.js +115 -48
  30. package/esm/src/utils/image_processing.d.ts +43 -0
  31. package/esm/src/utils/image_processing.js +230 -0
  32. package/package.json +1 -1
  33. package/script/mod.d.ts +4 -1
  34. package/script/mod.js +8 -2
  35. package/script/src/formats/apng.d.ts +50 -0
  36. package/script/src/formats/apng.js +368 -0
  37. package/script/src/formats/bmp.d.ts +0 -6
  38. package/script/src/formats/bmp.js +24 -47
  39. package/script/src/formats/dng.js +4 -4
  40. package/script/src/formats/gif.d.ts +0 -2
  41. package/script/src/formats/gif.js +10 -16
  42. package/script/src/formats/ico.d.ts +41 -0
  43. package/script/src/formats/ico.js +218 -0
  44. package/script/src/formats/pcx.js +1 -1
  45. package/script/src/formats/png.d.ts +2 -21
  46. package/script/src/formats/png.js +5 -429
  47. package/script/src/formats/png_base.d.ts +108 -0
  48. package/script/src/formats/png_base.js +491 -0
  49. package/script/src/formats/ppm.d.ts +50 -0
  50. package/script/src/formats/ppm.js +246 -0
  51. package/script/src/formats/tiff.d.ts +4 -0
  52. package/script/src/formats/tiff.js +163 -44
  53. package/script/src/formats/webp.d.ts +0 -1
  54. package/script/src/formats/webp.js +4 -7
  55. package/script/src/image.d.ts +30 -0
  56. package/script/src/image.js +61 -0
  57. package/script/src/utils/byte_utils.d.ts +30 -0
  58. package/script/src/utils/byte_utils.js +58 -0
  59. package/script/src/utils/gif_encoder.d.ts +3 -2
  60. package/script/src/utils/gif_encoder.js +115 -48
  61. package/script/src/utils/image_processing.d.ts +43 -0
  62. package/script/src/utils/image_processing.js +235 -0
package/esm/src/image.js CHANGED
@@ -1,14 +1,17 @@
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
+ import { adjustBrightness, adjustContrast, adjustExposure, adjustSaturation, boxBlur, composite, crop, fillRect, gaussianBlur, grayscale, invert, medianFilter, sepia, sharpen, } from "./utils/image_processing.js";
3
3
  import { PNGFormat } from "./formats/png.js";
4
+ import { APNGFormat } from "./formats/apng.js";
4
5
  import { JPEGFormat } from "./formats/jpeg.js";
5
6
  import { WebPFormat } from "./formats/webp.js";
6
7
  import { GIFFormat } from "./formats/gif.js";
7
8
  import { TIFFFormat } from "./formats/tiff.js";
8
9
  import { BMPFormat } from "./formats/bmp.js";
10
+ import { ICOFormat } from "./formats/ico.js";
9
11
  import { DNGFormat } from "./formats/dng.js";
10
12
  import { PAMFormat } from "./formats/pam.js";
11
13
  import { PCXFormat } from "./formats/pcx.js";
14
+ import { PPMFormat } from "./formats/ppm.js";
12
15
  import { ASCIIFormat } from "./formats/ascii.js";
13
16
  import { validateImageDimensions } from "./utils/security.js";
14
17
  /**
@@ -456,6 +459,61 @@ export class Image {
456
459
  this.imageData.data = grayscale(this.imageData.data);
457
460
  return this;
458
461
  }
462
+ /**
463
+ * Apply sepia tone effect to the image
464
+ * @returns This image instance for chaining
465
+ */
466
+ sepia() {
467
+ if (!this.imageData)
468
+ throw new Error("No image loaded");
469
+ this.imageData.data = sepia(this.imageData.data);
470
+ return this;
471
+ }
472
+ /**
473
+ * Apply box blur filter to the image
474
+ * @param radius Blur radius (default: 1)
475
+ * @returns This image instance for chaining
476
+ */
477
+ blur(radius = 1) {
478
+ if (!this.imageData)
479
+ throw new Error("No image loaded");
480
+ this.imageData.data = boxBlur(this.imageData.data, this.imageData.width, this.imageData.height, radius);
481
+ return this;
482
+ }
483
+ /**
484
+ * Apply Gaussian blur filter to the image
485
+ * @param radius Blur radius (default: 1)
486
+ * @param sigma Optional standard deviation (if not provided, calculated from radius)
487
+ * @returns This image instance for chaining
488
+ */
489
+ gaussianBlur(radius = 1, sigma) {
490
+ if (!this.imageData)
491
+ throw new Error("No image loaded");
492
+ this.imageData.data = gaussianBlur(this.imageData.data, this.imageData.width, this.imageData.height, radius, sigma);
493
+ return this;
494
+ }
495
+ /**
496
+ * Apply sharpen filter to the image
497
+ * @param amount Sharpening amount (0-1, default: 0.5)
498
+ * @returns This image instance for chaining
499
+ */
500
+ sharpen(amount = 0.5) {
501
+ if (!this.imageData)
502
+ throw new Error("No image loaded");
503
+ this.imageData.data = sharpen(this.imageData.data, this.imageData.width, this.imageData.height, amount);
504
+ return this;
505
+ }
506
+ /**
507
+ * Apply median filter to reduce noise
508
+ * @param radius Filter radius (default: 1)
509
+ * @returns This image instance for chaining
510
+ */
511
+ medianFilter(radius = 1) {
512
+ if (!this.imageData)
513
+ throw new Error("No image loaded");
514
+ this.imageData.data = medianFilter(this.imageData.data, this.imageData.width, this.imageData.height, radius);
515
+ return this;
516
+ }
459
517
  /**
460
518
  * Fill a rectangular region with a color
461
519
  * @param x Starting X position
@@ -551,14 +609,17 @@ Object.defineProperty(Image, "formats", {
551
609
  writable: true,
552
610
  value: [
553
611
  new PNGFormat(),
612
+ new APNGFormat(),
554
613
  new JPEGFormat(),
555
614
  new WebPFormat(),
556
615
  new GIFFormat(),
557
616
  new TIFFFormat(),
558
617
  new BMPFormat(),
618
+ new ICOFormat(),
559
619
  new DNGFormat(),
560
620
  new PAMFormat(),
561
621
  new PCXFormat(),
622
+ new PPMFormat(),
562
623
  new ASCIIFormat(),
563
624
  ]
564
625
  });
@@ -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);
@@ -88,4 +88,47 @@ export declare function crop(data: Uint8Array, width: number, height: number, x:
88
88
  width: number;
89
89
  height: number;
90
90
  };
91
+ /**
92
+ * Apply a box blur filter to an image
93
+ * @param data Image data (RGBA)
94
+ * @param width Image width
95
+ * @param height Image height
96
+ * @param radius Blur radius (default: 1)
97
+ * @returns New image data with box blur applied
98
+ */
99
+ export declare function boxBlur(data: Uint8Array, width: number, height: number, radius?: number): Uint8Array;
100
+ /**
101
+ * Apply Gaussian blur to an image
102
+ * @param data Image data (RGBA)
103
+ * @param width Image width
104
+ * @param height Image height
105
+ * @param radius Blur radius (default: 1)
106
+ * @param sigma Optional standard deviation (if not provided, calculated from radius)
107
+ * @returns New image data with Gaussian blur applied
108
+ */
109
+ export declare function gaussianBlur(data: Uint8Array, width: number, height: number, radius?: number, sigma?: number): Uint8Array;
110
+ /**
111
+ * Apply sharpen filter to an image
112
+ * @param data Image data (RGBA)
113
+ * @param width Image width
114
+ * @param height Image height
115
+ * @param amount Sharpening amount (0-1, default: 0.5)
116
+ * @returns New image data with sharpening applied
117
+ */
118
+ export declare function sharpen(data: Uint8Array, width: number, height: number, amount?: number): Uint8Array;
119
+ /**
120
+ * Apply sepia tone effect to an image
121
+ * @param data Image data (RGBA)
122
+ * @returns New image data with sepia tone applied
123
+ */
124
+ export declare function sepia(data: Uint8Array): Uint8Array;
125
+ /**
126
+ * Apply median filter to reduce noise
127
+ * @param data Image data (RGBA)
128
+ * @param width Image width
129
+ * @param height Image height
130
+ * @param radius Filter radius (default: 1)
131
+ * @returns New image data with median filter applied
132
+ */
133
+ export declare function medianFilter(data: Uint8Array, width: number, height: number, radius?: number): Uint8Array;
91
134
  //# sourceMappingURL=image_processing.d.ts.map