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
package/esm/src/formats/ico.js
CHANGED
|
@@ -49,7 +49,7 @@ export class ICOFormat {
|
|
|
49
49
|
* @param data Raw ICO image data
|
|
50
50
|
* @returns Decoded image data with RGBA pixels
|
|
51
51
|
*/
|
|
52
|
-
async decode(data) {
|
|
52
|
+
async decode(data, options) {
|
|
53
53
|
if (!this.canDecode(data)) {
|
|
54
54
|
throw new Error("Invalid ICO signature");
|
|
55
55
|
}
|
|
@@ -100,7 +100,7 @@ export class ICOFormat {
|
|
|
100
100
|
imageData[2] === 0x4e &&
|
|
101
101
|
imageData[3] === 0x47) {
|
|
102
102
|
// It's a PNG, decode it
|
|
103
|
-
return await this.pngFormat.decode(imageData);
|
|
103
|
+
return await this.pngFormat.decode(imageData, options);
|
|
104
104
|
}
|
|
105
105
|
// Otherwise, it's a BMP without the file header (DIB format)
|
|
106
106
|
return this.decodeDIB(imageData);
|
|
@@ -182,10 +182,10 @@ export class ICOFormat {
|
|
|
182
182
|
* @param imageData Image data to encode
|
|
183
183
|
* @returns Encoded ICO image bytes
|
|
184
184
|
*/
|
|
185
|
-
async encode(imageData) {
|
|
185
|
+
async encode(imageData, _options) {
|
|
186
186
|
const { width, height } = imageData;
|
|
187
187
|
// Encode the image as PNG
|
|
188
|
-
const pngData = await this.pngFormat.encode(imageData);
|
|
188
|
+
const pngData = await this.pngFormat.encode(imageData, undefined);
|
|
189
189
|
// Create ICO file structure
|
|
190
190
|
// ICONDIR (6 bytes) + ICONDIRENTRY (16 bytes) + PNG data
|
|
191
191
|
const icoSize = 6 + 16 + pngData.length;
|
|
@@ -211,4 +211,32 @@ export class ICOFormat {
|
|
|
211
211
|
result.set(pngData, 22);
|
|
212
212
|
return result;
|
|
213
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Get the list of metadata fields supported by ICO format
|
|
216
|
+
*/
|
|
217
|
+
getSupportedMetadata() {
|
|
218
|
+
return [
|
|
219
|
+
"frameCount", // Number of images in ICO file
|
|
220
|
+
];
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Extract metadata from ICO data without fully decoding the pixel data
|
|
224
|
+
* @param data Raw ICO data
|
|
225
|
+
* @returns Extracted metadata or undefined
|
|
226
|
+
*/
|
|
227
|
+
extractMetadata(data) {
|
|
228
|
+
if (!this.canDecode(data)) {
|
|
229
|
+
return Promise.resolve(undefined);
|
|
230
|
+
}
|
|
231
|
+
// ICO files can contain multiple images, count them
|
|
232
|
+
const count = readUint16LE(data, 4);
|
|
233
|
+
const metadata = {
|
|
234
|
+
format: "ico",
|
|
235
|
+
compression: "none", // ICO typically contains uncompressed PNG or BMP
|
|
236
|
+
frameCount: count,
|
|
237
|
+
bitDepth: 32, // Most modern ICOs use 32-bit RGBA
|
|
238
|
+
colorType: "rgba",
|
|
239
|
+
};
|
|
240
|
+
return Promise.resolve(metadata);
|
|
241
|
+
}
|
|
214
242
|
}
|
|
@@ -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
|
package/esm/src/formats/jpeg.js
CHANGED
|
@@ -37,7 +37,7 @@ export class JPEGFormat {
|
|
|
37
37
|
* @param data Raw JPEG image data
|
|
38
38
|
* @returns Decoded image data with RGBA pixels
|
|
39
39
|
*/
|
|
40
|
-
async decode(data) {
|
|
40
|
+
async decode(data, settings) {
|
|
41
41
|
if (!this.canDecode(data)) {
|
|
42
42
|
throw new Error("Invalid JPEG signature");
|
|
43
43
|
}
|
|
@@ -95,7 +95,7 @@ export class JPEGFormat {
|
|
|
95
95
|
validateImageDimensions(width, height);
|
|
96
96
|
// For a pure JS implementation, we'd need to implement full JPEG decoding
|
|
97
97
|
// which is very complex. Instead, we'll use the browser/runtime's decoder.
|
|
98
|
-
const rgba = await this.decodeUsingRuntime(data, width, height);
|
|
98
|
+
const rgba = await this.decodeUsingRuntime(data, width, height, settings);
|
|
99
99
|
return {
|
|
100
100
|
width,
|
|
101
101
|
height,
|
|
@@ -108,10 +108,13 @@ export class JPEGFormat {
|
|
|
108
108
|
* @param imageData Image data to encode
|
|
109
109
|
* @returns Encoded JPEG image bytes
|
|
110
110
|
*/
|
|
111
|
-
async encode(imageData) {
|
|
111
|
+
async encode(imageData, options) {
|
|
112
112
|
const { width, height, data, metadata } = imageData;
|
|
113
|
+
const requestedQuality = options?.quality;
|
|
114
|
+
const requestedProgressive = options?.progressive;
|
|
113
115
|
// Try to use runtime encoding if available (better quality)
|
|
114
|
-
|
|
116
|
+
// Note: progressive output is only supported by the pure-JS encoder.
|
|
117
|
+
if (!requestedProgressive && typeof OffscreenCanvas !== "undefined") {
|
|
115
118
|
try {
|
|
116
119
|
const canvas = new OffscreenCanvas(width, height);
|
|
117
120
|
const ctx = canvas.getContext("2d");
|
|
@@ -120,9 +123,12 @@ export class JPEGFormat {
|
|
|
120
123
|
const imgDataData = new Uint8ClampedArray(data);
|
|
121
124
|
imgData.data.set(imgDataData);
|
|
122
125
|
ctx.putImageData(imgData, 0, 0);
|
|
126
|
+
const quality = requestedQuality === undefined
|
|
127
|
+
? 0.9
|
|
128
|
+
: Math.max(1, Math.min(100, requestedQuality)) / 100;
|
|
123
129
|
const blob = await canvas.convertToBlob({
|
|
124
130
|
type: "image/jpeg",
|
|
125
|
-
quality
|
|
131
|
+
quality,
|
|
126
132
|
});
|
|
127
133
|
const arrayBuffer = await blob.arrayBuffer();
|
|
128
134
|
const encoded = new Uint8Array(arrayBuffer);
|
|
@@ -141,7 +147,10 @@ export class JPEGFormat {
|
|
|
141
147
|
const { JPEGEncoder } = await import("../utils/jpeg_encoder.js");
|
|
142
148
|
const dpiX = metadata?.dpiX ?? 72;
|
|
143
149
|
const dpiY = metadata?.dpiY ?? 72;
|
|
144
|
-
const encoder = new JPEGEncoder(
|
|
150
|
+
const encoder = new JPEGEncoder({
|
|
151
|
+
quality: requestedQuality,
|
|
152
|
+
progressive: requestedProgressive,
|
|
153
|
+
});
|
|
145
154
|
const encoded = encoder.encode(width, height, data, dpiX, dpiY);
|
|
146
155
|
// Add EXIF metadata if present
|
|
147
156
|
if (metadata && Object.keys(metadata).length > 0) {
|
|
@@ -185,9 +194,10 @@ export class JPEGFormat {
|
|
|
185
194
|
result.set(encoded.slice(pos), pos + app1.length);
|
|
186
195
|
return result;
|
|
187
196
|
}
|
|
188
|
-
async decodeUsingRuntime(data, _width, _height) {
|
|
197
|
+
async decodeUsingRuntime(data, _width, _height, settings) {
|
|
189
198
|
// Try to use ImageDecoder API if available (Deno, modern browsers)
|
|
190
|
-
if (
|
|
199
|
+
if (settings?.runtimeDecoding !== "never" &&
|
|
200
|
+
typeof ImageDecoder !== "undefined") {
|
|
191
201
|
try {
|
|
192
202
|
const decoder = new ImageDecoder({ data, type: "image/jpeg" });
|
|
193
203
|
const result = await decoder.decode();
|
|
@@ -202,15 +212,17 @@ export class JPEGFormat {
|
|
|
202
212
|
bitmap.close();
|
|
203
213
|
return new Uint8Array(imageData.data.buffer);
|
|
204
214
|
}
|
|
205
|
-
catch (
|
|
215
|
+
catch (_error) {
|
|
206
216
|
// ImageDecoder API failed, fall through to pure JS decoder
|
|
207
|
-
console.warn("JPEG decoding with ImageDecoder failed, using pure JS decoder:", error);
|
|
208
217
|
}
|
|
209
218
|
}
|
|
210
219
|
// Fallback to pure JavaScript decoder
|
|
211
220
|
try {
|
|
212
221
|
const { JPEGDecoder } = await import("../utils/jpeg_decoder.js");
|
|
213
|
-
const decoder = new JPEGDecoder(data
|
|
222
|
+
const decoder = new JPEGDecoder(data, {
|
|
223
|
+
tolerantDecoding: settings?.tolerantDecoding ?? true,
|
|
224
|
+
onWarning: settings?.onWarning,
|
|
225
|
+
});
|
|
214
226
|
return decoder.decode();
|
|
215
227
|
}
|
|
216
228
|
catch (error) {
|
|
@@ -1060,4 +1072,80 @@ export class JPEGFormat {
|
|
|
1060
1072
|
"dpiY",
|
|
1061
1073
|
];
|
|
1062
1074
|
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Extract metadata from JPEG data without fully decoding the pixel data
|
|
1077
|
+
* This quickly parses JFIF and EXIF markers to extract metadata
|
|
1078
|
+
* @param data Raw JPEG data
|
|
1079
|
+
* @returns Extracted metadata or undefined
|
|
1080
|
+
*/
|
|
1081
|
+
extractMetadata(data) {
|
|
1082
|
+
if (!this.canDecode(data)) {
|
|
1083
|
+
return Promise.resolve(undefined);
|
|
1084
|
+
}
|
|
1085
|
+
// Parse JPEG structure to extract metadata
|
|
1086
|
+
let pos = 2; // Skip initial FF D8
|
|
1087
|
+
const metadata = {
|
|
1088
|
+
format: "jpeg",
|
|
1089
|
+
compression: "dct",
|
|
1090
|
+
frameCount: 1,
|
|
1091
|
+
bitDepth: 8,
|
|
1092
|
+
colorType: "rgb",
|
|
1093
|
+
};
|
|
1094
|
+
let width = 0;
|
|
1095
|
+
let height = 0;
|
|
1096
|
+
while (pos < data.length - 1) {
|
|
1097
|
+
if (data[pos] !== 0xff) {
|
|
1098
|
+
pos++;
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
const marker = data[pos + 1];
|
|
1102
|
+
pos += 2;
|
|
1103
|
+
// SOF markers (Start of Frame) - get dimensions for DPI calculation
|
|
1104
|
+
if (marker >= 0xc0 && marker <= 0xcf &&
|
|
1105
|
+
marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
1106
|
+
const length = (data[pos] << 8) | data[pos + 1];
|
|
1107
|
+
// precision at pos+2
|
|
1108
|
+
const precision = data[pos + 2];
|
|
1109
|
+
if (precision && precision !== 8) {
|
|
1110
|
+
metadata.bitDepth = precision;
|
|
1111
|
+
}
|
|
1112
|
+
height = (data[pos + 3] << 8) | data[pos + 4];
|
|
1113
|
+
width = (data[pos + 5] << 8) | data[pos + 6];
|
|
1114
|
+
// Check number of components
|
|
1115
|
+
const numComponents = data[pos + 7];
|
|
1116
|
+
if (numComponents === 1) {
|
|
1117
|
+
metadata.colorType = "grayscale";
|
|
1118
|
+
}
|
|
1119
|
+
// Don't break - continue parsing for metadata
|
|
1120
|
+
pos += length;
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
// APP0 marker (JFIF)
|
|
1124
|
+
if (marker === 0xe0) {
|
|
1125
|
+
const length = (data[pos] << 8) | data[pos + 1];
|
|
1126
|
+
const appData = data.slice(pos + 2, pos + length);
|
|
1127
|
+
this.parseJFIF(appData, metadata, width, height);
|
|
1128
|
+
pos += length;
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
// APP1 marker (EXIF)
|
|
1132
|
+
if (marker === 0xe1) {
|
|
1133
|
+
const length = (data[pos] << 8) | data[pos + 1];
|
|
1134
|
+
const appData = data.slice(pos + 2, pos + length);
|
|
1135
|
+
this.parseEXIF(appData, metadata);
|
|
1136
|
+
pos += length;
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
// Skip other markers
|
|
1140
|
+
if (marker === 0xd9 || marker === 0xda)
|
|
1141
|
+
break; // EOI or SOS
|
|
1142
|
+
if (marker >= 0xd0 && marker <= 0xd8)
|
|
1143
|
+
continue; // RST markers have no length
|
|
1144
|
+
if (marker === 0x01)
|
|
1145
|
+
continue; // TEM has no length
|
|
1146
|
+
const length = (data[pos] << 8) | data[pos + 1];
|
|
1147
|
+
pos += length;
|
|
1148
|
+
}
|
|
1149
|
+
return Promise.resolve(Object.keys(metadata).length > 0 ? metadata : undefined);
|
|
1150
|
+
}
|
|
1063
1151
|
}
|
package/esm/src/formats/pam.d.ts
CHANGED
|
@@ -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
|
package/esm/src/formats/pam.js
CHANGED
|
@@ -53,7 +53,7 @@ export class PAMFormat {
|
|
|
53
53
|
* @param data Raw PAM image data
|
|
54
54
|
* @returns Decoded image data with RGBA pixels
|
|
55
55
|
*/
|
|
56
|
-
decode(data) {
|
|
56
|
+
decode(data, _options) {
|
|
57
57
|
if (!this.canDecode(data)) {
|
|
58
58
|
throw new Error("Invalid PAM signature");
|
|
59
59
|
}
|
|
@@ -150,7 +150,7 @@ export class PAMFormat {
|
|
|
150
150
|
* @param imageData Image data to encode
|
|
151
151
|
* @returns Encoded PAM image bytes
|
|
152
152
|
*/
|
|
153
|
-
encode(imageData) {
|
|
153
|
+
encode(imageData, _options) {
|
|
154
154
|
const { width, height, data } = imageData;
|
|
155
155
|
// Validate input
|
|
156
156
|
if (data.length !== width * height * 4) {
|
|
@@ -174,4 +174,70 @@ export class PAMFormat {
|
|
|
174
174
|
output.set(data, headerBytes.length);
|
|
175
175
|
return Promise.resolve(output);
|
|
176
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Get the list of metadata fields supported by PAM format
|
|
179
|
+
*/
|
|
180
|
+
getSupportedMetadata() {
|
|
181
|
+
return []; // PAM format doesn't support metadata preservation
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Extract metadata from PAM data without fully decoding the pixel data
|
|
185
|
+
* @param data Raw PAM data
|
|
186
|
+
* @returns Extracted metadata or undefined
|
|
187
|
+
*/
|
|
188
|
+
extractMetadata(data) {
|
|
189
|
+
if (!this.canDecode(data)) {
|
|
190
|
+
return Promise.resolve(undefined);
|
|
191
|
+
}
|
|
192
|
+
const metadata = {
|
|
193
|
+
format: "pam",
|
|
194
|
+
compression: "none",
|
|
195
|
+
frameCount: 1,
|
|
196
|
+
bitDepth: 8, // PAM typically uses 8 bits per channel
|
|
197
|
+
colorType: "rgba",
|
|
198
|
+
};
|
|
199
|
+
// Try to parse the header to get actual color type
|
|
200
|
+
const decoder = new TextDecoder();
|
|
201
|
+
const headerText = decoder.decode(data.slice(0, Math.min(200, data.length)));
|
|
202
|
+
// Look for TUPLTYPE to determine color type
|
|
203
|
+
const tupltypeMatch = headerText.match(/TUPLTYPE\s+(\S+)/);
|
|
204
|
+
if (tupltypeMatch) {
|
|
205
|
+
const tupltype = tupltypeMatch[1];
|
|
206
|
+
if (tupltype === "GRAYSCALE") {
|
|
207
|
+
metadata.colorType = "grayscale";
|
|
208
|
+
}
|
|
209
|
+
else if (tupltype === "RGB") {
|
|
210
|
+
metadata.colorType = "rgb";
|
|
211
|
+
}
|
|
212
|
+
else if (tupltype === "RGB_ALPHA") {
|
|
213
|
+
metadata.colorType = "rgba";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Look for DEPTH to determine number of channels
|
|
217
|
+
const depthMatch = headerText.match(/DEPTH\s+(\d+)/);
|
|
218
|
+
if (depthMatch) {
|
|
219
|
+
const depth = parseInt(depthMatch[1]);
|
|
220
|
+
if (depth === 1) {
|
|
221
|
+
metadata.colorType = "grayscale";
|
|
222
|
+
}
|
|
223
|
+
else if (depth === 3) {
|
|
224
|
+
metadata.colorType = "rgb";
|
|
225
|
+
}
|
|
226
|
+
else if (depth === 4) {
|
|
227
|
+
metadata.colorType = "rgba";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Look for MAXVAL to determine bit depth
|
|
231
|
+
const maxvalMatch = headerText.match(/MAXVAL\s+(\d+)/);
|
|
232
|
+
if (maxvalMatch) {
|
|
233
|
+
const maxval = parseInt(maxvalMatch[1]);
|
|
234
|
+
if (maxval === 255) {
|
|
235
|
+
metadata.bitDepth = 8;
|
|
236
|
+
}
|
|
237
|
+
else if (maxval === 65535) {
|
|
238
|
+
metadata.bitDepth = 16;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return Promise.resolve(metadata);
|
|
242
|
+
}
|
|
177
243
|
}
|
package/esm/src/formats/pcx.d.ts
CHANGED
|
@@ -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
|
package/esm/src/formats/pcx.js
CHANGED
|
@@ -28,7 +28,7 @@ export class PCXFormat {
|
|
|
28
28
|
data[1] === 5) &&
|
|
29
29
|
data[2] === 1;
|
|
30
30
|
}
|
|
31
|
-
decode(data) {
|
|
31
|
+
decode(data, _options) {
|
|
32
32
|
if (!this.canDecode(data)) {
|
|
33
33
|
return Promise.reject(new Error("Invalid PCX data"));
|
|
34
34
|
}
|
|
@@ -134,7 +134,7 @@ export class PCXFormat {
|
|
|
134
134
|
data: rgba,
|
|
135
135
|
});
|
|
136
136
|
}
|
|
137
|
-
encode(image) {
|
|
137
|
+
encode(image, _options) {
|
|
138
138
|
const width = image.width;
|
|
139
139
|
const height = image.height;
|
|
140
140
|
const data = image.data;
|
|
@@ -201,4 +201,49 @@ export class PCXFormat {
|
|
|
201
201
|
result.set(rleData, header.length);
|
|
202
202
|
return Promise.resolve(result);
|
|
203
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Get the list of metadata fields supported by PCX format
|
|
206
|
+
*/
|
|
207
|
+
getSupportedMetadata() {
|
|
208
|
+
return [
|
|
209
|
+
"dpiX", // DPI in header
|
|
210
|
+
"dpiY", // DPI in header
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Extract metadata from PCX data without fully decoding the pixel data
|
|
215
|
+
* @param data Raw PCX data
|
|
216
|
+
* @returns Extracted metadata or undefined
|
|
217
|
+
*/
|
|
218
|
+
extractMetadata(data) {
|
|
219
|
+
if (!this.canDecode(data)) {
|
|
220
|
+
return Promise.resolve(undefined);
|
|
221
|
+
}
|
|
222
|
+
const metadata = {
|
|
223
|
+
format: "pcx",
|
|
224
|
+
compression: "rle",
|
|
225
|
+
frameCount: 1,
|
|
226
|
+
bitDepth: data[3], // Bits per pixel per plane
|
|
227
|
+
colorType: "rgb",
|
|
228
|
+
};
|
|
229
|
+
// Check number of planes to determine color type
|
|
230
|
+
const numPlanes = data[65];
|
|
231
|
+
if (numPlanes === 1) {
|
|
232
|
+
metadata.colorType = "indexed";
|
|
233
|
+
}
|
|
234
|
+
else if (numPlanes === 3) {
|
|
235
|
+
metadata.colorType = "rgb";
|
|
236
|
+
}
|
|
237
|
+
else if (numPlanes === 4) {
|
|
238
|
+
metadata.colorType = "rgba";
|
|
239
|
+
}
|
|
240
|
+
// DPI information
|
|
241
|
+
const dpiX = data[12] | (data[13] << 8);
|
|
242
|
+
const dpiY = data[14] | (data[15] << 8);
|
|
243
|
+
if (dpiX > 0)
|
|
244
|
+
metadata.dpiX = dpiX;
|
|
245
|
+
if (dpiY > 0)
|
|
246
|
+
metadata.dpiY = dpiY;
|
|
247
|
+
return Promise.resolve(metadata);
|
|
248
|
+
}
|
|
204
249
|
}
|
package/esm/src/formats/png.d.ts
CHANGED
|
@@ -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
|
package/esm/src/formats/png.js
CHANGED
|
@@ -40,7 +40,7 @@ export class PNGFormat extends PNGBase {
|
|
|
40
40
|
* @param data Raw PNG image data
|
|
41
41
|
* @returns Decoded image data with RGBA pixels
|
|
42
42
|
*/
|
|
43
|
-
async decode(data) {
|
|
43
|
+
async decode(data, _options) {
|
|
44
44
|
if (!this.canDecode(data)) {
|
|
45
45
|
throw new Error("Invalid PNG signature");
|
|
46
46
|
}
|
|
@@ -112,7 +112,7 @@ export class PNGFormat extends PNGBase {
|
|
|
112
112
|
* @param imageData Image data to encode
|
|
113
113
|
* @returns Encoded PNG image bytes
|
|
114
114
|
*/
|
|
115
|
-
async encode(imageData) {
|
|
115
|
+
async encode(imageData, _options) {
|
|
116
116
|
const { width, height, data, metadata } = imageData;
|
|
117
117
|
// Prepare IHDR chunk
|
|
118
118
|
const ihdr = new Uint8Array(13);
|
|
@@ -137,4 +137,91 @@ export class PNGFormat extends PNGBase {
|
|
|
137
137
|
// Concatenate all chunks
|
|
138
138
|
return this.concatenateArrays(chunks);
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Get the list of metadata fields supported by PNG format
|
|
142
|
+
* Delegates to PNGBase implementation
|
|
143
|
+
*/
|
|
144
|
+
getSupportedMetadata() {
|
|
145
|
+
return super.getSupportedMetadata();
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Extract metadata from PNG data without fully decoding the pixel data
|
|
149
|
+
* This quickly parses PNG chunks to extract metadata
|
|
150
|
+
* @param data Raw PNG data
|
|
151
|
+
* @returns Extracted metadata or undefined
|
|
152
|
+
*/
|
|
153
|
+
extractMetadata(data) {
|
|
154
|
+
if (!this.canDecode(data)) {
|
|
155
|
+
return Promise.resolve(undefined);
|
|
156
|
+
}
|
|
157
|
+
let pos = 8; // Skip PNG signature
|
|
158
|
+
let width = 0;
|
|
159
|
+
let height = 0;
|
|
160
|
+
const metadata = {
|
|
161
|
+
format: "png",
|
|
162
|
+
compression: "deflate",
|
|
163
|
+
frameCount: 1,
|
|
164
|
+
};
|
|
165
|
+
// Parse chunks for metadata only
|
|
166
|
+
while (pos < data.length) {
|
|
167
|
+
if (pos + 8 > data.length)
|
|
168
|
+
break;
|
|
169
|
+
const length = this.readUint32(data, pos);
|
|
170
|
+
pos += 4;
|
|
171
|
+
const type = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
|
|
172
|
+
pos += 4;
|
|
173
|
+
if (pos + length + 4 > data.length)
|
|
174
|
+
break;
|
|
175
|
+
const chunkData = data.slice(pos, pos + length);
|
|
176
|
+
pos += length;
|
|
177
|
+
pos += 4; // Skip CRC
|
|
178
|
+
if (type === "IHDR") {
|
|
179
|
+
width = this.readUint32(chunkData, 0);
|
|
180
|
+
height = this.readUint32(chunkData, 4);
|
|
181
|
+
// Parse bit depth and color type from IHDR
|
|
182
|
+
if (chunkData.length >= 9) {
|
|
183
|
+
metadata.bitDepth = chunkData[8];
|
|
184
|
+
const colorTypeCode = chunkData[9];
|
|
185
|
+
// PNG color types: 0=grayscale, 2=rgb, 3=indexed, 4=grayscale+alpha, 6=rgba
|
|
186
|
+
switch (colorTypeCode) {
|
|
187
|
+
case 0:
|
|
188
|
+
metadata.colorType = "grayscale";
|
|
189
|
+
break;
|
|
190
|
+
case 2:
|
|
191
|
+
metadata.colorType = "rgb";
|
|
192
|
+
break;
|
|
193
|
+
case 3:
|
|
194
|
+
metadata.colorType = "indexed";
|
|
195
|
+
break;
|
|
196
|
+
case 4:
|
|
197
|
+
metadata.colorType = "grayscale-alpha";
|
|
198
|
+
break;
|
|
199
|
+
case 6:
|
|
200
|
+
metadata.colorType = "rgba";
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else if (type === "pHYs") {
|
|
206
|
+
// Physical pixel dimensions
|
|
207
|
+
this.parsePhysChunk(chunkData, metadata, width, height);
|
|
208
|
+
}
|
|
209
|
+
else if (type === "tEXt") {
|
|
210
|
+
// Text chunk
|
|
211
|
+
this.parseTextChunk(chunkData, metadata);
|
|
212
|
+
}
|
|
213
|
+
else if (type === "iTXt") {
|
|
214
|
+
// International text chunk
|
|
215
|
+
this.parseITxtChunk(chunkData, metadata);
|
|
216
|
+
}
|
|
217
|
+
else if (type === "eXIf") {
|
|
218
|
+
// EXIF chunk
|
|
219
|
+
this.parseExifChunk(chunkData, metadata);
|
|
220
|
+
}
|
|
221
|
+
else if (type === "IEND") {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return Promise.resolve(Object.keys(metadata).length > 0 ? metadata : undefined);
|
|
226
|
+
}
|
|
140
227
|
}
|
|
@@ -105,9 +105,7 @@ export class PNGBase {
|
|
|
105
105
|
for (let x = 0; x < scanline.length; x++) {
|
|
106
106
|
const left = x >= bytesPerPixel ? scanline[x - bytesPerPixel] : 0;
|
|
107
107
|
const above = prevLine ? prevLine[x] : 0;
|
|
108
|
-
const upperLeft = (x >= bytesPerPixel && prevLine)
|
|
109
|
-
? prevLine[x - bytesPerPixel]
|
|
110
|
-
: 0;
|
|
108
|
+
const upperLeft = (x >= bytesPerPixel && prevLine) ? prevLine[x - bytesPerPixel] : 0;
|
|
111
109
|
switch (filterType) {
|
|
112
110
|
case 0: // None
|
|
113
111
|
break;
|
|
@@ -121,8 +119,7 @@ export class PNGBase {
|
|
|
121
119
|
scanline[x] = (scanline[x] + Math.floor((left + above) / 2)) & 0xff;
|
|
122
120
|
break;
|
|
123
121
|
case 4: // Paeth
|
|
124
|
-
scanline[x] =
|
|
125
|
-
(scanline[x] + this.paethPredictor(left, above, upperLeft)) & 0xff;
|
|
122
|
+
scanline[x] = (scanline[x] + this.paethPredictor(left, above, upperLeft)) & 0xff;
|
|
126
123
|
break;
|
|
127
124
|
}
|
|
128
125
|
}
|