flet-box 1.0.6 → 1.0.7

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 (94) hide show
  1. package/README.md +28 -1
  2. package/bin/cli.js +77 -11
  3. package/bin/commands/auditFix.js +682 -0
  4. package/bin/commands/checkApi.js +7 -5
  5. package/bin/commands/create.js +40 -10
  6. package/bin/commands/createBundle.js +54 -1
  7. package/bin/commands/images.js +349 -0
  8. package/bin/commands/pruneAssets.js +170 -0
  9. package/bin/commands/release.js +34 -1
  10. package/bin/commands/runCloudflare.js +47 -9
  11. package/bin/commands/runServer.js +81 -24
  12. package/bin/commands/toPng.js +346 -0
  13. package/bin/commands/unpack.js +1173 -0
  14. package/bin/ota/loader.js +90 -0
  15. package/bin/utils/appSources.js +101 -0
  16. package/bin/utils/bundleConfig.js +166 -0
  17. package/bin/utils/dtsProps.js +147 -0
  18. package/bin/utils/imageTools.js +271 -0
  19. package/bin/utils/images/bmp.js +99 -0
  20. package/bin/utils/images/gif.js +244 -0
  21. package/bin/utils/images/index.js +163 -0
  22. package/bin/utils/images/jpeg.js +643 -0
  23. package/bin/utils/images/png.js +486 -0
  24. package/bin/utils/images/unsupported.js +29 -0
  25. package/bin/utils/neutralise.js +109 -6
  26. package/bin/utils/templates.js +307 -64
  27. package/docs/cli/README.md +82 -0
  28. package/docs/guides/online-updates.md +57 -9
  29. package/docs/widget/Accordion.md +1 -0
  30. package/docs/widget/AdMob.md +1 -0
  31. package/docs/widget/AdSense.md +1 -0
  32. package/docs/widget/AnimatedBox.md +1 -0
  33. package/docs/widget/AnimatedText.md +1 -0
  34. package/docs/widget/Audio.md +2 -1
  35. package/docs/widget/Avatar.md +12 -3
  36. package/docs/widget/Badge.md +1 -0
  37. package/docs/widget/Button.md +1 -0
  38. package/docs/widget/Card.md +1 -0
  39. package/docs/widget/Carousel.md +1 -0
  40. package/docs/widget/Chart.md +1 -0
  41. package/docs/widget/Checkbox.md +1 -0
  42. package/docs/widget/Chip.md +1 -0
  43. package/docs/widget/CircularBar.md +1 -0
  44. package/docs/widget/CircularChart.md +1 -0
  45. package/docs/widget/CodeViewer.md +1 -0
  46. package/docs/widget/Column.md +1 -0
  47. package/docs/widget/Container.md +1 -0
  48. package/docs/widget/DataTable.md +1 -0
  49. package/docs/widget/Divider.md +1 -0
  50. package/docs/widget/DraggBox.md +3 -2
  51. package/docs/widget/Dropdown.md +3 -0
  52. package/docs/widget/DroppBox.md +2 -1
  53. package/docs/widget/FloatingActionButton.md +1 -0
  54. package/docs/widget/GridView.md +1 -0
  55. package/docs/widget/Icon.md +1 -0
  56. package/docs/widget/Image.md +11 -3
  57. package/docs/widget/Input.md +1 -0
  58. package/docs/widget/InstallButton.md +1 -0
  59. package/docs/widget/ListTile.md +1 -0
  60. package/docs/widget/ListView.md +1 -0
  61. package/docs/widget/Maps.md +188 -0
  62. package/docs/widget/Markdown.md +1 -0
  63. package/docs/widget/MatrixRain.md +1 -0
  64. package/docs/widget/Pagination.md +1 -0
  65. package/docs/widget/ParallaxBox.md +1 -0
  66. package/docs/widget/ProgressBar.md +1 -0
  67. package/docs/widget/QRCode.md +1 -0
  68. package/docs/widget/README.md +12 -10
  69. package/docs/widget/Radio.md +1 -0
  70. package/docs/widget/Rating.md +1 -0
  71. package/docs/widget/Row.md +1 -0
  72. package/docs/widget/Skeleton.md +1 -0
  73. package/docs/widget/Slider.md +1 -0
  74. package/docs/widget/Stack.md +1 -0
  75. package/docs/widget/Stepper.md +1 -0
  76. package/docs/widget/Switch.md +1 -0
  77. package/docs/widget/Text.md +1 -0
  78. package/docs/widget/Tooltip.md +11 -13
  79. package/docs/widget/TreeView.md +1 -0
  80. package/docs/widget/Video.md +3 -2
  81. package/package.json +2 -2
  82. package/src/core/hmr-client.js +129 -34
  83. package/src/index.d.ts +132 -0
  84. package/src/index.js +1 -0
  85. package/src/navigations/AppBar.js +15 -4
  86. package/src/navigations/Drawer.js +9 -1
  87. package/src/widget-factory/translateProps.js +32 -1
  88. package/src/widgets/Avatar.js +228 -131
  89. package/src/widgets/Container.js +13 -0
  90. package/src/widgets/Dropdown.js +33 -1
  91. package/src/widgets/Image.js +102 -3
  92. package/src/widgets/Maps.js +1495 -0
  93. package/src/widgets/Tooltip.js +12 -2
  94. package/src/widgets/index.js +1 -0
