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/README.md CHANGED
@@ -1,20 +1,24 @@
1
1
  # @cross/image
2
2
 
3
3
  A pure JavaScript, dependency-free, cross-runtime image processing library for
4
- Deno, Node.js, and Bun.
4
+ Deno, Node.js, and Bun. Decode, encode, manipulate, and process images in
5
+ multiple formats including PNG, JPEG, WebP, GIF, and moreβ€”all without native
6
+ dependencies.
5
7
 
6
- πŸ“š **[Full Documentation](https://cross-org.github.io/image/)**
8
+ πŸ“š **[Full Documentation](https://cross-image.56k.guru/)**
7
9
 
8
10
  ## Features
9
11
 
10
12
  - πŸš€ **Pure JavaScript** - No native dependencies
11
13
  - πŸ”Œ **Pluggable formats** - Easy to extend with custom formats
12
14
  - πŸ“¦ **Cross-runtime** - Works on Deno, Node.js (18+), and Bun
13
- - 🎨 **Multiple formats** - PNG, JPEG, WebP, GIF, TIFF, BMP, DNG, PAM, and PCX
14
- support
15
+ - 🎨 **Multiple formats** - PNG, APNG, JPEG, WebP, GIF, TIFF, BMP, ICO, DNG,
16
+ PAM, PPM, PCX and ASCII support
15
17
  - βœ‚οΈ **Image manipulation** - Resize, crop, composite, and more
16
- - πŸŽ›οΈ **Image processing** - Adjust brightness, contrast, saturation, exposure
18
+ - πŸŽ›οΈ **Image processing** - Chainable filters including `brightness`,
19
+ `contrast`, `saturation`, `exposure`, `blur`, `sharpen`, `sepia`, and more
17
20
  - πŸ–ŒοΈ **Drawing operations** - Create, fill, and manipulate pixels
21
+ - 🧩 **Multi-frame** - Decode/encode animated GIFs, APNGs and multi-page TIFFs
18
22
  - πŸ”§ **Simple API** - Easy to use, intuitive interface
19
23
 
20
24
  ## Installation
@@ -64,11 +68,13 @@ const canvas = Image.create(800, 600, 255, 255, 255); // white background
64
68
  // Composite the loaded image on top
65
69
  canvas.composite(image, 50, 50);
66
70
 
67
- // Apply image processing
71
+ // Apply image processing filters
68
72
  canvas
69
73
  .brightness(0.1)
70
74
  .contrast(0.2)
71
- .saturation(-0.1);
75
+ .saturation(-0.1)
76
+ .blur(1)
77
+ .sharpen(0.3);
72
78
 
73
79
  // Encode in a different format
74
80
  const jpeg = await canvas.encode("jpeg");
@@ -83,7 +89,7 @@ import { readFile, writeFile } from "node:fs/promises";
83
89
 
84
90
  // Read an image (auto-detects format)
85
91
  const data = await readFile("input.png");
86
- const image = await Image.read(data);
92
+ const image = await Image.decode(data);
87
93
 
88
94
  console.log(`Image size: ${image.width}x${image.height}`);
89
95
 
@@ -91,39 +97,46 @@ console.log(`Image size: ${image.width}x${image.height}`);
91
97
  image.resize({ width: 800, height: 600 });
92
98
 
93
99
  // Save in a different format
94
- const jpeg = await image.save("jpeg");
100
+ const jpeg = await image.encode("jpeg");
95
101
  await writeFile("output.jpg", jpeg);
96
102
  ```
97
103
 
98
104
  ## Supported Formats
99
105
 
100
- | Format | Pure-JS | Notes |
101
- | ------ | ----------- | ------------------------------- |
102
- | PNG | βœ… Full | Complete pure-JS implementation |
103
- | BMP | βœ… Full | Complete pure-JS implementation |
104
- | GIF | βœ… Full | Complete pure-JS implementation |
105
- | RAW | βœ… Full | Uncompressed RGBA |
106
- | ASCII | βœ… Full | Text-based ASCII art |
107
- | JPEG | ⚠️ Baseline | Pure-JS baseline DCT only |
108
- | WebP | ⚠️ Lossless | Pure-JS lossless VP8L |
109
- | TIFF | ⚠️ Basic | Pure-JS uncompressed + LZW |
106
+ | Format | Pure-JS | Notes |
107
+ | ------ | ----------- | -------------------------------------- |
108
+ | PNG | βœ… Full | Complete pure-JS implementation |
109
+ | APNG | βœ… Full | Animated PNG with multi-frame |
110
+ | BMP | βœ… Full | Complete pure-JS implementation |
111
+ | ICO | βœ… Full | Windows Icon format |
112
+ | GIF | βœ… Full | Animated GIF with multi-frame |
113
+ | DNG | βœ… Full | Linear DNG (Uncompressed RGBA) |
114
+ | PAM | βœ… Full | Netpbm PAM format |
115
+ | PPM | βœ… Full | Netpbm PPM format (P3/P6) |
116
+ | PCX | βœ… Full | ZSoft PCX (RLE compressed) |
117
+ | ASCII | βœ… Full | Text-based ASCII art |
118
+ | JPEG | ⚠️ Baseline | Pure-JS baseline DCT only |
119
+ | WebP | ⚠️ Lossless | Pure-JS lossless VP8L |
120
+ | TIFF | ⚠️ Basic | Pure-JS uncompressed, LZW, & grayscale |
110
121
 
111
122
  See the
112
- [full format support documentation](https://cross-org.github.io/image/formats.html)
113
- for detailed compatibility information.
123
+ [full format support documentation](https://cross-image.56k.guru/formats/) for
124
+ detailed compatibility information.
114
125
 
115
126
  ## Documentation
116
127
 
117
- - **[API Reference](https://cross-org.github.io/image/api.html)** - Complete API
128
+ - **[API Reference](https://cross-image.56k.guru/api/)** - Complete API
118
129
  documentation
119
- - **[Examples](https://cross-org.github.io/image/examples.html)** - Usage
120
- examples for common tasks
121
- - **[Format Support](https://cross-org.github.io/image/formats.html)** -
122
- Supported formats and specifications
123
- - **[JPEG Implementation](https://cross-org.github.io/image/jpeg-implementation.html)** -
130
+ - **[Examples](https://cross-image.56k.guru/examples/)** - Usage examples for
131
+ common tasks
132
+ - **[Format Support](https://cross-image.56k.guru/formats/)** - Supported
133
+ formats and specifications
134
+ - **[JPEG Implementation](https://cross-image.56k.guru/implementation/jpeg-implementation/)** -
124
135
  Technical details for JPEG
125
- - **[WebP Implementation](https://cross-org.github.io/image/webp-implementation.html)** -
136
+ - **[WebP Implementation](https://cross-image.56k.guru/implementation/webp-implementation/)** -
126
137
  Technical details for WebP
138
+ - **[TIFF Implementation](https://cross-image.56k.guru/implementation/tiff-implementation/)** -
139
+ Technical details for TIFF
127
140
 
128
141
  ## Development
129
142
 
package/esm/mod.d.ts CHANGED
@@ -2,7 +2,7 @@
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, DNG, PAM, PCX).
5
+ * Supports decoding, resizing, and encoding common image formats (PNG, APNG, JPEG, WebP, GIF, TIFF, BMP, ICO, DNG, PAM, PPM, PCX).
6
6
  * Includes image processing capabilities like compositing, level adjustments, and pixel manipulation.
7
7
  *
8
8
  * @example
@@ -45,13 +45,16 @@
45
45
  export { Image } from "./src/image.js";
46
46
  export type { ASCIIOptions, FrameMetadata, ImageData, ImageFormat, ImageFrame, ImageMetadata, MultiFrameImageData, ResizeOptions, WebPEncodeOptions, } from "./src/types.js";
47
47
  export { PNGFormat } from "./src/formats/png.js";
48
+ export { APNGFormat } from "./src/formats/apng.js";
48
49
  export { JPEGFormat } from "./src/formats/jpeg.js";
49
50
  export { WebPFormat } from "./src/formats/webp.js";
50
51
  export { GIFFormat } from "./src/formats/gif.js";
51
52
  export { type TIFFEncodeOptions, TIFFFormat } from "./src/formats/tiff.js";
52
53
  export { BMPFormat } from "./src/formats/bmp.js";
54
+ export { ICOFormat } from "./src/formats/ico.js";
53
55
  export { DNGFormat } from "./src/formats/dng.js";
54
56
  export { PAMFormat } from "./src/formats/pam.js";
55
57
  export { PCXFormat } from "./src/formats/pcx.js";
58
+ export { PPMFormat } from "./src/formats/ppm.js";
56
59
  export { ASCIIFormat } from "./src/formats/ascii.js";
57
60
  //# sourceMappingURL=mod.d.ts.map
package/esm/mod.js CHANGED
@@ -2,7 +2,7 @@
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, DNG, PAM, PCX).
5
+ * Supports decoding, resizing, and encoding common image formats (PNG, APNG, JPEG, WebP, GIF, TIFF, BMP, ICO, DNG, PAM, PPM, PCX).
6
6
  * Includes image processing capabilities like compositing, level adjustments, and pixel manipulation.
7
7
  *
8
8
  * @example
@@ -44,12 +44,15 @@
44
44
  */
45
45
  export { Image } from "./src/image.js";
46
46
  export { PNGFormat } from "./src/formats/png.js";
47
+ export { APNGFormat } from "./src/formats/apng.js";
47
48
  export { JPEGFormat } from "./src/formats/jpeg.js";
48
49
  export { WebPFormat } from "./src/formats/webp.js";
49
50
  export { GIFFormat } from "./src/formats/gif.js";
50
51
  export { TIFFFormat } from "./src/formats/tiff.js";
51
52
  export { BMPFormat } from "./src/formats/bmp.js";
53
+ export { ICOFormat } from "./src/formats/ico.js";
52
54
  export { DNGFormat } from "./src/formats/dng.js";
53
55
  export { PAMFormat } from "./src/formats/pam.js";
54
56
  export { PCXFormat } from "./src/formats/pcx.js";
57
+ export { PPMFormat } from "./src/formats/ppm.js";
55
58
  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