@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.
@@ -0,0 +1,43 @@
1
+ export declare const CODE128_START_B = 104;
2
+ export declare const CODE128_START_C = 105;
3
+ export declare const CODE128_STOP = 106;
4
+ /** Every Code 128 pattern, exported so the tests can check the invariant. */
5
+ export declare function code128Widths(): ReadonlyArray<string>;
6
+ /**
7
+ * The symbol-character values for `text`: start character, data, check
8
+ * character, stop. Exported because the value sequence is what the
9
+ * check-character test asserts on.
10
+ *
11
+ * The code set is chosen once, for the whole symbol: C when the payload is
12
+ * an even number of digits and at least four long (two digits per symbol
13
+ * character, so a 13-digit-style payload halves in width), otherwise B.
14
+ * Switching sets mid-symbol would shorten a mixed payload like
15
+ * `AB12345678`; it is not implemented, and the cost is a slightly wider
16
+ * barcode, never an unreadable one.
17
+ */
18
+ export declare function code128Values(text: string): number[];
19
+ /** True when `text` will be encoded in code set C rather than B. */
20
+ export declare function code128UsesSetC(text: string): boolean;
21
+ /** Code 128 as a module string: `1` is a bar, `0` is a space. */
22
+ export declare function encodeCode128(text: string): {
23
+ modules: string;
24
+ values: number[];
25
+ };
26
+ /** The three EAN digit encodings, exported for the tests' invariants. */
27
+ export declare function eanTables(): {
28
+ L: ReadonlyArray<string>;
29
+ G: ReadonlyArray<string>;
30
+ R: ReadonlyArray<string>;
31
+ parity: ReadonlyArray<string>;
32
+ };
33
+ /** The EAN-13 check digit for twelve digits, as a number 0-9. */
34
+ export declare function ean13CheckDigit(twelve: string): number;
35
+ /**
36
+ * EAN-13 as a 95-module string. `digits` is twelve digits (the check digit
37
+ * is computed) or thirteen (the check digit is verified).
38
+ */
39
+ export declare function encodeEan13(digits: string): {
40
+ modules: string;
41
+ digits: string;
42
+ checkDigit: number;
43
+ };
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Linear barcode encoders: Code 128 and EAN-13.
3
+ *
4
+ * Both are defined as module patterns — a barcode is a string of bars and
5
+ * spaces one module wide each — so the encoders below produce a module
6
+ * string and leave rendering to the caller. That keeps the symbology logic
7
+ * testable against its own invariants rather than against a picture.
8
+ *
9
+ * ## Code 128
10
+ *
11
+ * One code set for the whole symbol: C when the payload is an even number
12
+ * of digits at least four long, otherwise B. Code set A — the one with the
13
+ * control characters and without lowercase — is never emitted, so the
14
+ * encodable range is ASCII 32 to 126, and anything outside it is refused by
15
+ * character rather than silently dropped. Mid-symbol set switching is not
16
+ * implemented; it would make a mixed alphanumeric-plus-long-digits payload
17
+ * narrower, and nothing else. The check character is the standard weighted
18
+ * modulo-103 sum.
19
+ *
20
+ * ## EAN-13
21
+ *
22
+ * Twelve digits in, a thirteenth check digit computed. The first digit is
23
+ * not encoded directly: it selects the odd/even parity pattern of the six
24
+ * left-hand digits, which is how a scanner recovers it. UPC-A is EAN-13
25
+ * with a leading zero, so passing `0` plus eleven digits produces a
26
+ * scannable UPC-A. EAN-8, EAN-5 and EAN-2 add-ons are not implemented.
27
+ */
28
+ import { MediaFormatError } from "./bytes";
29
+ // --- Code 128 ------------------------------------------------------------
30
+ /**
31
+ * Bar/space run widths for each of the 107 symbol characters, bar first.
32
+ * Each entry totals 11 modules; the stop pattern totals 13. The invariant
33
+ * is asserted in the unit tests, which is the cheapest guard against a
34
+ * transcription slip in a table this shape.
35
+ */
36
+ const CODE128_WIDTHS = [
37
+ "212222",
38
+ "222122",
39
+ "222221",
40
+ "121223",
41
+ "121322",
42
+ "131222",
43
+ "122213",
44
+ "122312",
45
+ "132212",
46
+ "221213",
47
+ "221312",
48
+ "231212",
49
+ "112232",
50
+ "122132",
51
+ "122231",
52
+ "113222",
53
+ "123122",
54
+ "123221",
55
+ "223211",
56
+ "221132",
57
+ "221231",
58
+ "213212",
59
+ "223112",
60
+ "312131",
61
+ "311222",
62
+ "321122",
63
+ "321221",
64
+ "312212",
65
+ "322112",
66
+ "322211",
67
+ "212123",
68
+ "212321",
69
+ "232121",
70
+ "111323",
71
+ "131123",
72
+ "131321",
73
+ "112313",
74
+ "132113",
75
+ "132311",
76
+ "211313",
77
+ "231113",
78
+ "231311",
79
+ "112133",
80
+ "112331",
81
+ "132131",
82
+ "113123",
83
+ "113321",
84
+ "133121",
85
+ "313121",
86
+ "211331",
87
+ "231131",
88
+ "213113",
89
+ "213311",
90
+ "213131",
91
+ "311123",
92
+ "311321",
93
+ "331121",
94
+ "312113",
95
+ "312311",
96
+ "332111",
97
+ "314111",
98
+ "221411",
99
+ "431111",
100
+ "111224",
101
+ "111422",
102
+ "121124",
103
+ "121421",
104
+ "141122",
105
+ "141221",
106
+ "112214",
107
+ "112412",
108
+ "122114",
109
+ "122411",
110
+ "142112",
111
+ "142211",
112
+ "241211",
113
+ "221114",
114
+ "413111",
115
+ "241112",
116
+ "134111",
117
+ "111242",
118
+ "121142",
119
+ "121241",
120
+ "114212",
121
+ "124112",
122
+ "124211",
123
+ "411212",
124
+ "421112",
125
+ "421211",
126
+ "212141",
127
+ "214121",
128
+ "412121",
129
+ "111143",
130
+ "111341",
131
+ "131141",
132
+ "114113",
133
+ "114311",
134
+ "411113",
135
+ "411311",
136
+ "113141",
137
+ "114131",
138
+ "311141",
139
+ "411131",
140
+ "211412",
141
+ "211214",
142
+ "211232",
143
+ "2331112",
144
+ ];
145
+ export const CODE128_START_B = 104;
146
+ export const CODE128_START_C = 105;
147
+ export const CODE128_STOP = 106;
148
+ /** Every Code 128 pattern, exported so the tests can check the invariant. */
149
+ export function code128Widths() {
150
+ return CODE128_WIDTHS;
151
+ }
152
+ /** A symbol character's widths expanded to modules: `1` bar, `0` space. */
153
+ function widthsToModules(widths) {
154
+ let out = "";
155
+ for (let i = 0; i < widths.length; i++) {
156
+ const run = Number.parseInt(widths[i], 10);
157
+ out += (i % 2 === 0 ? "1" : "0").repeat(run);
158
+ }
159
+ return out;
160
+ }
161
+ /**
162
+ * The symbol-character values for `text`: start character, data, check
163
+ * character, stop. Exported because the value sequence is what the
164
+ * check-character test asserts on.
165
+ *
166
+ * The code set is chosen once, for the whole symbol: C when the payload is
167
+ * an even number of digits and at least four long (two digits per symbol
168
+ * character, so a 13-digit-style payload halves in width), otherwise B.
169
+ * Switching sets mid-symbol would shorten a mixed payload like
170
+ * `AB12345678`; it is not implemented, and the cost is a slightly wider
171
+ * barcode, never an unreadable one.
172
+ */
173
+ export function code128Values(text) {
174
+ if (text.length === 0)
175
+ throw new MediaFormatError("Code 128 needs at least one character");
176
+ for (let i = 0; i < text.length; i++) {
177
+ const code = text.charCodeAt(i);
178
+ if (code < 32 || code > 126) {
179
+ throw new MediaFormatError(`character ${i} (U+${code.toString(16).toUpperCase().padStart(4, "0")}) is outside the ASCII 32-126 range code sets B and C cover`);
180
+ }
181
+ }
182
+ const useC = text.length >= 4 && text.length % 2 === 0 && /^[0-9]+$/.test(text);
183
+ const values = [useC ? CODE128_START_C : CODE128_START_B];
184
+ if (useC) {
185
+ for (let i = 0; i < text.length; i += 2) {
186
+ values.push(Number.parseInt(text.slice(i, i + 2), 10));
187
+ }
188
+ }
189
+ else {
190
+ for (let i = 0; i < text.length; i++)
191
+ values.push(text.charCodeAt(i) - 32);
192
+ }
193
+ // Weighted modulo-103 check character: the start value plus each
194
+ // subsequent value times its one-based position.
195
+ let sum = values[0];
196
+ for (let i = 1; i < values.length; i++)
197
+ sum += values[i] * i;
198
+ values.push(sum % 103);
199
+ values.push(CODE128_STOP);
200
+ return values;
201
+ }
202
+ /** True when `text` will be encoded in code set C rather than B. */
203
+ export function code128UsesSetC(text) {
204
+ return text.length >= 4 && text.length % 2 === 0 && /^[0-9]+$/.test(text);
205
+ }
206
+ /** Code 128 as a module string: `1` is a bar, `0` is a space. */
207
+ export function encodeCode128(text) {
208
+ const values = code128Values(text);
209
+ let modules = "";
210
+ for (const value of values) {
211
+ const widths = CODE128_WIDTHS[value];
212
+ if (widths === undefined)
213
+ throw new MediaFormatError(`no Code 128 pattern for value ${value}`);
214
+ modules += widthsToModules(widths);
215
+ }
216
+ return { modules, values };
217
+ }
218
+ // --- EAN-13 --------------------------------------------------------------
219
+ const EAN_L = [
220
+ "0001101",
221
+ "0011001",
222
+ "0010011",
223
+ "0111101",
224
+ "0100011",
225
+ "0110001",
226
+ "0101111",
227
+ "0111011",
228
+ "0110111",
229
+ "0001011",
230
+ ];
231
+ const EAN_G = [
232
+ "0100111",
233
+ "0110011",
234
+ "0011011",
235
+ "0100001",
236
+ "0011101",
237
+ "0111001",
238
+ "0000101",
239
+ "0010001",
240
+ "0001001",
241
+ "0010111",
242
+ ];
243
+ const EAN_R = [
244
+ "1110010",
245
+ "1100110",
246
+ "1101100",
247
+ "1000010",
248
+ "1011100",
249
+ "1001110",
250
+ "1010000",
251
+ "1000100",
252
+ "1001000",
253
+ "1110100",
254
+ ];
255
+ /** Parity of the six left-hand digits, selected by the first digit. */
256
+ const EAN_PARITY = [
257
+ "LLLLLL",
258
+ "LLGLGG",
259
+ "LLGGLG",
260
+ "LLGGGL",
261
+ "LGLLGG",
262
+ "LGGLLG",
263
+ "LGGGLL",
264
+ "LGLGLG",
265
+ "LGLGGL",
266
+ "LGGLGL",
267
+ ];
268
+ /** The three EAN digit encodings, exported for the tests' invariants. */
269
+ export function eanTables() {
270
+ return { L: EAN_L, G: EAN_G, R: EAN_R, parity: EAN_PARITY };
271
+ }
272
+ /** The EAN-13 check digit for twelve digits, as a number 0-9. */
273
+ export function ean13CheckDigit(twelve) {
274
+ if (!/^[0-9]{12}$/.test(twelve)) {
275
+ throw new MediaFormatError(`"${twelve}" is not twelve digits`);
276
+ }
277
+ let sum = 0;
278
+ for (let i = 0; i < 12; i++) {
279
+ sum += Number.parseInt(twelve[i], 10) * (i % 2 === 0 ? 1 : 3);
280
+ }
281
+ return (10 - (sum % 10)) % 10;
282
+ }
283
+ /**
284
+ * EAN-13 as a 95-module string. `digits` is twelve digits (the check digit
285
+ * is computed) or thirteen (the check digit is verified).
286
+ */
287
+ export function encodeEan13(digits) {
288
+ const cleaned = digits.replace(/[\s-]/g, "");
289
+ if (!/^[0-9]{12,13}$/.test(cleaned)) {
290
+ throw new MediaFormatError(`EAN-13 takes twelve digits (check digit computed) or thirteen (check digit verified); got "${digits}"`);
291
+ }
292
+ const body = cleaned.slice(0, 12);
293
+ const checkDigit = ean13CheckDigit(body);
294
+ if (cleaned.length === 13 && Number.parseInt(cleaned[12], 10) !== checkDigit) {
295
+ throw new MediaFormatError(`"${cleaned}" has check digit ${cleaned[12]}, but ${body} checks to ${checkDigit}`);
296
+ }
297
+ const full = body + String(checkDigit);
298
+ const parity = EAN_PARITY[Number.parseInt(full[0], 10)];
299
+ let modules = "101"; // left guard
300
+ for (let i = 1; i <= 6; i++) {
301
+ const digit = Number.parseInt(full[i], 10);
302
+ modules += (parity[i - 1] === "L" ? EAN_L : EAN_G)[digit];
303
+ }
304
+ modules += "01010"; // centre guard
305
+ for (let i = 7; i <= 12; i++) {
306
+ modules += EAN_R[Number.parseInt(full[i], 10)];
307
+ }
308
+ modules += "101"; // right guard
309
+ return { modules, digits: full, checkDigit };
310
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Byte-level primitives shared by every format reader in this package.
3
+ *
4
+ * Two rules hold everywhere below. First, a read that runs off the end of
5
+ * the buffer throws `MediaFormatError` rather than returning a silently
6
+ * wrong number — a truncated file must be reported as truncated, never
7
+ * decoded as if the missing bytes were zeroes. Second, nothing here
8
+ * allocates on behalf of a length the file itself supplied without that
9
+ * length having been checked against a cap first.
10
+ */
11
+ /** Raised when bytes do not say what the format says they should. */
12
+ export declare class MediaFormatError extends Error {
13
+ constructor(message: string);
14
+ }
15
+ /** A bounds-checked cursor over a byte buffer, big- or little-endian. */
16
+ export declare class ByteReader {
17
+ readonly bytes: Uint8Array;
18
+ private pos;
19
+ constructor(bytes: Uint8Array);
20
+ get offset(): number;
21
+ get remaining(): number;
22
+ seek(offset: number): void;
23
+ skip(count: number): void;
24
+ private need;
25
+ u8(): number;
26
+ u16be(): number;
27
+ u16le(): number;
28
+ u32be(): number;
29
+ u32le(): number;
30
+ i32le(): number;
31
+ /** `count` bytes as a subarray of the SAME backing store — no copy. */
32
+ take(count: number): Uint8Array;
33
+ /** ASCII of the next `count` bytes; non-ASCII becomes `?`. */
34
+ ascii(count: number): string;
35
+ }
36
+ /** True when `bytes` begins with `prefix`. */
37
+ export declare function startsWith(bytes: Uint8Array, prefix: ReadonlyArray<number>): boolean;
38
+ /** True when the ASCII of `bytes[at..]` equals `text`. */
39
+ export declare function asciiAt(bytes: Uint8Array, at: number, text: string): boolean;
40
+ /** Uppercase hex of a byte run, for hashes and magic-byte reporting. */
41
+ export declare function toHex(bytes: Uint8Array): string;
42
+ /** CRC-32/ISO-HDLC, as PNG chunks and ZIP entries both use it. */
43
+ export declare function crc32(...parts: ReadonlyArray<Uint8Array>): number;
44
+ /** Concatenate byte runs into one fresh buffer. */
45
+ export declare function concatBytes(parts: ReadonlyArray<Uint8Array>): Uint8Array;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Byte-level primitives shared by every format reader in this package.
3
+ *
4
+ * Two rules hold everywhere below. First, a read that runs off the end of
5
+ * the buffer throws `MediaFormatError` rather than returning a silently
6
+ * wrong number — a truncated file must be reported as truncated, never
7
+ * decoded as if the missing bytes were zeroes. Second, nothing here
8
+ * allocates on behalf of a length the file itself supplied without that
9
+ * length having been checked against a cap first.
10
+ */
11
+ /** Raised when bytes do not say what the format says they should. */
12
+ export class MediaFormatError extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = "MediaFormatError";
16
+ }
17
+ }
18
+ /** A bounds-checked cursor over a byte buffer, big- or little-endian. */
19
+ export class ByteReader {
20
+ bytes;
21
+ pos = 0;
22
+ constructor(bytes) {
23
+ this.bytes = bytes;
24
+ }
25
+ get offset() {
26
+ return this.pos;
27
+ }
28
+ get remaining() {
29
+ return this.bytes.length - this.pos;
30
+ }
31
+ seek(offset) {
32
+ if (offset < 0 || offset > this.bytes.length) {
33
+ throw new MediaFormatError(`seek to ${offset} is outside the ${this.bytes.length}-byte buffer`);
34
+ }
35
+ this.pos = offset;
36
+ }
37
+ skip(count) {
38
+ this.seek(this.pos + count);
39
+ }
40
+ need(count) {
41
+ if (this.pos + count > this.bytes.length) {
42
+ throw new MediaFormatError(`truncated: wanted ${count} byte(s) at offset ${this.pos}, only ${this.remaining} left`);
43
+ }
44
+ }
45
+ u8() {
46
+ this.need(1);
47
+ const v = this.bytes[this.pos];
48
+ this.pos += 1;
49
+ return v;
50
+ }
51
+ u16be() {
52
+ this.need(2);
53
+ const v = (this.bytes[this.pos] << 8) | this.bytes[this.pos + 1];
54
+ this.pos += 2;
55
+ return v;
56
+ }
57
+ u16le() {
58
+ this.need(2);
59
+ const v = this.bytes[this.pos] | (this.bytes[this.pos + 1] << 8);
60
+ this.pos += 2;
61
+ return v;
62
+ }
63
+ u32be() {
64
+ this.need(4);
65
+ const b = this.bytes;
66
+ const v = b[this.pos] * 0x1000000 +
67
+ ((b[this.pos + 1] << 16) |
68
+ (b[this.pos + 2] << 8) |
69
+ b[this.pos + 3]);
70
+ this.pos += 4;
71
+ return v;
72
+ }
73
+ u32le() {
74
+ this.need(4);
75
+ const b = this.bytes;
76
+ const low = b[this.pos] |
77
+ (b[this.pos + 1] << 8) |
78
+ (b[this.pos + 2] << 16);
79
+ const v = (low >>> 0) + b[this.pos + 3] * 0x1000000;
80
+ this.pos += 4;
81
+ return v;
82
+ }
83
+ i32le() {
84
+ const v = this.u32le();
85
+ return v >= 0x80000000 ? v - 0x100000000 : v;
86
+ }
87
+ /** `count` bytes as a subarray of the SAME backing store — no copy. */
88
+ take(count) {
89
+ this.need(count);
90
+ const out = this.bytes.subarray(this.pos, this.pos + count);
91
+ this.pos += count;
92
+ return out;
93
+ }
94
+ /** ASCII of the next `count` bytes; non-ASCII becomes `?`. */
95
+ ascii(count) {
96
+ const raw = this.take(count);
97
+ let out = "";
98
+ for (const byte of raw)
99
+ out += byte >= 0x20 && byte < 0x7f ? String.fromCharCode(byte) : "?";
100
+ return out;
101
+ }
102
+ }
103
+ /** True when `bytes` begins with `prefix`. */
104
+ export function startsWith(bytes, prefix) {
105
+ if (bytes.length < prefix.length)
106
+ return false;
107
+ for (let i = 0; i < prefix.length; i++) {
108
+ if (bytes[i] !== prefix[i])
109
+ return false;
110
+ }
111
+ return true;
112
+ }
113
+ /** True when the ASCII of `bytes[at..]` equals `text`. */
114
+ export function asciiAt(bytes, at, text) {
115
+ if (at + text.length > bytes.length)
116
+ return false;
117
+ for (let i = 0; i < text.length; i++) {
118
+ if (bytes[at + i] !== text.charCodeAt(i))
119
+ return false;
120
+ }
121
+ return true;
122
+ }
123
+ /** Uppercase hex of a byte run, for hashes and magic-byte reporting. */
124
+ export function toHex(bytes) {
125
+ let out = "";
126
+ for (const byte of bytes)
127
+ out += byte.toString(16).padStart(2, "0");
128
+ return out;
129
+ }
130
+ const CRC_TABLE = (() => {
131
+ const table = new Uint32Array(256);
132
+ for (let n = 0; n < 256; n++) {
133
+ let c = n;
134
+ for (let k = 0; k < 8; k++)
135
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
136
+ table[n] = c >>> 0;
137
+ }
138
+ return table;
139
+ })();
140
+ /** CRC-32/ISO-HDLC, as PNG chunks and ZIP entries both use it. */
141
+ export function crc32(...parts) {
142
+ let c = 0xffffffff;
143
+ for (const part of parts) {
144
+ for (const byte of part) {
145
+ c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8);
146
+ }
147
+ }
148
+ return (c ^ 0xffffffff) >>> 0;
149
+ }
150
+ /** Concatenate byte runs into one fresh buffer. */
151
+ export function concatBytes(parts) {
152
+ let total = 0;
153
+ for (const part of parts)
154
+ total += part.length;
155
+ const out = new Uint8Array(total);
156
+ let at = 0;
157
+ for (const part of parts) {
158
+ out.set(part, at);
159
+ at += part.length;
160
+ }
161
+ return out;
162
+ }
@@ -0,0 +1,32 @@
1
+ export type ChartType = "bar" | "line" | "scatter" | "pie";
2
+ export type ChartSeries = {
3
+ readonly name: string;
4
+ /** Bar charts: one value per category. */
5
+ readonly values?: ReadonlyArray<number>;
6
+ /** Line and scatter charts: `[x, y]` pairs. */
7
+ readonly points?: ReadonlyArray<readonly [number, number]>;
8
+ };
9
+ export type ChartSlice = {
10
+ readonly label: string;
11
+ readonly value: number;
12
+ };
13
+ export type ChartSpec = {
14
+ readonly type: ChartType;
15
+ readonly title?: string;
16
+ readonly width: number;
17
+ readonly height: number;
18
+ readonly categories?: ReadonlyArray<string>;
19
+ readonly series?: ReadonlyArray<ChartSeries>;
20
+ readonly slices?: ReadonlyArray<ChartSlice>;
21
+ readonly xLabel?: string;
22
+ readonly yLabel?: string;
23
+ readonly colors?: ReadonlyArray<string>;
24
+ readonly background?: string;
25
+ readonly legend?: boolean;
26
+ /** Bar charts only: stack the series instead of grouping them. */
27
+ readonly stacked?: boolean;
28
+ };
29
+ /** The font stack the renderers name, re-exported for the tests. */
30
+ export declare const CHART_FONT_STACK = "ui-sans-serif, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif";
31
+ /** Render a chart spec to a complete SVG document. */
32
+ export declare function renderChart(spec: ChartSpec): string;