@@ -0,0 +1,271 @@
1
+ /**
2
+ * @file bin/utils/imageTools.js
3
+ * @description Finds the lossy image encoder this machine already has, and names
4
+ * the one command that installs it when it does not.
5
+ *
6
+ * `to-png` is the zero-dependency half of image work: it rewrites flat art as the
7
+ * smallest lossless PNG Node core can encode. A photograph is never small in a
8
+ * lossless format, and the only honest way to shrink one is a real encoder —
9
+ * WebP or AVIF. This module does not implement one; it delegates to the platform
10
+ * binary, exactly as `runCloudflare.js` delegates the tunnel to `cloudflared` and
11
+ * `buildAndroid.js` delegates compiling to the Android SDK.
12
+ *
13
+ * A binary is not an npm dependency, so this stays inside the package contract:
14
+ * `dependencies: {}`, and nothing here may import outside `node:*`
15
+ * (scripts/check-cli-install.mjs is the tripwire).
16
+ */
17
+
18
+ import fs from "node:fs";
19
+ import os from "node:os";
20
+ import path from "node:path";
21
+ import { spawnSync } from "node:child_process";
22
+
23
+ /**
24
+ * One encoder invocation spelled out, so the command that runs it stays generic.
25
+ *
26
+ * @typedef {object} Encoder
27
+ * @property {string} bin Executable to probe and run.
28
+ * @property {string} format Output container it writes.
29
+ * @property {string} probe Args that only print a version.
30
+ * @property {(o: {width: number, quality: number, method: number, src: string, out: string}) => string[]} args
31
+ */
32
+
33
+ /** WebP via libwebp's own CLI: the smallest, most predictable surface. */
34
+ const CWEBP = {
35
+ bin: "cwebp",
36
+ format: "webp",
37
+ probe: ["-version"],
38
+ args: ({ width, quality, method, src, out }) => [
39
+ "-quiet",
40
+ "-resize",
41
+ String(width),
42
+ "0",
43
+ "-q",
44
+ String(quality),
45
+ "-m",
46
+ String(method),
47
+ src,
48
+ "-o",
49
+ out,
50
+ ],
51
+ };
52
+
53
+ /** ImageMagick, v7 (`magick`) and v6 (`convert`) take the same arguments here. */
54
+ const imagemagick = (bin) => ({
55
+ bin,
56
+ format: "webp",
57
+ probe: ["-version"],
58
+ args: ({ width, quality, method, src, out }) => [
59
+ src,
60
+ "-resize",
61
+ `${width}x`,
62
+ "-quality",
63
+ String(quality),
64
+ "-define",
65
+ `webp:method=${method}`,
66
+ out,
67
+ ],
68
+ });
69
+
70
+ /**
71
+ * Order matters: `cwebp` is the reference encoder for the container, and the two
72
+ * ImageMagick spellings catch the machines where the distro shipped that instead.
73
+ */
74
+ const ENCODERS = [CWEBP, imagemagick("magick"), imagemagick("convert")];
75
+
76
+ /**
77
+ * The package manager this machine is expected to have.
78
+ *
79
+ * macOS and Windows are read off `process.platform`. Linux needs the actual
80
+ * distribution: `apt`, `dnf` and `pacman` are different sentences for the same
81
+ * package, and guessing from the kernel would tell an Arch user to run apt.
82
+ *
83
+ * @returns {{id: string, name: string, install: string[]}}
84
+ */
85
+ export const platformInstaller = () => {
86
+ if (process.platform === "darwin") {
87
+ return { id: "brew", name: "Homebrew", install: ["brew", "install", "webp"] };
88
+ }
89
+ if (process.platform === "win32") {
90
+ return {
91
+ id: "winget",
92
+ name: "winget",
93
+ install: ["winget", "install", "--id", "Google.WebP", "-e", "--accept-package-agreements", "--accept-source-agreements"],
94
+ };
95
+ }
96
+ if (process.platform !== "linux") {
97
+ return { id: "unknown", name: process.platform, install: [] };
98
+ }
99
+ // /etc/os-release is the only ID every mainstream distro keeps current.
100
+ let lines = [];
101
+ try {
102
+ lines = fs.readFileSync("/etc/os-release", "utf8").split("\n");
103
+ } catch {
104
+ lines = [];
105
+ }
106
+ const read = (key) => {
107
+ const hit = lines.find((l) => l.startsWith(`${key}=`));
108
+ return hit ? hit.slice(key.length + 1).replace(/^"|"$/g, "") : "";
109
+ };
110
+ const family = [read("ID"), ...read("ID_LIKE").split(/\s+/)].filter(Boolean);
111
+ if (family.some((f) => /debian|ubuntu|linuxmint/.test(f))) {
112
+ return { id: "apt", name: "apt", install: ["sudo", "apt-get", "install", "-y", "webp"] };
113
+ }
114
+ if (family.some((f) => /fedora|rhel|centos|amzn/.test(f))) {
115
+ return { id: "dnf", name: "dnf", install: ["sudo", "dnf", "install", "-y", "libwebp-tools"] };
116
+ }
117
+ if (family.some((f) => /arch|manjaro|endeavouros/.test(f))) {
118
+ return { id: "pacman", name: "pacman", install: ["sudo", "pacman", "-S", "--noconfirm", "libwebp"] };
119
+ }
120
+ return { id: "apk", name: "apk", install: ["sudo", "apk", "add", "--no-cache", "webp"] };
121
+ };
122
+
123
+ /**
124
+ * True when `bin` is runnable — probed with `--version`, never with `which`,
125
+ * so a shim that exists but is broken is not counted as an encoder.
126
+ *
127
+ * @param {string} bin
128
+ * @returns {boolean}
129
+ */
130
+ const isRunnable = (bin) => {
131
+ const probe = spawnSync(bin, ["--version"], { encoding: "utf8" });
132
+ return !probe.error;
133
+ };
134
+
135
+ /**
136
+ * @returns {Encoder|null} The first usable encoder on PATH.
137
+ */
138
+ export const findEncoder = () => {
139
+ for (const candidate of ENCODERS) {
140
+ if (isRunnable(candidate.bin)) return candidate;
141
+ }
142
+ return null;
143
+ };
144
+
145
+ /**
146
+ * Runs one encoder to a temp file and returns the bytes it produced.
147
+ *
148
+ * @param {Encoder} encoder
149
+ * @param {{src: string, width: number, quality: number, method?: number, out: string}} opts
150
+ * @returns {number|null} Written size, or null when the encoder failed.
151
+ */
152
+ export const encode = (encoder, { src, width, quality, method = 6, out }) => {
153
+ const res = spawnSync(
154
+ encoder.bin,
155
+ encoder.args({ src, width, quality, method, out }),
156
+ { stdio: "ignore", encoding: "utf8" },
157
+ );
158
+ if (res.error || res.status !== 0 || !fs.existsSync(out)) return null;
159
+ return fs.statSync(out).size;
160
+ };
161
+
162
+ /**
163
+ * Asks before touching the system. Installing a package is a machine-wide change
164
+ * with a hundred ways to be wrong (no brew, no sudo rights, a locked-down laptop),
165
+ * and a build command that silently runs `sudo` is not something a developer
166
+ * expects from a UI framework's CLI.
167
+ *
168
+ * @param {Encoder|null} encoder - What was already found, if anything.
169
+ * @param {{autoInstall: boolean, isTTY: boolean, quiet: boolean}} opts
170
+ * @returns {Promise<Encoder|null>} The encoder to use, or null when it is missing.
171
+ */
172
+ export const ensureEncoder = async (encoder, { autoInstall = false, isTTY = process.stdout.isTTY, quiet = false } = {}) => {
173
+ if (encoder) return encoder;
174
+
175
+ const installer = platformInstaller();
176
+ const said = (line) => !quiet && console.log(line);
177
+ const found = findEncoder();
178
+ if (found) return found;
179
+
180
+ console.log("");
181
+ console.log(` No lossy WebP encoder is on PATH (tried: ${ENCODERS.map((e) => e.bin).join(", ")}).`);
182
+ if (!installer.install.length) {
183
+ said(` This platform (${process.platform}) has no known package manager here.`);
184
+ console.log(" Install libwebp's `cwebp` by hand, or keep the lossless `flet-box to-png`.");
185
+ return null;
186
+ }
187
+ const command = installer.install.join(" ");
188
+ console.log(` Install it with ${installer.name}: ${command}`);
189
+
190
+ if (!autoInstall) {
191
+ said(" Re-run with --install and this CLI will do it for you.");
192
+ return null;
193
+ }
194
+ if (!isTTY) {
195
+ console.log(" --install needs a terminal (it may ask for a password). Run the command above and retry.");
196
+ return null;
197
+ }
198
+ const res = spawnSync(command.split(" ")[0], command.split(" ").slice(1), { stdio: "inherit" });
199
+ if (res.error || res.status !== 0) {
200
+ console.log(` ${command} did not succeed — that machine decides, not this CLI.`);
201
+ return null;
202
+ }
203
+ const after = findEncoder();
204
+ if (after) console.log(` Installed: ${after.bin}`);
205
+ return after;
206
+ };
207
+
208
+ /**
209
+ * Where a scratch file can safely sit next to the asset being written.
210
+ *
211
+ * @param {string} dir
212
+ * @param {string} name
213
+ * @returns {string}
214
+ */
215
+ export const tempIn = (dir, name) => path.join(dir, `.${name}.fletbox-tmp.webp`);
216
+
217
+ /**
218
+ * True when any pixel of a decoded frame is see-through.
219
+ *
220
+ * @param {Buffer} rgba
221
+ * @returns {boolean}
222
+ */
223
+ export const hasTransparency = (rgba) => {
224
+ for (let i = 3; i < rgba.length; i += 4) if (rgba[i] !== 255) return true;
225
+ return false;
226
+ };
227
+
228
+ /**
229
+ * True when the WebP bytes carry an alpha plane.
230
+ *
231
+ * There is no WebP decoder in this package — that is what `to-png` declines at
232
+ * `bin/utils/images/index.js` — so "read every pixel back" is not available here.
233
+ * The container is: a transparent WebP says so in its chunk layout, and a lossy
234
+ * encoder that dropped the plane produces a file that simply lacks it. Three
235
+ * spellings, because the encoder chooses between them:
236
+ *
237
+ * VP8X extended header, bit 4 of its first flag byte is the A flag
238
+ * ALPH the separate alpha chunk a lossy frame with transparency gets
239
+ * VP8L lossless: bit 28 of the packed size/version word says alpha is used
240
+ *
241
+ * A plain lossy `VP8 ` frame has no alpha at all, which is the case this catches:
242
+ * an icon or a sticker flattened onto white would otherwise ship as the picture.
243
+ *
244
+ * @param {Buffer} buf - WebP file bytes.
245
+ * @returns {boolean}
246
+ */
247
+ export const webpCarriesAlpha = (buf) => {
248
+ if (buf.length < 12) return false;
249
+ if (buf.toString("ascii", 0, 4) !== "RIFF") return false;
250
+ if (buf.toString("ascii", 8, 12) !== "WEBP") return false;
251
+ let at = 12;
252
+ while (at + 8 <= buf.length) {
253
+ const fourcc = buf.toString("ascii", at, at + 4);
254
+ const size = buf.readUInt32LE(at + 4);
255
+ const body = buf.subarray(at + 8, Math.min(at + 8 + size, buf.length));
256
+ if (fourcc === "ALPH") return true;
257
+ if (fourcc === "VP8X") return body.length > 0 && (body[0] & 0x10) !== 0;
258
+ if (fourcc === "VP8L") {
259
+ // byte 0 is the 0x2f lossless signature, then the packed 32-bit field.
260
+ if (body.length < 5) return false;
261
+ return ((body.readUInt32LE(1) >> 28) & 1) === 1;
262
+ }
263
+ // Chunks are padded to an even byte count.
264
+ at += 8 + size + (size % 2);
265
+ }
266
+ return false;
267
+ };
268
+
269
+ /** Kept for callers that want to report the machine they probed. */
270
+ export const describeMachine = () => `${os.platform()}/${os.arch()} · ${platformInstaller().name}`;
271
+
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @file bin/utils/images/bmp.js
3
+ * @description Uncompressed Windows BMP reader.
4
+ *
5
+ * BMP is the one raster format whose pixels can be lifted straight out of the
6
+ * file, so this is mostly header arithmetic. Run-length variants (BI_RLE8,
7
+ * BI_RLE4) and OS/2 headers are refused rather than guessed at: they are
8
+ * decades old, rare in an assets folder, and a half-read RLE stream produces
9
+ * garbage that still looks like a picture.
10
+ */
11
+
12
+ import UnsupportedImage from "./unsupported.js";
13
+
14
+ /**
15
+ * Decodes a BMP into RGBA.
16
+ *
17
+ * @param {Buffer} buf - File bytes.
18
+ * @returns {{ width: number, height: number, rgba: Buffer }}
19
+ * @throws {Error} Naming the unsupported combination of header fields.
20
+ */
21
+ export const decodeBmp = (buf) => {
22
+ if (buf.length < 54 || buf.toString("ascii", 0, 2) !== "BM") {
23
+ throw new Error("not a BMP: missing the BM signature");
24
+ }
25
+
26
+ const dataOffset = buf.readUInt32LE(10);
27
+ const headerSize = buf.readUInt32LE(14);
28
+ if (headerSize < 40) throw new UnsupportedImage("BMP", "OS/2 BITMAPCOREHEADER is not implemented");
29
+
30
+ const width = buf.readInt32LE(18);
31
+ const rawHeight = buf.readInt32LE(22);
32
+ const bitCount = buf.readUInt16LE(28);
33
+ const compression = buf.readUInt32LE(30);
34
+
35
+ if (compression !== 0) {
36
+ throw new UnsupportedImage("BMP", `compression mode ${compression} (only uncompressed BI_RGB is implemented)`);
37
+ }
38
+ if (![1, 4, 8, 24, 32].includes(bitCount)) {
39
+ throw new UnsupportedImage("BMP", `${bitCount}-bit depth is not implemented (1, 4, 8, 24 and 32 are)`);
40
+ }
41
+
42
+ const topDown = rawHeight < 0;
43
+ const height = Math.abs(rawHeight);
44
+ const palette = [];
45
+ if (bitCount <= 8) {
46
+ // `biClrUsed` lives at 46 — reading it earlier returns the low bytes of the
47
+ // image size, and a file whose "palette" claims 67 million entries is only
48
+ // one bad offset away from exhausting the heap.
49
+ const paletteStart = 14 + headerSize;
50
+ const declared = buf.readUInt32LE(46);
51
+ const needed = 1 << bitCount;
52
+ const entries = declared > 0 ? Math.min(declared, needed) : needed;
53
+ const room = Math.floor((dataOffset - paletteStart) / 4);
54
+ if (room < entries) {
55
+ throw new Error(`BMP palette claims ${entries} entries but only ${Math.max(0, room)} fit before the pixels`);
56
+ }
57
+ for (let i = 0; i < entries; i++) {
58
+ const base = paletteStart + i * 4;
59
+ palette.push([buf[base + 2], buf[base + 1], buf[base], 255]);
60
+ }
61
+ }
62
+
63
+ const rgba = Buffer.alloc(width * height * 4);
64
+ const rowBytes = Math.ceil((width * bitCount) / 8);
65
+ const stride = Math.ceil(rowBytes / 4) * 4;
66
+
67
+ for (let y = 0; y < height; y++) {
68
+ const sourceRow = dataOffset + (topDown ? y : height - 1 - y) * stride;
69
+ for (let x = 0; x < width; x++) {
70
+ const dst = (y * width + x) * 4;
71
+ if (bitCount === 32) {
72
+ const base = sourceRow + x * 4;
73
+ rgba[dst] = buf[base + 2];
74
+ rgba[dst + 1] = buf[base + 1];
75
+ rgba[dst + 2] = buf[base];
76
+ rgba[dst + 3] = buf[base + 3];
77
+ } else if (bitCount === 24) {
78
+ const base = sourceRow + x * 3;
79
+ rgba[dst] = buf[base + 2];
80
+ rgba[dst + 1] = buf[base + 1];
81
+ rgba[dst + 2] = buf[base];
82
+ rgba[dst + 3] = 255;
83
+ } else {
84
+ const shift = bitCount === 1 ? 7 - (x % 8) : bitCount === 4 ? (x % 2 ? 0 : 4) : 0;
85
+ const mask = (1 << bitCount) - 1;
86
+ const index = (buf[sourceRow + Math.floor((x * bitCount) / 8)] >> shift) & mask;
87
+ const colour = palette[index] || [0, 0, 0, 255];
88
+ rgba[dst] = colour[0];
89
+ rgba[dst + 1] = colour[1];
90
+ rgba[dst + 2] = colour[2];
91
+ rgba[dst + 3] = colour[3];
92
+ }
93
+ }
94
+ }
95
+
96
+ return { width, height, rgba };
97
+ };
98
+
99
+ export default decodeBmp;
@@ -0,0 +1,244 @@
1
+ /**
2
+ * @file bin/utils/images/gif.js
3
+ * @description GIF reader: LZW, global or local palettes, transparency, interlace.
4
+ *
5
+ * A GIF is a canvas with one or more frames on it, and a PNG is one still
6
+ * image, so the conversion has to pick a frame. This takes the first one and
7
+ * places it at its own offset inside the logical screen, which is where a
8
+ * browser would show it. An animated file is converted with a note saying only
9
+ * the first frame survived — silently dropping the rest of a cartoon would be
10
+ * the worse half of that trade.
11
+ */
12
+
13
+ /**
14
+ * Decodes a GIF into RGBA.
15
+ *
16
+ * @param {Buffer} buf - File bytes.
17
+ * @returns {{ width: number, height: number, rgba: Buffer, note?: string }}
18
+ * @throws {Error} Naming the corruption or the feature this reader lacks.
19
+ */
20
+ export const decodeGif = (buf) => {
21
+ const magic = buf.toString("ascii", 0, 6);
22
+ if (magic !== "GIF87a" && magic !== "GIF89a") throw new Error("not a GIF: bad signature");
23
+
24
+ const canvasWidth = buf.readUInt16LE(6);
25
+ const canvasHeight = buf.readUInt16LE(8);
26
+ const packed = buf[10];
27
+ let offset = 13;
28
+
29
+ let globalTable = null;
30
+ if (packed & 0x80) {
31
+ globalTable = readColorTable(buf, offset, 2 << (packed & 7));
32
+ offset += 3 * (2 << (packed & 7));
33
+ }
34
+
35
+ let transparentIndex = -1;
36
+ let frames = 0;
37
+ let width = canvasWidth;
38
+ let height = canvasHeight;
39
+ let rgba = null;
40
+
41
+ while (offset < buf.length) {
42
+ const introducer = buf[offset];
43
+ offset++;
44
+
45
+ if (introducer === 0x3b) break; // trailer
46
+
47
+ if (introducer === 0x21) {
48
+ const label = buf[offset];
49
+ const blocks = readBlocks(buf, offset + 1);
50
+ if (label === 0xf9 && blocks.data.length >= 4 && blocks.data[0] & 0x01) {
51
+ transparentIndex = blocks.data[3];
52
+ }
53
+ offset = blocks.next;
54
+ continue;
55
+ }
56
+
57
+ if (introducer !== 0x2c) throw new Error(`unexpected GIF block 0x${introducer.toString(16)}`);
58
+
59
+ const x = buf.readUInt16LE(offset);
60
+ const y = buf.readUInt16LE(offset + 2);
61
+ const frameWidth = buf.readUInt16LE(offset + 4);
62
+ const frameHeight = buf.readUInt16LE(offset + 6);
63
+ const framePacked = buf.readUInt16LE(offset + 8) & 0xff;
64
+ offset += 9;
65
+
66
+ let table = globalTable;
67
+ if (framePacked & 0x80) {
68
+ const entries = 2 << (framePacked & 7);
69
+ table = readColorTable(buf, offset, entries);
70
+ offset += 3 * entries;
71
+ }
72
+ if (!table) throw new Error("GIF frame has no colour table");
73
+
74
+ frames++;
75
+ if (frames === 1) {
76
+ width = canvasWidth;
77
+ height = canvasHeight;
78
+ rgba = Buffer.alloc(canvasWidth * canvasHeight * 4);
79
+ const interlaced = (framePacked & 0x40) !== 0;
80
+ const pixels = decodeLzw(buf, offset, frameWidth * frameHeight);
81
+ const rows = interlaced ? deinterlace(frameHeight) : Array.from({ length: frameHeight }, (_, i) => i);
82
+
83
+ for (let row = 0; row < frameHeight; row++) {
84
+ const targetY = y + rows[row];
85
+ if (targetY < 0 || targetY >= canvasHeight) continue;
86
+ for (let col = 0; col < frameWidth; col++) {
87
+ const targetX = x + col;
88
+ if (targetX < 0 || targetX >= canvasWidth) continue;
89
+ const index = pixels[row * frameWidth + col];
90
+ const colour = table[index] || [0, 0, 0, 255];
91
+ const dst = (targetY * canvasWidth + targetX) * 4;
92
+ rgba[dst] = colour[0];
93
+ rgba[dst + 1] = colour[1];
94
+ rgba[dst + 2] = colour[2];
95
+ rgba[dst + 3] = index === transparentIndex ? 0 : colour[3];
96
+ }
97
+ }
98
+ }
99
+ offset = skipImage(buf, offset);
100
+ }
101
+
102
+ if (!rgba) throw new Error("GIF has no image block");
103
+
104
+ return {
105
+ width,
106
+ height,
107
+ rgba,
108
+ note: frames > 1 ? `${frames} frames: only the first was kept` : undefined,
109
+ };
110
+ };
111
+
112
+ /**
113
+ * @param {Buffer} buf
114
+ * @param {number} start - Offset of the first table entry.
115
+ * @param {number} entries
116
+ * @returns {number[][]} `[r, g, b, a]` per entry; GIF stores no alpha.
117
+ */
118
+ const readColorTable = (buf, start, entries) => {
119
+ const table = [];
120
+ for (let i = 0; i < entries; i++) {
121
+ const base = start + i * 3;
122
+ table.push([buf[base], buf[base + 1], buf[base + 2], 255]);
123
+ }
124
+ return table;
125
+ };
126
+
127
+ /**
128
+ * Walks a GIF sub-block chain.
129
+ *
130
+ * @param {Buffer} buf
131
+ * @param {number} start - Offset of the first size byte.
132
+ * @returns {{ data: Buffer, next: number }}
133
+ */
134
+ const readBlocks = (buf, start) => {
135
+ const parts = [];
136
+ let offset = start;
137
+ while (offset < buf.length) {
138
+ const size = buf[offset];
139
+ offset++;
140
+ if (size === 0) break;
141
+ parts.push(buf.subarray(offset, offset + size));
142
+ offset += size;
143
+ }
144
+ return { data: Buffer.concat(parts), next: offset };
145
+ };
146
+
147
+ /**
148
+ * Reads an image's LZW sub-blocks and decodes them into palette indices.
149
+ *
150
+ * @param {Buffer} buf
151
+ * @param {number} offset - Where the LZW minimum code size byte sits.
152
+ * @param {number} count - Pixels in this frame.
153
+ * @returns {Uint8Array}
154
+ */
155
+ const decodeLzw = (buf, offset, count) => {
156
+ const { data } = readBlocks(buf, offset + 1);
157
+ const minCodeSize = buf[offset];
158
+ const clearCode = 1 << minCodeSize;
159
+ const endCode = clearCode + 1;
160
+
161
+ const pixels = new Uint8Array(count);
162
+ let dict = [];
163
+ let codeSize = minCodeSize + 1;
164
+ let next = endCode + 1;
165
+ let bitPos = 0;
166
+ let written = 0;
167
+ let previous = null;
168
+
169
+ const resetDict = () => {
170
+ dict = [];
171
+ for (let i = 0; i < clearCode; i++) dict[i] = [i];
172
+ dict[clearCode] = [];
173
+ dict[endCode] = [];
174
+ codeSize = minCodeSize + 1;
175
+ next = endCode + 1;
176
+ previous = null;
177
+ };
178
+
179
+ const readCode = () => {
180
+ let code = 0;
181
+ for (let i = 0; i < codeSize; i++) {
182
+ const byteIndex = (bitPos + i) >> 3;
183
+ if (byteIndex >= data.length) return -1;
184
+ code |= ((data[byteIndex] >> ((bitPos + i) & 7)) & 1) << i;
185
+ }
186
+ bitPos += codeSize;
187
+ return code;
188
+ };
189
+
190
+ resetDict();
191
+ while (written < count) {
192
+ const code = readCode();
193
+ if (code === -1 || code === endCode) break;
194
+ if (code === clearCode) {
195
+ resetDict();
196
+ continue;
197
+ }
198
+
199
+ let entry = dict[code];
200
+ if (!entry) {
201
+ // The classic KwKwK case: the code being asked for is the one this
202
+ // iteration is about to define, so the new entry is `previous` plus its
203
+ // own first index.
204
+ if (!previous) throw new Error("GIF LZW stream references an undefined code");
205
+ entry = [...previous, previous[0]];
206
+ dict[next++] = entry;
207
+ } else if (previous) {
208
+ dict[next++] = [...previous, entry[0]];
209
+ }
210
+ previous = entry;
211
+
212
+ for (const value of entry) {
213
+ if (written < count) pixels[written++] = value;
214
+ }
215
+ if (next === (1 << codeSize) && codeSize < 12) codeSize++;
216
+ }
217
+
218
+ return pixels;
219
+ };
220
+
221
+ /**
222
+ * Row order of an interlaced GIF: 4/8, then 2/8, then 1/4, then 1/2 passes.
223
+ *
224
+ * @param {number} height
225
+ * @returns {number[]} Destination row for each stored row, in order.
226
+ */
227
+ const deinterlace = (height) => {
228
+ const rows = [];
229
+ for (let pass = 0; pass < 4; pass++) {
230
+ const start = [0, 4, 2, 1][pass];
231
+ const step = [8, 8, 4, 2][pass];
232
+ for (let y = start; y < height; y += step) rows.push(y);
233
+ }
234
+ return rows;
235
+ };
236
+
237
+ /**
238
+ * @param {Buffer} buf
239
+ * @param {number} offset - Where the LZW minimum code size byte sits.
240
+ * @returns {number} Offset after this image's sub-blocks.
241
+ */
242
+ const skipImage = (buf, offset) => readBlocks(buf, offset + 1).next;
243
+
244
+ export default decodeGif;