@crewhaus/tool-media 0.7.0
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 +156 -0
- package/dist/fixtures.d.ts +81 -0
- package/dist/fixtures.js +385 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +1132 -0
- package/dist/lib/barcode.d.ts +43 -0
- package/dist/lib/barcode.js +310 -0
- package/dist/lib/bytes.d.ts +45 -0
- package/dist/lib/bytes.js +162 -0
- package/dist/lib/chart.d.ts +32 -0
- package/dist/lib/chart.js +290 -0
- package/dist/lib/color.d.ts +53 -0
- package/dist/lib/color.js +267 -0
- package/dist/lib/diagram.d.ts +48 -0
- package/dist/lib/diagram.js +256 -0
- package/dist/lib/ffprobe.d.ts +36 -0
- package/dist/lib/ffprobe.js +154 -0
- package/dist/lib/identify.d.ts +39 -0
- package/dist/lib/identify.js +519 -0
- package/dist/lib/imageops.d.ts +86 -0
- package/dist/lib/imageops.js +236 -0
- package/dist/lib/jpeg.d.ts +81 -0
- package/dist/lib/jpeg.js +388 -0
- package/dist/lib/png.d.ts +59 -0
- package/dist/lib/png.js +466 -0
- package/dist/lib/qr.d.ts +70 -0
- package/dist/lib/qr.js +544 -0
- package/dist/lib/raster.d.ts +18 -0
- package/dist/lib/raster.js +82 -0
- package/dist/lib/sparkline.d.ts +21 -0
- package/dist/lib/sparkline.js +93 -0
- package/dist/lib/subtitle.d.ts +62 -0
- package/dist/lib/subtitle.js +233 -0
- package/dist/lib/svg.d.ts +76 -0
- package/dist/lib/svg.js +168 -0
- package/dist/paths.d.ts +53 -0
- package/dist/paths.js +161 -0
- package/dist/proc.d.ts +39 -0
- package/dist/proc.js +158 -0
- package/package.json +45 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export declare const PNG_SIGNATURE: ReadonlyArray<number>;
|
|
2
|
+
/** Decoded pixels, always 8-bit RGBA, row-major, top-left origin. */
|
|
3
|
+
export type RgbaImage = {
|
|
4
|
+
readonly width: number;
|
|
5
|
+
readonly height: number;
|
|
6
|
+
/** `width * height * 4` bytes: R, G, B, A. */
|
|
7
|
+
readonly data: Uint8Array;
|
|
8
|
+
};
|
|
9
|
+
export type PngColorType = "rgba" | "rgb" | "gray" | "gray-alpha";
|
|
10
|
+
/** What a decoded file turned out to be, for reporting back to the caller. */
|
|
11
|
+
export type PngDecoded = RgbaImage & {
|
|
12
|
+
readonly sourceColorType: number;
|
|
13
|
+
readonly sourceBitDepth: number;
|
|
14
|
+
readonly hadAlpha: boolean;
|
|
15
|
+
};
|
|
16
|
+
/** Greyscale luma, ITU-R BT.601, the coefficients PNG's own spec cites. */
|
|
17
|
+
export declare function luma(r: number, g: number, b: number): number;
|
|
18
|
+
/**
|
|
19
|
+
* True when every pixel is fully opaque — the fact `colorType: "auto"`
|
|
20
|
+
* keys on when choosing between colour type 6 and the smaller type 2.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isOpaque(image: RgbaImage): boolean;
|
|
23
|
+
/** True when every pixel's R, G and B are equal, so greyscale loses nothing. */
|
|
24
|
+
export declare function isGrayscale(image: RgbaImage): boolean;
|
|
25
|
+
/** The colour type `"auto"` resolves to for these pixels. */
|
|
26
|
+
export declare function autoColorType(image: RgbaImage): PngColorType;
|
|
27
|
+
export type EncodeOptions = {
|
|
28
|
+
/** `"auto"` picks the smallest lossless type for these exact pixels. */
|
|
29
|
+
readonly colorType?: PngColorType | "auto";
|
|
30
|
+
/** zlib level, pinned so two runs of the same render compare equal. */
|
|
31
|
+
readonly level?: number;
|
|
32
|
+
};
|
|
33
|
+
/** Encode RGBA pixels as a PNG file. */
|
|
34
|
+
export declare function encodePng(image: RgbaImage, options?: EncodeOptions): Uint8Array;
|
|
35
|
+
export type DecodeLimits = {
|
|
36
|
+
/** Largest `width * height` this will decode. */
|
|
37
|
+
readonly maxPixels: number;
|
|
38
|
+
};
|
|
39
|
+
export declare const DEFAULT_DECODE_LIMITS: DecodeLimits;
|
|
40
|
+
/** Header facts, read without inflating anything. */
|
|
41
|
+
export type PngHeader = {
|
|
42
|
+
readonly width: number;
|
|
43
|
+
readonly height: number;
|
|
44
|
+
readonly bitDepth: number;
|
|
45
|
+
readonly colorType: number;
|
|
46
|
+
readonly interlace: number;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Read `IHDR` alone. Throws `MediaFormatError` if this is not a PNG.
|
|
50
|
+
*
|
|
51
|
+
* The CRC is checked BEFORE the fields are believed. Every cap downstream is
|
|
52
|
+
* derived from these thirteen bytes, so a flipped bit here is the difference
|
|
53
|
+
* between "this is a 4x4 image" and a dimension nothing else in the file
|
|
54
|
+
* agrees with — and the honest answer to that is "this file is corrupt",
|
|
55
|
+
* not a refusal that blames the caller for a size they never wrote.
|
|
56
|
+
*/
|
|
57
|
+
export declare function readPngHeader(bytes: Uint8Array): PngHeader;
|
|
58
|
+
/** Decode a PNG to 8-bit RGBA. */
|
|
59
|
+
export declare function decodePng(bytes: Uint8Array, limits?: DecodeLimits): PngDecoded;
|
package/dist/lib/png.js
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A PNG encoder and decoder, written against the PNG specification (ISO
|
|
3
|
+
* 15948) rather than pulled from a dependency, because a deterministic
|
|
4
|
+
* package cannot ship a native image library and still promise the same
|
|
5
|
+
* bytes on every machine.
|
|
6
|
+
*
|
|
7
|
+
* ## Writing — supported
|
|
8
|
+
*
|
|
9
|
+
* - 8-bit truecolour with alpha (colour type 6), truecolour (2), greyscale
|
|
10
|
+
* with alpha (4) and greyscale (0).
|
|
11
|
+
* - One `IHDR`, one `IDAT`, one `IEND`. No interlacing, no ancillary
|
|
12
|
+
* chunks, no palette. The filter used on every scanline is chosen from
|
|
13
|
+
* None/Sub/Up/Average/Paeth by the minimum-sum-of-absolute-differences
|
|
14
|
+
* heuristic the specification itself suggests, which is a pure function
|
|
15
|
+
* of the pixels — so the same pixels always produce the same file.
|
|
16
|
+
* - zlib compression comes from `node:zlib` at a pinned level, again so the
|
|
17
|
+
* bytes do not move underneath a caller comparing two renders.
|
|
18
|
+
*
|
|
19
|
+
* ## Reading — supported
|
|
20
|
+
*
|
|
21
|
+
* - Bit depth 8, colour types 0, 2, 3 (palette, with `tRNS`), 4 and 6.
|
|
22
|
+
* - Multiple `IDAT` chunks, concatenated as the specification requires.
|
|
23
|
+
* - `tRNS` for greyscale and truecolour (a single transparent sample value).
|
|
24
|
+
* - The CRC of every chunk it reads is checked against the bytes, so a
|
|
25
|
+
* corrupt file is reported as corrupt rather than decoded into pixels that
|
|
26
|
+
* are not the picture.
|
|
27
|
+
*
|
|
28
|
+
* ## Reading — refused, loudly, rather than guessed at
|
|
29
|
+
*
|
|
30
|
+
* - Bit depths 1, 2, 4 and 16. A 16-bit image silently truncated to 8 bits
|
|
31
|
+
* is not the image the caller handed over.
|
|
32
|
+
* - Adam7 interlacing.
|
|
33
|
+
* - Any file whose declared dimensions exceed the caller's pixel cap. The
|
|
34
|
+
* check happens on `IHDR`, before a single byte is inflated, and the
|
|
35
|
+
* inflate itself is given `maxOutputLength` so a lying stream cannot get
|
|
36
|
+
* past it either.
|
|
37
|
+
*/
|
|
38
|
+
import { deflateSync, inflateSync } from "node:zlib";
|
|
39
|
+
import { ByteReader, MediaFormatError, concatBytes, crc32, startsWith } from "./bytes";
|
|
40
|
+
export const PNG_SIGNATURE = [
|
|
41
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
42
|
+
];
|
|
43
|
+
const COLOR_TYPE_CODE = {
|
|
44
|
+
gray: 0,
|
|
45
|
+
rgb: 2,
|
|
46
|
+
"gray-alpha": 4,
|
|
47
|
+
rgba: 6,
|
|
48
|
+
};
|
|
49
|
+
const CHANNELS = { gray: 1, rgb: 3, "gray-alpha": 2, rgba: 4 };
|
|
50
|
+
/** A CRC as the eight hex digits a PNG tool would print it as. */
|
|
51
|
+
function hex8(value) {
|
|
52
|
+
return value.toString(16).padStart(8, "0");
|
|
53
|
+
}
|
|
54
|
+
function u32(value) {
|
|
55
|
+
return new Uint8Array([
|
|
56
|
+
(value >>> 24) & 0xff,
|
|
57
|
+
(value >>> 16) & 0xff,
|
|
58
|
+
(value >>> 8) & 0xff,
|
|
59
|
+
value & 0xff,
|
|
60
|
+
]);
|
|
61
|
+
}
|
|
62
|
+
function chunk(type, data) {
|
|
63
|
+
const typeBytes = new Uint8Array(4);
|
|
64
|
+
for (let i = 0; i < 4; i++)
|
|
65
|
+
typeBytes[i] = type.charCodeAt(i);
|
|
66
|
+
return concatBytes([u32(data.length), typeBytes, data, u32(crc32(typeBytes, data))]);
|
|
67
|
+
}
|
|
68
|
+
/** Greyscale luma, ITU-R BT.601, the coefficients PNG's own spec cites. */
|
|
69
|
+
export function luma(r, g, b) {
|
|
70
|
+
return Math.round(0.299 * r + 0.587 * g + 0.114 * b);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* True when every pixel is fully opaque — the fact `colorType: "auto"`
|
|
74
|
+
* keys on when choosing between colour type 6 and the smaller type 2.
|
|
75
|
+
*/
|
|
76
|
+
export function isOpaque(image) {
|
|
77
|
+
for (let i = 3; i < image.data.length; i += 4) {
|
|
78
|
+
if (image.data[i] !== 255)
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
/** True when every pixel's R, G and B are equal, so greyscale loses nothing. */
|
|
84
|
+
export function isGrayscale(image) {
|
|
85
|
+
for (let i = 0; i < image.data.length; i += 4) {
|
|
86
|
+
if (image.data[i] !== image.data[i + 1] || image.data[i] !== image.data[i + 2])
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
/** The colour type `"auto"` resolves to for these pixels. */
|
|
92
|
+
export function autoColorType(image) {
|
|
93
|
+
const opaque = isOpaque(image);
|
|
94
|
+
const gray = isGrayscale(image);
|
|
95
|
+
if (gray)
|
|
96
|
+
return opaque ? "gray" : "gray-alpha";
|
|
97
|
+
return opaque ? "rgb" : "rgba";
|
|
98
|
+
}
|
|
99
|
+
function paeth(a, b, c) {
|
|
100
|
+
const p = a + b - c;
|
|
101
|
+
const pa = Math.abs(p - a);
|
|
102
|
+
const pb = Math.abs(p - b);
|
|
103
|
+
const pc = Math.abs(p - c);
|
|
104
|
+
if (pa <= pb && pa <= pc)
|
|
105
|
+
return a;
|
|
106
|
+
return pb <= pc ? b : c;
|
|
107
|
+
}
|
|
108
|
+
/** Pack RGBA pixels into the raw sample stream for `colorType`. */
|
|
109
|
+
function packSamples(image, colorType) {
|
|
110
|
+
const bpp = CHANNELS[colorType];
|
|
111
|
+
const out = new Uint8Array(image.width * image.height * bpp);
|
|
112
|
+
const src = image.data;
|
|
113
|
+
let o = 0;
|
|
114
|
+
for (let i = 0; i < src.length; i += 4) {
|
|
115
|
+
const r = src[i];
|
|
116
|
+
const g = src[i + 1];
|
|
117
|
+
const b = src[i + 2];
|
|
118
|
+
const a = src[i + 3];
|
|
119
|
+
switch (colorType) {
|
|
120
|
+
case "rgba":
|
|
121
|
+
out[o++] = r;
|
|
122
|
+
out[o++] = g;
|
|
123
|
+
out[o++] = b;
|
|
124
|
+
out[o++] = a;
|
|
125
|
+
break;
|
|
126
|
+
case "rgb":
|
|
127
|
+
out[o++] = r;
|
|
128
|
+
out[o++] = g;
|
|
129
|
+
out[o++] = b;
|
|
130
|
+
break;
|
|
131
|
+
case "gray-alpha":
|
|
132
|
+
out[o++] = luma(r, g, b);
|
|
133
|
+
out[o++] = a;
|
|
134
|
+
break;
|
|
135
|
+
case "gray":
|
|
136
|
+
out[o++] = luma(r, g, b);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Filter each scanline with whichever of the five filter types gives the
|
|
144
|
+
* smallest sum of absolute signed differences, the heuristic in the PNG
|
|
145
|
+
* spec's own "Filter selection" note. Pure in the pixels, so reproducible.
|
|
146
|
+
*/
|
|
147
|
+
function filterScanlines(samples, width, height, bpp) {
|
|
148
|
+
const stride = width * bpp;
|
|
149
|
+
const out = new Uint8Array(height * (stride + 1));
|
|
150
|
+
const candidate = new Uint8Array(stride);
|
|
151
|
+
const best = new Uint8Array(stride);
|
|
152
|
+
let prev = new Uint8Array(stride);
|
|
153
|
+
for (let y = 0; y < height; y++) {
|
|
154
|
+
const rowStart = y * stride;
|
|
155
|
+
const row = samples.subarray(rowStart, rowStart + stride);
|
|
156
|
+
let bestType = 0;
|
|
157
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
158
|
+
for (let type = 0; type <= 4; type++) {
|
|
159
|
+
let score = 0;
|
|
160
|
+
for (let x = 0; x < stride; x++) {
|
|
161
|
+
const raw = row[x];
|
|
162
|
+
const left = x >= bpp ? row[x - bpp] : 0;
|
|
163
|
+
const up = prev[x];
|
|
164
|
+
const upLeft = x >= bpp ? prev[x - bpp] : 0;
|
|
165
|
+
let value;
|
|
166
|
+
switch (type) {
|
|
167
|
+
case 1:
|
|
168
|
+
value = (raw - left) & 0xff;
|
|
169
|
+
break;
|
|
170
|
+
case 2:
|
|
171
|
+
value = (raw - up) & 0xff;
|
|
172
|
+
break;
|
|
173
|
+
case 3:
|
|
174
|
+
value = (raw - ((left + up) >> 1)) & 0xff;
|
|
175
|
+
break;
|
|
176
|
+
case 4:
|
|
177
|
+
value = (raw - paeth(left, up, upLeft)) & 0xff;
|
|
178
|
+
break;
|
|
179
|
+
default:
|
|
180
|
+
value = raw;
|
|
181
|
+
}
|
|
182
|
+
candidate[x] = value;
|
|
183
|
+
score += value < 128 ? value : 256 - value;
|
|
184
|
+
}
|
|
185
|
+
if (score < bestScore) {
|
|
186
|
+
bestScore = score;
|
|
187
|
+
bestType = type;
|
|
188
|
+
best.set(candidate);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
out[y * (stride + 1)] = bestType;
|
|
192
|
+
out.set(best, y * (stride + 1) + 1);
|
|
193
|
+
prev = row.slice();
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
/** Encode RGBA pixels as a PNG file. */
|
|
198
|
+
export function encodePng(image, options = {}) {
|
|
199
|
+
if (image.width <= 0 || image.height <= 0) {
|
|
200
|
+
throw new MediaFormatError(`cannot encode a ${image.width}x${image.height} image`);
|
|
201
|
+
}
|
|
202
|
+
const expected = image.width * image.height * 4;
|
|
203
|
+
if (image.data.length !== expected) {
|
|
204
|
+
throw new MediaFormatError(`pixel buffer is ${image.data.length} bytes, but ${image.width}x${image.height} RGBA needs ${expected}`);
|
|
205
|
+
}
|
|
206
|
+
const requested = options.colorType ?? "auto";
|
|
207
|
+
const colorType = requested === "auto" ? autoColorType(image) : requested;
|
|
208
|
+
const bpp = CHANNELS[colorType];
|
|
209
|
+
const samples = packSamples(image, colorType);
|
|
210
|
+
const filtered = filterScanlines(samples, image.width, image.height, bpp);
|
|
211
|
+
const idat = deflateSync(filtered, { level: options.level ?? 9 });
|
|
212
|
+
const ihdr = concatBytes([
|
|
213
|
+
u32(image.width),
|
|
214
|
+
u32(image.height),
|
|
215
|
+
new Uint8Array([8, COLOR_TYPE_CODE[colorType], 0, 0, 0]),
|
|
216
|
+
]);
|
|
217
|
+
return concatBytes([
|
|
218
|
+
new Uint8Array(PNG_SIGNATURE),
|
|
219
|
+
chunk("IHDR", ihdr),
|
|
220
|
+
chunk("IDAT", new Uint8Array(idat.buffer, idat.byteOffset, idat.byteLength)),
|
|
221
|
+
chunk("IEND", new Uint8Array(0)),
|
|
222
|
+
]);
|
|
223
|
+
}
|
|
224
|
+
export const DEFAULT_DECODE_LIMITS = Object.freeze({ maxPixels: 16_000_000 });
|
|
225
|
+
/**
|
|
226
|
+
* Read `IHDR` alone. Throws `MediaFormatError` if this is not a PNG.
|
|
227
|
+
*
|
|
228
|
+
* The CRC is checked BEFORE the fields are believed. Every cap downstream is
|
|
229
|
+
* derived from these thirteen bytes, so a flipped bit here is the difference
|
|
230
|
+
* between "this is a 4x4 image" and a dimension nothing else in the file
|
|
231
|
+
* agrees with — and the honest answer to that is "this file is corrupt",
|
|
232
|
+
* not a refusal that blames the caller for a size they never wrote.
|
|
233
|
+
*/
|
|
234
|
+
export function readPngHeader(bytes) {
|
|
235
|
+
if (!startsWith(bytes, PNG_SIGNATURE)) {
|
|
236
|
+
throw new MediaFormatError("not a PNG: the 8-byte signature does not match");
|
|
237
|
+
}
|
|
238
|
+
const reader = new ByteReader(bytes);
|
|
239
|
+
reader.seek(8);
|
|
240
|
+
const length = reader.u32be();
|
|
241
|
+
const type = reader.ascii(4);
|
|
242
|
+
if (type !== "IHDR" || length !== 13) {
|
|
243
|
+
throw new MediaFormatError(`expected a 13-byte IHDR first, found a ${length}-byte ${type}`);
|
|
244
|
+
}
|
|
245
|
+
const declaredCrc = new ByteReader(bytes.subarray(8 + 8 + 13, 8 + 8 + 13 + 4)).u32be();
|
|
246
|
+
const actualCrc = crc32(bytes.subarray(12, 16), bytes.subarray(16, 29));
|
|
247
|
+
if (actualCrc !== declaredCrc) {
|
|
248
|
+
throw new MediaFormatError(`chunk IHDR at offset 8 declares CRC ${hex8(declaredCrc)}, but its 13 bytes check to ${hex8(actualCrc)}`);
|
|
249
|
+
}
|
|
250
|
+
const width = reader.u32be();
|
|
251
|
+
const height = reader.u32be();
|
|
252
|
+
const bitDepth = reader.u8();
|
|
253
|
+
const colorType = reader.u8();
|
|
254
|
+
reader.u8(); // compression method; only 0 is defined
|
|
255
|
+
reader.u8(); // filter method; only 0 is defined
|
|
256
|
+
const interlace = reader.u8();
|
|
257
|
+
if (width === 0 || height === 0) {
|
|
258
|
+
throw new MediaFormatError(`IHDR declares a ${width}x${height} image, which is not valid`);
|
|
259
|
+
}
|
|
260
|
+
return { width, height, bitDepth, colorType, interlace };
|
|
261
|
+
}
|
|
262
|
+
const SAMPLES_PER_PIXEL = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
|
|
263
|
+
/**
|
|
264
|
+
* Chunks whose CRC is checked. Every chunk this decoder READS is here: a
|
|
265
|
+
* corrupt `IDAT` that inflates anyway would hand back pixels that are not
|
|
266
|
+
* the picture, and a flipped bit in `IHDR` or `PLTE` is worse still. Chunks
|
|
267
|
+
* it skips are left alone, so an ancillary chunk written by something sloppy
|
|
268
|
+
* does not stop a file this decoder can otherwise read correctly.
|
|
269
|
+
*/
|
|
270
|
+
const CRC_CHECKED = new Set(["IHDR", "PLTE", "IDAT", "tRNS", "IEND"]);
|
|
271
|
+
/** Decode a PNG to 8-bit RGBA. */
|
|
272
|
+
export function decodePng(bytes, limits = DEFAULT_DECODE_LIMITS) {
|
|
273
|
+
const header = readPngHeader(bytes);
|
|
274
|
+
const { width, height, bitDepth, colorType, interlace } = header;
|
|
275
|
+
if (interlace !== 0) {
|
|
276
|
+
throw new MediaFormatError("Adam7-interlaced PNGs are not supported by this decoder");
|
|
277
|
+
}
|
|
278
|
+
if (bitDepth !== 8) {
|
|
279
|
+
throw new MediaFormatError(`bit depth ${bitDepth} is not supported; this decoder reads 8-bit samples only`);
|
|
280
|
+
}
|
|
281
|
+
const channels = SAMPLES_PER_PIXEL[colorType];
|
|
282
|
+
if (channels === undefined) {
|
|
283
|
+
throw new MediaFormatError(`colour type ${colorType} is not a PNG colour type`);
|
|
284
|
+
}
|
|
285
|
+
// CAP FIRST. The dimensions come from the file, so they are checked before
|
|
286
|
+
// anything sized by them is allocated or inflated.
|
|
287
|
+
if (width * height > limits.maxPixels) {
|
|
288
|
+
throw new MediaFormatError(`${width}x${height} is ${width * height} pixels, over the ${limits.maxPixels} cap`);
|
|
289
|
+
}
|
|
290
|
+
const reader = new ByteReader(bytes);
|
|
291
|
+
reader.seek(8);
|
|
292
|
+
const idatParts = [];
|
|
293
|
+
let palette;
|
|
294
|
+
let paletteAlpha;
|
|
295
|
+
let transparentSample;
|
|
296
|
+
let sawEnd = false;
|
|
297
|
+
while (reader.remaining >= 8 && !sawEnd) {
|
|
298
|
+
const chunkAt = reader.offset;
|
|
299
|
+
const length = reader.u32be();
|
|
300
|
+
const typeAt = reader.offset;
|
|
301
|
+
const type = reader.ascii(4);
|
|
302
|
+
if (length > bytes.length) {
|
|
303
|
+
throw new MediaFormatError(`chunk ${type} declares ${length} bytes, longer than the file`);
|
|
304
|
+
}
|
|
305
|
+
const data = reader.take(length);
|
|
306
|
+
const declaredCrc = reader.u32be();
|
|
307
|
+
if (CRC_CHECKED.has(type)) {
|
|
308
|
+
const actual = crc32(bytes.subarray(typeAt, typeAt + 4), data);
|
|
309
|
+
if (actual !== declaredCrc) {
|
|
310
|
+
throw new MediaFormatError(`chunk ${type} at offset ${chunkAt} declares CRC ${hex8(declaredCrc)}, but its ${length} bytes check to ${hex8(actual)}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
switch (type) {
|
|
314
|
+
case "IDAT":
|
|
315
|
+
idatParts.push(data);
|
|
316
|
+
break;
|
|
317
|
+
case "PLTE":
|
|
318
|
+
palette = data.slice();
|
|
319
|
+
break;
|
|
320
|
+
case "tRNS":
|
|
321
|
+
if (colorType === 3)
|
|
322
|
+
paletteAlpha = data.slice();
|
|
323
|
+
else if (colorType === 0)
|
|
324
|
+
transparentSample = [new ByteReader(data).u16be()];
|
|
325
|
+
else if (colorType === 2) {
|
|
326
|
+
const r = new ByteReader(data);
|
|
327
|
+
transparentSample = [r.u16be(), r.u16be(), r.u16be()];
|
|
328
|
+
}
|
|
329
|
+
break;
|
|
330
|
+
case "IEND":
|
|
331
|
+
sawEnd = true;
|
|
332
|
+
break;
|
|
333
|
+
default:
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (idatParts.length === 0)
|
|
338
|
+
throw new MediaFormatError("the file carries no IDAT data");
|
|
339
|
+
if (colorType === 3 && palette === undefined) {
|
|
340
|
+
throw new MediaFormatError("a palette image with no PLTE chunk cannot be decoded");
|
|
341
|
+
}
|
|
342
|
+
const stride = width * channels;
|
|
343
|
+
const rawSize = height * (stride + 1);
|
|
344
|
+
// `maxOutputLength` makes the inflate itself refuse to exceed the size the
|
|
345
|
+
// header implies, so a compression bomb aborts mid-stream.
|
|
346
|
+
let raw;
|
|
347
|
+
try {
|
|
348
|
+
const inflated = inflateSync(concatBytes(idatParts), { maxOutputLength: rawSize });
|
|
349
|
+
raw = new Uint8Array(inflated.buffer, inflated.byteOffset, inflated.byteLength);
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
throw new MediaFormatError(`the IDAT stream did not inflate: ${err.message}`);
|
|
353
|
+
}
|
|
354
|
+
if (raw.length < rawSize) {
|
|
355
|
+
throw new MediaFormatError(`IDAT inflated to ${raw.length} bytes, short of the ${rawSize} the header implies`);
|
|
356
|
+
}
|
|
357
|
+
const out = new Uint8Array(width * height * 4);
|
|
358
|
+
const line = new Uint8Array(stride);
|
|
359
|
+
let prev = new Uint8Array(stride);
|
|
360
|
+
let hadAlpha = colorType === 4 || colorType === 6 || paletteAlpha !== undefined;
|
|
361
|
+
if (transparentSample !== undefined)
|
|
362
|
+
hadAlpha = true;
|
|
363
|
+
for (let y = 0; y < height; y++) {
|
|
364
|
+
const at = y * (stride + 1);
|
|
365
|
+
const filter = raw[at];
|
|
366
|
+
for (let x = 0; x < stride; x++) {
|
|
367
|
+
const value = raw[at + 1 + x];
|
|
368
|
+
const left = x >= channels ? line[x - channels] : 0;
|
|
369
|
+
const up = prev[x];
|
|
370
|
+
const upLeft = x >= channels ? prev[x - channels] : 0;
|
|
371
|
+
let restored;
|
|
372
|
+
switch (filter) {
|
|
373
|
+
case 0:
|
|
374
|
+
restored = value;
|
|
375
|
+
break;
|
|
376
|
+
case 1:
|
|
377
|
+
restored = value + left;
|
|
378
|
+
break;
|
|
379
|
+
case 2:
|
|
380
|
+
restored = value + up;
|
|
381
|
+
break;
|
|
382
|
+
case 3:
|
|
383
|
+
restored = value + ((left + up) >> 1);
|
|
384
|
+
break;
|
|
385
|
+
case 4:
|
|
386
|
+
restored = value + paeth(left, up, upLeft);
|
|
387
|
+
break;
|
|
388
|
+
default:
|
|
389
|
+
throw new MediaFormatError(`scanline ${y} uses filter type ${filter}, which is not defined`);
|
|
390
|
+
}
|
|
391
|
+
line[x] = restored & 0xff;
|
|
392
|
+
}
|
|
393
|
+
// Expand this scanline's samples to RGBA.
|
|
394
|
+
let o = y * width * 4;
|
|
395
|
+
for (let x = 0; x < width; x++) {
|
|
396
|
+
const s = x * channels;
|
|
397
|
+
let r;
|
|
398
|
+
let g;
|
|
399
|
+
let b;
|
|
400
|
+
let a = 255;
|
|
401
|
+
switch (colorType) {
|
|
402
|
+
case 0: {
|
|
403
|
+
r = line[s];
|
|
404
|
+
g = r;
|
|
405
|
+
b = r;
|
|
406
|
+
if (transparentSample !== undefined && r === transparentSample[0])
|
|
407
|
+
a = 0;
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
case 2: {
|
|
411
|
+
r = line[s];
|
|
412
|
+
g = line[s + 1];
|
|
413
|
+
b = line[s + 2];
|
|
414
|
+
if (transparentSample !== undefined &&
|
|
415
|
+
r === transparentSample[0] &&
|
|
416
|
+
g === transparentSample[1] &&
|
|
417
|
+
b === transparentSample[2]) {
|
|
418
|
+
a = 0;
|
|
419
|
+
}
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
case 3: {
|
|
423
|
+
const index = line[s];
|
|
424
|
+
const pal = palette;
|
|
425
|
+
if (index * 3 + 2 >= pal.length) {
|
|
426
|
+
throw new MediaFormatError(`palette index ${index} is outside the PLTE chunk`);
|
|
427
|
+
}
|
|
428
|
+
r = pal[index * 3];
|
|
429
|
+
g = pal[index * 3 + 1];
|
|
430
|
+
b = pal[index * 3 + 2];
|
|
431
|
+
if (paletteAlpha !== undefined && index < paletteAlpha.length) {
|
|
432
|
+
a = paletteAlpha[index];
|
|
433
|
+
}
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
case 4: {
|
|
437
|
+
r = line[s];
|
|
438
|
+
g = r;
|
|
439
|
+
b = r;
|
|
440
|
+
a = line[s + 1];
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
default: {
|
|
444
|
+
r = line[s];
|
|
445
|
+
g = line[s + 1];
|
|
446
|
+
b = line[s + 2];
|
|
447
|
+
a = line[s + 3];
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
out[o++] = r;
|
|
452
|
+
out[o++] = g;
|
|
453
|
+
out[o++] = b;
|
|
454
|
+
out[o++] = a;
|
|
455
|
+
}
|
|
456
|
+
prev = line.slice();
|
|
457
|
+
}
|
|
458
|
+
return {
|
|
459
|
+
width,
|
|
460
|
+
height,
|
|
461
|
+
data: out,
|
|
462
|
+
sourceColorType: colorType,
|
|
463
|
+
sourceBitDepth: bitDepth,
|
|
464
|
+
hadAlpha,
|
|
465
|
+
};
|
|
466
|
+
}
|
package/dist/lib/qr.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type EccLevel = "L" | "M" | "Q" | "H";
|
|
2
|
+
export declare const ECC_LEVELS: ReadonlyArray<EccLevel>;
|
|
3
|
+
/** Lowest and highest symbol version this encoder produces. */
|
|
4
|
+
export declare const MIN_VERSION = 1;
|
|
5
|
+
export declare const MAX_VERSION = 10;
|
|
6
|
+
/** The Reed-Solomon remainder — the error-correction codewords for a block. */
|
|
7
|
+
export declare function rsRemainder(data: Uint8Array, divisor: Uint8Array): Uint8Array;
|
|
8
|
+
/** Alignment-pattern centre coordinates for a version, ascending. */
|
|
9
|
+
export declare function alignmentPositions(version: number): number[];
|
|
10
|
+
/** Data codewords (payload plus padding) a version/level pair carries. */
|
|
11
|
+
export declare function dataCodewordCount(version: number, ecc: EccLevel): number;
|
|
12
|
+
/**
|
|
13
|
+
* How a version/level pair splits its codewords into blocks. Exported
|
|
14
|
+
* because reversing the interleave — which is what a test that reads a
|
|
15
|
+
* finished symbol back has to do — needs exactly these five numbers.
|
|
16
|
+
*/
|
|
17
|
+
export declare function blockLayout(version: number, ecc: EccLevel): {
|
|
18
|
+
rawCodewords: number;
|
|
19
|
+
numBlocks: number;
|
|
20
|
+
blockEccLen: number;
|
|
21
|
+
shortBlockLen: number;
|
|
22
|
+
numShortBlocks: number;
|
|
23
|
+
dataCodewords: number;
|
|
24
|
+
};
|
|
25
|
+
/** Remainder bits after the last codeword, which stay light. */
|
|
26
|
+
export declare function remainderBits(version: number): number;
|
|
27
|
+
/** Payload bytes that fit in this version at this level, in byte mode. */
|
|
28
|
+
export declare function byteCapacity(version: number, ecc: EccLevel): number;
|
|
29
|
+
/** The smallest version that fits `byteLength` at `ecc`, or `undefined`. */
|
|
30
|
+
export declare function smallestVersion(byteLength: number, ecc: EccLevel): number | undefined;
|
|
31
|
+
/** Mode indicator, character count, payload, terminator and pad codewords. */
|
|
32
|
+
export declare function encodeDataCodewords(payload: Uint8Array, version: number, ecc: EccLevel): Uint8Array;
|
|
33
|
+
/** Split into blocks, append each block's ECC, and interleave the result. */
|
|
34
|
+
export declare function addEccAndInterleave(data: Uint8Array, version: number, ecc: EccLevel): Uint8Array;
|
|
35
|
+
/** A finished symbol: `modules[row][col]` is true where the module is dark. */
|
|
36
|
+
export type QrSymbol = {
|
|
37
|
+
readonly version: number;
|
|
38
|
+
readonly ecc: EccLevel;
|
|
39
|
+
readonly mask: number;
|
|
40
|
+
readonly size: number;
|
|
41
|
+
readonly modules: ReadonlyArray<ReadonlyArray<boolean>>;
|
|
42
|
+
};
|
|
43
|
+
declare class Matrix {
|
|
44
|
+
readonly size: number;
|
|
45
|
+
readonly modules: boolean[][];
|
|
46
|
+
readonly reserved: boolean[][];
|
|
47
|
+
constructor(size: number);
|
|
48
|
+
setFunction(row: number, col: number, dark: boolean): void;
|
|
49
|
+
isDark(row: number, col: number): boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The zigzag codeword walk: two-module-wide columns from the right edge
|
|
53
|
+
* leftwards, alternating upward and downward, skipping the vertical timing
|
|
54
|
+
* column. Exported so a test can walk it back the other way.
|
|
55
|
+
*/
|
|
56
|
+
export declare function modulePositions(size: number): Array<[number, number]>;
|
|
57
|
+
/** Penalty score under the standard's four rules; lower is better. */
|
|
58
|
+
export declare function penaltyScore(matrix: Matrix): number;
|
|
59
|
+
export type EncodeQrOptions = {
|
|
60
|
+
readonly ecc?: EccLevel;
|
|
61
|
+
/** Force a version; otherwise the smallest that fits is chosen. */
|
|
62
|
+
readonly version?: number;
|
|
63
|
+
/** Force a mask 0-7; otherwise the lowest-penalty one is chosen. */
|
|
64
|
+
readonly mask?: number;
|
|
65
|
+
};
|
|
66
|
+
/** Encode `text` (as UTF-8) into a QR symbol. */
|
|
67
|
+
export declare function encodeQr(text: string, options?: EncodeQrOptions): QrSymbol;
|
|
68
|
+
/** Render a symbol as text, one line per row, with a quiet zone. */
|
|
69
|
+
export declare function symbolToText(symbol: QrSymbol, dark: string, light: string, quietZone: number): string;
|
|
70
|
+
export {};
|