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.
Files changed (62) hide show
  1. package/README.md +186 -5
  2. package/esm/mod.d.ts +3 -1
  3. package/esm/mod.js +2 -0
  4. package/esm/src/formats/apng.d.ts +5 -3
  5. package/esm/src/formats/apng.js +11 -4
  6. package/esm/src/formats/avif.d.ts +2 -2
  7. package/esm/src/formats/avif.js +11 -1
  8. package/esm/src/formats/gif.d.ts +3 -3
  9. package/esm/src/formats/gif.js +4 -4
  10. package/esm/src/formats/heic.d.ts +2 -2
  11. package/esm/src/formats/heic.js +11 -1
  12. package/esm/src/formats/jpeg.d.ts +21 -1
  13. package/esm/src/formats/jpeg.js +59 -0
  14. package/esm/src/formats/png.d.ts +3 -2
  15. package/esm/src/formats/png.js +8 -2
  16. package/esm/src/formats/png_base.d.ts +42 -1
  17. package/esm/src/formats/png_base.js +198 -5
  18. package/esm/src/formats/tiff.js +76 -6
  19. package/esm/src/image.d.ts +54 -1
  20. package/esm/src/image.js +97 -1
  21. package/esm/src/types.d.ts +129 -0
  22. package/esm/src/utils/base64.d.ts +32 -0
  23. package/esm/src/utils/base64.js +173 -0
  24. package/esm/src/utils/gif_encoder.d.ts +3 -1
  25. package/esm/src/utils/gif_encoder.js +4 -2
  26. package/esm/src/utils/image_processing.d.ts +31 -0
  27. package/esm/src/utils/image_processing.js +88 -0
  28. package/esm/src/utils/jpeg_decoder.d.ts +25 -2
  29. package/esm/src/utils/jpeg_decoder.js +101 -10
  30. package/esm/src/utils/jpeg_encoder.d.ts +19 -0
  31. package/esm/src/utils/jpeg_encoder.js +267 -0
  32. package/package.json +1 -1
  33. package/script/mod.d.ts +3 -1
  34. package/script/mod.js +11 -1
  35. package/script/src/formats/apng.d.ts +5 -3
  36. package/script/src/formats/apng.js +11 -4
  37. package/script/src/formats/avif.d.ts +2 -2
  38. package/script/src/formats/avif.js +11 -1
  39. package/script/src/formats/gif.d.ts +3 -3
  40. package/script/src/formats/gif.js +4 -4
  41. package/script/src/formats/heic.d.ts +2 -2
  42. package/script/src/formats/heic.js +11 -1
  43. package/script/src/formats/jpeg.d.ts +21 -1
  44. package/script/src/formats/jpeg.js +59 -0
  45. package/script/src/formats/png.d.ts +3 -2
  46. package/script/src/formats/png.js +8 -2
  47. package/script/src/formats/png_base.d.ts +42 -1
  48. package/script/src/formats/png_base.js +198 -5
  49. package/script/src/formats/tiff.js +76 -6
  50. package/script/src/image.d.ts +54 -1
  51. package/script/src/image.js +96 -0
  52. package/script/src/types.d.ts +129 -0
  53. package/script/src/utils/base64.d.ts +32 -0
  54. package/script/src/utils/base64.js +179 -0
  55. package/script/src/utils/gif_encoder.d.ts +3 -1
  56. package/script/src/utils/gif_encoder.js +4 -2
  57. package/script/src/utils/image_processing.d.ts +31 -0
  58. package/script/src/utils/image_processing.js +92 -0
  59. package/script/src/utils/jpeg_decoder.d.ts +25 -2
  60. package/script/src/utils/jpeg_decoder.js +101 -10
  61. package/script/src/utils/jpeg_encoder.d.ts +19 -0
  62. 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, _options?: unknown): Promise<Uint8Array>;
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, _options?: unknown): Promise<Uint8Array>;
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, _options) {
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, _options) {
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, _options?: unknown): Promise<Uint8Array>;
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, _options) {
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, _options?: unknown): 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
@@ -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, _options) {
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
- protected filterData(data: Uint8Array, width: number, height: number): Uint8Array;
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
- filterData(data, width, height) {
148
- // Use filter type 0 (None) for simplicity
149
- const filtered = new Uint8Array(height * (1 + width * 4));
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
- for (let x = 0; x < width * 4; x++) {
154
- filtered[pos++] = data[y * width * 4 + x];
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
  */