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