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.
- package/README.md +28 -1
- package/bin/cli.js +77 -11
- package/bin/commands/auditFix.js +682 -0
- package/bin/commands/checkApi.js +7 -5
- package/bin/commands/create.js +40 -10
- package/bin/commands/createBundle.js +54 -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 +81 -24
- 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/templates.js +307 -64
- package/docs/cli/README.md +82 -0
- package/docs/guides/online-updates.md +57 -9
- package/docs/widget/Accordion.md +1 -0
- package/docs/widget/AdMob.md +1 -0
- package/docs/widget/AdSense.md +1 -0
- package/docs/widget/AnimatedBox.md +1 -0
- package/docs/widget/AnimatedText.md +1 -0
- package/docs/widget/Audio.md +2 -1
- package/docs/widget/Avatar.md +12 -3
- package/docs/widget/Badge.md +1 -0
- package/docs/widget/Button.md +1 -0
- package/docs/widget/Card.md +1 -0
- package/docs/widget/Carousel.md +1 -0
- package/docs/widget/Chart.md +1 -0
- package/docs/widget/Checkbox.md +1 -0
- package/docs/widget/Chip.md +1 -0
- package/docs/widget/CircularBar.md +1 -0
- package/docs/widget/CircularChart.md +1 -0
- package/docs/widget/CodeViewer.md +1 -0
- package/docs/widget/Column.md +1 -0
- package/docs/widget/Container.md +1 -0
- package/docs/widget/DataTable.md +1 -0
- package/docs/widget/Divider.md +1 -0
- package/docs/widget/DraggBox.md +3 -2
- package/docs/widget/Dropdown.md +3 -0
- package/docs/widget/DroppBox.md +2 -1
- package/docs/widget/FloatingActionButton.md +1 -0
- package/docs/widget/GridView.md +1 -0
- package/docs/widget/Icon.md +1 -0
- package/docs/widget/Image.md +11 -3
- package/docs/widget/Input.md +1 -0
- package/docs/widget/InstallButton.md +1 -0
- package/docs/widget/ListTile.md +1 -0
- package/docs/widget/ListView.md +1 -0
- package/docs/widget/Maps.md +188 -0
- package/docs/widget/Markdown.md +1 -0
- package/docs/widget/MatrixRain.md +1 -0
- package/docs/widget/Pagination.md +1 -0
- package/docs/widget/ParallaxBox.md +1 -0
- package/docs/widget/ProgressBar.md +1 -0
- package/docs/widget/QRCode.md +1 -0
- package/docs/widget/README.md +12 -10
- package/docs/widget/Radio.md +1 -0
- package/docs/widget/Rating.md +1 -0
- package/docs/widget/Row.md +1 -0
- package/docs/widget/Skeleton.md +1 -0
- package/docs/widget/Slider.md +1 -0
- package/docs/widget/Stack.md +1 -0
- package/docs/widget/Stepper.md +1 -0
- package/docs/widget/Switch.md +1 -0
- package/docs/widget/Text.md +1 -0
- package/docs/widget/Tooltip.md +11 -13
- package/docs/widget/TreeView.md +1 -0
- package/docs/widget/Video.md +3 -2
- package/package.json +2 -2
- package/src/core/hmr-client.js +129 -34
- package/src/index.d.ts +132 -0
- package/src/index.js +1 -0
- package/src/navigations/AppBar.js +15 -4
- package/src/navigations/Drawer.js +9 -1
- package/src/widget-factory/translateProps.js +32 -1
- package/src/widgets/Avatar.js +228 -131
- package/src/widgets/Container.js +13 -0
- package/src/widgets/Dropdown.js +33 -1
- package/src/widgets/Image.js +102 -3
- package/src/widgets/Maps.js +1495 -0
- package/src/widgets/Tooltip.js +12 -2
- package/src/widgets/index.js +1 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file bin/utils/images/jpeg.js
|
|
3
|
+
* @description Baseline and extended sequential JPEG decoder, in plain JS.
|
|
4
|
+
*
|
|
5
|
+
* Covers what a camera or an export tool writes for a normal `.jpg`: 8-bit DCT
|
|
6
|
+
* with Huffman coding, one or three components, any sampling factor (4:4:4,
|
|
7
|
+
* 4:2:2, 4:2:0, 4:4:0 and the odd 4:1:1), restart intervals, and the non-interleaved
|
|
8
|
+
* form where each component gets its own scan.
|
|
9
|
+
*
|
|
10
|
+
* What it refuses — naming the feature it found — is progressive JPEG,
|
|
11
|
+
* arithmetic coding, lossless JPEG, 12-bit precision and CMYK. Those are
|
|
12
|
+
* different entropy coders and colour spaces, not edge cases of this one, and a
|
|
13
|
+
* converter that quietly produced a plausible-but-wrong picture would be worse
|
|
14
|
+
* than one that declines. Refusals are `UnsupportedImage`, so the caller reports
|
|
15
|
+
* them as declined assets rather than broken files: a folder of WhatsApp photos
|
|
16
|
+
* is not a failing build.
|
|
17
|
+
*
|
|
18
|
+
* The IDCT is the textbook separable float transform rather than an AAN-style
|
|
19
|
+
* fast one: it is a few lines, obviously correct, and asset-sized images are not
|
|
20
|
+
* the bottleneck of a build step. Subsampled chroma is interpolated, not
|
|
21
|
+
* nearest-picked, which is the difference between agreeing with a viewer to
|
|
22
|
+
* within 2 units and standing eight times further off on a hard colour edge.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import UnsupportedImage from "./unsupported.js";
|
|
26
|
+
|
|
27
|
+
/** Coefficient k of the scan order, expressed as its position in the block. */
|
|
28
|
+
const ZIGZAG = [
|
|
29
|
+
0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5,
|
|
30
|
+
12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28,
|
|
31
|
+
35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51,
|
|
32
|
+
58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** COS[i][k] = ½·C(k)·cos((2i+1)kπ/16): the separable IDCT kernel. */
|
|
36
|
+
const COS = (() => {
|
|
37
|
+
const table = [];
|
|
38
|
+
for (let i = 0; i < 8; i++) {
|
|
39
|
+
const row = [];
|
|
40
|
+
for (let k = 0; k < 8; k++) {
|
|
41
|
+
const scale = k === 0 ? Math.SQRT1_2 : 1;
|
|
42
|
+
row.push(0.5 * scale * Math.cos(((2 * i + 1) * k * Math.PI) / 16));
|
|
43
|
+
}
|
|
44
|
+
table.push(row);
|
|
45
|
+
}
|
|
46
|
+
return table;
|
|
47
|
+
})();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Builds a Huffman table from its code counts and symbol values.
|
|
51
|
+
*
|
|
52
|
+
* Keys pack the code length into the high bits so the bit reader can look a code
|
|
53
|
+
* up without building a string per bit — on a 6 MB scan that difference is the
|
|
54
|
+
* whole decode time.
|
|
55
|
+
*
|
|
56
|
+
* @param {Buffer} counts - 16 bytes: how many codes of each length, 1 through 16.
|
|
57
|
+
* @param {Buffer} values - The symbol values, in code order.
|
|
58
|
+
* @returns {Map<number, number>}
|
|
59
|
+
*/
|
|
60
|
+
const buildHuffman = (counts, values) => {
|
|
61
|
+
const map = new Map();
|
|
62
|
+
let code = 0;
|
|
63
|
+
let index = 0;
|
|
64
|
+
for (let length = 1; length <= 16; length++) {
|
|
65
|
+
for (let n = 0; n < counts[length - 1]; n++) {
|
|
66
|
+
map.set((length << 16) | code, values[index]);
|
|
67
|
+
code++;
|
|
68
|
+
index++;
|
|
69
|
+
}
|
|
70
|
+
code <<= 1;
|
|
71
|
+
}
|
|
72
|
+
return map;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Reads a JPEG entropy-coded segment one bit at a time. */
|
|
76
|
+
class BitStream {
|
|
77
|
+
/** @param {Buffer} buf - Already un-stuffed bytes of one restart interval. */
|
|
78
|
+
constructor(buf) {
|
|
79
|
+
this.buf = buf;
|
|
80
|
+
this.byte = 0;
|
|
81
|
+
this.bit = 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @returns {number} Next bit, 0 or 1. */
|
|
85
|
+
readBit() {
|
|
86
|
+
if (this.byte >= this.buf.length) throw new Error("JPEG scan data ended early");
|
|
87
|
+
const bit = (this.buf[this.byte] >> (7 - this.bit)) & 1;
|
|
88
|
+
if (++this.bit === 8) {
|
|
89
|
+
this.bit = 0;
|
|
90
|
+
this.byte++;
|
|
91
|
+
}
|
|
92
|
+
return bit;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @param {number} count - Up to 16 bits, most significant first. */
|
|
96
|
+
readBits(count) {
|
|
97
|
+
let value = 0;
|
|
98
|
+
for (let i = 0; i < count; i++) value = (value << 1) | this.readBit();
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Walks the Huffman tree bit by bit.
|
|
104
|
+
*
|
|
105
|
+
* @param {Map<number, number>} table
|
|
106
|
+
* @returns {number} Symbol; for AC tables this packs the zero-run in the high
|
|
107
|
+
* nibble and the magnitude size in the low one.
|
|
108
|
+
*/
|
|
109
|
+
decode(table) {
|
|
110
|
+
let code = 0;
|
|
111
|
+
for (let length = 1; length <= 16; length++) {
|
|
112
|
+
code = (code << 1) | this.readBit();
|
|
113
|
+
const symbol = table.get((length << 16) | code);
|
|
114
|
+
if (symbol !== undefined) return symbol;
|
|
115
|
+
}
|
|
116
|
+
throw new Error("JPEG Huffman code is not canonical");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Reads the amplitude that follows a size symbol.
|
|
122
|
+
*
|
|
123
|
+
* JPEG writes these as offset binary: size 3 covers -7..-4 and 4..7, so a value
|
|
124
|
+
* below the halfway point has had its leading bit cleared, not its sign set.
|
|
125
|
+
*
|
|
126
|
+
* @param {BitStream} stream
|
|
127
|
+
* @param {number} size
|
|
128
|
+
* @returns {number}
|
|
129
|
+
*/
|
|
130
|
+
const extend = (stream, size) => {
|
|
131
|
+
if (size === 0) return 0;
|
|
132
|
+
const value = stream.readBits(size);
|
|
133
|
+
return value >= 1 << (size - 1) ? value : value - ((1 << size) - 1);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Dequantises nothing, inverse-transforms everything: one 8×8 block, in place.
|
|
138
|
+
*
|
|
139
|
+
* @param {Float64Array} io - 64 coefficients in block order, replaced by pixels.
|
|
140
|
+
* @param {Float64Array} scratch - 64 reusable slots for the row pass.
|
|
141
|
+
* @returns {void}
|
|
142
|
+
*/
|
|
143
|
+
const inverseDct = (io, scratch) => {
|
|
144
|
+
for (let row = 0; row < 8; row++) {
|
|
145
|
+
for (let x = 0; x < 8; x++) {
|
|
146
|
+
let sum = 0;
|
|
147
|
+
for (let k = 0; k < 8; k++) sum += io[row * 8 + k] * COS[x][k];
|
|
148
|
+
scratch[row * 8 + x] = sum;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (let col = 0; col < 8; col++) {
|
|
152
|
+
for (let y = 0; y < 8; y++) {
|
|
153
|
+
let sum = 0;
|
|
154
|
+
for (let k = 0; k < 8; k++) sum += scratch[k * 8 + col] * COS[y][k];
|
|
155
|
+
io[y * 8 + col] = sum + 128;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Cuts a scan into restart intervals, removing byte stuffing on the way.
|
|
162
|
+
*
|
|
163
|
+
* The bit reader never has to look ahead for 0xFF00, and each interval becomes
|
|
164
|
+
* an independent segment whose DC predictors start over — which is what makes a
|
|
165
|
+
* truncated file stop at its interval instead of garbling the rest.
|
|
166
|
+
*
|
|
167
|
+
* @param {Buffer} buf - Bytes from the first entropy byte to the marker that ends the scan.
|
|
168
|
+
* @returns {Buffer[]} One entry per interval, including the trailing one.
|
|
169
|
+
*/
|
|
170
|
+
const splitScan = (buf) => {
|
|
171
|
+
const segments = [];
|
|
172
|
+
let current = [];
|
|
173
|
+
let i = 0;
|
|
174
|
+
while (i < buf.length) {
|
|
175
|
+
const byte = buf[i];
|
|
176
|
+
if (byte !== 0xff) {
|
|
177
|
+
current.push(byte);
|
|
178
|
+
i++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const next = buf[i + 1];
|
|
182
|
+
if (next === 0x00) {
|
|
183
|
+
current.push(0xff);
|
|
184
|
+
i += 2;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (next === 0xff) {
|
|
188
|
+
// Fill byte: the encoder may pad before any marker, and `0xFFFF…` is how
|
|
189
|
+
// some tools stretch a segment boundary. Dropping one byte keeps the pair
|
|
190
|
+
// readable on the next pass instead of ending the scan early.
|
|
191
|
+
i++;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (next >= 0xd0 && next <= 0xd7) {
|
|
195
|
+
segments.push(Buffer.from(current));
|
|
196
|
+
current = [];
|
|
197
|
+
i += 2;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
break; // a real marker ends the scan
|
|
201
|
+
}
|
|
202
|
+
segments.push(Buffer.from(current));
|
|
203
|
+
return segments;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Decodes a JPEG file into RGBA pixels.
|
|
208
|
+
*
|
|
209
|
+
* @param {Buffer} buf - File bytes.
|
|
210
|
+
* @returns {{ width: number, height: number, rgba: Buffer }}
|
|
211
|
+
* @throws {UnsupportedImage} For a JPEG mode this decoder will never read.
|
|
212
|
+
* @throws {Error} For a file whose bytes do not add up.
|
|
213
|
+
*/
|
|
214
|
+
export const decodeJpeg = (buf) => {
|
|
215
|
+
if (buf.length < 4 || buf[0] !== 0xff || buf[1] !== 0xd8) {
|
|
216
|
+
throw new Error("not a JPEG: missing SOI marker");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const quant = [];
|
|
220
|
+
const dc = [];
|
|
221
|
+
const ac = [];
|
|
222
|
+
let frame = null;
|
|
223
|
+
let restartInterval = 0;
|
|
224
|
+
const scans = [];
|
|
225
|
+
|
|
226
|
+
let offset = 2;
|
|
227
|
+
while (offset + 1 < buf.length) {
|
|
228
|
+
if (buf[offset] !== 0xff) {
|
|
229
|
+
offset++;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
let marker = buf[offset + 1];
|
|
233
|
+
while (marker === 0xff) {
|
|
234
|
+
offset++;
|
|
235
|
+
marker = buf[offset + 1];
|
|
236
|
+
}
|
|
237
|
+
offset += 2;
|
|
238
|
+
|
|
239
|
+
// Standalone markers carry no length: stuffed bytes, fill, TEM, and RSTn.
|
|
240
|
+
if (marker === 0x00 || marker === 0x01 || marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd7)) continue;
|
|
241
|
+
if (marker === 0xd9) break;
|
|
242
|
+
|
|
243
|
+
const length = buf.readUInt16BE(offset);
|
|
244
|
+
const payload = buf.subarray(offset + 2, offset + 2 + length - 2);
|
|
245
|
+
offset += length;
|
|
246
|
+
|
|
247
|
+
if (marker === 0xdb) readDqt(payload, quant);
|
|
248
|
+
else if (marker === 0xc4) readDht(payload, dc, ac);
|
|
249
|
+
else if (marker === 0xdd) restartInterval = payload.readUInt16BE(0);
|
|
250
|
+
else if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8) {
|
|
251
|
+
frame = readFrame(marker, payload);
|
|
252
|
+
} else if (marker === 0xda) {
|
|
253
|
+
// Jump the entropy data: a multi-scan file continues here, and reading
|
|
254
|
+
// forward through stuffed bytes as if they were markers is how a second
|
|
255
|
+
// scan starts decoding the first one's leftovers.
|
|
256
|
+
const start = offset;
|
|
257
|
+
const end = findScanEnd(buf, start);
|
|
258
|
+
// Snapshot what is in force at this SOS. A later marker may redefine the
|
|
259
|
+
// same table or interval slot, and if the scans are decoded once the whole
|
|
260
|
+
// header has been walked, that later definition silently rewrites the
|
|
261
|
+
// earlier scan's pixels.
|
|
262
|
+
scans.push({ payload, start, end, restartInterval, tables: { quant: quant.slice(), dc: dc.slice(), ac: ac.slice() } });
|
|
263
|
+
offset = end;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!frame) throw new Error("JPEG has no start-of-frame marker");
|
|
268
|
+
if (!scans.length) throw new Error("JPEG has no scan");
|
|
269
|
+
|
|
270
|
+
const { width, height, components } = frame;
|
|
271
|
+
const maxH = Math.max(...components.map((c) => c.h));
|
|
272
|
+
const maxV = Math.max(...components.map((c) => c.v));
|
|
273
|
+
const mcusPerRow = Math.ceil(width / (8 * maxH));
|
|
274
|
+
const mcusPerColumn = Math.ceil(height / (8 * maxV));
|
|
275
|
+
|
|
276
|
+
for (const component of components) {
|
|
277
|
+
component.planeWidth = mcusPerRow * component.h * 8;
|
|
278
|
+
component.planeHeight = mcusPerColumn * component.v * 8;
|
|
279
|
+
component.plane = new Float64Array(component.planeWidth * component.planeHeight);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const block = new Float64Array(64);
|
|
283
|
+
const scratch = new Float64Array(64);
|
|
284
|
+
for (const scan of scans) {
|
|
285
|
+
decodeScan(scan, { buf, components, width, height, maxH, maxV, block, scratch });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const rgba = Buffer.alloc(width * height * 4);
|
|
289
|
+
// The frame declares components in its own order; the identifiers say which is
|
|
290
|
+
// which, and a file whose list is Y-Cr-Cb would otherwise come out inverted.
|
|
291
|
+
const byId = (id) => components.find((c) => c.id === id);
|
|
292
|
+
const luma = components.length === 1 ? components[0] : byId(1) ?? components[0];
|
|
293
|
+
const chromaB = byId(2) ?? components[1];
|
|
294
|
+
const chromaR = byId(3) ?? components[2];
|
|
295
|
+
|
|
296
|
+
if (components.length === 1) {
|
|
297
|
+
for (let y = 0; y < height; y++) {
|
|
298
|
+
for (let x = 0; x < width; x++) {
|
|
299
|
+
const grey = clamp255(sample(luma, x, y, maxH, maxV));
|
|
300
|
+
const dst = (y * width + x) * 4;
|
|
301
|
+
rgba[dst] = grey;
|
|
302
|
+
rgba[dst + 1] = grey;
|
|
303
|
+
rgba[dst + 2] = grey;
|
|
304
|
+
rgba[dst + 3] = 255;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
for (let y = 0; y < height; y++) {
|
|
309
|
+
for (let x = 0; x < width; x++) {
|
|
310
|
+
const yy = sample(luma, x, y, maxH, maxV);
|
|
311
|
+
const cb = sample(chromaB, x, y, maxH, maxV) - 128;
|
|
312
|
+
const cr = sample(chromaR, x, y, maxH, maxV) - 128;
|
|
313
|
+
const dst = (y * width + x) * 4;
|
|
314
|
+
rgba[dst] = clamp255(yy + 1.402 * cr);
|
|
315
|
+
rgba[dst + 1] = clamp255(yy - 0.344136 * cb - 0.714136 * cr);
|
|
316
|
+
rgba[dst + 2] = clamp255(yy + 1.772 * cb);
|
|
317
|
+
rgba[dst + 3] = 255;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { width, height, rgba };
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Decodes every MCU of one scan into its components' planes.
|
|
327
|
+
*
|
|
328
|
+
* Blocks are read in *scan* order, which is the order of the SOS component
|
|
329
|
+
* specifiers — not the order the frame listed them in. They are usually the same
|
|
330
|
+
* list, and when they are not, reading them by frame order hands each block the
|
|
331
|
+
* wrong Huffman tables and the rest of the scan decodes as noise.
|
|
332
|
+
*
|
|
333
|
+
* @param {{ payload: Buffer, start: number, end: number, restartInterval: number,
|
|
334
|
+
* tables: { quant: Object[], dc: Map[], ac: Map[] } }} scan
|
|
335
|
+
* @param {Object} ctx - Frame geometry, tables and reusable buffers.
|
|
336
|
+
* @returns {void}
|
|
337
|
+
*/
|
|
338
|
+
const decodeScan = (scan, ctx) => {
|
|
339
|
+
const { buf, components, width, height, maxH, maxV, block, scratch } = ctx;
|
|
340
|
+
const { restartInterval, tables: { quant, dc, ac } } = scan;
|
|
341
|
+
const specs = readScanHeader(scan.payload, components.length);
|
|
342
|
+
if (specs.length !== components.length && specs.length !== 1) {
|
|
343
|
+
throw new Error(`JPEG scan covers ${specs.length} of ${components.length} components, which is neither interleaved nor single-component`);
|
|
344
|
+
}
|
|
345
|
+
const interleaved = specs.length === components.length;
|
|
346
|
+
|
|
347
|
+
const list = specs.map((spec) => {
|
|
348
|
+
const component = components.find((c) => c.id === spec.id);
|
|
349
|
+
if (!component) throw new Error(`JPEG scan names component ${spec.id}, which the frame never declared`);
|
|
350
|
+
component.dcTable = dc[spec.tables >> 4];
|
|
351
|
+
component.acTable = ac[spec.tables & 15];
|
|
352
|
+
if (!component.dcTable || !component.acTable) {
|
|
353
|
+
throw new Error("JPEG scan names a Huffman table that was never defined");
|
|
354
|
+
}
|
|
355
|
+
return component;
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// In a single-component scan the MCU is one block and the scan walks that
|
|
359
|
+
// component's own block grid in raster order; the h×v block group only exists
|
|
360
|
+
// in the interleaved form. Reading a non-interleaved luma scan as 2×2 groups
|
|
361
|
+
// instead keeps the right number of blocks and still puts half of each MCU row
|
|
362
|
+
// one block-row early, which scrambles the picture without tripping any check.
|
|
363
|
+
const rowMcus = interleaved
|
|
364
|
+
? Math.ceil(width / (8 * maxH))
|
|
365
|
+
: Math.ceil(Math.ceil((width * list[0].h) / maxH) / 8);
|
|
366
|
+
const colMcus = interleaved
|
|
367
|
+
? Math.ceil(height / (8 * maxV))
|
|
368
|
+
: Math.ceil(Math.ceil((height * list[0].v) / maxV) / 8);
|
|
369
|
+
const totalMcus = rowMcus * colMcus;
|
|
370
|
+
|
|
371
|
+
const segments = splitScan(buf.subarray(scan.start, scan.end));
|
|
372
|
+
const intervals = restartInterval > 0 ? Math.ceil(totalMcus / restartInterval) : 1;
|
|
373
|
+
if (segments.length < intervals) {
|
|
374
|
+
throw new Error("JPEG scan data ended before the frame was complete");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const predictors = new Int32Array(list.length);
|
|
378
|
+
let stream = new BitStream(segments[0]);
|
|
379
|
+
let segment = 0;
|
|
380
|
+
let mcusLeft = restartInterval > 0 ? Math.min(restartInterval, totalMcus) : totalMcus;
|
|
381
|
+
|
|
382
|
+
for (let mcu = 0; mcu < totalMcus; mcu++) {
|
|
383
|
+
if (mcusLeft === 0) {
|
|
384
|
+
segment++;
|
|
385
|
+
stream = new BitStream(segments[segment]);
|
|
386
|
+
mcusLeft = Math.min(restartInterval, totalMcus - mcu);
|
|
387
|
+
predictors.fill(0);
|
|
388
|
+
}
|
|
389
|
+
mcusLeft--;
|
|
390
|
+
|
|
391
|
+
const mcuX = mcu % rowMcus;
|
|
392
|
+
const mcuY = Math.floor(mcu / rowMcus);
|
|
393
|
+
|
|
394
|
+
for (let ci = 0; ci < list.length; ci++) {
|
|
395
|
+
const component = list[ci];
|
|
396
|
+
const table = quant[component.quantId];
|
|
397
|
+
if (!table) throw new Error(`JPEG names quantisation table ${component.quantId}, which was never defined`);
|
|
398
|
+
|
|
399
|
+
// Interleaved: this MCU carries the component's whole h×v block group.
|
|
400
|
+
// Non-interleaved: one block, and the max factors only decide how the
|
|
401
|
+
// plane maps back onto luma pixels, in `sample()`.
|
|
402
|
+
const wide = interleaved ? component.h : 1;
|
|
403
|
+
const tall = interleaved ? component.v : 1;
|
|
404
|
+
for (let by = 0; by < tall; by++) {
|
|
405
|
+
for (let bx = 0; bx < wide; bx++) {
|
|
406
|
+
decodeBlock(component, ci, stream, predictors, block);
|
|
407
|
+
for (let k = 0; k < 64; k++) block[k] *= table[k];
|
|
408
|
+
inverseDct(block, scratch);
|
|
409
|
+
const originX = (mcuX * wide + bx) * 8;
|
|
410
|
+
const originY = (mcuY * tall + by) * 8;
|
|
411
|
+
for (let y = 0; y < 8; y++) {
|
|
412
|
+
const rowStart = (originY + y) * component.planeWidth + originX;
|
|
413
|
+
for (let x = 0; x < 8; x++) component.plane[rowStart + x] = block[y * 8 + x];
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* One MCU block: DC prediction then the AC run-length symbols.
|
|
423
|
+
*
|
|
424
|
+
* @param {Object} component - Supplies this scan's Huffman tables.
|
|
425
|
+
* @param {number} ci - Slot in the scan's predictor array.
|
|
426
|
+
* @param {BitStream} stream
|
|
427
|
+
* @param {Int32Array} predictors
|
|
428
|
+
* @param {Float64Array} block - Cleared, then filled with dequant-ready coefficients.
|
|
429
|
+
* @returns {void}
|
|
430
|
+
*/
|
|
431
|
+
const decodeBlock = (component, ci, stream, predictors, block) => {
|
|
432
|
+
block.fill(0);
|
|
433
|
+
predictors[ci] += extend(stream, stream.decode(component.dcTable));
|
|
434
|
+
block[0] = predictors[ci];
|
|
435
|
+
|
|
436
|
+
let k = 1;
|
|
437
|
+
while (k < 64) {
|
|
438
|
+
const rs = stream.decode(component.acTable);
|
|
439
|
+
const run = rs >> 4;
|
|
440
|
+
const size = rs & 15;
|
|
441
|
+
if (size === 0) {
|
|
442
|
+
if (run === 0) break; // EOB — the rest of the block is zero
|
|
443
|
+
// 0xF0 is ZRL: sixteen zeros, so the next coefficient is sixteen along.
|
|
444
|
+
// Advancing by the run nibble alone (fifteen) leaves every following block
|
|
445
|
+
// one symbol short, and a block coded to its last coefficient then reads
|
|
446
|
+
// its neighbour's first symbol — which desynchronises the whole rest of
|
|
447
|
+
// the scan. That is how an ordinary photo turned into "AC coefficient ran
|
|
448
|
+
// past the block" while small test images decoded fine.
|
|
449
|
+
if (run !== 15) throw new Error(`JPEG AC symbol 0x${rs.toString(16)} is reserved`);
|
|
450
|
+
k += 16;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
k += run;
|
|
454
|
+
if (k >= 64) throw new Error("JPEG AC coefficient ran past the block");
|
|
455
|
+
block[ZIGZAG[k]] = extend(stream, size);
|
|
456
|
+
k++;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Reads an SOFn header.
|
|
462
|
+
*
|
|
463
|
+
* @param {number} marker - The SOF marker byte.
|
|
464
|
+
* @param {Buffer} payload
|
|
465
|
+
* @returns {{ width: number, height: number, components: Object[] }}
|
|
466
|
+
* @throws {UnsupportedImage} For the frame modes this decoder does not implement.
|
|
467
|
+
*/
|
|
468
|
+
const readFrame = (marker, payload) => {
|
|
469
|
+
const kind = {
|
|
470
|
+
0xc0: "baseline",
|
|
471
|
+
0xc1: "extended sequential",
|
|
472
|
+
0xc2: "progressive",
|
|
473
|
+
0xc3: "lossless",
|
|
474
|
+
}[marker];
|
|
475
|
+
if (marker === 0xc2) throw new UnsupportedImage("JPEG", "progressive (multi-scan) entropy coding is not implemented");
|
|
476
|
+
if (marker === 0xc3) throw new UnsupportedImage("JPEG", "lossless (predictive) JPEG is not implemented");
|
|
477
|
+
if (marker >= 0xc6 && marker <= 0xcf) throw new UnsupportedImage("JPEG", "arithmetic coding is not implemented");
|
|
478
|
+
if (!kind) throw new UnsupportedImage("JPEG", `frame marker 0x${marker.toString(16)} is not implemented`);
|
|
479
|
+
if (payload[0] !== 8) throw new UnsupportedImage("JPEG", `${payload[0]}-bit samples are not implemented (only 8-bit)`);
|
|
480
|
+
|
|
481
|
+
const height = payload.readUInt16BE(1);
|
|
482
|
+
const width = payload.readUInt16BE(3);
|
|
483
|
+
const count = payload[5];
|
|
484
|
+
if (count === 4) throw new UnsupportedImage("JPEG", "CMYK (four components) is not implemented");
|
|
485
|
+
if (count !== 1 && count !== 3) {
|
|
486
|
+
throw new UnsupportedImage("JPEG", `${count} components is not implemented (only greyscale and YCbCr)`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const components = [];
|
|
490
|
+
for (let i = 0; i < count; i++) {
|
|
491
|
+
const base = 6 + i * 3;
|
|
492
|
+
components.push({
|
|
493
|
+
id: payload[base],
|
|
494
|
+
h: payload[base + 1] >> 4,
|
|
495
|
+
v: payload[base + 1] & 15,
|
|
496
|
+
quantId: payload[base + 2],
|
|
497
|
+
dcTable: null,
|
|
498
|
+
acTable: null,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
return { width, height, components };
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Reads a DQT segment, which may hold several tables.
|
|
506
|
+
*
|
|
507
|
+
* @param {Buffer} payload
|
|
508
|
+
* @param {Float64Array[]} quant - Filled by table id.
|
|
509
|
+
* @returns {void}
|
|
510
|
+
*/
|
|
511
|
+
const readDqt = (payload, quant) => {
|
|
512
|
+
let p = 0;
|
|
513
|
+
while (p + 65 <= payload.length) {
|
|
514
|
+
const precision = payload[p] >> 4;
|
|
515
|
+
const id = payload[p] & 15;
|
|
516
|
+
p++;
|
|
517
|
+
if (precision !== 0) throw new UnsupportedImage("JPEG", "16-bit quantisation tables belong to 12-bit JPEG, which is not implemented");
|
|
518
|
+
// The table arrives in scan order; coefficients are stored in block
|
|
519
|
+
// order, so it is de-zigzagged once here rather than per block.
|
|
520
|
+
const table = new Float64Array(64);
|
|
521
|
+
for (let k = 0; k < 64; k++) table[ZIGZAG[k]] = payload[p + k];
|
|
522
|
+
p += 64;
|
|
523
|
+
quant[id] = table;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Reads a DHT segment.
|
|
529
|
+
*
|
|
530
|
+
* The spec puts one table per marker, but plenty of camera software writes all
|
|
531
|
+
* four in a single segment, and libjpeg walks the payload rather than trusting
|
|
532
|
+
* that. Reading only the first table leaves a scan pointing at Huffman tables
|
|
533
|
+
* that were never defined — for a file that opens fine in every viewer.
|
|
534
|
+
*
|
|
535
|
+
* @param {Buffer} payload
|
|
536
|
+
* @param {Map<number, number>[]} dc
|
|
537
|
+
* @param {Map<number, number>[]} ac
|
|
538
|
+
* @returns {void}
|
|
539
|
+
*/
|
|
540
|
+
const readDht = (payload, dc, ac) => {
|
|
541
|
+
let p = 0;
|
|
542
|
+
while (p + 17 <= payload.length) {
|
|
543
|
+
const classId = payload[p];
|
|
544
|
+
const counts = payload.subarray(p + 1, p + 17);
|
|
545
|
+
const total = counts.reduce((a, b) => a + b, 0);
|
|
546
|
+
if (p + 17 + total > payload.length) {
|
|
547
|
+
throw new Error("JPEG Huffman table claims more codes than its segment holds");
|
|
548
|
+
}
|
|
549
|
+
const table = buildHuffman(counts, payload.subarray(p + 17, p + 17 + total));
|
|
550
|
+
if (classId >> 4 === 0) dc[classId & 15] = table;
|
|
551
|
+
else ac[classId & 15] = table;
|
|
552
|
+
p += 17 + total;
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Reads an SOS header and rejects anything that is not a full-band, first-pass
|
|
558
|
+
* scan.
|
|
559
|
+
*
|
|
560
|
+
* @param {Buffer} payload
|
|
561
|
+
* @param {number} componentCount
|
|
562
|
+
* @returns {{ id: number, tables: number }[]} Component specifiers, in scan order.
|
|
563
|
+
* @throws {UnsupportedImage} For a progressive or refined scan.
|
|
564
|
+
*/
|
|
565
|
+
const readScanHeader = (payload, componentCount) => {
|
|
566
|
+
const count = payload[0];
|
|
567
|
+
if (payload.length < 4 + count * 2) throw new Error("JPEG scan header is truncated");
|
|
568
|
+
const specs = [];
|
|
569
|
+
for (let i = 0; i < count; i++) {
|
|
570
|
+
specs.push({ id: payload[1 + i * 2], tables: payload[2 + i * 2] });
|
|
571
|
+
}
|
|
572
|
+
const spectralStart = payload[1 + count * 2];
|
|
573
|
+
const spectralEnd = payload[2 + count * 2];
|
|
574
|
+
const approximation = payload[3 + count * 2];
|
|
575
|
+
if (spectralStart !== 0 || spectralEnd !== 63 || approximation !== 0) {
|
|
576
|
+
throw new UnsupportedImage("JPEG", "progressive (multi-scan) entropy coding is not implemented");
|
|
577
|
+
}
|
|
578
|
+
return specs;
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Sample of a possibly subsampled component plane at a luma pixel.
|
|
583
|
+
*
|
|
584
|
+
* The chroma sample belonging to a luma pixel sits at the centre of the block it
|
|
585
|
+
* covers, hence the half-pixel offset before scaling. Where the scale is above
|
|
586
|
+
* 1:1 the two neighbouring samples are blended instead of one being chosen: a
|
|
587
|
+
* hard colour edge otherwise comes out with stair-stepped fringes, which is what
|
|
588
|
+
* every viewer's upsampler exists to avoid. At 1:1 the weights are exactly 0 and
|
|
589
|
+
* the luma plane passes through untouched.
|
|
590
|
+
*
|
|
591
|
+
* @param {Object} component
|
|
592
|
+
* @param {number} x
|
|
593
|
+
* @param {number} y
|
|
594
|
+
* @param {number} maxH
|
|
595
|
+
* @param {number} maxV
|
|
596
|
+
* @returns {number}
|
|
597
|
+
*/
|
|
598
|
+
const sample = (component, x, y, maxH, maxV) => {
|
|
599
|
+
const plane = component.plane;
|
|
600
|
+
const pw = component.planeWidth;
|
|
601
|
+
const u = Math.min(Math.max((x + 0.5) * component.h / maxH - 0.5, 0), pw - 1);
|
|
602
|
+
const v = Math.min(Math.max((y + 0.5) * component.v / maxV - 0.5, 0), component.planeHeight - 1);
|
|
603
|
+
const x0 = Math.floor(u), y0 = Math.floor(v);
|
|
604
|
+
const x1 = Math.min(x0 + 1, pw - 1), y1 = Math.min(y0 + 1, component.planeHeight - 1);
|
|
605
|
+
const fx = u - x0, fy = v - y0;
|
|
606
|
+
const top = plane[y0 * pw + x0] * (1 - fx) + plane[y0 * pw + x1] * fx;
|
|
607
|
+
const bottom = plane[y1 * pw + x0] * (1 - fx) + plane[y1 * pw + x1] * fx;
|
|
608
|
+
return top * (1 - fy) + bottom * fy;
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
/** @param {number} value @returns {number} Rounded into 0-255. */
|
|
612
|
+
const clamp255 = (value) => {
|
|
613
|
+
const rounded = Math.round(value);
|
|
614
|
+
return rounded < 0 ? 0 : rounded > 255 ? 255 : rounded;
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* @param {Buffer} buf
|
|
619
|
+
* @param {number} start - First entropy byte of the scan.
|
|
620
|
+
* @returns {number} Offset of the first marker that ends the scan.
|
|
621
|
+
*/
|
|
622
|
+
const findScanEnd = (buf, start) => {
|
|
623
|
+
let i = start;
|
|
624
|
+
while (i + 1 < buf.length) {
|
|
625
|
+
if (buf[i] !== 0xff) {
|
|
626
|
+
i++;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const marker = buf[i + 1];
|
|
630
|
+
if (marker === 0x00 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
631
|
+
i += 2;
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
if (marker === 0xff) {
|
|
635
|
+
i++;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
return i;
|
|
639
|
+
}
|
|
640
|
+
return buf.length;
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
export default decodeJpeg;
|