cross-image 0.2.4 → 0.4.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/LICENSE +21 -21
- package/README.md +615 -333
- package/esm/mod.d.ts +6 -4
- package/esm/mod.js +4 -2
- package/esm/src/formats/apng.d.ts +7 -5
- package/esm/src/formats/apng.js +15 -10
- package/esm/src/formats/ascii.d.ts +3 -3
- package/esm/src/formats/ascii.js +1 -1
- package/esm/src/formats/avif.d.ts +3 -3
- package/esm/src/formats/avif.js +17 -7
- package/esm/src/formats/bmp.d.ts +3 -3
- package/esm/src/formats/bmp.js +2 -2
- package/esm/src/formats/dng.d.ts +1 -1
- package/esm/src/formats/dng.js +1 -1
- package/esm/src/formats/gif.d.ts +5 -5
- package/esm/src/formats/gif.js +17 -13
- package/esm/src/formats/heic.d.ts +3 -3
- package/esm/src/formats/heic.js +17 -7
- package/esm/src/formats/ico.d.ts +3 -3
- package/esm/src/formats/ico.js +4 -4
- package/esm/src/formats/jpeg.d.ts +3 -3
- package/esm/src/formats/jpeg.js +23 -11
- package/esm/src/formats/pam.d.ts +3 -3
- package/esm/src/formats/pam.js +2 -2
- package/esm/src/formats/pcx.d.ts +3 -3
- package/esm/src/formats/pcx.js +2 -2
- package/esm/src/formats/png.d.ts +4 -3
- package/esm/src/formats/png.js +9 -3
- package/esm/src/formats/png_base.d.ts +42 -1
- package/esm/src/formats/png_base.js +200 -10
- package/esm/src/formats/ppm.d.ts +3 -3
- package/esm/src/formats/ppm.js +2 -2
- package/esm/src/formats/tiff.d.ts +7 -18
- package/esm/src/formats/tiff.js +162 -27
- package/esm/src/formats/webp.d.ts +3 -3
- package/esm/src/formats/webp.js +11 -8
- package/esm/src/image.d.ts +26 -3
- package/esm/src/image.js +66 -22
- package/esm/src/types.d.ts +122 -4
- package/esm/src/utils/base64.d.ts +32 -0
- package/esm/src/utils/base64.js +173 -0
- package/esm/src/utils/gif_decoder.d.ts +4 -1
- package/esm/src/utils/gif_decoder.js +91 -65
- package/esm/src/utils/gif_encoder.d.ts +3 -1
- package/esm/src/utils/gif_encoder.js +4 -2
- package/esm/src/utils/image_processing.d.ts +31 -0
- package/esm/src/utils/image_processing.js +232 -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 +1 -1
- package/script/mod.d.ts +6 -4
- package/script/mod.js +13 -3
- package/script/src/formats/apng.d.ts +7 -5
- package/script/src/formats/apng.js +15 -10
- package/script/src/formats/ascii.d.ts +3 -3
- package/script/src/formats/ascii.js +1 -1
- package/script/src/formats/avif.d.ts +3 -3
- package/script/src/formats/avif.js +17 -7
- package/script/src/formats/bmp.d.ts +3 -3
- package/script/src/formats/bmp.js +2 -2
- package/script/src/formats/dng.d.ts +1 -1
- package/script/src/formats/dng.js +1 -1
- package/script/src/formats/gif.d.ts +5 -5
- package/script/src/formats/gif.js +17 -13
- package/script/src/formats/heic.d.ts +3 -3
- package/script/src/formats/heic.js +17 -7
- package/script/src/formats/ico.d.ts +3 -3
- package/script/src/formats/ico.js +4 -4
- package/script/src/formats/jpeg.d.ts +3 -3
- package/script/src/formats/jpeg.js +23 -11
- package/script/src/formats/pam.d.ts +3 -3
- package/script/src/formats/pam.js +2 -2
- package/script/src/formats/pcx.d.ts +3 -3
- package/script/src/formats/pcx.js +2 -2
- package/script/src/formats/png.d.ts +4 -3
- package/script/src/formats/png.js +9 -3
- package/script/src/formats/png_base.d.ts +42 -1
- package/script/src/formats/png_base.js +200 -10
- package/script/src/formats/ppm.d.ts +3 -3
- package/script/src/formats/ppm.js +2 -2
- package/script/src/formats/tiff.d.ts +7 -18
- package/script/src/formats/tiff.js +162 -27
- package/script/src/formats/webp.d.ts +3 -3
- package/script/src/formats/webp.js +11 -8
- package/script/src/image.d.ts +26 -3
- package/script/src/image.js +64 -20
- package/script/src/types.d.ts +122 -4
- package/script/src/utils/base64.d.ts +32 -0
- package/script/src/utils/base64.js +179 -0
- package/script/src/utils/gif_decoder.d.ts +4 -1
- package/script/src/utils/gif_decoder.js +91 -65
- package/script/src/utils/gif_encoder.d.ts +3 -1
- package/script/src/utils/gif_encoder.js +4 -2
- package/script/src/utils/image_processing.d.ts +31 -0
- package/script/src/utils/image_processing.js +236 -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/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) {
|
package/esm/src/formats/pam.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat, ImageMetadata } 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,13 +32,13 @@ 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
42
|
/**
|
|
43
43
|
* Get the list of metadata fields supported by PAM format
|
|
44
44
|
*/
|
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) {
|
package/esm/src/formats/pcx.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat, ImageMetadata } 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,8 +7,8 @@ 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
12
|
/**
|
|
13
13
|
* Get the list of metadata fields supported by PCX format
|
|
14
14
|
*/
|
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;
|
package/esm/src/formats/png.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat, ImageMetadata } from "../types.js";
|
|
1
|
+
import type { ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata, PNGEncoderOptions } from "../types.js";
|
|
2
2
|
import { PNGBase } from "./png_base.js";
|
|
3
3
|
/**
|
|
4
4
|
* PNG format handler
|
|
@@ -20,13 +20,14 @@ 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
|
+
* @param options Encoding options (compressionLevel 0-9, default 6)
|
|
27
28
|
* @returns Encoded PNG image bytes
|
|
28
29
|
*/
|
|
29
|
-
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
30
|
+
encode(imageData: ImageData, options?: PNGEncoderOptions): Promise<Uint8Array>;
|
|
30
31
|
/**
|
|
31
32
|
* Get the list of metadata fields supported by PNG format
|
|
32
33
|
* Delegates to PNGBase implementation
|
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
|
}
|
|
@@ -110,10 +110,16 @@ export class PNGFormat extends PNGBase {
|
|
|
110
110
|
/**
|
|
111
111
|
* Encode RGBA image data to PNG format
|
|
112
112
|
* @param imageData Image data to encode
|
|
113
|
+
* @param options Encoding options (compressionLevel 0-9, default 6)
|
|
113
114
|
* @returns Encoded PNG image bytes
|
|
114
115
|
*/
|
|
115
|
-
async encode(imageData) {
|
|
116
|
+
async encode(imageData, options) {
|
|
116
117
|
const { width, height, data, metadata } = imageData;
|
|
118
|
+
const compressionLevel = options?.compressionLevel ?? 6;
|
|
119
|
+
// Validate compression level
|
|
120
|
+
if (compressionLevel < 0 || compressionLevel > 9) {
|
|
121
|
+
throw new Error("Compression level must be between 0 and 9");
|
|
122
|
+
}
|
|
117
123
|
// Prepare IHDR chunk
|
|
118
124
|
const ihdr = new Uint8Array(13);
|
|
119
125
|
this.writeUint32(ihdr, 0, width);
|
|
@@ -124,7 +130,7 @@ export class PNGFormat extends PNGBase {
|
|
|
124
130
|
ihdr[11] = 0; // filter method
|
|
125
131
|
ihdr[12] = 0; // interlace method
|
|
126
132
|
// Filter and compress image data
|
|
127
|
-
const filtered = this.filterData(data, width, height);
|
|
133
|
+
const filtered = this.filterData(data, width, height, compressionLevel);
|
|
128
134
|
const compressed = await this.deflate(filtered);
|
|
129
135
|
// Build PNG
|
|
130
136
|
const chunks = [];
|
|
@@ -43,7 +43,48 @@ export declare abstract class PNGBase {
|
|
|
43
43
|
/**
|
|
44
44
|
* Filter PNG data for encoding (using filter type 0 - None)
|
|
45
45
|
*/
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Apply PNG filter to image data based on compression level
|
|
48
|
+
* @param data Raw RGBA pixel data
|
|
49
|
+
* @param width Image width
|
|
50
|
+
* @param height Image height
|
|
51
|
+
* @param compressionLevel Compression level (0-9, default 6)
|
|
52
|
+
* @returns Filtered data with filter type byte per scanline
|
|
53
|
+
*/
|
|
54
|
+
protected filterData(data: Uint8Array, width: number, height: number, compressionLevel?: number): Uint8Array;
|
|
55
|
+
/**
|
|
56
|
+
* Apply filter type 0 (None) - no filtering
|
|
57
|
+
*/
|
|
58
|
+
private applyNoFilter;
|
|
59
|
+
/**
|
|
60
|
+
* Apply filter type 1 (Sub) - subtract left pixel
|
|
61
|
+
*/
|
|
62
|
+
private applySubFilter;
|
|
63
|
+
/**
|
|
64
|
+
* Apply filter type 2 (Up) - subtract above pixel
|
|
65
|
+
*/
|
|
66
|
+
private applyUpFilter;
|
|
67
|
+
/**
|
|
68
|
+
* Apply filter type 3 (Average) - subtract average of left and above
|
|
69
|
+
*/
|
|
70
|
+
private applyAverageFilter;
|
|
71
|
+
/**
|
|
72
|
+
* Apply filter type 4 (Paeth) - Paeth predictor
|
|
73
|
+
*/
|
|
74
|
+
private applyPaethFilter;
|
|
75
|
+
/**
|
|
76
|
+
* Calculate sum of absolute differences for a filtered scanline
|
|
77
|
+
* Lower values indicate better compression potential
|
|
78
|
+
*/
|
|
79
|
+
private calculateFilterScore;
|
|
80
|
+
/**
|
|
81
|
+
* Apply adaptive filtering - choose best filter per scanline
|
|
82
|
+
*/
|
|
83
|
+
private applyAdaptiveFilter;
|
|
84
|
+
/**
|
|
85
|
+
* Filter a single scanline with specified filter type
|
|
86
|
+
*/
|
|
87
|
+
private filterScanline;
|
|
47
88
|
/**
|
|
48
89
|
* Get bytes per pixel for a given color type and bit depth
|
|
49
90
|
*/
|
|
@@ -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
|
}
|
|
@@ -144,18 +141,211 @@ export class PNGBase {
|
|
|
144
141
|
/**
|
|
145
142
|
* Filter PNG data for encoding (using filter type 0 - None)
|
|
146
143
|
*/
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Apply PNG filter to image data based on compression level
|
|
146
|
+
* @param data Raw RGBA pixel data
|
|
147
|
+
* @param width Image width
|
|
148
|
+
* @param height Image height
|
|
149
|
+
* @param compressionLevel Compression level (0-9, default 6)
|
|
150
|
+
* @returns Filtered data with filter type byte per scanline
|
|
151
|
+
*/
|
|
152
|
+
filterData(data, width, height, compressionLevel = 6) {
|
|
153
|
+
// Choose filtering strategy based on compression level
|
|
154
|
+
if (compressionLevel <= 2) {
|
|
155
|
+
// Fast: No filtering
|
|
156
|
+
return this.applyNoFilter(data, width, height);
|
|
157
|
+
}
|
|
158
|
+
else if (compressionLevel <= 6) {
|
|
159
|
+
// Balanced: Sub filter
|
|
160
|
+
return this.applySubFilter(data, width, height);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// Best: Adaptive filtering (choose best filter per scanline)
|
|
164
|
+
return this.applyAdaptiveFilter(data, width, height);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Apply filter type 0 (None) - no filtering
|
|
169
|
+
*/
|
|
170
|
+
applyNoFilter(data, width, height) {
|
|
171
|
+
const bytesPerScanline = width * 4;
|
|
172
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
150
173
|
let pos = 0;
|
|
151
174
|
for (let y = 0; y < height; y++) {
|
|
152
175
|
filtered[pos++] = 0; // Filter type: None
|
|
153
|
-
|
|
154
|
-
|
|
176
|
+
const scanlineStart = y * bytesPerScanline;
|
|
177
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
178
|
+
filtered[pos++] = data[scanlineStart + x];
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return filtered;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Apply filter type 1 (Sub) - subtract left pixel
|
|
185
|
+
*/
|
|
186
|
+
applySubFilter(data, width, height) {
|
|
187
|
+
const bytesPerScanline = width * 4;
|
|
188
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
189
|
+
let pos = 0;
|
|
190
|
+
for (let y = 0; y < height; y++) {
|
|
191
|
+
filtered[pos++] = 1; // Filter type: Sub
|
|
192
|
+
const scanlineStart = y * bytesPerScanline;
|
|
193
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
194
|
+
const current = data[scanlineStart + x];
|
|
195
|
+
const left = x >= 4 ? data[scanlineStart + x - 4] : 0;
|
|
196
|
+
filtered[pos++] = (current - left) & 0xff;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return filtered;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Apply filter type 2 (Up) - subtract above pixel
|
|
203
|
+
*/
|
|
204
|
+
applyUpFilter(data, width, height) {
|
|
205
|
+
const bytesPerScanline = width * 4;
|
|
206
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
207
|
+
let pos = 0;
|
|
208
|
+
for (let y = 0; y < height; y++) {
|
|
209
|
+
filtered[pos++] = 2; // Filter type: Up
|
|
210
|
+
const scanlineStart = y * bytesPerScanline;
|
|
211
|
+
const prevScanlineStart = (y - 1) * bytesPerScanline;
|
|
212
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
213
|
+
const current = data[scanlineStart + x];
|
|
214
|
+
const up = y > 0 ? data[prevScanlineStart + x] : 0;
|
|
215
|
+
filtered[pos++] = (current - up) & 0xff;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return filtered;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Apply filter type 3 (Average) - subtract average of left and above
|
|
222
|
+
*/
|
|
223
|
+
applyAverageFilter(data, width, height) {
|
|
224
|
+
const bytesPerScanline = width * 4;
|
|
225
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
226
|
+
let pos = 0;
|
|
227
|
+
for (let y = 0; y < height; y++) {
|
|
228
|
+
filtered[pos++] = 3; // Filter type: Average
|
|
229
|
+
const scanlineStart = y * bytesPerScanline;
|
|
230
|
+
const prevScanlineStart = (y - 1) * bytesPerScanline;
|
|
231
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
232
|
+
const current = data[scanlineStart + x];
|
|
233
|
+
const left = x >= 4 ? data[scanlineStart + x - 4] : 0;
|
|
234
|
+
const up = y > 0 ? data[prevScanlineStart + x] : 0;
|
|
235
|
+
const avg = Math.floor((left + up) / 2);
|
|
236
|
+
filtered[pos++] = (current - avg) & 0xff;
|
|
155
237
|
}
|
|
156
238
|
}
|
|
157
239
|
return filtered;
|
|
158
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Apply filter type 4 (Paeth) - Paeth predictor
|
|
243
|
+
*/
|
|
244
|
+
applyPaethFilter(data, width, height) {
|
|
245
|
+
const bytesPerScanline = width * 4;
|
|
246
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
247
|
+
let pos = 0;
|
|
248
|
+
for (let y = 0; y < height; y++) {
|
|
249
|
+
filtered[pos++] = 4; // Filter type: Paeth
|
|
250
|
+
const scanlineStart = y * bytesPerScanline;
|
|
251
|
+
const prevScanlineStart = (y - 1) * bytesPerScanline;
|
|
252
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
253
|
+
const current = data[scanlineStart + x];
|
|
254
|
+
const left = x >= 4 ? data[scanlineStart + x - 4] : 0;
|
|
255
|
+
const up = y > 0 ? data[prevScanlineStart + x] : 0;
|
|
256
|
+
const upLeft = (y > 0 && x >= 4) ? data[prevScanlineStart + x - 4] : 0;
|
|
257
|
+
const paeth = this.paethPredictor(left, up, upLeft);
|
|
258
|
+
filtered[pos++] = (current - paeth) & 0xff;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return filtered;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Calculate sum of absolute differences for a filtered scanline
|
|
265
|
+
* Lower values indicate better compression potential
|
|
266
|
+
*/
|
|
267
|
+
calculateFilterScore(filtered) {
|
|
268
|
+
let sum = 0;
|
|
269
|
+
for (let i = 1; i < filtered.length; i++) {
|
|
270
|
+
const byte = filtered[i];
|
|
271
|
+
// Penalize larger absolute values
|
|
272
|
+
sum += byte < 128 ? byte : (256 - byte);
|
|
273
|
+
}
|
|
274
|
+
return sum;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Apply adaptive filtering - choose best filter per scanline
|
|
278
|
+
*/
|
|
279
|
+
applyAdaptiveFilter(data, width, height) {
|
|
280
|
+
const bytesPerScanline = width * 4;
|
|
281
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
282
|
+
let outPos = 0;
|
|
283
|
+
// Try each filter type and choose the best for each scanline
|
|
284
|
+
const filters = [
|
|
285
|
+
(y) => this.filterScanline(data, y, width, 0), // None
|
|
286
|
+
(y) => this.filterScanline(data, y, width, 1), // Sub
|
|
287
|
+
(y) => this.filterScanline(data, y, width, 2), // Up
|
|
288
|
+
(y) => this.filterScanline(data, y, width, 3), // Average
|
|
289
|
+
(y) => this.filterScanline(data, y, width, 4), // Paeth
|
|
290
|
+
];
|
|
291
|
+
for (let y = 0; y < height; y++) {
|
|
292
|
+
let bestFilter = null;
|
|
293
|
+
let bestScore = Infinity;
|
|
294
|
+
// Try each filter type
|
|
295
|
+
for (const filterFn of filters) {
|
|
296
|
+
const result = filterFn(y);
|
|
297
|
+
const score = this.calculateFilterScore(result);
|
|
298
|
+
if (score < bestScore) {
|
|
299
|
+
bestScore = score;
|
|
300
|
+
bestFilter = result;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Copy best filter result
|
|
304
|
+
if (bestFilter) {
|
|
305
|
+
filtered.set(bestFilter, outPos);
|
|
306
|
+
outPos += bestFilter.length;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return filtered;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Filter a single scanline with specified filter type
|
|
313
|
+
*/
|
|
314
|
+
filterScanline(data, y, width, filterType) {
|
|
315
|
+
const bytesPerScanline = width * 4;
|
|
316
|
+
const result = new Uint8Array(1 + bytesPerScanline);
|
|
317
|
+
result[0] = filterType;
|
|
318
|
+
const scanlineStart = y * bytesPerScanline;
|
|
319
|
+
const prevScanlineStart = (y - 1) * bytesPerScanline;
|
|
320
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
321
|
+
const current = data[scanlineStart + x];
|
|
322
|
+
const left = x >= 4 ? data[scanlineStart + x - 4] : 0;
|
|
323
|
+
const up = y > 0 ? data[prevScanlineStart + x] : 0;
|
|
324
|
+
const upLeft = (y > 0 && x >= 4) ? data[prevScanlineStart + x - 4] : 0;
|
|
325
|
+
let filtered;
|
|
326
|
+
switch (filterType) {
|
|
327
|
+
case 0: // None
|
|
328
|
+
filtered = current;
|
|
329
|
+
break;
|
|
330
|
+
case 1: // Sub
|
|
331
|
+
filtered = (current - left) & 0xff;
|
|
332
|
+
break;
|
|
333
|
+
case 2: // Up
|
|
334
|
+
filtered = (current - up) & 0xff;
|
|
335
|
+
break;
|
|
336
|
+
case 3: // Average
|
|
337
|
+
filtered = (current - Math.floor((left + up) / 2)) & 0xff;
|
|
338
|
+
break;
|
|
339
|
+
case 4: // Paeth
|
|
340
|
+
filtered = (current - this.paethPredictor(left, up, upLeft)) & 0xff;
|
|
341
|
+
break;
|
|
342
|
+
default:
|
|
343
|
+
filtered = current;
|
|
344
|
+
}
|
|
345
|
+
result[x + 1] = filtered;
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
159
349
|
/**
|
|
160
350
|
* Get bytes per pixel for a given color type and bit depth
|
|
161
351
|
*/
|
package/esm/src/formats/ppm.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat, ImageMetadata } from "../types.js";
|
|
1
|
+
import type { ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* PPM format handler
|
|
4
4
|
* Implements the Netpbm PPM (Portable PixMap) format.
|
|
@@ -34,14 +34,14 @@ export declare class PPMFormat implements ImageFormat {
|
|
|
34
34
|
* @param data Raw PPM image data
|
|
35
35
|
* @returns Decoded image data with RGBA pixels
|
|
36
36
|
*/
|
|
37
|
-
decode(data: Uint8Array): Promise<ImageData>;
|
|
37
|
+
decode(data: Uint8Array, _options?: ImageDecoderOptions): Promise<ImageData>;
|
|
38
38
|
/**
|
|
39
39
|
* Encode RGBA image data to PPM format (P6 binary)
|
|
40
40
|
* Note: Alpha channel is ignored as PPM doesn't support transparency
|
|
41
41
|
* @param imageData Image data to encode
|
|
42
42
|
* @returns Encoded PPM image bytes
|
|
43
43
|
*/
|
|
44
|
-
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
44
|
+
encode(imageData: ImageData, _options?: unknown): Promise<Uint8Array>;
|
|
45
45
|
/**
|
|
46
46
|
* Check if a byte is whitespace (space, tab, CR, LF)
|
|
47
47
|
*/
|
package/esm/src/formats/ppm.js
CHANGED
|
@@ -56,7 +56,7 @@ export class PPMFormat {
|
|
|
56
56
|
* @param data Raw PPM 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 PPM signature");
|
|
62
62
|
}
|
|
@@ -208,7 +208,7 @@ export class PPMFormat {
|
|
|
208
208
|
* @param imageData Image data to encode
|
|
209
209
|
* @returns Encoded PPM image bytes
|
|
210
210
|
*/
|
|
211
|
-
encode(imageData) {
|
|
211
|
+
encode(imageData, _options) {
|
|
212
212
|
const { width, height, data } = imageData;
|
|
213
213
|
// Validate input
|
|
214
214
|
if (data.length !== width * height * 4) {
|
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat, ImageMetadata, MultiFrameImageData } from "../types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Options for TIFF encoding
|
|
4
|
-
*/
|
|
5
|
-
export interface TIFFEncodeOptions {
|
|
6
|
-
/** Compression method: "none" for uncompressed (default), "lzw" for LZW compression */
|
|
7
|
-
compression?: "none" | "lzw";
|
|
8
|
-
/** Encode as grayscale instead of RGB/RGBA */
|
|
9
|
-
grayscale?: boolean;
|
|
10
|
-
/** Encode as RGB without alpha channel (ignored if grayscale is true) */
|
|
11
|
-
rgb?: boolean;
|
|
12
|
-
}
|
|
1
|
+
import type { ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata, MultiFrameImageData } from "../types.js";
|
|
13
2
|
/**
|
|
14
3
|
* TIFF format handler
|
|
15
|
-
* Implements pure-JS TIFF decoder for uncompressed and
|
|
16
|
-
* and encoder for uncompressed and
|
|
17
|
-
* for
|
|
4
|
+
* Implements pure-JS TIFF decoder for uncompressed, LZW, PackBits, and Deflate-compressed RGB/RGBA images
|
|
5
|
+
* and encoder for uncompressed, LZW, PackBits, and Deflate-compressed RGBA TIFFs. Falls back to ImageDecoder
|
|
6
|
+
* for JPEG-compressed TIFFs.
|
|
18
7
|
* Supports multi-page TIFF files.
|
|
19
8
|
*/
|
|
20
9
|
export declare class TIFFFormat implements ImageFormat {
|
|
@@ -38,12 +27,12 @@ export declare class TIFFFormat implements ImageFormat {
|
|
|
38
27
|
* @param data Raw TIFF image data
|
|
39
28
|
* @returns Decoded image data with RGBA pixels of first page
|
|
40
29
|
*/
|
|
41
|
-
decode(data: Uint8Array): Promise<ImageData>;
|
|
30
|
+
decode(data: Uint8Array, _options?: ImageDecoderOptions): Promise<ImageData>;
|
|
42
31
|
encode(imageData: ImageData, options?: unknown): Promise<Uint8Array>;
|
|
43
32
|
/**
|
|
44
33
|
* Decode all pages from a multi-page TIFF
|
|
45
34
|
*/
|
|
46
|
-
decodeFrames(data: Uint8Array): Promise<MultiFrameImageData>;
|
|
35
|
+
decodeFrames(data: Uint8Array, _options?: ImageDecoderOptions): Promise<MultiFrameImageData>;
|
|
47
36
|
/**
|
|
48
37
|
* Encode multi-page TIFF
|
|
49
38
|
*/
|
|
@@ -65,7 +54,7 @@ export declare class TIFFFormat implements ImageFormat {
|
|
|
65
54
|
private decodeUsingRuntime;
|
|
66
55
|
private readString;
|
|
67
56
|
/**
|
|
68
|
-
* Pure JavaScript TIFF decoder for uncompressed and
|
|
57
|
+
* Pure JavaScript TIFF decoder for uncompressed, LZW, PackBits, and Deflate-compressed RGB/RGBA images
|
|
69
58
|
* Returns null if the TIFF uses unsupported features
|
|
70
59
|
*/
|
|
71
60
|
private decodePureJS;
|