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/README.md CHANGED
@@ -3,15 +3,20 @@
3
3
  A pure JavaScript, dependency-free, cross-runtime image processing library for
4
4
  Deno, Node.js, and Bun.
5
5
 
6
- 📚 **[Full Documentation](https://cross-org.github.io/image/)**
6
+ 📚 **[Full Documentation](https://cross-image.56k.guru/)**
7
7
 
8
8
  ## Features
9
9
 
10
10
  - 🚀 **Pure JavaScript** - No native dependencies
11
11
  - 🔌 **Pluggable formats** - Easy to extend with custom formats
12
12
  - 📦 **Cross-runtime** - Works on Deno, Node.js (18+), and Bun
13
- - 🎨 **Multiple formats** - PNG, JPEG, WebP, GIF, TIFF, BMP, and RAW support
14
- - ✂️ **Image manipulation** - Resize with multiple algorithms
13
+ - 🎨 **Multiple formats** - PNG, APNG, JPEG, WebP, GIF, TIFF, BMP, ICO, DNG,
14
+ PAM, PCX and ASCII support
15
+ - ✂️ **Image manipulation** - Resize, crop, composite, and more
16
+ - 🎛️ **Image processing** - Chainable `brightness`, `contrast`, `saturation`,
17
+ and `exposure` helpers
18
+ - 🖌️ **Drawing operations** - Create, fill, and manipulate pixels
19
+ - 🧩 **Multi-frame** - Decode/encode animated GIFs, APNGs and multi-page TIFFs
15
20
  - 🔧 **Simple API** - Easy to use, intuitive interface
16
21
 
17
22
  ## Installation
@@ -55,11 +60,20 @@ const image = await Image.decode(data);
55
60
 
56
61
  console.log(`Image size: ${image.width}x${image.height}`);
57
62
 
58
- // Resize the image
59
- image.resize({ width: 800, height: 600 });
63
+ // Create a new blank image
64
+ const canvas = Image.create(800, 600, 255, 255, 255); // white background
65
+
66
+ // Composite the loaded image on top
67
+ canvas.composite(image, 50, 50);
68
+
69
+ // Apply image processing
70
+ canvas
71
+ .brightness(0.1)
72
+ .contrast(0.2)
73
+ .saturation(-0.1);
60
74
 
61
75
  // Encode in a different format
62
- const jpeg = await image.encode("jpeg");
76
+ const jpeg = await canvas.encode("jpeg");
63
77
  await Deno.writeFile("output.jpg", jpeg);
64
78
  ```
65
79
 
@@ -71,7 +85,7 @@ import { readFile, writeFile } from "node:fs/promises";
71
85
 
72
86
  // Read an image (auto-detects format)
73
87
  const data = await readFile("input.png");
74
- const image = await Image.read(data);
88
+ const image = await Image.decode(data);
75
89
 
76
90
  console.log(`Image size: ${image.width}x${image.height}`);
77
91
 
@@ -79,7 +93,7 @@ console.log(`Image size: ${image.width}x${image.height}`);
79
93
  image.resize({ width: 800, height: 600 });
80
94
 
81
95
  // Save in a different format
82
- const jpeg = await image.save("jpeg");
96
+ const jpeg = await image.encode("jpeg");
83
97
  await writeFile("output.jpg", jpeg);
84
98
  ```
85
99
 
@@ -88,29 +102,33 @@ await writeFile("output.jpg", jpeg);
88
102
  | Format | Pure-JS | Notes |
89
103
  | ------ | ----------- | ------------------------------- |
90
104
  | PNG | ✅ Full | Complete pure-JS implementation |
105
+ | APNG | ✅ Full | Animated PNG with multi-frame |
91
106
  | BMP | ✅ Full | Complete pure-JS implementation |
92
- | GIF | ✅ Full | Complete pure-JS implementation |
93
- | RAW | ✅ Full | Uncompressed RGBA |
107
+ | ICO | ✅ Full | Windows Icon format |
108
+ | GIF | ✅ Full | Animated GIF with multi-frame |
109
+ | DNG | ✅ Full | Linear DNG (Uncompressed RGBA) |
110
+ | PAM | ✅ Full | Netpbm PAM format |
111
+ | PCX | ✅ Full | ZSoft PCX (RLE compressed) |
94
112
  | ASCII | ✅ Full | Text-based ASCII art |
95
113
  | JPEG | ⚠️ Baseline | Pure-JS baseline DCT only |
96
114
  | WebP | ⚠️ Lossless | Pure-JS lossless VP8L |
97
115
  | TIFF | ⚠️ Basic | Pure-JS uncompressed + LZW |
98
116
 
99
117
  See the
100
- [full format support documentation](https://cross-org.github.io/image/formats.html)
118
+ [full format support documentation](https://cross-image.56k.guru/formats.html)
101
119
  for detailed compatibility information.
102
120
 
103
121
  ## Documentation
104
122
 
105
- - **[API Reference](https://cross-org.github.io/image/api.html)** - Complete API
123
+ - **[API Reference](https://cross-image.56k.guru/api.html)** - Complete API
106
124
  documentation
107
- - **[Examples](https://cross-org.github.io/image/examples.html)** - Usage
108
- examples for common tasks
109
- - **[Format Support](https://cross-org.github.io/image/formats.html)** -
110
- Supported formats and specifications
111
- - **[JPEG Implementation](https://cross-org.github.io/image/jpeg-implementation.html)** -
125
+ - **[Examples](https://cross-image.56k.guru/examples.html)** - Usage examples
126
+ for common tasks
127
+ - **[Format Support](https://cross-image.56k.guru/formats.html)** - Supported
128
+ formats and specifications
129
+ - **[JPEG Implementation](https://cross-image.56k.guru/implementation/jpeg-implementation.html)** -
112
130
  Technical details for JPEG
113
- - **[WebP Implementation](https://cross-org.github.io/image/webp-implementation.html)** -
131
+ - **[WebP Implementation](https://cross-image.56k.guru/implementation/webp-implementation.html)** -
114
132
  Technical details for WebP
115
133
 
116
134
  ## Development
package/esm/mod.d.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  * @module @cross/image
3
3
  *
4
4
  * A pure JavaScript, dependency-free, cross-runtime image processing library.
5
- * Supports decoding, resizing, and encoding common image formats (PNG, JPEG, WebP, GIF, TIFF, BMP, RAW).
5
+ * Supports decoding, resizing, and encoding common image formats (PNG, APNG, JPEG, WebP, GIF, TIFF, BMP, ICO, DNG, PAM, PCX).
6
+ * Includes image processing capabilities like compositing, level adjustments, and pixel manipulation.
6
7
  *
7
8
  * @example
8
9
  * ```ts
@@ -12,22 +13,47 @@
12
13
  * const data = await Deno.readFile("input.png");
13
14
  * const image = await Image.decode(data);
14
15
  *
15
- * // Resize it
16
- * image.resize({ width: 200, height: 200 });
16
+ * // Apply image processing
17
+ * image
18
+ * .resize({ width: 200, height: 200 })
19
+ * .brightness(0.1)
20
+ * .contrast(0.2);
17
21
  *
18
22
  * // Encode as different format
19
23
  * const output = await image.encode("jpeg");
20
24
  * await Deno.writeFile("output.jpg", output);
21
25
  * ```
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { Image } from "@cross/image";
30
+ *
31
+ * // Create a blank canvas
32
+ * const canvas = Image.create(400, 300, 255, 255, 255);
33
+ *
34
+ * // Draw on it
35
+ * canvas.fillRect(50, 50, 100, 100, 255, 0, 0, 255);
36
+ *
37
+ * // Load and composite another image
38
+ * const overlay = await Image.decode(await Deno.readFile("logo.png"));
39
+ * canvas.composite(overlay, 10, 10, 0.8);
40
+ *
41
+ * // Save the result
42
+ * await Deno.writeFile("result.png", await canvas.encode("png"));
43
+ * ```
22
44
  */
23
45
  export { Image } from "./src/image.js";
24
46
  export type { ASCIIOptions, FrameMetadata, ImageData, ImageFormat, ImageFrame, ImageMetadata, MultiFrameImageData, ResizeOptions, WebPEncodeOptions, } from "./src/types.js";
25
47
  export { PNGFormat } from "./src/formats/png.js";
48
+ export { APNGFormat } from "./src/formats/apng.js";
26
49
  export { JPEGFormat } from "./src/formats/jpeg.js";
27
50
  export { WebPFormat } from "./src/formats/webp.js";
28
51
  export { GIFFormat } from "./src/formats/gif.js";
29
52
  export { type TIFFEncodeOptions, TIFFFormat } from "./src/formats/tiff.js";
30
53
  export { BMPFormat } from "./src/formats/bmp.js";
31
- export { RAWFormat } from "./src/formats/raw.js";
54
+ export { ICOFormat } from "./src/formats/ico.js";
55
+ export { DNGFormat } from "./src/formats/dng.js";
56
+ export { PAMFormat } from "./src/formats/pam.js";
57
+ export { PCXFormat } from "./src/formats/pcx.js";
32
58
  export { ASCIIFormat } from "./src/formats/ascii.js";
33
59
  //# sourceMappingURL=mod.d.ts.map
package/esm/mod.js CHANGED
@@ -2,7 +2,8 @@
2
2
  * @module @cross/image
3
3
  *
4
4
  * A pure JavaScript, dependency-free, cross-runtime image processing library.
5
- * Supports decoding, resizing, and encoding common image formats (PNG, JPEG, WebP, GIF, TIFF, BMP, RAW).
5
+ * Supports decoding, resizing, and encoding common image formats (PNG, APNG, JPEG, WebP, GIF, TIFF, BMP, ICO, DNG, PAM, PCX).
6
+ * Includes image processing capabilities like compositing, level adjustments, and pixel manipulation.
6
7
  *
7
8
  * @example
8
9
  * ```ts
@@ -12,20 +13,45 @@
12
13
  * const data = await Deno.readFile("input.png");
13
14
  * const image = await Image.decode(data);
14
15
  *
15
- * // Resize it
16
- * image.resize({ width: 200, height: 200 });
16
+ * // Apply image processing
17
+ * image
18
+ * .resize({ width: 200, height: 200 })
19
+ * .brightness(0.1)
20
+ * .contrast(0.2);
17
21
  *
18
22
  * // Encode as different format
19
23
  * const output = await image.encode("jpeg");
20
24
  * await Deno.writeFile("output.jpg", output);
21
25
  * ```
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { Image } from "@cross/image";
30
+ *
31
+ * // Create a blank canvas
32
+ * const canvas = Image.create(400, 300, 255, 255, 255);
33
+ *
34
+ * // Draw on it
35
+ * canvas.fillRect(50, 50, 100, 100, 255, 0, 0, 255);
36
+ *
37
+ * // Load and composite another image
38
+ * const overlay = await Image.decode(await Deno.readFile("logo.png"));
39
+ * canvas.composite(overlay, 10, 10, 0.8);
40
+ *
41
+ * // Save the result
42
+ * await Deno.writeFile("result.png", await canvas.encode("png"));
43
+ * ```
22
44
  */
23
45
  export { Image } from "./src/image.js";
24
46
  export { PNGFormat } from "./src/formats/png.js";
47
+ export { APNGFormat } from "./src/formats/apng.js";
25
48
  export { JPEGFormat } from "./src/formats/jpeg.js";
26
49
  export { WebPFormat } from "./src/formats/webp.js";
27
50
  export { GIFFormat } from "./src/formats/gif.js";
28
51
  export { TIFFFormat } from "./src/formats/tiff.js";
29
52
  export { BMPFormat } from "./src/formats/bmp.js";
30
- export { RAWFormat } from "./src/formats/raw.js";
53
+ export { ICOFormat } from "./src/formats/ico.js";
54
+ export { DNGFormat } from "./src/formats/dng.js";
55
+ export { PAMFormat } from "./src/formats/pam.js";
56
+ export { PCXFormat } from "./src/formats/pcx.js";
31
57
  export { ASCIIFormat } from "./src/formats/ascii.js";
@@ -0,0 +1,50 @@
1
+ import type { ImageData, ImageFormat, MultiFrameImageData } from "../types.js";
2
+ import { PNGBase } from "./png_base.js";
3
+ /**
4
+ * APNG (Animated PNG) format handler
5
+ * Implements support for animated PNG images with multiple frames
6
+ * APNG extends PNG with animation control chunks (acTL, fcTL, fdAT)
7
+ */
8
+ export declare class APNGFormat extends PNGBase implements ImageFormat {
9
+ /** Format name identifier */
10
+ readonly name = "apng";
11
+ /** MIME type for APNG images */
12
+ readonly mimeType = "image/apng";
13
+ /**
14
+ * Check if this format supports multiple frames (animations)
15
+ * @returns true for APNG format
16
+ */
17
+ supportsMultipleFrames(): boolean;
18
+ /**
19
+ * Check if the given data is an APNG image
20
+ * @param data Raw image data to check
21
+ * @returns true if data has PNG signature and contains acTL chunk
22
+ */
23
+ canDecode(data: Uint8Array): boolean;
24
+ /**
25
+ * Decode APNG image data to RGBA (first frame only)
26
+ * @param data Raw APNG image data
27
+ * @returns Decoded image data with RGBA pixels of first frame
28
+ */
29
+ decode(data: Uint8Array): Promise<ImageData>;
30
+ /**
31
+ * Decode all frames from APNG image
32
+ * @param data Raw APNG image data
33
+ * @returns Decoded multi-frame image data
34
+ */
35
+ decodeFrames(data: Uint8Array): Promise<MultiFrameImageData>;
36
+ /**
37
+ * Encode RGBA image data to APNG format (single frame)
38
+ * @param imageData Image data to encode
39
+ * @returns Encoded APNG image bytes
40
+ */
41
+ encode(imageData: ImageData): Promise<Uint8Array>;
42
+ /**
43
+ * Encode multi-frame image data to APNG format
44
+ * @param imageData Multi-frame image data to encode
45
+ * @returns Encoded APNG image bytes
46
+ */
47
+ encodeFrames(imageData: MultiFrameImageData): Promise<Uint8Array>;
48
+ private decodeFrameData;
49
+ }
50
+ //# sourceMappingURL=apng.d.ts.map
@@ -0,0 +1,364 @@
1
+ import { validateImageDimensions } from "../utils/security.js";
2
+ import { PNGBase } from "./png_base.js";
3
+ /**
4
+ * APNG (Animated PNG) format handler
5
+ * Implements support for animated PNG images with multiple frames
6
+ * APNG extends PNG with animation control chunks (acTL, fcTL, fdAT)
7
+ */
8
+ export class APNGFormat extends PNGBase {
9
+ constructor() {
10
+ super(...arguments);
11
+ /** Format name identifier */
12
+ Object.defineProperty(this, "name", {
13
+ enumerable: true,
14
+ configurable: true,
15
+ writable: true,
16
+ value: "apng"
17
+ });
18
+ /** MIME type for APNG images */
19
+ Object.defineProperty(this, "mimeType", {
20
+ enumerable: true,
21
+ configurable: true,
22
+ writable: true,
23
+ value: "image/apng"
24
+ });
25
+ }
26
+ /**
27
+ * Check if this format supports multiple frames (animations)
28
+ * @returns true for APNG format
29
+ */
30
+ supportsMultipleFrames() {
31
+ return true;
32
+ }
33
+ /**
34
+ * Check if the given data is an APNG image
35
+ * @param data Raw image data to check
36
+ * @returns true if data has PNG signature and contains acTL chunk
37
+ */
38
+ canDecode(data) {
39
+ // PNG signature: 137 80 78 71 13 10 26 10
40
+ if (data.length < 8 ||
41
+ data[0] !== 137 || data[1] !== 80 ||
42
+ data[2] !== 78 || data[3] !== 71 ||
43
+ data[4] !== 13 || data[5] !== 10 ||
44
+ data[6] !== 26 || data[7] !== 10) {
45
+ return false;
46
+ }
47
+ // Check for acTL (animation control) chunk to identify APNG
48
+ let pos = 8;
49
+ while (pos + 8 < data.length) {
50
+ const length = this.readUint32(data, pos);
51
+ pos += 4;
52
+ const type = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
53
+ pos += 4;
54
+ if (type === "acTL") {
55
+ return true;
56
+ }
57
+ pos += length + 4; // Skip chunk data and CRC
58
+ if (type === "IDAT") {
59
+ // If we hit IDAT before acTL, it's not an APNG
60
+ return false;
61
+ }
62
+ }
63
+ return false;
64
+ }
65
+ /**
66
+ * Decode APNG image data to RGBA (first frame only)
67
+ * @param data Raw APNG image data
68
+ * @returns Decoded image data with RGBA pixels of first frame
69
+ */
70
+ async decode(data) {
71
+ const frames = await this.decodeFrames(data);
72
+ const firstFrame = frames.frames[0];
73
+ return {
74
+ width: firstFrame.width,
75
+ height: firstFrame.height,
76
+ data: firstFrame.data,
77
+ metadata: frames.metadata,
78
+ };
79
+ }
80
+ /**
81
+ * Decode all frames from APNG image
82
+ * @param data Raw APNG image data
83
+ * @returns Decoded multi-frame image data
84
+ */
85
+ async decodeFrames(data) {
86
+ if (!this.canDecode(data)) {
87
+ throw new Error("Invalid APNG signature or missing acTL chunk");
88
+ }
89
+ let pos = 8; // Skip PNG signature
90
+ let width = 0;
91
+ let height = 0;
92
+ let bitDepth = 0;
93
+ let colorType = 0;
94
+ const metadata = {};
95
+ const frames = [];
96
+ // First pass: parse structure and extract metadata
97
+ const chunkList = [];
98
+ while (pos < data.length) {
99
+ const length = this.readUint32(data, pos);
100
+ pos += 4;
101
+ const type = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
102
+ pos += 4;
103
+ const chunkData = data.slice(pos, pos + length);
104
+ const chunkPos = pos;
105
+ pos += length;
106
+ pos += 4; // Skip CRC
107
+ chunkList.push({ type, data: chunkData, pos: chunkPos });
108
+ if (type === "IHDR") {
109
+ width = this.readUint32(chunkData, 0);
110
+ height = this.readUint32(chunkData, 4);
111
+ bitDepth = chunkData[8];
112
+ colorType = chunkData[9];
113
+ }
114
+ else if (type === "acTL") {
115
+ // Animation control chunk - we'll use frame count later if needed
116
+ // const numFrames = this.readUint32(chunkData, 0);
117
+ // const numPlays = this.readUint32(chunkData, 4);
118
+ }
119
+ else if (type === "pHYs") {
120
+ this.parsePhysChunk(chunkData, metadata, width, height);
121
+ }
122
+ else if (type === "tEXt") {
123
+ this.parseTextChunk(chunkData, metadata);
124
+ }
125
+ else if (type === "iTXt") {
126
+ this.parseITxtChunk(chunkData, metadata);
127
+ }
128
+ else if (type === "eXIf") {
129
+ this.parseExifChunk(chunkData, metadata);
130
+ }
131
+ else if (type === "IEND") {
132
+ break;
133
+ }
134
+ }
135
+ if (width === 0 || height === 0) {
136
+ throw new Error("Invalid APNG: missing IHDR chunk");
137
+ }
138
+ validateImageDimensions(width, height);
139
+ // Second pass: decode frames
140
+ let currentFrameControl = null;
141
+ let frameDataChunks = [];
142
+ let defaultImageChunks = [];
143
+ let hasSeenFcTL = false;
144
+ for (const chunk of chunkList) {
145
+ if (chunk.type === "fcTL") {
146
+ // If we have a previous frame to decode
147
+ if (frameDataChunks.length > 0 && currentFrameControl) {
148
+ const frameData = await this.decodeFrameData(frameDataChunks, currentFrameControl.width, currentFrameControl.height, bitDepth, colorType);
149
+ frames.push({
150
+ width: currentFrameControl.width,
151
+ height: currentFrameControl.height,
152
+ data: frameData,
153
+ frameMetadata: {
154
+ delay: currentFrameControl.delay,
155
+ disposal: currentFrameControl.disposal,
156
+ left: currentFrameControl.xOffset,
157
+ top: currentFrameControl.yOffset,
158
+ },
159
+ });
160
+ frameDataChunks = [];
161
+ }
162
+ // Parse frame control
163
+ const _fcSeq = this.readUint32(chunk.data, 0);
164
+ const fcWidth = this.readUint32(chunk.data, 4);
165
+ const fcHeight = this.readUint32(chunk.data, 8);
166
+ const fcXOffset = this.readUint32(chunk.data, 12);
167
+ const fcYOffset = this.readUint32(chunk.data, 16);
168
+ const delayNum = this.readUint16(chunk.data, 20);
169
+ const delayDen = this.readUint16(chunk.data, 22);
170
+ const disposeOp = chunk.data[24];
171
+ const blendOp = chunk.data[25];
172
+ const delay = delayDen === 0
173
+ ? delayNum * 10
174
+ : Math.round((delayNum / delayDen) * 1000);
175
+ let disposal = "none";
176
+ if (disposeOp === 1)
177
+ disposal = "background";
178
+ else if (disposeOp === 2)
179
+ disposal = "previous";
180
+ currentFrameControl = {
181
+ width: fcWidth,
182
+ height: fcHeight,
183
+ xOffset: fcXOffset,
184
+ yOffset: fcYOffset,
185
+ delay,
186
+ disposal,
187
+ blend: blendOp === 1 ? "over" : "source",
188
+ };
189
+ // If this is the first fcTL and we have default image data, use it for this frame
190
+ if (frames.length === 0 && defaultImageChunks.length > 0) {
191
+ frameDataChunks = defaultImageChunks;
192
+ defaultImageChunks = [];
193
+ }
194
+ hasSeenFcTL = true;
195
+ }
196
+ else if (chunk.type === "IDAT") {
197
+ if (!hasSeenFcTL) {
198
+ // Collect default image chunks
199
+ defaultImageChunks.push(chunk.data);
200
+ }
201
+ else if (currentFrameControl) {
202
+ // IDAT after first fcTL belongs to that frame
203
+ frameDataChunks.push(chunk.data);
204
+ }
205
+ }
206
+ else if (chunk.type === "fdAT") {
207
+ // Frame data chunk (skip sequence number)
208
+ const _frameSeq = this.readUint32(chunk.data, 0);
209
+ frameDataChunks.push(chunk.data.slice(4));
210
+ }
211
+ else if (chunk.type === "IEND") {
212
+ // Decode last frame if any
213
+ if (frameDataChunks.length > 0 && currentFrameControl) {
214
+ const frameData = await this.decodeFrameData(frameDataChunks, currentFrameControl.width, currentFrameControl.height, bitDepth, colorType);
215
+ frames.push({
216
+ width: currentFrameControl.width,
217
+ height: currentFrameControl.height,
218
+ data: frameData,
219
+ frameMetadata: {
220
+ delay: currentFrameControl.delay,
221
+ disposal: currentFrameControl.disposal,
222
+ left: currentFrameControl.xOffset,
223
+ top: currentFrameControl.yOffset,
224
+ },
225
+ });
226
+ }
227
+ else if (defaultImageChunks.length > 0) {
228
+ // Only default image, no fcTL found - treat as single frame
229
+ const frameData = await this.decodeFrameData(defaultImageChunks, width, height, bitDepth, colorType);
230
+ frames.push({
231
+ width,
232
+ height,
233
+ data: frameData,
234
+ frameMetadata: {
235
+ delay: 0,
236
+ disposal: "none",
237
+ left: 0,
238
+ top: 0,
239
+ },
240
+ });
241
+ }
242
+ break;
243
+ }
244
+ }
245
+ return {
246
+ width,
247
+ height,
248
+ frames,
249
+ metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
250
+ };
251
+ }
252
+ /**
253
+ * Encode RGBA image data to APNG format (single frame)
254
+ * @param imageData Image data to encode
255
+ * @returns Encoded APNG image bytes
256
+ */
257
+ encode(imageData) {
258
+ // For single frame, create a multi-frame with one frame
259
+ const multiFrame = {
260
+ width: imageData.width,
261
+ height: imageData.height,
262
+ frames: [{
263
+ width: imageData.width,
264
+ height: imageData.height,
265
+ data: imageData.data,
266
+ frameMetadata: { delay: 0 },
267
+ }],
268
+ metadata: imageData.metadata,
269
+ };
270
+ return this.encodeFrames(multiFrame);
271
+ }
272
+ /**
273
+ * Encode multi-frame image data to APNG format
274
+ * @param imageData Multi-frame image data to encode
275
+ * @returns Encoded APNG image bytes
276
+ */
277
+ async encodeFrames(imageData) {
278
+ const { width, height, frames, metadata } = imageData;
279
+ if (frames.length === 0) {
280
+ throw new Error("No frames to encode");
281
+ }
282
+ // Prepare IHDR chunk
283
+ const ihdr = new Uint8Array(13);
284
+ this.writeUint32(ihdr, 0, width);
285
+ this.writeUint32(ihdr, 4, height);
286
+ ihdr[8] = 8; // bit depth
287
+ ihdr[9] = 6; // color type: RGBA
288
+ ihdr[10] = 0; // compression method
289
+ ihdr[11] = 0; // filter method
290
+ ihdr[12] = 0; // interlace method
291
+ // Build PNG
292
+ const chunks = [];
293
+ chunks.push(new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])); // PNG signature
294
+ chunks.push(this.createChunk("IHDR", ihdr));
295
+ // Add acTL chunk for animation control
296
+ const actl = new Uint8Array(8);
297
+ this.writeUint32(actl, 0, frames.length); // num_frames
298
+ this.writeUint32(actl, 4, 0); // num_plays (0 = infinite)
299
+ chunks.push(this.createChunk("acTL", actl));
300
+ // Add metadata chunks if available
301
+ this.addMetadataChunks(chunks, metadata);
302
+ // Add frames
303
+ let sequenceNumber = 0;
304
+ for (let i = 0; i < frames.length; i++) {
305
+ const frame = frames[i];
306
+ const fctl = new Uint8Array(26);
307
+ this.writeUint32(fctl, 0, sequenceNumber++); // sequence_number
308
+ this.writeUint32(fctl, 4, frame.width); // width
309
+ this.writeUint32(fctl, 8, frame.height); // height
310
+ this.writeUint32(fctl, 12, frame.frameMetadata?.left ?? 0); // x_offset
311
+ this.writeUint32(fctl, 16, frame.frameMetadata?.top ?? 0); // y_offset
312
+ // Convert delay from milliseconds to fraction
313
+ const delay = frame.frameMetadata?.delay ?? 100;
314
+ // Use milliseconds directly if possible (up to ~65 seconds)
315
+ if (delay < 65536) {
316
+ this.writeUint16(fctl, 20, delay); // delay_num
317
+ this.writeUint16(fctl, 22, 1000); // delay_den (1/1000 sec)
318
+ }
319
+ else {
320
+ // Fallback to 1/100 sec for longer delays
321
+ this.writeUint16(fctl, 20, Math.round(delay / 10)); // delay_num
322
+ this.writeUint16(fctl, 22, 100); // delay_den (1/100 sec)
323
+ }
324
+ // Disposal method
325
+ let disposeOp = 0; // APNG_DISPOSE_OP_NONE
326
+ if (frame.frameMetadata?.disposal === "background") {
327
+ disposeOp = 1; // APNG_DISPOSE_OP_BACKGROUND
328
+ }
329
+ else if (frame.frameMetadata?.disposal === "previous") {
330
+ disposeOp = 2; // APNG_DISPOSE_OP_PREVIOUS
331
+ }
332
+ fctl[24] = disposeOp;
333
+ fctl[25] = 0; // blend_op: APNG_BLEND_OP_SOURCE
334
+ chunks.push(this.createChunk("fcTL", fctl));
335
+ // Filter and compress frame data
336
+ const filtered = this.filterData(frame.data, frame.width, frame.height);
337
+ const compressed = await this.deflate(filtered);
338
+ if (i === 0) {
339
+ // First frame uses IDAT
340
+ chunks.push(this.createChunk("IDAT", compressed));
341
+ }
342
+ else {
343
+ // Subsequent frames use fdAT with sequence number
344
+ const fdat = new Uint8Array(4 + compressed.length);
345
+ this.writeUint32(fdat, 0, sequenceNumber++);
346
+ fdat.set(compressed, 4);
347
+ chunks.push(this.createChunk("fdAT", fdat));
348
+ }
349
+ }
350
+ chunks.push(this.createChunk("IEND", new Uint8Array(0)));
351
+ // Concatenate all chunks
352
+ return this.concatenateArrays(chunks);
353
+ }
354
+ // Helper methods for frame decoding
355
+ async decodeFrameData(chunks, width, height, bitDepth, colorType) {
356
+ // Concatenate chunks
357
+ const idatData = this.concatenateArrays(chunks);
358
+ // Decompress data
359
+ const decompressed = await this.inflate(idatData);
360
+ // Unfilter and convert to RGBA
361
+ const rgba = this.unfilterAndConvert(decompressed, width, height, bitDepth, colorType);
362
+ return rgba;
363
+ }
364
+ }
@@ -26,11 +26,5 @@ export declare class BMPFormat implements ImageFormat {
26
26
  * @returns Encoded BMP image bytes
27
27
  */
28
28
  encode(imageData: ImageData): Promise<Uint8Array>;
29
- private readUint16LE;
30
- private readUint32LE;
31
- private readInt32LE;
32
- private writeUint16LE;
33
- private writeUint32LE;
34
- private writeInt32LE;
35
29
  }
36
30
  //# sourceMappingURL=bmp.d.ts.map