cross-image 0.2.0 → 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 +22 -16
- package/esm/mod.d.ts +3 -1
- package/esm/mod.js +3 -1
- 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.js +4 -4
- 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/pcx.js +1 -1
- 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/webp.d.ts +0 -1
- package/esm/src/formats/webp.js +4 -7
- package/esm/src/image.js +4 -0
- 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/package.json +1 -1
- package/script/mod.d.ts +3 -1
- package/script/mod.js +6 -2
- 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.js +4 -4
- 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/pcx.js +1 -1
- 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/webp.d.ts +0 -1
- package/script/src/formats/webp.js +4 -7
- package/script/src/image.js +4 -0
- 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/README.md
CHANGED
|
@@ -3,18 +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
|
-
support
|
|
13
|
+
- 🎨 **Multiple formats** - PNG, APNG, JPEG, WebP, GIF, TIFF, BMP, ICO, DNG,
|
|
14
|
+
PAM, PCX and ASCII support
|
|
15
15
|
- ✂️ **Image manipulation** - Resize, crop, composite, and more
|
|
16
|
-
- 🎛️ **Image processing** -
|
|
16
|
+
- 🎛️ **Image processing** - Chainable `brightness`, `contrast`, `saturation`,
|
|
17
|
+
and `exposure` helpers
|
|
17
18
|
- 🖌️ **Drawing operations** - Create, fill, and manipulate pixels
|
|
19
|
+
- 🧩 **Multi-frame** - Decode/encode animated GIFs, APNGs and multi-page TIFFs
|
|
18
20
|
- 🔧 **Simple API** - Easy to use, intuitive interface
|
|
19
21
|
|
|
20
22
|
## Installation
|
|
@@ -83,7 +85,7 @@ import { readFile, writeFile } from "node:fs/promises";
|
|
|
83
85
|
|
|
84
86
|
// Read an image (auto-detects format)
|
|
85
87
|
const data = await readFile("input.png");
|
|
86
|
-
const image = await Image.
|
|
88
|
+
const image = await Image.decode(data);
|
|
87
89
|
|
|
88
90
|
console.log(`Image size: ${image.width}x${image.height}`);
|
|
89
91
|
|
|
@@ -91,7 +93,7 @@ console.log(`Image size: ${image.width}x${image.height}`);
|
|
|
91
93
|
image.resize({ width: 800, height: 600 });
|
|
92
94
|
|
|
93
95
|
// Save in a different format
|
|
94
|
-
const jpeg = await image.
|
|
96
|
+
const jpeg = await image.encode("jpeg");
|
|
95
97
|
await writeFile("output.jpg", jpeg);
|
|
96
98
|
```
|
|
97
99
|
|
|
@@ -100,29 +102,33 @@ await writeFile("output.jpg", jpeg);
|
|
|
100
102
|
| Format | Pure-JS | Notes |
|
|
101
103
|
| ------ | ----------- | ------------------------------- |
|
|
102
104
|
| PNG | ✅ Full | Complete pure-JS implementation |
|
|
105
|
+
| APNG | ✅ Full | Animated PNG with multi-frame |
|
|
103
106
|
| BMP | ✅ Full | Complete pure-JS implementation |
|
|
104
|
-
|
|
|
105
|
-
|
|
|
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) |
|
|
106
112
|
| ASCII | ✅ Full | Text-based ASCII art |
|
|
107
113
|
| JPEG | ⚠️ Baseline | Pure-JS baseline DCT only |
|
|
108
114
|
| WebP | ⚠️ Lossless | Pure-JS lossless VP8L |
|
|
109
115
|
| TIFF | ⚠️ Basic | Pure-JS uncompressed + LZW |
|
|
110
116
|
|
|
111
117
|
See the
|
|
112
|
-
[full format support documentation](https://cross-
|
|
118
|
+
[full format support documentation](https://cross-image.56k.guru/formats.html)
|
|
113
119
|
for detailed compatibility information.
|
|
114
120
|
|
|
115
121
|
## Documentation
|
|
116
122
|
|
|
117
|
-
- **[API Reference](https://cross-
|
|
123
|
+
- **[API Reference](https://cross-image.56k.guru/api.html)** - Complete API
|
|
118
124
|
documentation
|
|
119
|
-
- **[Examples](https://cross-
|
|
120
|
-
|
|
121
|
-
- **[Format Support](https://cross-
|
|
122
|
-
|
|
123
|
-
- **[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)** -
|
|
124
130
|
Technical details for JPEG
|
|
125
|
-
- **[WebP Implementation](https://cross-
|
|
131
|
+
- **[WebP Implementation](https://cross-image.56k.guru/implementation/webp-implementation.html)** -
|
|
126
132
|
Technical details for WebP
|
|
127
133
|
|
|
128
134
|
## Development
|
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, PCX).
|
|
6
6
|
* Includes image processing capabilities like compositing, level adjustments, and pixel manipulation.
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
@@ -45,11 +45,13 @@
|
|
|
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";
|
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, PCX).
|
|
6
6
|
* Includes image processing capabilities like compositing, level adjustments, and pixel manipulation.
|
|
7
7
|
*
|
|
8
8
|
* @example
|
|
@@ -44,11 +44,13 @@
|
|
|
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";
|
|
@@ -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
|
package/esm/src/formats/bmp.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateImageDimensions } from "../utils/security.js";
|
|
2
|
+
import { readInt32LE, readUint16LE, readUint32LE, writeInt32LE, writeUint16LE, writeUint32LE, } from "../utils/byte_utils.js";
|
|
2
3
|
// Constants for unit conversions
|
|
3
4
|
const INCHES_PER_METER = 39.3701;
|
|
4
5
|
/**
|
|
@@ -42,10 +43,10 @@ export class BMPFormat {
|
|
|
42
43
|
throw new Error("Invalid BMP signature");
|
|
43
44
|
}
|
|
44
45
|
// Read BMP file header (14 bytes)
|
|
45
|
-
const _fileSize =
|
|
46
|
-
const dataOffset =
|
|
46
|
+
const _fileSize = readUint32LE(data, 2);
|
|
47
|
+
const dataOffset = readUint32LE(data, 10);
|
|
47
48
|
// Read DIB header (at least 40 bytes for BITMAPINFOHEADER)
|
|
48
|
-
const dibHeaderSize =
|
|
49
|
+
const dibHeaderSize = readUint32LE(data, 14);
|
|
49
50
|
let width;
|
|
50
51
|
let height;
|
|
51
52
|
let bitDepth;
|
|
@@ -53,13 +54,13 @@ export class BMPFormat {
|
|
|
53
54
|
const metadata = {};
|
|
54
55
|
if (dibHeaderSize >= 40) {
|
|
55
56
|
// BITMAPINFOHEADER or later
|
|
56
|
-
width =
|
|
57
|
-
height =
|
|
58
|
-
bitDepth =
|
|
59
|
-
compression =
|
|
57
|
+
width = readInt32LE(data, 18);
|
|
58
|
+
height = readInt32LE(data, 22);
|
|
59
|
+
bitDepth = readUint16LE(data, 28);
|
|
60
|
+
compression = readUint32LE(data, 30);
|
|
60
61
|
// Read DPI information (pixels per meter)
|
|
61
|
-
const xPixelsPerMeter =
|
|
62
|
-
const yPixelsPerMeter =
|
|
62
|
+
const xPixelsPerMeter = readInt32LE(data, 38);
|
|
63
|
+
const yPixelsPerMeter = readInt32LE(data, 42);
|
|
63
64
|
if (xPixelsPerMeter > 0 && yPixelsPerMeter > 0) {
|
|
64
65
|
// Convert pixels per meter to DPI
|
|
65
66
|
metadata.dpiX = Math.round(xPixelsPerMeter / INCHES_PER_METER);
|
|
@@ -134,21 +135,21 @@ export class BMPFormat {
|
|
|
134
135
|
// BMP File Header (14 bytes)
|
|
135
136
|
result[0] = 0x42; // 'B'
|
|
136
137
|
result[1] = 0x4d; // 'M'
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
writeUint32LE(result, 2, fileSize); // File size
|
|
139
|
+
writeUint32LE(result, 6, 0); // Reserved
|
|
140
|
+
writeUint32LE(result, 10, 54); // Offset to pixel data (14 + 40)
|
|
140
141
|
// DIB Header (BITMAPINFOHEADER - 40 bytes)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
142
|
+
writeUint32LE(result, 14, 40); // DIB header size
|
|
143
|
+
writeInt32LE(result, 18, width); // Width
|
|
144
|
+
writeInt32LE(result, 22, height); // Height (positive = bottom-up)
|
|
145
|
+
writeUint16LE(result, 26, 1); // Planes
|
|
146
|
+
writeUint16LE(result, 28, 32); // Bits per pixel
|
|
147
|
+
writeUint32LE(result, 30, 0); // Compression (0 = uncompressed)
|
|
148
|
+
writeUint32LE(result, 34, pixelDataSize); // Image size
|
|
149
|
+
writeInt32LE(result, 38, xPixelsPerMeter); // X pixels per meter
|
|
150
|
+
writeInt32LE(result, 42, yPixelsPerMeter); // Y pixels per meter
|
|
151
|
+
writeUint32LE(result, 46, 0); // Colors in palette
|
|
152
|
+
writeUint32LE(result, 50, 0); // Important colors
|
|
152
153
|
// Write pixel data (bottom-to-top, BGR(A) format)
|
|
153
154
|
let offset = 54;
|
|
154
155
|
for (let y = height - 1; y >= 0; y--) {
|
|
@@ -167,28 +168,4 @@ export class BMPFormat {
|
|
|
167
168
|
}
|
|
168
169
|
return Promise.resolve(result);
|
|
169
170
|
}
|
|
170
|
-
readUint16LE(data, offset) {
|
|
171
|
-
return data[offset] | (data[offset + 1] << 8);
|
|
172
|
-
}
|
|
173
|
-
readUint32LE(data, offset) {
|
|
174
|
-
return data[offset] | (data[offset + 1] << 8) |
|
|
175
|
-
(data[offset + 2] << 16) | (data[offset + 3] << 24);
|
|
176
|
-
}
|
|
177
|
-
readInt32LE(data, offset) {
|
|
178
|
-
const value = this.readUint32LE(data, offset);
|
|
179
|
-
return value > 0x7fffffff ? value - 0x100000000 : value;
|
|
180
|
-
}
|
|
181
|
-
writeUint16LE(data, offset, value) {
|
|
182
|
-
data[offset] = value & 0xff;
|
|
183
|
-
data[offset + 1] = (value >>> 8) & 0xff;
|
|
184
|
-
}
|
|
185
|
-
writeUint32LE(data, offset, value) {
|
|
186
|
-
data[offset] = value & 0xff;
|
|
187
|
-
data[offset + 1] = (value >>> 8) & 0xff;
|
|
188
|
-
data[offset + 2] = (value >>> 16) & 0xff;
|
|
189
|
-
data[offset + 3] = (value >>> 24) & 0xff;
|
|
190
|
-
}
|
|
191
|
-
writeInt32LE(data, offset, value) {
|
|
192
|
-
this.writeUint32LE(data, offset, value < 0 ? value + 0x100000000 : value);
|
|
193
|
-
}
|
|
194
171
|
}
|