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/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
|
}
|
|
@@ -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
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { TIFFFormat } from "./tiff.js";
|
|
2
|
+
/**
|
|
3
|
+
* DNG format handler
|
|
4
|
+
* Implements a basic Linear DNG (Digital Negative) writer.
|
|
5
|
+
* DNG is based on TIFF/EP. This implementation creates a valid DNG
|
|
6
|
+
* containing uncompressed linear RGB data (demosaiced).
|
|
7
|
+
*/
|
|
8
|
+
export class DNGFormat extends TIFFFormat {
|
|
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: "dng"
|
|
17
|
+
});
|
|
18
|
+
/** MIME type for DNG images */
|
|
19
|
+
Object.defineProperty(this, "mimeType", {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: "image/x-adobe-dng"
|
|
24
|
+
});
|
|
25
|
+
// Helper methods duplicated from TIFFFormat because they are protected/private there
|
|
26
|
+
// and we can't easily access them if they are private.
|
|
27
|
+
// Let's check TIFFFormat visibility.
|
|
28
|
+
// The read/write helpers were not exported in the previous read_file output.
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Check if the given data is a DNG image
|
|
32
|
+
* @param data Raw image data to check
|
|
33
|
+
* @returns true if data has DNG signature (TIFF signature + DNGVersion tag)
|
|
34
|
+
*/
|
|
35
|
+
canDecode(data) {
|
|
36
|
+
// DNG is a TIFF file, so it must have a TIFF signature
|
|
37
|
+
if (!super.canDecode(data)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
// To be strictly a DNG, it should have the DNGVersion tag (0xC612 / 50706)
|
|
41
|
+
// However, scanning for tags in canDecode might be slow.
|
|
42
|
+
// For now, we rely on the fact that it's a TIFF-based format.
|
|
43
|
+
// If we want to be stricter, we would need to parse the IFD here.
|
|
44
|
+
// Let's do a quick check for the DNGVersion tag in the first few bytes if possible,
|
|
45
|
+
// but tags are in the IFD which can be anywhere.
|
|
46
|
+
// So we'll just check if it's a TIFF and maybe rely on extension or user intent.
|
|
47
|
+
// But wait, if we register both TIFF and DNG, who wins?
|
|
48
|
+
// The first one in the list.
|
|
49
|
+
// So we should probably implement a proper check if possible, or just accept that
|
|
50
|
+
// DNGs are TIFFs.
|
|
51
|
+
// For this implementation, we'll assume if it's a TIFF, it *could* be a DNG.
|
|
52
|
+
// But to distinguish, we really should check for DNGVersion.
|
|
53
|
+
// Let's try to find the DNGVersion tag (50706) in the first IFD.
|
|
54
|
+
try {
|
|
55
|
+
const isLittleEndian = data[0] === 0x49;
|
|
56
|
+
const ifdOffset = this.readUint32(data, 4, isLittleEndian);
|
|
57
|
+
// Safety check for offset
|
|
58
|
+
if (ifdOffset >= data.length)
|
|
59
|
+
return false;
|
|
60
|
+
const numEntries = this.readUint16(data, ifdOffset, isLittleEndian);
|
|
61
|
+
for (let i = 0; i < numEntries; i++) {
|
|
62
|
+
const entryOffset = ifdOffset + 2 + (i * 12);
|
|
63
|
+
if (entryOffset + 12 > data.length)
|
|
64
|
+
break;
|
|
65
|
+
const tag = this.readUint16(data, entryOffset, isLittleEndian);
|
|
66
|
+
if (tag === 50706) { // DNGVersion
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Encode RGBA image data to DNG format (Linear DNG)
|
|
78
|
+
* @param imageData Image data to encode
|
|
79
|
+
* @returns Encoded DNG image bytes
|
|
80
|
+
*/
|
|
81
|
+
encode(imageData) {
|
|
82
|
+
const { width, height, data } = imageData;
|
|
83
|
+
// We'll create a Linear DNG (demosaiced RGB)
|
|
84
|
+
// This is very similar to a standard TIFF but with specific tags.
|
|
85
|
+
const result = [];
|
|
86
|
+
// Header (8 bytes)
|
|
87
|
+
// Little-endian byte order
|
|
88
|
+
result.push(0x49, 0x49); // "II"
|
|
89
|
+
result.push(0x2a, 0x00); // 42
|
|
90
|
+
// IFD offset (will be after header and pixel data)
|
|
91
|
+
const ifdOffset = 8 + data.length;
|
|
92
|
+
this.writeUint32LE(result, ifdOffset);
|
|
93
|
+
// Pixel data (Uncompressed RGBA)
|
|
94
|
+
for (let i = 0; i < data.length; i++) {
|
|
95
|
+
result.push(data[i]);
|
|
96
|
+
}
|
|
97
|
+
// IFD (Image File Directory)
|
|
98
|
+
const ifdStart = result.length;
|
|
99
|
+
// Tags we need for DNG:
|
|
100
|
+
// - NewSubfileType (254)
|
|
101
|
+
// - ImageWidth (256)
|
|
102
|
+
// - ImageHeight (257)
|
|
103
|
+
// - BitsPerSample (258)
|
|
104
|
+
// - Compression (259)
|
|
105
|
+
// - PhotometricInterpretation (262)
|
|
106
|
+
// - StripOffsets (273)
|
|
107
|
+
// - SamplesPerPixel (277)
|
|
108
|
+
// - RowsPerStrip (278)
|
|
109
|
+
// - StripByteCounts (279)
|
|
110
|
+
// - PlanarConfiguration (284)
|
|
111
|
+
// - ExtraSamples (338) - for Alpha
|
|
112
|
+
// - DNGVersion (50706)
|
|
113
|
+
// - UniqueCameraModel (50708)
|
|
114
|
+
const numEntries = 14;
|
|
115
|
+
this.writeUint16LE(result, numEntries);
|
|
116
|
+
// Calculate offsets for variable-length data
|
|
117
|
+
let dataOffset = ifdStart + 2 + numEntries * 12 + 4; // +4 for next IFD offset (0)
|
|
118
|
+
// 1. NewSubfileType (0x00FE) - 0 = Full resolution image
|
|
119
|
+
this.writeIFDEntry(result, 0x00FE, 4, 1, 0);
|
|
120
|
+
// 2. ImageWidth (0x0100)
|
|
121
|
+
this.writeIFDEntry(result, 0x0100, 4, 1, width);
|
|
122
|
+
// 3. ImageHeight (0x0101)
|
|
123
|
+
this.writeIFDEntry(result, 0x0101, 4, 1, height);
|
|
124
|
+
// 4. BitsPerSample (0x0102) - 8, 8, 8, 8
|
|
125
|
+
this.writeIFDEntry(result, 0x0102, 3, 4, dataOffset);
|
|
126
|
+
// Write the actual values later
|
|
127
|
+
const _bitsPerSampleOffset = dataOffset;
|
|
128
|
+
dataOffset += 8; // 4 * 2 bytes
|
|
129
|
+
// 5. Compression (0x0103) - 1 = Uncompressed
|
|
130
|
+
this.writeIFDEntry(result, 0x0103, 3, 1, 1);
|
|
131
|
+
// 6. PhotometricInterpretation (0x0106) - 2 = RGB (Linear DNG)
|
|
132
|
+
// For Raw DNG it would be 32803 (CFA), but we are saving processed RGB data
|
|
133
|
+
this.writeIFDEntry(result, 0x0106, 3, 1, 2);
|
|
134
|
+
// 7. StripOffsets (0x0111)
|
|
135
|
+
this.writeIFDEntry(result, 0x0111, 4, 1, 8); // Pixel data starts at offset 8
|
|
136
|
+
// 8. SamplesPerPixel (0x0115) - 4 (RGBA)
|
|
137
|
+
this.writeIFDEntry(result, 0x0115, 3, 1, 4);
|
|
138
|
+
// 9. RowsPerStrip (0x0116)
|
|
139
|
+
this.writeIFDEntry(result, 0x0116, 4, 1, height);
|
|
140
|
+
// 10. StripByteCounts (0x0117)
|
|
141
|
+
this.writeIFDEntry(result, 0x0117, 4, 1, data.length);
|
|
142
|
+
// 11. PlanarConfiguration (0x011C) - 1 = Chunky
|
|
143
|
+
this.writeIFDEntry(result, 0x011C, 3, 1, 1);
|
|
144
|
+
// 12. ExtraSamples (0x0152) - 2 = Unassociated alpha
|
|
145
|
+
this.writeIFDEntry(result, 0x0152, 3, 1, 2); // 1 value, fits in offset field? No, it's SHORT (3), count 1. Value 2.
|
|
146
|
+
// Wait, writeIFDEntry puts value in offset field if count*type_size <= 4.
|
|
147
|
+
// SHORT is 2 bytes. 1 * 2 = 2 <= 4. So value goes in offset.
|
|
148
|
+
// 13. DNGVersion (0xC612 / 50706) - 1, 4, 0, 0
|
|
149
|
+
this.writeIFDEntry(result, 50706, 1, 4, 0x01040000);
|
|
150
|
+
// BYTE (1) count 4. 1*4=4. Fits.
|
|
151
|
+
// 1, 4, 0, 0 -> 0x01, 0x04, 0x00, 0x00.
|
|
152
|
+
// Little endian: 01 04 00 00.
|
|
153
|
+
// As uint32: 0x00000401? No, bytes are 1, 4, 0, 0.
|
|
154
|
+
// In file: 01 04 00 00.
|
|
155
|
+
// readUint32LE would read this as 0x00000401.
|
|
156
|
+
// So we pass 0x00000401?
|
|
157
|
+
// Let's verify writeIFDEntry logic in TIFFFormat (I can't see it but I assume it writes value directly if it fits).
|
|
158
|
+
// Actually, I need to check how writeIFDEntry works.
|
|
159
|
+
// Assuming it takes a number and writes it.
|
|
160
|
+
// 14. UniqueCameraModel (0xC614 / 50708) - "Cross Image DNG"
|
|
161
|
+
const modelName = "Cross Image DNG\0";
|
|
162
|
+
const modelNameBytes = new TextEncoder().encode(modelName);
|
|
163
|
+
this.writeIFDEntry(result, 50708, 2, modelNameBytes.length, dataOffset);
|
|
164
|
+
const _modelNameOffset = dataOffset;
|
|
165
|
+
dataOffset += modelNameBytes.length;
|
|
166
|
+
// Next IFD offset (0)
|
|
167
|
+
this.writeUint32LE(result, 0);
|
|
168
|
+
// Write variable length data
|
|
169
|
+
// BitsPerSample data (8, 8, 8, 8)
|
|
170
|
+
// We need to write this at bitsPerSampleOffset
|
|
171
|
+
// But we are appending to result array.
|
|
172
|
+
// We calculated dataOffset relative to start of file?
|
|
173
|
+
// No, dataOffset was initialized to `ifdStart + 2 + numEntries * 12 + 4`.
|
|
174
|
+
// This is correct absolute offset.
|
|
175
|
+
// But we need to fill the gap between end of IFD and dataOffset?
|
|
176
|
+
// No, we are writing sequentially.
|
|
177
|
+
// Wait, `result` is an array of bytes.
|
|
178
|
+
// We wrote the IFD entries. Now we are at `ifdStart + 2 + numEntries * 12 + 4`.
|
|
179
|
+
// This matches `bitsPerSampleOffset`.
|
|
180
|
+
// Write BitsPerSample (8, 8, 8, 8)
|
|
181
|
+
this.writeUint16LE(result, 8);
|
|
182
|
+
this.writeUint16LE(result, 8);
|
|
183
|
+
this.writeUint16LE(result, 8);
|
|
184
|
+
this.writeUint16LE(result, 8);
|
|
185
|
+
// Write UniqueCameraModel string
|
|
186
|
+
for (let i = 0; i < modelNameBytes.length; i++) {
|
|
187
|
+
result.push(modelNameBytes[i]);
|
|
188
|
+
}
|
|
189
|
+
return Promise.resolve(new Uint8Array(result));
|
|
190
|
+
}
|
|
191
|
+
}
|
package/esm/src/formats/gif.d.ts
CHANGED
|
@@ -46,11 +46,9 @@ export declare class GIFFormat implements ImageFormat {
|
|
|
46
46
|
decodeFrames(data: Uint8Array): Promise<MultiFrameImageData>;
|
|
47
47
|
/**
|
|
48
48
|
* Encode multi-frame image data to animated GIF
|
|
49
|
-
* Note: Currently not implemented, will encode only first frame
|
|
50
49
|
*/
|
|
51
50
|
encodeFrames(imageData: MultiFrameImageData, _options?: unknown): Promise<Uint8Array>;
|
|
52
51
|
private mapDisposalMethod;
|
|
53
|
-
private readUint16LE;
|
|
54
52
|
private decodeUsingRuntime;
|
|
55
53
|
private readDataSubBlocks;
|
|
56
54
|
private parseComment;
|
package/esm/src/formats/gif.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { GIFDecoder } from "../utils/gif_decoder.js";
|
|
2
2
|
import { GIFEncoder } from "../utils/gif_encoder.js";
|
|
3
3
|
import { validateImageDimensions } from "../utils/security.js";
|
|
4
|
+
import { readUint16LE } from "../utils/byte_utils.js";
|
|
4
5
|
/**
|
|
5
6
|
* GIF format handler
|
|
6
7
|
* Now includes pure-JS implementation with custom LZW compression/decompression
|
|
@@ -78,9 +79,9 @@ export class GIFFormat {
|
|
|
78
79
|
// Fall back to runtime decoder if pure-JS fails
|
|
79
80
|
console.warn("Pure-JS GIF decoder failed, falling back to runtime:", error);
|
|
80
81
|
let pos = 6; // Skip "GIF89a" or "GIF87a"
|
|
81
|
-
const width =
|
|
82
|
+
const width = readUint16LE(data, pos);
|
|
82
83
|
pos += 2;
|
|
83
|
-
const height =
|
|
84
|
+
const height = readUint16LE(data, pos);
|
|
84
85
|
// Validate dimensions for security (prevent integer overflow and heap exhaustion)
|
|
85
86
|
validateImageDimensions(width, height);
|
|
86
87
|
const rgba = await this.decodeUsingRuntime(data, width, height);
|
|
@@ -231,22 +232,18 @@ export class GIFFormat {
|
|
|
231
232
|
}
|
|
232
233
|
/**
|
|
233
234
|
* Encode multi-frame image data to animated GIF
|
|
234
|
-
* Note: Currently not implemented, will encode only first frame
|
|
235
235
|
*/
|
|
236
236
|
encodeFrames(imageData, _options) {
|
|
237
|
-
// For now, just encode the first frame using the existing encoder
|
|
238
|
-
// Full multi-frame encoding would require a more complex GIFEncoder
|
|
239
237
|
if (imageData.frames.length === 0) {
|
|
240
238
|
throw new Error("No frames to encode");
|
|
241
239
|
}
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
return this.encode(singleFrameData);
|
|
240
|
+
const encoder = new GIFEncoder(imageData.width, imageData.height);
|
|
241
|
+
for (const frame of imageData.frames) {
|
|
242
|
+
// Get delay from metadata (default to 100ms if not set)
|
|
243
|
+
const delay = frame.frameMetadata?.delay ?? 100;
|
|
244
|
+
encoder.addFrame(frame.data, delay);
|
|
245
|
+
}
|
|
246
|
+
return Promise.resolve(encoder.encode());
|
|
250
247
|
}
|
|
251
248
|
mapDisposalMethod(disposal) {
|
|
252
249
|
switch (disposal) {
|
|
@@ -261,9 +258,6 @@ export class GIFFormat {
|
|
|
261
258
|
return "none";
|
|
262
259
|
}
|
|
263
260
|
}
|
|
264
|
-
readUint16LE(data, offset) {
|
|
265
|
-
return data[offset] | (data[offset + 1] << 8);
|
|
266
|
-
}
|
|
267
261
|
async decodeUsingRuntime(data, _width, _height) {
|
|
268
262
|
// Try to use ImageDecoder API if available (Deno, modern browsers)
|
|
269
263
|
if (typeof ImageDecoder !== "undefined") {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ImageData, ImageFormat } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* ICO format handler
|
|
4
|
+
* Implements a pure JavaScript ICO (Windows Icon) decoder and encoder
|
|
5
|
+
*
|
|
6
|
+
* ICO files can contain multiple images at different sizes.
|
|
7
|
+
* This implementation decodes the largest image and encodes as a single-image ICO.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ICOFormat implements ImageFormat {
|
|
10
|
+
/** Format name identifier */
|
|
11
|
+
readonly name = "ico";
|
|
12
|
+
/** MIME type for ICO images */
|
|
13
|
+
readonly mimeType = "image/x-icon";
|
|
14
|
+
private pngFormat;
|
|
15
|
+
/**
|
|
16
|
+
* Check if the given data is an ICO image
|
|
17
|
+
* @param data Raw image data to check
|
|
18
|
+
* @returns true if data has ICO/CUR signature
|
|
19
|
+
*/
|
|
20
|
+
canDecode(data: Uint8Array): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Decode ICO image data to RGBA
|
|
23
|
+
* Selects and decodes the largest image in the ICO file
|
|
24
|
+
* @param data Raw ICO image data
|
|
25
|
+
* @returns Decoded image data with RGBA pixels
|
|
26
|
+
*/
|
|
27
|
+
decode(data: Uint8Array): Promise<ImageData>;
|
|
28
|
+
/**
|
|
29
|
+
* Decode a DIB (Device Independent Bitmap) format
|
|
30
|
+
* This is a BMP without the 14-byte file header
|
|
31
|
+
*/
|
|
32
|
+
private decodeDIB;
|
|
33
|
+
/**
|
|
34
|
+
* Encode RGBA image data to ICO format
|
|
35
|
+
* Creates an ICO file with a single PNG-encoded image
|
|
36
|
+
* @param imageData Image data to encode
|
|
37
|
+
* @returns Encoded ICO image bytes
|
|
38
|
+
*/
|
|
39
|
+
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=ico.d.ts.map
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { validateImageDimensions } from "../utils/security.js";
|
|
2
|
+
import { PNGFormat } from "./png.js";
|
|
3
|
+
import { readInt32LE, readUint16LE, readUint32LE, writeUint16LE, writeUint32LE, } from "../utils/byte_utils.js";
|
|
4
|
+
/**
|
|
5
|
+
* ICO format handler
|
|
6
|
+
* Implements a pure JavaScript ICO (Windows Icon) decoder and encoder
|
|
7
|
+
*
|
|
8
|
+
* ICO files can contain multiple images at different sizes.
|
|
9
|
+
* This implementation decodes the largest image and encodes as a single-image ICO.
|
|
10
|
+
*/
|
|
11
|
+
export class ICOFormat {
|
|
12
|
+
constructor() {
|
|
13
|
+
/** Format name identifier */
|
|
14
|
+
Object.defineProperty(this, "name", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: "ico"
|
|
19
|
+
});
|
|
20
|
+
/** MIME type for ICO images */
|
|
21
|
+
Object.defineProperty(this, "mimeType", {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
writable: true,
|
|
25
|
+
value: "image/x-icon"
|
|
26
|
+
});
|
|
27
|
+
Object.defineProperty(this, "pngFormat", {
|
|
28
|
+
enumerable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
writable: true,
|
|
31
|
+
value: new PNGFormat()
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if the given data is an ICO image
|
|
36
|
+
* @param data Raw image data to check
|
|
37
|
+
* @returns true if data has ICO/CUR signature
|
|
38
|
+
*/
|
|
39
|
+
canDecode(data) {
|
|
40
|
+
// ICO signature: reserved=0, type=1 (icon) or 2 (cursor)
|
|
41
|
+
return data.length >= 6 &&
|
|
42
|
+
data[0] === 0 && data[1] === 0 && // Reserved
|
|
43
|
+
(data[2] === 1 || data[2] === 2) && data[3] === 0 && // Type = 1 (icon) or 2 (cursor)
|
|
44
|
+
data[4] !== 0; // Count > 0
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Decode ICO image data to RGBA
|
|
48
|
+
* Selects and decodes the largest image in the ICO file
|
|
49
|
+
* @param data Raw ICO image data
|
|
50
|
+
* @returns Decoded image data with RGBA pixels
|
|
51
|
+
*/
|
|
52
|
+
async decode(data) {
|
|
53
|
+
if (!this.canDecode(data)) {
|
|
54
|
+
throw new Error("Invalid ICO signature");
|
|
55
|
+
}
|
|
56
|
+
// Read ICONDIR header
|
|
57
|
+
const count = readUint16LE(data, 4);
|
|
58
|
+
if (count === 0) {
|
|
59
|
+
throw new Error("ICO file contains no images");
|
|
60
|
+
}
|
|
61
|
+
// Read all ICONDIRENTRY structures (16 bytes each, starting at offset 6)
|
|
62
|
+
const entries = [];
|
|
63
|
+
for (let i = 0; i < count; i++) {
|
|
64
|
+
const entryOffset = 6 + i * 16;
|
|
65
|
+
if (entryOffset + 16 > data.length) {
|
|
66
|
+
throw new Error("Invalid ICO file: entry data out of bounds");
|
|
67
|
+
}
|
|
68
|
+
let width = data[entryOffset]; // 0 means 256
|
|
69
|
+
let height = data[entryOffset + 1]; // 0 means 256
|
|
70
|
+
// Width/height of 0 means 256 pixels
|
|
71
|
+
if (width === 0)
|
|
72
|
+
width = 256;
|
|
73
|
+
if (height === 0)
|
|
74
|
+
height = 256;
|
|
75
|
+
const size = readUint32LE(data, entryOffset + 8);
|
|
76
|
+
const offset = readUint32LE(data, entryOffset + 12);
|
|
77
|
+
entries.push({ width, height, size, offset });
|
|
78
|
+
}
|
|
79
|
+
// Find the largest image (by area)
|
|
80
|
+
let largestEntry = entries[0];
|
|
81
|
+
let largestArea = largestEntry.width * largestEntry.height;
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
const area = entry.width * entry.height;
|
|
84
|
+
if (area > largestArea) {
|
|
85
|
+
largestEntry = entry;
|
|
86
|
+
largestArea = area;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Extract image data
|
|
90
|
+
const imageStart = largestEntry.offset;
|
|
91
|
+
const imageEnd = imageStart + largestEntry.size;
|
|
92
|
+
if (imageEnd > data.length) {
|
|
93
|
+
throw new Error("Invalid ICO file: image data out of bounds");
|
|
94
|
+
}
|
|
95
|
+
const imageData = data.slice(imageStart, imageEnd);
|
|
96
|
+
// Check if it's a PNG (starts with PNG signature)
|
|
97
|
+
if (imageData.length >= 8 &&
|
|
98
|
+
imageData[0] === 0x89 &&
|
|
99
|
+
imageData[1] === 0x50 &&
|
|
100
|
+
imageData[2] === 0x4e &&
|
|
101
|
+
imageData[3] === 0x47) {
|
|
102
|
+
// It's a PNG, decode it
|
|
103
|
+
return await this.pngFormat.decode(imageData);
|
|
104
|
+
}
|
|
105
|
+
// Otherwise, it's a BMP without the file header (DIB format)
|
|
106
|
+
return this.decodeDIB(imageData);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Decode a DIB (Device Independent Bitmap) format
|
|
110
|
+
* This is a BMP without the 14-byte file header
|
|
111
|
+
*/
|
|
112
|
+
decodeDIB(data) {
|
|
113
|
+
// Read DIB header
|
|
114
|
+
const dibHeaderSize = readUint32LE(data, 0);
|
|
115
|
+
if (dibHeaderSize < 40) {
|
|
116
|
+
throw new Error("Unsupported DIB header size");
|
|
117
|
+
}
|
|
118
|
+
const width = readInt32LE(data, 4);
|
|
119
|
+
const height = readInt32LE(data, 8);
|
|
120
|
+
const bitDepth = readUint16LE(data, 14);
|
|
121
|
+
const compression = readUint32LE(data, 16);
|
|
122
|
+
// Validate dimensions
|
|
123
|
+
validateImageDimensions(width, Math.abs(height) / 2); // DIB height includes both XOR and AND mask data
|
|
124
|
+
// ICO files store height as 2x actual height (for AND mask)
|
|
125
|
+
const actualHeight = Math.abs(height) / 2;
|
|
126
|
+
// Only support uncompressed DIBs
|
|
127
|
+
if (compression !== 0) {
|
|
128
|
+
throw new Error(`Compressed DIB not supported (compression: ${compression})`);
|
|
129
|
+
}
|
|
130
|
+
// Support common bit depths
|
|
131
|
+
if (bitDepth !== 24 && bitDepth !== 32) {
|
|
132
|
+
throw new Error(`Unsupported bit depth: ${bitDepth}`);
|
|
133
|
+
}
|
|
134
|
+
// Calculate data offset (after header and optional color table)
|
|
135
|
+
const dataOffset = dibHeaderSize;
|
|
136
|
+
// Calculate row size (must be multiple of 4 bytes)
|
|
137
|
+
const bytesPerPixel = bitDepth / 8;
|
|
138
|
+
const rowSize = Math.floor((bitDepth * width + 31) / 32) * 4;
|
|
139
|
+
// Read XOR mask (color data)
|
|
140
|
+
const rgba = new Uint8Array(width * actualHeight * 4);
|
|
141
|
+
for (let y = 0; y < actualHeight; y++) {
|
|
142
|
+
// DIB data is stored bottom-to-top
|
|
143
|
+
const rowIndex = actualHeight - 1 - y;
|
|
144
|
+
const rowOffset = dataOffset + y * rowSize;
|
|
145
|
+
for (let x = 0; x < width; x++) {
|
|
146
|
+
const pixelOffset = rowOffset + x * bytesPerPixel;
|
|
147
|
+
const outIndex = (rowIndex * width + x) * 4;
|
|
148
|
+
// DIB stores pixels as BGR(A)
|
|
149
|
+
rgba[outIndex] = data[pixelOffset + 2]; // R
|
|
150
|
+
rgba[outIndex + 1] = data[pixelOffset + 1]; // G
|
|
151
|
+
rgba[outIndex + 2] = data[pixelOffset]; // B
|
|
152
|
+
rgba[outIndex + 3] = bitDepth === 32 ? data[pixelOffset + 3] : 255; // A
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Read AND mask if present (for transparency in 24-bit images)
|
|
156
|
+
if (bitDepth === 24) {
|
|
157
|
+
const andMaskOffset = dataOffset + rowSize * actualHeight;
|
|
158
|
+
const andMaskRowSize = Math.floor((width + 31) / 32) * 4;
|
|
159
|
+
for (let y = 0; y < actualHeight; y++) {
|
|
160
|
+
const rowIndex = actualHeight - 1 - y;
|
|
161
|
+
const rowOffset = andMaskOffset + y * andMaskRowSize;
|
|
162
|
+
for (let x = 0; x < width; x++) {
|
|
163
|
+
const byteOffset = rowOffset + Math.floor(x / 8);
|
|
164
|
+
const bitOffset = 7 - (x % 8);
|
|
165
|
+
const isTransparent = (data[byteOffset] & (1 << bitOffset)) !== 0;
|
|
166
|
+
if (isTransparent) {
|
|
167
|
+
const outIndex = (rowIndex * width + x) * 4 + 3;
|
|
168
|
+
rgba[outIndex] = 0; // Set alpha to 0 for transparent pixels
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return Promise.resolve({
|
|
174
|
+
width,
|
|
175
|
+
height: actualHeight,
|
|
176
|
+
data: rgba,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Encode RGBA image data to ICO format
|
|
181
|
+
* Creates an ICO file with a single PNG-encoded image
|
|
182
|
+
* @param imageData Image data to encode
|
|
183
|
+
* @returns Encoded ICO image bytes
|
|
184
|
+
*/
|
|
185
|
+
async encode(imageData) {
|
|
186
|
+
const { width, height } = imageData;
|
|
187
|
+
// Encode the image as PNG
|
|
188
|
+
const pngData = await this.pngFormat.encode(imageData);
|
|
189
|
+
// Create ICO file structure
|
|
190
|
+
// ICONDIR (6 bytes) + ICONDIRENTRY (16 bytes) + PNG data
|
|
191
|
+
const icoSize = 6 + 16 + pngData.length;
|
|
192
|
+
const result = new Uint8Array(icoSize);
|
|
193
|
+
// Write ICONDIR header
|
|
194
|
+
result[0] = 0; // Reserved
|
|
195
|
+
result[1] = 0; // Reserved
|
|
196
|
+
result[2] = 1; // Type = 1 (icon)
|
|
197
|
+
result[3] = 0; // Type high byte
|
|
198
|
+
result[4] = 1; // Count = 1
|
|
199
|
+
result[5] = 0; // Count high byte
|
|
200
|
+
// Write ICONDIRENTRY
|
|
201
|
+
const entryOffset = 6;
|
|
202
|
+
result[entryOffset] = width >= 256 ? 0 : width; // Width (0 = 256)
|
|
203
|
+
result[entryOffset + 1] = height >= 256 ? 0 : height; // Height (0 = 256)
|
|
204
|
+
result[entryOffset + 2] = 0; // Color count (0 = no palette)
|
|
205
|
+
result[entryOffset + 3] = 0; // Reserved
|
|
206
|
+
writeUint16LE(result, entryOffset + 4, 1); // Color planes
|
|
207
|
+
writeUint16LE(result, entryOffset + 6, 32); // Bits per pixel
|
|
208
|
+
writeUint32LE(result, entryOffset + 8, pngData.length); // Image size
|
|
209
|
+
writeUint32LE(result, entryOffset + 12, 22); // Image offset (6 + 16)
|
|
210
|
+
// Write PNG data
|
|
211
|
+
result.set(pngData, 22);
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
}
|