cross-image 0.2.0 → 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 +22 -16
- package/esm/mod.d.ts +3 -1
- package/esm/mod.js +3 -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/webp.d.ts +0 -1
- package/esm/src/formats/webp.js +4 -7
- package/esm/src/image.js +4 -0
- 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/package.json +1 -1
- package/script/mod.d.ts +3 -1
- package/script/mod.js +6 -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/webp.d.ts +0 -1
- package/script/src/formats/webp.js +4 -7
- package/script/src/image.js +4 -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
|
@@ -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;
|
|
@@ -158,7 +158,7 @@ class PCXFormat {
|
|
|
158
158
|
view.setUint16(66, width + (width % 2), true); // BytesPerLine (must be even)
|
|
159
159
|
view.setUint16(68, 1, true); // PaletteInfo (Color/BW)
|
|
160
160
|
const bytesPerLine = width + (width % 2);
|
|
161
|
-
const
|
|
161
|
+
const _scanlineLength = bytesPerLine * 3;
|
|
162
162
|
const rleData = [];
|
|
163
163
|
// Helper to write RLE
|
|
164
164
|
const writeRLE = (byte, count) => {
|
|
@@ -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
|