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.
- package/README.md +77 -4
- package/esm/mod.d.ts +1 -1
- package/esm/src/formats/gif.d.ts +1 -0
- package/esm/src/formats/gif.js +19 -2
- 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/gif_encoder.d.ts +13 -4
- package/esm/src/utils/gif_encoder.js +100 -97
- 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/esm/src/utils/tiff_deflate.d.ts +0 -1
- package/esm/src/utils/tiff_deflate.js +0 -1
- package/package.json +1 -1
- package/script/mod.d.ts +1 -1
- package/script/src/formats/gif.d.ts +1 -0
- package/script/src/formats/gif.js +19 -2
- 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/gif_encoder.d.ts +13 -4
- package/script/src/utils/gif_encoder.js +100 -97
- 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
- package/script/src/utils/tiff_deflate.d.ts +0 -1
- package/script/src/utils/tiff_deflate.js +0 -1
|
@@ -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, "
|
|
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, "
|
|
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.
|
|
27
|
-
this.
|
|
26
|
+
this.canvasWidth = width;
|
|
27
|
+
this.canvasHeight = height;
|
|
28
28
|
if (data) {
|
|
29
29
|
this.addFrame(data);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
74
|
+
* Quantize RGBA image to 256 colors with transparency support
|
|
61
75
|
*/
|
|
62
|
-
quantize(data) {
|
|
63
|
-
//
|
|
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
|
-
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 <
|
|
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
|
|
86
|
-
if (colors.length >=
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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(
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
184
|
-
this.writeUint16LE(output, this.
|
|
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);
|
|
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);
|
|
210
|
-
|
|
211
|
-
|
|
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 =
|
|
239
|
+
indexed = firstResult.indexed;
|
|
240
|
+
hasTransparency = firstResult.hasTransparency;
|
|
241
|
+
transparentIndex = firstResult.transparentIndex;
|
|
223
242
|
}
|
|
224
243
|
else {
|
|
225
|
-
|
|
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
|
-
|
|
232
|
-
|
|
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);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
//
|
|
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
|
|
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);
|
|
253
|
-
this.writeUint16LE(output,
|
|
254
|
-
this.writeUint16LE(output,
|
|
255
|
-
this.writeUint16LE(output,
|
|
256
|
-
this.writeUint16LE(output,
|
|
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);
|
|
279
|
+
output.push(0);
|
|
274
280
|
}
|
|
275
|
-
// LZW
|
|
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
|
-
|
|
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
|
-
|
|
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
|