flet-box 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/bin/cli.js +90 -11
- package/bin/commands/auditFix.js +682 -0
- package/bin/commands/checkApi.js +20 -6
- package/bin/commands/checkUi.js +1369 -0
- package/bin/commands/create.js +86 -12
- package/bin/commands/createBundle.js +75 -1
- package/bin/commands/images.js +349 -0
- package/bin/commands/pruneAssets.js +170 -0
- package/bin/commands/release.js +34 -1
- package/bin/commands/runCloudflare.js +47 -9
- package/bin/commands/runServer.js +91 -25
- package/bin/commands/toPng.js +346 -0
- package/bin/commands/unpack.js +1173 -0
- package/bin/ota/loader.js +90 -0
- package/bin/utils/appSources.js +101 -0
- package/bin/utils/bundleConfig.js +166 -0
- package/bin/utils/dtsProps.js +147 -0
- package/bin/utils/imageTools.js +271 -0
- package/bin/utils/images/bmp.js +99 -0
- package/bin/utils/images/gif.js +244 -0
- package/bin/utils/images/index.js +163 -0
- package/bin/utils/images/jpeg.js +643 -0
- package/bin/utils/images/png.js +486 -0
- package/bin/utils/images/unsupported.js +29 -0
- package/bin/utils/neutralise.js +109 -6
- package/bin/utils/pwaShell.js +137 -0
- package/bin/utils/templates.js +1653 -471
- package/docs/README.md +2 -0
- package/docs/cli/README.md +144 -1
- package/docs/guides/README.md +2 -0
- package/docs/guides/clean-ui.md +211 -0
- package/docs/guides/minification-and-protection.md +1 -1
- package/docs/guides/online-updates.md +105 -9
- package/docs/tools/README.md +1 -1
- package/docs/tools/animation.md +1 -1
- package/docs/tools/markdown.md +3 -3
- package/docs/tools/theme.md +41 -1
- package/docs/widget/Accordion.md +4 -0
- package/docs/widget/AdMob.md +5 -1
- package/docs/widget/AdSense.md +4 -0
- package/docs/widget/AnimatedBox.md +7 -3
- package/docs/widget/AnimatedText.md +4 -0
- package/docs/widget/Audio.md +5 -1
- package/docs/widget/Avatar.md +15 -3
- package/docs/widget/Badge.md +4 -0
- package/docs/widget/Button.md +4 -0
- package/docs/widget/COMMON_PROPS.md +52 -0
- package/docs/widget/Card.md +4 -0
- package/docs/widget/Carousel.md +4 -0
- package/docs/widget/Chart.md +4 -0
- package/docs/widget/Checkbox.md +4 -0
- package/docs/widget/Chip.md +4 -0
- package/docs/widget/CircularBar.md +4 -0
- package/docs/widget/CircularChart.md +4 -0
- package/docs/widget/CodeViewer.md +4 -0
- package/docs/widget/Column.md +4 -0
- package/docs/widget/Container.md +4 -0
- package/docs/widget/DataTable.md +4 -0
- package/docs/widget/Divider.md +4 -0
- package/docs/widget/DraggBox.md +6 -2
- package/docs/widget/Dropdown.md +6 -0
- package/docs/widget/DroppBox.md +5 -1
- package/docs/widget/FloatingActionButton.md +4 -0
- package/docs/widget/GridView.md +4 -0
- package/docs/widget/Icon.md +4 -0
- package/docs/widget/Image.md +14 -3
- package/docs/widget/Input.md +4 -0
- package/docs/widget/Inspector.md +2 -2
- package/docs/widget/InstallButton.md +4 -0
- package/docs/widget/ListTile.md +4 -0
- package/docs/widget/ListView.md +4 -0
- package/docs/widget/Maps.md +191 -0
- package/docs/widget/Markdown.md +4 -0
- package/docs/widget/MatrixRain.md +4 -0
- package/docs/widget/Pagination.md +4 -0
- package/docs/widget/ParallaxBox.md +4 -0
- package/docs/widget/ProgressBar.md +4 -0
- package/docs/widget/QRCode.md +4 -0
- package/docs/widget/README.md +12 -10
- package/docs/widget/Radio.md +5 -1
- package/docs/widget/Rating.md +4 -0
- package/docs/widget/Row.md +4 -0
- package/docs/widget/Skeleton.md +4 -0
- package/docs/widget/Slider.md +4 -0
- package/docs/widget/Stack.md +4 -0
- package/docs/widget/Stepper.md +4 -0
- package/docs/widget/Switch.md +4 -0
- package/docs/widget/Tabs.md +5 -5
- package/docs/widget/Text.md +4 -0
- package/docs/widget/Tooltip.md +14 -13
- package/docs/widget/TreeView.md +4 -0
- package/docs/widget/Video.md +6 -2
- package/package.json +2 -2
- package/src/animations/AnimatedBox.js +4 -1
- package/src/animations/MatrixRain.js +3 -3
- package/src/core/hmr-client.js +129 -34
- package/src/index.d.ts +195 -7
- package/src/index.js +2 -0
- package/src/navigations/AppBar.js +22 -16
- package/src/navigations/BottomNavigation.js +8 -13
- package/src/navigations/CollapsibleSideBar.js +30 -8
- package/src/navigations/Drawer.js +25 -15
- package/src/navigations/DrawerItem.js +3 -1
- package/src/navigations/Scaffold.js +26 -12
- package/src/navigations/Tabs.js +104 -59
- package/src/tools/color.js +54 -0
- package/src/tools/index.js +9 -1
- package/src/tools/scale.js +41 -0
- package/src/tools/shadow.js +20 -0
- package/src/utils/index.js +1 -0
- package/src/utils/readableInk.js +19 -0
- package/src/utils/syntaxHighlight.js +78 -10
- package/src/utils/themes.js +217 -54
- package/src/utils/units.js +33 -11
- package/src/widget-factory/effects.js +56 -5
- package/src/widget-factory/processProps.js +10 -1
- package/src/widget-factory/remTool.js +15 -29
- package/src/widget-factory/stackPosition.js +18 -3
- package/src/widget-factory/translateProps.js +60 -1
- package/src/widgets/Accordion.js +20 -23
- package/src/widgets/AdMob.js +7 -2
- package/src/widgets/Avatar.js +236 -133
- package/src/widgets/Badge.js +3 -2
- package/src/widgets/BottomSheet.js +14 -13
- package/src/widgets/Button.js +69 -30
- package/src/widgets/Card.js +6 -2
- package/src/widgets/Carousel.js +12 -10
- package/src/widgets/Chart.js +28 -5
- package/src/widgets/Checkbox.js +76 -18
- package/src/widgets/Chip.js +31 -10
- package/src/widgets/CircularBar.js +7 -7
- package/src/widgets/CircularChart.js +26 -8
- package/src/widgets/CodeViewer.js +93 -20
- package/src/widgets/Container.js +13 -0
- package/src/widgets/DataTable.js +17 -11
- package/src/widgets/DraggBox.js +2 -1
- package/src/widgets/Dropdown.js +59 -14
- package/src/widgets/DroppBox.js +4 -2
- package/src/widgets/FloatingActionButton.js +13 -7
- package/src/widgets/Icon.js +3 -1
- package/src/widgets/Image.js +102 -3
- package/src/widgets/Input.js +30 -23
- package/src/widgets/Inspector.js +54 -1
- package/src/widgets/InstallButton.js +6 -4
- package/src/widgets/ListTile.js +4 -3
- package/src/widgets/ListView.js +30 -12
- package/src/widgets/Maps.js +1495 -0
- package/src/widgets/Markdown.js +37 -26
- package/src/widgets/Modal.js +14 -11
- package/src/widgets/Pagination.js +16 -11
- package/src/widgets/ProgressBar.js +6 -4
- package/src/widgets/Radio.js +73 -15
- package/src/widgets/Rating.js +24 -6
- package/src/widgets/Skeleton.js +31 -31
- package/src/widgets/Slider.js +29 -18
- package/src/widgets/SnackBar.js +35 -39
- package/src/widgets/Stepper.js +13 -9
- package/src/widgets/Switch.js +59 -18
- package/src/widgets/Tooltip.js +24 -12
- package/src/widgets/TreeView.js +27 -15
- package/src/widgets/index.js +1 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file bin/utils/images/png.js
|
|
3
|
+
* @description PNG reader and writer built on `node:zlib` only.
|
|
4
|
+
*
|
|
5
|
+
* PNG is the target format of `flet-box to-png`, so the writer is the important
|
|
6
|
+
* half; the reader exists because a PNG already in `assets/` still has to be
|
|
7
|
+
* decoded to be re-encoded (and because every other decoder is checked against a
|
|
8
|
+
* PNG in the test script).
|
|
9
|
+
*
|
|
10
|
+
* The writer picks the smallest output it can find for the pixels it is handed,
|
|
11
|
+
* and it is lossless by construction: the layout is chosen from what the frame
|
|
12
|
+
* actually contains (a palette only when there really are 256 colours or fewer,
|
|
13
|
+
* an alpha channel only when some pixel uses it), and the row filter is chosen by
|
|
14
|
+
* deflating the candidates and keeping the shortest. Nothing here approximates a
|
|
15
|
+
* pixel, so reading the file back returns the same RGBA bytes that went in.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import zlib from "node:zlib";
|
|
19
|
+
import UnsupportedImage from "./unsupported.js";
|
|
20
|
+
|
|
21
|
+
const SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
22
|
+
|
|
23
|
+
/** CRC-32 (IEEE 802.3) table, as required by the PNG chunk format. */
|
|
24
|
+
const CRC_TABLE = (() => {
|
|
25
|
+
const table = new Int32Array(256);
|
|
26
|
+
for (let n = 0; n < 256; n++) {
|
|
27
|
+
let c = n;
|
|
28
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
29
|
+
table[n] = c;
|
|
30
|
+
}
|
|
31
|
+
return table;
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {Buffer} buf
|
|
36
|
+
* @returns {number} Unsigned CRC-32 of `buf`.
|
|
37
|
+
*/
|
|
38
|
+
const crc32 = (buf) => {
|
|
39
|
+
let c = -1;
|
|
40
|
+
for (const byte of buf) c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8);
|
|
41
|
+
return (c ^ -1) >>> 0;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Assembles one PNG chunk (length, type, data, CRC over type+data).
|
|
46
|
+
*
|
|
47
|
+
* @param {string} type - Four-character chunk type.
|
|
48
|
+
* @param {Buffer} data
|
|
49
|
+
* @returns {Buffer}
|
|
50
|
+
*/
|
|
51
|
+
const chunk = (type, data) => {
|
|
52
|
+
const out = Buffer.alloc(12 + data.length);
|
|
53
|
+
out.writeUInt32BE(data.length, 0);
|
|
54
|
+
out.write(type, 4, "ascii");
|
|
55
|
+
data.copy(out, 8);
|
|
56
|
+
out.writeUInt32BE(crc32(out.subarray(4, 8 + data.length)), 8 + data.length);
|
|
57
|
+
return out;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Raw bytes above which the writer stops trying each row filter on its own.
|
|
62
|
+
*
|
|
63
|
+
* Every extra variant costs a full `deflate` pass, and a photographic frame is
|
|
64
|
+
* thirty megabytes of them. Past this size only the per-row heuristic is
|
|
65
|
+
* searched, which measured identical to the full six-way search on the
|
|
66
|
+
* photographic fixtures and within 0.4% on the flat ones.
|
|
67
|
+
*/
|
|
68
|
+
const FULL_SEARCH_LIMIT = 8 * 1024 * 1024;
|
|
69
|
+
|
|
70
|
+
/** The five filters of the specification, in the order the search tries them. */
|
|
71
|
+
const ROW_FILTERS = [0, 1, 2, 3, 4];
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Applies one row filter to one scanline of original pixels.
|
|
75
|
+
*
|
|
76
|
+
* Encoding is the easy direction: the predictors read the *unfiltered* rows, so
|
|
77
|
+
* a line only needs its neighbours, never the bytes already written.
|
|
78
|
+
*
|
|
79
|
+
* @param {number} mode - 0 None, 1 Sub, 2 Up, 3 Average, 4 Paeth.
|
|
80
|
+
* @param {Buffer} line - Original scanline.
|
|
81
|
+
* @param {Buffer} previous - Original row above (zeros for the first row).
|
|
82
|
+
* @param {number} channels - Bytes per pixel, for the left-hand reference.
|
|
83
|
+
* @returns {Buffer} The filtered line, ready for its filter byte.
|
|
84
|
+
*/
|
|
85
|
+
const filteredRow = (mode, line, previous, channels) => {
|
|
86
|
+
const out = Buffer.alloc(line.length);
|
|
87
|
+
for (let i = 0; i < line.length; i++) {
|
|
88
|
+
const a = i >= channels ? line[i - channels] : 0;
|
|
89
|
+
const b = previous[i];
|
|
90
|
+
const c = i >= channels ? previous[i - channels] : 0;
|
|
91
|
+
let predict = 0;
|
|
92
|
+
if (mode === 1) predict = a;
|
|
93
|
+
else if (mode === 2) predict = b;
|
|
94
|
+
else if (mode === 3) predict = (a + b) >> 1;
|
|
95
|
+
else if (mode === 4) predict = paeth(a, b, c);
|
|
96
|
+
out[i] = (line[i] - predict) & 0xff;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Sum of the residuals read as signed bytes: the classic per-row heuristic.
|
|
103
|
+
*
|
|
104
|
+
* @param {Buffer} row - One filtered scanline.
|
|
105
|
+
* @returns {number}
|
|
106
|
+
*/
|
|
107
|
+
const residual = (row) => {
|
|
108
|
+
let sum = 0;
|
|
109
|
+
for (const byte of row) sum += byte < 128 ? byte : 256 - byte;
|
|
110
|
+
return sum;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Builds the deflatable stream of a whole frame.
|
|
115
|
+
*
|
|
116
|
+
* @param {Buffer} data - Pixel bytes, row after row, `channels` per pixel.
|
|
117
|
+
* @param {number} width
|
|
118
|
+
* @param {number} height
|
|
119
|
+
* @param {number} channels
|
|
120
|
+
* @param {number[]} modes - One filter for every row, or all five to choose per row.
|
|
121
|
+
* @returns {Buffer} Filter byte plus filtered bytes, repeated per row.
|
|
122
|
+
*/
|
|
123
|
+
const filterFrame = (data, width, height, channels, modes) => {
|
|
124
|
+
const stride = width * channels;
|
|
125
|
+
const parts = [];
|
|
126
|
+
for (let y = 0; y < height; y++) {
|
|
127
|
+
const line = data.subarray(y * stride, y * stride + stride);
|
|
128
|
+
const previous = y > 0 ? data.subarray((y - 1) * stride, (y - 1) * stride + stride) : Buffer.alloc(stride);
|
|
129
|
+
let best = null;
|
|
130
|
+
for (const mode of modes) {
|
|
131
|
+
const row = filteredRow(mode, line, previous, channels);
|
|
132
|
+
const score = residual(row);
|
|
133
|
+
if (!best || score < best.score) best = { mode, row, score };
|
|
134
|
+
}
|
|
135
|
+
parts.push(Buffer.from([best.mode]), best.row);
|
|
136
|
+
}
|
|
137
|
+
return Buffer.concat(parts);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The pixel layouts worth writing for one RGBA frame.
|
|
142
|
+
*
|
|
143
|
+
* Each is the same picture in fewer bytes: a palette when the frame really holds
|
|
144
|
+
* 256 distinct colours or fewer (an address is not a ramp, so this is exact, not
|
|
145
|
+
* a quantisation), grey when every channel already agrees, no alpha when every
|
|
146
|
+
* pixel is opaque. Three bytes per pixel is always offered for an opaque frame
|
|
147
|
+
* and four for one with alpha, so the list has a floor and the answer is never
|
|
148
|
+
* worse than the plain layout.
|
|
149
|
+
*
|
|
150
|
+
* @param {{ width: number, height: number, rgba: Buffer }} image
|
|
151
|
+
* @returns {{ colourType: number, channels: number, data: Buffer, palette: Buffer|null, transparency: Buffer|null }[]}
|
|
152
|
+
*/
|
|
153
|
+
const layoutsOf = ({ width, height, rgba }) => {
|
|
154
|
+
const count = width * height;
|
|
155
|
+
let opaque = true;
|
|
156
|
+
let grey = true;
|
|
157
|
+
const colours = new Map();
|
|
158
|
+
|
|
159
|
+
for (let i = 0; i < count; i++) {
|
|
160
|
+
const o = i * 4;
|
|
161
|
+
const r = rgba[o];
|
|
162
|
+
const g = rgba[o + 1];
|
|
163
|
+
const b = rgba[o + 2];
|
|
164
|
+
const a = rgba[o + 3];
|
|
165
|
+
if (a !== 255) opaque = false;
|
|
166
|
+
if (r !== g || g !== b) grey = false;
|
|
167
|
+
// 257 keys is the answer "too many": keep the map bounded instead of
|
|
168
|
+
// memorising every colour of a photograph to prove it has too many.
|
|
169
|
+
if (colours.size <= 256) colours.set(((r << 24) | (g << 16) | (b << 8) | a) >>> 0, true);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const keyOf = (i) => ((rgba[i * 4] << 24) | (rgba[i * 4 + 1] << 16) | (rgba[i * 4 + 2] << 8) | rgba[i * 4 + 3]) >>> 0;
|
|
173
|
+
const layouts = [];
|
|
174
|
+
|
|
175
|
+
if (colours.size <= 256) {
|
|
176
|
+
const keys = [...colours.keys()];
|
|
177
|
+
const at = new Map(keys.map((key, i) => [key, i]));
|
|
178
|
+
const data = Buffer.alloc(count);
|
|
179
|
+
for (let i = 0; i < count; i++) data[i] = at.get(keyOf(i));
|
|
180
|
+
const palette = Buffer.alloc(keys.length * 3);
|
|
181
|
+
const alphas = [];
|
|
182
|
+
keys.forEach((key, i) => {
|
|
183
|
+
palette[i * 3] = (key >>> 24) & 255;
|
|
184
|
+
palette[i * 3 + 1] = (key >>> 16) & 255;
|
|
185
|
+
palette[i * 3 + 2] = (key >>> 8) & 255;
|
|
186
|
+
alphas[i] = key & 255;
|
|
187
|
+
});
|
|
188
|
+
// A palette tRNS is one alpha per entry *in order*, and entries past the end
|
|
189
|
+
// are opaque, so the chunk can stop at the last entry that is not 255.
|
|
190
|
+
let used = alphas.length;
|
|
191
|
+
while (used > 0 && alphas[used - 1] === 255) used--;
|
|
192
|
+
layouts.push({
|
|
193
|
+
colourType: 3,
|
|
194
|
+
channels: 1,
|
|
195
|
+
data,
|
|
196
|
+
palette,
|
|
197
|
+
transparency: used > 0 ? Buffer.from(alphas.slice(0, used)) : null,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (grey) {
|
|
202
|
+
if (opaque) {
|
|
203
|
+
const data = Buffer.alloc(count);
|
|
204
|
+
for (let i = 0; i < count; i++) data[i] = rgba[i * 4];
|
|
205
|
+
layouts.push({ colourType: 0, channels: 1, data, palette: null, transparency: null });
|
|
206
|
+
} else {
|
|
207
|
+
const data = Buffer.alloc(count * 2);
|
|
208
|
+
for (let i = 0; i < count; i++) {
|
|
209
|
+
data[i * 2] = rgba[i * 4];
|
|
210
|
+
data[i * 2 + 1] = rgba[i * 4 + 3];
|
|
211
|
+
}
|
|
212
|
+
layouts.push({ colourType: 4, channels: 2, data, palette: null, transparency: null });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (opaque) {
|
|
217
|
+
const data = Buffer.alloc(count * 3);
|
|
218
|
+
for (let i = 0, o = 0; o < data.length; i += 4, o += 3) {
|
|
219
|
+
data[o] = rgba[i];
|
|
220
|
+
data[o + 1] = rgba[i + 1];
|
|
221
|
+
data[o + 2] = rgba[i + 2];
|
|
222
|
+
}
|
|
223
|
+
layouts.push({ colourType: 2, channels: 3, data, palette: null, transparency: null });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Truecolour with alpha is only offered when the frame has alpha to say. On an
|
|
227
|
+
// opaque picture that fourth byte is the same 255 in every pixel: filtered it
|
|
228
|
+
// is a run of zeros, so the layout can match the three-byte one but never beat
|
|
229
|
+
// it — and searching it would cost a full deflate pass over thirty megabytes on
|
|
230
|
+
// a photograph, which is the one file size this writer is asked to shrink.
|
|
231
|
+
if (!opaque) {
|
|
232
|
+
layouts.push({ colourType: 6, channels: 4, data: rgba, palette: null, transparency: null });
|
|
233
|
+
}
|
|
234
|
+
return layouts;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Encodes one layout, searching the row filters by actually compressing them.
|
|
239
|
+
*
|
|
240
|
+
* @param {number} width
|
|
241
|
+
* @param {number} height
|
|
242
|
+
* @param {{ colourType: number, channels: number, data: Buffer, palette: Buffer|null, transparency: Buffer|null }} layout
|
|
243
|
+
* @returns {Buffer} Complete PNG file bytes.
|
|
244
|
+
*/
|
|
245
|
+
const encodeLayout = (width, height, layout) => {
|
|
246
|
+
const { colourType, channels, data, palette, transparency } = layout;
|
|
247
|
+
const wholeImage = channels * width <= FULL_SEARCH_LIMIT / height ? ROW_FILTERS : [0];
|
|
248
|
+
const searches = [...wholeImage.map((mode) => [mode]), ROW_FILTERS];
|
|
249
|
+
|
|
250
|
+
let shortest = null;
|
|
251
|
+
for (const modes of searches) {
|
|
252
|
+
const idat = zlib.deflateSync(filterFrame(data, width, height, channels, modes), { level: 9 });
|
|
253
|
+
if (!shortest || idat.length < shortest.length) shortest = idat;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const ihdr = Buffer.alloc(13);
|
|
257
|
+
ihdr.writeUInt32BE(width, 0);
|
|
258
|
+
ihdr.writeUInt32BE(height, 4);
|
|
259
|
+
ihdr[8] = 8; // bit depth
|
|
260
|
+
ihdr[9] = colourType;
|
|
261
|
+
ihdr[10] = 0; // deflate
|
|
262
|
+
ihdr[11] = 0; // adaptive filtering
|
|
263
|
+
ihdr[12] = 0; // no interlace
|
|
264
|
+
|
|
265
|
+
// PLTE and tRNS both have to sit between IHDR and IDAT.
|
|
266
|
+
return Buffer.concat([
|
|
267
|
+
SIGNATURE,
|
|
268
|
+
chunk("IHDR", ihdr),
|
|
269
|
+
...(palette ? [chunk("PLTE", palette)] : []),
|
|
270
|
+
...(transparency ? [chunk("tRNS", transparency)] : []),
|
|
271
|
+
chunk("IDAT", shortest),
|
|
272
|
+
chunk("IEND", Buffer.alloc(0)),
|
|
273
|
+
]);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Encodes raw RGBA pixels as the smallest PNG that holds exactly those pixels.
|
|
278
|
+
*
|
|
279
|
+
* @param {{ width: number, height: number, rgba: Buffer }} image
|
|
280
|
+
* @returns {Buffer} PNG file bytes.
|
|
281
|
+
*/
|
|
282
|
+
export const encodePng = (image) => {
|
|
283
|
+
let best = null;
|
|
284
|
+
for (const layout of layoutsOf(image)) {
|
|
285
|
+
const bytes = encodeLayout(image.width, image.height, layout);
|
|
286
|
+
if (!best || bytes.length < best.length) best = bytes;
|
|
287
|
+
}
|
|
288
|
+
return best;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Reads the pixels of a PNG back into RGBA.
|
|
293
|
+
*
|
|
294
|
+
* Colour types 0/2/3/4/6 are expanded to RGBA; 16-bit sources are narrowed with
|
|
295
|
+
* the high byte, which is what an 8-bit container can carry without a rounding
|
|
296
|
+
* pass. Adam7 interlacing is refused rather than silently mis-assembled: the
|
|
297
|
+
* seven sub-image passes need a different row walk, and an interlaced asset is
|
|
298
|
+
* rare enough that a clear error beats a wrong picture.
|
|
299
|
+
*
|
|
300
|
+
* @param {Buffer} buf - PNG file bytes.
|
|
301
|
+
* @returns {{ width: number, height: number, rgba: Buffer }}
|
|
302
|
+
* @throws {Error} When the file is not a PNG it can read.
|
|
303
|
+
*/
|
|
304
|
+
export const decodePng = (buf) => {
|
|
305
|
+
if (buf.length < 8 || buf.compare(SIGNATURE, 0, 8, 0, 8) !== 0) {
|
|
306
|
+
throw new Error("not a PNG: the signature is missing");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
let offset = 8;
|
|
310
|
+
let width = 0;
|
|
311
|
+
let height = 0;
|
|
312
|
+
let bitDepth = 0;
|
|
313
|
+
let colorType = 0;
|
|
314
|
+
let interlace = 0;
|
|
315
|
+
const idat = [];
|
|
316
|
+
let palette = null;
|
|
317
|
+
let transparency = null;
|
|
318
|
+
|
|
319
|
+
while (offset + 8 <= buf.length) {
|
|
320
|
+
const length = buf.readUInt32BE(offset);
|
|
321
|
+
const type = buf.toString("ascii", offset + 4, offset + 8);
|
|
322
|
+
const data = buf.subarray(offset + 8, offset + 8 + length);
|
|
323
|
+
offset += 12 + length;
|
|
324
|
+
|
|
325
|
+
if (type === "IHDR") {
|
|
326
|
+
width = data.readUInt32BE(0);
|
|
327
|
+
height = data.readUInt32BE(4);
|
|
328
|
+
bitDepth = data[8];
|
|
329
|
+
colorType = data[9];
|
|
330
|
+
if (data[10] !== 0) throw new Error("PNG uses a compression method other than deflate");
|
|
331
|
+
interlace = data[12];
|
|
332
|
+
} else if (type === "PLTE") {
|
|
333
|
+
palette = data;
|
|
334
|
+
} else if (type === "tRNS") {
|
|
335
|
+
transparency = data;
|
|
336
|
+
} else if (type === "IDAT") {
|
|
337
|
+
idat.push(Buffer.from(data));
|
|
338
|
+
} else if (type === "IEND") {
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!width || !height) throw new Error("PNG has no IHDR");
|
|
344
|
+
if (interlace !== 0) throw new UnsupportedImage("PNG", "Adam7 interlacing is not implemented");
|
|
345
|
+
if (![1, 2, 4, 8, 16].includes(bitDepth)) throw new UnsupportedImage("PNG", `${bitDepth}-bit samples are not implemented`);
|
|
346
|
+
|
|
347
|
+
const channels = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[colorType];
|
|
348
|
+
if (!channels) throw new Error(`unknown PNG colour type ${colorType}`);
|
|
349
|
+
if (colorType === 3 && !palette) throw new Error("palette PNG has no PLTE chunk");
|
|
350
|
+
|
|
351
|
+
const bitsPerPixel = channels * bitDepth;
|
|
352
|
+
const stride = Math.ceil((width * bitsPerPixel) / 8);
|
|
353
|
+
const raw = zlib.inflateSync(Buffer.concat(idat));
|
|
354
|
+
if (raw.length !== stride * height + height) {
|
|
355
|
+
throw new Error("PNG pixel data has an unexpected length");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const bytesPerPixel = Math.max(1, Math.ceil(bitsPerPixel / 8));
|
|
359
|
+
const out = Buffer.alloc(width * height * 4);
|
|
360
|
+
const line = Buffer.alloc(stride);
|
|
361
|
+
const previous = Buffer.alloc(stride);
|
|
362
|
+
|
|
363
|
+
for (let y = 0; y < height; y++) {
|
|
364
|
+
const rowStart = y * (stride + 1);
|
|
365
|
+
const filter = raw[rowStart];
|
|
366
|
+
raw.copy(line, 0, rowStart + 1, rowStart + 1 + stride);
|
|
367
|
+
unfilter(filter, line, previous, bytesPerPixel);
|
|
368
|
+
|
|
369
|
+
for (let x = 0; x < width; x++) {
|
|
370
|
+
const rgba = sampleOf(line, x, channels, bitDepth, palette, transparency, colorType);
|
|
371
|
+
const dst = (y * width + x) * 4;
|
|
372
|
+
out[dst] = rgba[0];
|
|
373
|
+
out[dst + 1] = rgba[1];
|
|
374
|
+
out[dst + 2] = rgba[2];
|
|
375
|
+
out[dst + 3] = rgba[3];
|
|
376
|
+
}
|
|
377
|
+
line.copy(previous);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return { width, height, rgba: out };
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Reverses the per-row filter of one scanline, in place.
|
|
385
|
+
*
|
|
386
|
+
* @param {number} filter - 0 None, 1 Sub, 2 Up, 3 Average, 4 Paeth.
|
|
387
|
+
* @param {Buffer} line - Raw scanline, modified in place.
|
|
388
|
+
* @param {Buffer} previous - Reconstructed previous scanline (zeros on row 0).
|
|
389
|
+
* @param {number} bpp - Bytes per pixel used by the left-neighbour reference.
|
|
390
|
+
* @returns {void}
|
|
391
|
+
*/
|
|
392
|
+
const unfilter = (filter, line, previous, bpp) => {
|
|
393
|
+
if (filter === 0) return;
|
|
394
|
+
for (let i = 0; i < line.length; i++) {
|
|
395
|
+
const a = i >= bpp ? line[i - bpp] : 0;
|
|
396
|
+
const b = previous[i];
|
|
397
|
+
const c = i >= bpp ? previous[i - bpp] : 0;
|
|
398
|
+
let predict;
|
|
399
|
+
if (filter === 1) predict = a;
|
|
400
|
+
else if (filter === 2) predict = b;
|
|
401
|
+
else if (filter === 3) predict = (a + b) >> 1;
|
|
402
|
+
else if (filter === 4) predict = paeth(a, b, c);
|
|
403
|
+
else throw new Error(`unknown PNG row filter ${filter}`);
|
|
404
|
+
line[i] = (line[i] + predict) & 0xff;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* PNG's Paeth predictor: the neighbour whose distance to `a + b - c` is least.
|
|
410
|
+
*
|
|
411
|
+
* @param {number} a - Left pixel byte.
|
|
412
|
+
* @param {number} b - Pixel above.
|
|
413
|
+
* @param {number} c - Pixel above-left.
|
|
414
|
+
* @returns {number}
|
|
415
|
+
*/
|
|
416
|
+
const paeth = (a, b, c) => {
|
|
417
|
+
const p = a + b - c;
|
|
418
|
+
const pa = Math.abs(p - a);
|
|
419
|
+
const pb = Math.abs(p - b);
|
|
420
|
+
const pc = Math.abs(p - c);
|
|
421
|
+
if (pa <= pb && pa <= pc) return a;
|
|
422
|
+
return pb <= pc ? b : c;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Expands one pixel of any supported colour type to RGBA.
|
|
427
|
+
*
|
|
428
|
+
* @param {Buffer} line - Reconstructed scanline.
|
|
429
|
+
* @param {number} x - Pixel index.
|
|
430
|
+
* @param {number} channels
|
|
431
|
+
* @param {number} bitDepth
|
|
432
|
+
* @param {Buffer|null} palette
|
|
433
|
+
* @param {Buffer|null} transparency
|
|
434
|
+
* @param {number} colorType
|
|
435
|
+
* @returns {number[]} `[r, g, b, a]`, each 0-255.
|
|
436
|
+
*/
|
|
437
|
+
const sampleOf = (line, x, channels, bitDepth, palette, transparency, colorType) => {
|
|
438
|
+
// The x-th sample of the row, unpacked from whatever sub-byte packing it uses.
|
|
439
|
+
const unpack = (channel) => {
|
|
440
|
+
const position = x * channels + channel;
|
|
441
|
+
if (bitDepth === 16) return line.readUInt16BE(position * 2) >> 8;
|
|
442
|
+
if (bitDepth === 8) return line[position];
|
|
443
|
+
const perByte = Math.floor(8 / bitDepth);
|
|
444
|
+
const byte = line[Math.floor(position / perByte)];
|
|
445
|
+
const shift = 8 - bitDepth * ((position % perByte) + 1);
|
|
446
|
+
return (byte >> shift) & ((1 << bitDepth) - 1);
|
|
447
|
+
};
|
|
448
|
+
// A grey or truecolour ramp stored below 8 bits is a truncated range, so it is
|
|
449
|
+
// scaled up to fill 0-255. A palette index is an address, never a ramp: 3 out
|
|
450
|
+
// of 4 bits means entry 3, not entry 255.
|
|
451
|
+
const at = (channel) =>
|
|
452
|
+
bitDepth >= 8 ? unpack(channel) : Math.round((unpack(channel) * 255) / ((1 << bitDepth) - 1));
|
|
453
|
+
|
|
454
|
+
if (colorType === 3) {
|
|
455
|
+
const index = unpack(0);
|
|
456
|
+
return [
|
|
457
|
+
palette[index * 3] ?? 0,
|
|
458
|
+
palette[index * 3 + 1] ?? 0,
|
|
459
|
+
palette[index * 3 + 2] ?? 0,
|
|
460
|
+
// For a palette the tRNS chunk is one alpha byte per entry, in order.
|
|
461
|
+
transparency?.[index] ?? 255,
|
|
462
|
+
];
|
|
463
|
+
}
|
|
464
|
+
// For colour types 0 and 2 the tRNS chunk always stores each sample as two
|
|
465
|
+
// big-endian bytes, whatever the bit depth.
|
|
466
|
+
if (colorType === 0) {
|
|
467
|
+
const grey = at(0);
|
|
468
|
+
return [grey, grey, grey, transparency ? (transparency.readUInt16BE(0) >> 8) === grey ? 0 : 255 : 255];
|
|
469
|
+
}
|
|
470
|
+
if (colorType === 4) return [at(0), at(0), at(0), at(1)];
|
|
471
|
+
if (colorType === 2) {
|
|
472
|
+
let alpha = 255;
|
|
473
|
+
if (transparency) {
|
|
474
|
+
const r = at(0), g = at(1), b = at(2);
|
|
475
|
+
if (r === (transparency.readUInt16BE(0) >> 8) &&
|
|
476
|
+
g === (transparency.readUInt16BE(2) >> 8) &&
|
|
477
|
+
b === (transparency.readUInt16BE(4) >> 8)) {
|
|
478
|
+
alpha = 0;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return [at(0), at(1), at(2), alpha];
|
|
482
|
+
}
|
|
483
|
+
return [at(0), at(1), at(2), at(3)];
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
export default encodePng;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file bin/utils/images/unsupported.js
|
|
3
|
+
* @description The error every decoder throws for a feature it will never read.
|
|
4
|
+
*
|
|
5
|
+
* It lives apart from `index.js` because the decoders are imported by it: sharing
|
|
6
|
+
* the class from the barrel would be a circular import, and a class declaration
|
|
7
|
+
* half-initialised during a cycle is how `instanceof` starts lying.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Thrown when a file is a format, or a mode of a format, this tool will never
|
|
12
|
+
* read. The command prints it as a declined asset rather than a failure: the
|
|
13
|
+
* file is fine, the feature is simply out of scope, and a folder full of WebP
|
|
14
|
+
* must not look like a broken build.
|
|
15
|
+
*/
|
|
16
|
+
export class UnsupportedImage extends Error {
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} format - Human-readable format name.
|
|
19
|
+
* @param {string} reason - Why it cannot be converted.
|
|
20
|
+
*/
|
|
21
|
+
constructor(format, reason) {
|
|
22
|
+
super(`${format}: ${reason}`);
|
|
23
|
+
this.name = "UnsupportedImage";
|
|
24
|
+
this.format = format;
|
|
25
|
+
this.reason = reason;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default UnsupportedImage;
|
package/bin/utils/neutralise.js
CHANGED
|
@@ -1,14 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file bin/utils/neutralise.js
|
|
3
|
-
* @description Comment/string stripper that preserves character offsets.
|
|
3
|
+
* @description Comment/string/regex stripper that preserves character offsets.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Characters after which a `/` starts a regex literal rather than a division.
|
|
8
|
+
*/
|
|
9
|
+
const REGEX_AFTER = new Set([
|
|
10
|
+
"(",
|
|
11
|
+
",",
|
|
12
|
+
"=",
|
|
13
|
+
":",
|
|
14
|
+
"[",
|
|
15
|
+
"!",
|
|
16
|
+
"&",
|
|
17
|
+
"|",
|
|
18
|
+
"?",
|
|
19
|
+
"{",
|
|
20
|
+
"}",
|
|
21
|
+
";",
|
|
22
|
+
"+",
|
|
23
|
+
"-",
|
|
24
|
+
"*",
|
|
25
|
+
"%",
|
|
26
|
+
"^",
|
|
27
|
+
"~",
|
|
28
|
+
"<",
|
|
29
|
+
">",
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const KEYWORD_BEFORE = /(?:^|[^\w$])(?:return|typeof|instanceof|in|of|new|case|do|void|delete|else)$/;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} emitted Text produced so far (same length as the source prefix).
|
|
36
|
+
* @returns {boolean} `true` when a `/` here opens a regex literal.
|
|
37
|
+
*/
|
|
38
|
+
const regexPosition = (emitted) => {
|
|
39
|
+
const text = emitted.replace(/\s+$/, "");
|
|
40
|
+
if (!text) return true;
|
|
41
|
+
return REGEX_AFTER.has(text[text.length - 1]) || KEYWORD_BEFORE.test(text);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Strip comments, string/regex bodies and template placeholders, keeping
|
|
46
|
+
* character offsets.
|
|
8
47
|
*
|
|
9
48
|
* Two passes are needed: imports must read their module specifier, while the
|
|
10
|
-
* call-site scanner must not see braces inside string literals.
|
|
11
|
-
*
|
|
49
|
+
* call-site scanner must not see braces inside string literals. Skipped content
|
|
50
|
+
* is replaced by an equal-length filler so line numbers stay correct.
|
|
51
|
+
*
|
|
52
|
+
* Three constructs desynchronise a naive quote scanner, and each one has shown
|
|
53
|
+
* up in this repo's own source:
|
|
54
|
+
* - a regex containing a quote (`value.replace(/'/g, "\\'")`), which looks
|
|
55
|
+
* exactly like a string start to a scanner that only tracks quotes;
|
|
56
|
+
* - a template placeholder (`${ … }`), which is code, not string text, and
|
|
57
|
+
* can itself hold quotes or another backtick.
|
|
58
|
+
* Both are scanned here instead of being treated as characters.
|
|
12
59
|
*
|
|
13
60
|
* @param {string} code
|
|
14
61
|
* @param {{ keepStrings?: boolean }} [options]
|
|
@@ -17,6 +64,9 @@
|
|
|
17
64
|
export const neutralise = (code, { keepStrings = false } = {}) => {
|
|
18
65
|
let out = "";
|
|
19
66
|
let i = 0;
|
|
67
|
+
const copy = (value) => {
|
|
68
|
+
out += keepStrings ? value : value === "\n" ? "\n" : " ";
|
|
69
|
+
};
|
|
20
70
|
while (i < code.length) {
|
|
21
71
|
const ch = code[i];
|
|
22
72
|
const next = code[i + 1];
|
|
@@ -36,6 +86,40 @@ export const neutralise = (code, { keepStrings = false } = {}) => {
|
|
|
36
86
|
i += 2;
|
|
37
87
|
continue;
|
|
38
88
|
}
|
|
89
|
+
if (ch === "/" && regexPosition(out)) {
|
|
90
|
+
// Regex literal: ends at the unescaped `/` outside its character class.
|
|
91
|
+
let j = i + 1;
|
|
92
|
+
let inClass = false;
|
|
93
|
+
let closed = false;
|
|
94
|
+
while (j < code.length) {
|
|
95
|
+
const c = code[j];
|
|
96
|
+
if (c === "\n") break;
|
|
97
|
+
if (c === "\\") {
|
|
98
|
+
j += 2;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (c === "[") inClass = true;
|
|
102
|
+
else if (c === "]") inClass = false;
|
|
103
|
+
else if (c === "/" && !inClass) {
|
|
104
|
+
closed = true;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
j++;
|
|
108
|
+
}
|
|
109
|
+
if (closed) {
|
|
110
|
+
j++; // flags (g, i, m, u, s, y) live right after the closing slash
|
|
111
|
+
while (j < code.length && /[a-z]/i.test(code[j])) j++;
|
|
112
|
+
for (let k = i; k < j; k++) {
|
|
113
|
+
out += keepStrings ? code[k] : code[k] === "\n" ? "\n" : " ";
|
|
114
|
+
}
|
|
115
|
+
i = j;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// Unterminated on this line: it was division after all.
|
|
119
|
+
out += ch;
|
|
120
|
+
i++;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
39
123
|
if (ch === '"' || ch === "'" || ch === "`") {
|
|
40
124
|
const quote = ch;
|
|
41
125
|
out += ch;
|
|
@@ -51,8 +135,25 @@ export const neutralise = (code, { keepStrings = false } = {}) => {
|
|
|
51
135
|
i++;
|
|
52
136
|
break;
|
|
53
137
|
}
|
|
54
|
-
|
|
55
|
-
|
|
138
|
+
if (quote === "`" && code[i] === "$" && code[i + 1] === "{") {
|
|
139
|
+
out += keepStrings ? "${" : " ";
|
|
140
|
+
i += 2;
|
|
141
|
+
let depth = 1;
|
|
142
|
+
while (i < code.length && depth > 0) {
|
|
143
|
+
const inner = code[i];
|
|
144
|
+
if (inner === "{") depth++;
|
|
145
|
+
else if (inner === "}") depth--;
|
|
146
|
+
if (depth === 0) {
|
|
147
|
+
out += keepStrings ? inner : " ";
|
|
148
|
+
i++;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
copy(inner);
|
|
152
|
+
i++;
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
copy(code[i]);
|
|
56
157
|
i++;
|
|
57
158
|
}
|
|
58
159
|
continue;
|
|
@@ -62,3 +163,5 @@ export const neutralise = (code, { keepStrings = false } = {}) => {
|
|
|
62
163
|
}
|
|
63
164
|
return out;
|
|
64
165
|
};
|
|
166
|
+
|
|
167
|
+
export default neutralise;
|