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
|
@@ -3,11 +3,20 @@
|
|
|
3
3
|
* Supports GIF89a format with LZW compression
|
|
4
4
|
*/
|
|
5
5
|
export declare class GIFEncoder {
|
|
6
|
-
private
|
|
7
|
-
private
|
|
6
|
+
private canvasWidth;
|
|
7
|
+
private canvasHeight;
|
|
8
8
|
private frames;
|
|
9
9
|
constructor(width: number, height: number, data?: Uint8Array);
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Add a frame with optional metadata for partial frame support
|
|
12
|
+
*/
|
|
13
|
+
addFrame(data: Uint8Array, delay?: number, options?: {
|
|
14
|
+
left?: number;
|
|
15
|
+
top?: number;
|
|
16
|
+
width?: number;
|
|
17
|
+
height?: number;
|
|
18
|
+
disposal?: number;
|
|
19
|
+
}): void;
|
|
11
20
|
private writeBytes;
|
|
12
21
|
private writeUint16LE;
|
|
13
22
|
private writeString;
|
|
@@ -20,7 +29,7 @@ export declare class GIFEncoder {
|
|
|
20
29
|
*/
|
|
21
30
|
private quantizeChannel;
|
|
22
31
|
/**
|
|
23
|
-
* Quantize RGBA image to 256 colors
|
|
32
|
+
* Quantize RGBA image to 256 colors with transparency support
|
|
24
33
|
*/
|
|
25
34
|
private quantize;
|
|
26
35
|
private nextPowerOf2;
|
|
@@ -8,13 +8,13 @@ exports.GIFEncoder = void 0;
|
|
|
8
8
|
const lzw_js_1 = require("./lzw.js");
|
|
9
9
|
class GIFEncoder {
|
|
10
10
|
constructor(width, height, data) {
|
|
11
|
-
Object.defineProperty(this, "
|
|
11
|
+
Object.defineProperty(this, "canvasWidth", {
|
|
12
12
|
enumerable: true,
|
|
13
13
|
configurable: true,
|
|
14
14
|
writable: true,
|
|
15
15
|
value: void 0
|
|
16
16
|
});
|
|
17
|
-
Object.defineProperty(this, "
|
|
17
|
+
Object.defineProperty(this, "canvasHeight", {
|
|
18
18
|
enumerable: true,
|
|
19
19
|
configurable: true,
|
|
20
20
|
writable: true,
|
|
@@ -26,14 +26,28 @@ class GIFEncoder {
|
|
|
26
26
|
writable: true,
|
|
27
27
|
value: []
|
|
28
28
|
});
|
|
29
|
-
this.
|
|
30
|
-
this.
|
|
29
|
+
this.canvasWidth = width;
|
|
30
|
+
this.canvasHeight = height;
|
|
31
31
|
if (data) {
|
|
32
32
|
this.addFrame(data);
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Add a frame with optional metadata for partial frame support
|
|
37
|
+
*/
|
|
38
|
+
addFrame(data, delay = 0, options) {
|
|
39
|
+
const pixelCount = data.length / 4;
|
|
40
|
+
const frameWidth = options?.width ?? this.canvasWidth;
|
|
41
|
+
const frameHeight = options?.height ?? Math.ceil(pixelCount / frameWidth);
|
|
42
|
+
this.frames.push({
|
|
43
|
+
data,
|
|
44
|
+
delay,
|
|
45
|
+
left: options?.left ?? 0,
|
|
46
|
+
top: options?.top ?? 0,
|
|
47
|
+
width: frameWidth,
|
|
48
|
+
height: frameHeight,
|
|
49
|
+
disposal: options?.disposal ?? 0,
|
|
50
|
+
});
|
|
37
51
|
}
|
|
38
52
|
writeBytes(output, bytes) {
|
|
39
53
|
output.push(...bytes);
|
|
@@ -60,49 +74,59 @@ class GIFEncoder {
|
|
|
60
74
|
return Math.round(Math.round(value * levels / 255) * step);
|
|
61
75
|
}
|
|
62
76
|
/**
|
|
63
|
-
* Quantize RGBA image to 256 colors
|
|
77
|
+
* Quantize RGBA image to 256 colors with transparency support
|
|
64
78
|
*/
|
|
65
|
-
quantize(data) {
|
|
66
|
-
//
|
|
79
|
+
quantize(data, frameWidth, frameHeight) {
|
|
80
|
+
// Check for transparent pixels (alpha < 128)
|
|
81
|
+
let hasTransparency = false;
|
|
82
|
+
for (let i = 3; i < data.length; i += 4) {
|
|
83
|
+
if (data[i] < 128) {
|
|
84
|
+
hasTransparency = true;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
67
88
|
const colorMap = new Map();
|
|
68
89
|
const colors = [];
|
|
69
|
-
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
90
|
+
const transparentIndex = 0;
|
|
91
|
+
// Reserve index 0 for transparency if needed
|
|
92
|
+
if (hasTransparency) {
|
|
93
|
+
colors.push({ r: 0, g: 0, b: 0 });
|
|
94
|
+
}
|
|
95
|
+
// Color quantization parameters
|
|
96
|
+
const RG_LEVELS = 7;
|
|
97
|
+
const B_LEVELS = 3;
|
|
98
|
+
const rgStep = 255 / RG_LEVELS;
|
|
99
|
+
const bStep = 255 / B_LEVELS;
|
|
100
|
+
const maxColors = hasTransparency ? 255 : 256;
|
|
101
|
+
// Collect unique colors from opaque pixels only
|
|
76
102
|
for (let i = 0; i < data.length; i += 4) {
|
|
103
|
+
if (data[i + 3] < 128)
|
|
104
|
+
continue; // Skip transparent
|
|
77
105
|
const r = data[i];
|
|
78
106
|
const g = data[i + 1];
|
|
79
107
|
const b = data[i + 2];
|
|
80
108
|
const key = `${r},${g},${b}`;
|
|
81
|
-
if (!colorMap.has(key) && colors.length <
|
|
109
|
+
if (!colorMap.has(key) && colors.length < maxColors) {
|
|
82
110
|
colorMap.set(key, colors.length);
|
|
83
111
|
colors.push({ r, g, b });
|
|
84
112
|
}
|
|
85
113
|
}
|
|
86
|
-
// Track if color reduction was applied
|
|
87
114
|
let useColorReduction = false;
|
|
88
|
-
// If
|
|
89
|
-
if (colors.length >=
|
|
90
|
-
// Downsample colors to 256 by reducing color depth
|
|
115
|
+
// If too many colors, reduce color depth
|
|
116
|
+
if (colors.length >= maxColors) {
|
|
91
117
|
colorMap.clear();
|
|
92
|
-
colors.length = 0;
|
|
118
|
+
colors.length = hasTransparency ? 1 : 0;
|
|
93
119
|
useColorReduction = true;
|
|
94
120
|
for (let i = 0; i < data.length; i += 4) {
|
|
95
|
-
|
|
96
|
-
|
|
121
|
+
if (data[i + 3] < 128)
|
|
122
|
+
continue;
|
|
97
123
|
const r = this.quantizeChannel(data[i], RG_LEVELS, rgStep);
|
|
98
124
|
const g = this.quantizeChannel(data[i + 1], RG_LEVELS, rgStep);
|
|
99
125
|
const b = this.quantizeChannel(data[i + 2], B_LEVELS, bStep);
|
|
100
126
|
const key = `${r},${g},${b}`;
|
|
101
|
-
if (!colorMap.has(key)) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
colors.push({ r, g, b });
|
|
105
|
-
}
|
|
127
|
+
if (!colorMap.has(key) && colors.length < maxColors) {
|
|
128
|
+
colorMap.set(key, colors.length);
|
|
129
|
+
colors.push({ r, g, b });
|
|
106
130
|
}
|
|
107
131
|
}
|
|
108
132
|
}
|
|
@@ -118,28 +142,31 @@ class GIFEncoder {
|
|
|
118
142
|
palette[i * 3 + 1] = colors[i].g;
|
|
119
143
|
palette[i * 3 + 2] = colors[i].b;
|
|
120
144
|
}
|
|
121
|
-
// Create indexed data
|
|
122
|
-
const indexed = new Uint8Array(
|
|
123
|
-
for (let i = 0, j = 0; i < data.length; i += 4, j++) {
|
|
145
|
+
// Create indexed data using frame dimensions
|
|
146
|
+
const indexed = new Uint8Array(frameWidth * frameHeight);
|
|
147
|
+
for (let i = 0, j = 0; i < data.length && j < indexed.length; i += 4, j++) {
|
|
148
|
+
// Handle transparent pixels
|
|
149
|
+
if (data[i + 3] < 128) {
|
|
150
|
+
indexed[j] = transparentIndex;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
124
153
|
let r = data[i];
|
|
125
154
|
let g = data[i + 1];
|
|
126
155
|
let b = data[i + 2];
|
|
127
|
-
// Apply color reduction if it was used for building the palette
|
|
128
156
|
if (useColorReduction) {
|
|
129
157
|
r = this.quantizeChannel(r, RG_LEVELS, rgStep);
|
|
130
158
|
g = this.quantizeChannel(g, RG_LEVELS, rgStep);
|
|
131
159
|
b = this.quantizeChannel(b, B_LEVELS, bStep);
|
|
132
160
|
}
|
|
133
161
|
const key = `${r},${g},${b}`;
|
|
134
|
-
// Try fast O(1) lookup first
|
|
135
162
|
if (colorMap.has(key)) {
|
|
136
163
|
indexed[j] = colorMap.get(key);
|
|
137
164
|
}
|
|
138
165
|
else {
|
|
139
|
-
//
|
|
166
|
+
// Find closest color
|
|
140
167
|
let minDist = Infinity;
|
|
141
|
-
let bestIdx = 0;
|
|
142
|
-
for (let k = 0; k < colors.length; k++) {
|
|
168
|
+
let bestIdx = hasTransparency ? 1 : 0;
|
|
169
|
+
for (let k = hasTransparency ? 1 : 0; k < colors.length; k++) {
|
|
143
170
|
const dr = r - colors[k].r;
|
|
144
171
|
const dg = g - colors[k].g;
|
|
145
172
|
const db = b - colors[k].b;
|
|
@@ -152,7 +179,7 @@ class GIFEncoder {
|
|
|
152
179
|
indexed[j] = bestIdx;
|
|
153
180
|
}
|
|
154
181
|
}
|
|
155
|
-
return { palette, indexed };
|
|
182
|
+
return { palette, indexed, hasTransparency, transparentIndex };
|
|
156
183
|
}
|
|
157
184
|
nextPowerOf2(n) {
|
|
158
185
|
let power = 1;
|
|
@@ -173,46 +200,34 @@ class GIFEncoder {
|
|
|
173
200
|
throw new Error("No frames to encode");
|
|
174
201
|
}
|
|
175
202
|
const output = [];
|
|
176
|
-
// Get loop count from options (default to 0 = infinite)
|
|
177
203
|
const loopCount = options?.loop ?? 0;
|
|
178
204
|
// Quantize first frame for Global Color Table
|
|
179
205
|
const firstFrame = this.frames[0];
|
|
180
|
-
const
|
|
206
|
+
const firstResult = this.quantize(firstFrame.data, firstFrame.width, firstFrame.height);
|
|
207
|
+
const globalPalette = firstResult.palette;
|
|
181
208
|
const paletteSize = globalPalette.length / 3;
|
|
182
209
|
const bitsPerColor = this.getBitsPerColor(paletteSize);
|
|
183
210
|
// Header
|
|
184
211
|
this.writeString(output, "GIF89a");
|
|
185
212
|
// Logical Screen Descriptor
|
|
186
|
-
this.writeUint16LE(output, this.
|
|
187
|
-
this.writeUint16LE(output, this.
|
|
188
|
-
// Packed field:
|
|
189
|
-
// - Global Color Table Flag (1 bit): 1
|
|
190
|
-
// - Color Resolution (3 bits): bitsPerColor - 1
|
|
191
|
-
// - Sort Flag (1 bit): 0
|
|
192
|
-
// - Size of Global Color Table (3 bits): bitsPerColor - 1
|
|
213
|
+
this.writeUint16LE(output, this.canvasWidth);
|
|
214
|
+
this.writeUint16LE(output, this.canvasHeight);
|
|
193
215
|
const packed = 0x80 | ((bitsPerColor - 1) << 4) | (bitsPerColor - 1);
|
|
194
216
|
output.push(packed);
|
|
195
|
-
// Background Color Index
|
|
196
|
-
output.push(0);
|
|
197
|
-
// Pixel Aspect Ratio
|
|
198
|
-
output.push(0);
|
|
217
|
+
output.push(0); // Background Color Index
|
|
218
|
+
output.push(0); // Pixel Aspect Ratio
|
|
199
219
|
// Global Color Table
|
|
200
|
-
// The GCT size is 2^(n+1) where n is the value in the packed field
|
|
201
|
-
// So we need to write that many colors, padding if necessary
|
|
202
220
|
const gctSize = 1 << bitsPerColor;
|
|
203
221
|
const paddedPalette = new Uint8Array(gctSize * 3);
|
|
204
222
|
paddedPalette.set(globalPalette);
|
|
205
223
|
this.writeBytes(output, paddedPalette);
|
|
206
224
|
// Netscape Application Extension (Looping)
|
|
207
225
|
if (this.frames.length > 1) {
|
|
208
|
-
output.push(0x21);
|
|
209
|
-
output.push(0xff); // Application Extension Label
|
|
210
|
-
output.push(11); // Block Size
|
|
226
|
+
output.push(0x21, 0xff, 11);
|
|
211
227
|
this.writeString(output, "NETSCAPE2.0");
|
|
212
|
-
output.push(3);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
output.push(0); // Block Terminator
|
|
228
|
+
output.push(3, 1);
|
|
229
|
+
this.writeUint16LE(output, loopCount);
|
|
230
|
+
output.push(0);
|
|
216
231
|
}
|
|
217
232
|
// Encode frames
|
|
218
233
|
for (let i = 0; i < this.frames.length; i++) {
|
|
@@ -221,67 +236,56 @@ class GIFEncoder {
|
|
|
221
236
|
let useLocalPalette = false;
|
|
222
237
|
let localPalette = null;
|
|
223
238
|
let localBitsPerColor = bitsPerColor;
|
|
239
|
+
let hasTransparency;
|
|
240
|
+
let transparentIndex;
|
|
224
241
|
if (i === 0) {
|
|
225
|
-
indexed =
|
|
242
|
+
indexed = firstResult.indexed;
|
|
243
|
+
hasTransparency = firstResult.hasTransparency;
|
|
244
|
+
transparentIndex = firstResult.transparentIndex;
|
|
226
245
|
}
|
|
227
246
|
else {
|
|
228
|
-
|
|
229
|
-
// For simplicity, we use a Local Color Table for each frame to ensure colors are correct
|
|
230
|
-
const result = this.quantize(frame.data);
|
|
247
|
+
const result = this.quantize(frame.data, frame.width, frame.height);
|
|
231
248
|
indexed = result.indexed;
|
|
232
249
|
localPalette = result.palette;
|
|
233
250
|
useLocalPalette = true;
|
|
234
|
-
|
|
235
|
-
|
|
251
|
+
hasTransparency = result.hasTransparency;
|
|
252
|
+
transparentIndex = result.transparentIndex;
|
|
253
|
+
localBitsPerColor = this.getBitsPerColor(localPalette.length / 3);
|
|
236
254
|
}
|
|
237
255
|
// Graphic Control Extension
|
|
238
|
-
output.push(0x21);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
//
|
|
244
|
-
// User Input Flag (1 bit): 0
|
|
245
|
-
// Transparent Color Flag (1 bit): 0
|
|
246
|
-
output.push(0x08); // Disposal method 2 (Restore to background)
|
|
247
|
-
// Delay Time (1/100ths of a second)
|
|
248
|
-
// Default to 10 (100ms) if not specified
|
|
256
|
+
output.push(0x21, 0xf9, 4);
|
|
257
|
+
// Packed: disposal method + transparency flag
|
|
258
|
+
const disposal = frame.disposal & 0x07;
|
|
259
|
+
const gcePacked = (disposal << 2) | (hasTransparency ? 1 : 0);
|
|
260
|
+
output.push(gcePacked);
|
|
261
|
+
// Delay time
|
|
249
262
|
const delay = frame.delay > 0 ? Math.round(frame.delay / 10) : 10;
|
|
250
263
|
this.writeUint16LE(output, delay);
|
|
251
|
-
// Transparent
|
|
252
|
-
output.push(0);
|
|
264
|
+
// Transparent color index
|
|
265
|
+
output.push(hasTransparency ? transparentIndex : 0);
|
|
253
266
|
output.push(0); // Block Terminator
|
|
254
267
|
// Image Descriptor
|
|
255
|
-
output.push(0x2c);
|
|
256
|
-
this.writeUint16LE(output,
|
|
257
|
-
this.writeUint16LE(output,
|
|
258
|
-
this.writeUint16LE(output,
|
|
259
|
-
this.writeUint16LE(output,
|
|
260
|
-
// Packed Field
|
|
268
|
+
output.push(0x2c);
|
|
269
|
+
this.writeUint16LE(output, frame.left);
|
|
270
|
+
this.writeUint16LE(output, frame.top);
|
|
271
|
+
this.writeUint16LE(output, frame.width);
|
|
272
|
+
this.writeUint16LE(output, frame.height);
|
|
261
273
|
if (useLocalPalette && localPalette) {
|
|
262
|
-
// LCT Flag: 1
|
|
263
|
-
// Interlace: 0
|
|
264
|
-
// Sort: 0
|
|
265
|
-
// Reserved: 0
|
|
266
|
-
// Size of LCT: localBitsPerColor - 1
|
|
267
274
|
const lctPacked = 0x80 | (localBitsPerColor - 1);
|
|
268
275
|
output.push(lctPacked);
|
|
269
|
-
// Write Local Color Table
|
|
270
276
|
const lctSize = 1 << localBitsPerColor;
|
|
271
277
|
const paddedLct = new Uint8Array(lctSize * 3);
|
|
272
278
|
paddedLct.set(localPalette);
|
|
273
279
|
this.writeBytes(output, paddedLct);
|
|
274
280
|
}
|
|
275
281
|
else {
|
|
276
|
-
output.push(0);
|
|
282
|
+
output.push(0);
|
|
277
283
|
}
|
|
278
|
-
// LZW
|
|
284
|
+
// LZW compression
|
|
279
285
|
const minCodeSize = Math.max(2, useLocalPalette ? localBitsPerColor : bitsPerColor);
|
|
280
286
|
output.push(minCodeSize);
|
|
281
|
-
// Compress image data with LZW
|
|
282
287
|
const encoder = new lzw_js_1.LZWEncoder(minCodeSize);
|
|
283
288
|
const compressed = encoder.compress(indexed);
|
|
284
|
-
// Write compressed data in sub-blocks (max 255 bytes per block)
|
|
285
289
|
for (let k = 0; k < compressed.length; k += 255) {
|
|
286
290
|
const blockSize = Math.min(255, compressed.length - k);
|
|
287
291
|
output.push(blockSize);
|
|
@@ -291,8 +295,7 @@ class GIFEncoder {
|
|
|
291
295
|
}
|
|
292
296
|
output.push(0); // Block Terminator
|
|
293
297
|
}
|
|
294
|
-
// Trailer
|
|
295
|
-
output.push(0x3b);
|
|
298
|
+
output.push(0x3b); // Trailer
|
|
296
299
|
return new Uint8Array(output);
|
|
297
300
|
}
|
|
298
301
|
}
|
|
@@ -8,7 +8,17 @@
|
|
|
8
8
|
* This is a pure JavaScript implementation that handles common JPEG files.
|
|
9
9
|
* For complex or non-standard JPEGs, the ImageDecoder API fallback is preferred.
|
|
10
10
|
*/
|
|
11
|
-
import type { ImageDecoderOptions } from "../types.js";
|
|
11
|
+
import type { ImageDecoderOptions, JPEGQuantizedCoefficients } from "../types.js";
|
|
12
|
+
/**
|
|
13
|
+
* Extended decoder options including coefficient extraction
|
|
14
|
+
*/
|
|
15
|
+
interface JPEGDecoderOptions extends ImageDecoderOptions {
|
|
16
|
+
/**
|
|
17
|
+
* When true, stores quantized DCT coefficients for later retrieval
|
|
18
|
+
* via getQuantizedCoefficients(). Coefficients are stored in zigzag order.
|
|
19
|
+
*/
|
|
20
|
+
extractCoefficients?: boolean;
|
|
21
|
+
}
|
|
12
22
|
export declare class JPEGDecoder {
|
|
13
23
|
private data;
|
|
14
24
|
private pos;
|
|
@@ -29,8 +39,20 @@ export declare class JPEGDecoder {
|
|
|
29
39
|
private successiveLow;
|
|
30
40
|
private scanComponentIds;
|
|
31
41
|
private eobRun;
|
|
32
|
-
|
|
42
|
+
private quantizedCoefficients;
|
|
43
|
+
constructor(data: Uint8Array, settings?: JPEGDecoderOptions);
|
|
33
44
|
decode(): Uint8Array;
|
|
45
|
+
/**
|
|
46
|
+
* Get the quantized DCT coefficients after decoding
|
|
47
|
+
* Only available if extractCoefficients option was set to true
|
|
48
|
+
* @returns JPEGQuantizedCoefficients or undefined if not available
|
|
49
|
+
*/
|
|
50
|
+
getQuantizedCoefficients(): JPEGQuantizedCoefficients | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Store quantized coefficients in the output structure
|
|
53
|
+
* Called after decoding when extractCoefficients is true
|
|
54
|
+
*/
|
|
55
|
+
private storeQuantizedCoefficients;
|
|
34
56
|
private readMarker;
|
|
35
57
|
private readUint16;
|
|
36
58
|
private skipSegment;
|
|
@@ -49,4 +71,5 @@ export declare class JPEGDecoder {
|
|
|
49
71
|
private idct;
|
|
50
72
|
private convertToRGB;
|
|
51
73
|
}
|
|
74
|
+
export {};
|
|
52
75
|
//# sourceMappingURL=jpeg_decoder.d.ts.map
|
|
@@ -214,10 +214,18 @@ class JPEGDecoder {
|
|
|
214
214
|
writable: true,
|
|
215
215
|
value: 0
|
|
216
216
|
}); // Remaining blocks to skip due to EOBn
|
|
217
|
+
// Storage for quantized coefficients (when extractCoefficients is true)
|
|
218
|
+
Object.defineProperty(this, "quantizedCoefficients", {
|
|
219
|
+
enumerable: true,
|
|
220
|
+
configurable: true,
|
|
221
|
+
writable: true,
|
|
222
|
+
value: null
|
|
223
|
+
});
|
|
217
224
|
this.data = data;
|
|
218
225
|
this.options = {
|
|
219
226
|
tolerantDecoding: settings.tolerantDecoding ?? true,
|
|
220
227
|
onWarning: settings.onWarning,
|
|
228
|
+
extractCoefficients: settings.extractCoefficients ?? false,
|
|
221
229
|
};
|
|
222
230
|
}
|
|
223
231
|
decode() {
|
|
@@ -279,7 +287,8 @@ class JPEGDecoder {
|
|
|
279
287
|
// For progressive JPEGs, perform IDCT on all blocks after all scans are complete
|
|
280
288
|
// This ensures that frequency-domain coefficients from multiple scans are properly
|
|
281
289
|
// accumulated before transformation to spatial domain
|
|
282
|
-
|
|
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
|