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.
- package/README.md +36 -18
- package/esm/mod.d.ts +30 -4
- package/esm/mod.js +30 -4
- package/esm/src/formats/apng.d.ts +50 -0
- package/esm/src/formats/apng.js +364 -0
- package/esm/src/formats/bmp.d.ts +0 -6
- package/esm/src/formats/bmp.js +24 -47
- package/esm/src/formats/dng.d.ts +27 -0
- package/esm/src/formats/dng.js +191 -0
- package/esm/src/formats/gif.d.ts +0 -2
- package/esm/src/formats/gif.js +10 -16
- package/esm/src/formats/ico.d.ts +41 -0
- package/esm/src/formats/ico.js +214 -0
- package/esm/src/formats/pam.d.ts +43 -0
- package/esm/src/formats/pam.js +177 -0
- package/esm/src/formats/pcx.d.ts +13 -0
- package/esm/src/formats/pcx.js +204 -0
- package/esm/src/formats/png.d.ts +2 -21
- package/esm/src/formats/png.js +5 -429
- package/esm/src/formats/png_base.d.ts +108 -0
- package/esm/src/formats/png_base.js +487 -0
- package/esm/src/formats/tiff.d.ts +7 -7
- package/esm/src/formats/webp.d.ts +0 -1
- package/esm/src/formats/webp.js +4 -7
- package/esm/src/image.d.ts +99 -0
- package/esm/src/image.js +204 -2
- package/esm/src/utils/byte_utils.d.ts +30 -0
- package/esm/src/utils/byte_utils.js +50 -0
- package/esm/src/utils/gif_encoder.d.ts +3 -2
- package/esm/src/utils/gif_encoder.js +115 -48
- package/esm/src/utils/image_processing.d.ts +91 -0
- package/esm/src/utils/image_processing.js +231 -0
- package/esm/src/utils/webp_decoder.js +47 -12
- package/esm/src/utils/webp_encoder.js +97 -39
- package/package.json +4 -1
- package/script/mod.d.ts +30 -4
- package/script/mod.js +36 -6
- package/script/src/formats/apng.d.ts +50 -0
- package/script/src/formats/apng.js +368 -0
- package/script/src/formats/bmp.d.ts +0 -6
- package/script/src/formats/bmp.js +24 -47
- package/script/src/formats/dng.d.ts +27 -0
- package/script/src/formats/dng.js +195 -0
- package/script/src/formats/gif.d.ts +0 -2
- package/script/src/formats/gif.js +10 -16
- package/script/src/formats/ico.d.ts +41 -0
- package/script/src/formats/ico.js +218 -0
- package/script/src/formats/pam.d.ts +43 -0
- package/script/src/formats/pam.js +181 -0
- package/script/src/formats/pcx.d.ts +13 -0
- package/script/src/formats/pcx.js +208 -0
- package/script/src/formats/png.d.ts +2 -21
- package/script/src/formats/png.js +5 -429
- package/script/src/formats/png_base.d.ts +108 -0
- package/script/src/formats/png_base.js +491 -0
- package/script/src/formats/tiff.d.ts +7 -7
- package/script/src/formats/webp.d.ts +0 -1
- package/script/src/formats/webp.js +4 -7
- package/script/src/image.d.ts +99 -0
- package/script/src/image.js +204 -2
- package/script/src/utils/byte_utils.d.ts +30 -0
- package/script/src/utils/byte_utils.js +58 -0
- package/script/src/utils/gif_encoder.d.ts +3 -2
- package/script/src/utils/gif_encoder.js +115 -48
- package/script/src/utils/image_processing.d.ts +91 -0
- package/script/src/utils/image_processing.js +242 -0
- package/script/src/utils/webp_decoder.js +47 -12
- package/script/src/utils/webp_encoder.js +97 -39
- package/esm/src/formats/raw.d.ts +0 -40
- package/esm/src/formats/raw.js +0 -118
- package/script/src/formats/raw.d.ts +0 -40
- 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-
|
|
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,
|
|
14
|
-
|
|
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
|
-
//
|
|
59
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
|
93
|
-
|
|
|
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-
|
|
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-
|
|
123
|
+
- **[API Reference](https://cross-image.56k.guru/api.html)** - Complete API
|
|
106
124
|
documentation
|
|
107
|
-
- **[Examples](https://cross-
|
|
108
|
-
|
|
109
|
-
- **[Format Support](https://cross-
|
|
110
|
-
|
|
111
|
-
- **[JPEG Implementation](https://cross-
|
|
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-
|
|
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,
|
|
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
|
-
* //
|
|
16
|
-
* image
|
|
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 {
|
|
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,
|
|
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
|
-
* //
|
|
16
|
-
* image
|
|
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 {
|
|
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
|
+
}
|
package/esm/src/formats/bmp.d.ts
CHANGED
|
@@ -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
|