cross-image 0.2.0 → 0.2.2
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 +41 -28
- package/esm/mod.d.ts +4 -1
- package/esm/mod.js +4 -1
- package/esm/src/formats/apng.d.ts +50 -0
- package/esm/src/formats/apng.js +364 -0
- package/esm/src/formats/bmp.d.ts +0 -6
- package/esm/src/formats/bmp.js +24 -47
- package/esm/src/formats/dng.js +4 -4
- package/esm/src/formats/gif.d.ts +0 -2
- package/esm/src/formats/gif.js +10 -16
- package/esm/src/formats/ico.d.ts +41 -0
- package/esm/src/formats/ico.js +214 -0
- package/esm/src/formats/pcx.js +1 -1
- package/esm/src/formats/png.d.ts +2 -21
- package/esm/src/formats/png.js +5 -429
- package/esm/src/formats/png_base.d.ts +108 -0
- package/esm/src/formats/png_base.js +487 -0
- package/esm/src/formats/ppm.d.ts +50 -0
- package/esm/src/formats/ppm.js +242 -0
- package/esm/src/formats/tiff.d.ts +4 -0
- package/esm/src/formats/tiff.js +163 -44
- package/esm/src/formats/webp.d.ts +0 -1
- package/esm/src/formats/webp.js +4 -7
- package/esm/src/image.d.ts +30 -0
- package/esm/src/image.js +62 -1
- 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 +43 -0
- package/esm/src/utils/image_processing.js +230 -0
- package/package.json +1 -1
- package/script/mod.d.ts +4 -1
- package/script/mod.js +8 -2
- package/script/src/formats/apng.d.ts +50 -0
- package/script/src/formats/apng.js +368 -0
- package/script/src/formats/bmp.d.ts +0 -6
- package/script/src/formats/bmp.js +24 -47
- package/script/src/formats/dng.js +4 -4
- package/script/src/formats/gif.d.ts +0 -2
- package/script/src/formats/gif.js +10 -16
- package/script/src/formats/ico.d.ts +41 -0
- package/script/src/formats/ico.js +218 -0
- package/script/src/formats/pcx.js +1 -1
- package/script/src/formats/png.d.ts +2 -21
- package/script/src/formats/png.js +5 -429
- package/script/src/formats/png_base.d.ts +108 -0
- package/script/src/formats/png_base.js +491 -0
- package/script/src/formats/ppm.d.ts +50 -0
- package/script/src/formats/ppm.js +246 -0
- package/script/src/formats/tiff.d.ts +4 -0
- package/script/src/formats/tiff.js +163 -44
- package/script/src/formats/webp.d.ts +0 -1
- package/script/src/formats/webp.js +4 -7
- package/script/src/image.d.ts +30 -0
- package/script/src/image.js +61 -0
- package/script/src/utils/byte_utils.d.ts +30 -0
- package/script/src/utils/byte_utils.js +58 -0
- package/script/src/utils/gif_encoder.d.ts +3 -2
- package/script/src/utils/gif_encoder.js +115 -48
- package/script/src/utils/image_processing.d.ts +43 -0
- package/script/src/utils/image_processing.js +235 -0
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
|
}
|
package/esm/src/formats/dng.js
CHANGED
|
@@ -78,7 +78,7 @@ export class DNGFormat extends TIFFFormat {
|
|
|
78
78
|
* @param imageData Image data to encode
|
|
79
79
|
* @returns Encoded DNG image bytes
|
|
80
80
|
*/
|
|
81
|
-
|
|
81
|
+
encode(imageData) {
|
|
82
82
|
const { width, height, data } = imageData;
|
|
83
83
|
// We'll create a Linear DNG (demosaiced RGB)
|
|
84
84
|
// This is very similar to a standard TIFF but with specific tags.
|
|
@@ -124,7 +124,7 @@ export class DNGFormat extends TIFFFormat {
|
|
|
124
124
|
// 4. BitsPerSample (0x0102) - 8, 8, 8, 8
|
|
125
125
|
this.writeIFDEntry(result, 0x0102, 3, 4, dataOffset);
|
|
126
126
|
// Write the actual values later
|
|
127
|
-
const
|
|
127
|
+
const _bitsPerSampleOffset = dataOffset;
|
|
128
128
|
dataOffset += 8; // 4 * 2 bytes
|
|
129
129
|
// 5. Compression (0x0103) - 1 = Uncompressed
|
|
130
130
|
this.writeIFDEntry(result, 0x0103, 3, 1, 1);
|
|
@@ -161,7 +161,7 @@ export class DNGFormat extends TIFFFormat {
|
|
|
161
161
|
const modelName = "Cross Image DNG\0";
|
|
162
162
|
const modelNameBytes = new TextEncoder().encode(modelName);
|
|
163
163
|
this.writeIFDEntry(result, 50708, 2, modelNameBytes.length, dataOffset);
|
|
164
|
-
const
|
|
164
|
+
const _modelNameOffset = dataOffset;
|
|
165
165
|
dataOffset += modelNameBytes.length;
|
|
166
166
|
// Next IFD offset (0)
|
|
167
167
|
this.writeUint32LE(result, 0);
|
|
@@ -186,6 +186,6 @@ export class DNGFormat extends TIFFFormat {
|
|
|
186
186
|
for (let i = 0; i < modelNameBytes.length; i++) {
|
|
187
187
|
result.push(modelNameBytes[i]);
|
|
188
188
|
}
|
|
189
|
-
return new Uint8Array(result);
|
|
189
|
+
return Promise.resolve(new Uint8Array(result));
|
|
190
190
|
}
|
|
191
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
|
+
}
|
package/esm/src/formats/pcx.js
CHANGED
|
@@ -155,7 +155,7 @@ export class PCXFormat {
|
|
|
155
155
|
view.setUint16(66, width + (width % 2), true); // BytesPerLine (must be even)
|
|
156
156
|
view.setUint16(68, 1, true); // PaletteInfo (Color/BW)
|
|
157
157
|
const bytesPerLine = width + (width % 2);
|
|
158
|
-
const
|
|
158
|
+
const _scanlineLength = bytesPerLine * 3;
|
|
159
159
|
const rleData = [];
|
|
160
160
|
// Helper to write RLE
|
|
161
161
|
const writeRLE = (byte, count) => {
|
package/esm/src/formats/png.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { ImageData, ImageFormat } from "../types.js";
|
|
2
|
+
import { PNGBase } from "./png_base.js";
|
|
2
3
|
/**
|
|
3
4
|
* PNG format handler
|
|
4
5
|
* Implements a pure JavaScript PNG decoder and encoder
|
|
5
6
|
*/
|
|
6
|
-
export declare class PNGFormat implements ImageFormat {
|
|
7
|
+
export declare class PNGFormat extends PNGBase implements ImageFormat {
|
|
7
8
|
/** Format name identifier */
|
|
8
9
|
readonly name = "png";
|
|
9
10
|
/** MIME type for PNG images */
|
|
@@ -26,25 +27,5 @@ export declare class PNGFormat implements ImageFormat {
|
|
|
26
27
|
* @returns Encoded PNG image bytes
|
|
27
28
|
*/
|
|
28
29
|
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
29
|
-
private readUint32;
|
|
30
|
-
private writeUint32;
|
|
31
|
-
private concatenateChunks;
|
|
32
|
-
private inflate;
|
|
33
|
-
private deflate;
|
|
34
|
-
private unfilterAndConvert;
|
|
35
|
-
private unfilterScanline;
|
|
36
|
-
private paethPredictor;
|
|
37
|
-
private filterData;
|
|
38
|
-
private getBytesPerPixel;
|
|
39
|
-
private getBitsPerPixel;
|
|
40
|
-
private createChunk;
|
|
41
|
-
private crc32;
|
|
42
|
-
private parsePhysChunk;
|
|
43
|
-
private parseTextChunk;
|
|
44
|
-
private parseITxtChunk;
|
|
45
|
-
private parseExifChunk;
|
|
46
|
-
private createPhysChunk;
|
|
47
|
-
private createTextChunk;
|
|
48
|
-
private createExifChunk;
|
|
49
30
|
}
|
|
50
31
|
//# sourceMappingURL=png.d.ts.map
|