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
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @cross/image
2
2
 
3
- A pure JavaScript, dependency-free, cross-runtime image processing library for Deno, Node.js, and
4
- Bun. Decode, encode, manipulate, and process images in multiple formats including PNG, JPEG, WebP,
5
- GIF, and more—all without native dependencies.
3
+ A pure JavaScript, dependency-free, cross-runtime image processing library for Deno, Node.js, Bun
4
+ and browsers. Decode, encode, manipulate, and process images in multiple formats including PNG,
5
+ JPEG, WebP, GIF, and more—all without native dependencies.
6
6
 
7
7
  📚 **[Full Documentation](https://cross-image.56k.guru/)**
8
8
 
@@ -10,7 +10,7 @@ GIF, and more—all without native dependencies.
10
10
 
11
11
  - 🚀 **Pure JavaScript** - No native dependencies
12
12
  - 🔌 **Pluggable formats** - Easy to extend with custom formats
13
- - 📦 **Cross-runtime** - Works on Deno, Node.js (18+), and Bun
13
+ - 📦 **Cross-runtime** - Works on Deno, Node.js (18+), Bun and Browsers.
14
14
  - 🎨 **Multiple formats** - PNG, APNG, JPEG, WebP, GIF, TIFF, BMP, ICO, DNG, PAM, PPM, PCX, ASCII,
15
15
  HEIC, and AVIF support
16
16
  - ✂️ **Image manipulation** - Resize, crop, composite, and more
@@ -196,6 +196,185 @@ const imageWithWarnings = await Image.decode(data, {
196
196
  ```
197
197
 
198
198
  **Note:** When using `Image.decode()`, the library automatically tries runtime-optimized decoders
199
+
200
+ ## CMYK Color Space Support
201
+
202
+ The library provides utilities for working with CMYK (Cyan, Magenta, Yellow, Key/Black) color space,
203
+ commonly used in professional printing and color manipulation.
204
+
205
+ ### Color Conversion Utilities
206
+
207
+ ```ts
208
+ import { cmykToRgb, rgbToCmyk } from "jsr:@cross/image";
209
+
210
+ // Convert RGB to CMYK
211
+ const [c, m, y, k] = rgbToCmyk(255, 0, 0); // Red
212
+ console.log({ c, m, y, k }); // { c: 0, m: 1, y: 1, k: 0 }
213
+
214
+ // Convert CMYK back to RGB
215
+ const [r, g, b] = cmykToRgb(c, m, y, k);
216
+ console.log({ r, g, b }); // { r: 255, g: 0, b: 0 }
217
+ ```
218
+
219
+ ### Image-Level CMYK Operations
220
+
221
+ ```ts
222
+ import { Image } from "jsr:@cross/image";
223
+
224
+ // Load an image and convert to CMYK
225
+ const data = await Deno.readFile("photo.jpg");
226
+ const image = await Image.decode(data);
227
+
228
+ // Get CMYK representation (Float32Array with 4 values per pixel)
229
+ const cmykData = image.toCMYK();
230
+
231
+ // Create an image from CMYK data
232
+ const restored = Image.fromCMYK(cmykData, image.width, image.height);
233
+
234
+ // Save the restored image
235
+ await Deno.writeFile("output.png", await restored.encode("png"));
236
+ ```
237
+
238
+ ### Batch Conversion
239
+
240
+ ```ts
241
+ import { cmykToRgba, rgbaToCmyk } from "jsr:@cross/image";
242
+
243
+ // Convert entire image data to CMYK
244
+ const rgbaData = new Uint8Array([255, 0, 0, 255]); // Red pixel
245
+ const cmykData = rgbaToCmyk(rgbaData);
246
+
247
+ // Convert CMYK data back to RGBA
248
+ const rgbaRestored = cmykToRgba(cmykData);
249
+ ```
250
+
251
+ **Use Cases:**
252
+
253
+ - Pre-press and print preparation workflows
254
+ - Color space conversion for professional printing
255
+ - Color analysis and manipulation in CMYK space
256
+ - Educational tools for understanding color models
257
+
258
+ ### CMYK TIFF Support
259
+
260
+ TIFF format has native support for CMYK images:
261
+
262
+ ```ts
263
+ import { Image } from "jsr:@cross/image";
264
+
265
+ // Decode CMYK TIFF (automatically converted to RGBA)
266
+ const cmykTiff = await Deno.readFile("cmyk-image.tif");
267
+ const image = await Image.decode(cmykTiff); // Automatic CMYK → RGBA conversion
268
+
269
+ // Encode image as CMYK TIFF
270
+ const output = await image.encode("tiff", { cmyk: true });
271
+ await Deno.writeFile("output-cmyk.tif", output);
272
+
273
+ // CMYK works with all TIFF compression methods
274
+ const compressed = await image.encode("tiff", {
275
+ cmyk: true,
276
+ compression: "lzw", // or "packbits", "deflate", "none"
277
+ });
278
+ ```
279
+
280
+ **Benefits:**
281
+
282
+ - **Seamless workflow**: CMYK TIFFs decode automatically, no special handling needed
283
+ - **Print-ready output**: Generate CMYK TIFFs for professional printing workflows
284
+ - **Full compression support**: CMYK works with all TIFF compression methods
285
+ - **Industry standard**: TIFF is the preferred format for CMYK images in print production
286
+
287
+ ## JPEG Coefficient Extraction
288
+
289
+ The library provides advanced APIs for extracting and encoding JPEG quantized DCT coefficients,
290
+ enabling coefficient-domain steganography and other frequency-domain processing techniques.
291
+
292
+ ### Extracting Coefficients
293
+
294
+ ```typescript
295
+ import { Image } from "jsr:@cross/image";
296
+
297
+ const data = await Deno.readFile("photo.jpg");
298
+
299
+ // Extract quantized DCT coefficients
300
+ const coefficients = await Image.extractCoefficients(data, "jpeg");
301
+
302
+ if (coefficients) {
303
+ console.log(`Image: ${coefficients.width}x${coefficients.height}`);
304
+ console.log(`Progressive: ${coefficients.isProgressive}`);
305
+ console.log(`Components: ${coefficients.components.length}`);
306
+
307
+ // Access coefficient blocks (Y, Cb, Cr components)
308
+ for (const comp of coefficients.components) {
309
+ console.log(`Component ${comp.id}: ${comp.blocks.length} block rows`);
310
+ }
311
+ }
312
+ ```
313
+
314
+ ### Modifying and Re-encoding Coefficients
315
+
316
+ ```typescript
317
+ import { Image } from "jsr:@cross/image";
318
+
319
+ const data = await Deno.readFile("input.jpg");
320
+ const coefficients = await Image.extractCoefficients(data, "jpeg");
321
+
322
+ if (coefficients) {
323
+ // Modify coefficients (e.g., for steganography)
324
+ for (const comp of coefficients.components) {
325
+ for (const row of comp.blocks) {
326
+ for (const block of row) {
327
+ // Modify AC coefficients (indices 1-63)
328
+ // DC coefficient is at index 0
329
+ // block is an Int32Array of 64 quantized DCT values in zigzag order
330
+ }
331
+ }
332
+ }
333
+
334
+ // Re-encode to JPEG
335
+ const encoded = await Image.encodeFromCoefficients(coefficients, "jpeg");
336
+ await Deno.writeFile("output.jpg", encoded);
337
+ }
338
+ ```
339
+
340
+ ### Coefficient Structure
341
+
342
+ The `JPEGQuantizedCoefficients` type provides:
343
+
344
+ - `width`, `height` - Image dimensions
345
+ - `isProgressive` - Whether the source was progressive JPEG
346
+ - `components` - Array of Y, Cb, Cr (or grayscale) component data
347
+ - `quantizationTables` - The quantization tables used
348
+ - `mcuWidth`, `mcuHeight` - MCU block dimensions
349
+
350
+ Each component contains `blocks[][]` where each block is an `Int32Array` of 64 quantized DCT
351
+ coefficients in zigzag order.
352
+
353
+ **Use Cases:**
354
+
355
+ - DCT-domain steganography (survives JPEG re-compression)
356
+ - Coefficient analysis and visualization
357
+ - Custom frequency-domain filtering
358
+ - Forensic analysis of JPEG compression artifacts
359
+
360
+ ## Base64 / Data URLs
361
+
362
+ The library includes small utilities for working with base64 and `data:` URLs.
363
+
364
+ ```ts
365
+ import { Image, parseDataUrl, toDataUrl } from "jsr:@cross/image";
366
+
367
+ const image = Image.create(2, 2, 255, 0, 0);
368
+ const pngBytes = await image.encode("png");
369
+
370
+ const dataUrl = toDataUrl("image/png", pngBytes);
371
+ const parsed = parseDataUrl(dataUrl);
372
+
373
+ // parsed.bytes is a Uint8Array containing the PNG
374
+ const roundtrip = await Image.decode(parsed.bytes, "png");
375
+ console.log(roundtrip.width, roundtrip.height);
376
+ ```
377
+
199
378
  (ImageDecoder API) first, falling back to the pure JS decoder with tolerant mode for maximum
200
379
  compatibility.
201
380
 
@@ -394,7 +573,7 @@ const jpegSupports = Image.getSupportedMetadata("jpeg");
394
573
  console.log(jpegSupports); // Includes ISO, camera info, GPS, etc.
395
574
 
396
575
  // Save with metadata
397
- const jpeg = await image.save("jpeg");
576
+ const jpeg = await image.encode("jpeg");
398
577
  await Deno.writeFile("output.jpg", jpeg);
399
578
 
400
579
  // Metadata is preserved on reload!
@@ -488,6 +667,8 @@ Image.getSupportedMetadata("avif"); // Full camera metadata + GPS (19 fields)
488
667
  Technical details for WebP
489
668
  - **[TIFF Implementation](https://cross-image.56k.guru/implementation/tiff-implementation/)** -
490
669
  Technical details for TIFF
670
+ - **[GIF Implementation](https://cross-image.56k.guru/implementation/gif-implementation/)** -
671
+ Technical details for GIF
491
672
 
492
673
  ## Development
493
674
 
package/esm/mod.d.ts CHANGED
@@ -43,7 +43,7 @@
43
43
  * ```
44
44
  */
45
45
  export { Image } from "./src/image.js";
46
- export type { ASCIIEncoderOptions, FrameMetadata, ImageData, ImageDecoderOptions, ImageFormat, ImageFrame, ImageMetadata, JPEGEncoderOptions, MultiFrameImageData, ResizeOptions, TIFFEncoderOptions, WebPEncoderOptions, } from "./src/types.js";
46
+ export type { APNGEncoderOptions, ASCIIEncoderOptions, AVIFEncoderOptions, CoefficientData, FrameMetadata, GIFEncoderOptions, HEICEncoderOptions, ImageData, ImageDecoderOptions, ImageFormat, ImageFrame, ImageMetadata, JPEGComponentCoefficients, JPEGEncoderOptions, JPEGQuantizedCoefficients, MultiFrameImageData, PNGEncoderOptions, ResizeOptions, TIFFEncoderOptions, WebPEncoderOptions, } from "./src/types.js";
47
47
  export { PNGFormat } from "./src/formats/png.js";
48
48
  export { APNGFormat } from "./src/formats/apng.js";
49
49
  export { JPEGFormat } from "./src/formats/jpeg.js";
@@ -59,4 +59,6 @@ export { PPMFormat } from "./src/formats/ppm.js";
59
59
  export { ASCIIFormat } from "./src/formats/ascii.js";
60
60
  export { HEICFormat } from "./src/formats/heic.js";
61
61
  export { AVIFFormat } from "./src/formats/avif.js";
62
+ export { decodeBase64, encodeBase64, parseDataUrl, toDataUrl } from "./src/utils/base64.js";
63
+ export { cmykToRgb, cmykToRgba, rgbaToCmyk, rgbToCmyk } from "./src/utils/image_processing.js";
62
64
  //# sourceMappingURL=mod.d.ts.map
package/esm/mod.js CHANGED
@@ -58,3 +58,5 @@ export { PPMFormat } from "./src/formats/ppm.js";
58
58
  export { ASCIIFormat } from "./src/formats/ascii.js";
59
59
  export { HEICFormat } from "./src/formats/heic.js";
60
60
  export { AVIFFormat } from "./src/formats/avif.js";
61
+ export { decodeBase64, encodeBase64, parseDataUrl, toDataUrl } from "./src/utils/base64.js";
62
+ export { cmykToRgb, cmykToRgba, rgbaToCmyk, rgbToCmyk } from "./src/utils/image_processing.js";
@@ -1,4 +1,4 @@
1
- import type { ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata, MultiFrameImageData } from "../types.js";
1
+ import type { APNGEncoderOptions, ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata, MultiFrameImageData } from "../types.js";
2
2
  import { PNGBase } from "./png_base.js";
3
3
  /**
4
4
  * APNG (Animated PNG) format handler
@@ -36,15 +36,17 @@ export declare class APNGFormat extends PNGBase implements ImageFormat {
36
36
  /**
37
37
  * Encode RGBA image data to APNG format (single frame)
38
38
  * @param imageData Image data to encode
39
+ * @param options Encoding options (compressionLevel 0-9, default 6)
39
40
  * @returns Encoded APNG image bytes
40
41
  */
41
- encode(imageData: ImageData, _options?: unknown): Promise<Uint8Array>;
42
+ encode(imageData: ImageData, options?: APNGEncoderOptions): Promise<Uint8Array>;
42
43
  /**
43
44
  * Encode multi-frame image data to APNG format
44
45
  * @param imageData Multi-frame image data to encode
46
+ * @param options Encoding options (compressionLevel 0-9, default 6)
45
47
  * @returns Encoded APNG image bytes
46
48
  */
47
- encodeFrames(imageData: MultiFrameImageData, _options?: unknown): Promise<Uint8Array>;
49
+ encodeFrames(imageData: MultiFrameImageData, options?: APNGEncoderOptions): Promise<Uint8Array>;
48
50
  private decodeFrameData;
49
51
  /**
50
52
  * Get the list of metadata fields supported by APNG format
@@ -250,9 +250,10 @@ export class APNGFormat extends PNGBase {
250
250
  /**
251
251
  * Encode RGBA image data to APNG format (single frame)
252
252
  * @param imageData Image data to encode
253
+ * @param options Encoding options (compressionLevel 0-9, default 6)
253
254
  * @returns Encoded APNG image bytes
254
255
  */
255
- encode(imageData, _options) {
256
+ encode(imageData, options) {
256
257
  // For single frame, create a multi-frame with one frame
257
258
  const multiFrame = {
258
259
  width: imageData.width,
@@ -265,15 +266,21 @@ export class APNGFormat extends PNGBase {
265
266
  }],
266
267
  metadata: imageData.metadata,
267
268
  };
268
- return this.encodeFrames(multiFrame, _options);
269
+ return this.encodeFrames(multiFrame, options);
269
270
  }
270
271
  /**
271
272
  * Encode multi-frame image data to APNG format
272
273
  * @param imageData Multi-frame image data to encode
274
+ * @param options Encoding options (compressionLevel 0-9, default 6)
273
275
  * @returns Encoded APNG image bytes
274
276
  */
275
- async encodeFrames(imageData, _options) {
277
+ async encodeFrames(imageData, options) {
276
278
  const { width, height, frames, metadata } = imageData;
279
+ const compressionLevel = options?.compressionLevel ?? 6;
280
+ // Validate compression level
281
+ if (compressionLevel < 0 || compressionLevel > 9) {
282
+ throw new Error("Compression level must be between 0 and 9");
283
+ }
277
284
  if (frames.length === 0) {
278
285
  throw new Error("No frames to encode");
279
286
  }
@@ -331,7 +338,7 @@ export class APNGFormat extends PNGBase {
331
338
  fctl[25] = 0; // blend_op: APNG_BLEND_OP_SOURCE
332
339
  chunks.push(this.createChunk("fcTL", fctl));
333
340
  // Filter and compress frame data
334
- const filtered = this.filterData(frame.data, frame.width, frame.height);
341
+ const filtered = this.filterData(frame.data, frame.width, frame.height, compressionLevel);
335
342
  const compressed = await this.deflate(filtered);
336
343
  if (i === 0) {
337
344
  // First frame uses IDAT
@@ -1,4 +1,4 @@
1
- import type { ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata } from "../types.js";
1
+ import type { AVIFEncoderOptions, ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata } from "../types.js";
2
2
  /**
3
3
  * AVIF format handler
4
4
  * Supports AVIF images using runtime APIs (ImageDecoder/OffscreenCanvas)
@@ -32,7 +32,7 @@ export declare class AVIFFormat implements ImageFormat {
32
32
  * @param imageData Image data to encode
33
33
  * @returns Encoded AVIF image bytes
34
34
  */
35
- encode(imageData: ImageData, _options?: unknown): Promise<Uint8Array>;
35
+ encode(imageData: ImageData, options?: AVIFEncoderOptions): Promise<Uint8Array>;
36
36
  /**
37
37
  * Decode using runtime APIs
38
38
  * @param data Raw AVIF data
@@ -79,8 +79,9 @@ export class AVIFFormat {
79
79
  * @param imageData Image data to encode
80
80
  * @returns Encoded AVIF image bytes
81
81
  */
82
- async encode(imageData, _options) {
82
+ async encode(imageData, options) {
83
83
  const { width, height, data, metadata: _metadata } = imageData;
84
+ const requestedQuality = options?.quality;
84
85
  // Try to use runtime encoding if available
85
86
  if (typeof OffscreenCanvas !== "undefined") {
86
87
  try {
@@ -91,10 +92,19 @@ export class AVIFFormat {
91
92
  const imgDataData = new Uint8ClampedArray(data);
92
93
  imgData.data.set(imgDataData);
93
94
  ctx.putImageData(imgData, 0, 0);
95
+ const quality = requestedQuality === undefined
96
+ ? undefined
97
+ : (requestedQuality <= 1
98
+ ? Math.max(0, Math.min(1, requestedQuality))
99
+ : Math.max(1, Math.min(100, requestedQuality)) / 100);
94
100
  // Try to encode as AVIF
95
101
  const blob = await canvas.convertToBlob({
96
102
  type: "image/avif",
103
+ ...(quality === undefined ? {} : { quality }),
97
104
  });
105
+ if (blob.type !== "image/avif") {
106
+ throw new Error(`Runtime did not encode AVIF (got '${blob.type || "(empty)"}')`);
107
+ }
98
108
  const arrayBuffer = await blob.arrayBuffer();
99
109
  const encoded = new Uint8Array(arrayBuffer);
100
110
  // Note: Metadata injection for AVIF is complex and would require
@@ -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;
@@ -153,12 +153,12 @@ export class GIFFormat {
153
153
  * @param imageData Image data to encode
154
154
  * @returns Encoded GIF image bytes
155
155
  */
156
- async encode(imageData, _options) {
156
+ async encode(imageData, options) {
157
157
  const { width, height, data, metadata } = imageData;
158
158
  // Try pure-JS encoder first
159
159
  try {
160
160
  const encoder = new GIFEncoder(width, height, data);
161
- const encoded = encoder.encode();
161
+ const encoded = encoder.encode(options);
162
162
  // Inject metadata if present
163
163
  if (metadata && Object.keys(metadata).length > 0) {
164
164
  const injected = this.injectMetadata(encoded, metadata);
@@ -237,7 +237,7 @@ export class GIFFormat {
237
237
  /**
238
238
  * Encode multi-frame image data to animated GIF
239
239
  */
240
- encodeFrames(imageData, _options) {
240
+ encodeFrames(imageData, options) {
241
241
  if (imageData.frames.length === 0) {
242
242
  throw new Error("No frames to encode");
243
243
  }
@@ -247,7 +247,7 @@ export class GIFFormat {
247
247
  const delay = frame.frameMetadata?.delay ?? 100;
248
248
  encoder.addFrame(frame.data, delay);
249
249
  }
250
- return Promise.resolve(encoder.encode());
250
+ return Promise.resolve(encoder.encode(options));
251
251
  }
252
252
  mapDisposalMethod(disposal) {
253
253
  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
@@ -80,8 +80,9 @@ export class HEICFormat {
80
80
  * @param imageData Image data to encode
81
81
  * @returns Encoded HEIC image bytes
82
82
  */
83
- async encode(imageData, _options) {
83
+ async encode(imageData, options) {
84
84
  const { width, height, data, metadata: _metadata } = imageData;
85
+ const requestedQuality = options?.quality;
85
86
  // Try to use runtime encoding if available
86
87
  if (typeof OffscreenCanvas !== "undefined") {
87
88
  try {
@@ -92,10 +93,19 @@ export class HEICFormat {
92
93
  const imgDataData = new Uint8ClampedArray(data);
93
94
  imgData.data.set(imgDataData);
94
95
  ctx.putImageData(imgData, 0, 0);
96
+ const quality = requestedQuality === undefined
97
+ ? undefined
98
+ : (requestedQuality <= 1
99
+ ? Math.max(0, Math.min(1, requestedQuality))
100
+ : Math.max(1, Math.min(100, requestedQuality)) / 100);
95
101
  // Try to encode as HEIC
96
102
  const blob = await canvas.convertToBlob({
97
103
  type: "image/heic",
104
+ ...(quality === undefined ? {} : { quality }),
98
105
  });
106
+ if (blob.type !== "image/heic") {
107
+ throw new Error(`Runtime did not encode HEIC (got '${blob.type || "(empty)"}')`);
108
+ }
99
109
  const arrayBuffer = await blob.arrayBuffer();
100
110
  const encoded = new Uint8Array(arrayBuffer);
101
111
  // 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
@@ -1072,6 +1072,65 @@ export class JPEGFormat {
1072
1072
  "dpiY",
1073
1073
  ];
1074
1074
  }
1075
+ /**
1076
+ * Extract quantized DCT coefficients from JPEG data
1077
+ * These coefficients can be modified for steganography and re-encoded
1078
+ * @param data Raw JPEG data
1079
+ * @param options Decoder options
1080
+ * @returns JPEGQuantizedCoefficients or undefined if extraction fails
1081
+ */
1082
+ async extractCoefficients(data, options) {
1083
+ if (!this.canDecode(data)) {
1084
+ return undefined;
1085
+ }
1086
+ try {
1087
+ // Force pure-JS decoding since runtime decoders don't expose coefficients
1088
+ const { JPEGDecoder } = await import("../utils/jpeg_decoder.js");
1089
+ const decoder = new JPEGDecoder(data, {
1090
+ tolerantDecoding: options?.tolerantDecoding ?? true,
1091
+ onWarning: options?.onWarning,
1092
+ extractCoefficients: true,
1093
+ });
1094
+ // Decode to extract coefficients
1095
+ decoder.decode();
1096
+ // Get the quantized coefficients
1097
+ return decoder.getQuantizedCoefficients();
1098
+ }
1099
+ catch (error) {
1100
+ if (options?.onWarning) {
1101
+ options.onWarning(`Failed to extract JPEG coefficients: ${error}`, error);
1102
+ }
1103
+ return undefined;
1104
+ }
1105
+ }
1106
+ /**
1107
+ * Type guard to check if coefficient data is JPEG format
1108
+ */
1109
+ isJPEGCoefficients(coeffs) {
1110
+ return ("format" in coeffs &&
1111
+ coeffs.format === "jpeg" &&
1112
+ "components" in coeffs &&
1113
+ "quantizationTables" in coeffs &&
1114
+ "isProgressive" in coeffs);
1115
+ }
1116
+ /**
1117
+ * Encode JPEG from quantized DCT coefficients
1118
+ * Useful for steganography - modify coefficients and re-encode
1119
+ * @param coeffs JPEG quantized coefficients
1120
+ * @param options Encoding options
1121
+ * @returns Encoded JPEG bytes
1122
+ */
1123
+ async encodeFromCoefficients(coeffs, options) {
1124
+ if (!this.isJPEGCoefficients(coeffs)) {
1125
+ throw new Error("Invalid coefficient format for JPEG");
1126
+ }
1127
+ const { JPEGEncoder } = await import("../utils/jpeg_encoder.js");
1128
+ const encoder = new JPEGEncoder({
1129
+ quality: options?.quality,
1130
+ progressive: options?.progressive ?? coeffs.isProgressive,
1131
+ });
1132
+ return encoder.encodeFromCoefficients(coeffs, options);
1133
+ }
1075
1134
  /**
1076
1135
  * Extract metadata from JPEG data without fully decoding the pixel data
1077
1136
  * 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
@@ -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, _options) {
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
- 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
  */