cross-image 0.4.1 → 0.4.3

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.
@@ -3,11 +3,20 @@
3
3
  * Supports GIF89a format with LZW compression
4
4
  */
5
5
  export declare class GIFEncoder {
6
- private width;
7
- private height;
6
+ private canvasWidth;
7
+ private canvasHeight;
8
8
  private frames;
9
9
  constructor(width: number, height: number, data?: Uint8Array);
10
- addFrame(data: Uint8Array, delay?: number): void;
10
+ /**
11
+ * Add a frame with optional metadata for partial frame support
12
+ */
13
+ addFrame(data: Uint8Array, delay?: number, options?: {
14
+ left?: number;
15
+ top?: number;
16
+ width?: number;
17
+ height?: number;
18
+ disposal?: number;
19
+ }): void;
11
20
  private writeBytes;
12
21
  private writeUint16LE;
13
22
  private writeString;
@@ -20,7 +29,7 @@ export declare class GIFEncoder {
20
29
  */
21
30
  private quantizeChannel;
22
31
  /**
23
- * Quantize RGBA image to 256 colors using median cut algorithm
32
+ * Quantize RGBA image to 256 colors with transparency support
24
33
  */
25
34
  private quantize;
26
35
  private nextPowerOf2;
@@ -8,13 +8,13 @@ exports.GIFEncoder = void 0;
8
8
  const lzw_js_1 = require("./lzw.js");
9
9
  class GIFEncoder {
10
10
  constructor(width, height, data) {
11
- Object.defineProperty(this, "width", {
11
+ Object.defineProperty(this, "canvasWidth", {
12
12
  enumerable: true,
13
13
  configurable: true,
14
14
  writable: true,
15
15
  value: void 0
16
16
  });
17
- Object.defineProperty(this, "height", {
17
+ Object.defineProperty(this, "canvasHeight", {
18
18
  enumerable: true,
19
19
  configurable: true,
20
20
  writable: true,
@@ -26,14 +26,28 @@ class GIFEncoder {
26
26
  writable: true,
27
27
  value: []
28
28
  });
29
- this.width = width;
30
- this.height = height;
29
+ this.canvasWidth = width;
30
+ this.canvasHeight = height;
31
31
  if (data) {
32
32
  this.addFrame(data);
33
33
  }
34
34
  }
35
- addFrame(data, delay = 0) {
36
- this.frames.push({ data, delay });
35
+ /**
36
+ * Add a frame with optional metadata for partial frame support
37
+ */
38
+ addFrame(data, delay = 0, options) {
39
+ const pixelCount = data.length / 4;
40
+ const frameWidth = options?.width ?? this.canvasWidth;
41
+ const frameHeight = options?.height ?? Math.ceil(pixelCount / frameWidth);
42
+ this.frames.push({
43
+ data,
44
+ delay,
45
+ left: options?.left ?? 0,
46
+ top: options?.top ?? 0,
47
+ width: frameWidth,
48
+ height: frameHeight,
49
+ disposal: options?.disposal ?? 0,
50
+ });
37
51
  }
38
52
  writeBytes(output, bytes) {
39
53
  output.push(...bytes);
@@ -60,49 +74,59 @@ class GIFEncoder {
60
74
  return Math.round(Math.round(value * levels / 255) * step);
61
75
  }
62
76
  /**
63
- * Quantize RGBA image to 256 colors using median cut algorithm
77
+ * Quantize RGBA image to 256 colors with transparency support
64
78
  */
65
- quantize(data) {
66
- // Simple quantization: collect unique colors and build palette
79
+ quantize(data, frameWidth, frameHeight) {
80
+ // Check for transparent pixels (alpha < 128)
81
+ let hasTransparency = false;
82
+ for (let i = 3; i < data.length; i += 4) {
83
+ if (data[i] < 128) {
84
+ hasTransparency = true;
85
+ break;
86
+ }
87
+ }
67
88
  const colorMap = new Map();
68
89
  const colors = [];
69
- // Color quantization parameters for 8-bit palette (256 colors)
70
- // R/G: 8 levels (0-7) using 3 bits, B: 4 levels (0-3) using 2 bits (8*8*4=256)
71
- const RG_LEVELS = 7; // Max level for R/G (8 total levels: 0-7)
72
- const B_LEVELS = 3; // Max level for B (4 total levels: 0-3)
73
- const rgStep = 255 / RG_LEVELS; // Step size for R/G quantization
74
- const bStep = 255 / B_LEVELS; // Step size for B quantization
75
- // Collect unique colors
90
+ const transparentIndex = 0;
91
+ // Reserve index 0 for transparency if needed
92
+ if (hasTransparency) {
93
+ colors.push({ r: 0, g: 0, b: 0 });
94
+ }
95
+ // Color quantization parameters
96
+ const RG_LEVELS = 7;
97
+ const B_LEVELS = 3;
98
+ const rgStep = 255 / RG_LEVELS;
99
+ const bStep = 255 / B_LEVELS;
100
+ const maxColors = hasTransparency ? 255 : 256;
101
+ // Collect unique colors from opaque pixels only
76
102
  for (let i = 0; i < data.length; i += 4) {
103
+ if (data[i + 3] < 128)
104
+ continue; // Skip transparent
77
105
  const r = data[i];
78
106
  const g = data[i + 1];
79
107
  const b = data[i + 2];
80
108
  const key = `${r},${g},${b}`;
81
- if (!colorMap.has(key) && colors.length < 256) {
109
+ if (!colorMap.has(key) && colors.length < maxColors) {
82
110
  colorMap.set(key, colors.length);
83
111
  colors.push({ r, g, b });
84
112
  }
85
113
  }
86
- // Track if color reduction was applied
87
114
  let useColorReduction = false;
88
- // If we have too many colors, use simple color reduction
89
- if (colors.length >= 256) {
90
- // Downsample colors to 256 by reducing color depth
115
+ // If too many colors, reduce color depth
116
+ if (colors.length >= maxColors) {
91
117
  colorMap.clear();
92
- colors.length = 0;
118
+ colors.length = hasTransparency ? 1 : 0;
93
119
  useColorReduction = true;
94
120
  for (let i = 0; i < data.length; i += 4) {
95
- // Reduce color depth: 3 bits for R/G channels, 2 bits for B channel
96
- // This gives us 8 bits total = 256 possible colors
121
+ if (data[i + 3] < 128)
122
+ continue;
97
123
  const r = this.quantizeChannel(data[i], RG_LEVELS, rgStep);
98
124
  const g = this.quantizeChannel(data[i + 1], RG_LEVELS, rgStep);
99
125
  const b = this.quantizeChannel(data[i + 2], B_LEVELS, bStep);
100
126
  const key = `${r},${g},${b}`;
101
- if (!colorMap.has(key)) {
102
- if (colors.length < 256) {
103
- colorMap.set(key, colors.length);
104
- colors.push({ r, g, b });
105
- }
127
+ if (!colorMap.has(key) && colors.length < maxColors) {
128
+ colorMap.set(key, colors.length);
129
+ colors.push({ r, g, b });
106
130
  }
107
131
  }
108
132
  }
@@ -118,28 +142,31 @@ class GIFEncoder {
118
142
  palette[i * 3 + 1] = colors[i].g;
119
143
  palette[i * 3 + 2] = colors[i].b;
120
144
  }
121
- // Create indexed data
122
- const indexed = new Uint8Array(this.width * this.height);
123
- for (let i = 0, j = 0; i < data.length; i += 4, j++) {
145
+ // Create indexed data using frame dimensions
146
+ const indexed = new Uint8Array(frameWidth * frameHeight);
147
+ for (let i = 0, j = 0; i < data.length && j < indexed.length; i += 4, j++) {
148
+ // Handle transparent pixels
149
+ if (data[i + 3] < 128) {
150
+ indexed[j] = transparentIndex;
151
+ continue;
152
+ }
124
153
  let r = data[i];
125
154
  let g = data[i + 1];
126
155
  let b = data[i + 2];
127
- // Apply color reduction if it was used for building the palette
128
156
  if (useColorReduction) {
129
157
  r = this.quantizeChannel(r, RG_LEVELS, rgStep);
130
158
  g = this.quantizeChannel(g, RG_LEVELS, rgStep);
131
159
  b = this.quantizeChannel(b, B_LEVELS, bStep);
132
160
  }
133
161
  const key = `${r},${g},${b}`;
134
- // Try fast O(1) lookup first
135
162
  if (colorMap.has(key)) {
136
163
  indexed[j] = colorMap.get(key);
137
164
  }
138
165
  else {
139
- // Fallback: find closest color in palette (shouldn't happen often)
166
+ // Find closest color
140
167
  let minDist = Infinity;
141
- let bestIdx = 0;
142
- for (let k = 0; k < colors.length; k++) {
168
+ let bestIdx = hasTransparency ? 1 : 0;
169
+ for (let k = hasTransparency ? 1 : 0; k < colors.length; k++) {
143
170
  const dr = r - colors[k].r;
144
171
  const dg = g - colors[k].g;
145
172
  const db = b - colors[k].b;
@@ -152,7 +179,7 @@ class GIFEncoder {
152
179
  indexed[j] = bestIdx;
153
180
  }
154
181
  }
155
- return { palette, indexed };
182
+ return { palette, indexed, hasTransparency, transparentIndex };
156
183
  }
157
184
  nextPowerOf2(n) {
158
185
  let power = 1;
@@ -173,46 +200,34 @@ class GIFEncoder {
173
200
  throw new Error("No frames to encode");
174
201
  }
175
202
  const output = [];
176
- // Get loop count from options (default to 0 = infinite)
177
203
  const loopCount = options?.loop ?? 0;
178
204
  // Quantize first frame for Global Color Table
179
205
  const firstFrame = this.frames[0];
180
- const { palette: globalPalette, indexed: firstIndexed } = this.quantize(firstFrame.data);
206
+ const firstResult = this.quantize(firstFrame.data, firstFrame.width, firstFrame.height);
207
+ const globalPalette = firstResult.palette;
181
208
  const paletteSize = globalPalette.length / 3;
182
209
  const bitsPerColor = this.getBitsPerColor(paletteSize);
183
210
  // Header
184
211
  this.writeString(output, "GIF89a");
185
212
  // Logical Screen Descriptor
186
- this.writeUint16LE(output, this.width);
187
- this.writeUint16LE(output, this.height);
188
- // Packed field:
189
- // - Global Color Table Flag (1 bit): 1
190
- // - Color Resolution (3 bits): bitsPerColor - 1
191
- // - Sort Flag (1 bit): 0
192
- // - Size of Global Color Table (3 bits): bitsPerColor - 1
213
+ this.writeUint16LE(output, this.canvasWidth);
214
+ this.writeUint16LE(output, this.canvasHeight);
193
215
  const packed = 0x80 | ((bitsPerColor - 1) << 4) | (bitsPerColor - 1);
194
216
  output.push(packed);
195
- // Background Color Index
196
- output.push(0);
197
- // Pixel Aspect Ratio
198
- output.push(0);
217
+ output.push(0); // Background Color Index
218
+ output.push(0); // Pixel Aspect Ratio
199
219
  // Global Color Table
200
- // The GCT size is 2^(n+1) where n is the value in the packed field
201
- // So we need to write that many colors, padding if necessary
202
220
  const gctSize = 1 << bitsPerColor;
203
221
  const paddedPalette = new Uint8Array(gctSize * 3);
204
222
  paddedPalette.set(globalPalette);
205
223
  this.writeBytes(output, paddedPalette);
206
224
  // Netscape Application Extension (Looping)
207
225
  if (this.frames.length > 1) {
208
- output.push(0x21); // Extension Introducer
209
- output.push(0xff); // Application Extension Label
210
- output.push(11); // Block Size
226
+ output.push(0x21, 0xff, 11);
211
227
  this.writeString(output, "NETSCAPE2.0");
212
- output.push(3); // Sub-block Size
213
- output.push(1); // Loop Indicator (1 = loop)
214
- this.writeUint16LE(output, loopCount); // Loop Count (0 = infinite, 1+ = specific count)
215
- output.push(0); // Block Terminator
228
+ output.push(3, 1);
229
+ this.writeUint16LE(output, loopCount);
230
+ output.push(0);
216
231
  }
217
232
  // Encode frames
218
233
  for (let i = 0; i < this.frames.length; i++) {
@@ -221,67 +236,56 @@ class GIFEncoder {
221
236
  let useLocalPalette = false;
222
237
  let localPalette = null;
223
238
  let localBitsPerColor = bitsPerColor;
239
+ let hasTransparency;
240
+ let transparentIndex;
224
241
  if (i === 0) {
225
- indexed = firstIndexed;
242
+ indexed = firstResult.indexed;
243
+ hasTransparency = firstResult.hasTransparency;
244
+ transparentIndex = firstResult.transparentIndex;
226
245
  }
227
246
  else {
228
- // Quantize subsequent frames
229
- // For simplicity, we use a Local Color Table for each frame to ensure colors are correct
230
- const result = this.quantize(frame.data);
247
+ const result = this.quantize(frame.data, frame.width, frame.height);
231
248
  indexed = result.indexed;
232
249
  localPalette = result.palette;
233
250
  useLocalPalette = true;
234
- const localPaletteSize = localPalette.length / 3;
235
- localBitsPerColor = this.getBitsPerColor(localPaletteSize);
251
+ hasTransparency = result.hasTransparency;
252
+ transparentIndex = result.transparentIndex;
253
+ localBitsPerColor = this.getBitsPerColor(localPalette.length / 3);
236
254
  }
237
255
  // Graphic Control Extension
238
- output.push(0x21); // Extension Introducer
239
- output.push(0xf9); // Graphic Control Label
240
- output.push(4); // Byte Size
241
- // Packed Field
242
- // Reserved (3 bits)
243
- // Disposal Method (3 bits): 2 (Restore to background) - usually safe for animation
244
- // User Input Flag (1 bit): 0
245
- // Transparent Color Flag (1 bit): 0
246
- output.push(0x08); // Disposal method 2 (Restore to background)
247
- // Delay Time (1/100ths of a second)
248
- // Default to 10 (100ms) if not specified
256
+ output.push(0x21, 0xf9, 4);
257
+ // Packed: disposal method + transparency flag
258
+ const disposal = frame.disposal & 0x07;
259
+ const gcePacked = (disposal << 2) | (hasTransparency ? 1 : 0);
260
+ output.push(gcePacked);
261
+ // Delay time
249
262
  const delay = frame.delay > 0 ? Math.round(frame.delay / 10) : 10;
250
263
  this.writeUint16LE(output, delay);
251
- // Transparent Color Index
252
- output.push(0);
264
+ // Transparent color index
265
+ output.push(hasTransparency ? transparentIndex : 0);
253
266
  output.push(0); // Block Terminator
254
267
  // Image Descriptor
255
- output.push(0x2c); // Image Separator
256
- this.writeUint16LE(output, 0); // Left
257
- this.writeUint16LE(output, 0); // Top
258
- this.writeUint16LE(output, this.width);
259
- this.writeUint16LE(output, this.height);
260
- // Packed Field
268
+ output.push(0x2c);
269
+ this.writeUint16LE(output, frame.left);
270
+ this.writeUint16LE(output, frame.top);
271
+ this.writeUint16LE(output, frame.width);
272
+ this.writeUint16LE(output, frame.height);
261
273
  if (useLocalPalette && localPalette) {
262
- // LCT Flag: 1
263
- // Interlace: 0
264
- // Sort: 0
265
- // Reserved: 0
266
- // Size of LCT: localBitsPerColor - 1
267
274
  const lctPacked = 0x80 | (localBitsPerColor - 1);
268
275
  output.push(lctPacked);
269
- // Write Local Color Table
270
276
  const lctSize = 1 << localBitsPerColor;
271
277
  const paddedLct = new Uint8Array(lctSize * 3);
272
278
  paddedLct.set(localPalette);
273
279
  this.writeBytes(output, paddedLct);
274
280
  }
275
281
  else {
276
- output.push(0); // No LCT
282
+ output.push(0);
277
283
  }
278
- // LZW Minimum Code Size
284
+ // LZW compression
279
285
  const minCodeSize = Math.max(2, useLocalPalette ? localBitsPerColor : bitsPerColor);
280
286
  output.push(minCodeSize);
281
- // Compress image data with LZW
282
287
  const encoder = new lzw_js_1.LZWEncoder(minCodeSize);
283
288
  const compressed = encoder.compress(indexed);
284
- // Write compressed data in sub-blocks (max 255 bytes per block)
285
289
  for (let k = 0; k < compressed.length; k += 255) {
286
290
  const blockSize = Math.min(255, compressed.length - k);
287
291
  output.push(blockSize);
@@ -291,8 +295,7 @@ class GIFEncoder {
291
295
  }
292
296
  output.push(0); // Block Terminator
293
297
  }
294
- // Trailer
295
- output.push(0x3b);
298
+ output.push(0x3b); // Trailer
296
299
  return new Uint8Array(output);
297
300
  }
298
301
  }
@@ -8,7 +8,17 @@
8
8
  * This is a pure JavaScript implementation that handles common JPEG files.
9
9
  * For complex or non-standard JPEGs, the ImageDecoder API fallback is preferred.
10
10
  */
11
- import type { ImageDecoderOptions } from "../types.js";
11
+ import type { ImageDecoderOptions, JPEGQuantizedCoefficients } from "../types.js";
12
+ /**
13
+ * Extended decoder options including coefficient extraction
14
+ */
15
+ interface JPEGDecoderOptions extends ImageDecoderOptions {
16
+ /**
17
+ * When true, stores quantized DCT coefficients for later retrieval
18
+ * via getQuantizedCoefficients(). Coefficients are stored in zigzag order.
19
+ */
20
+ extractCoefficients?: boolean;
21
+ }
12
22
  export declare class JPEGDecoder {
13
23
  private data;
14
24
  private pos;
@@ -29,8 +39,20 @@ export declare class JPEGDecoder {
29
39
  private successiveLow;
30
40
  private scanComponentIds;
31
41
  private eobRun;
32
- constructor(data: Uint8Array, settings?: ImageDecoderOptions);
42
+ private quantizedCoefficients;
43
+ constructor(data: Uint8Array, settings?: JPEGDecoderOptions);
33
44
  decode(): Uint8Array;
45
+ /**
46
+ * Get the quantized DCT coefficients after decoding
47
+ * Only available if extractCoefficients option was set to true
48
+ * @returns JPEGQuantizedCoefficients or undefined if not available
49
+ */
50
+ getQuantizedCoefficients(): JPEGQuantizedCoefficients | undefined;
51
+ /**
52
+ * Store quantized coefficients in the output structure
53
+ * Called after decoding when extractCoefficients is true
54
+ */
55
+ private storeQuantizedCoefficients;
34
56
  private readMarker;
35
57
  private readUint16;
36
58
  private skipSegment;
@@ -49,4 +71,5 @@ export declare class JPEGDecoder {
49
71
  private idct;
50
72
  private convertToRGB;
51
73
  }
74
+ export {};
52
75
  //# sourceMappingURL=jpeg_decoder.d.ts.map
@@ -214,10 +214,18 @@ class JPEGDecoder {
214
214
  writable: true,
215
215
  value: 0
216
216
  }); // Remaining blocks to skip due to EOBn
217
+ // Storage for quantized coefficients (when extractCoefficients is true)
218
+ Object.defineProperty(this, "quantizedCoefficients", {
219
+ enumerable: true,
220
+ configurable: true,
221
+ writable: true,
222
+ value: null
223
+ });
217
224
  this.data = data;
218
225
  this.options = {
219
226
  tolerantDecoding: settings.tolerantDecoding ?? true,
220
227
  onWarning: settings.onWarning,
228
+ extractCoefficients: settings.extractCoefficients ?? false,
221
229
  };
222
230
  }
223
231
  decode() {
@@ -279,7 +287,8 @@ class JPEGDecoder {
279
287
  // For progressive JPEGs, perform IDCT on all blocks after all scans are complete
280
288
  // This ensures that frequency-domain coefficients from multiple scans are properly
281
289
  // accumulated before transformation to spatial domain
282
- if (this.isProgressive) {
290
+ // Skip IDCT when extracting coefficients - we want the quantized DCT values
291
+ if (this.isProgressive && !this.options.extractCoefficients) {
283
292
  for (const component of this.components) {
284
293
  if (component.blocks) {
285
294
  for (const row of component.blocks) {
@@ -290,9 +299,63 @@ class JPEGDecoder {
290
299
  }
291
300
  }
292
301
  }
302
+ // If extracting coefficients, store them before converting to RGB
303
+ if (this.options.extractCoefficients) {
304
+ this.storeQuantizedCoefficients();
305
+ }
293
306
  // Convert YCbCr to RGB
294
307
  return this.convertToRGB();
295
308
  }
309
+ /**
310
+ * Get the quantized DCT coefficients after decoding
311
+ * Only available if extractCoefficients option was set to true
312
+ * @returns JPEGQuantizedCoefficients or undefined if not available
313
+ */
314
+ getQuantizedCoefficients() {
315
+ return this.quantizedCoefficients ?? undefined;
316
+ }
317
+ /**
318
+ * Store quantized coefficients in the output structure
319
+ * Called after decoding when extractCoefficients is true
320
+ */
321
+ storeQuantizedCoefficients() {
322
+ // Calculate MCU dimensions
323
+ const maxH = Math.max(...this.components.map((c) => c.h));
324
+ const maxV = Math.max(...this.components.map((c) => c.v));
325
+ const mcuWidth = Math.ceil(this.width / (8 * maxH));
326
+ const mcuHeight = Math.ceil(this.height / (8 * maxV));
327
+ // Build component coefficients
328
+ const componentCoeffs = this.components.map((comp) => ({
329
+ id: comp.id,
330
+ h: comp.h,
331
+ v: comp.v,
332
+ qTable: comp.qTable,
333
+ blocks: comp.blocks.map((row) => row.map((block) => {
334
+ // Convert to Int32Array if not already
335
+ if (block instanceof Int32Array) {
336
+ return block;
337
+ }
338
+ return new Int32Array(block);
339
+ })),
340
+ }));
341
+ // Copy quantization tables
342
+ const qTables = this.qTables.map((table) => {
343
+ if (table instanceof Uint8Array) {
344
+ return new Uint8Array(table);
345
+ }
346
+ return new Uint8Array(table);
347
+ });
348
+ this.quantizedCoefficients = {
349
+ format: "jpeg",
350
+ width: this.width,
351
+ height: this.height,
352
+ isProgressive: this.isProgressive,
353
+ components: componentCoeffs,
354
+ quantizationTables: qTables,
355
+ mcuWidth,
356
+ mcuHeight,
357
+ };
358
+ }
296
359
  readMarker() {
297
360
  while (this.pos < this.data.length && this.data[this.pos] !== 0xFF) {
298
361
  this.pos++;
@@ -593,7 +656,13 @@ class JPEGDecoder {
593
656
  component.pred += dcDiff;
594
657
  // For successive approximation, shift the coefficient left by Al bits
595
658
  const coeff = component.pred << this.successiveLow;
596
- block[0] = coeff * this.qTables[component.qTable][0];
659
+ // When extracting coefficients, store quantized value without dequantization
660
+ if (this.options.extractCoefficients) {
661
+ block[0] = coeff;
662
+ }
663
+ else {
664
+ block[0] = coeff * this.qTables[component.qTable][0];
665
+ }
597
666
  }
598
667
  else {
599
668
  // DC refinement scan: add a refinement bit
@@ -601,7 +670,12 @@ class JPEGDecoder {
601
670
  if (bit) {
602
671
  // Add the refinement bit at position Al
603
672
  const refinement = 1 << this.successiveLow;
604
- block[0] += refinement * this.qTables[component.qTable][0];
673
+ if (this.options.extractCoefficients) {
674
+ block[0] += refinement;
675
+ }
676
+ else {
677
+ block[0] += refinement * this.qTables[component.qTable][0];
678
+ }
605
679
  }
606
680
  }
607
681
  }
@@ -648,8 +722,14 @@ class JPEGDecoder {
648
722
  break;
649
723
  // For successive approximation, shift the coefficient left by Al bits
650
724
  const coeff = this.receiveBits(s) << this.successiveLow;
651
- block[ZIGZAG[k]] = coeff *
652
- this.qTables[component.qTable][ZIGZAG[k]];
725
+ // When extracting coefficients, store quantized value without dequantization
726
+ if (this.options.extractCoefficients) {
727
+ block[ZIGZAG[k]] = coeff;
728
+ }
729
+ else {
730
+ block[ZIGZAG[k]] = coeff *
731
+ this.qTables[component.qTable][ZIGZAG[k]];
732
+ }
653
733
  k++;
654
734
  }
655
735
  }
@@ -696,7 +776,10 @@ class JPEGDecoder {
696
776
  if (current !== 0) {
697
777
  const bit = this.readBit();
698
778
  if (bit) {
699
- const refinement = (1 << this.successiveLow) * qTable[z];
779
+ // When extracting coefficients, don't dequantize
780
+ const refinement = this.options.extractCoefficients
781
+ ? (1 << this.successiveLow)
782
+ : (1 << this.successiveLow) * qTable[z];
700
783
  block[z] += direction * refinement;
701
784
  }
702
785
  }
@@ -711,13 +794,17 @@ class JPEGDecoder {
711
794
  if (current !== 0) {
712
795
  const bit = this.readBit();
713
796
  if (bit) {
714
- const refinement = (1 << this.successiveLow) * qTable[z];
797
+ // When extracting coefficients, don't dequantize
798
+ const refinement = this.options.extractCoefficients
799
+ ? (1 << this.successiveLow)
800
+ : (1 << this.successiveLow) * qTable[z];
715
801
  block[z] += direction * refinement;
716
802
  }
717
803
  }
718
804
  else {
719
805
  const newCoeff = successiveACNextValue << this.successiveLow;
720
- block[z] = newCoeff * qTable[z];
806
+ // When extracting coefficients, don't dequantize
807
+ block[z] = this.options.extractCoefficients ? newCoeff : newCoeff * qTable[z];
721
808
  successiveACState = 0;
722
809
  }
723
810
  break;
@@ -725,7 +812,10 @@ class JPEGDecoder {
725
812
  if (current !== 0) {
726
813
  const bit = this.readBit();
727
814
  if (bit) {
728
- const refinement = (1 << this.successiveLow) * qTable[z];
815
+ // When extracting coefficients, don't dequantize
816
+ const refinement = this.options.extractCoefficients
817
+ ? (1 << this.successiveLow)
818
+ : (1 << this.successiveLow) * qTable[z];
729
819
  block[z] += direction * refinement;
730
820
  }
731
821
  }
@@ -744,7 +834,8 @@ class JPEGDecoder {
744
834
  // Perform IDCT only for baseline JPEGs
745
835
  // For progressive JPEGs, IDCT is deferred until all scans are complete
746
836
  // to preserve frequency-domain coefficients for accumulation across scans
747
- if (!this.isProgressive) {
837
+ // Skip IDCT when extracting coefficients - we want the quantized DCT values
838
+ if (!this.isProgressive && !this.options.extractCoefficients) {
748
839
  this.idct(block);
749
840
  }
750
841
  }
@@ -5,6 +5,7 @@
5
5
  * This is a simplified implementation focusing on correctness over performance.
6
6
  * For production use with better quality/size, the OffscreenCanvas API is preferred.
7
7
  */
8
+ import type { JPEGQuantizedCoefficients } from "../types.js";
8
9
  export interface JPEGEncoderOptions {
9
10
  quality?: number;
10
11
  progressive?: boolean;
@@ -43,5 +44,23 @@ export declare class JPEGEncoder {
43
44
  private forwardDCT;
44
45
  private encodeDC;
45
46
  private encodeAC;
47
+ /**
48
+ * Encode JPEG from pre-quantized DCT coefficients
49
+ * Skips DCT and quantization - uses provided coefficients directly
50
+ * Useful for steganography where coefficients are modified and re-encoded
51
+ * @param coeffs JPEG quantized coefficients
52
+ * @param _options Optional encoding options (currently unused)
53
+ * @returns Encoded JPEG bytes
54
+ */
55
+ encodeFromCoefficients(coeffs: JPEGQuantizedCoefficients, _options?: JPEGEncoderOptions): Uint8Array;
56
+ private writeDQTFromCoeffs;
57
+ private writeSOF0FromCoeffs;
58
+ private writeSOF2FromCoeffs;
59
+ private encodeScanFromCoeffs;
60
+ private encodeACFromCoeffs;
61
+ private encodeProgressiveFromCoeffs;
62
+ private encodeProgressiveDCScanFromCoeffs;
63
+ private encodeProgressiveACScanFromCoeffs;
64
+ private encodeOnlyACFromCoeffs;
46
65
  }
47
66
  //# sourceMappingURL=jpeg_encoder.d.ts.map