cross-image 0.2.3 → 0.4.0
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/LICENSE +21 -21
- package/README.md +292 -74
- package/esm/mod.d.ts +6 -4
- package/esm/mod.js +4 -2
- package/esm/src/formats/apng.d.ts +17 -5
- package/esm/src/formats/apng.js +104 -9
- package/esm/src/formats/ascii.d.ts +13 -3
- package/esm/src/formats/ascii.js +25 -1
- package/esm/src/formats/avif.d.ts +96 -0
- package/esm/src/formats/avif.js +607 -0
- package/esm/src/formats/bmp.d.ts +13 -3
- package/esm/src/formats/bmp.js +75 -2
- package/esm/src/formats/dng.d.ts +14 -2
- package/esm/src/formats/dng.js +27 -5
- package/esm/src/formats/gif.d.ts +18 -5
- package/esm/src/formats/gif.js +160 -14
- package/esm/src/formats/heic.d.ts +96 -0
- package/esm/src/formats/heic.js +608 -0
- package/esm/src/formats/ico.d.ts +13 -3
- package/esm/src/formats/ico.js +32 -4
- package/esm/src/formats/jpeg.d.ts +10 -3
- package/esm/src/formats/jpeg.js +99 -11
- package/esm/src/formats/pam.d.ts +13 -3
- package/esm/src/formats/pam.js +68 -2
- package/esm/src/formats/pcx.d.ts +13 -3
- package/esm/src/formats/pcx.js +47 -2
- package/esm/src/formats/png.d.ts +15 -3
- package/esm/src/formats/png.js +89 -2
- package/esm/src/formats/png_base.js +2 -5
- package/esm/src/formats/ppm.d.ts +13 -3
- package/esm/src/formats/ppm.js +36 -2
- package/esm/src/formats/tiff.d.ts +14 -18
- package/esm/src/formats/tiff.js +219 -20
- package/esm/src/formats/webp.d.ts +10 -3
- package/esm/src/formats/webp.js +103 -8
- package/esm/src/image.d.ts +20 -3
- package/esm/src/image.js +65 -21
- package/esm/src/types.d.ts +74 -4
- package/esm/src/utils/gif_decoder.d.ts +4 -1
- package/esm/src/utils/gif_decoder.js +91 -65
- package/esm/src/utils/image_processing.js +144 -70
- package/esm/src/utils/jpeg_decoder.d.ts +17 -4
- package/esm/src/utils/jpeg_decoder.js +448 -83
- package/esm/src/utils/jpeg_encoder.d.ts +15 -1
- package/esm/src/utils/jpeg_encoder.js +263 -24
- package/esm/src/utils/resize.js +51 -20
- package/esm/src/utils/tiff_deflate.d.ts +18 -0
- package/esm/src/utils/tiff_deflate.js +27 -0
- package/esm/src/utils/tiff_packbits.d.ts +24 -0
- package/esm/src/utils/tiff_packbits.js +90 -0
- package/esm/src/utils/webp_decoder.d.ts +3 -1
- package/esm/src/utils/webp_decoder.js +144 -63
- package/esm/src/utils/webp_encoder.js +5 -11
- package/package.json +18 -1
- package/script/mod.d.ts +6 -4
- package/script/mod.js +7 -3
- package/script/src/formats/apng.d.ts +17 -5
- package/script/src/formats/apng.js +104 -9
- package/script/src/formats/ascii.d.ts +13 -3
- package/script/src/formats/ascii.js +25 -1
- package/script/src/formats/avif.d.ts +96 -0
- package/script/src/formats/avif.js +611 -0
- package/script/src/formats/bmp.d.ts +13 -3
- package/script/src/formats/bmp.js +75 -2
- package/script/src/formats/dng.d.ts +14 -2
- package/script/src/formats/dng.js +27 -5
- package/script/src/formats/gif.d.ts +18 -5
- package/script/src/formats/gif.js +160 -14
- package/script/src/formats/heic.d.ts +96 -0
- package/script/src/formats/heic.js +612 -0
- package/script/src/formats/ico.d.ts +13 -3
- package/script/src/formats/ico.js +32 -4
- package/script/src/formats/jpeg.d.ts +10 -3
- package/script/src/formats/jpeg.js +99 -11
- package/script/src/formats/pam.d.ts +13 -3
- package/script/src/formats/pam.js +68 -2
- package/script/src/formats/pcx.d.ts +13 -3
- package/script/src/formats/pcx.js +47 -2
- package/script/src/formats/png.d.ts +15 -3
- package/script/src/formats/png.js +89 -2
- package/script/src/formats/png_base.js +2 -5
- package/script/src/formats/ppm.d.ts +13 -3
- package/script/src/formats/ppm.js +36 -2
- package/script/src/formats/tiff.d.ts +14 -18
- package/script/src/formats/tiff.js +219 -20
- package/script/src/formats/webp.d.ts +10 -3
- package/script/src/formats/webp.js +103 -8
- package/script/src/image.d.ts +20 -3
- package/script/src/image.js +64 -20
- package/script/src/types.d.ts +74 -4
- package/script/src/utils/gif_decoder.d.ts +4 -1
- package/script/src/utils/gif_decoder.js +91 -65
- package/script/src/utils/image_processing.js +144 -70
- package/script/src/utils/jpeg_decoder.d.ts +17 -4
- package/script/src/utils/jpeg_decoder.js +448 -83
- package/script/src/utils/jpeg_encoder.d.ts +15 -1
- package/script/src/utils/jpeg_encoder.js +263 -24
- package/script/src/utils/resize.js +51 -20
- package/script/src/utils/tiff_deflate.d.ts +18 -0
- package/script/src/utils/tiff_deflate.js +31 -0
- package/script/src/utils/tiff_packbits.d.ts +24 -0
- package/script/src/utils/tiff_packbits.js +94 -0
- package/script/src/utils/webp_decoder.d.ts +3 -1
- package/script/src/utils/webp_decoder.js +144 -63
- package/script/src/utils/webp_encoder.js +5 -11
|
@@ -52,7 +52,7 @@ class ICOFormat {
|
|
|
52
52
|
* @param data Raw ICO image data
|
|
53
53
|
* @returns Decoded image data with RGBA pixels
|
|
54
54
|
*/
|
|
55
|
-
async decode(data) {
|
|
55
|
+
async decode(data, options) {
|
|
56
56
|
if (!this.canDecode(data)) {
|
|
57
57
|
throw new Error("Invalid ICO signature");
|
|
58
58
|
}
|
|
@@ -103,7 +103,7 @@ class ICOFormat {
|
|
|
103
103
|
imageData[2] === 0x4e &&
|
|
104
104
|
imageData[3] === 0x47) {
|
|
105
105
|
// It's a PNG, decode it
|
|
106
|
-
return await this.pngFormat.decode(imageData);
|
|
106
|
+
return await this.pngFormat.decode(imageData, options);
|
|
107
107
|
}
|
|
108
108
|
// Otherwise, it's a BMP without the file header (DIB format)
|
|
109
109
|
return this.decodeDIB(imageData);
|
|
@@ -185,10 +185,10 @@ class ICOFormat {
|
|
|
185
185
|
* @param imageData Image data to encode
|
|
186
186
|
* @returns Encoded ICO image bytes
|
|
187
187
|
*/
|
|
188
|
-
async encode(imageData) {
|
|
188
|
+
async encode(imageData, _options) {
|
|
189
189
|
const { width, height } = imageData;
|
|
190
190
|
// Encode the image as PNG
|
|
191
|
-
const pngData = await this.pngFormat.encode(imageData);
|
|
191
|
+
const pngData = await this.pngFormat.encode(imageData, undefined);
|
|
192
192
|
// Create ICO file structure
|
|
193
193
|
// ICONDIR (6 bytes) + ICONDIRENTRY (16 bytes) + PNG data
|
|
194
194
|
const icoSize = 6 + 16 + pngData.length;
|
|
@@ -214,5 +214,33 @@ class ICOFormat {
|
|
|
214
214
|
result.set(pngData, 22);
|
|
215
215
|
return result;
|
|
216
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Get the list of metadata fields supported by ICO format
|
|
219
|
+
*/
|
|
220
|
+
getSupportedMetadata() {
|
|
221
|
+
return [
|
|
222
|
+
"frameCount", // Number of images in ICO file
|
|
223
|
+
];
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Extract metadata from ICO data without fully decoding the pixel data
|
|
227
|
+
* @param data Raw ICO data
|
|
228
|
+
* @returns Extracted metadata or undefined
|
|
229
|
+
*/
|
|
230
|
+
extractMetadata(data) {
|
|
231
|
+
if (!this.canDecode(data)) {
|
|
232
|
+
return Promise.resolve(undefined);
|
|
233
|
+
}
|
|
234
|
+
// ICO files can contain multiple images, count them
|
|
235
|
+
const count = (0, byte_utils_js_1.readUint16LE)(data, 4);
|
|
236
|
+
const metadata = {
|
|
237
|
+
format: "ico",
|
|
238
|
+
compression: "none", // ICO typically contains uncompressed PNG or BMP
|
|
239
|
+
frameCount: count,
|
|
240
|
+
bitDepth: 32, // Most modern ICOs use 32-bit RGBA
|
|
241
|
+
colorType: "rgba",
|
|
242
|
+
};
|
|
243
|
+
return Promise.resolve(metadata);
|
|
244
|
+
}
|
|
217
245
|
}
|
|
218
246
|
exports.ICOFormat = ICOFormat;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat, ImageMetadata } from "../types.js";
|
|
1
|
+
import type { ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata, JPEGEncoderOptions } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* JPEG format handler
|
|
4
4
|
* Implements a basic JPEG decoder and encoder
|
|
@@ -19,13 +19,13 @@ export declare class JPEGFormat implements ImageFormat {
|
|
|
19
19
|
* @param data Raw JPEG image data
|
|
20
20
|
* @returns Decoded image data with RGBA pixels
|
|
21
21
|
*/
|
|
22
|
-
decode(data: Uint8Array): Promise<ImageData>;
|
|
22
|
+
decode(data: Uint8Array, settings?: ImageDecoderOptions): Promise<ImageData>;
|
|
23
23
|
/**
|
|
24
24
|
* Encode RGBA image data to JPEG format
|
|
25
25
|
* @param imageData Image data to encode
|
|
26
26
|
* @returns Encoded JPEG image bytes
|
|
27
27
|
*/
|
|
28
|
-
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
28
|
+
encode(imageData: ImageData, options?: JPEGEncoderOptions): Promise<Uint8Array>;
|
|
29
29
|
private injectMetadata;
|
|
30
30
|
private decodeUsingRuntime;
|
|
31
31
|
private parseJFIF;
|
|
@@ -42,5 +42,12 @@ export declare class JPEGFormat implements ImageFormat {
|
|
|
42
42
|
* Get the list of metadata fields supported by JPEG format
|
|
43
43
|
*/
|
|
44
44
|
getSupportedMetadata(): Array<keyof ImageMetadata>;
|
|
45
|
+
/**
|
|
46
|
+
* Extract metadata from JPEG data without fully decoding the pixel data
|
|
47
|
+
* This quickly parses JFIF and EXIF markers to extract metadata
|
|
48
|
+
* @param data Raw JPEG data
|
|
49
|
+
* @returns Extracted metadata or undefined
|
|
50
|
+
*/
|
|
51
|
+
extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
|
|
45
52
|
}
|
|
46
53
|
//# sourceMappingURL=jpeg.d.ts.map
|
|
@@ -73,7 +73,7 @@ class JPEGFormat {
|
|
|
73
73
|
* @param data Raw JPEG image data
|
|
74
74
|
* @returns Decoded image data with RGBA pixels
|
|
75
75
|
*/
|
|
76
|
-
async decode(data) {
|
|
76
|
+
async decode(data, settings) {
|
|
77
77
|
if (!this.canDecode(data)) {
|
|
78
78
|
throw new Error("Invalid JPEG signature");
|
|
79
79
|
}
|
|
@@ -131,7 +131,7 @@ class JPEGFormat {
|
|
|
131
131
|
(0, security_js_1.validateImageDimensions)(width, height);
|
|
132
132
|
// For a pure JS implementation, we'd need to implement full JPEG decoding
|
|
133
133
|
// which is very complex. Instead, we'll use the browser/runtime's decoder.
|
|
134
|
-
const rgba = await this.decodeUsingRuntime(data, width, height);
|
|
134
|
+
const rgba = await this.decodeUsingRuntime(data, width, height, settings);
|
|
135
135
|
return {
|
|
136
136
|
width,
|
|
137
137
|
height,
|
|
@@ -144,10 +144,13 @@ class JPEGFormat {
|
|
|
144
144
|
* @param imageData Image data to encode
|
|
145
145
|
* @returns Encoded JPEG image bytes
|
|
146
146
|
*/
|
|
147
|
-
async encode(imageData) {
|
|
147
|
+
async encode(imageData, options) {
|
|
148
148
|
const { width, height, data, metadata } = imageData;
|
|
149
|
+
const requestedQuality = options?.quality;
|
|
150
|
+
const requestedProgressive = options?.progressive;
|
|
149
151
|
// Try to use runtime encoding if available (better quality)
|
|
150
|
-
|
|
152
|
+
// Note: progressive output is only supported by the pure-JS encoder.
|
|
153
|
+
if (!requestedProgressive && typeof OffscreenCanvas !== "undefined") {
|
|
151
154
|
try {
|
|
152
155
|
const canvas = new OffscreenCanvas(width, height);
|
|
153
156
|
const ctx = canvas.getContext("2d");
|
|
@@ -156,9 +159,12 @@ class JPEGFormat {
|
|
|
156
159
|
const imgDataData = new Uint8ClampedArray(data);
|
|
157
160
|
imgData.data.set(imgDataData);
|
|
158
161
|
ctx.putImageData(imgData, 0, 0);
|
|
162
|
+
const quality = requestedQuality === undefined
|
|
163
|
+
? 0.9
|
|
164
|
+
: Math.max(1, Math.min(100, requestedQuality)) / 100;
|
|
159
165
|
const blob = await canvas.convertToBlob({
|
|
160
166
|
type: "image/jpeg",
|
|
161
|
-
quality
|
|
167
|
+
quality,
|
|
162
168
|
});
|
|
163
169
|
const arrayBuffer = await blob.arrayBuffer();
|
|
164
170
|
const encoded = new Uint8Array(arrayBuffer);
|
|
@@ -177,7 +183,10 @@ class JPEGFormat {
|
|
|
177
183
|
const { JPEGEncoder } = await Promise.resolve().then(() => __importStar(require("../utils/jpeg_encoder.js")));
|
|
178
184
|
const dpiX = metadata?.dpiX ?? 72;
|
|
179
185
|
const dpiY = metadata?.dpiY ?? 72;
|
|
180
|
-
const encoder = new JPEGEncoder(
|
|
186
|
+
const encoder = new JPEGEncoder({
|
|
187
|
+
quality: requestedQuality,
|
|
188
|
+
progressive: requestedProgressive,
|
|
189
|
+
});
|
|
181
190
|
const encoded = encoder.encode(width, height, data, dpiX, dpiY);
|
|
182
191
|
// Add EXIF metadata if present
|
|
183
192
|
if (metadata && Object.keys(metadata).length > 0) {
|
|
@@ -221,9 +230,10 @@ class JPEGFormat {
|
|
|
221
230
|
result.set(encoded.slice(pos), pos + app1.length);
|
|
222
231
|
return result;
|
|
223
232
|
}
|
|
224
|
-
async decodeUsingRuntime(data, _width, _height) {
|
|
233
|
+
async decodeUsingRuntime(data, _width, _height, settings) {
|
|
225
234
|
// Try to use ImageDecoder API if available (Deno, modern browsers)
|
|
226
|
-
if (
|
|
235
|
+
if (settings?.runtimeDecoding !== "never" &&
|
|
236
|
+
typeof ImageDecoder !== "undefined") {
|
|
227
237
|
try {
|
|
228
238
|
const decoder = new ImageDecoder({ data, type: "image/jpeg" });
|
|
229
239
|
const result = await decoder.decode();
|
|
@@ -238,15 +248,17 @@ class JPEGFormat {
|
|
|
238
248
|
bitmap.close();
|
|
239
249
|
return new Uint8Array(imageData.data.buffer);
|
|
240
250
|
}
|
|
241
|
-
catch (
|
|
251
|
+
catch (_error) {
|
|
242
252
|
// ImageDecoder API failed, fall through to pure JS decoder
|
|
243
|
-
console.warn("JPEG decoding with ImageDecoder failed, using pure JS decoder:", error);
|
|
244
253
|
}
|
|
245
254
|
}
|
|
246
255
|
// Fallback to pure JavaScript decoder
|
|
247
256
|
try {
|
|
248
257
|
const { JPEGDecoder } = await Promise.resolve().then(() => __importStar(require("../utils/jpeg_decoder.js")));
|
|
249
|
-
const decoder = new JPEGDecoder(data
|
|
258
|
+
const decoder = new JPEGDecoder(data, {
|
|
259
|
+
tolerantDecoding: settings?.tolerantDecoding ?? true,
|
|
260
|
+
onWarning: settings?.onWarning,
|
|
261
|
+
});
|
|
250
262
|
return decoder.decode();
|
|
251
263
|
}
|
|
252
264
|
catch (error) {
|
|
@@ -1096,5 +1108,81 @@ class JPEGFormat {
|
|
|
1096
1108
|
"dpiY",
|
|
1097
1109
|
];
|
|
1098
1110
|
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Extract metadata from JPEG data without fully decoding the pixel data
|
|
1113
|
+
* This quickly parses JFIF and EXIF markers to extract metadata
|
|
1114
|
+
* @param data Raw JPEG data
|
|
1115
|
+
* @returns Extracted metadata or undefined
|
|
1116
|
+
*/
|
|
1117
|
+
extractMetadata(data) {
|
|
1118
|
+
if (!this.canDecode(data)) {
|
|
1119
|
+
return Promise.resolve(undefined);
|
|
1120
|
+
}
|
|
1121
|
+
// Parse JPEG structure to extract metadata
|
|
1122
|
+
let pos = 2; // Skip initial FF D8
|
|
1123
|
+
const metadata = {
|
|
1124
|
+
format: "jpeg",
|
|
1125
|
+
compression: "dct",
|
|
1126
|
+
frameCount: 1,
|
|
1127
|
+
bitDepth: 8,
|
|
1128
|
+
colorType: "rgb",
|
|
1129
|
+
};
|
|
1130
|
+
let width = 0;
|
|
1131
|
+
let height = 0;
|
|
1132
|
+
while (pos < data.length - 1) {
|
|
1133
|
+
if (data[pos] !== 0xff) {
|
|
1134
|
+
pos++;
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
const marker = data[pos + 1];
|
|
1138
|
+
pos += 2;
|
|
1139
|
+
// SOF markers (Start of Frame) - get dimensions for DPI calculation
|
|
1140
|
+
if (marker >= 0xc0 && marker <= 0xcf &&
|
|
1141
|
+
marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
1142
|
+
const length = (data[pos] << 8) | data[pos + 1];
|
|
1143
|
+
// precision at pos+2
|
|
1144
|
+
const precision = data[pos + 2];
|
|
1145
|
+
if (precision && precision !== 8) {
|
|
1146
|
+
metadata.bitDepth = precision;
|
|
1147
|
+
}
|
|
1148
|
+
height = (data[pos + 3] << 8) | data[pos + 4];
|
|
1149
|
+
width = (data[pos + 5] << 8) | data[pos + 6];
|
|
1150
|
+
// Check number of components
|
|
1151
|
+
const numComponents = data[pos + 7];
|
|
1152
|
+
if (numComponents === 1) {
|
|
1153
|
+
metadata.colorType = "grayscale";
|
|
1154
|
+
}
|
|
1155
|
+
// Don't break - continue parsing for metadata
|
|
1156
|
+
pos += length;
|
|
1157
|
+
continue;
|
|
1158
|
+
}
|
|
1159
|
+
// APP0 marker (JFIF)
|
|
1160
|
+
if (marker === 0xe0) {
|
|
1161
|
+
const length = (data[pos] << 8) | data[pos + 1];
|
|
1162
|
+
const appData = data.slice(pos + 2, pos + length);
|
|
1163
|
+
this.parseJFIF(appData, metadata, width, height);
|
|
1164
|
+
pos += length;
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
// APP1 marker (EXIF)
|
|
1168
|
+
if (marker === 0xe1) {
|
|
1169
|
+
const length = (data[pos] << 8) | data[pos + 1];
|
|
1170
|
+
const appData = data.slice(pos + 2, pos + length);
|
|
1171
|
+
this.parseEXIF(appData, metadata);
|
|
1172
|
+
pos += length;
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
// Skip other markers
|
|
1176
|
+
if (marker === 0xd9 || marker === 0xda)
|
|
1177
|
+
break; // EOI or SOS
|
|
1178
|
+
if (marker >= 0xd0 && marker <= 0xd8)
|
|
1179
|
+
continue; // RST markers have no length
|
|
1180
|
+
if (marker === 0x01)
|
|
1181
|
+
continue; // TEM has no length
|
|
1182
|
+
const length = (data[pos] << 8) | data[pos + 1];
|
|
1183
|
+
pos += length;
|
|
1184
|
+
}
|
|
1185
|
+
return Promise.resolve(Object.keys(metadata).length > 0 ? metadata : undefined);
|
|
1186
|
+
}
|
|
1099
1187
|
}
|
|
1100
1188
|
exports.JPEGFormat = JPEGFormat;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat } from "../types.js";
|
|
1
|
+
import type { ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* PAM format handler
|
|
4
4
|
* Implements the Netpbm PAM (Portable Arbitrary Map) format.
|
|
@@ -32,12 +32,22 @@ export declare class PAMFormat implements ImageFormat {
|
|
|
32
32
|
* @param data Raw PAM image data
|
|
33
33
|
* @returns Decoded image data with RGBA pixels
|
|
34
34
|
*/
|
|
35
|
-
decode(data: Uint8Array): Promise<ImageData>;
|
|
35
|
+
decode(data: Uint8Array, _options?: ImageDecoderOptions): Promise<ImageData>;
|
|
36
36
|
/**
|
|
37
37
|
* Encode RGBA image data to PAM format
|
|
38
38
|
* @param imageData Image data to encode
|
|
39
39
|
* @returns Encoded PAM image bytes
|
|
40
40
|
*/
|
|
41
|
-
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
41
|
+
encode(imageData: ImageData, _options?: unknown): Promise<Uint8Array>;
|
|
42
|
+
/**
|
|
43
|
+
* Get the list of metadata fields supported by PAM format
|
|
44
|
+
*/
|
|
45
|
+
getSupportedMetadata(): Array<keyof ImageMetadata>;
|
|
46
|
+
/**
|
|
47
|
+
* Extract metadata from PAM data without fully decoding the pixel data
|
|
48
|
+
* @param data Raw PAM data
|
|
49
|
+
* @returns Extracted metadata or undefined
|
|
50
|
+
*/
|
|
51
|
+
extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
|
|
42
52
|
}
|
|
43
53
|
//# sourceMappingURL=pam.d.ts.map
|
|
@@ -56,7 +56,7 @@ class PAMFormat {
|
|
|
56
56
|
* @param data Raw PAM image data
|
|
57
57
|
* @returns Decoded image data with RGBA pixels
|
|
58
58
|
*/
|
|
59
|
-
decode(data) {
|
|
59
|
+
decode(data, _options) {
|
|
60
60
|
if (!this.canDecode(data)) {
|
|
61
61
|
throw new Error("Invalid PAM signature");
|
|
62
62
|
}
|
|
@@ -153,7 +153,7 @@ class PAMFormat {
|
|
|
153
153
|
* @param imageData Image data to encode
|
|
154
154
|
* @returns Encoded PAM image bytes
|
|
155
155
|
*/
|
|
156
|
-
encode(imageData) {
|
|
156
|
+
encode(imageData, _options) {
|
|
157
157
|
const { width, height, data } = imageData;
|
|
158
158
|
// Validate input
|
|
159
159
|
if (data.length !== width * height * 4) {
|
|
@@ -177,5 +177,71 @@ class PAMFormat {
|
|
|
177
177
|
output.set(data, headerBytes.length);
|
|
178
178
|
return Promise.resolve(output);
|
|
179
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Get the list of metadata fields supported by PAM format
|
|
182
|
+
*/
|
|
183
|
+
getSupportedMetadata() {
|
|
184
|
+
return []; // PAM format doesn't support metadata preservation
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Extract metadata from PAM data without fully decoding the pixel data
|
|
188
|
+
* @param data Raw PAM data
|
|
189
|
+
* @returns Extracted metadata or undefined
|
|
190
|
+
*/
|
|
191
|
+
extractMetadata(data) {
|
|
192
|
+
if (!this.canDecode(data)) {
|
|
193
|
+
return Promise.resolve(undefined);
|
|
194
|
+
}
|
|
195
|
+
const metadata = {
|
|
196
|
+
format: "pam",
|
|
197
|
+
compression: "none",
|
|
198
|
+
frameCount: 1,
|
|
199
|
+
bitDepth: 8, // PAM typically uses 8 bits per channel
|
|
200
|
+
colorType: "rgba",
|
|
201
|
+
};
|
|
202
|
+
// Try to parse the header to get actual color type
|
|
203
|
+
const decoder = new TextDecoder();
|
|
204
|
+
const headerText = decoder.decode(data.slice(0, Math.min(200, data.length)));
|
|
205
|
+
// Look for TUPLTYPE to determine color type
|
|
206
|
+
const tupltypeMatch = headerText.match(/TUPLTYPE\s+(\S+)/);
|
|
207
|
+
if (tupltypeMatch) {
|
|
208
|
+
const tupltype = tupltypeMatch[1];
|
|
209
|
+
if (tupltype === "GRAYSCALE") {
|
|
210
|
+
metadata.colorType = "grayscale";
|
|
211
|
+
}
|
|
212
|
+
else if (tupltype === "RGB") {
|
|
213
|
+
metadata.colorType = "rgb";
|
|
214
|
+
}
|
|
215
|
+
else if (tupltype === "RGB_ALPHA") {
|
|
216
|
+
metadata.colorType = "rgba";
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Look for DEPTH to determine number of channels
|
|
220
|
+
const depthMatch = headerText.match(/DEPTH\s+(\d+)/);
|
|
221
|
+
if (depthMatch) {
|
|
222
|
+
const depth = parseInt(depthMatch[1]);
|
|
223
|
+
if (depth === 1) {
|
|
224
|
+
metadata.colorType = "grayscale";
|
|
225
|
+
}
|
|
226
|
+
else if (depth === 3) {
|
|
227
|
+
metadata.colorType = "rgb";
|
|
228
|
+
}
|
|
229
|
+
else if (depth === 4) {
|
|
230
|
+
metadata.colorType = "rgba";
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Look for MAXVAL to determine bit depth
|
|
234
|
+
const maxvalMatch = headerText.match(/MAXVAL\s+(\d+)/);
|
|
235
|
+
if (maxvalMatch) {
|
|
236
|
+
const maxval = parseInt(maxvalMatch[1]);
|
|
237
|
+
if (maxval === 255) {
|
|
238
|
+
metadata.bitDepth = 8;
|
|
239
|
+
}
|
|
240
|
+
else if (maxval === 65535) {
|
|
241
|
+
metadata.bitDepth = 16;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return Promise.resolve(metadata);
|
|
245
|
+
}
|
|
180
246
|
}
|
|
181
247
|
exports.PAMFormat = PAMFormat;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat } from "../types.js";
|
|
1
|
+
import type { ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* PCX format handler
|
|
4
4
|
* Implements PCX decoder and encoder
|
|
@@ -7,7 +7,17 @@ export declare class PCXFormat implements ImageFormat {
|
|
|
7
7
|
readonly name = "pcx";
|
|
8
8
|
readonly mimeType = "image/x-pcx";
|
|
9
9
|
canDecode(data: Uint8Array): boolean;
|
|
10
|
-
decode(data: Uint8Array): Promise<ImageData>;
|
|
11
|
-
encode(image: ImageData): Promise<Uint8Array>;
|
|
10
|
+
decode(data: Uint8Array, _options?: ImageDecoderOptions): Promise<ImageData>;
|
|
11
|
+
encode(image: ImageData, _options?: unknown): Promise<Uint8Array>;
|
|
12
|
+
/**
|
|
13
|
+
* Get the list of metadata fields supported by PCX format
|
|
14
|
+
*/
|
|
15
|
+
getSupportedMetadata(): Array<keyof ImageMetadata>;
|
|
16
|
+
/**
|
|
17
|
+
* Extract metadata from PCX data without fully decoding the pixel data
|
|
18
|
+
* @param data Raw PCX data
|
|
19
|
+
* @returns Extracted metadata or undefined
|
|
20
|
+
*/
|
|
21
|
+
extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
|
|
12
22
|
}
|
|
13
23
|
//# sourceMappingURL=pcx.d.ts.map
|
|
@@ -31,7 +31,7 @@ class PCXFormat {
|
|
|
31
31
|
data[1] === 5) &&
|
|
32
32
|
data[2] === 1;
|
|
33
33
|
}
|
|
34
|
-
decode(data) {
|
|
34
|
+
decode(data, _options) {
|
|
35
35
|
if (!this.canDecode(data)) {
|
|
36
36
|
return Promise.reject(new Error("Invalid PCX data"));
|
|
37
37
|
}
|
|
@@ -137,7 +137,7 @@ class PCXFormat {
|
|
|
137
137
|
data: rgba,
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
|
-
encode(image) {
|
|
140
|
+
encode(image, _options) {
|
|
141
141
|
const width = image.width;
|
|
142
142
|
const height = image.height;
|
|
143
143
|
const data = image.data;
|
|
@@ -204,5 +204,50 @@ class PCXFormat {
|
|
|
204
204
|
result.set(rleData, header.length);
|
|
205
205
|
return Promise.resolve(result);
|
|
206
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Get the list of metadata fields supported by PCX format
|
|
209
|
+
*/
|
|
210
|
+
getSupportedMetadata() {
|
|
211
|
+
return [
|
|
212
|
+
"dpiX", // DPI in header
|
|
213
|
+
"dpiY", // DPI in header
|
|
214
|
+
];
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Extract metadata from PCX data without fully decoding the pixel data
|
|
218
|
+
* @param data Raw PCX data
|
|
219
|
+
* @returns Extracted metadata or undefined
|
|
220
|
+
*/
|
|
221
|
+
extractMetadata(data) {
|
|
222
|
+
if (!this.canDecode(data)) {
|
|
223
|
+
return Promise.resolve(undefined);
|
|
224
|
+
}
|
|
225
|
+
const metadata = {
|
|
226
|
+
format: "pcx",
|
|
227
|
+
compression: "rle",
|
|
228
|
+
frameCount: 1,
|
|
229
|
+
bitDepth: data[3], // Bits per pixel per plane
|
|
230
|
+
colorType: "rgb",
|
|
231
|
+
};
|
|
232
|
+
// Check number of planes to determine color type
|
|
233
|
+
const numPlanes = data[65];
|
|
234
|
+
if (numPlanes === 1) {
|
|
235
|
+
metadata.colorType = "indexed";
|
|
236
|
+
}
|
|
237
|
+
else if (numPlanes === 3) {
|
|
238
|
+
metadata.colorType = "rgb";
|
|
239
|
+
}
|
|
240
|
+
else if (numPlanes === 4) {
|
|
241
|
+
metadata.colorType = "rgba";
|
|
242
|
+
}
|
|
243
|
+
// DPI information
|
|
244
|
+
const dpiX = data[12] | (data[13] << 8);
|
|
245
|
+
const dpiY = data[14] | (data[15] << 8);
|
|
246
|
+
if (dpiX > 0)
|
|
247
|
+
metadata.dpiX = dpiX;
|
|
248
|
+
if (dpiY > 0)
|
|
249
|
+
metadata.dpiY = dpiY;
|
|
250
|
+
return Promise.resolve(metadata);
|
|
251
|
+
}
|
|
207
252
|
}
|
|
208
253
|
exports.PCXFormat = PCXFormat;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat } from "../types.js";
|
|
1
|
+
import type { ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata } from "../types.js";
|
|
2
2
|
import { PNGBase } from "./png_base.js";
|
|
3
3
|
/**
|
|
4
4
|
* PNG format handler
|
|
@@ -20,12 +20,24 @@ export declare class PNGFormat extends PNGBase implements ImageFormat {
|
|
|
20
20
|
* @param data Raw PNG image data
|
|
21
21
|
* @returns Decoded image data with RGBA pixels
|
|
22
22
|
*/
|
|
23
|
-
decode(data: Uint8Array): Promise<ImageData>;
|
|
23
|
+
decode(data: Uint8Array, _options?: ImageDecoderOptions): Promise<ImageData>;
|
|
24
24
|
/**
|
|
25
25
|
* Encode RGBA image data to PNG format
|
|
26
26
|
* @param imageData Image data to encode
|
|
27
27
|
* @returns Encoded PNG image bytes
|
|
28
28
|
*/
|
|
29
|
-
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
29
|
+
encode(imageData: ImageData, _options?: unknown): Promise<Uint8Array>;
|
|
30
|
+
/**
|
|
31
|
+
* Get the list of metadata fields supported by PNG format
|
|
32
|
+
* Delegates to PNGBase implementation
|
|
33
|
+
*/
|
|
34
|
+
getSupportedMetadata(): Array<keyof ImageMetadata>;
|
|
35
|
+
/**
|
|
36
|
+
* Extract metadata from PNG data without fully decoding the pixel data
|
|
37
|
+
* This quickly parses PNG chunks to extract metadata
|
|
38
|
+
* @param data Raw PNG data
|
|
39
|
+
* @returns Extracted metadata or undefined
|
|
40
|
+
*/
|
|
41
|
+
extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
|
|
30
42
|
}
|
|
31
43
|
//# sourceMappingURL=png.d.ts.map
|
|
@@ -43,7 +43,7 @@ class PNGFormat extends png_base_js_1.PNGBase {
|
|
|
43
43
|
* @param data Raw PNG image data
|
|
44
44
|
* @returns Decoded image data with RGBA pixels
|
|
45
45
|
*/
|
|
46
|
-
async decode(data) {
|
|
46
|
+
async decode(data, _options) {
|
|
47
47
|
if (!this.canDecode(data)) {
|
|
48
48
|
throw new Error("Invalid PNG signature");
|
|
49
49
|
}
|
|
@@ -115,7 +115,7 @@ class PNGFormat extends png_base_js_1.PNGBase {
|
|
|
115
115
|
* @param imageData Image data to encode
|
|
116
116
|
* @returns Encoded PNG image bytes
|
|
117
117
|
*/
|
|
118
|
-
async encode(imageData) {
|
|
118
|
+
async encode(imageData, _options) {
|
|
119
119
|
const { width, height, data, metadata } = imageData;
|
|
120
120
|
// Prepare IHDR chunk
|
|
121
121
|
const ihdr = new Uint8Array(13);
|
|
@@ -140,5 +140,92 @@ class PNGFormat extends png_base_js_1.PNGBase {
|
|
|
140
140
|
// Concatenate all chunks
|
|
141
141
|
return this.concatenateArrays(chunks);
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Get the list of metadata fields supported by PNG format
|
|
145
|
+
* Delegates to PNGBase implementation
|
|
146
|
+
*/
|
|
147
|
+
getSupportedMetadata() {
|
|
148
|
+
return super.getSupportedMetadata();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Extract metadata from PNG data without fully decoding the pixel data
|
|
152
|
+
* This quickly parses PNG chunks to extract metadata
|
|
153
|
+
* @param data Raw PNG data
|
|
154
|
+
* @returns Extracted metadata or undefined
|
|
155
|
+
*/
|
|
156
|
+
extractMetadata(data) {
|
|
157
|
+
if (!this.canDecode(data)) {
|
|
158
|
+
return Promise.resolve(undefined);
|
|
159
|
+
}
|
|
160
|
+
let pos = 8; // Skip PNG signature
|
|
161
|
+
let width = 0;
|
|
162
|
+
let height = 0;
|
|
163
|
+
const metadata = {
|
|
164
|
+
format: "png",
|
|
165
|
+
compression: "deflate",
|
|
166
|
+
frameCount: 1,
|
|
167
|
+
};
|
|
168
|
+
// Parse chunks for metadata only
|
|
169
|
+
while (pos < data.length) {
|
|
170
|
+
if (pos + 8 > data.length)
|
|
171
|
+
break;
|
|
172
|
+
const length = this.readUint32(data, pos);
|
|
173
|
+
pos += 4;
|
|
174
|
+
const type = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
|
|
175
|
+
pos += 4;
|
|
176
|
+
if (pos + length + 4 > data.length)
|
|
177
|
+
break;
|
|
178
|
+
const chunkData = data.slice(pos, pos + length);
|
|
179
|
+
pos += length;
|
|
180
|
+
pos += 4; // Skip CRC
|
|
181
|
+
if (type === "IHDR") {
|
|
182
|
+
width = this.readUint32(chunkData, 0);
|
|
183
|
+
height = this.readUint32(chunkData, 4);
|
|
184
|
+
// Parse bit depth and color type from IHDR
|
|
185
|
+
if (chunkData.length >= 9) {
|
|
186
|
+
metadata.bitDepth = chunkData[8];
|
|
187
|
+
const colorTypeCode = chunkData[9];
|
|
188
|
+
// PNG color types: 0=grayscale, 2=rgb, 3=indexed, 4=grayscale+alpha, 6=rgba
|
|
189
|
+
switch (colorTypeCode) {
|
|
190
|
+
case 0:
|
|
191
|
+
metadata.colorType = "grayscale";
|
|
192
|
+
break;
|
|
193
|
+
case 2:
|
|
194
|
+
metadata.colorType = "rgb";
|
|
195
|
+
break;
|
|
196
|
+
case 3:
|
|
197
|
+
metadata.colorType = "indexed";
|
|
198
|
+
break;
|
|
199
|
+
case 4:
|
|
200
|
+
metadata.colorType = "grayscale-alpha";
|
|
201
|
+
break;
|
|
202
|
+
case 6:
|
|
203
|
+
metadata.colorType = "rgba";
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else if (type === "pHYs") {
|
|
209
|
+
// Physical pixel dimensions
|
|
210
|
+
this.parsePhysChunk(chunkData, metadata, width, height);
|
|
211
|
+
}
|
|
212
|
+
else if (type === "tEXt") {
|
|
213
|
+
// Text chunk
|
|
214
|
+
this.parseTextChunk(chunkData, metadata);
|
|
215
|
+
}
|
|
216
|
+
else if (type === "iTXt") {
|
|
217
|
+
// International text chunk
|
|
218
|
+
this.parseITxtChunk(chunkData, metadata);
|
|
219
|
+
}
|
|
220
|
+
else if (type === "eXIf") {
|
|
221
|
+
// EXIF chunk
|
|
222
|
+
this.parseExifChunk(chunkData, metadata);
|
|
223
|
+
}
|
|
224
|
+
else if (type === "IEND") {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return Promise.resolve(Object.keys(metadata).length > 0 ? metadata : undefined);
|
|
229
|
+
}
|
|
143
230
|
}
|
|
144
231
|
exports.PNGFormat = PNGFormat;
|
|
@@ -108,9 +108,7 @@ class PNGBase {
|
|
|
108
108
|
for (let x = 0; x < scanline.length; x++) {
|
|
109
109
|
const left = x >= bytesPerPixel ? scanline[x - bytesPerPixel] : 0;
|
|
110
110
|
const above = prevLine ? prevLine[x] : 0;
|
|
111
|
-
const upperLeft = (x >= bytesPerPixel && prevLine)
|
|
112
|
-
? prevLine[x - bytesPerPixel]
|
|
113
|
-
: 0;
|
|
111
|
+
const upperLeft = (x >= bytesPerPixel && prevLine) ? prevLine[x - bytesPerPixel] : 0;
|
|
114
112
|
switch (filterType) {
|
|
115
113
|
case 0: // None
|
|
116
114
|
break;
|
|
@@ -124,8 +122,7 @@ class PNGBase {
|
|
|
124
122
|
scanline[x] = (scanline[x] + Math.floor((left + above) / 2)) & 0xff;
|
|
125
123
|
break;
|
|
126
124
|
case 4: // Paeth
|
|
127
|
-
scanline[x] =
|
|
128
|
-
(scanline[x] + this.paethPredictor(left, above, upperLeft)) & 0xff;
|
|
125
|
+
scanline[x] = (scanline[x] + this.paethPredictor(left, above, upperLeft)) & 0xff;
|
|
129
126
|
break;
|
|
130
127
|
}
|
|
131
128
|
}
|