cross-image 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -5
- package/esm/mod.d.ts +3 -1
- package/esm/mod.js +2 -0
- package/esm/src/formats/apng.d.ts +5 -3
- package/esm/src/formats/apng.js +11 -4
- package/esm/src/formats/avif.d.ts +2 -2
- package/esm/src/formats/avif.js +11 -1
- package/esm/src/formats/gif.d.ts +3 -3
- package/esm/src/formats/gif.js +4 -4
- package/esm/src/formats/heic.d.ts +2 -2
- package/esm/src/formats/heic.js +11 -1
- package/esm/src/formats/jpeg.d.ts +21 -1
- package/esm/src/formats/jpeg.js +59 -0
- package/esm/src/formats/png.d.ts +3 -2
- package/esm/src/formats/png.js +8 -2
- package/esm/src/formats/png_base.d.ts +42 -1
- package/esm/src/formats/png_base.js +198 -5
- package/esm/src/formats/tiff.js +76 -6
- package/esm/src/image.d.ts +54 -1
- package/esm/src/image.js +97 -1
- package/esm/src/types.d.ts +129 -0
- package/esm/src/utils/base64.d.ts +32 -0
- package/esm/src/utils/base64.js +173 -0
- 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 +88 -0
- package/esm/src/utils/jpeg_decoder.d.ts +25 -2
- package/esm/src/utils/jpeg_decoder.js +101 -10
- package/esm/src/utils/jpeg_encoder.d.ts +19 -0
- package/esm/src/utils/jpeg_encoder.js +267 -0
- package/package.json +1 -1
- package/script/mod.d.ts +3 -1
- package/script/mod.js +11 -1
- package/script/src/formats/apng.d.ts +5 -3
- package/script/src/formats/apng.js +11 -4
- package/script/src/formats/avif.d.ts +2 -2
- package/script/src/formats/avif.js +11 -1
- package/script/src/formats/gif.d.ts +3 -3
- package/script/src/formats/gif.js +4 -4
- package/script/src/formats/heic.d.ts +2 -2
- package/script/src/formats/heic.js +11 -1
- package/script/src/formats/jpeg.d.ts +21 -1
- package/script/src/formats/jpeg.js +59 -0
- package/script/src/formats/png.d.ts +3 -2
- package/script/src/formats/png.js +8 -2
- package/script/src/formats/png_base.d.ts +42 -1
- package/script/src/formats/png_base.js +198 -5
- package/script/src/formats/tiff.js +76 -6
- package/script/src/image.d.ts +54 -1
- package/script/src/image.js +96 -0
- package/script/src/types.d.ts +129 -0
- package/script/src/utils/base64.d.ts +32 -0
- package/script/src/utils/base64.js +179 -0
- 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 +92 -0
- package/script/src/utils/jpeg_decoder.d.ts +25 -2
- package/script/src/utils/jpeg_decoder.js +101 -10
- package/script/src/utils/jpeg_encoder.d.ts +19 -0
- package/script/src/utils/jpeg_encoder.js +267 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata, MultiFrameImageData } from "../types.js";
|
|
1
|
+
import type { GIFEncoderOptions, ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata, MultiFrameImageData } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* GIF format handler
|
|
4
4
|
* Now includes pure-JS implementation with custom LZW compression/decompression
|
|
@@ -39,7 +39,7 @@ export declare class GIFFormat implements ImageFormat {
|
|
|
39
39
|
* @param imageData Image data to encode
|
|
40
40
|
* @returns Encoded GIF image bytes
|
|
41
41
|
*/
|
|
42
|
-
encode(imageData: ImageData,
|
|
42
|
+
encode(imageData: ImageData, options?: GIFEncoderOptions): Promise<Uint8Array>;
|
|
43
43
|
/**
|
|
44
44
|
* Decode all frames from an animated GIF
|
|
45
45
|
*/
|
|
@@ -47,7 +47,7 @@ export declare class GIFFormat implements ImageFormat {
|
|
|
47
47
|
/**
|
|
48
48
|
* Encode multi-frame image data to animated GIF
|
|
49
49
|
*/
|
|
50
|
-
encodeFrames(imageData: MultiFrameImageData,
|
|
50
|
+
encodeFrames(imageData: MultiFrameImageData, options?: GIFEncoderOptions): Promise<Uint8Array>;
|
|
51
51
|
private mapDisposalMethod;
|
|
52
52
|
private decodeUsingRuntime;
|
|
53
53
|
private readDataSubBlocks;
|
|
@@ -156,12 +156,12 @@ class GIFFormat {
|
|
|
156
156
|
* @param imageData Image data to encode
|
|
157
157
|
* @returns Encoded GIF image bytes
|
|
158
158
|
*/
|
|
159
|
-
async encode(imageData,
|
|
159
|
+
async encode(imageData, options) {
|
|
160
160
|
const { width, height, data, metadata } = imageData;
|
|
161
161
|
// Try pure-JS encoder first
|
|
162
162
|
try {
|
|
163
163
|
const encoder = new gif_encoder_js_1.GIFEncoder(width, height, data);
|
|
164
|
-
const encoded = encoder.encode();
|
|
164
|
+
const encoded = encoder.encode(options);
|
|
165
165
|
// Inject metadata if present
|
|
166
166
|
if (metadata && Object.keys(metadata).length > 0) {
|
|
167
167
|
const injected = this.injectMetadata(encoded, metadata);
|
|
@@ -240,7 +240,7 @@ class GIFFormat {
|
|
|
240
240
|
/**
|
|
241
241
|
* Encode multi-frame image data to animated GIF
|
|
242
242
|
*/
|
|
243
|
-
encodeFrames(imageData,
|
|
243
|
+
encodeFrames(imageData, options) {
|
|
244
244
|
if (imageData.frames.length === 0) {
|
|
245
245
|
throw new Error("No frames to encode");
|
|
246
246
|
}
|
|
@@ -250,7 +250,7 @@ class GIFFormat {
|
|
|
250
250
|
const delay = frame.frameMetadata?.delay ?? 100;
|
|
251
251
|
encoder.addFrame(frame.data, delay);
|
|
252
252
|
}
|
|
253
|
-
return Promise.resolve(encoder.encode());
|
|
253
|
+
return Promise.resolve(encoder.encode(options));
|
|
254
254
|
}
|
|
255
255
|
mapDisposalMethod(disposal) {
|
|
256
256
|
switch (disposal) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata } from "../types.js";
|
|
1
|
+
import type { HEICEncoderOptions, ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* HEIC format handler
|
|
4
4
|
* Supports HEIC/HEIF images using runtime APIs (ImageDecoder/OffscreenCanvas)
|
|
@@ -32,7 +32,7 @@ export declare class HEICFormat implements ImageFormat {
|
|
|
32
32
|
* @param imageData Image data to encode
|
|
33
33
|
* @returns Encoded HEIC image bytes
|
|
34
34
|
*/
|
|
35
|
-
encode(imageData: ImageData,
|
|
35
|
+
encode(imageData: ImageData, options?: HEICEncoderOptions): Promise<Uint8Array>;
|
|
36
36
|
/**
|
|
37
37
|
* Decode using runtime APIs
|
|
38
38
|
* @param data Raw HEIC data
|
|
@@ -83,8 +83,9 @@ class HEICFormat {
|
|
|
83
83
|
* @param imageData Image data to encode
|
|
84
84
|
* @returns Encoded HEIC image bytes
|
|
85
85
|
*/
|
|
86
|
-
async encode(imageData,
|
|
86
|
+
async encode(imageData, options) {
|
|
87
87
|
const { width, height, data, metadata: _metadata } = imageData;
|
|
88
|
+
const requestedQuality = options?.quality;
|
|
88
89
|
// Try to use runtime encoding if available
|
|
89
90
|
if (typeof OffscreenCanvas !== "undefined") {
|
|
90
91
|
try {
|
|
@@ -95,10 +96,19 @@ class HEICFormat {
|
|
|
95
96
|
const imgDataData = new Uint8ClampedArray(data);
|
|
96
97
|
imgData.data.set(imgDataData);
|
|
97
98
|
ctx.putImageData(imgData, 0, 0);
|
|
99
|
+
const quality = requestedQuality === undefined
|
|
100
|
+
? undefined
|
|
101
|
+
: (requestedQuality <= 1
|
|
102
|
+
? Math.max(0, Math.min(1, requestedQuality))
|
|
103
|
+
: Math.max(1, Math.min(100, requestedQuality)) / 100);
|
|
98
104
|
// Try to encode as HEIC
|
|
99
105
|
const blob = await canvas.convertToBlob({
|
|
100
106
|
type: "image/heic",
|
|
107
|
+
...(quality === undefined ? {} : { quality }),
|
|
101
108
|
});
|
|
109
|
+
if (blob.type !== "image/heic") {
|
|
110
|
+
throw new Error(`Runtime did not encode HEIC (got '${blob.type || "(empty)"}')`);
|
|
111
|
+
}
|
|
102
112
|
const arrayBuffer = await blob.arrayBuffer();
|
|
103
113
|
const encoded = new Uint8Array(arrayBuffer);
|
|
104
114
|
// Note: Metadata injection for HEIC is complex and would require
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata, JPEGEncoderOptions } from "../types.js";
|
|
1
|
+
import type { CoefficientData, ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata, JPEGEncoderOptions, JPEGQuantizedCoefficients } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* JPEG format handler
|
|
4
4
|
* Implements a basic JPEG decoder and encoder
|
|
@@ -42,6 +42,26 @@ 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 quantized DCT coefficients from JPEG data
|
|
47
|
+
* These coefficients can be modified for steganography and re-encoded
|
|
48
|
+
* @param data Raw JPEG data
|
|
49
|
+
* @param options Decoder options
|
|
50
|
+
* @returns JPEGQuantizedCoefficients or undefined if extraction fails
|
|
51
|
+
*/
|
|
52
|
+
extractCoefficients(data: Uint8Array, options?: ImageDecoderOptions): Promise<JPEGQuantizedCoefficients | undefined>;
|
|
53
|
+
/**
|
|
54
|
+
* Type guard to check if coefficient data is JPEG format
|
|
55
|
+
*/
|
|
56
|
+
private isJPEGCoefficients;
|
|
57
|
+
/**
|
|
58
|
+
* Encode JPEG from quantized DCT coefficients
|
|
59
|
+
* Useful for steganography - modify coefficients and re-encode
|
|
60
|
+
* @param coeffs JPEG quantized coefficients
|
|
61
|
+
* @param options Encoding options
|
|
62
|
+
* @returns Encoded JPEG bytes
|
|
63
|
+
*/
|
|
64
|
+
encodeFromCoefficients(coeffs: CoefficientData, options?: JPEGEncoderOptions): Promise<Uint8Array>;
|
|
45
65
|
/**
|
|
46
66
|
* Extract metadata from JPEG data without fully decoding the pixel data
|
|
47
67
|
* This quickly parses JFIF and EXIF markers to extract metadata
|
|
@@ -1108,6 +1108,65 @@ class JPEGFormat {
|
|
|
1108
1108
|
"dpiY",
|
|
1109
1109
|
];
|
|
1110
1110
|
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Extract quantized DCT coefficients from JPEG data
|
|
1113
|
+
* These coefficients can be modified for steganography and re-encoded
|
|
1114
|
+
* @param data Raw JPEG data
|
|
1115
|
+
* @param options Decoder options
|
|
1116
|
+
* @returns JPEGQuantizedCoefficients or undefined if extraction fails
|
|
1117
|
+
*/
|
|
1118
|
+
async extractCoefficients(data, options) {
|
|
1119
|
+
if (!this.canDecode(data)) {
|
|
1120
|
+
return undefined;
|
|
1121
|
+
}
|
|
1122
|
+
try {
|
|
1123
|
+
// Force pure-JS decoding since runtime decoders don't expose coefficients
|
|
1124
|
+
const { JPEGDecoder } = await Promise.resolve().then(() => __importStar(require("../utils/jpeg_decoder.js")));
|
|
1125
|
+
const decoder = new JPEGDecoder(data, {
|
|
1126
|
+
tolerantDecoding: options?.tolerantDecoding ?? true,
|
|
1127
|
+
onWarning: options?.onWarning,
|
|
1128
|
+
extractCoefficients: true,
|
|
1129
|
+
});
|
|
1130
|
+
// Decode to extract coefficients
|
|
1131
|
+
decoder.decode();
|
|
1132
|
+
// Get the quantized coefficients
|
|
1133
|
+
return decoder.getQuantizedCoefficients();
|
|
1134
|
+
}
|
|
1135
|
+
catch (error) {
|
|
1136
|
+
if (options?.onWarning) {
|
|
1137
|
+
options.onWarning(`Failed to extract JPEG coefficients: ${error}`, error);
|
|
1138
|
+
}
|
|
1139
|
+
return undefined;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Type guard to check if coefficient data is JPEG format
|
|
1144
|
+
*/
|
|
1145
|
+
isJPEGCoefficients(coeffs) {
|
|
1146
|
+
return ("format" in coeffs &&
|
|
1147
|
+
coeffs.format === "jpeg" &&
|
|
1148
|
+
"components" in coeffs &&
|
|
1149
|
+
"quantizationTables" in coeffs &&
|
|
1150
|
+
"isProgressive" in coeffs);
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Encode JPEG from quantized DCT coefficients
|
|
1154
|
+
* Useful for steganography - modify coefficients and re-encode
|
|
1155
|
+
* @param coeffs JPEG quantized coefficients
|
|
1156
|
+
* @param options Encoding options
|
|
1157
|
+
* @returns Encoded JPEG bytes
|
|
1158
|
+
*/
|
|
1159
|
+
async encodeFromCoefficients(coeffs, options) {
|
|
1160
|
+
if (!this.isJPEGCoefficients(coeffs)) {
|
|
1161
|
+
throw new Error("Invalid coefficient format for JPEG");
|
|
1162
|
+
}
|
|
1163
|
+
const { JPEGEncoder } = await Promise.resolve().then(() => __importStar(require("../utils/jpeg_encoder.js")));
|
|
1164
|
+
const encoder = new JPEGEncoder({
|
|
1165
|
+
quality: options?.quality,
|
|
1166
|
+
progressive: options?.progressive ?? coeffs.isProgressive,
|
|
1167
|
+
});
|
|
1168
|
+
return encoder.encodeFromCoefficients(coeffs, options);
|
|
1169
|
+
}
|
|
1111
1170
|
/**
|
|
1112
1171
|
* Extract metadata from JPEG data without fully decoding the pixel data
|
|
1113
1172
|
* This quickly parses JFIF and EXIF markers to extract metadata
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageDecoderOptions, 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
|
|
@@ -24,9 +24,10 @@ export declare class PNGFormat extends PNGBase implements ImageFormat {
|
|
|
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,
|
|
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
|
|
@@ -113,10 +113,16 @@ class PNGFormat extends png_base_js_1.PNGBase {
|
|
|
113
113
|
/**
|
|
114
114
|
* Encode RGBA image data to PNG format
|
|
115
115
|
* @param imageData Image data to encode
|
|
116
|
+
* @param options Encoding options (compressionLevel 0-9, default 6)
|
|
116
117
|
* @returns Encoded PNG image bytes
|
|
117
118
|
*/
|
|
118
|
-
async encode(imageData,
|
|
119
|
+
async encode(imageData, options) {
|
|
119
120
|
const { width, height, data, metadata } = imageData;
|
|
121
|
+
const compressionLevel = options?.compressionLevel ?? 6;
|
|
122
|
+
// Validate compression level
|
|
123
|
+
if (compressionLevel < 0 || compressionLevel > 9) {
|
|
124
|
+
throw new Error("Compression level must be between 0 and 9");
|
|
125
|
+
}
|
|
120
126
|
// Prepare IHDR chunk
|
|
121
127
|
const ihdr = new Uint8Array(13);
|
|
122
128
|
this.writeUint32(ihdr, 0, width);
|
|
@@ -127,7 +133,7 @@ class PNGFormat extends png_base_js_1.PNGBase {
|
|
|
127
133
|
ihdr[11] = 0; // filter method
|
|
128
134
|
ihdr[12] = 0; // interlace method
|
|
129
135
|
// Filter and compress image data
|
|
130
|
-
const filtered = this.filterData(data, width, height);
|
|
136
|
+
const filtered = this.filterData(data, width, height, compressionLevel);
|
|
131
137
|
const compressed = await this.deflate(filtered);
|
|
132
138
|
// Build PNG
|
|
133
139
|
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
|
*/
|
|
@@ -144,18 +144,211 @@ class PNGBase {
|
|
|
144
144
|
/**
|
|
145
145
|
* Filter PNG data for encoding (using filter type 0 - None)
|
|
146
146
|
*/
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Apply PNG filter to image data based on compression level
|
|
149
|
+
* @param data Raw RGBA pixel data
|
|
150
|
+
* @param width Image width
|
|
151
|
+
* @param height Image height
|
|
152
|
+
* @param compressionLevel Compression level (0-9, default 6)
|
|
153
|
+
* @returns Filtered data with filter type byte per scanline
|
|
154
|
+
*/
|
|
155
|
+
filterData(data, width, height, compressionLevel = 6) {
|
|
156
|
+
// Choose filtering strategy based on compression level
|
|
157
|
+
if (compressionLevel <= 2) {
|
|
158
|
+
// Fast: No filtering
|
|
159
|
+
return this.applyNoFilter(data, width, height);
|
|
160
|
+
}
|
|
161
|
+
else if (compressionLevel <= 6) {
|
|
162
|
+
// Balanced: Sub filter
|
|
163
|
+
return this.applySubFilter(data, width, height);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
// Best: Adaptive filtering (choose best filter per scanline)
|
|
167
|
+
return this.applyAdaptiveFilter(data, width, height);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Apply filter type 0 (None) - no filtering
|
|
172
|
+
*/
|
|
173
|
+
applyNoFilter(data, width, height) {
|
|
174
|
+
const bytesPerScanline = width * 4;
|
|
175
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
150
176
|
let pos = 0;
|
|
151
177
|
for (let y = 0; y < height; y++) {
|
|
152
178
|
filtered[pos++] = 0; // Filter type: None
|
|
153
|
-
|
|
154
|
-
|
|
179
|
+
const scanlineStart = y * bytesPerScanline;
|
|
180
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
181
|
+
filtered[pos++] = data[scanlineStart + x];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return filtered;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Apply filter type 1 (Sub) - subtract left pixel
|
|
188
|
+
*/
|
|
189
|
+
applySubFilter(data, width, height) {
|
|
190
|
+
const bytesPerScanline = width * 4;
|
|
191
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
192
|
+
let pos = 0;
|
|
193
|
+
for (let y = 0; y < height; y++) {
|
|
194
|
+
filtered[pos++] = 1; // Filter type: Sub
|
|
195
|
+
const scanlineStart = y * bytesPerScanline;
|
|
196
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
197
|
+
const current = data[scanlineStart + x];
|
|
198
|
+
const left = x >= 4 ? data[scanlineStart + x - 4] : 0;
|
|
199
|
+
filtered[pos++] = (current - left) & 0xff;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return filtered;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Apply filter type 2 (Up) - subtract above pixel
|
|
206
|
+
*/
|
|
207
|
+
applyUpFilter(data, width, height) {
|
|
208
|
+
const bytesPerScanline = width * 4;
|
|
209
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
210
|
+
let pos = 0;
|
|
211
|
+
for (let y = 0; y < height; y++) {
|
|
212
|
+
filtered[pos++] = 2; // Filter type: Up
|
|
213
|
+
const scanlineStart = y * bytesPerScanline;
|
|
214
|
+
const prevScanlineStart = (y - 1) * bytesPerScanline;
|
|
215
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
216
|
+
const current = data[scanlineStart + x];
|
|
217
|
+
const up = y > 0 ? data[prevScanlineStart + x] : 0;
|
|
218
|
+
filtered[pos++] = (current - up) & 0xff;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return filtered;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Apply filter type 3 (Average) - subtract average of left and above
|
|
225
|
+
*/
|
|
226
|
+
applyAverageFilter(data, width, height) {
|
|
227
|
+
const bytesPerScanline = width * 4;
|
|
228
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
229
|
+
let pos = 0;
|
|
230
|
+
for (let y = 0; y < height; y++) {
|
|
231
|
+
filtered[pos++] = 3; // Filter type: Average
|
|
232
|
+
const scanlineStart = y * bytesPerScanline;
|
|
233
|
+
const prevScanlineStart = (y - 1) * bytesPerScanline;
|
|
234
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
235
|
+
const current = data[scanlineStart + x];
|
|
236
|
+
const left = x >= 4 ? data[scanlineStart + x - 4] : 0;
|
|
237
|
+
const up = y > 0 ? data[prevScanlineStart + x] : 0;
|
|
238
|
+
const avg = Math.floor((left + up) / 2);
|
|
239
|
+
filtered[pos++] = (current - avg) & 0xff;
|
|
155
240
|
}
|
|
156
241
|
}
|
|
157
242
|
return filtered;
|
|
158
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Apply filter type 4 (Paeth) - Paeth predictor
|
|
246
|
+
*/
|
|
247
|
+
applyPaethFilter(data, width, height) {
|
|
248
|
+
const bytesPerScanline = width * 4;
|
|
249
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
250
|
+
let pos = 0;
|
|
251
|
+
for (let y = 0; y < height; y++) {
|
|
252
|
+
filtered[pos++] = 4; // Filter type: Paeth
|
|
253
|
+
const scanlineStart = y * bytesPerScanline;
|
|
254
|
+
const prevScanlineStart = (y - 1) * bytesPerScanline;
|
|
255
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
256
|
+
const current = data[scanlineStart + x];
|
|
257
|
+
const left = x >= 4 ? data[scanlineStart + x - 4] : 0;
|
|
258
|
+
const up = y > 0 ? data[prevScanlineStart + x] : 0;
|
|
259
|
+
const upLeft = (y > 0 && x >= 4) ? data[prevScanlineStart + x - 4] : 0;
|
|
260
|
+
const paeth = this.paethPredictor(left, up, upLeft);
|
|
261
|
+
filtered[pos++] = (current - paeth) & 0xff;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return filtered;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Calculate sum of absolute differences for a filtered scanline
|
|
268
|
+
* Lower values indicate better compression potential
|
|
269
|
+
*/
|
|
270
|
+
calculateFilterScore(filtered) {
|
|
271
|
+
let sum = 0;
|
|
272
|
+
for (let i = 1; i < filtered.length; i++) {
|
|
273
|
+
const byte = filtered[i];
|
|
274
|
+
// Penalize larger absolute values
|
|
275
|
+
sum += byte < 128 ? byte : (256 - byte);
|
|
276
|
+
}
|
|
277
|
+
return sum;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Apply adaptive filtering - choose best filter per scanline
|
|
281
|
+
*/
|
|
282
|
+
applyAdaptiveFilter(data, width, height) {
|
|
283
|
+
const bytesPerScanline = width * 4;
|
|
284
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
285
|
+
let outPos = 0;
|
|
286
|
+
// Try each filter type and choose the best for each scanline
|
|
287
|
+
const filters = [
|
|
288
|
+
(y) => this.filterScanline(data, y, width, 0), // None
|
|
289
|
+
(y) => this.filterScanline(data, y, width, 1), // Sub
|
|
290
|
+
(y) => this.filterScanline(data, y, width, 2), // Up
|
|
291
|
+
(y) => this.filterScanline(data, y, width, 3), // Average
|
|
292
|
+
(y) => this.filterScanline(data, y, width, 4), // Paeth
|
|
293
|
+
];
|
|
294
|
+
for (let y = 0; y < height; y++) {
|
|
295
|
+
let bestFilter = null;
|
|
296
|
+
let bestScore = Infinity;
|
|
297
|
+
// Try each filter type
|
|
298
|
+
for (const filterFn of filters) {
|
|
299
|
+
const result = filterFn(y);
|
|
300
|
+
const score = this.calculateFilterScore(result);
|
|
301
|
+
if (score < bestScore) {
|
|
302
|
+
bestScore = score;
|
|
303
|
+
bestFilter = result;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Copy best filter result
|
|
307
|
+
if (bestFilter) {
|
|
308
|
+
filtered.set(bestFilter, outPos);
|
|
309
|
+
outPos += bestFilter.length;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return filtered;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Filter a single scanline with specified filter type
|
|
316
|
+
*/
|
|
317
|
+
filterScanline(data, y, width, filterType) {
|
|
318
|
+
const bytesPerScanline = width * 4;
|
|
319
|
+
const result = new Uint8Array(1 + bytesPerScanline);
|
|
320
|
+
result[0] = filterType;
|
|
321
|
+
const scanlineStart = y * bytesPerScanline;
|
|
322
|
+
const prevScanlineStart = (y - 1) * bytesPerScanline;
|
|
323
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
324
|
+
const current = data[scanlineStart + x];
|
|
325
|
+
const left = x >= 4 ? data[scanlineStart + x - 4] : 0;
|
|
326
|
+
const up = y > 0 ? data[prevScanlineStart + x] : 0;
|
|
327
|
+
const upLeft = (y > 0 && x >= 4) ? data[prevScanlineStart + x - 4] : 0;
|
|
328
|
+
let filtered;
|
|
329
|
+
switch (filterType) {
|
|
330
|
+
case 0: // None
|
|
331
|
+
filtered = current;
|
|
332
|
+
break;
|
|
333
|
+
case 1: // Sub
|
|
334
|
+
filtered = (current - left) & 0xff;
|
|
335
|
+
break;
|
|
336
|
+
case 2: // Up
|
|
337
|
+
filtered = (current - up) & 0xff;
|
|
338
|
+
break;
|
|
339
|
+
case 3: // Average
|
|
340
|
+
filtered = (current - Math.floor((left + up) / 2)) & 0xff;
|
|
341
|
+
break;
|
|
342
|
+
case 4: // Paeth
|
|
343
|
+
filtered = (current - this.paethPredictor(left, up, upLeft)) & 0xff;
|
|
344
|
+
break;
|
|
345
|
+
default:
|
|
346
|
+
filtered = current;
|
|
347
|
+
}
|
|
348
|
+
result[x + 1] = filtered;
|
|
349
|
+
}
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
159
352
|
/**
|
|
160
353
|
* Get bytes per pixel for a given color type and bit depth
|
|
161
354
|
*/
|