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.
- package/README.md +186 -5
- package/esm/mod.d.ts +3 -1
- package/esm/mod.js +2 -0
- package/esm/src/formats/apng.d.ts +5 -3
- package/esm/src/formats/apng.js +11 -4
- package/esm/src/formats/avif.d.ts +2 -2
- package/esm/src/formats/avif.js +11 -1
- package/esm/src/formats/gif.d.ts +3 -3
- package/esm/src/formats/gif.js +4 -4
- package/esm/src/formats/heic.d.ts +2 -2
- package/esm/src/formats/heic.js +11 -1
- package/esm/src/formats/jpeg.d.ts +21 -1
- package/esm/src/formats/jpeg.js +59 -0
- package/esm/src/formats/png.d.ts +3 -2
- package/esm/src/formats/png.js +8 -2
- package/esm/src/formats/png_base.d.ts +42 -1
- package/esm/src/formats/png_base.js +198 -5
- package/esm/src/formats/tiff.js +76 -6
- package/esm/src/image.d.ts +54 -1
- package/esm/src/image.js +97 -1
- package/esm/src/types.d.ts +129 -0
- package/esm/src/utils/base64.d.ts +32 -0
- package/esm/src/utils/base64.js +173 -0
- package/esm/src/utils/gif_encoder.d.ts +3 -1
- package/esm/src/utils/gif_encoder.js +4 -2
- package/esm/src/utils/image_processing.d.ts +31 -0
- package/esm/src/utils/image_processing.js +88 -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 +3 -1
- package/script/mod.js +11 -1
- package/script/src/formats/apng.d.ts +5 -3
- package/script/src/formats/apng.js +11 -4
- package/script/src/formats/avif.d.ts +2 -2
- package/script/src/formats/avif.js +11 -1
- package/script/src/formats/gif.d.ts +3 -3
- package/script/src/formats/gif.js +4 -4
- package/script/src/formats/heic.d.ts +2 -2
- package/script/src/formats/heic.js +11 -1
- package/script/src/formats/jpeg.d.ts +21 -1
- package/script/src/formats/jpeg.js +59 -0
- package/script/src/formats/png.d.ts +3 -2
- package/script/src/formats/png.js +8 -2
- package/script/src/formats/png_base.d.ts +42 -1
- package/script/src/formats/png_base.js +198 -5
- package/script/src/formats/tiff.js +76 -6
- package/script/src/image.d.ts +54 -1
- package/script/src/image.js +96 -0
- package/script/src/types.d.ts +129 -0
- package/script/src/utils/base64.d.ts +32 -0
- package/script/src/utils/base64.js +179 -0
- package/script/src/utils/gif_encoder.d.ts +3 -1
- package/script/src/utils/gif_encoder.js +4 -2
- package/script/src/utils/image_processing.d.ts +31 -0
- package/script/src/utils/image_processing.js +92 -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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
649
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -1253,4 +1253,271 @@ export class JPEGEncoder {
|
|
|
1253
1253
|
bitWriter.writeBits(huffTable.codes[0x00], huffTable.sizes[0x00]);
|
|
1254
1254
|
}
|
|
1255
1255
|
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Encode JPEG from pre-quantized DCT coefficients
|
|
1258
|
+
* Skips DCT and quantization - uses provided coefficients directly
|
|
1259
|
+
* Useful for steganography where coefficients are modified and re-encoded
|
|
1260
|
+
* @param coeffs JPEG quantized coefficients
|
|
1261
|
+
* @param _options Optional encoding options (currently unused)
|
|
1262
|
+
* @returns Encoded JPEG bytes
|
|
1263
|
+
*/
|
|
1264
|
+
encodeFromCoefficients(coeffs, _options) {
|
|
1265
|
+
const output = [];
|
|
1266
|
+
const { width, height, components, quantizationTables, isProgressive } = coeffs;
|
|
1267
|
+
// SOI (Start of Image)
|
|
1268
|
+
output.push(0xff, 0xd8);
|
|
1269
|
+
// APP0 (JFIF marker) - use default 72 DPI
|
|
1270
|
+
this.writeAPP0(output, 72, 72);
|
|
1271
|
+
// DQT (Define Quantization Tables) - use original tables from coefficients
|
|
1272
|
+
this.writeDQTFromCoeffs(output, quantizationTables);
|
|
1273
|
+
// SOF (Start of Frame)
|
|
1274
|
+
if (isProgressive) {
|
|
1275
|
+
this.writeSOF2FromCoeffs(output, width, height, components);
|
|
1276
|
+
}
|
|
1277
|
+
else {
|
|
1278
|
+
this.writeSOF0FromCoeffs(output, width, height, components);
|
|
1279
|
+
}
|
|
1280
|
+
// DHT (Define Huffman Tables)
|
|
1281
|
+
this.writeDHT(output);
|
|
1282
|
+
if (isProgressive) {
|
|
1283
|
+
// Progressive encoding: encode from coefficients
|
|
1284
|
+
this.encodeProgressiveFromCoeffs(output, coeffs);
|
|
1285
|
+
}
|
|
1286
|
+
else {
|
|
1287
|
+
// Baseline encoding: single scan
|
|
1288
|
+
this.writeSOS(output);
|
|
1289
|
+
// Encode scan data from coefficients
|
|
1290
|
+
const scanData = this.encodeScanFromCoeffs(coeffs);
|
|
1291
|
+
for (let i = 0; i < scanData.length; i++) {
|
|
1292
|
+
output.push(scanData[i]);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
// EOI (End of Image)
|
|
1296
|
+
output.push(0xff, 0xd9);
|
|
1297
|
+
return new Uint8Array(output);
|
|
1298
|
+
}
|
|
1299
|
+
writeDQTFromCoeffs(output, quantizationTables) {
|
|
1300
|
+
// Write each quantization table
|
|
1301
|
+
for (let tableId = 0; tableId < quantizationTables.length; tableId++) {
|
|
1302
|
+
const table = quantizationTables[tableId];
|
|
1303
|
+
if (!table)
|
|
1304
|
+
continue;
|
|
1305
|
+
output.push(0xff, 0xdb); // DQT marker
|
|
1306
|
+
output.push(0x00, 0x43); // Length (67 bytes)
|
|
1307
|
+
output.push(tableId); // Table ID, 8-bit precision
|
|
1308
|
+
// Write table values in zigzag order
|
|
1309
|
+
for (let i = 0; i < 64; i++) {
|
|
1310
|
+
output.push(table[ZIGZAG[i]] ?? 1);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
writeSOF0FromCoeffs(output, width, height, components) {
|
|
1315
|
+
const numComponents = components.length;
|
|
1316
|
+
const length = 8 + numComponents * 3;
|
|
1317
|
+
output.push(0xff, 0xc0); // SOF0 marker
|
|
1318
|
+
output.push((length >> 8) & 0xff, length & 0xff);
|
|
1319
|
+
output.push(0x08); // Precision (8 bits)
|
|
1320
|
+
output.push((height >> 8) & 0xff, height & 0xff);
|
|
1321
|
+
output.push((width >> 8) & 0xff, width & 0xff);
|
|
1322
|
+
output.push(numComponents);
|
|
1323
|
+
for (const comp of components) {
|
|
1324
|
+
output.push(comp.id);
|
|
1325
|
+
output.push((comp.h << 4) | comp.v);
|
|
1326
|
+
output.push(comp.qTable);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
writeSOF2FromCoeffs(output, width, height, components) {
|
|
1330
|
+
const numComponents = components.length;
|
|
1331
|
+
const length = 8 + numComponents * 3;
|
|
1332
|
+
output.push(0xff, 0xc2); // SOF2 marker (Progressive DCT)
|
|
1333
|
+
output.push((length >> 8) & 0xff, length & 0xff);
|
|
1334
|
+
output.push(0x08); // Precision (8 bits)
|
|
1335
|
+
output.push((height >> 8) & 0xff, height & 0xff);
|
|
1336
|
+
output.push((width >> 8) & 0xff, width & 0xff);
|
|
1337
|
+
output.push(numComponents);
|
|
1338
|
+
for (const comp of components) {
|
|
1339
|
+
output.push(comp.id);
|
|
1340
|
+
output.push((comp.h << 4) | comp.v);
|
|
1341
|
+
output.push(comp.qTable);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
encodeScanFromCoeffs(coeffs) {
|
|
1345
|
+
const bitWriter = new BitWriter();
|
|
1346
|
+
const { components } = coeffs;
|
|
1347
|
+
// DC predictors for each component
|
|
1348
|
+
const dcPreds = new Map();
|
|
1349
|
+
for (const comp of components) {
|
|
1350
|
+
dcPreds.set(comp.id, 0);
|
|
1351
|
+
}
|
|
1352
|
+
// Encode MCUs in raster order
|
|
1353
|
+
// For non-subsampled images (1:1:1), each MCU contains one block per component
|
|
1354
|
+
const mcuHeight = coeffs.mcuHeight;
|
|
1355
|
+
const mcuWidth = coeffs.mcuWidth;
|
|
1356
|
+
for (let mcuY = 0; mcuY < mcuHeight; mcuY++) {
|
|
1357
|
+
for (let mcuX = 0; mcuX < mcuWidth; mcuX++) {
|
|
1358
|
+
// Encode each component's blocks in this MCU
|
|
1359
|
+
for (const comp of components) {
|
|
1360
|
+
// Handle subsampling: component may have multiple blocks per MCU
|
|
1361
|
+
for (let v = 0; v < comp.v; v++) {
|
|
1362
|
+
for (let h = 0; h < comp.h; h++) {
|
|
1363
|
+
const blockY = mcuY * comp.v + v;
|
|
1364
|
+
const blockX = mcuX * comp.h + h;
|
|
1365
|
+
if (blockY < comp.blocks.length &&
|
|
1366
|
+
blockX < comp.blocks[0].length) {
|
|
1367
|
+
const block = comp.blocks[blockY][blockX];
|
|
1368
|
+
// Get the block in zigzag order for encoding
|
|
1369
|
+
const zigzagBlock = new Int32Array(64);
|
|
1370
|
+
for (let i = 0; i < 64; i++) {
|
|
1371
|
+
zigzagBlock[i] = block[ZIGZAG[i]];
|
|
1372
|
+
}
|
|
1373
|
+
// Encode DC coefficient
|
|
1374
|
+
const prevDC = dcPreds.get(comp.id) ?? 0;
|
|
1375
|
+
const dcHuffman = comp.id === 1
|
|
1376
|
+
? this.dcLuminanceHuffman
|
|
1377
|
+
: this.dcChrominanceHuffman;
|
|
1378
|
+
const acHuffman = comp.id === 1
|
|
1379
|
+
? this.acLuminanceHuffman
|
|
1380
|
+
: this.acChrominanceHuffman;
|
|
1381
|
+
// DC coefficient is at index 0 of the block (already in zigzag order from decoder)
|
|
1382
|
+
const dc = block[0];
|
|
1383
|
+
const dcDiff = dc - prevDC;
|
|
1384
|
+
this.encodeDC(dcDiff, dcHuffman, bitWriter);
|
|
1385
|
+
dcPreds.set(comp.id, dc);
|
|
1386
|
+
// Encode AC coefficients (indices 1-63 in zigzag order)
|
|
1387
|
+
this.encodeACFromCoeffs(block, acHuffman, bitWriter);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
bitWriter.flush();
|
|
1395
|
+
return Array.from(bitWriter.getBytes());
|
|
1396
|
+
}
|
|
1397
|
+
encodeACFromCoeffs(block, huffTable, bitWriter) {
|
|
1398
|
+
let zeroCount = 0;
|
|
1399
|
+
// Block is stored with DC at index 0, and AC at indices corresponding to zigzag positions
|
|
1400
|
+
// We need to iterate in zigzag order for AC coefficients (indices 1-63)
|
|
1401
|
+
for (let i = 1; i < 64; i++) {
|
|
1402
|
+
const coef = block[ZIGZAG[i]];
|
|
1403
|
+
if (coef === 0) {
|
|
1404
|
+
zeroCount++;
|
|
1405
|
+
}
|
|
1406
|
+
else {
|
|
1407
|
+
// Write any pending zero runs
|
|
1408
|
+
while (zeroCount >= 16) {
|
|
1409
|
+
bitWriter.writeBits(huffTable.codes[0xf0], huffTable.sizes[0xf0]);
|
|
1410
|
+
zeroCount -= 16;
|
|
1411
|
+
}
|
|
1412
|
+
// Clamp coefficient
|
|
1413
|
+
const maxAC = 1023;
|
|
1414
|
+
const clampedCoef = Math.max(-maxAC, Math.min(maxAC, coef));
|
|
1415
|
+
const absCoef = Math.abs(clampedCoef);
|
|
1416
|
+
const size = Math.floor(Math.log2(absCoef)) + 1;
|
|
1417
|
+
const symbol = (zeroCount << 4) | size;
|
|
1418
|
+
bitWriter.writeBits(huffTable.codes[symbol], huffTable.sizes[symbol]);
|
|
1419
|
+
const magnitude = clampedCoef < 0 ? clampedCoef + (1 << size) - 1 : clampedCoef;
|
|
1420
|
+
bitWriter.writeBits(magnitude, size);
|
|
1421
|
+
zeroCount = 0;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
// Write EOB if there are trailing zeros
|
|
1425
|
+
if (zeroCount > 0) {
|
|
1426
|
+
bitWriter.writeBits(huffTable.codes[0x00], huffTable.sizes[0x00]);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
encodeProgressiveFromCoeffs(output, coeffs) {
|
|
1430
|
+
const { components } = coeffs;
|
|
1431
|
+
// Pre-extract blocks for each component (already quantized)
|
|
1432
|
+
const componentBlocks = new Map();
|
|
1433
|
+
for (const comp of components) {
|
|
1434
|
+
const blocks = [];
|
|
1435
|
+
for (const row of comp.blocks) {
|
|
1436
|
+
for (const block of row) {
|
|
1437
|
+
blocks.push(block);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
componentBlocks.set(comp.id, blocks);
|
|
1441
|
+
}
|
|
1442
|
+
// Scan 1: DC-only (interleaved across all components)
|
|
1443
|
+
this.writeProgressiveSOS(output, components.map((c) => c.id), 0, 0, 0, 0);
|
|
1444
|
+
const dcScanData = this.encodeProgressiveDCScanFromCoeffs(componentBlocks, components);
|
|
1445
|
+
for (let i = 0; i < dcScanData.length; i++) {
|
|
1446
|
+
output.push(dcScanData[i]);
|
|
1447
|
+
}
|
|
1448
|
+
// Scan 2+: AC coefficients (one scan per component)
|
|
1449
|
+
for (const comp of components) {
|
|
1450
|
+
this.writeProgressiveSOS(output, [comp.id], 1, 63, 0, 0);
|
|
1451
|
+
const blocks = componentBlocks.get(comp.id) ?? [];
|
|
1452
|
+
const acHuffman = comp.id === 1 ? this.acLuminanceHuffman : this.acChrominanceHuffman;
|
|
1453
|
+
const acScanData = this.encodeProgressiveACScanFromCoeffs(blocks, acHuffman);
|
|
1454
|
+
for (let i = 0; i < acScanData.length; i++) {
|
|
1455
|
+
output.push(acScanData[i]);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
encodeProgressiveDCScanFromCoeffs(componentBlocks, components) {
|
|
1460
|
+
const bitWriter = new BitWriter();
|
|
1461
|
+
const dcPreds = new Map();
|
|
1462
|
+
for (const comp of components) {
|
|
1463
|
+
dcPreds.set(comp.id, 0);
|
|
1464
|
+
}
|
|
1465
|
+
// Get number of blocks (should be same for all components in interleaved scan)
|
|
1466
|
+
const numBlocks = componentBlocks.get(components[0].id)?.length ?? 0;
|
|
1467
|
+
for (let i = 0; i < numBlocks; i++) {
|
|
1468
|
+
for (const comp of components) {
|
|
1469
|
+
const blocks = componentBlocks.get(comp.id);
|
|
1470
|
+
if (!blocks || i >= blocks.length)
|
|
1471
|
+
continue;
|
|
1472
|
+
const block = blocks[i];
|
|
1473
|
+
const dc = block[0]; // DC coefficient at index 0
|
|
1474
|
+
const prevDC = dcPreds.get(comp.id) ?? 0;
|
|
1475
|
+
const dcHuffman = comp.id === 1 ? this.dcLuminanceHuffman : this.dcChrominanceHuffman;
|
|
1476
|
+
this.encodeOnlyDC(dc, prevDC, dcHuffman, bitWriter);
|
|
1477
|
+
dcPreds.set(comp.id, dc);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
bitWriter.flush();
|
|
1481
|
+
return Array.from(bitWriter.getBytes());
|
|
1482
|
+
}
|
|
1483
|
+
encodeProgressiveACScanFromCoeffs(blocks, acHuffman) {
|
|
1484
|
+
const bitWriter = new BitWriter();
|
|
1485
|
+
for (const block of blocks) {
|
|
1486
|
+
this.encodeOnlyACFromCoeffs(block, acHuffman, bitWriter);
|
|
1487
|
+
}
|
|
1488
|
+
bitWriter.flush();
|
|
1489
|
+
return Array.from(bitWriter.getBytes());
|
|
1490
|
+
}
|
|
1491
|
+
encodeOnlyACFromCoeffs(block, acTable, bitWriter) {
|
|
1492
|
+
let zeroCount = 0;
|
|
1493
|
+
// Iterate AC coefficients in zigzag order (indices 1-63)
|
|
1494
|
+
for (let i = 1; i < 64; i++) {
|
|
1495
|
+
const coef = block[ZIGZAG[i]];
|
|
1496
|
+
const clampedCoef = Math.max(-1023, Math.min(1023, coef));
|
|
1497
|
+
if (clampedCoef === 0) {
|
|
1498
|
+
zeroCount++;
|
|
1499
|
+
if (zeroCount === 16) {
|
|
1500
|
+
bitWriter.writeBits(acTable.codes[0xf0], acTable.sizes[0xf0]);
|
|
1501
|
+
zeroCount = 0;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
else {
|
|
1505
|
+
while (zeroCount >= 16) {
|
|
1506
|
+
bitWriter.writeBits(acTable.codes[0xf0], acTable.sizes[0xf0]);
|
|
1507
|
+
zeroCount -= 16;
|
|
1508
|
+
}
|
|
1509
|
+
const absCoef = Math.abs(clampedCoef);
|
|
1510
|
+
const size = Math.floor(Math.log2(absCoef)) + 1;
|
|
1511
|
+
const symbol = (zeroCount << 4) | size;
|
|
1512
|
+
bitWriter.writeBits(acTable.codes[symbol], acTable.sizes[symbol]);
|
|
1513
|
+
const magnitude = clampedCoef < 0 ? clampedCoef + (1 << size) - 1 : clampedCoef;
|
|
1514
|
+
bitWriter.writeBits(magnitude, size);
|
|
1515
|
+
zeroCount = 0;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
// Write EOB if there are trailing zeros
|
|
1519
|
+
if (zeroCount > 0) {
|
|
1520
|
+
bitWriter.writeBits(acTable.codes[0x00], acTable.sizes[0x00]);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1256
1523
|
}
|
package/package.json
CHANGED
package/script/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/script/mod.js
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
* ```
|
|
45
45
|
*/
|
|
46
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
-
exports.AVIFFormat = exports.HEICFormat = exports.ASCIIFormat = exports.PPMFormat = exports.PCXFormat = exports.PAMFormat = exports.DNGFormat = exports.ICOFormat = exports.BMPFormat = exports.TIFFFormat = exports.GIFFormat = exports.WebPFormat = exports.JPEGFormat = exports.APNGFormat = exports.PNGFormat = exports.Image = void 0;
|
|
47
|
+
exports.rgbToCmyk = exports.rgbaToCmyk = exports.cmykToRgba = exports.cmykToRgb = exports.toDataUrl = exports.parseDataUrl = exports.encodeBase64 = exports.decodeBase64 = exports.AVIFFormat = exports.HEICFormat = exports.ASCIIFormat = exports.PPMFormat = exports.PCXFormat = exports.PAMFormat = exports.DNGFormat = exports.ICOFormat = exports.BMPFormat = exports.TIFFFormat = exports.GIFFormat = exports.WebPFormat = exports.JPEGFormat = exports.APNGFormat = exports.PNGFormat = exports.Image = void 0;
|
|
48
48
|
var image_js_1 = require("./src/image.js");
|
|
49
49
|
Object.defineProperty(exports, "Image", { enumerable: true, get: function () { return image_js_1.Image; } });
|
|
50
50
|
var png_js_1 = require("./src/formats/png.js");
|
|
@@ -77,3 +77,13 @@ var heic_js_1 = require("./src/formats/heic.js");
|
|
|
77
77
|
Object.defineProperty(exports, "HEICFormat", { enumerable: true, get: function () { return heic_js_1.HEICFormat; } });
|
|
78
78
|
var avif_js_1 = require("./src/formats/avif.js");
|
|
79
79
|
Object.defineProperty(exports, "AVIFFormat", { enumerable: true, get: function () { return avif_js_1.AVIFFormat; } });
|
|
80
|
+
var base64_js_1 = require("./src/utils/base64.js");
|
|
81
|
+
Object.defineProperty(exports, "decodeBase64", { enumerable: true, get: function () { return base64_js_1.decodeBase64; } });
|
|
82
|
+
Object.defineProperty(exports, "encodeBase64", { enumerable: true, get: function () { return base64_js_1.encodeBase64; } });
|
|
83
|
+
Object.defineProperty(exports, "parseDataUrl", { enumerable: true, get: function () { return base64_js_1.parseDataUrl; } });
|
|
84
|
+
Object.defineProperty(exports, "toDataUrl", { enumerable: true, get: function () { return base64_js_1.toDataUrl; } });
|
|
85
|
+
var image_processing_js_1 = require("./src/utils/image_processing.js");
|
|
86
|
+
Object.defineProperty(exports, "cmykToRgb", { enumerable: true, get: function () { return image_processing_js_1.cmykToRgb; } });
|
|
87
|
+
Object.defineProperty(exports, "cmykToRgba", { enumerable: true, get: function () { return image_processing_js_1.cmykToRgba; } });
|
|
88
|
+
Object.defineProperty(exports, "rgbaToCmyk", { enumerable: true, get: function () { return image_processing_js_1.rgbaToCmyk; } });
|
|
89
|
+
Object.defineProperty(exports, "rgbToCmyk", { enumerable: true, get: function () { return image_processing_js_1.rgbToCmyk; } });
|
|
@@ -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,
|
|
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,
|
|
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
|
|
@@ -253,9 +253,10 @@ class APNGFormat extends png_base_js_1.PNGBase {
|
|
|
253
253
|
/**
|
|
254
254
|
* Encode RGBA image data to APNG format (single frame)
|
|
255
255
|
* @param imageData Image data to encode
|
|
256
|
+
* @param options Encoding options (compressionLevel 0-9, default 6)
|
|
256
257
|
* @returns Encoded APNG image bytes
|
|
257
258
|
*/
|
|
258
|
-
encode(imageData,
|
|
259
|
+
encode(imageData, options) {
|
|
259
260
|
// For single frame, create a multi-frame with one frame
|
|
260
261
|
const multiFrame = {
|
|
261
262
|
width: imageData.width,
|
|
@@ -268,15 +269,21 @@ class APNGFormat extends png_base_js_1.PNGBase {
|
|
|
268
269
|
}],
|
|
269
270
|
metadata: imageData.metadata,
|
|
270
271
|
};
|
|
271
|
-
return this.encodeFrames(multiFrame,
|
|
272
|
+
return this.encodeFrames(multiFrame, options);
|
|
272
273
|
}
|
|
273
274
|
/**
|
|
274
275
|
* Encode multi-frame image data to APNG format
|
|
275
276
|
* @param imageData Multi-frame image data to encode
|
|
277
|
+
* @param options Encoding options (compressionLevel 0-9, default 6)
|
|
276
278
|
* @returns Encoded APNG image bytes
|
|
277
279
|
*/
|
|
278
|
-
async encodeFrames(imageData,
|
|
280
|
+
async encodeFrames(imageData, options) {
|
|
279
281
|
const { width, height, frames, metadata } = imageData;
|
|
282
|
+
const compressionLevel = options?.compressionLevel ?? 6;
|
|
283
|
+
// Validate compression level
|
|
284
|
+
if (compressionLevel < 0 || compressionLevel > 9) {
|
|
285
|
+
throw new Error("Compression level must be between 0 and 9");
|
|
286
|
+
}
|
|
280
287
|
if (frames.length === 0) {
|
|
281
288
|
throw new Error("No frames to encode");
|
|
282
289
|
}
|
|
@@ -334,7 +341,7 @@ class APNGFormat extends png_base_js_1.PNGBase {
|
|
|
334
341
|
fctl[25] = 0; // blend_op: APNG_BLEND_OP_SOURCE
|
|
335
342
|
chunks.push(this.createChunk("fcTL", fctl));
|
|
336
343
|
// Filter and compress frame data
|
|
337
|
-
const filtered = this.filterData(frame.data, frame.width, frame.height);
|
|
344
|
+
const filtered = this.filterData(frame.data, frame.width, frame.height, compressionLevel);
|
|
338
345
|
const compressed = await this.deflate(filtered);
|
|
339
346
|
if (i === 0) {
|
|
340
347
|
// 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,
|
|
35
|
+
encode(imageData: ImageData, options?: AVIFEncoderOptions): Promise<Uint8Array>;
|
|
36
36
|
/**
|
|
37
37
|
* Decode using runtime APIs
|
|
38
38
|
* @param data Raw AVIF data
|
|
@@ -82,8 +82,9 @@ class AVIFFormat {
|
|
|
82
82
|
* @param imageData Image data to encode
|
|
83
83
|
* @returns Encoded AVIF image bytes
|
|
84
84
|
*/
|
|
85
|
-
async encode(imageData,
|
|
85
|
+
async encode(imageData, options) {
|
|
86
86
|
const { width, height, data, metadata: _metadata } = imageData;
|
|
87
|
+
const requestedQuality = options?.quality;
|
|
87
88
|
// Try to use runtime encoding if available
|
|
88
89
|
if (typeof OffscreenCanvas !== "undefined") {
|
|
89
90
|
try {
|
|
@@ -94,10 +95,19 @@ class AVIFFormat {
|
|
|
94
95
|
const imgDataData = new Uint8ClampedArray(data);
|
|
95
96
|
imgData.data.set(imgDataData);
|
|
96
97
|
ctx.putImageData(imgData, 0, 0);
|
|
98
|
+
const quality = requestedQuality === undefined
|
|
99
|
+
? undefined
|
|
100
|
+
: (requestedQuality <= 1
|
|
101
|
+
? Math.max(0, Math.min(1, requestedQuality))
|
|
102
|
+
: Math.max(1, Math.min(100, requestedQuality)) / 100);
|
|
97
103
|
// Try to encode as AVIF
|
|
98
104
|
const blob = await canvas.convertToBlob({
|
|
99
105
|
type: "image/avif",
|
|
106
|
+
...(quality === undefined ? {} : { quality }),
|
|
100
107
|
});
|
|
108
|
+
if (blob.type !== "image/avif") {
|
|
109
|
+
throw new Error(`Runtime did not encode AVIF (got '${blob.type || "(empty)"}')`);
|
|
110
|
+
}
|
|
101
111
|
const arrayBuffer = await blob.arrayBuffer();
|
|
102
112
|
const encoded = new Uint8Array(arrayBuffer);
|
|
103
113
|
// Note: Metadata injection for AVIF is complex and would require
|