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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Base64 utilities.
|
|
4
|
+
*
|
|
5
|
+
* Designed to work across Deno, Node.js, and Bun without external dependencies.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.encodeBase64 = encodeBase64;
|
|
9
|
+
exports.decodeBase64 = decodeBase64;
|
|
10
|
+
exports.toDataUrl = toDataUrl;
|
|
11
|
+
exports.parseDataUrl = parseDataUrl;
|
|
12
|
+
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
13
|
+
const DECODE_TABLE = (() => {
|
|
14
|
+
const table = new Uint8Array(256);
|
|
15
|
+
table.fill(0xff);
|
|
16
|
+
for (let i = 0; i < BASE64_ALPHABET.length; i++) {
|
|
17
|
+
table[BASE64_ALPHABET.charCodeAt(i)] = i;
|
|
18
|
+
}
|
|
19
|
+
return table;
|
|
20
|
+
})();
|
|
21
|
+
function stripWhitespace(input) {
|
|
22
|
+
// Avoid regex to keep this fast and allocation-light.
|
|
23
|
+
let out = "";
|
|
24
|
+
for (let i = 0; i < input.length; i++) {
|
|
25
|
+
const code = input.charCodeAt(i);
|
|
26
|
+
// Space, tab, CR, LF
|
|
27
|
+
if (code === 0x20 || code === 0x09 || code === 0x0d || code === 0x0a)
|
|
28
|
+
continue;
|
|
29
|
+
out += input[i];
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function normalizeBase64(input) {
|
|
34
|
+
// Accept URL-safe alphabet in decode paths.
|
|
35
|
+
let s = stripWhitespace(input.trim());
|
|
36
|
+
if (s.length === 0)
|
|
37
|
+
return s;
|
|
38
|
+
// Map base64url to base64.
|
|
39
|
+
if (s.includes("-") || s.includes("_")) {
|
|
40
|
+
s = s.replaceAll("-", "+").replaceAll("_", "/");
|
|
41
|
+
}
|
|
42
|
+
const remainder = s.length % 4;
|
|
43
|
+
if (remainder === 1) {
|
|
44
|
+
throw new Error("Invalid base64: length must not be 1 (mod 4)");
|
|
45
|
+
}
|
|
46
|
+
if (remainder === 2)
|
|
47
|
+
s += "==";
|
|
48
|
+
if (remainder === 3)
|
|
49
|
+
s += "=";
|
|
50
|
+
return s;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Encode bytes into a standard Base64 string.
|
|
54
|
+
*/
|
|
55
|
+
function encodeBase64(bytes) {
|
|
56
|
+
if (bytes.length === 0)
|
|
57
|
+
return "";
|
|
58
|
+
const outLen = Math.ceil(bytes.length / 3) * 4;
|
|
59
|
+
const out = new Array(outLen);
|
|
60
|
+
let i = 0;
|
|
61
|
+
let o = 0;
|
|
62
|
+
for (; i + 2 < bytes.length; i += 3) {
|
|
63
|
+
const n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
|
|
64
|
+
out[o++] = BASE64_ALPHABET[(n >> 18) & 63];
|
|
65
|
+
out[o++] = BASE64_ALPHABET[(n >> 12) & 63];
|
|
66
|
+
out[o++] = BASE64_ALPHABET[(n >> 6) & 63];
|
|
67
|
+
out[o++] = BASE64_ALPHABET[n & 63];
|
|
68
|
+
}
|
|
69
|
+
const remaining = bytes.length - i;
|
|
70
|
+
if (remaining === 1) {
|
|
71
|
+
const n = bytes[i] << 16;
|
|
72
|
+
out[o++] = BASE64_ALPHABET[(n >> 18) & 63];
|
|
73
|
+
out[o++] = BASE64_ALPHABET[(n >> 12) & 63];
|
|
74
|
+
out[o++] = "=";
|
|
75
|
+
out[o++] = "=";
|
|
76
|
+
}
|
|
77
|
+
else if (remaining === 2) {
|
|
78
|
+
const n = (bytes[i] << 16) | (bytes[i + 1] << 8);
|
|
79
|
+
out[o++] = BASE64_ALPHABET[(n >> 18) & 63];
|
|
80
|
+
out[o++] = BASE64_ALPHABET[(n >> 12) & 63];
|
|
81
|
+
out[o++] = BASE64_ALPHABET[(n >> 6) & 63];
|
|
82
|
+
out[o++] = "=";
|
|
83
|
+
}
|
|
84
|
+
return out.join("");
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Decode a Base64 (or Base64URL) string into bytes.
|
|
88
|
+
*
|
|
89
|
+
* - Whitespace is ignored.
|
|
90
|
+
* - Missing padding is tolerated.
|
|
91
|
+
* - `-`/`_` are accepted as URL-safe variants.
|
|
92
|
+
*/
|
|
93
|
+
function decodeBase64(base64) {
|
|
94
|
+
const s = normalizeBase64(base64);
|
|
95
|
+
if (s.length === 0)
|
|
96
|
+
return new Uint8Array(0);
|
|
97
|
+
let padding = 0;
|
|
98
|
+
if (s.endsWith("=="))
|
|
99
|
+
padding = 2;
|
|
100
|
+
else if (s.endsWith("="))
|
|
101
|
+
padding = 1;
|
|
102
|
+
const quadCount = s.length / 4;
|
|
103
|
+
const outLen = quadCount * 3 - padding;
|
|
104
|
+
const out = new Uint8Array(outLen);
|
|
105
|
+
let o = 0;
|
|
106
|
+
for (let i = 0; i < s.length; i += 4) {
|
|
107
|
+
const c0 = s.charCodeAt(i);
|
|
108
|
+
const c1 = s.charCodeAt(i + 1);
|
|
109
|
+
const c2 = s.charCodeAt(i + 2);
|
|
110
|
+
const c3 = s.charCodeAt(i + 3);
|
|
111
|
+
const v0 = DECODE_TABLE[c0];
|
|
112
|
+
const v1 = DECODE_TABLE[c1];
|
|
113
|
+
if (v0 === 0xff || v1 === 0xff) {
|
|
114
|
+
throw new Error("Invalid base64: invalid character");
|
|
115
|
+
}
|
|
116
|
+
const isPad2 = c2 === 0x3d; // '='
|
|
117
|
+
const isPad3 = c3 === 0x3d;
|
|
118
|
+
const v2 = isPad2 ? 0 : DECODE_TABLE[c2];
|
|
119
|
+
const v3 = isPad3 ? 0 : DECODE_TABLE[c3];
|
|
120
|
+
if ((!isPad2 && v2 === 0xff) || (!isPad3 && v3 === 0xff)) {
|
|
121
|
+
throw new Error("Invalid base64: invalid character");
|
|
122
|
+
}
|
|
123
|
+
const n = (v0 << 18) | (v1 << 12) | (v2 << 6) | v3;
|
|
124
|
+
out[o++] = (n >> 16) & 0xff;
|
|
125
|
+
if (!isPad2) {
|
|
126
|
+
if (o >= out.length)
|
|
127
|
+
break;
|
|
128
|
+
out[o++] = (n >> 8) & 0xff;
|
|
129
|
+
}
|
|
130
|
+
if (!isPad3) {
|
|
131
|
+
if (o >= out.length)
|
|
132
|
+
break;
|
|
133
|
+
out[o++] = n & 0xff;
|
|
134
|
+
}
|
|
135
|
+
if (isPad2 || isPad3) {
|
|
136
|
+
// Padding is only valid in the final quartet.
|
|
137
|
+
if (i + 4 !== s.length) {
|
|
138
|
+
throw new Error("Invalid base64: padding can only appear at the end");
|
|
139
|
+
}
|
|
140
|
+
// If third char is padding, fourth must also be padding.
|
|
141
|
+
if (isPad2 && !isPad3) {
|
|
142
|
+
throw new Error("Invalid base64: invalid padding");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Create a base64 data URL.
|
|
150
|
+
*/
|
|
151
|
+
function toDataUrl(mime, bytes) {
|
|
152
|
+
if (!mime)
|
|
153
|
+
throw new Error("mime is required");
|
|
154
|
+
return `data:${mime};base64,${encodeBase64(bytes)}`;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Parse a base64 data URL.
|
|
158
|
+
*
|
|
159
|
+
* Supports `data:<mime>;base64,<payload>`.
|
|
160
|
+
*/
|
|
161
|
+
function parseDataUrl(url) {
|
|
162
|
+
if (!url.startsWith("data:")) {
|
|
163
|
+
throw new Error("Invalid data URL: must start with 'data:'");
|
|
164
|
+
}
|
|
165
|
+
const commaIndex = url.indexOf(",");
|
|
166
|
+
if (commaIndex === -1) {
|
|
167
|
+
throw new Error("Invalid data URL: missing ',' separator");
|
|
168
|
+
}
|
|
169
|
+
const meta = url.slice(5, commaIndex);
|
|
170
|
+
const payload = url.slice(commaIndex + 1);
|
|
171
|
+
// We only support base64 payloads.
|
|
172
|
+
const metaParts = meta.split(";");
|
|
173
|
+
const mime = metaParts[0] || "application/octet-stream";
|
|
174
|
+
const isBase64 = metaParts.some((p) => p.toLowerCase() === "base64");
|
|
175
|
+
if (!isBase64) {
|
|
176
|
+
throw new Error("Invalid data URL: only base64 payloads are supported");
|
|
177
|
+
}
|
|
178
|
+
return { mime, bytes: decodeBase64(payload) };
|
|
179
|
+
}
|
|
@@ -168,11 +168,13 @@ class GIFEncoder {
|
|
|
168
168
|
}
|
|
169
169
|
return Math.max(2, bits);
|
|
170
170
|
}
|
|
171
|
-
encode() {
|
|
171
|
+
encode(options) {
|
|
172
172
|
if (this.frames.length === 0) {
|
|
173
173
|
throw new Error("No frames to encode");
|
|
174
174
|
}
|
|
175
175
|
const output = [];
|
|
176
|
+
// Get loop count from options (default to 0 = infinite)
|
|
177
|
+
const loopCount = options?.loop ?? 0;
|
|
176
178
|
// Quantize first frame for Global Color Table
|
|
177
179
|
const firstFrame = this.frames[0];
|
|
178
180
|
const { palette: globalPalette, indexed: firstIndexed } = this.quantize(firstFrame.data);
|
|
@@ -209,7 +211,7 @@ class GIFEncoder {
|
|
|
209
211
|
this.writeString(output, "NETSCAPE2.0");
|
|
210
212
|
output.push(3); // Sub-block Size
|
|
211
213
|
output.push(1); // Loop Indicator (1 = loop)
|
|
212
|
-
this.writeUint16LE(output,
|
|
214
|
+
this.writeUint16LE(output, loopCount); // Loop Count (0 = infinite, 1+ = specific count)
|
|
213
215
|
output.push(0); // Block Terminator
|
|
214
216
|
}
|
|
215
217
|
// Encode frames
|
|
@@ -186,4 +186,35 @@ export declare function flipHorizontal(data: Uint8Array, width: number, height:
|
|
|
186
186
|
* @returns Flipped image data
|
|
187
187
|
*/
|
|
188
188
|
export declare function flipVertical(data: Uint8Array, width: number, height: number): Uint8Array;
|
|
189
|
+
/**
|
|
190
|
+
* Convert RGB color to CMYK color space
|
|
191
|
+
* @param r Red component (0-255)
|
|
192
|
+
* @param g Green component (0-255)
|
|
193
|
+
* @param b Blue component (0-255)
|
|
194
|
+
* @returns CMYK values: [c (0-1), m (0-1), y (0-1), k (0-1)]
|
|
195
|
+
*/
|
|
196
|
+
export declare function rgbToCmyk(r: number, g: number, b: number): [number, number, number, number];
|
|
197
|
+
/**
|
|
198
|
+
* Convert CMYK color to RGB color space
|
|
199
|
+
* @param c Cyan component (0-1)
|
|
200
|
+
* @param m Magenta component (0-1)
|
|
201
|
+
* @param y Yellow component (0-1)
|
|
202
|
+
* @param k Key/Black component (0-1)
|
|
203
|
+
* @returns RGB values: [r (0-255), g (0-255), b (0-255)]
|
|
204
|
+
*/
|
|
205
|
+
export declare function cmykToRgb(c: number, m: number, y: number, k: number): [number, number, number];
|
|
206
|
+
/**
|
|
207
|
+
* Convert RGBA image data to CMYK representation
|
|
208
|
+
* Returns a Float32Array with 4 values per pixel (C, M, Y, K) in 0-1 range
|
|
209
|
+
* @param data Image data (RGBA)
|
|
210
|
+
* @returns CMYK image data as Float32Array (4 values per pixel)
|
|
211
|
+
*/
|
|
212
|
+
export declare function rgbaToCmyk(data: Uint8Array): Float32Array;
|
|
213
|
+
/**
|
|
214
|
+
* Convert CMYK image data to RGBA representation
|
|
215
|
+
* @param cmykData CMYK image data (4 values per pixel in 0-1 range)
|
|
216
|
+
* @param alpha Optional alpha value for all pixels (0-255, default: 255)
|
|
217
|
+
* @returns RGBA image data
|
|
218
|
+
*/
|
|
219
|
+
export declare function cmykToRgba(cmykData: Float32Array, alpha?: number): Uint8Array;
|
|
189
220
|
//# sourceMappingURL=image_processing.d.ts.map
|
|
@@ -24,6 +24,10 @@ exports.rotate180 = rotate180;
|
|
|
24
24
|
exports.rotate270 = rotate270;
|
|
25
25
|
exports.flipHorizontal = flipHorizontal;
|
|
26
26
|
exports.flipVertical = flipVertical;
|
|
27
|
+
exports.rgbToCmyk = rgbToCmyk;
|
|
28
|
+
exports.cmykToRgb = cmykToRgb;
|
|
29
|
+
exports.rgbaToCmyk = rgbaToCmyk;
|
|
30
|
+
exports.cmykToRgba = cmykToRgba;
|
|
27
31
|
/**
|
|
28
32
|
* Detect system endianness
|
|
29
33
|
* Returns true if little-endian (most common), false if big-endian
|
|
@@ -765,3 +769,91 @@ function flipVertical(data, width, height) {
|
|
|
765
769
|
}
|
|
766
770
|
return result;
|
|
767
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* Convert RGB color to CMYK color space
|
|
774
|
+
* @param r Red component (0-255)
|
|
775
|
+
* @param g Green component (0-255)
|
|
776
|
+
* @param b Blue component (0-255)
|
|
777
|
+
* @returns CMYK values: [c (0-1), m (0-1), y (0-1), k (0-1)]
|
|
778
|
+
*/
|
|
779
|
+
function rgbToCmyk(r, g, b) {
|
|
780
|
+
// Normalize RGB values to 0-1 range
|
|
781
|
+
const rNorm = r / 255;
|
|
782
|
+
const gNorm = g / 255;
|
|
783
|
+
const bNorm = b / 255;
|
|
784
|
+
// Calculate K (key/black)
|
|
785
|
+
const k = 1 - Math.max(rNorm, gNorm, bNorm);
|
|
786
|
+
// If K is 1 (pure black), CMY are undefined but conventionally set to 0
|
|
787
|
+
if (k === 1) {
|
|
788
|
+
return [0, 0, 0, 1];
|
|
789
|
+
}
|
|
790
|
+
// Calculate CMY components
|
|
791
|
+
const c = (1 - rNorm - k) / (1 - k);
|
|
792
|
+
const m = (1 - gNorm - k) / (1 - k);
|
|
793
|
+
const y = (1 - bNorm - k) / (1 - k);
|
|
794
|
+
return [c, m, y, k];
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Convert CMYK color to RGB color space
|
|
798
|
+
* @param c Cyan component (0-1)
|
|
799
|
+
* @param m Magenta component (0-1)
|
|
800
|
+
* @param y Yellow component (0-1)
|
|
801
|
+
* @param k Key/Black component (0-1)
|
|
802
|
+
* @returns RGB values: [r (0-255), g (0-255), b (0-255)]
|
|
803
|
+
*/
|
|
804
|
+
function cmykToRgb(c, m, y, k) {
|
|
805
|
+
// Convert CMYK to RGB
|
|
806
|
+
const r = 255 * (1 - c) * (1 - k);
|
|
807
|
+
const g = 255 * (1 - m) * (1 - k);
|
|
808
|
+
const b = 255 * (1 - y) * (1 - k);
|
|
809
|
+
return [
|
|
810
|
+
Math.round(Math.max(0, Math.min(255, r))),
|
|
811
|
+
Math.round(Math.max(0, Math.min(255, g))),
|
|
812
|
+
Math.round(Math.max(0, Math.min(255, b))),
|
|
813
|
+
];
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Convert RGBA image data to CMYK representation
|
|
817
|
+
* Returns a Float32Array with 4 values per pixel (C, M, Y, K) in 0-1 range
|
|
818
|
+
* @param data Image data (RGBA)
|
|
819
|
+
* @returns CMYK image data as Float32Array (4 values per pixel)
|
|
820
|
+
*/
|
|
821
|
+
function rgbaToCmyk(data) {
|
|
822
|
+
const pixelCount = data.length / 4;
|
|
823
|
+
const result = new Float32Array(pixelCount * 4);
|
|
824
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
825
|
+
const r = data[i];
|
|
826
|
+
const g = data[i + 1];
|
|
827
|
+
const b = data[i + 2];
|
|
828
|
+
const [c, m, y, k] = rgbToCmyk(r, g, b);
|
|
829
|
+
const outIdx = (i / 4) * 4;
|
|
830
|
+
result[outIdx] = c;
|
|
831
|
+
result[outIdx + 1] = m;
|
|
832
|
+
result[outIdx + 2] = y;
|
|
833
|
+
result[outIdx + 3] = k;
|
|
834
|
+
}
|
|
835
|
+
return result;
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Convert CMYK image data to RGBA representation
|
|
839
|
+
* @param cmykData CMYK image data (4 values per pixel in 0-1 range)
|
|
840
|
+
* @param alpha Optional alpha value for all pixels (0-255, default: 255)
|
|
841
|
+
* @returns RGBA image data
|
|
842
|
+
*/
|
|
843
|
+
function cmykToRgba(cmykData, alpha = 255) {
|
|
844
|
+
const pixelCount = cmykData.length / 4;
|
|
845
|
+
const result = new Uint8Array(pixelCount * 4);
|
|
846
|
+
for (let i = 0; i < cmykData.length; i += 4) {
|
|
847
|
+
const c = cmykData[i];
|
|
848
|
+
const m = cmykData[i + 1];
|
|
849
|
+
const y = cmykData[i + 2];
|
|
850
|
+
const k = cmykData[i + 3];
|
|
851
|
+
const [r, g, b] = cmykToRgb(c, m, y, k);
|
|
852
|
+
const outIdx = i;
|
|
853
|
+
result[outIdx] = r;
|
|
854
|
+
result[outIdx + 1] = g;
|
|
855
|
+
result[outIdx + 2] = b;
|
|
856
|
+
result[outIdx + 3] = alpha;
|
|
857
|
+
}
|
|
858
|
+
return result;
|
|
859
|
+
}
|
|
@@ -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
|