@photivra/engine 2.21.0 → 2.22.0
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/CHANGELOG.md +118 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +11 -0
- package/dist/capture/c2pa-embed.d.ts +116 -0
- package/dist/capture/c2pa-embed.js +493 -0
- package/dist/capture/c2pa-embed.js.map +1 -0
- package/dist/capture/dng-import.d.ts +73 -8
- package/dist/capture/dng-import.js +315 -80
- package/dist/capture/dng-import.js.map +1 -1
- package/dist/capture/dng-opcodes.d.ts +96 -0
- package/dist/capture/dng-opcodes.js +391 -0
- package/dist/capture/dng-opcodes.js.map +1 -0
- package/dist/capture/dng-profile-maps.d.ts +99 -0
- package/dist/capture/dng-profile-maps.js +381 -0
- package/dist/capture/dng-profile-maps.js.map +1 -0
- package/dist/capture/dng-stage-image.d.ts +23 -0
- package/dist/capture/dng-stage-image.js +65 -0
- package/dist/capture/dng-stage-image.js.map +1 -0
- package/dist/capture/jpeg-xl-tiles.d.ts +22 -0
- package/dist/capture/jpeg-xl-tiles.js +151 -0
- package/dist/capture/jpeg-xl-tiles.js.map +1 -0
- package/dist/capture/lossy-jpeg.d.ts +17 -0
- package/dist/capture/lossy-jpeg.js +369 -0
- package/dist/capture/lossy-jpeg.js.map +1 -0
- package/dist/capture/native-raw-development.d.ts +2 -1
- package/dist/capture/native-raw-development.js.map +1 -1
- package/dist/color/neighbor-highlight-reconstruction.d.ts +66 -0
- package/dist/color/neighbor-highlight-reconstruction.js +234 -0
- package/dist/color/neighbor-highlight-reconstruction.js.map +1 -0
- package/dist/color/sensor-color-development.d.ts +5 -0
- package/dist/color/sensor-color-development.js +7 -1
- package/dist/color/sensor-color-development.js.map +1 -1
- package/dist/color/spectral-spikiness.d.ts +11 -0
- package/dist/color/spectral-spikiness.js +23 -0
- package/dist/color/spectral-spikiness.js.map +1 -0
- package/dist/core/execution-error.d.ts +21 -0
- package/dist/core/execution-error.js +12 -0
- package/dist/core/execution-error.js.map +1 -0
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/facade/catalogue-lens-signal.d.ts +26 -1
- package/dist/facade/catalogue-lens-signal.js +25 -7
- package/dist/facade/catalogue-lens-signal.js.map +1 -1
- package/dist/facade/diffraction-provider.d.ts +1 -1
- package/dist/facade/diffraction-provider.js +4 -4
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view-gpu-session.js +38 -6
- package/dist/facade/display-view-gpu-session.js.map +1 -1
- package/dist/facade/display-view-gpu.d.ts +13 -5
- package/dist/facade/display-view-gpu.js.map +1 -1
- package/dist/facade/display-view-normal.d.ts +15 -0
- package/dist/facade/display-view-normal.js +44 -0
- package/dist/facade/display-view-normal.js.map +1 -0
- package/dist/facade/display-view-rendering.d.ts +97 -0
- package/dist/facade/display-view-rendering.js +598 -0
- package/dist/facade/display-view-rendering.js.map +1 -0
- package/dist/facade/display-view.d.ts +37 -65
- package/dist/facade/display-view.js +147 -476
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.js +2 -2
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/fast-methods.d.ts +1 -1
- package/dist/facade/fast-methods.js +37 -8
- package/dist/facade/fast-methods.js.map +1 -1
- package/dist/facade/generic-lens-capture.d.ts +1 -1
- package/dist/facade/generic-lens-capture.js +1 -1
- package/dist/facade/generic-lens-capture.js.map +1 -1
- package/dist/facade/image-plane.js +41 -10
- package/dist/facade/image-plane.js.map +1 -1
- package/dist/facade/on-demand-kits.d.ts +14 -0
- package/dist/facade/on-demand-kits.js +36 -0
- package/dist/facade/on-demand-kits.js.map +1 -0
- package/dist/facade/opt-in-kit.d.ts +9 -0
- package/dist/facade/opt-in-kit.js +11 -0
- package/dist/facade/opt-in-kit.js.map +1 -0
- package/dist/facade/quality-kit.d.ts +21 -0
- package/dist/facade/quality-kit.js +23 -0
- package/dist/facade/quality-kit.js.map +1 -0
- package/dist/facade/quality-paths.d.ts +22 -0
- package/dist/facade/quality-paths.js +44 -0
- package/dist/facade/quality-paths.js.map +1 -0
- package/dist/facade/scene-radiance.d.ts +18 -2
- package/dist/facade/scene-radiance.js +6 -4
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +32 -13
- package/dist/facade/simulate-capture.js +162 -69
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +10 -12
- package/dist/facade/simulate-display-view.js +57 -48
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/display-view-gpu-renderer.d.ts +43 -0
- package/dist/gpu/display-view-gpu-renderer.js +134 -0
- package/dist/gpu/display-view-gpu-renderer.js.map +1 -0
- package/dist/gpu/display-view-image-material-module.d.ts +3 -2
- package/dist/gpu/display-view-image-material-module.js +25 -3
- package/dist/gpu/display-view-image-material-module.js.map +1 -1
- package/dist/gpu/display-view-image-quality.d.ts +43 -1
- package/dist/gpu/display-view-image-quality.js +256 -11
- package/dist/gpu/display-view-image-quality.js.map +1 -1
- package/dist/gpu/display-view-quality-gpu.d.ts +13 -64
- package/dist/gpu/display-view-quality-gpu.js +60 -249
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/display-view-speed-gpu.d.ts +16 -16
- package/dist/gpu/display-view-speed-gpu.js +50 -12
- package/dist/gpu/display-view-speed-gpu.js.map +1 -1
- package/dist/gpu/image-kernel-accelerator.js +3 -0
- package/dist/gpu/image-kernel-accelerator.js.map +1 -1
- package/dist/gpu/image-kernel-execution.d.ts +2 -13
- package/dist/gpu/image-kernel-execution.js +2 -10
- package/dist/gpu/image-kernel-execution.js.map +1 -1
- package/dist/gpu/wgsl-display-view-coupled-stub.d.ts +8 -0
- package/dist/gpu/wgsl-display-view-coupled-stub.js +21 -0
- package/dist/gpu/wgsl-display-view-coupled-stub.js.map +1 -0
- package/dist/gpu/wgsl-display-view-coupled.d.ts +88 -0
- package/dist/gpu/wgsl-display-view-coupled.js +353 -0
- package/dist/gpu/wgsl-display-view-coupled.js.map +1 -0
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +4 -9
- package/dist/gpu/wgsl-display-view-quality-kernel.js +109 -111
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +1 -41
- package/dist/gpu/wgsl-display-view-speed-kernel.js +71 -8
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/output/camera-channel-response.d.ts +50 -0
- package/dist/output/camera-channel-response.js +69 -0
- package/dist/output/camera-channel-response.js.map +1 -0
- package/dist/output/native-capture-sdr.js +5 -2
- package/dist/output/native-capture-sdr.js.map +1 -1
- package/dist/output/sdr-rendering.d.ts +18 -1
- package/dist/output/sdr-rendering.js +39 -2
- package/dist/output/sdr-rendering.js.map +1 -1
- package/dist/sensor/native-sensor-report.d.ts +10 -0
- package/dist/sensor/native-sensor-report.js +16 -0
- package/dist/sensor/native-sensor-report.js.map +1 -0
- package/dist/sensor/sensor-characteristics.d.ts +81 -13
- package/dist/sensor/sensor-characteristics.js +10 -6
- package/dist/sensor/sensor-characteristics.js.map +1 -1
- package/docs/API_REFERENCE.md +117 -4
- package/docs/QUICK_START.md +37 -8
- package/docs/USAGE.md +104 -22
- package/docs/api/capture-c2pa-embed.md +177 -0
- package/docs/api/capture-dng-import.md +86 -10
- package/docs/api/capture-dng-opcodes.md +93 -0
- package/docs/api/capture-dng-profile-maps.md +141 -0
- package/docs/api/capture-dng-stage-image.md +43 -0
- package/docs/api/capture-jpeg-xl-tiles.md +33 -0
- package/docs/api/capture-lossy-jpeg.md +41 -0
- package/docs/api/capture-native-raw-development.md +2 -1
- package/docs/api/core-execution-error.md +43 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +255 -5
- package/docs/api/facade-display-view-gpu.md +5 -2
- package/docs/api/facade-display-view.md +11 -15
- package/docs/api/facade-fast-methods.md +38 -9
- package/docs/api/facade-quality-paths.md +24 -0
- package/docs/api/facade-scene-radiance.md +1 -1
- package/docs/api/facade-simulate-capture.md +24 -7
- package/docs/api/facade-simulate-display-view.md +10 -12
- package/docs/api/gpu-image-kernel-execution.md +0 -36
- package/docs/api/output-camera-channel-response.md +55 -0
- package/docs/api/output-sdr-rendering.md +6 -1
- package/docs/api/sensor-sensor-characteristics.md +54 -2
- package/llms.txt +1 -0
- package/package.json +6 -5
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* #598: JPEG XL (DNG Compression 52546, DNG 1.7.0.0) strips and tiles. The engine has no JPEG XL decoder and no runtime
|
|
4
|
+
* dependency: the caller passes one (`importDngRawImage`'s `jpegXlDecoder`), which returns each tile as a PNG. The owner chose the
|
|
5
|
+
* prebuilt jxl-oxide WebAssembly package (npm `jxl-oxide-wasm`, MIT OR Apache-2.0); `createJxlOxideDngDecoder` adapts it without
|
|
6
|
+
* importing it, so the decoder is loaded only by an application that opens such a file (THIRD_PARTY.md, "JPEG XL").
|
|
7
|
+
*
|
|
8
|
+
* The PNG is read here (W3C PNG Specification, 3rd edition: IHDR, IDAT, IEND; zlib per RFC 1950/1951 through the platform's
|
|
9
|
+
* `DecompressionStream("deflate")`; the five filter types of §9). It must be grey or RGB with no alpha, the tile's size, and 16-bit
|
|
10
|
+
* for a tile of more than 8 bits. jxl-oxide writes a sample of bit depth b at 16 bits as round(v·65535/(2^b − 1)), so the stored
|
|
11
|
+
* value is recovered exactly as round(p·(2^b − 1)/65535) (each step of 65535/(2^b − 1) ≥ 1 maps to a distinct value; measured
|
|
12
|
+
* bit-exact for 8-, 12- and 16-bit lossless tiles in test/dng-import-phone.test.ts).
|
|
13
|
+
*/
|
|
14
|
+
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
15
|
+
/**
|
|
16
|
+
* A {@link DngJpegXlDecoder} over jxl-oxide-wasm: `forceSrgb` off (the samples stay in the codestream's encoding), one render per
|
|
17
|
+
* tile, and the image object freed after it. Usage: `import init, * as jxl from "jxl-oxide-wasm"; await init();
|
|
18
|
+
* createJxlOxideDngDecoder(jxl)`.
|
|
19
|
+
*/
|
|
20
|
+
export function createJxlOxideDngDecoder(module) {
|
|
21
|
+
if (module === null || typeof module !== "object" || typeof module.JxlImage !== "function")
|
|
22
|
+
throw new InvalidConfigurationError("createJxlOxideDngDecoder: module must be the initialized jxl-oxide-wasm module.");
|
|
23
|
+
return (codestream) => {
|
|
24
|
+
const image = new module.JxlImage();
|
|
25
|
+
try {
|
|
26
|
+
image.forceSrgb = false;
|
|
27
|
+
image.feedBytes(codestream);
|
|
28
|
+
if (!image.tryInit())
|
|
29
|
+
throw new InvalidConfigurationError("DNG import: the JPEG XL tile is truncated.");
|
|
30
|
+
// encodeToPng takes the render result by value (it is freed by the call; a later free() fails).
|
|
31
|
+
return image.render().encodeToPng();
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
image.free();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function fail(message) { throw new InvalidConfigurationError("DNG import: " + message); }
|
|
39
|
+
/** @internal The decoded tile's samples at the DNG's bit depth, interleaved (pixel, channel). */
|
|
40
|
+
export async function readJpegXlTilePng(png, width, height, channels, bits) {
|
|
41
|
+
if (!(png instanceof Uint8Array) || png.length < 33 || png[0] !== 0x89 || png[1] !== 0x50 || png[2] !== 0x4e || png[3] !== 0x47)
|
|
42
|
+
fail("the JPEG XL decoder did not return a PNG.");
|
|
43
|
+
const view = new DataView(png.buffer, png.byteOffset, png.byteLength);
|
|
44
|
+
let p = 8, depth = 0, colorType = -1, w = 0, h = 0, idatBytes = 0;
|
|
45
|
+
const idat = [];
|
|
46
|
+
while (p + 12 <= png.length) {
|
|
47
|
+
const length = view.getUint32(p), type = String.fromCharCode(png[p + 4], png[p + 5], png[p + 6], png[p + 7]);
|
|
48
|
+
if (p + 12 + length > png.length)
|
|
49
|
+
fail("the decoded PNG is truncated.");
|
|
50
|
+
if (type === "IHDR") {
|
|
51
|
+
w = view.getUint32(p + 8);
|
|
52
|
+
h = view.getUint32(p + 12);
|
|
53
|
+
depth = png[p + 16];
|
|
54
|
+
colorType = png[p + 17];
|
|
55
|
+
if (png[p + 20] !== 0)
|
|
56
|
+
fail("the decoded PNG is interlaced.");
|
|
57
|
+
}
|
|
58
|
+
else if (type === "IDAT") {
|
|
59
|
+
idat.push(png.subarray(p + 8, p + 8 + length));
|
|
60
|
+
idatBytes += length;
|
|
61
|
+
}
|
|
62
|
+
else if (type === "IEND")
|
|
63
|
+
break;
|
|
64
|
+
p += 12 + length;
|
|
65
|
+
}
|
|
66
|
+
const pngChannels = colorType === 0 ? 1 : colorType === 2 ? 3 : 0;
|
|
67
|
+
if (!pngChannels)
|
|
68
|
+
fail("the decoded JPEG XL tile has an alpha or palette channel; DNG tiles are grey or RGB.");
|
|
69
|
+
if (w !== width || h !== height || pngChannels !== channels)
|
|
70
|
+
fail("the decoded JPEG XL tile does not match the strip or tile size and SamplesPerPixel.");
|
|
71
|
+
if (depth !== 16 && !(depth === 8 && bits === 8))
|
|
72
|
+
fail("the JPEG XL decoder returned " + depth + "-bit samples for a " + bits + "-bit tile.");
|
|
73
|
+
const joined = new Uint8Array(idatBytes);
|
|
74
|
+
let at = 0;
|
|
75
|
+
for (const part of idat) {
|
|
76
|
+
joined.set(part, at);
|
|
77
|
+
at += part.length;
|
|
78
|
+
}
|
|
79
|
+
const bpp = channels * depth / 8, stride = 1 + width * bpp, expected = stride * height;
|
|
80
|
+
const raw = await inflate(joined, expected);
|
|
81
|
+
const out = new Uint16Array(width * height * channels), scale = (2 ** bits - 1) / 65535;
|
|
82
|
+
let previous = new Uint8Array(stride - 1), row = new Uint8Array(stride - 1);
|
|
83
|
+
for (let y = 0; y < height; y++) {
|
|
84
|
+
const filter = raw[y * stride];
|
|
85
|
+
row.set(raw.subarray(y * stride + 1, (y + 1) * stride));
|
|
86
|
+
for (let i = 0; i < row.length; i++) {
|
|
87
|
+
const a = i >= bpp ? row[i - bpp] : 0, b = previous[i], c = i >= bpp ? previous[i - bpp] : 0;
|
|
88
|
+
let predictor = 0;
|
|
89
|
+
switch (filter) {
|
|
90
|
+
case 0: break;
|
|
91
|
+
case 1:
|
|
92
|
+
predictor = a;
|
|
93
|
+
break;
|
|
94
|
+
case 2:
|
|
95
|
+
predictor = b;
|
|
96
|
+
break;
|
|
97
|
+
case 3:
|
|
98
|
+
predictor = (a + b) >> 1;
|
|
99
|
+
break;
|
|
100
|
+
case 4: {
|
|
101
|
+
const pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - 2 * c);
|
|
102
|
+
predictor = pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
default: fail("the decoded PNG has an invalid filter type.");
|
|
106
|
+
}
|
|
107
|
+
row[i] = (row[i] + predictor) & 255;
|
|
108
|
+
}
|
|
109
|
+
const o = y * width * channels;
|
|
110
|
+
if (depth === 8)
|
|
111
|
+
for (let i = 0; i < width * channels; i++)
|
|
112
|
+
out[o + i] = row[i];
|
|
113
|
+
else
|
|
114
|
+
for (let i = 0; i < width * channels; i++) {
|
|
115
|
+
const v = (row[2 * i] << 8) | row[2 * i + 1];
|
|
116
|
+
out[o + i] = bits === 16 ? v : Math.round(v * scale);
|
|
117
|
+
}
|
|
118
|
+
[previous, row] = [row, previous];
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
/** zlib inflate through the platform's DecompressionStream; the output length must be exactly `expected`. */
|
|
123
|
+
async function inflate(data, expected) {
|
|
124
|
+
if (typeof DecompressionStream !== "function")
|
|
125
|
+
fail("this platform has no DecompressionStream.");
|
|
126
|
+
const out = new Uint8Array(expected);
|
|
127
|
+
let at = 0;
|
|
128
|
+
try {
|
|
129
|
+
const reader = new Blob([data]).stream().pipeThrough(new DecompressionStream("deflate")).getReader();
|
|
130
|
+
for (;;) {
|
|
131
|
+
const { done, value } = await reader.read();
|
|
132
|
+
if (done)
|
|
133
|
+
break;
|
|
134
|
+
if (at + value.length > expected) {
|
|
135
|
+
await reader.cancel();
|
|
136
|
+
fail("the decoded PNG holds more data than its size.");
|
|
137
|
+
}
|
|
138
|
+
out.set(value, at);
|
|
139
|
+
at += value.length;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (error instanceof InvalidConfigurationError)
|
|
144
|
+
throw error;
|
|
145
|
+
fail("the decoded PNG's image data is corrupt.");
|
|
146
|
+
}
|
|
147
|
+
if (at !== expected)
|
|
148
|
+
fail("the decoded PNG holds less data than its size.");
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=jpeg-xl-tiles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jpeg-xl-tiles.js","sourceRoot":"","sources":["../../src/capture/jpeg-xl-tiles.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * #598: JPEG XL (DNG Compression 52546, DNG 1.7.0.0) strips and tiles. The engine has no JPEG XL decoder and no runtime\n * dependency: the caller passes one (`importDngRawImage`'s `jpegXlDecoder`), which returns each tile as a PNG. The owner chose the\n * prebuilt jxl-oxide WebAssembly package (npm `jxl-oxide-wasm`, MIT OR Apache-2.0); `createJxlOxideDngDecoder` adapts it without\n * importing it, so the decoder is loaded only by an application that opens such a file (THIRD_PARTY.md, \"JPEG XL\").\n *\n * The PNG is read here (W3C PNG Specification, 3rd edition: IHDR, IDAT, IEND; zlib per RFC 1950/1951 through the platform's\n * `DecompressionStream(\"deflate\")`; the five filter types of §9). It must be grey or RGB with no alpha, the tile's size, and 16-bit\n * for a tile of more than 8 bits. jxl-oxide writes a sample of bit depth b at 16 bits as round(v·65535/(2^b − 1)), so the stored\n * value is recovered exactly as round(p·(2^b − 1)/65535) (each step of 65535/(2^b − 1) ≥ 1 maps to a distinct value; measured\n * bit-exact for 8-, 12- and 16-bit lossless tiles in test/dng-import-phone.test.ts).\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\n/** Decodes one JPEG XL codestream (a DNG strip or tile) to PNG bytes in the codestream's own colour encoding (no colour conversion). */\nexport type DngJpegXlDecoder = (codestream: Uint8Array) => Uint8Array | Promise<Uint8Array>;\n\n/** The part of the jxl-oxide-wasm module the adapter uses (its `JxlImage` class, after the module's `init`). */\nexport interface JxlOxideModule {\n JxlImage: new () => { feedBytes(bytes: Uint8Array): unknown; tryInit(): boolean; render(): { encodeToPng(): Uint8Array }; forceSrgb: boolean; free(): void };\n}\n\n/**\n * A {@link DngJpegXlDecoder} over jxl-oxide-wasm: `forceSrgb` off (the samples stay in the codestream's encoding), one render per\n * tile, and the image object freed after it. Usage: `import init, * as jxl from \"jxl-oxide-wasm\"; await init();\n * createJxlOxideDngDecoder(jxl)`.\n */\nexport function createJxlOxideDngDecoder(module: JxlOxideModule): DngJpegXlDecoder {\n if (module === null || typeof module !== \"object\" || typeof module.JxlImage !== \"function\") throw new InvalidConfigurationError(\"createJxlOxideDngDecoder: module must be the initialized jxl-oxide-wasm module.\");\n return (codestream) => {\n const image = new module.JxlImage();\n try {\n image.forceSrgb = false;\n image.feedBytes(codestream);\n if (!image.tryInit()) throw new InvalidConfigurationError(\"DNG import: the JPEG XL tile is truncated.\");\n // encodeToPng takes the render result by value (it is freed by the call; a later free() fails).\n return image.render().encodeToPng();\n } finally { image.free(); }\n };\n}\n\nfunction fail(message: string): never { throw new InvalidConfigurationError(\"DNG import: \" + message); }\n\n/** @internal The decoded tile's samples at the DNG's bit depth, interleaved (pixel, channel). */\nexport async function readJpegXlTilePng(png: Uint8Array, width: number, height: number, channels: number, bits: number): Promise<Uint16Array> {\n if (!(png instanceof Uint8Array) || png.length < 33 || png[0] !== 0x89 || png[1] !== 0x50 || png[2] !== 0x4e || png[3] !== 0x47) fail(\"the JPEG XL decoder did not return a PNG.\");\n const view = new DataView(png.buffer, png.byteOffset, png.byteLength);\n let p = 8, depth = 0, colorType = -1, w = 0, h = 0, idatBytes = 0;\n const idat: Uint8Array[] = [];\n while (p + 12 <= png.length) {\n const length = view.getUint32(p), type = String.fromCharCode(png[p + 4]!, png[p + 5]!, png[p + 6]!, png[p + 7]!);\n if (p + 12 + length > png.length) fail(\"the decoded PNG is truncated.\");\n if (type === \"IHDR\") {\n w = view.getUint32(p + 8); h = view.getUint32(p + 12); depth = png[p + 16]!; colorType = png[p + 17]!;\n if (png[p + 20] !== 0) fail(\"the decoded PNG is interlaced.\");\n } else if (type === \"IDAT\") { idat.push(png.subarray(p + 8, p + 8 + length)); idatBytes += length; }\n else if (type === \"IEND\") break;\n p += 12 + length;\n }\n const pngChannels = colorType === 0 ? 1 : colorType === 2 ? 3 : 0;\n if (!pngChannels) fail(\"the decoded JPEG XL tile has an alpha or palette channel; DNG tiles are grey or RGB.\");\n if (w !== width || h !== height || pngChannels !== channels) fail(\"the decoded JPEG XL tile does not match the strip or tile size and SamplesPerPixel.\");\n if (depth !== 16 && !(depth === 8 && bits === 8)) fail(\"the JPEG XL decoder returned \" + depth + \"-bit samples for a \" + bits + \"-bit tile.\");\n const joined = new Uint8Array(idatBytes);\n let at = 0; for (const part of idat) { joined.set(part, at); at += part.length; }\n const bpp = channels * depth / 8, stride = 1 + width * bpp, expected = stride * height;\n const raw = await inflate(joined, expected);\n const out = new Uint16Array(width * height * channels), scale = (2 ** bits - 1) / 65535;\n let previous = new Uint8Array(stride - 1), row = new Uint8Array(stride - 1);\n for (let y = 0; y < height; y++) {\n const filter = raw[y * stride]!;\n row.set(raw.subarray(y * stride + 1, (y + 1) * stride));\n for (let i = 0; i < row.length; i++) {\n const a = i >= bpp ? row[i - bpp]! : 0, b = previous[i]!, c = i >= bpp ? previous[i - bpp]! : 0;\n let predictor = 0;\n switch (filter) {\n case 0: break;\n case 1: predictor = a; break;\n case 2: predictor = b; break;\n case 3: predictor = (a + b) >> 1; break;\n case 4: { const pa = Math.abs(b - c), pb = Math.abs(a - c), pc = Math.abs(a + b - 2 * c); predictor = pa <= pb && pa <= pc ? a : pb <= pc ? b : c; break; }\n default: fail(\"the decoded PNG has an invalid filter type.\");\n }\n row[i] = (row[i]! + predictor) & 255;\n }\n const o = y * width * channels;\n if (depth === 8) for (let i = 0; i < width * channels; i++) out[o + i] = row[i]!;\n else for (let i = 0; i < width * channels; i++) {\n const v = (row[2 * i]! << 8) | row[2 * i + 1]!;\n out[o + i] = bits === 16 ? v : Math.round(v * scale);\n }\n [previous, row] = [row, previous];\n }\n return out;\n}\n\n/** zlib inflate through the platform's DecompressionStream; the output length must be exactly `expected`. */\nasync function inflate(data: Uint8Array, expected: number): Promise<Uint8Array> {\n if (typeof DecompressionStream !== \"function\") fail(\"this platform has no DecompressionStream.\");\n const out = new Uint8Array(expected);\n let at = 0;\n try {\n const reader = new Blob([data as Uint8Array<ArrayBuffer>]).stream().pipeThrough(new DecompressionStream(\"deflate\")).getReader();\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (at + value.length > expected) { await reader.cancel(); fail(\"the decoded PNG holds more data than its size.\"); }\n out.set(value, at); at += value.length;\n }\n } catch (error) {\n if (error instanceof InvalidConfigurationError) throw error;\n fail(\"the decoded PNG's image data is corrupt.\");\n }\n if (at !== expected) fail(\"the decoded PNG holds less data than its size.\");\n return out;\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Decoded samples, interleaved by component within each line (line, x, component), 0..255. */
|
|
2
|
+
export interface LossyJpegImage {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
components: number;
|
|
6
|
+
/** "ycbcr-to-rgb" when three YCbCr components were converted to RGB; "none" otherwise. */
|
|
7
|
+
colorTransform: "ycbcr-to-rgb" | "none";
|
|
8
|
+
samples: Uint8Array;
|
|
9
|
+
}
|
|
10
|
+
export interface LossyJpegLimits {
|
|
11
|
+
/** Largest width x height x components accepted, checked before any sample buffer is allocated. */
|
|
12
|
+
maximumSamples: number;
|
|
13
|
+
/** An abbreviated table-specification stream (SOI, DQT/DHT, EOI) read before `bytes`, as DNG's JPEGTables tag (347) supplies. */
|
|
14
|
+
tables?: Uint8Array;
|
|
15
|
+
}
|
|
16
|
+
/** Decodes one DCT JPEG stream (see the module notes). Every length, count and table index is checked against the buffer. */
|
|
17
|
+
export declare function parseLossyJpeg(bytes: Uint8Array, limits: LossyJpegLimits): LossyJpegImage;
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Size-limited decoder for 8-bit sequential DCT JPEG (ITU-T T.81 | ISO/IEC 10918-1, processes 1 and 2: SOF0 baseline and SOF1
|
|
4
|
+
* extended sequential with Huffman coding), the variant DNG uses for Compression = 34892 ("lossy JPEG", DNG 1.4.0.0) strips and
|
|
5
|
+
* tiles. Written independently from the published standard, with no third-party decoder source. Browser-safe: plain typed arrays.
|
|
6
|
+
*
|
|
7
|
+
* Decoding follows T.81 Annex F (Huffman decoding F.2.2, DC prediction, run-length AC decoding, restart intervals F.2.2.5) and the
|
|
8
|
+
* inverse DCT of A.3.3, evaluated directly in double precision with the correctly rounded cosine table `JPEG_DCT_COSINES`, then
|
|
9
|
+
* rounded half away from zero, level-shifted by 128 and clamped to 0..255 (A.3.1). DCT decoders may differ by one code from each
|
|
10
|
+
* other (T.81 leaves the IDCT accuracy to ITU-T T.83 compliance limits); this one is deterministic.
|
|
11
|
+
*
|
|
12
|
+
* Subsampled components are interpolated linearly to full size with centred sample siting (T.871, JFIF: chroma samples sit at the
|
|
13
|
+
* centre of the luma samples they cover), so a 2:1 factor uses the weights 3/4 and 1/4. Three-component data is converted from
|
|
14
|
+
* YCbCr to RGB with the T.871 equations unless the stream says it is RGB: an APP14 "Adobe" segment with transform 0, or, without
|
|
15
|
+
* JFIF or APP14 segments, component identifiers "R", "G", "B" (the conventions ITU-T T.872 describes). Four components are
|
|
16
|
+
* returned as stored.
|
|
17
|
+
*
|
|
18
|
+
* Supported: precision 8, 1..4 components with sampling factors 1..4, interleaved and non-interleaved sequential scans, DRI restart
|
|
19
|
+
* intervals, 8- and 16-bit quantization tables, tables defined in a separate abbreviated stream (DNG JPEGTables). Rejected:
|
|
20
|
+
* progressive, hierarchical, lossless and arithmetic-coded frames, 12-bit precision, truncated data.
|
|
21
|
+
*/
|
|
22
|
+
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
23
|
+
import { JPEG_DCT_COSINES } from "./export-jpeg.js";
|
|
24
|
+
function fail(message) { throw new InvalidConfigurationError("Lossy JPEG: " + message); }
|
|
25
|
+
const ZIGZAG = /* @__PURE__ */ (() => {
|
|
26
|
+
const z = new Int32Array(64);
|
|
27
|
+
for (let d = 0, i = 0; d <= 14; d++) {
|
|
28
|
+
const points = [];
|
|
29
|
+
for (let y = 0; y < 8; y++) {
|
|
30
|
+
const x = d - y;
|
|
31
|
+
if (x >= 0 && x < 8)
|
|
32
|
+
points.push(y * 8 + x);
|
|
33
|
+
}
|
|
34
|
+
for (const p of d % 2 === 0 ? points.reverse() : points)
|
|
35
|
+
z[i++] = p;
|
|
36
|
+
}
|
|
37
|
+
return z;
|
|
38
|
+
})();
|
|
39
|
+
/** T.81 Annex C code generation and F.2.2.3 decoder tables. */
|
|
40
|
+
function buildHuffman(counts, values) {
|
|
41
|
+
const maxCode = new Int32Array(18).fill(-1), minCode = new Int32Array(17), valPtr = new Int32Array(17);
|
|
42
|
+
let code = 0, k = 0;
|
|
43
|
+
for (let length = 1; length <= 16; length++) {
|
|
44
|
+
const n = counts[length - 1];
|
|
45
|
+
if (n) {
|
|
46
|
+
valPtr[length] = k;
|
|
47
|
+
minCode[length] = code;
|
|
48
|
+
code += n;
|
|
49
|
+
k += n;
|
|
50
|
+
maxCode[length] = code - 1;
|
|
51
|
+
}
|
|
52
|
+
if (code > (1 << length))
|
|
53
|
+
fail("over-subscribed Huffman table.");
|
|
54
|
+
code <<= 1;
|
|
55
|
+
}
|
|
56
|
+
maxCode[17] = 0x7fffffff;
|
|
57
|
+
return { maxCode, minCode, valPtr, values };
|
|
58
|
+
}
|
|
59
|
+
/** Decodes one DCT JPEG stream (see the module notes). Every length, count and table index is checked against the buffer. */
|
|
60
|
+
export function parseLossyJpeg(bytes, limits) {
|
|
61
|
+
if (!(bytes instanceof Uint8Array))
|
|
62
|
+
fail("input must be a Uint8Array.");
|
|
63
|
+
if (limits === null || typeof limits !== "object" || !Number.isSafeInteger(limits.maximumSamples) || limits.maximumSamples < 1)
|
|
64
|
+
fail("invalid sample limit.");
|
|
65
|
+
if (limits.tables !== undefined && !(limits.tables instanceof Uint8Array))
|
|
66
|
+
fail("tables must be a Uint8Array.");
|
|
67
|
+
const quant = [], dcTables = [], acTables = [];
|
|
68
|
+
const state = { restart: 0, jfif: false, adobe: -1 };
|
|
69
|
+
let scanned = false;
|
|
70
|
+
let frame;
|
|
71
|
+
const segments = (data, tablesOnly) => {
|
|
72
|
+
const n = data.length;
|
|
73
|
+
if (n < 4 || data[0] !== 0xff || data[1] !== 0xd8)
|
|
74
|
+
fail("missing SOI marker.");
|
|
75
|
+
let p = 2;
|
|
76
|
+
for (;;) {
|
|
77
|
+
while (p < n && data[p] === 0xff && data[p + 1] === 0xff)
|
|
78
|
+
p++;
|
|
79
|
+
if (p + 2 > n || data[p] !== 0xff)
|
|
80
|
+
fail("malformed marker segment.");
|
|
81
|
+
const marker = data[p + 1];
|
|
82
|
+
if (marker === 0xd9) {
|
|
83
|
+
if (tablesOnly || scanned)
|
|
84
|
+
return;
|
|
85
|
+
fail("no scan before EOI.");
|
|
86
|
+
}
|
|
87
|
+
if (p + 4 > n)
|
|
88
|
+
fail("malformed marker segment.");
|
|
89
|
+
const size = data[p + 2] * 256 + data[p + 3], start = p + 4, end = p + 2 + size;
|
|
90
|
+
if (size < 2 || end > n)
|
|
91
|
+
fail("segment exceeds the stream.");
|
|
92
|
+
if (marker === 0xdb) {
|
|
93
|
+
for (let q = start; q < end;) {
|
|
94
|
+
const pq = data[q] >> 4, tq = data[q] & 15, len = pq ? 128 : 64;
|
|
95
|
+
if (pq > 1 || tq > 3 || q + 1 + len > end)
|
|
96
|
+
fail("invalid quantization table.");
|
|
97
|
+
const table = new Float64Array(64);
|
|
98
|
+
for (let i = 0; i < 64; i++) {
|
|
99
|
+
const v = pq ? data[q + 1 + i * 2] * 256 + data[q + 2 + i * 2] : data[q + 1 + i];
|
|
100
|
+
if (v === 0)
|
|
101
|
+
fail("zero quantization value.");
|
|
102
|
+
table[ZIGZAG[i]] = v;
|
|
103
|
+
}
|
|
104
|
+
quant[tq] = table;
|
|
105
|
+
q += 1 + len;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (marker === 0xc4) {
|
|
109
|
+
for (let q = start; q < end;) {
|
|
110
|
+
const tc = data[q] >> 4, th = data[q] & 15;
|
|
111
|
+
if (tc > 1 || th > 3 || q + 17 > end)
|
|
112
|
+
fail("invalid Huffman table.");
|
|
113
|
+
const counts = data.subarray(q + 1, q + 17);
|
|
114
|
+
let total = 0;
|
|
115
|
+
for (const c of counts)
|
|
116
|
+
total += c;
|
|
117
|
+
if (total > 256 || q + 17 + total > end)
|
|
118
|
+
fail("Huffman table exceeds its segment.");
|
|
119
|
+
(tc ? acTables : dcTables)[th] = buildHuffman(counts, data.slice(q + 17, q + 17 + total));
|
|
120
|
+
q += 17 + total;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else if (marker === 0xdd) {
|
|
124
|
+
if (size !== 4)
|
|
125
|
+
fail("invalid restart interval.");
|
|
126
|
+
state.restart = data[start] * 256 + data[start + 1];
|
|
127
|
+
}
|
|
128
|
+
else if (marker === 0xe0) {
|
|
129
|
+
if (size >= 7 && data[start] === 0x4a && data[start + 1] === 0x46 && data[start + 2] === 0x49 && data[start + 3] === 0x46 && data[start + 4] === 0)
|
|
130
|
+
state.jfif = true;
|
|
131
|
+
}
|
|
132
|
+
else if (marker === 0xee) {
|
|
133
|
+
if (size >= 14 && String.fromCharCode(...data.subarray(start, start + 5)) === "Adobe")
|
|
134
|
+
state.adobe = data[start + 11];
|
|
135
|
+
}
|
|
136
|
+
else if (marker === 0xc0 || marker === 0xc1) {
|
|
137
|
+
if (tablesOnly || frame)
|
|
138
|
+
fail("unexpected frame header.");
|
|
139
|
+
if (size < 8)
|
|
140
|
+
fail("unsupported frame header.");
|
|
141
|
+
const precision = data[start], height = data[start + 1] * 256 + data[start + 2], width = data[start + 3] * 256 + data[start + 4], nf = data[start + 5];
|
|
142
|
+
if (precision !== 8)
|
|
143
|
+
fail("only 8-bit precision is supported.");
|
|
144
|
+
if (height < 1 || width < 1 || nf < 1 || nf > 4 || size !== 8 + 3 * nf)
|
|
145
|
+
fail("unsupported frame header.");
|
|
146
|
+
if (width * height * nf > limits.maximumSamples)
|
|
147
|
+
fail("frame exceeds the sample limit.");
|
|
148
|
+
const raw = [];
|
|
149
|
+
for (let i = 0; i < nf; i++) {
|
|
150
|
+
const o = start + 6 + i * 3, h = data[o + 1] >> 4, v = data[o + 1] & 15, tq = data[o + 2];
|
|
151
|
+
if (h < 1 || h > 4 || v < 1 || v > 4 || tq > 3)
|
|
152
|
+
fail("invalid component sampling or table.");
|
|
153
|
+
if (raw.some((c) => c.id === data[o]))
|
|
154
|
+
fail("duplicate component identifier.");
|
|
155
|
+
raw.push({ id: data[o], h, v, tq });
|
|
156
|
+
}
|
|
157
|
+
const hMax = Math.max(...raw.map((c) => c.h)), vMax = Math.max(...raw.map((c) => c.v));
|
|
158
|
+
const mcusWide = Math.ceil(width / (8 * hMax)), mcusHigh = Math.ceil(height / (8 * vMax));
|
|
159
|
+
const components = raw.map((c) => {
|
|
160
|
+
const blocksWide = mcusWide * c.h, blocksHigh = mcusHigh * c.v;
|
|
161
|
+
return { ...c, blocksWide, blocksHigh, plane: new Uint8Array(blocksWide * blocksHigh * 64), previousDc: 0 };
|
|
162
|
+
});
|
|
163
|
+
frame = { width, height, components, hMax, vMax, mcusWide, mcusHigh };
|
|
164
|
+
}
|
|
165
|
+
else if (marker === 0xda) {
|
|
166
|
+
if (tablesOnly || !frame)
|
|
167
|
+
fail("scan before frame header.");
|
|
168
|
+
const ns = data[start];
|
|
169
|
+
if (ns < 1 || ns > 4 || size !== 6 + 2 * ns)
|
|
170
|
+
fail("invalid scan header.");
|
|
171
|
+
const scan = [];
|
|
172
|
+
for (let i = 0; i < ns; i++) {
|
|
173
|
+
const c = frame.components.find((x) => x.id === data[start + 1 + i * 2]);
|
|
174
|
+
if (!c || scan.includes(c))
|
|
175
|
+
fail("scan references an unknown component.");
|
|
176
|
+
const td = data[start + 2 + i * 2] >> 4, ta = data[start + 2 + i * 2] & 15;
|
|
177
|
+
c.dc = dcTables[td];
|
|
178
|
+
c.ac = acTables[ta];
|
|
179
|
+
if (!c.dc || !c.ac)
|
|
180
|
+
fail("scan references an undefined Huffman table.");
|
|
181
|
+
if (!quant[c.tq])
|
|
182
|
+
fail("component references an undefined quantization table.");
|
|
183
|
+
scan.push(c);
|
|
184
|
+
}
|
|
185
|
+
const ss = data[start + 1 + 2 * ns], se = data[start + 2 + 2 * ns], a = data[start + 3 + 2 * ns];
|
|
186
|
+
if (ss !== 0 || se !== 63 || a !== 0)
|
|
187
|
+
fail("only sequential scans are supported.");
|
|
188
|
+
if (scan.reduce((s, c) => s + (ns === 1 ? 1 : c.h * c.v), 0) > 10)
|
|
189
|
+
fail("too many blocks per MCU.");
|
|
190
|
+
// Further scans may follow (non-interleaved sequential); EOI ends the image.
|
|
191
|
+
p = decodeScan(data, end, frame, scan, quant, state.restart);
|
|
192
|
+
scanned = true;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
else if ((marker >= 0xc2 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) || marker === 0xde) {
|
|
196
|
+
fail("only sequential Huffman DCT frames (SOF0, SOF1) are supported.");
|
|
197
|
+
}
|
|
198
|
+
else if (marker === 0xcc) {
|
|
199
|
+
fail("arithmetic coding is not supported.");
|
|
200
|
+
}
|
|
201
|
+
else if (marker < 0xc0 || marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
202
|
+
fail("unexpected marker.");
|
|
203
|
+
}
|
|
204
|
+
p = end;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
if (limits.tables)
|
|
208
|
+
segments(limits.tables, true);
|
|
209
|
+
segments(bytes, false);
|
|
210
|
+
if (!frame)
|
|
211
|
+
fail("no image.");
|
|
212
|
+
if (frame.components.some((c) => !c.dc))
|
|
213
|
+
fail("a component has no scan.");
|
|
214
|
+
return assemble(frame, state);
|
|
215
|
+
}
|
|
216
|
+
function decodeScan(data, start, frame, scan, quant, restartInterval) {
|
|
217
|
+
let p = start, buffer = 0, bits = 0, padding = 0, marker = -1;
|
|
218
|
+
const fill = () => {
|
|
219
|
+
let byte = 0;
|
|
220
|
+
if (marker < 0 && p < data.length && data[p] !== 0xff)
|
|
221
|
+
byte = data[p++];
|
|
222
|
+
else if (marker < 0 && p + 1 < data.length && data[p] === 0xff && data[p + 1] === 0x00) {
|
|
223
|
+
byte = 0xff;
|
|
224
|
+
p += 2;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
if (marker < 0 && p + 1 < data.length)
|
|
228
|
+
marker = data[p + 1];
|
|
229
|
+
if (++padding > 8)
|
|
230
|
+
fail("entropy-coded data is truncated.");
|
|
231
|
+
}
|
|
232
|
+
buffer = ((buffer << 8) | byte) >>> 0;
|
|
233
|
+
bits += 8;
|
|
234
|
+
};
|
|
235
|
+
const readBits = (count) => {
|
|
236
|
+
if (count === 0)
|
|
237
|
+
return 0;
|
|
238
|
+
while (bits < count)
|
|
239
|
+
fill();
|
|
240
|
+
bits -= count;
|
|
241
|
+
return (buffer >>> bits) & ((1 << count) - 1);
|
|
242
|
+
};
|
|
243
|
+
const decode = (t) => {
|
|
244
|
+
let code = readBits(1), len = 1;
|
|
245
|
+
while (code > t.maxCode[len]) {
|
|
246
|
+
if (++len > 16)
|
|
247
|
+
fail("invalid Huffman code.");
|
|
248
|
+
code = (code << 1) | readBits(1);
|
|
249
|
+
}
|
|
250
|
+
const v = t.values[t.valPtr[len] + code - t.minCode[len]];
|
|
251
|
+
if (v === undefined)
|
|
252
|
+
fail("invalid Huffman code.");
|
|
253
|
+
return v;
|
|
254
|
+
};
|
|
255
|
+
const extend = (v, s) => v < (1 << (s - 1)) ? v - (1 << s) + 1 : v;
|
|
256
|
+
const coefficients = new Float64Array(64), rows = new Float64Array(64), cos = JPEG_DCT_COSINES;
|
|
257
|
+
const block = (c, bx, by) => {
|
|
258
|
+
coefficients.fill(0);
|
|
259
|
+
const t = decode(c.dc);
|
|
260
|
+
if (t > 11)
|
|
261
|
+
fail("invalid DC difference category.");
|
|
262
|
+
c.previousDc += t ? extend(readBits(t), t) : 0;
|
|
263
|
+
const q = quant[c.tq];
|
|
264
|
+
coefficients[0] = c.previousDc * q[0];
|
|
265
|
+
for (let k = 1; k < 64;) {
|
|
266
|
+
const rs = decode(c.ac), r = rs >> 4, s = rs & 15;
|
|
267
|
+
if (s === 0) {
|
|
268
|
+
if (r === 15) {
|
|
269
|
+
k += 16;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
k += r;
|
|
275
|
+
if (k > 63 || s > 10)
|
|
276
|
+
fail("invalid AC coefficient.");
|
|
277
|
+
const z = ZIGZAG[k];
|
|
278
|
+
coefficients[z] = extend(readBits(s), s) * q[z];
|
|
279
|
+
k++;
|
|
280
|
+
}
|
|
281
|
+
// A.3.3: s(y,x) = 1/4 Σ_u Σ_v C(u)C(v) S(v,u) cos((2x+1)uπ/16) cos((2y+1)vπ/16), rows (u) first, then columns (v).
|
|
282
|
+
for (let v = 0; v < 8; v++)
|
|
283
|
+
for (let x = 0; x < 8; x++) {
|
|
284
|
+
let sum = 0;
|
|
285
|
+
for (let u = 0; u < 8; u++)
|
|
286
|
+
sum += (u === 0 ? Math.SQRT1_2 : 1) * coefficients[v * 8 + u] * cos[u * 8 + x];
|
|
287
|
+
rows[v * 8 + x] = sum;
|
|
288
|
+
}
|
|
289
|
+
const stride = c.blocksWide * 8, base = by * 8 * stride + bx * 8;
|
|
290
|
+
for (let y = 0; y < 8; y++)
|
|
291
|
+
for (let x = 0; x < 8; x++) {
|
|
292
|
+
let sum = 0;
|
|
293
|
+
for (let v = 0; v < 8; v++)
|
|
294
|
+
sum += (v === 0 ? Math.SQRT1_2 : 1) * rows[v * 8 + x] * cos[v * 8 + y];
|
|
295
|
+
const value = sum / 4, rounded = value < 0 ? -Math.round(-value) : Math.round(value);
|
|
296
|
+
c.plane[base + y * stride + x] = Math.min(255, Math.max(0, rounded + 128));
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
for (const c of scan)
|
|
300
|
+
c.previousDc = 0;
|
|
301
|
+
// A single-component scan is non-interleaved: one block per MCU over the component's own (unpadded-to-MCU) block grid.
|
|
302
|
+
const single = scan.length === 1 ? scan[0] : undefined;
|
|
303
|
+
const unitsWide = single ? Math.ceil(Math.ceil(frame.width * single.h / frame.hMax) / 8) : frame.mcusWide;
|
|
304
|
+
const unitsHigh = single ? Math.ceil(Math.ceil(frame.height * single.v / frame.vMax) / 8) : frame.mcusHigh;
|
|
305
|
+
const total = unitsWide * unitsHigh;
|
|
306
|
+
for (let m = 0, restarts = 0; m < total; m++) {
|
|
307
|
+
if (restartInterval && m > 0 && m % restartInterval === 0) {
|
|
308
|
+
bits = 0;
|
|
309
|
+
buffer = 0;
|
|
310
|
+
padding = 0;
|
|
311
|
+
marker = -1;
|
|
312
|
+
if (p + 1 >= data.length || data[p] !== 0xff || data[p + 1] !== (0xd0 | (restarts & 7)))
|
|
313
|
+
fail("missing restart marker.");
|
|
314
|
+
p += 2;
|
|
315
|
+
restarts++;
|
|
316
|
+
for (const c of scan)
|
|
317
|
+
c.previousDc = 0;
|
|
318
|
+
}
|
|
319
|
+
const mx = m % unitsWide, my = Math.floor(m / unitsWide);
|
|
320
|
+
if (single)
|
|
321
|
+
block(single, mx, my);
|
|
322
|
+
else
|
|
323
|
+
for (const c of scan)
|
|
324
|
+
for (let v = 0; v < c.v; v++)
|
|
325
|
+
for (let h = 0; h < c.h; h++)
|
|
326
|
+
block(c, mx * c.h + h, my * c.v + v);
|
|
327
|
+
}
|
|
328
|
+
// Skip to the next marker: remaining fill bits are discarded (T.81 F.1.2.3 pads with 1-bits).
|
|
329
|
+
while (p < data.length && !(data[p] === 0xff && data[p + 1] !== 0x00 && !(data[p + 1] >= 0xd0 && data[p + 1] <= 0xd7)))
|
|
330
|
+
p++;
|
|
331
|
+
if (p + 1 >= data.length)
|
|
332
|
+
fail("missing EOI marker.");
|
|
333
|
+
return p;
|
|
334
|
+
}
|
|
335
|
+
function assemble(frame, state) {
|
|
336
|
+
const { width, height, components, hMax, vMax } = frame, nf = components.length, out = new Uint8Array(width * height * nf);
|
|
337
|
+
components.forEach((c, ci) => {
|
|
338
|
+
const stride = c.blocksWide * 8, cw = Math.ceil(width * c.h / hMax), ch = Math.ceil(height * c.v / vMax);
|
|
339
|
+
if (c.h === hMax && c.v === vMax) {
|
|
340
|
+
for (let y = 0; y < height; y++)
|
|
341
|
+
for (let x = 0; x < width; x++)
|
|
342
|
+
out[(y * width + x) * nf + ci] = c.plane[y * stride + x];
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// Centred linear interpolation: output sample x covers component position (x + 0.5)·h/hMax − 0.5, clamped to the samples.
|
|
346
|
+
const sx = c.h / hMax, sy = c.v / vMax;
|
|
347
|
+
for (let y = 0; y < height; y++) {
|
|
348
|
+
const fy = Math.min(Math.max((y + 0.5) * sy - 0.5, 0), ch - 1), y0 = Math.floor(fy), y1 = Math.min(y0 + 1, ch - 1), wy = fy - y0;
|
|
349
|
+
for (let x = 0; x < width; x++) {
|
|
350
|
+
const fx = Math.min(Math.max((x + 0.5) * sx - 0.5, 0), cw - 1), x0 = Math.floor(fx), x1 = Math.min(x0 + 1, cw - 1), wx = fx - x0;
|
|
351
|
+
const a = c.plane[y0 * stride + x0], b = c.plane[y0 * stride + x1], d = c.plane[y1 * stride + x0], e = c.plane[y1 * stride + x1];
|
|
352
|
+
out[(y * width + x) * nf + ci] = Math.round((a * (1 - wx) + b * wx) * (1 - wy) + (d * (1 - wx) + e * wx) * wy);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
const ids = components.map((c) => c.id), rgbIds = ids[0] === 0x52 && ids[1] === 0x47 && ids[2] === 0x42;
|
|
357
|
+
const ycc = nf === 3 && (state.adobe >= 0 ? state.adobe !== 0 : state.jfif || !rgbIds);
|
|
358
|
+
if (ycc)
|
|
359
|
+
for (let i = 0; i < out.length; i += 3) {
|
|
360
|
+
// T.871 §7: R = Y + 1.402 (Cr − 128), G = Y − 0.344136 (Cb − 128) − 0.714136 (Cr − 128), B = Y + 1.772 (Cb − 128).
|
|
361
|
+
const y = out[i], cb = out[i + 1] - 128, cr = out[i + 2] - 128;
|
|
362
|
+
out[i] = clamp8(y + 1.402 * cr);
|
|
363
|
+
out[i + 1] = clamp8(y - 0.344136 * cb - 0.714136 * cr);
|
|
364
|
+
out[i + 2] = clamp8(y + 1.772 * cb);
|
|
365
|
+
}
|
|
366
|
+
return { width, height, components: nf, colorTransform: ycc ? "ycbcr-to-rgb" : "none", samples: out };
|
|
367
|
+
}
|
|
368
|
+
function clamp8(v) { return Math.min(255, Math.max(0, Math.round(v))); }
|
|
369
|
+
//# sourceMappingURL=lossy-jpeg.js.map
|