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