@tongue-core/engine 0.0.0-stage → 0.1.0-rc.10
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/LICENSE +21 -0
- package/README.md +62 -2
- package/dist/assets/codecs/jpeg.d.ts +2 -0
- package/dist/assets/codecs/jpeg.js +591 -0
- package/dist/assets/codecs/png.d.ts +9 -0
- package/dist/assets/codecs/png.js +250 -0
- package/dist/assets/codecs/qoi.d.ts +3 -0
- package/dist/assets/codecs/qoi.js +157 -0
- package/dist/assets/codecs/resize.d.ts +2 -0
- package/dist/assets/codecs/resize.js +121 -0
- package/dist/assets/codecs/types.d.ts +13 -0
- package/dist/assets/codecs/types.js +2 -0
- package/dist/assets/codecs/wav.d.ts +6 -0
- package/dist/assets/codecs/wav.js +155 -0
- package/dist/assets/paths.d.ts +28 -0
- package/dist/assets/paths.js +24 -0
- package/dist/assets/store.d.ts +99 -0
- package/dist/assets/store.js +212 -0
- package/dist/audio/mix.d.ts +52 -0
- package/dist/audio/mix.js +257 -0
- package/dist/evaluate/evaluate.d.ts +40 -0
- package/dist/evaluate/evaluate.js +485 -0
- package/dist/evaluate/scene-frame.d.ts +65 -0
- package/dist/evaluate/scene-frame.js +248 -0
- package/dist/evaluate/tree.d.ts +94 -0
- package/dist/evaluate/tree.js +1 -0
- package/dist/evaluate/typing.d.ts +13 -0
- package/dist/evaluate/typing.js +93 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +22 -0
- package/dist/layout/layout.d.ts +30 -0
- package/dist/layout/layout.js +513 -0
- package/dist/media/mp4.d.ts +28 -0
- package/dist/media/mp4.js +177 -0
- package/dist/media/video.d.ts +23 -0
- package/dist/media/video.js +32 -0
- package/dist/media/yuv.d.ts +34 -0
- package/dist/media/yuv.js +82 -0
- package/dist/paint/geometry.d.ts +34 -0
- package/dist/paint/geometry.js +107 -0
- package/dist/paint/list.d.ts +132 -0
- package/dist/paint/list.js +270 -0
- package/dist/paint/morph.d.ts +17 -0
- package/dist/paint/morph.js +210 -0
- package/dist/paint/paint.d.ts +120 -0
- package/dist/paint/paint.js +676 -0
- package/dist/paint/stroke.d.ts +26 -0
- package/dist/paint/stroke.js +233 -0
- package/dist/raster/backend.d.ts +22 -0
- package/dist/raster/backend.js +1 -0
- package/dist/raster/raster.d.ts +43 -0
- package/dist/raster/raster.js +154 -0
- package/dist/raster/wasm-bytes.d.ts +1 -0
- package/dist/raster/wasm-bytes.js +3 -0
- package/dist/render/compose.d.ts +19 -0
- package/dist/render/compose.js +110 -0
- package/dist/render/renderer.d.ts +76 -0
- package/dist/render/renderer.js +207 -0
- package/dist/render/size.d.ts +6 -0
- package/dist/render/size.js +7 -0
- package/dist/render/sounds.d.ts +15 -0
- package/dist/render/sounds.js +49 -0
- package/dist/style/computed.d.ts +142 -0
- package/dist/style/computed.js +1 -0
- package/dist/style/parse.d.ts +19 -0
- package/dist/style/parse.js +180 -0
- package/dist/style/resolve.d.ts +10 -0
- package/dist/style/resolve.js +289 -0
- package/dist/style/svg.d.ts +4 -0
- package/dist/style/svg.js +185 -0
- package/dist/text/font-types.d.ts +63 -0
- package/dist/text/font-types.js +3 -0
- package/dist/text/fonts.d.ts +28 -0
- package/dist/text/fonts.js +61 -0
- package/dist/text/paragraph.d.ts +65 -0
- package/dist/text/paragraph.js +220 -0
- package/dist/text/sfnt.d.ts +51 -0
- package/dist/text/sfnt.js +105 -0
- package/dist/text/shape.d.ts +15 -0
- package/dist/text/shape.js +28 -0
- package/dist/text/ttf.d.ts +6 -0
- package/dist/text/ttf.js +727 -0
- package/dist/text/variation.d.ts +53 -0
- package/dist/text/variation.js +367 -0
- package/dist/text/woff2.d.ts +1 -0
- package/dist/text/woff2.js +390 -0
- package/package.json +32 -4
- package/raster.wasm +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Backpine
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,63 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @tongue-core/engine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Frame → pixels. Given a planned film (`planFilm` from `@tongue-core/film`) and its loaded assets, it
|
|
4
|
+
composes the tracks at a frame, evaluates each clip's scene at its local time, lays it out, paints
|
|
5
|
+
a display list and rasterizes it with the embedded WebAssembly raster. Plain TypeScript with no
|
|
6
|
+
dependencies besides `@tongue-core/film`, and deterministic: the same frame is byte-identical in Bun,
|
|
7
|
+
browsers and Cloudflare Workers.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { planFilm } from "@tongue-core/film";
|
|
11
|
+
import { FilmRenderer, loadAssets } from "@tongue-core/engine";
|
|
12
|
+
|
|
13
|
+
const plan = planFilm(film, { media });
|
|
14
|
+
const assets = await loadAssets(plan.assets, (path) => fetchBytes(`/assets/${path}`), { audio: false });
|
|
15
|
+
const renderer = await FilmRenderer.create(plan, assets); // the wasm raster, or pass a RasterBackend
|
|
16
|
+
renderer.render(120, { scale: 0.5 });
|
|
17
|
+
const rgba = renderer.rgba8(); // or yuv420(), png()
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**In a Cloudflare Worker**, which may not compile WebAssembly from bytes, import the raster as a
|
|
21
|
+
module (`@tongue-core/engine/raster.wasm`, the same bytes as the embedded copy) and pass it in:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { FilmRenderer, Raster } from "@tongue-core/engine";
|
|
25
|
+
import raster from "@tongue-core/engine/raster.wasm"; // a WebAssembly.Module in a Worker
|
|
26
|
+
|
|
27
|
+
const renderer = await FilmRenderer.create(plan, assets, await Raster.create(raster));
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
With little memory (a Worker), hold only what each frame draws: `displayList` throws `ImageNotLoaded`
|
|
31
|
+
naming the image a frame needs, and `renderer.keepOnly(list)` frees the rest from the raster before
|
|
32
|
+
`renderer.draw(list)`. `apps/frames-worker/src/runner.ts` does this.
|
|
33
|
+
|
|
34
|
+
TypeScript needs `declare module "*.wasm" { const module: WebAssembly.Module; export default module; }`
|
|
35
|
+
somewhere in the Worker's sources.
|
|
36
|
+
|
|
37
|
+
- **Frames are independent.** Nothing carries over between frames except caches, so frames
|
|
38
|
+
render in any order, on any number of machines.
|
|
39
|
+
- **Video frames come from the host** (`FrameOptions.video`, a `VideoFrameSource`): ffmpeg on a
|
|
40
|
+
server, WebCodecs in a browser. Without one, a clip shows its poster. `videoFramesAt(frame)` lists
|
|
41
|
+
what a frame needs so a host can decode ahead.
|
|
42
|
+
- **Other rasters plug in** through `RasterBackend` (`src/raster/backend.ts`): the SDK's native
|
|
43
|
+
and GPU rasters draw the same display lists. The format is in `docs/display-list.md`.
|
|
44
|
+
- **Sound** is mixed here too: `audioClips(plan, assets)` and `collectSounds(plan)` (typing and
|
|
45
|
+
clicks scenes make) into `mix(…)`, with time-stretch that keeps pitch.
|
|
46
|
+
- **Assets** are what the SDK imports: TrueType fonts, QOI images, WAV audio, video facts and
|
|
47
|
+
posters (`loadAssets` with a fetcher). The codecs (PNG, JPEG, QOI, WAV, WOFF2) are ours.
|
|
48
|
+
|
|
49
|
+
## Layout
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
src/
|
|
53
|
+
render/ FilmRenderer: tracks → one frame (compose.ts), and the sounds a film makes
|
|
54
|
+
evaluate/ a scene at a frame → a tree of boxes (camera, pointer, typing, morph partners)
|
|
55
|
+
layout/ block, flex, grid and stack layout for the style subset
|
|
56
|
+
text/ fonts (TTF, WOFF2, variable), shaping, paragraphs
|
|
57
|
+
style/ style parsing and resolution
|
|
58
|
+
paint/ tree → display list (and morphs between two trees)
|
|
59
|
+
raster/ the embedded wasm raster and the RasterBackend contract
|
|
60
|
+
audio/ the mixer
|
|
61
|
+
assets/ loading imported assets, and the codecs
|
|
62
|
+
media/ video frame lookup
|
|
63
|
+
```
|
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
// JPEG decoder, from scratch: baseline and extended sequential (SOF0/SOF1) and progressive
|
|
2
|
+
// (SOF2) Huffman JPEGs with any sampling factors and restart intervals. Coefficients for the
|
|
3
|
+
// whole image are kept until the last scan, because progressive scans refine them in passes.
|
|
4
|
+
import { CodecError } from "./png.js";
|
|
5
|
+
const fail = (message) => {
|
|
6
|
+
throw new CodecError("jpeg", message);
|
|
7
|
+
};
|
|
8
|
+
// Zigzag position k → natural (row-major) index in the 8×8 block. Padded with 63s so a corrupt
|
|
9
|
+
// run that overshoots lands on the last coefficient instead of outside the block.
|
|
10
|
+
const ZIGZAG = new Uint8Array([
|
|
11
|
+
0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28,
|
|
12
|
+
35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
|
|
13
|
+
63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63, 63,
|
|
14
|
+
]);
|
|
15
|
+
function huffman(counts, values) {
|
|
16
|
+
const maxcode = new Int32Array(18).fill(-1);
|
|
17
|
+
const mincode = new Int32Array(17);
|
|
18
|
+
const valptr = new Int32Array(17);
|
|
19
|
+
let code = 0;
|
|
20
|
+
let k = 0;
|
|
21
|
+
for (let length = 1; length <= 16; length++) {
|
|
22
|
+
const count = counts[length - 1] ?? 0;
|
|
23
|
+
valptr[length] = k;
|
|
24
|
+
mincode[length] = code;
|
|
25
|
+
code += count;
|
|
26
|
+
k += count;
|
|
27
|
+
if (count > 0)
|
|
28
|
+
maxcode[length] = code - 1;
|
|
29
|
+
if (code > 1 << length)
|
|
30
|
+
fail("Huffman table has too many codes");
|
|
31
|
+
code <<= 1;
|
|
32
|
+
}
|
|
33
|
+
maxcode[17] = 0x7fffffff;
|
|
34
|
+
return { maxcode, mincode, valptr, values };
|
|
35
|
+
}
|
|
36
|
+
class Component {
|
|
37
|
+
id;
|
|
38
|
+
h;
|
|
39
|
+
v;
|
|
40
|
+
table;
|
|
41
|
+
pred = 0;
|
|
42
|
+
dc;
|
|
43
|
+
ac;
|
|
44
|
+
coefficients = new Int16Array(0);
|
|
45
|
+
blocksPerLine = 0; // visible blocks, used by non-interleaved scans
|
|
46
|
+
blocksPerColumn = 0;
|
|
47
|
+
blocksPerLineForMcu = 0; // padded to whole MCUs, the storage stride
|
|
48
|
+
blocksPerColumnForMcu = 0;
|
|
49
|
+
constructor(id, h, v, table) {
|
|
50
|
+
this.id = id;
|
|
51
|
+
this.h = h;
|
|
52
|
+
this.v = v;
|
|
53
|
+
this.table = table;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function decodeJpeg(bytes) {
|
|
57
|
+
if (bytes[0] !== 0xff || bytes[1] !== 0xd8)
|
|
58
|
+
fail("not a JPEG file (no SOI marker)");
|
|
59
|
+
const quantization = [];
|
|
60
|
+
const dcTables = [];
|
|
61
|
+
const acTables = [];
|
|
62
|
+
let frame;
|
|
63
|
+
let restartInterval = 0;
|
|
64
|
+
let adobeTransform = -1;
|
|
65
|
+
const u16 = (at) => ((bytes[at] ?? 0) << 8) | (bytes[at + 1] ?? 0);
|
|
66
|
+
let at = 2;
|
|
67
|
+
for (;;) {
|
|
68
|
+
// Markers may be preceded by any number of 0xFF fill bytes; skip stray data between segments.
|
|
69
|
+
while (at < bytes.length && bytes[at] !== 0xff)
|
|
70
|
+
at++;
|
|
71
|
+
while (at < bytes.length && bytes[at] === 0xff)
|
|
72
|
+
at++;
|
|
73
|
+
if (at >= bytes.length)
|
|
74
|
+
break;
|
|
75
|
+
const marker = bytes[at++] ?? 0;
|
|
76
|
+
if (marker === 0xd9)
|
|
77
|
+
break; // EOI
|
|
78
|
+
if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd7) || marker === 0x01)
|
|
79
|
+
continue; // no length
|
|
80
|
+
if (at + 2 > bytes.length)
|
|
81
|
+
fail("truncated segment");
|
|
82
|
+
const length = u16(at);
|
|
83
|
+
const start = at + 2;
|
|
84
|
+
const end = at + length;
|
|
85
|
+
if (length < 2 || end > bytes.length)
|
|
86
|
+
fail(`segment ${marker.toString(16)} runs past the end of the file`);
|
|
87
|
+
switch (marker) {
|
|
88
|
+
case 0xdb: { // DQT
|
|
89
|
+
for (let p = start; p < end;) {
|
|
90
|
+
const pq = (bytes[p] ?? 0) >> 4;
|
|
91
|
+
const tq = (bytes[p] ?? 0) & 15;
|
|
92
|
+
p++;
|
|
93
|
+
if (tq > 3)
|
|
94
|
+
fail("quantization table id out of range");
|
|
95
|
+
const table = new Uint16Array(64);
|
|
96
|
+
for (let k = 0; k < 64; k++) {
|
|
97
|
+
table[ZIGZAG[k] ?? 0] = pq === 0 ? bytes[p + k] ?? 0 : u16(p + k * 2);
|
|
98
|
+
}
|
|
99
|
+
p += pq === 0 ? 64 : 128;
|
|
100
|
+
quantization[tq] = table;
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case 0xc4: { // DHT
|
|
105
|
+
for (let p = start; p < end;) {
|
|
106
|
+
const tc = (bytes[p] ?? 0) >> 4;
|
|
107
|
+
const th = (bytes[p] ?? 0) & 15;
|
|
108
|
+
if (th > 3 || tc > 1)
|
|
109
|
+
fail("Huffman table id out of range");
|
|
110
|
+
const counts = bytes.slice(p + 1, p + 17);
|
|
111
|
+
const total = counts.reduce((sum, n) => sum + n, 0);
|
|
112
|
+
const values = bytes.slice(p + 17, p + 17 + total);
|
|
113
|
+
if (values.length !== total)
|
|
114
|
+
fail("truncated Huffman table");
|
|
115
|
+
(tc === 0 ? dcTables : acTables)[th] = huffman(counts, values);
|
|
116
|
+
p += 17 + total;
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case 0xdd: // DRI
|
|
121
|
+
restartInterval = u16(start);
|
|
122
|
+
break;
|
|
123
|
+
case 0xee: // APP14 "Adobe": says whether three components are YCbCr or plain RGB
|
|
124
|
+
if (length >= 14 && String.fromCharCode(...bytes.subarray(start, start + 5)) === "Adobe")
|
|
125
|
+
adobeTransform = bytes[start + 11] ?? 0;
|
|
126
|
+
break;
|
|
127
|
+
case 0xc0:
|
|
128
|
+
case 0xc1:
|
|
129
|
+
case 0xc2: {
|
|
130
|
+
if (frame !== undefined)
|
|
131
|
+
fail("more than one frame");
|
|
132
|
+
if (bytes[start] !== 8)
|
|
133
|
+
fail(`${bytes[start] ?? 0}-bit samples are not supported`);
|
|
134
|
+
const height = u16(start + 1);
|
|
135
|
+
const width = u16(start + 3);
|
|
136
|
+
const count = bytes[start + 5] ?? 0;
|
|
137
|
+
if (width === 0 || height === 0)
|
|
138
|
+
fail("zero-sized image (DNL is not supported)");
|
|
139
|
+
if (count !== 1 && count !== 3)
|
|
140
|
+
fail(`${count}-component JPEGs are not supported`);
|
|
141
|
+
const components = [];
|
|
142
|
+
for (let i = 0; i < count; i++) {
|
|
143
|
+
const p = start + 6 + i * 3;
|
|
144
|
+
const h = (bytes[p + 1] ?? 0) >> 4;
|
|
145
|
+
const v = (bytes[p + 1] ?? 0) & 15;
|
|
146
|
+
if (h < 1 || h > 4 || v < 1 || v > 4)
|
|
147
|
+
fail("bad sampling factors");
|
|
148
|
+
components.push(new Component(bytes[p] ?? 0, h, v, bytes[p + 2] ?? 0));
|
|
149
|
+
}
|
|
150
|
+
frame = layout(marker === 0xc2, width, height, components);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
case 0xc3:
|
|
154
|
+
case 0xc5:
|
|
155
|
+
case 0xc6:
|
|
156
|
+
case 0xc7:
|
|
157
|
+
case 0xc9:
|
|
158
|
+
case 0xca:
|
|
159
|
+
case 0xcb:
|
|
160
|
+
case 0xcd:
|
|
161
|
+
case 0xce:
|
|
162
|
+
case 0xcf:
|
|
163
|
+
return fail("lossless, hierarchical and arithmetic-coded JPEGs are not supported");
|
|
164
|
+
case 0xda: { // SOS
|
|
165
|
+
if (frame === undefined)
|
|
166
|
+
return fail("scan before frame header");
|
|
167
|
+
const count = bytes[start] ?? 0;
|
|
168
|
+
const scan = [];
|
|
169
|
+
for (let i = 0; i < count; i++) {
|
|
170
|
+
const id = bytes[start + 1 + i * 2] ?? 0;
|
|
171
|
+
const tables = bytes[start + 2 + i * 2] ?? 0;
|
|
172
|
+
const component = frame.components.find((c) => c.id === id) ?? fail(`scan refers to unknown component ${id}`);
|
|
173
|
+
component.dc = dcTables[tables >> 4];
|
|
174
|
+
component.ac = acTables[tables & 15];
|
|
175
|
+
scan.push(component);
|
|
176
|
+
}
|
|
177
|
+
const p = start + 1 + count * 2;
|
|
178
|
+
const ss = bytes[p] ?? 0;
|
|
179
|
+
const se = bytes[p + 1] ?? 0;
|
|
180
|
+
const ah = (bytes[p + 2] ?? 0) >> 4;
|
|
181
|
+
const al = (bytes[p + 2] ?? 0) & 15;
|
|
182
|
+
at = decodeScan(bytes, end, frame, scan, restartInterval, ss, se, ah, al);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
default:
|
|
186
|
+
break; // APPn, COM and the rest carry nothing we need
|
|
187
|
+
}
|
|
188
|
+
at = end;
|
|
189
|
+
}
|
|
190
|
+
if (frame === undefined)
|
|
191
|
+
return fail("no frame header");
|
|
192
|
+
return toRgba(frame, quantization, adobeTransform);
|
|
193
|
+
}
|
|
194
|
+
function layout(progressive, width, height, components) {
|
|
195
|
+
const hmax = Math.max(...components.map((c) => c.h));
|
|
196
|
+
const vmax = Math.max(...components.map((c) => c.v));
|
|
197
|
+
const mcusPerLine = Math.ceil(width / (8 * hmax));
|
|
198
|
+
const mcusPerColumn = Math.ceil(height / (8 * vmax));
|
|
199
|
+
for (const c of components) {
|
|
200
|
+
c.blocksPerLine = Math.ceil(Math.ceil((width * c.h) / hmax) / 8);
|
|
201
|
+
c.blocksPerColumn = Math.ceil(Math.ceil((height * c.v) / vmax) / 8);
|
|
202
|
+
c.blocksPerLineForMcu = mcusPerLine * c.h;
|
|
203
|
+
c.blocksPerColumnForMcu = mcusPerColumn * c.v;
|
|
204
|
+
c.coefficients = new Int16Array(c.blocksPerLineForMcu * c.blocksPerColumnForMcu * 64);
|
|
205
|
+
}
|
|
206
|
+
return { progressive, width, height, components, hmax, vmax, mcusPerLine, mcusPerColumn };
|
|
207
|
+
}
|
|
208
|
+
// Decodes one scan's entropy-coded data starting at `start`; returns the offset of the marker
|
|
209
|
+
// that ends it.
|
|
210
|
+
function decodeScan(data, start, frame, components, restartInterval, ss, se, ah, al) {
|
|
211
|
+
let offset = start;
|
|
212
|
+
let bits = 0;
|
|
213
|
+
let bitCount = 0;
|
|
214
|
+
let atMarker = false;
|
|
215
|
+
// One bit, MSB first, undoing 0xFF00 byte stuffing. On reaching a marker the stream reads as
|
|
216
|
+
// zeros from then on, the way libjpeg pads a truncated interval.
|
|
217
|
+
const readBit = () => {
|
|
218
|
+
if (bitCount === 0) {
|
|
219
|
+
if (atMarker)
|
|
220
|
+
return 0;
|
|
221
|
+
const byte = data[offset] ?? 0;
|
|
222
|
+
if (byte === 0xff) {
|
|
223
|
+
const next = data[offset + 1] ?? 0;
|
|
224
|
+
if (next !== 0) {
|
|
225
|
+
atMarker = true;
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
offset += 2;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
offset++;
|
|
232
|
+
}
|
|
233
|
+
if (offset > data.length) {
|
|
234
|
+
atMarker = true;
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
bits = byte;
|
|
238
|
+
bitCount = 8;
|
|
239
|
+
}
|
|
240
|
+
bitCount--;
|
|
241
|
+
return (bits >> bitCount) & 1;
|
|
242
|
+
};
|
|
243
|
+
const receive = (length) => {
|
|
244
|
+
let n = 0;
|
|
245
|
+
for (let i = 0; i < length; i++)
|
|
246
|
+
n = (n << 1) | readBit();
|
|
247
|
+
return n;
|
|
248
|
+
};
|
|
249
|
+
// A magnitude category's bits as a signed value (Annex F.2.2.1, EXTEND).
|
|
250
|
+
const extend = (length) => {
|
|
251
|
+
if (length === 0)
|
|
252
|
+
return 0;
|
|
253
|
+
const n = receive(length);
|
|
254
|
+
return n >= 1 << (length - 1) ? n : n - (1 << length) + 1;
|
|
255
|
+
};
|
|
256
|
+
const decode = (table) => {
|
|
257
|
+
if (table === undefined)
|
|
258
|
+
return fail("scan uses an undefined Huffman table");
|
|
259
|
+
let code = readBit();
|
|
260
|
+
let length = 1;
|
|
261
|
+
while (code > (table.maxcode[length] ?? 0x7fffffff)) {
|
|
262
|
+
code = (code << 1) | readBit();
|
|
263
|
+
length++;
|
|
264
|
+
}
|
|
265
|
+
if (length > 16)
|
|
266
|
+
return 0; // corrupt code: treat as zero and let the restart marker resync
|
|
267
|
+
return table.values[(table.valptr[length] ?? 0) + code - (table.mincode[length] ?? 0)] ?? 0;
|
|
268
|
+
};
|
|
269
|
+
let eobrun = 0;
|
|
270
|
+
let refineState = 0; // successive-approximation AC state machine, Annex G.1.2.3
|
|
271
|
+
let refineValue = 0;
|
|
272
|
+
const baseline = (c, o) => {
|
|
273
|
+
const t = decode(c.dc);
|
|
274
|
+
c.pred += extend(t);
|
|
275
|
+
c.coefficients[o] = c.pred;
|
|
276
|
+
for (let k = 1; k < 64;) {
|
|
277
|
+
const rs = decode(c.ac);
|
|
278
|
+
const s = rs & 15;
|
|
279
|
+
const r = rs >> 4;
|
|
280
|
+
if (s === 0) {
|
|
281
|
+
if (r < 15)
|
|
282
|
+
break;
|
|
283
|
+
k += 16;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
k += r;
|
|
287
|
+
if (k > 63)
|
|
288
|
+
break;
|
|
289
|
+
c.coefficients[o + (ZIGZAG[k] ?? 63)] = extend(s);
|
|
290
|
+
k++;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
const dcFirst = (c, o) => {
|
|
294
|
+
const t = decode(c.dc);
|
|
295
|
+
c.pred += extend(t) * (1 << al);
|
|
296
|
+
c.coefficients[o] = c.pred;
|
|
297
|
+
};
|
|
298
|
+
const dcRefine = (c, o) => {
|
|
299
|
+
if (readBit())
|
|
300
|
+
c.coefficients[o] = (c.coefficients[o] ?? 0) | (1 << al);
|
|
301
|
+
};
|
|
302
|
+
const acFirst = (c, o) => {
|
|
303
|
+
if (eobrun > 0) {
|
|
304
|
+
eobrun--;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
for (let k = ss; k <= se;) {
|
|
308
|
+
const rs = decode(c.ac);
|
|
309
|
+
const s = rs & 15;
|
|
310
|
+
const r = rs >> 4;
|
|
311
|
+
if (s === 0) {
|
|
312
|
+
if (r < 15) {
|
|
313
|
+
eobrun = receive(r) + (1 << r) - 1;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
k += 16;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
k += r;
|
|
320
|
+
if (k > 63)
|
|
321
|
+
break;
|
|
322
|
+
c.coefficients[o + (ZIGZAG[k] ?? 63)] = extend(s) * (1 << al);
|
|
323
|
+
k++;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
// Refinement scans add one bit to every coefficient already nonzero and place new ±1 values
|
|
327
|
+
// into zero positions after skipping `r` zeros. The state carries across blocks for EOB runs.
|
|
328
|
+
const acRefine = (c, o) => {
|
|
329
|
+
const coefficients = c.coefficients;
|
|
330
|
+
let r = 0;
|
|
331
|
+
for (let k = ss; k <= se;) {
|
|
332
|
+
const z = o + (ZIGZAG[k] ?? 63);
|
|
333
|
+
const current = coefficients[z] ?? 0;
|
|
334
|
+
const sign = current < 0 ? -1 : 1;
|
|
335
|
+
switch (refineState) {
|
|
336
|
+
case 0: {
|
|
337
|
+
const rs = decode(c.ac);
|
|
338
|
+
const s = rs & 15;
|
|
339
|
+
r = rs >> 4;
|
|
340
|
+
if (s === 0) {
|
|
341
|
+
if (r < 15) {
|
|
342
|
+
eobrun = receive(r) + (1 << r);
|
|
343
|
+
refineState = 4;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
r = 16;
|
|
347
|
+
refineState = 1;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
if (s !== 1)
|
|
352
|
+
fail("bad refinement value size");
|
|
353
|
+
refineValue = extend(s);
|
|
354
|
+
refineState = r > 0 ? 2 : 3;
|
|
355
|
+
}
|
|
356
|
+
continue; // reconsider the same k in the new state
|
|
357
|
+
}
|
|
358
|
+
case 1:
|
|
359
|
+
case 2:
|
|
360
|
+
if (current !== 0) {
|
|
361
|
+
coefficients[z] = current + sign * (readBit() << al);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
r--;
|
|
365
|
+
if (r === 0)
|
|
366
|
+
refineState = refineState === 2 ? 3 : 0;
|
|
367
|
+
}
|
|
368
|
+
break;
|
|
369
|
+
case 3:
|
|
370
|
+
if (current !== 0) {
|
|
371
|
+
coefficients[z] = current + sign * (readBit() << al);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
coefficients[z] = refineValue * (1 << al);
|
|
375
|
+
refineState = 0;
|
|
376
|
+
}
|
|
377
|
+
break;
|
|
378
|
+
default: // 4: inside an EOB run, only correction bits
|
|
379
|
+
if (current !== 0)
|
|
380
|
+
coefficients[z] = current + sign * (readBit() << al);
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
k++;
|
|
384
|
+
}
|
|
385
|
+
if (refineState === 4) {
|
|
386
|
+
eobrun--;
|
|
387
|
+
if (eobrun === 0)
|
|
388
|
+
refineState = 0;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
let decodeBlock;
|
|
392
|
+
if (!frame.progressive)
|
|
393
|
+
decodeBlock = baseline;
|
|
394
|
+
else if (ss === 0) {
|
|
395
|
+
if (se !== 0)
|
|
396
|
+
fail("progressive DC scan with AC coefficients");
|
|
397
|
+
decodeBlock = ah === 0 ? dcFirst : dcRefine;
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
if (components.length !== 1)
|
|
401
|
+
fail("progressive AC scan with more than one component");
|
|
402
|
+
if (se > 63 || ss > se)
|
|
403
|
+
fail("bad spectral selection");
|
|
404
|
+
decodeBlock = ah === 0 ? acFirst : acRefine;
|
|
405
|
+
}
|
|
406
|
+
const single = components.length === 1 ? components[0] : undefined;
|
|
407
|
+
const total = single !== undefined ? single.blocksPerLine * single.blocksPerColumn : frame.mcusPerLine * frame.mcusPerColumn;
|
|
408
|
+
const decodeMcu = (mcu) => {
|
|
409
|
+
if (single !== undefined) {
|
|
410
|
+
// Non-interleaved: one block per MCU, covering only the component's visible blocks.
|
|
411
|
+
const row = Math.floor(mcu / single.blocksPerLine);
|
|
412
|
+
const col = mcu % single.blocksPerLine;
|
|
413
|
+
decodeBlock(single, (row * single.blocksPerLineForMcu + col) * 64);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const mcuRow = Math.floor(mcu / frame.mcusPerLine);
|
|
417
|
+
const mcuCol = mcu % frame.mcusPerLine;
|
|
418
|
+
for (const c of components) {
|
|
419
|
+
for (let v = 0; v < c.v; v++) {
|
|
420
|
+
for (let h = 0; h < c.h; h++) {
|
|
421
|
+
const row = mcuRow * c.v + v;
|
|
422
|
+
const col = mcuCol * c.h + h;
|
|
423
|
+
decodeBlock(c, (row * c.blocksPerLineForMcu + col) * 64);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
for (let mcu = 0; mcu < total;) {
|
|
429
|
+
for (const c of components)
|
|
430
|
+
c.pred = 0;
|
|
431
|
+
eobrun = 0;
|
|
432
|
+
refineState = 0;
|
|
433
|
+
bitCount = 0;
|
|
434
|
+
const end = restartInterval > 0 ? Math.min(total, mcu + restartInterval) : total;
|
|
435
|
+
for (; mcu < end; mcu++)
|
|
436
|
+
decodeMcu(mcu);
|
|
437
|
+
// Find the next marker: a restart marker continues the scan, anything else ends it.
|
|
438
|
+
atMarker = false;
|
|
439
|
+
bitCount = 0;
|
|
440
|
+
while (offset < data.length && !(data[offset] === 0xff && (data[offset + 1] ?? 0) !== 0 && data[offset + 1] !== 0xff))
|
|
441
|
+
offset++;
|
|
442
|
+
const marker = data[offset + 1] ?? 0;
|
|
443
|
+
if (marker >= 0xd0 && marker <= 0xd7) {
|
|
444
|
+
offset += 2;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
return offset;
|
|
450
|
+
}
|
|
451
|
+
// Dequantize, inverse DCT, upsample chroma and convert to RGB.
|
|
452
|
+
function toRgba(frame, quantization, adobeTransform) {
|
|
453
|
+
const planes = frame.components.map((c) => {
|
|
454
|
+
const table = quantization[c.table] ?? fail(`missing quantization table ${c.table}`);
|
|
455
|
+
const width = c.blocksPerLineForMcu * 8;
|
|
456
|
+
const plane = new Uint8Array(width * c.blocksPerColumnForMcu * 8);
|
|
457
|
+
const block = new Float32Array(64);
|
|
458
|
+
const out = new Uint8Array(64);
|
|
459
|
+
for (let row = 0; row < c.blocksPerColumnForMcu; row++) {
|
|
460
|
+
for (let col = 0; col < c.blocksPerLineForMcu; col++) {
|
|
461
|
+
const offset = (row * c.blocksPerLineForMcu + col) * 64;
|
|
462
|
+
for (let i = 0; i < 64; i++)
|
|
463
|
+
block[i] = (c.coefficients[offset + i] ?? 0) * (table[i] ?? 0);
|
|
464
|
+
idct(block, out);
|
|
465
|
+
for (let y = 0; y < 8; y++)
|
|
466
|
+
plane.set(out.subarray(y * 8, y * 8 + 8), (row * 8 + y) * width + col * 8);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
plane, width,
|
|
471
|
+
visibleWidth: Math.ceil((frame.width * c.h) / frame.hmax),
|
|
472
|
+
visibleHeight: Math.ceil((frame.height * c.v) / frame.vmax),
|
|
473
|
+
sx: c.h / frame.hmax, sy: c.v / frame.vmax,
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
const { width, height } = frame;
|
|
477
|
+
const data = new Uint8Array(width * height * 4);
|
|
478
|
+
// Each component resampled to full size: bilinear with centered samples, so 2:1 chroma
|
|
479
|
+
// gets libjpeg's "fancy upsampling" weights (3/4, 1/4).
|
|
480
|
+
const full = planes.map((p) => {
|
|
481
|
+
if (p.sx === 1 && p.sy === 1)
|
|
482
|
+
return { plane: p.plane, stride: p.width };
|
|
483
|
+
const plane = new Uint8Array(width * height);
|
|
484
|
+
const xs = new Int32Array(width), xw = new Float32Array(width);
|
|
485
|
+
for (let x = 0; x < width; x++) {
|
|
486
|
+
const fx = Math.min(Math.max((x + 0.5) * p.sx - 0.5, 0), p.visibleWidth - 1);
|
|
487
|
+
xs[x] = Math.floor(fx);
|
|
488
|
+
xw[x] = fx - Math.floor(fx);
|
|
489
|
+
}
|
|
490
|
+
for (let y = 0; y < height; y++) {
|
|
491
|
+
const fy = Math.min(Math.max((y + 0.5) * p.sy - 0.5, 0), p.visibleHeight - 1);
|
|
492
|
+
const y0 = Math.floor(fy);
|
|
493
|
+
const wy = fy - y0;
|
|
494
|
+
const r0 = y0 * p.width;
|
|
495
|
+
const r1 = Math.min(y0 + 1, p.visibleHeight - 1) * p.width;
|
|
496
|
+
for (let x = 0; x < width; x++) {
|
|
497
|
+
const x0 = xs[x] ?? 0;
|
|
498
|
+
const x1 = Math.min(x0 + 1, p.visibleWidth - 1);
|
|
499
|
+
const wx = xw[x] ?? 0;
|
|
500
|
+
const top = (p.plane[r0 + x0] ?? 0) + ((p.plane[r0 + x1] ?? 0) - (p.plane[r0 + x0] ?? 0)) * wx;
|
|
501
|
+
const bottom = (p.plane[r1 + x0] ?? 0) + ((p.plane[r1 + x1] ?? 0) - (p.plane[r1 + x0] ?? 0)) * wx;
|
|
502
|
+
plane[y * width + x] = Math.round(top + (bottom - top) * wy);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return { plane, stride: width };
|
|
506
|
+
});
|
|
507
|
+
const [a, b, c] = full;
|
|
508
|
+
if (a === undefined)
|
|
509
|
+
return fail("no components");
|
|
510
|
+
if (b === undefined || c === undefined) {
|
|
511
|
+
for (let y = 0; y < height; y++) {
|
|
512
|
+
for (let x = 0; x < width; x++) {
|
|
513
|
+
const g = a.plane[y * a.stride + x] ?? 0;
|
|
514
|
+
const o = (y * width + x) * 4;
|
|
515
|
+
data[o] = g;
|
|
516
|
+
data[o + 1] = g;
|
|
517
|
+
data[o + 2] = g;
|
|
518
|
+
data[o + 3] = 255;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return { width, height, data };
|
|
522
|
+
}
|
|
523
|
+
// Three components are YCbCr unless an Adobe marker says "no transform" or the component ids
|
|
524
|
+
// spell R, G, B.
|
|
525
|
+
const ids = frame.components.map((comp) => comp.id).join(",");
|
|
526
|
+
const rgb = adobeTransform === 0 || ids === "82,71,66";
|
|
527
|
+
const clamp = (v) => (v < 0 ? 0 : v > 255 ? 255 : Math.round(v));
|
|
528
|
+
for (let y = 0; y < height; y++) {
|
|
529
|
+
for (let x = 0; x < width; x++) {
|
|
530
|
+
const Y = a.plane[y * a.stride + x] ?? 0;
|
|
531
|
+
const Cb = b.plane[y * b.stride + x] ?? 0;
|
|
532
|
+
const Cr = c.plane[y * c.stride + x] ?? 0;
|
|
533
|
+
const o = (y * width + x) * 4;
|
|
534
|
+
if (rgb) {
|
|
535
|
+
data[o] = Y;
|
|
536
|
+
data[o + 1] = Cb;
|
|
537
|
+
data[o + 2] = Cr;
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
data[o] = clamp(Y + 1.402 * (Cr - 128));
|
|
541
|
+
data[o + 1] = clamp(Y - 0.344136 * (Cb - 128) - 0.714136 * (Cr - 128));
|
|
542
|
+
data[o + 2] = clamp(Y + 1.772 * (Cb - 128));
|
|
543
|
+
}
|
|
544
|
+
data[o + 3] = 255;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return { width, height, data };
|
|
548
|
+
}
|
|
549
|
+
// Separable float IDCT: out(x) = Σ_u C(u)/2 · F(u) · cos((2x+1)uπ/16), rows then columns,
|
|
550
|
+
// skipping rows that are all zero past DC (the common case after quantization).
|
|
551
|
+
const COS = (() => {
|
|
552
|
+
const table = new Float32Array(64);
|
|
553
|
+
for (let x = 0; x < 8; x++) {
|
|
554
|
+
for (let u = 0; u < 8; u++)
|
|
555
|
+
table[x * 8 + u] = ((u === 0 ? Math.SQRT1_2 : 1) / 2) * Math.cos(((2 * x + 1) * u * Math.PI) / 16);
|
|
556
|
+
}
|
|
557
|
+
return table;
|
|
558
|
+
})();
|
|
559
|
+
const scratch = new Float32Array(64);
|
|
560
|
+
function idct(block, out) {
|
|
561
|
+
for (let y = 0; y < 8; y++) {
|
|
562
|
+
const r = y * 8;
|
|
563
|
+
let ac = false;
|
|
564
|
+
for (let u = 1; u < 8; u++)
|
|
565
|
+
if ((block[r + u] ?? 0) !== 0) {
|
|
566
|
+
ac = true;
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
if (!ac) {
|
|
570
|
+
const dc = (block[r] ?? 0) * (COS[0] ?? 0);
|
|
571
|
+
for (let x = 0; x < 8; x++)
|
|
572
|
+
scratch[r + x] = dc;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
for (let x = 0; x < 8; x++) {
|
|
576
|
+
let sum = 0;
|
|
577
|
+
for (let u = 0; u < 8; u++)
|
|
578
|
+
sum += (block[r + u] ?? 0) * (COS[x * 8 + u] ?? 0);
|
|
579
|
+
scratch[r + x] = sum;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
for (let x = 0; x < 8; x++) {
|
|
583
|
+
for (let y = 0; y < 8; y++) {
|
|
584
|
+
let sum = 0;
|
|
585
|
+
for (let v = 0; v < 8; v++)
|
|
586
|
+
sum += (scratch[v * 8 + x] ?? 0) * (COS[y * 8 + v] ?? 0);
|
|
587
|
+
const value = Math.round(sum + 128);
|
|
588
|
+
out[y * 8 + x] = value < 0 ? 0 : value > 255 ? 255 : value;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RgbaImage } from "./types.js";
|
|
2
|
+
export type Codec = "png" | "jpeg" | "qoi" | "wav" | "resize";
|
|
3
|
+
/** Malformed or unsupported input to one of the import-time codecs. */
|
|
4
|
+
export declare class CodecError extends Error {
|
|
5
|
+
readonly codec: Codec;
|
|
6
|
+
readonly name = "CodecError";
|
|
7
|
+
constructor(codec: Codec, message: string);
|
|
8
|
+
}
|
|
9
|
+
export declare function decodePng(bytes: Uint8Array, inflate: (zlib: Uint8Array) => Uint8Array): RgbaImage;
|