cross-image 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -4
- package/esm/mod.d.ts +1 -1
- package/esm/src/formats/jpeg.d.ts +21 -1
- package/esm/src/formats/jpeg.js +59 -0
- package/esm/src/image.d.ts +39 -1
- package/esm/src/image.js +68 -0
- package/esm/src/types.d.ts +63 -0
- package/esm/src/utils/jpeg_decoder.d.ts +25 -2
- package/esm/src/utils/jpeg_decoder.js +101 -10
- package/esm/src/utils/jpeg_encoder.d.ts +19 -0
- package/esm/src/utils/jpeg_encoder.js +267 -0
- package/package.json +1 -1
- package/script/mod.d.ts +1 -1
- package/script/src/formats/jpeg.d.ts +21 -1
- package/script/src/formats/jpeg.js +59 -0
- package/script/src/image.d.ts +39 -1
- package/script/src/image.js +68 -0
- package/script/src/types.d.ts +63 -0
- package/script/src/utils/jpeg_decoder.d.ts +25 -2
- package/script/src/utils/jpeg_decoder.js +101 -10
- package/script/src/utils/jpeg_encoder.d.ts +19 -0
- package/script/src/utils/jpeg_encoder.js +267 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
652
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -1256,5 +1256,272 @@ class JPEGEncoder {
|
|
|
1256
1256
|
bitWriter.writeBits(huffTable.codes[0x00], huffTable.sizes[0x00]);
|
|
1257
1257
|
}
|
|
1258
1258
|
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Encode JPEG from pre-quantized DCT coefficients
|
|
1261
|
+
* Skips DCT and quantization - uses provided coefficients directly
|
|
1262
|
+
* Useful for steganography where coefficients are modified and re-encoded
|
|
1263
|
+
* @param coeffs JPEG quantized coefficients
|
|
1264
|
+
* @param _options Optional encoding options (currently unused)
|
|
1265
|
+
* @returns Encoded JPEG bytes
|
|
1266
|
+
*/
|
|
1267
|
+
encodeFromCoefficients(coeffs, _options) {
|
|
1268
|
+
const output = [];
|
|
1269
|
+
const { width, height, components, quantizationTables, isProgressive } = coeffs;
|
|
1270
|
+
// SOI (Start of Image)
|
|
1271
|
+
output.push(0xff, 0xd8);
|
|
1272
|
+
// APP0 (JFIF marker) - use default 72 DPI
|
|
1273
|
+
this.writeAPP0(output, 72, 72);
|
|
1274
|
+
// DQT (Define Quantization Tables) - use original tables from coefficients
|
|
1275
|
+
this.writeDQTFromCoeffs(output, quantizationTables);
|
|
1276
|
+
// SOF (Start of Frame)
|
|
1277
|
+
if (isProgressive) {
|
|
1278
|
+
this.writeSOF2FromCoeffs(output, width, height, components);
|
|
1279
|
+
}
|
|
1280
|
+
else {
|
|
1281
|
+
this.writeSOF0FromCoeffs(output, width, height, components);
|
|
1282
|
+
}
|
|
1283
|
+
// DHT (Define Huffman Tables)
|
|
1284
|
+
this.writeDHT(output);
|
|
1285
|
+
if (isProgressive) {
|
|
1286
|
+
// Progressive encoding: encode from coefficients
|
|
1287
|
+
this.encodeProgressiveFromCoeffs(output, coeffs);
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
// Baseline encoding: single scan
|
|
1291
|
+
this.writeSOS(output);
|
|
1292
|
+
// Encode scan data from coefficients
|
|
1293
|
+
const scanData = this.encodeScanFromCoeffs(coeffs);
|
|
1294
|
+
for (let i = 0; i < scanData.length; i++) {
|
|
1295
|
+
output.push(scanData[i]);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
// EOI (End of Image)
|
|
1299
|
+
output.push(0xff, 0xd9);
|
|
1300
|
+
return new Uint8Array(output);
|
|
1301
|
+
}
|
|
1302
|
+
writeDQTFromCoeffs(output, quantizationTables) {
|
|
1303
|
+
// Write each quantization table
|
|
1304
|
+
for (let tableId = 0; tableId < quantizationTables.length; tableId++) {
|
|
1305
|
+
const table = quantizationTables[tableId];
|
|
1306
|
+
if (!table)
|
|
1307
|
+
continue;
|
|
1308
|
+
output.push(0xff, 0xdb); // DQT marker
|
|
1309
|
+
output.push(0x00, 0x43); // Length (67 bytes)
|
|
1310
|
+
output.push(tableId); // Table ID, 8-bit precision
|
|
1311
|
+
// Write table values in zigzag order
|
|
1312
|
+
for (let i = 0; i < 64; i++) {
|
|
1313
|
+
output.push(table[ZIGZAG[i]] ?? 1);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
writeSOF0FromCoeffs(output, width, height, components) {
|
|
1318
|
+
const numComponents = components.length;
|
|
1319
|
+
const length = 8 + numComponents * 3;
|
|
1320
|
+
output.push(0xff, 0xc0); // SOF0 marker
|
|
1321
|
+
output.push((length >> 8) & 0xff, length & 0xff);
|
|
1322
|
+
output.push(0x08); // Precision (8 bits)
|
|
1323
|
+
output.push((height >> 8) & 0xff, height & 0xff);
|
|
1324
|
+
output.push((width >> 8) & 0xff, width & 0xff);
|
|
1325
|
+
output.push(numComponents);
|
|
1326
|
+
for (const comp of components) {
|
|
1327
|
+
output.push(comp.id);
|
|
1328
|
+
output.push((comp.h << 4) | comp.v);
|
|
1329
|
+
output.push(comp.qTable);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
writeSOF2FromCoeffs(output, width, height, components) {
|
|
1333
|
+
const numComponents = components.length;
|
|
1334
|
+
const length = 8 + numComponents * 3;
|
|
1335
|
+
output.push(0xff, 0xc2); // SOF2 marker (Progressive DCT)
|
|
1336
|
+
output.push((length >> 8) & 0xff, length & 0xff);
|
|
1337
|
+
output.push(0x08); // Precision (8 bits)
|
|
1338
|
+
output.push((height >> 8) & 0xff, height & 0xff);
|
|
1339
|
+
output.push((width >> 8) & 0xff, width & 0xff);
|
|
1340
|
+
output.push(numComponents);
|
|
1341
|
+
for (const comp of components) {
|
|
1342
|
+
output.push(comp.id);
|
|
1343
|
+
output.push((comp.h << 4) | comp.v);
|
|
1344
|
+
output.push(comp.qTable);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
encodeScanFromCoeffs(coeffs) {
|
|
1348
|
+
const bitWriter = new BitWriter();
|
|
1349
|
+
const { components } = coeffs;
|
|
1350
|
+
// DC predictors for each component
|
|
1351
|
+
const dcPreds = new Map();
|
|
1352
|
+
for (const comp of components) {
|
|
1353
|
+
dcPreds.set(comp.id, 0);
|
|
1354
|
+
}
|
|
1355
|
+
// Encode MCUs in raster order
|
|
1356
|
+
// For non-subsampled images (1:1:1), each MCU contains one block per component
|
|
1357
|
+
const mcuHeight = coeffs.mcuHeight;
|
|
1358
|
+
const mcuWidth = coeffs.mcuWidth;
|
|
1359
|
+
for (let mcuY = 0; mcuY < mcuHeight; mcuY++) {
|
|
1360
|
+
for (let mcuX = 0; mcuX < mcuWidth; mcuX++) {
|
|
1361
|
+
// Encode each component's blocks in this MCU
|
|
1362
|
+
for (const comp of components) {
|
|
1363
|
+
// Handle subsampling: component may have multiple blocks per MCU
|
|
1364
|
+
for (let v = 0; v < comp.v; v++) {
|
|
1365
|
+
for (let h = 0; h < comp.h; h++) {
|
|
1366
|
+
const blockY = mcuY * comp.v + v;
|
|
1367
|
+
const blockX = mcuX * comp.h + h;
|
|
1368
|
+
if (blockY < comp.blocks.length &&
|
|
1369
|
+
blockX < comp.blocks[0].length) {
|
|
1370
|
+
const block = comp.blocks[blockY][blockX];
|
|
1371
|
+
// Get the block in zigzag order for encoding
|
|
1372
|
+
const zigzagBlock = new Int32Array(64);
|
|
1373
|
+
for (let i = 0; i < 64; i++) {
|
|
1374
|
+
zigzagBlock[i] = block[ZIGZAG[i]];
|
|
1375
|
+
}
|
|
1376
|
+
// Encode DC coefficient
|
|
1377
|
+
const prevDC = dcPreds.get(comp.id) ?? 0;
|
|
1378
|
+
const dcHuffman = comp.id === 1
|
|
1379
|
+
? this.dcLuminanceHuffman
|
|
1380
|
+
: this.dcChrominanceHuffman;
|
|
1381
|
+
const acHuffman = comp.id === 1
|
|
1382
|
+
? this.acLuminanceHuffman
|
|
1383
|
+
: this.acChrominanceHuffman;
|
|
1384
|
+
// DC coefficient is at index 0 of the block (already in zigzag order from decoder)
|
|
1385
|
+
const dc = block[0];
|
|
1386
|
+
const dcDiff = dc - prevDC;
|
|
1387
|
+
this.encodeDC(dcDiff, dcHuffman, bitWriter);
|
|
1388
|
+
dcPreds.set(comp.id, dc);
|
|
1389
|
+
// Encode AC coefficients (indices 1-63 in zigzag order)
|
|
1390
|
+
this.encodeACFromCoeffs(block, acHuffman, bitWriter);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
bitWriter.flush();
|
|
1398
|
+
return Array.from(bitWriter.getBytes());
|
|
1399
|
+
}
|
|
1400
|
+
encodeACFromCoeffs(block, huffTable, bitWriter) {
|
|
1401
|
+
let zeroCount = 0;
|
|
1402
|
+
// Block is stored with DC at index 0, and AC at indices corresponding to zigzag positions
|
|
1403
|
+
// We need to iterate in zigzag order for AC coefficients (indices 1-63)
|
|
1404
|
+
for (let i = 1; i < 64; i++) {
|
|
1405
|
+
const coef = block[ZIGZAG[i]];
|
|
1406
|
+
if (coef === 0) {
|
|
1407
|
+
zeroCount++;
|
|
1408
|
+
}
|
|
1409
|
+
else {
|
|
1410
|
+
// Write any pending zero runs
|
|
1411
|
+
while (zeroCount >= 16) {
|
|
1412
|
+
bitWriter.writeBits(huffTable.codes[0xf0], huffTable.sizes[0xf0]);
|
|
1413
|
+
zeroCount -= 16;
|
|
1414
|
+
}
|
|
1415
|
+
// Clamp coefficient
|
|
1416
|
+
const maxAC = 1023;
|
|
1417
|
+
const clampedCoef = Math.max(-maxAC, Math.min(maxAC, coef));
|
|
1418
|
+
const absCoef = Math.abs(clampedCoef);
|
|
1419
|
+
const size = Math.floor(Math.log2(absCoef)) + 1;
|
|
1420
|
+
const symbol = (zeroCount << 4) | size;
|
|
1421
|
+
bitWriter.writeBits(huffTable.codes[symbol], huffTable.sizes[symbol]);
|
|
1422
|
+
const magnitude = clampedCoef < 0 ? clampedCoef + (1 << size) - 1 : clampedCoef;
|
|
1423
|
+
bitWriter.writeBits(magnitude, size);
|
|
1424
|
+
zeroCount = 0;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
// Write EOB if there are trailing zeros
|
|
1428
|
+
if (zeroCount > 0) {
|
|
1429
|
+
bitWriter.writeBits(huffTable.codes[0x00], huffTable.sizes[0x00]);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
encodeProgressiveFromCoeffs(output, coeffs) {
|
|
1433
|
+
const { components } = coeffs;
|
|
1434
|
+
// Pre-extract blocks for each component (already quantized)
|
|
1435
|
+
const componentBlocks = new Map();
|
|
1436
|
+
for (const comp of components) {
|
|
1437
|
+
const blocks = [];
|
|
1438
|
+
for (const row of comp.blocks) {
|
|
1439
|
+
for (const block of row) {
|
|
1440
|
+
blocks.push(block);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
componentBlocks.set(comp.id, blocks);
|
|
1444
|
+
}
|
|
1445
|
+
// Scan 1: DC-only (interleaved across all components)
|
|
1446
|
+
this.writeProgressiveSOS(output, components.map((c) => c.id), 0, 0, 0, 0);
|
|
1447
|
+
const dcScanData = this.encodeProgressiveDCScanFromCoeffs(componentBlocks, components);
|
|
1448
|
+
for (let i = 0; i < dcScanData.length; i++) {
|
|
1449
|
+
output.push(dcScanData[i]);
|
|
1450
|
+
}
|
|
1451
|
+
// Scan 2+: AC coefficients (one scan per component)
|
|
1452
|
+
for (const comp of components) {
|
|
1453
|
+
this.writeProgressiveSOS(output, [comp.id], 1, 63, 0, 0);
|
|
1454
|
+
const blocks = componentBlocks.get(comp.id) ?? [];
|
|
1455
|
+
const acHuffman = comp.id === 1 ? this.acLuminanceHuffman : this.acChrominanceHuffman;
|
|
1456
|
+
const acScanData = this.encodeProgressiveACScanFromCoeffs(blocks, acHuffman);
|
|
1457
|
+
for (let i = 0; i < acScanData.length; i++) {
|
|
1458
|
+
output.push(acScanData[i]);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
encodeProgressiveDCScanFromCoeffs(componentBlocks, components) {
|
|
1463
|
+
const bitWriter = new BitWriter();
|
|
1464
|
+
const dcPreds = new Map();
|
|
1465
|
+
for (const comp of components) {
|
|
1466
|
+
dcPreds.set(comp.id, 0);
|
|
1467
|
+
}
|
|
1468
|
+
// Get number of blocks (should be same for all components in interleaved scan)
|
|
1469
|
+
const numBlocks = componentBlocks.get(components[0].id)?.length ?? 0;
|
|
1470
|
+
for (let i = 0; i < numBlocks; i++) {
|
|
1471
|
+
for (const comp of components) {
|
|
1472
|
+
const blocks = componentBlocks.get(comp.id);
|
|
1473
|
+
if (!blocks || i >= blocks.length)
|
|
1474
|
+
continue;
|
|
1475
|
+
const block = blocks[i];
|
|
1476
|
+
const dc = block[0]; // DC coefficient at index 0
|
|
1477
|
+
const prevDC = dcPreds.get(comp.id) ?? 0;
|
|
1478
|
+
const dcHuffman = comp.id === 1 ? this.dcLuminanceHuffman : this.dcChrominanceHuffman;
|
|
1479
|
+
this.encodeOnlyDC(dc, prevDC, dcHuffman, bitWriter);
|
|
1480
|
+
dcPreds.set(comp.id, dc);
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
bitWriter.flush();
|
|
1484
|
+
return Array.from(bitWriter.getBytes());
|
|
1485
|
+
}
|
|
1486
|
+
encodeProgressiveACScanFromCoeffs(blocks, acHuffman) {
|
|
1487
|
+
const bitWriter = new BitWriter();
|
|
1488
|
+
for (const block of blocks) {
|
|
1489
|
+
this.encodeOnlyACFromCoeffs(block, acHuffman, bitWriter);
|
|
1490
|
+
}
|
|
1491
|
+
bitWriter.flush();
|
|
1492
|
+
return Array.from(bitWriter.getBytes());
|
|
1493
|
+
}
|
|
1494
|
+
encodeOnlyACFromCoeffs(block, acTable, bitWriter) {
|
|
1495
|
+
let zeroCount = 0;
|
|
1496
|
+
// Iterate AC coefficients in zigzag order (indices 1-63)
|
|
1497
|
+
for (let i = 1; i < 64; i++) {
|
|
1498
|
+
const coef = block[ZIGZAG[i]];
|
|
1499
|
+
const clampedCoef = Math.max(-1023, Math.min(1023, coef));
|
|
1500
|
+
if (clampedCoef === 0) {
|
|
1501
|
+
zeroCount++;
|
|
1502
|
+
if (zeroCount === 16) {
|
|
1503
|
+
bitWriter.writeBits(acTable.codes[0xf0], acTable.sizes[0xf0]);
|
|
1504
|
+
zeroCount = 0;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
else {
|
|
1508
|
+
while (zeroCount >= 16) {
|
|
1509
|
+
bitWriter.writeBits(acTable.codes[0xf0], acTable.sizes[0xf0]);
|
|
1510
|
+
zeroCount -= 16;
|
|
1511
|
+
}
|
|
1512
|
+
const absCoef = Math.abs(clampedCoef);
|
|
1513
|
+
const size = Math.floor(Math.log2(absCoef)) + 1;
|
|
1514
|
+
const symbol = (zeroCount << 4) | size;
|
|
1515
|
+
bitWriter.writeBits(acTable.codes[symbol], acTable.sizes[symbol]);
|
|
1516
|
+
const magnitude = clampedCoef < 0 ? clampedCoef + (1 << size) - 1 : clampedCoef;
|
|
1517
|
+
bitWriter.writeBits(magnitude, size);
|
|
1518
|
+
zeroCount = 0;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
// Write EOB if there are trailing zeros
|
|
1522
|
+
if (zeroCount > 0) {
|
|
1523
|
+
bitWriter.writeBits(acTable.codes[0x00], acTable.sizes[0x00]);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1259
1526
|
}
|
|
1260
1527
|
exports.JPEGEncoder = JPEGEncoder;
|