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/script/mod.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* @module @cross/image
|
|
4
4
|
*
|
|
5
5
|
* A pure JavaScript, dependency-free, cross-runtime image processing library.
|
|
6
|
-
* Supports decoding, resizing, and encoding common image formats (PNG, JPEG, WebP, GIF, TIFF, BMP,
|
|
6
|
+
* Supports decoding, resizing, and encoding common image formats (PNG, APNG, JPEG, WebP, GIF, TIFF, BMP, ICO, DNG, PAM, PCX).
|
|
7
|
+
* Includes image processing capabilities like compositing, level adjustments, and pixel manipulation.
|
|
7
8
|
*
|
|
8
9
|
* @example
|
|
9
10
|
* ```ts
|
|
@@ -13,20 +14,43 @@
|
|
|
13
14
|
* const data = await Deno.readFile("input.png");
|
|
14
15
|
* const image = await Image.decode(data);
|
|
15
16
|
*
|
|
16
|
-
* //
|
|
17
|
-
* image
|
|
17
|
+
* // Apply image processing
|
|
18
|
+
* image
|
|
19
|
+
* .resize({ width: 200, height: 200 })
|
|
20
|
+
* .brightness(0.1)
|
|
21
|
+
* .contrast(0.2);
|
|
18
22
|
*
|
|
19
23
|
* // Encode as different format
|
|
20
24
|
* const output = await image.encode("jpeg");
|
|
21
25
|
* await Deno.writeFile("output.jpg", output);
|
|
22
26
|
* ```
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* import { Image } from "@cross/image";
|
|
31
|
+
*
|
|
32
|
+
* // Create a blank canvas
|
|
33
|
+
* const canvas = Image.create(400, 300, 255, 255, 255);
|
|
34
|
+
*
|
|
35
|
+
* // Draw on it
|
|
36
|
+
* canvas.fillRect(50, 50, 100, 100, 255, 0, 0, 255);
|
|
37
|
+
*
|
|
38
|
+
* // Load and composite another image
|
|
39
|
+
* const overlay = await Image.decode(await Deno.readFile("logo.png"));
|
|
40
|
+
* canvas.composite(overlay, 10, 10, 0.8);
|
|
41
|
+
*
|
|
42
|
+
* // Save the result
|
|
43
|
+
* await Deno.writeFile("result.png", await canvas.encode("png"));
|
|
44
|
+
* ```
|
|
23
45
|
*/
|
|
24
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.ASCIIFormat = exports.
|
|
47
|
+
exports.ASCIIFormat = exports.PCXFormat = exports.PAMFormat = exports.DNGFormat = exports.ICOFormat = exports.BMPFormat = exports.TIFFFormat = exports.GIFFormat = exports.WebPFormat = exports.JPEGFormat = exports.APNGFormat = exports.PNGFormat = exports.Image = void 0;
|
|
26
48
|
var image_js_1 = require("./src/image.js");
|
|
27
49
|
Object.defineProperty(exports, "Image", { enumerable: true, get: function () { return image_js_1.Image; } });
|
|
28
50
|
var png_js_1 = require("./src/formats/png.js");
|
|
29
51
|
Object.defineProperty(exports, "PNGFormat", { enumerable: true, get: function () { return png_js_1.PNGFormat; } });
|
|
52
|
+
var apng_js_1 = require("./src/formats/apng.js");
|
|
53
|
+
Object.defineProperty(exports, "APNGFormat", { enumerable: true, get: function () { return apng_js_1.APNGFormat; } });
|
|
30
54
|
var jpeg_js_1 = require("./src/formats/jpeg.js");
|
|
31
55
|
Object.defineProperty(exports, "JPEGFormat", { enumerable: true, get: function () { return jpeg_js_1.JPEGFormat; } });
|
|
32
56
|
var webp_js_1 = require("./src/formats/webp.js");
|
|
@@ -37,7 +61,13 @@ var tiff_js_1 = require("./src/formats/tiff.js");
|
|
|
37
61
|
Object.defineProperty(exports, "TIFFFormat", { enumerable: true, get: function () { return tiff_js_1.TIFFFormat; } });
|
|
38
62
|
var bmp_js_1 = require("./src/formats/bmp.js");
|
|
39
63
|
Object.defineProperty(exports, "BMPFormat", { enumerable: true, get: function () { return bmp_js_1.BMPFormat; } });
|
|
40
|
-
var
|
|
41
|
-
Object.defineProperty(exports, "
|
|
64
|
+
var ico_js_1 = require("./src/formats/ico.js");
|
|
65
|
+
Object.defineProperty(exports, "ICOFormat", { enumerable: true, get: function () { return ico_js_1.ICOFormat; } });
|
|
66
|
+
var dng_js_1 = require("./src/formats/dng.js");
|
|
67
|
+
Object.defineProperty(exports, "DNGFormat", { enumerable: true, get: function () { return dng_js_1.DNGFormat; } });
|
|
68
|
+
var pam_js_1 = require("./src/formats/pam.js");
|
|
69
|
+
Object.defineProperty(exports, "PAMFormat", { enumerable: true, get: function () { return pam_js_1.PAMFormat; } });
|
|
70
|
+
var pcx_js_1 = require("./src/formats/pcx.js");
|
|
71
|
+
Object.defineProperty(exports, "PCXFormat", { enumerable: true, get: function () { return pcx_js_1.PCXFormat; } });
|
|
42
72
|
var ascii_js_1 = require("./src/formats/ascii.js");
|
|
43
73
|
Object.defineProperty(exports, "ASCIIFormat", { enumerable: true, get: function () { return ascii_js_1.ASCIIFormat; } });
|
|
@@ -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,368 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.APNGFormat = void 0;
|
|
4
|
+
const security_js_1 = require("../utils/security.js");
|
|
5
|
+
const png_base_js_1 = require("./png_base.js");
|
|
6
|
+
/**
|
|
7
|
+
* APNG (Animated PNG) format handler
|
|
8
|
+
* Implements support for animated PNG images with multiple frames
|
|
9
|
+
* APNG extends PNG with animation control chunks (acTL, fcTL, fdAT)
|
|
10
|
+
*/
|
|
11
|
+
class APNGFormat extends png_base_js_1.PNGBase {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(...arguments);
|
|
14
|
+
/** Format name identifier */
|
|
15
|
+
Object.defineProperty(this, "name", {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
value: "apng"
|
|
20
|
+
});
|
|
21
|
+
/** MIME type for APNG images */
|
|
22
|
+
Object.defineProperty(this, "mimeType", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: "image/apng"
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if this format supports multiple frames (animations)
|
|
31
|
+
* @returns true for APNG format
|
|
32
|
+
*/
|
|
33
|
+
supportsMultipleFrames() {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Check if the given data is an APNG image
|
|
38
|
+
* @param data Raw image data to check
|
|
39
|
+
* @returns true if data has PNG signature and contains acTL chunk
|
|
40
|
+
*/
|
|
41
|
+
canDecode(data) {
|
|
42
|
+
// PNG signature: 137 80 78 71 13 10 26 10
|
|
43
|
+
if (data.length < 8 ||
|
|
44
|
+
data[0] !== 137 || data[1] !== 80 ||
|
|
45
|
+
data[2] !== 78 || data[3] !== 71 ||
|
|
46
|
+
data[4] !== 13 || data[5] !== 10 ||
|
|
47
|
+
data[6] !== 26 || data[7] !== 10) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
// Check for acTL (animation control) chunk to identify APNG
|
|
51
|
+
let pos = 8;
|
|
52
|
+
while (pos + 8 < data.length) {
|
|
53
|
+
const length = this.readUint32(data, pos);
|
|
54
|
+
pos += 4;
|
|
55
|
+
const type = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
|
|
56
|
+
pos += 4;
|
|
57
|
+
if (type === "acTL") {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
pos += length + 4; // Skip chunk data and CRC
|
|
61
|
+
if (type === "IDAT") {
|
|
62
|
+
// If we hit IDAT before acTL, it's not an APNG
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Decode APNG image data to RGBA (first frame only)
|
|
70
|
+
* @param data Raw APNG image data
|
|
71
|
+
* @returns Decoded image data with RGBA pixels of first frame
|
|
72
|
+
*/
|
|
73
|
+
async decode(data) {
|
|
74
|
+
const frames = await this.decodeFrames(data);
|
|
75
|
+
const firstFrame = frames.frames[0];
|
|
76
|
+
return {
|
|
77
|
+
width: firstFrame.width,
|
|
78
|
+
height: firstFrame.height,
|
|
79
|
+
data: firstFrame.data,
|
|
80
|
+
metadata: frames.metadata,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Decode all frames from APNG image
|
|
85
|
+
* @param data Raw APNG image data
|
|
86
|
+
* @returns Decoded multi-frame image data
|
|
87
|
+
*/
|
|
88
|
+
async decodeFrames(data) {
|
|
89
|
+
if (!this.canDecode(data)) {
|
|
90
|
+
throw new Error("Invalid APNG signature or missing acTL chunk");
|
|
91
|
+
}
|
|
92
|
+
let pos = 8; // Skip PNG signature
|
|
93
|
+
let width = 0;
|
|
94
|
+
let height = 0;
|
|
95
|
+
let bitDepth = 0;
|
|
96
|
+
let colorType = 0;
|
|
97
|
+
const metadata = {};
|
|
98
|
+
const frames = [];
|
|
99
|
+
// First pass: parse structure and extract metadata
|
|
100
|
+
const chunkList = [];
|
|
101
|
+
while (pos < data.length) {
|
|
102
|
+
const length = this.readUint32(data, pos);
|
|
103
|
+
pos += 4;
|
|
104
|
+
const type = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
|
|
105
|
+
pos += 4;
|
|
106
|
+
const chunkData = data.slice(pos, pos + length);
|
|
107
|
+
const chunkPos = pos;
|
|
108
|
+
pos += length;
|
|
109
|
+
pos += 4; // Skip CRC
|
|
110
|
+
chunkList.push({ type, data: chunkData, pos: chunkPos });
|
|
111
|
+
if (type === "IHDR") {
|
|
112
|
+
width = this.readUint32(chunkData, 0);
|
|
113
|
+
height = this.readUint32(chunkData, 4);
|
|
114
|
+
bitDepth = chunkData[8];
|
|
115
|
+
colorType = chunkData[9];
|
|
116
|
+
}
|
|
117
|
+
else if (type === "acTL") {
|
|
118
|
+
// Animation control chunk - we'll use frame count later if needed
|
|
119
|
+
// const numFrames = this.readUint32(chunkData, 0);
|
|
120
|
+
// const numPlays = this.readUint32(chunkData, 4);
|
|
121
|
+
}
|
|
122
|
+
else if (type === "pHYs") {
|
|
123
|
+
this.parsePhysChunk(chunkData, metadata, width, height);
|
|
124
|
+
}
|
|
125
|
+
else if (type === "tEXt") {
|
|
126
|
+
this.parseTextChunk(chunkData, metadata);
|
|
127
|
+
}
|
|
128
|
+
else if (type === "iTXt") {
|
|
129
|
+
this.parseITxtChunk(chunkData, metadata);
|
|
130
|
+
}
|
|
131
|
+
else if (type === "eXIf") {
|
|
132
|
+
this.parseExifChunk(chunkData, metadata);
|
|
133
|
+
}
|
|
134
|
+
else if (type === "IEND") {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (width === 0 || height === 0) {
|
|
139
|
+
throw new Error("Invalid APNG: missing IHDR chunk");
|
|
140
|
+
}
|
|
141
|
+
(0, security_js_1.validateImageDimensions)(width, height);
|
|
142
|
+
// Second pass: decode frames
|
|
143
|
+
let currentFrameControl = null;
|
|
144
|
+
let frameDataChunks = [];
|
|
145
|
+
let defaultImageChunks = [];
|
|
146
|
+
let hasSeenFcTL = false;
|
|
147
|
+
for (const chunk of chunkList) {
|
|
148
|
+
if (chunk.type === "fcTL") {
|
|
149
|
+
// If we have a previous frame to decode
|
|
150
|
+
if (frameDataChunks.length > 0 && currentFrameControl) {
|
|
151
|
+
const frameData = await this.decodeFrameData(frameDataChunks, currentFrameControl.width, currentFrameControl.height, bitDepth, colorType);
|
|
152
|
+
frames.push({
|
|
153
|
+
width: currentFrameControl.width,
|
|
154
|
+
height: currentFrameControl.height,
|
|
155
|
+
data: frameData,
|
|
156
|
+
frameMetadata: {
|
|
157
|
+
delay: currentFrameControl.delay,
|
|
158
|
+
disposal: currentFrameControl.disposal,
|
|
159
|
+
left: currentFrameControl.xOffset,
|
|
160
|
+
top: currentFrameControl.yOffset,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
frameDataChunks = [];
|
|
164
|
+
}
|
|
165
|
+
// Parse frame control
|
|
166
|
+
const _fcSeq = this.readUint32(chunk.data, 0);
|
|
167
|
+
const fcWidth = this.readUint32(chunk.data, 4);
|
|
168
|
+
const fcHeight = this.readUint32(chunk.data, 8);
|
|
169
|
+
const fcXOffset = this.readUint32(chunk.data, 12);
|
|
170
|
+
const fcYOffset = this.readUint32(chunk.data, 16);
|
|
171
|
+
const delayNum = this.readUint16(chunk.data, 20);
|
|
172
|
+
const delayDen = this.readUint16(chunk.data, 22);
|
|
173
|
+
const disposeOp = chunk.data[24];
|
|
174
|
+
const blendOp = chunk.data[25];
|
|
175
|
+
const delay = delayDen === 0
|
|
176
|
+
? delayNum * 10
|
|
177
|
+
: Math.round((delayNum / delayDen) * 1000);
|
|
178
|
+
let disposal = "none";
|
|
179
|
+
if (disposeOp === 1)
|
|
180
|
+
disposal = "background";
|
|
181
|
+
else if (disposeOp === 2)
|
|
182
|
+
disposal = "previous";
|
|
183
|
+
currentFrameControl = {
|
|
184
|
+
width: fcWidth,
|
|
185
|
+
height: fcHeight,
|
|
186
|
+
xOffset: fcXOffset,
|
|
187
|
+
yOffset: fcYOffset,
|
|
188
|
+
delay,
|
|
189
|
+
disposal,
|
|
190
|
+
blend: blendOp === 1 ? "over" : "source",
|
|
191
|
+
};
|
|
192
|
+
// If this is the first fcTL and we have default image data, use it for this frame
|
|
193
|
+
if (frames.length === 0 && defaultImageChunks.length > 0) {
|
|
194
|
+
frameDataChunks = defaultImageChunks;
|
|
195
|
+
defaultImageChunks = [];
|
|
196
|
+
}
|
|
197
|
+
hasSeenFcTL = true;
|
|
198
|
+
}
|
|
199
|
+
else if (chunk.type === "IDAT") {
|
|
200
|
+
if (!hasSeenFcTL) {
|
|
201
|
+
// Collect default image chunks
|
|
202
|
+
defaultImageChunks.push(chunk.data);
|
|
203
|
+
}
|
|
204
|
+
else if (currentFrameControl) {
|
|
205
|
+
// IDAT after first fcTL belongs to that frame
|
|
206
|
+
frameDataChunks.push(chunk.data);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else if (chunk.type === "fdAT") {
|
|
210
|
+
// Frame data chunk (skip sequence number)
|
|
211
|
+
const _frameSeq = this.readUint32(chunk.data, 0);
|
|
212
|
+
frameDataChunks.push(chunk.data.slice(4));
|
|
213
|
+
}
|
|
214
|
+
else if (chunk.type === "IEND") {
|
|
215
|
+
// Decode last frame if any
|
|
216
|
+
if (frameDataChunks.length > 0 && currentFrameControl) {
|
|
217
|
+
const frameData = await this.decodeFrameData(frameDataChunks, currentFrameControl.width, currentFrameControl.height, bitDepth, colorType);
|
|
218
|
+
frames.push({
|
|
219
|
+
width: currentFrameControl.width,
|
|
220
|
+
height: currentFrameControl.height,
|
|
221
|
+
data: frameData,
|
|
222
|
+
frameMetadata: {
|
|
223
|
+
delay: currentFrameControl.delay,
|
|
224
|
+
disposal: currentFrameControl.disposal,
|
|
225
|
+
left: currentFrameControl.xOffset,
|
|
226
|
+
top: currentFrameControl.yOffset,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
else if (defaultImageChunks.length > 0) {
|
|
231
|
+
// Only default image, no fcTL found - treat as single frame
|
|
232
|
+
const frameData = await this.decodeFrameData(defaultImageChunks, width, height, bitDepth, colorType);
|
|
233
|
+
frames.push({
|
|
234
|
+
width,
|
|
235
|
+
height,
|
|
236
|
+
data: frameData,
|
|
237
|
+
frameMetadata: {
|
|
238
|
+
delay: 0,
|
|
239
|
+
disposal: "none",
|
|
240
|
+
left: 0,
|
|
241
|
+
top: 0,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
width,
|
|
250
|
+
height,
|
|
251
|
+
frames,
|
|
252
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Encode RGBA image data to APNG format (single frame)
|
|
257
|
+
* @param imageData Image data to encode
|
|
258
|
+
* @returns Encoded APNG image bytes
|
|
259
|
+
*/
|
|
260
|
+
encode(imageData) {
|
|
261
|
+
// For single frame, create a multi-frame with one frame
|
|
262
|
+
const multiFrame = {
|
|
263
|
+
width: imageData.width,
|
|
264
|
+
height: imageData.height,
|
|
265
|
+
frames: [{
|
|
266
|
+
width: imageData.width,
|
|
267
|
+
height: imageData.height,
|
|
268
|
+
data: imageData.data,
|
|
269
|
+
frameMetadata: { delay: 0 },
|
|
270
|
+
}],
|
|
271
|
+
metadata: imageData.metadata,
|
|
272
|
+
};
|
|
273
|
+
return this.encodeFrames(multiFrame);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Encode multi-frame image data to APNG format
|
|
277
|
+
* @param imageData Multi-frame image data to encode
|
|
278
|
+
* @returns Encoded APNG image bytes
|
|
279
|
+
*/
|
|
280
|
+
async encodeFrames(imageData) {
|
|
281
|
+
const { width, height, frames, metadata } = imageData;
|
|
282
|
+
if (frames.length === 0) {
|
|
283
|
+
throw new Error("No frames to encode");
|
|
284
|
+
}
|
|
285
|
+
// Prepare IHDR chunk
|
|
286
|
+
const ihdr = new Uint8Array(13);
|
|
287
|
+
this.writeUint32(ihdr, 0, width);
|
|
288
|
+
this.writeUint32(ihdr, 4, height);
|
|
289
|
+
ihdr[8] = 8; // bit depth
|
|
290
|
+
ihdr[9] = 6; // color type: RGBA
|
|
291
|
+
ihdr[10] = 0; // compression method
|
|
292
|
+
ihdr[11] = 0; // filter method
|
|
293
|
+
ihdr[12] = 0; // interlace method
|
|
294
|
+
// Build PNG
|
|
295
|
+
const chunks = [];
|
|
296
|
+
chunks.push(new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])); // PNG signature
|
|
297
|
+
chunks.push(this.createChunk("IHDR", ihdr));
|
|
298
|
+
// Add acTL chunk for animation control
|
|
299
|
+
const actl = new Uint8Array(8);
|
|
300
|
+
this.writeUint32(actl, 0, frames.length); // num_frames
|
|
301
|
+
this.writeUint32(actl, 4, 0); // num_plays (0 = infinite)
|
|
302
|
+
chunks.push(this.createChunk("acTL", actl));
|
|
303
|
+
// Add metadata chunks if available
|
|
304
|
+
this.addMetadataChunks(chunks, metadata);
|
|
305
|
+
// Add frames
|
|
306
|
+
let sequenceNumber = 0;
|
|
307
|
+
for (let i = 0; i < frames.length; i++) {
|
|
308
|
+
const frame = frames[i];
|
|
309
|
+
const fctl = new Uint8Array(26);
|
|
310
|
+
this.writeUint32(fctl, 0, sequenceNumber++); // sequence_number
|
|
311
|
+
this.writeUint32(fctl, 4, frame.width); // width
|
|
312
|
+
this.writeUint32(fctl, 8, frame.height); // height
|
|
313
|
+
this.writeUint32(fctl, 12, frame.frameMetadata?.left ?? 0); // x_offset
|
|
314
|
+
this.writeUint32(fctl, 16, frame.frameMetadata?.top ?? 0); // y_offset
|
|
315
|
+
// Convert delay from milliseconds to fraction
|
|
316
|
+
const delay = frame.frameMetadata?.delay ?? 100;
|
|
317
|
+
// Use milliseconds directly if possible (up to ~65 seconds)
|
|
318
|
+
if (delay < 65536) {
|
|
319
|
+
this.writeUint16(fctl, 20, delay); // delay_num
|
|
320
|
+
this.writeUint16(fctl, 22, 1000); // delay_den (1/1000 sec)
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
// Fallback to 1/100 sec for longer delays
|
|
324
|
+
this.writeUint16(fctl, 20, Math.round(delay / 10)); // delay_num
|
|
325
|
+
this.writeUint16(fctl, 22, 100); // delay_den (1/100 sec)
|
|
326
|
+
}
|
|
327
|
+
// Disposal method
|
|
328
|
+
let disposeOp = 0; // APNG_DISPOSE_OP_NONE
|
|
329
|
+
if (frame.frameMetadata?.disposal === "background") {
|
|
330
|
+
disposeOp = 1; // APNG_DISPOSE_OP_BACKGROUND
|
|
331
|
+
}
|
|
332
|
+
else if (frame.frameMetadata?.disposal === "previous") {
|
|
333
|
+
disposeOp = 2; // APNG_DISPOSE_OP_PREVIOUS
|
|
334
|
+
}
|
|
335
|
+
fctl[24] = disposeOp;
|
|
336
|
+
fctl[25] = 0; // blend_op: APNG_BLEND_OP_SOURCE
|
|
337
|
+
chunks.push(this.createChunk("fcTL", fctl));
|
|
338
|
+
// Filter and compress frame data
|
|
339
|
+
const filtered = this.filterData(frame.data, frame.width, frame.height);
|
|
340
|
+
const compressed = await this.deflate(filtered);
|
|
341
|
+
if (i === 0) {
|
|
342
|
+
// First frame uses IDAT
|
|
343
|
+
chunks.push(this.createChunk("IDAT", compressed));
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
// Subsequent frames use fdAT with sequence number
|
|
347
|
+
const fdat = new Uint8Array(4 + compressed.length);
|
|
348
|
+
this.writeUint32(fdat, 0, sequenceNumber++);
|
|
349
|
+
fdat.set(compressed, 4);
|
|
350
|
+
chunks.push(this.createChunk("fdAT", fdat));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
chunks.push(this.createChunk("IEND", new Uint8Array(0)));
|
|
354
|
+
// Concatenate all chunks
|
|
355
|
+
return this.concatenateArrays(chunks);
|
|
356
|
+
}
|
|
357
|
+
// Helper methods for frame decoding
|
|
358
|
+
async decodeFrameData(chunks, width, height, bitDepth, colorType) {
|
|
359
|
+
// Concatenate chunks
|
|
360
|
+
const idatData = this.concatenateArrays(chunks);
|
|
361
|
+
// Decompress data
|
|
362
|
+
const decompressed = await this.inflate(idatData);
|
|
363
|
+
// Unfilter and convert to RGBA
|
|
364
|
+
const rgba = this.unfilterAndConvert(decompressed, width, height, bitDepth, colorType);
|
|
365
|
+
return rgba;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
exports.APNGFormat = APNGFormat;
|
|
@@ -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
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BMPFormat = void 0;
|
|
4
4
|
const security_js_1 = require("../utils/security.js");
|
|
5
|
+
const byte_utils_js_1 = require("../utils/byte_utils.js");
|
|
5
6
|
// Constants for unit conversions
|
|
6
7
|
const INCHES_PER_METER = 39.3701;
|
|
7
8
|
/**
|
|
@@ -45,10 +46,10 @@ class BMPFormat {
|
|
|
45
46
|
throw new Error("Invalid BMP signature");
|
|
46
47
|
}
|
|
47
48
|
// Read BMP file header (14 bytes)
|
|
48
|
-
const _fileSize =
|
|
49
|
-
const dataOffset =
|
|
49
|
+
const _fileSize = (0, byte_utils_js_1.readUint32LE)(data, 2);
|
|
50
|
+
const dataOffset = (0, byte_utils_js_1.readUint32LE)(data, 10);
|
|
50
51
|
// Read DIB header (at least 40 bytes for BITMAPINFOHEADER)
|
|
51
|
-
const dibHeaderSize =
|
|
52
|
+
const dibHeaderSize = (0, byte_utils_js_1.readUint32LE)(data, 14);
|
|
52
53
|
let width;
|
|
53
54
|
let height;
|
|
54
55
|
let bitDepth;
|
|
@@ -56,13 +57,13 @@ class BMPFormat {
|
|
|
56
57
|
const metadata = {};
|
|
57
58
|
if (dibHeaderSize >= 40) {
|
|
58
59
|
// BITMAPINFOHEADER or later
|
|
59
|
-
width =
|
|
60
|
-
height =
|
|
61
|
-
bitDepth =
|
|
62
|
-
compression =
|
|
60
|
+
width = (0, byte_utils_js_1.readInt32LE)(data, 18);
|
|
61
|
+
height = (0, byte_utils_js_1.readInt32LE)(data, 22);
|
|
62
|
+
bitDepth = (0, byte_utils_js_1.readUint16LE)(data, 28);
|
|
63
|
+
compression = (0, byte_utils_js_1.readUint32LE)(data, 30);
|
|
63
64
|
// Read DPI information (pixels per meter)
|
|
64
|
-
const xPixelsPerMeter =
|
|
65
|
-
const yPixelsPerMeter =
|
|
65
|
+
const xPixelsPerMeter = (0, byte_utils_js_1.readInt32LE)(data, 38);
|
|
66
|
+
const yPixelsPerMeter = (0, byte_utils_js_1.readInt32LE)(data, 42);
|
|
66
67
|
if (xPixelsPerMeter > 0 && yPixelsPerMeter > 0) {
|
|
67
68
|
// Convert pixels per meter to DPI
|
|
68
69
|
metadata.dpiX = Math.round(xPixelsPerMeter / INCHES_PER_METER);
|
|
@@ -137,21 +138,21 @@ class BMPFormat {
|
|
|
137
138
|
// BMP File Header (14 bytes)
|
|
138
139
|
result[0] = 0x42; // 'B'
|
|
139
140
|
result[1] = 0x4d; // 'M'
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
(0, byte_utils_js_1.writeUint32LE)(result, 2, fileSize); // File size
|
|
142
|
+
(0, byte_utils_js_1.writeUint32LE)(result, 6, 0); // Reserved
|
|
143
|
+
(0, byte_utils_js_1.writeUint32LE)(result, 10, 54); // Offset to pixel data (14 + 40)
|
|
143
144
|
// DIB Header (BITMAPINFOHEADER - 40 bytes)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
145
|
+
(0, byte_utils_js_1.writeUint32LE)(result, 14, 40); // DIB header size
|
|
146
|
+
(0, byte_utils_js_1.writeInt32LE)(result, 18, width); // Width
|
|
147
|
+
(0, byte_utils_js_1.writeInt32LE)(result, 22, height); // Height (positive = bottom-up)
|
|
148
|
+
(0, byte_utils_js_1.writeUint16LE)(result, 26, 1); // Planes
|
|
149
|
+
(0, byte_utils_js_1.writeUint16LE)(result, 28, 32); // Bits per pixel
|
|
150
|
+
(0, byte_utils_js_1.writeUint32LE)(result, 30, 0); // Compression (0 = uncompressed)
|
|
151
|
+
(0, byte_utils_js_1.writeUint32LE)(result, 34, pixelDataSize); // Image size
|
|
152
|
+
(0, byte_utils_js_1.writeInt32LE)(result, 38, xPixelsPerMeter); // X pixels per meter
|
|
153
|
+
(0, byte_utils_js_1.writeInt32LE)(result, 42, yPixelsPerMeter); // Y pixels per meter
|
|
154
|
+
(0, byte_utils_js_1.writeUint32LE)(result, 46, 0); // Colors in palette
|
|
155
|
+
(0, byte_utils_js_1.writeUint32LE)(result, 50, 0); // Important colors
|
|
155
156
|
// Write pixel data (bottom-to-top, BGR(A) format)
|
|
156
157
|
let offset = 54;
|
|
157
158
|
for (let y = height - 1; y >= 0; y--) {
|
|
@@ -170,29 +171,5 @@ class BMPFormat {
|
|
|
170
171
|
}
|
|
171
172
|
return Promise.resolve(result);
|
|
172
173
|
}
|
|
173
|
-
readUint16LE(data, offset) {
|
|
174
|
-
return data[offset] | (data[offset + 1] << 8);
|
|
175
|
-
}
|
|
176
|
-
readUint32LE(data, offset) {
|
|
177
|
-
return data[offset] | (data[offset + 1] << 8) |
|
|
178
|
-
(data[offset + 2] << 16) | (data[offset + 3] << 24);
|
|
179
|
-
}
|
|
180
|
-
readInt32LE(data, offset) {
|
|
181
|
-
const value = this.readUint32LE(data, offset);
|
|
182
|
-
return value > 0x7fffffff ? value - 0x100000000 : value;
|
|
183
|
-
}
|
|
184
|
-
writeUint16LE(data, offset, value) {
|
|
185
|
-
data[offset] = value & 0xff;
|
|
186
|
-
data[offset + 1] = (value >>> 8) & 0xff;
|
|
187
|
-
}
|
|
188
|
-
writeUint32LE(data, offset, value) {
|
|
189
|
-
data[offset] = value & 0xff;
|
|
190
|
-
data[offset + 1] = (value >>> 8) & 0xff;
|
|
191
|
-
data[offset + 2] = (value >>> 16) & 0xff;
|
|
192
|
-
data[offset + 3] = (value >>> 24) & 0xff;
|
|
193
|
-
}
|
|
194
|
-
writeInt32LE(data, offset, value) {
|
|
195
|
-
this.writeUint32LE(data, offset, value < 0 ? value + 0x100000000 : value);
|
|
196
|
-
}
|
|
197
174
|
}
|
|
198
175
|
exports.BMPFormat = BMPFormat;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ImageData } from "../types.js";
|
|
2
|
+
import { TIFFFormat } from "./tiff.js";
|
|
3
|
+
/**
|
|
4
|
+
* DNG format handler
|
|
5
|
+
* Implements a basic Linear DNG (Digital Negative) writer.
|
|
6
|
+
* DNG is based on TIFF/EP. This implementation creates a valid DNG
|
|
7
|
+
* containing uncompressed linear RGB data (demosaiced).
|
|
8
|
+
*/
|
|
9
|
+
export declare class DNGFormat extends TIFFFormat {
|
|
10
|
+
/** Format name identifier */
|
|
11
|
+
readonly name = "dng";
|
|
12
|
+
/** MIME type for DNG images */
|
|
13
|
+
readonly mimeType = "image/x-adobe-dng";
|
|
14
|
+
/**
|
|
15
|
+
* Check if the given data is a DNG image
|
|
16
|
+
* @param data Raw image data to check
|
|
17
|
+
* @returns true if data has DNG signature (TIFF signature + DNGVersion tag)
|
|
18
|
+
*/
|
|
19
|
+
canDecode(data: Uint8Array): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Encode RGBA image data to DNG format (Linear DNG)
|
|
22
|
+
* @param imageData Image data to encode
|
|
23
|
+
* @returns Encoded DNG image bytes
|
|
24
|
+
*/
|
|
25
|
+
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=dng.d.ts.map
|