@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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -2
  3. package/dist/assets/codecs/jpeg.d.ts +2 -0
  4. package/dist/assets/codecs/jpeg.js +591 -0
  5. package/dist/assets/codecs/png.d.ts +9 -0
  6. package/dist/assets/codecs/png.js +250 -0
  7. package/dist/assets/codecs/qoi.d.ts +3 -0
  8. package/dist/assets/codecs/qoi.js +157 -0
  9. package/dist/assets/codecs/resize.d.ts +2 -0
  10. package/dist/assets/codecs/resize.js +121 -0
  11. package/dist/assets/codecs/types.d.ts +13 -0
  12. package/dist/assets/codecs/types.js +2 -0
  13. package/dist/assets/codecs/wav.d.ts +6 -0
  14. package/dist/assets/codecs/wav.js +155 -0
  15. package/dist/assets/paths.d.ts +28 -0
  16. package/dist/assets/paths.js +24 -0
  17. package/dist/assets/store.d.ts +99 -0
  18. package/dist/assets/store.js +212 -0
  19. package/dist/audio/mix.d.ts +52 -0
  20. package/dist/audio/mix.js +257 -0
  21. package/dist/evaluate/evaluate.d.ts +40 -0
  22. package/dist/evaluate/evaluate.js +485 -0
  23. package/dist/evaluate/scene-frame.d.ts +65 -0
  24. package/dist/evaluate/scene-frame.js +248 -0
  25. package/dist/evaluate/tree.d.ts +94 -0
  26. package/dist/evaluate/tree.js +1 -0
  27. package/dist/evaluate/typing.d.ts +13 -0
  28. package/dist/evaluate/typing.js +93 -0
  29. package/dist/index.d.ts +22 -0
  30. package/dist/index.js +22 -0
  31. package/dist/layout/layout.d.ts +30 -0
  32. package/dist/layout/layout.js +513 -0
  33. package/dist/media/mp4.d.ts +28 -0
  34. package/dist/media/mp4.js +177 -0
  35. package/dist/media/video.d.ts +23 -0
  36. package/dist/media/video.js +32 -0
  37. package/dist/media/yuv.d.ts +34 -0
  38. package/dist/media/yuv.js +82 -0
  39. package/dist/paint/geometry.d.ts +34 -0
  40. package/dist/paint/geometry.js +107 -0
  41. package/dist/paint/list.d.ts +132 -0
  42. package/dist/paint/list.js +270 -0
  43. package/dist/paint/morph.d.ts +17 -0
  44. package/dist/paint/morph.js +210 -0
  45. package/dist/paint/paint.d.ts +120 -0
  46. package/dist/paint/paint.js +676 -0
  47. package/dist/paint/stroke.d.ts +26 -0
  48. package/dist/paint/stroke.js +233 -0
  49. package/dist/raster/backend.d.ts +22 -0
  50. package/dist/raster/backend.js +1 -0
  51. package/dist/raster/raster.d.ts +43 -0
  52. package/dist/raster/raster.js +154 -0
  53. package/dist/raster/wasm-bytes.d.ts +1 -0
  54. package/dist/raster/wasm-bytes.js +3 -0
  55. package/dist/render/compose.d.ts +19 -0
  56. package/dist/render/compose.js +110 -0
  57. package/dist/render/renderer.d.ts +76 -0
  58. package/dist/render/renderer.js +207 -0
  59. package/dist/render/size.d.ts +6 -0
  60. package/dist/render/size.js +7 -0
  61. package/dist/render/sounds.d.ts +15 -0
  62. package/dist/render/sounds.js +49 -0
  63. package/dist/style/computed.d.ts +142 -0
  64. package/dist/style/computed.js +1 -0
  65. package/dist/style/parse.d.ts +19 -0
  66. package/dist/style/parse.js +180 -0
  67. package/dist/style/resolve.d.ts +10 -0
  68. package/dist/style/resolve.js +289 -0
  69. package/dist/style/svg.d.ts +4 -0
  70. package/dist/style/svg.js +185 -0
  71. package/dist/text/font-types.d.ts +63 -0
  72. package/dist/text/font-types.js +3 -0
  73. package/dist/text/fonts.d.ts +28 -0
  74. package/dist/text/fonts.js +61 -0
  75. package/dist/text/paragraph.d.ts +65 -0
  76. package/dist/text/paragraph.js +220 -0
  77. package/dist/text/sfnt.d.ts +51 -0
  78. package/dist/text/sfnt.js +105 -0
  79. package/dist/text/shape.d.ts +15 -0
  80. package/dist/text/shape.js +28 -0
  81. package/dist/text/ttf.d.ts +6 -0
  82. package/dist/text/ttf.js +727 -0
  83. package/dist/text/variation.d.ts +53 -0
  84. package/dist/text/variation.js +367 -0
  85. package/dist/text/woff2.d.ts +1 -0
  86. package/dist/text/woff2.js +390 -0
  87. package/package.json +32 -4
  88. 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
- # Temporary Holding Version
1
+ # @tongue-core/engine
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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,2 @@
1
+ import type { RgbaImage } from "./types.js";
2
+ export declare function decodeJpeg(bytes: Uint8Array): RgbaImage;
@@ -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;