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.
Files changed (72) hide show
  1. package/README.md +36 -18
  2. package/esm/mod.d.ts +30 -4
  3. package/esm/mod.js +30 -4
  4. package/esm/src/formats/apng.d.ts +50 -0
  5. package/esm/src/formats/apng.js +364 -0
  6. package/esm/src/formats/bmp.d.ts +0 -6
  7. package/esm/src/formats/bmp.js +24 -47
  8. package/esm/src/formats/dng.d.ts +27 -0
  9. package/esm/src/formats/dng.js +191 -0
  10. package/esm/src/formats/gif.d.ts +0 -2
  11. package/esm/src/formats/gif.js +10 -16
  12. package/esm/src/formats/ico.d.ts +41 -0
  13. package/esm/src/formats/ico.js +214 -0
  14. package/esm/src/formats/pam.d.ts +43 -0
  15. package/esm/src/formats/pam.js +177 -0
  16. package/esm/src/formats/pcx.d.ts +13 -0
  17. package/esm/src/formats/pcx.js +204 -0
  18. package/esm/src/formats/png.d.ts +2 -21
  19. package/esm/src/formats/png.js +5 -429
  20. package/esm/src/formats/png_base.d.ts +108 -0
  21. package/esm/src/formats/png_base.js +487 -0
  22. package/esm/src/formats/tiff.d.ts +7 -7
  23. package/esm/src/formats/webp.d.ts +0 -1
  24. package/esm/src/formats/webp.js +4 -7
  25. package/esm/src/image.d.ts +99 -0
  26. package/esm/src/image.js +204 -2
  27. package/esm/src/utils/byte_utils.d.ts +30 -0
  28. package/esm/src/utils/byte_utils.js +50 -0
  29. package/esm/src/utils/gif_encoder.d.ts +3 -2
  30. package/esm/src/utils/gif_encoder.js +115 -48
  31. package/esm/src/utils/image_processing.d.ts +91 -0
  32. package/esm/src/utils/image_processing.js +231 -0
  33. package/esm/src/utils/webp_decoder.js +47 -12
  34. package/esm/src/utils/webp_encoder.js +97 -39
  35. package/package.json +4 -1
  36. package/script/mod.d.ts +30 -4
  37. package/script/mod.js +36 -6
  38. package/script/src/formats/apng.d.ts +50 -0
  39. package/script/src/formats/apng.js +368 -0
  40. package/script/src/formats/bmp.d.ts +0 -6
  41. package/script/src/formats/bmp.js +24 -47
  42. package/script/src/formats/dng.d.ts +27 -0
  43. package/script/src/formats/dng.js +195 -0
  44. package/script/src/formats/gif.d.ts +0 -2
  45. package/script/src/formats/gif.js +10 -16
  46. package/script/src/formats/ico.d.ts +41 -0
  47. package/script/src/formats/ico.js +218 -0
  48. package/script/src/formats/pam.d.ts +43 -0
  49. package/script/src/formats/pam.js +181 -0
  50. package/script/src/formats/pcx.d.ts +13 -0
  51. package/script/src/formats/pcx.js +208 -0
  52. package/script/src/formats/png.d.ts +2 -21
  53. package/script/src/formats/png.js +5 -429
  54. package/script/src/formats/png_base.d.ts +108 -0
  55. package/script/src/formats/png_base.js +491 -0
  56. package/script/src/formats/tiff.d.ts +7 -7
  57. package/script/src/formats/webp.d.ts +0 -1
  58. package/script/src/formats/webp.js +4 -7
  59. package/script/src/image.d.ts +99 -0
  60. package/script/src/image.js +204 -2
  61. package/script/src/utils/byte_utils.d.ts +30 -0
  62. package/script/src/utils/byte_utils.js +58 -0
  63. package/script/src/utils/gif_encoder.d.ts +3 -2
  64. package/script/src/utils/gif_encoder.js +115 -48
  65. package/script/src/utils/image_processing.d.ts +91 -0
  66. package/script/src/utils/image_processing.js +242 -0
  67. package/script/src/utils/webp_decoder.js +47 -12
  68. package/script/src/utils/webp_encoder.js +97 -39
  69. package/esm/src/formats/raw.d.ts +0 -40
  70. package/esm/src/formats/raw.js +0 -118
  71. package/script/src/formats/raw.d.ts +0 -40
  72. 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 = this.readUint16LE(data, pos);
85
+ const width = (0, byte_utils_js_1.readUint16LE)(data, pos);
85
86
  pos += 2;
86
- const height = this.readUint16LE(data, pos);
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 firstFrame = imageData.frames[0];
246
- const singleFrameData = {
247
- width: firstFrame.width,
248
- height: firstFrame.height,
249
- data: firstFrame.data,
250
- metadata: imageData.metadata,
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