specqr 1.0.0-rc.1

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,666 @@
1
+ import { BitBuffer } from "./bit-buffer.js";
2
+ import { getCharacterCountBitLength, getDataCodewordCount } from "../core/tables.js";
3
+ import {
4
+ DataTooLongError,
5
+ InvalidEciError,
6
+ InvalidGs1Error,
7
+ InvalidInputError,
8
+ InvalidModeError
9
+ } from "../errors.js";
10
+ import {
11
+ assertKanjiModeText,
12
+ canEncodeKanjiModeCharacter,
13
+ getKanjiModeByteCount,
14
+ getKanjiModeValue
15
+ } from "./shift-jis.js";
16
+
17
+ const MODE_INDICATORS = {
18
+ eci: 0b0111,
19
+ fnc1: 0b0101,
20
+ numeric: 0b0001,
21
+ alphanumeric: 0b0010,
22
+ byte: 0b0100,
23
+ kanji: 0b1000
24
+ };
25
+
26
+ const ALPHANUMERIC_CHARSET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
27
+ const ALPHANUMERIC_MAP = new Map(
28
+ Array.from(ALPHANUMERIC_CHARSET, (character, index) => [character, index])
29
+ );
30
+ const PAD_CODEWORDS = [0xEC, 0x11];
31
+ const MODES = ["numeric", "alphanumeric", "kanji", "byte"];
32
+
33
+ export function encodeUtf8(text) {
34
+ if (typeof text !== "string") {
35
+ throw new InvalidInputError(`QR input must be a string; got ${typeof text}`);
36
+ }
37
+
38
+ return Array.from(new TextEncoder().encode(text));
39
+ }
40
+
41
+ export function isBinaryInput(value) {
42
+ return Array.isArray(value) ||
43
+ value instanceof Uint8Array ||
44
+ value instanceof ArrayBuffer ||
45
+ ArrayBuffer.isView(value);
46
+ }
47
+
48
+ export function toByteArray(value, label = "input") {
49
+ if (Array.isArray(value)) {
50
+ return validateByteValues(value, label);
51
+ }
52
+ if (value instanceof Uint8Array) {
53
+ return Array.from(value);
54
+ }
55
+ if (value instanceof ArrayBuffer) {
56
+ return Array.from(new Uint8Array(value));
57
+ }
58
+ if (ArrayBuffer.isView(value)) {
59
+ return Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
60
+ }
61
+
62
+ throw new InvalidInputError(
63
+ `${label} must be a string, Uint8Array, ArrayBuffer, or ArrayBuffer view`
64
+ );
65
+ }
66
+
67
+ export function selectEncodingMode(text, requestedMode) {
68
+ assertText(text);
69
+
70
+ if (requestedMode !== "auto") {
71
+ validateTextForMode(text, requestedMode);
72
+ return requestedMode;
73
+ }
74
+
75
+ if (text.length > 0 && isNumeric(text)) {
76
+ return "numeric";
77
+ }
78
+ if (text.length > 0 && isAlphanumeric(text)) {
79
+ return "alphanumeric";
80
+ }
81
+ if (text.length > 0 && isKanji(text)) {
82
+ return "kanji";
83
+ }
84
+ return "byte";
85
+ }
86
+
87
+ export function getModeBitLength(text, mode, version) {
88
+ validateTextForMode(text, mode);
89
+ return getSegmentBitLength({ mode, text }, version);
90
+ }
91
+
92
+ export function encodeMode(text, mode, version, errorCorrectionLevel) {
93
+ validateTextForMode(text, mode);
94
+ return encodeSegments([{ mode, text }], version, errorCorrectionLevel);
95
+ }
96
+
97
+ export function createSegments(input, requestedMode, version, optimizeSegments = true, eciAssignmentNumber = false) {
98
+ if (isBinaryInput(input)) {
99
+ if (requestedMode !== "auto" && requestedMode !== "byte") {
100
+ throw new InvalidModeError(`Binary input can only be encoded in byte mode; got ${requestedMode}`);
101
+ }
102
+ return prependEciSegment([{ mode: "byte", bytes: toByteArray(input) }], eciAssignmentNumber);
103
+ }
104
+
105
+ assertText(input);
106
+
107
+ let segments;
108
+ if (requestedMode !== "auto") {
109
+ validateTextForMode(input, requestedMode);
110
+ segments = [{ mode: requestedMode, text: input }];
111
+ } else if (!optimizeSegments) {
112
+ const selectedMode = selectEncodingMode(input, "auto");
113
+ const mode = eciAssignmentNumber !== false && selectedMode === "kanji" ? "byte" : selectedMode;
114
+ segments = [{ mode, text: input }];
115
+ } else {
116
+ segments = optimizeSegmentModes(input, version, eciAssignmentNumber === false);
117
+ }
118
+
119
+ return prependEciSegment(segments, eciAssignmentNumber);
120
+ }
121
+
122
+ export function normalizeManualSegments(segments) {
123
+ if (!Array.isArray(segments)) {
124
+ throw new InvalidInputError("manual segments must be an array");
125
+ }
126
+
127
+ return validateControlSegments(segments.map((segment, index) => normalizeManualSegment(segment, index)));
128
+ }
129
+
130
+ export function prependEciSegment(segments, eciAssignmentNumber = false) {
131
+ if (eciAssignmentNumber !== false) {
132
+ if (segments.some((segment) => segment.mode === "fnc1")) {
133
+ throw new InvalidGs1Error("eci cannot be combined with FNC1 first position in this implementation");
134
+ }
135
+ validateEciAssignmentNumber(eciAssignmentNumber);
136
+ return [{ mode: "eci", assignmentNumber: eciAssignmentNumber }, ...segments];
137
+ }
138
+ return segments;
139
+ }
140
+
141
+ export function prependFnc1Segment(segments, enabled = false) {
142
+ if (!enabled) {
143
+ return segments;
144
+ }
145
+ if (segments.some((segment) => segment.mode === "fnc1")) {
146
+ throw new InvalidGs1Error("gs1 option cannot be combined with a manual fnc1 segment");
147
+ }
148
+ if (segments.some((segment) => segment.mode === "eci")) {
149
+ throw new InvalidGs1Error("gs1 and eci cannot be combined in this FNC1 first position implementation");
150
+ }
151
+ return [{ mode: "fnc1" }, ...segments];
152
+ }
153
+
154
+ export function getSegmentsBitLength(segments, version) {
155
+ return segments.reduce((total, segment) => total + getSegmentBitLength(segment, version), 0);
156
+ }
157
+
158
+ export function getSegmentByteCount(segment) {
159
+ validateSegment(segment);
160
+ if (segment.mode === "eci" || segment.mode === "fnc1") {
161
+ return 0;
162
+ }
163
+ if (segment.mode === "byte") {
164
+ return getByteValues(segment).length;
165
+ }
166
+ if (segment.mode === "kanji") {
167
+ return getKanjiModeByteCount(segment.text);
168
+ }
169
+ return encodeUtf8(segment.text).length;
170
+ }
171
+
172
+ export function getSegmentTextCharacterCount(segment) {
173
+ validateSegment(segment);
174
+ if (segment.mode === "eci" || segment.mode === "fnc1" || segment.bytes !== undefined) {
175
+ return 0;
176
+ }
177
+ return Array.from(segment.text).length;
178
+ }
179
+
180
+ export function encodeSegments(segments, version, errorCorrectionLevel) {
181
+ for (const segment of segments) {
182
+ validateSegment(segment);
183
+ }
184
+
185
+ const dataCodewords = getDataCodewordCount(version, errorCorrectionLevel);
186
+ const capacityBits = dataCodewords * 8;
187
+ const buffer = new BitBuffer();
188
+
189
+ for (const segment of segments) {
190
+ if (segment.mode === "fnc1") {
191
+ buffer.append(MODE_INDICATORS.fnc1, 4);
192
+ continue;
193
+ }
194
+
195
+ if (segment.mode === "eci") {
196
+ buffer.append(MODE_INDICATORS.eci, 4);
197
+ appendEciDesignatorBits(buffer, segment.assignmentNumber);
198
+ continue;
199
+ }
200
+
201
+ const count = getCharacterCount(segment);
202
+ const countBitLength = getCharacterCountBitLength(version, segment.mode);
203
+
204
+ if (count >= 2 ** countBitLength) {
205
+ throw new DataTooLongError(`Input has ${count} ${segment.mode} characters, too many for version ${version}`);
206
+ }
207
+
208
+ buffer.append(MODE_INDICATORS[segment.mode], 4);
209
+ buffer.append(count, countBitLength);
210
+ appendPayloadBits(buffer, segment);
211
+ }
212
+
213
+ if (buffer.length > capacityBits) {
214
+ throw new DataTooLongError(
215
+ `Input requires ${buffer.length} bits, but version ${version}-${errorCorrectionLevel} has ${capacityBits} data bits`
216
+ );
217
+ }
218
+
219
+ const terminatorLength = Math.min(4, capacityBits - buffer.length);
220
+ buffer.append(0, terminatorLength);
221
+
222
+ while (buffer.length % 8 !== 0) {
223
+ buffer.append(0, 1);
224
+ }
225
+
226
+ for (let i = 0; buffer.length < capacityBits; i += 1) {
227
+ buffer.append(PAD_CODEWORDS[i % 2], 8);
228
+ }
229
+
230
+ return buffer.toBytes(dataCodewords);
231
+ }
232
+
233
+ export function isNumeric(text) {
234
+ return /^[0-9]*$/.test(text);
235
+ }
236
+
237
+ export function isAlphanumeric(text) {
238
+ return /^[A-Z0-9 $%*+\-./:]*$/.test(text);
239
+ }
240
+
241
+ export function isKanji(text) {
242
+ return Array.from(text).every((character) => canEncodeKanjiModeCharacter(character));
243
+ }
244
+
245
+ function getPayloadBitLength(segment) {
246
+ switch (segment.mode) {
247
+ case "numeric": {
248
+ const text = getTextPayload(segment);
249
+ const groups = Math.floor(text.length / 3);
250
+ const remainder = text.length % 3;
251
+ return groups * 10 + (remainder === 1 ? 4 : remainder === 2 ? 7 : 0);
252
+ }
253
+ case "alphanumeric": {
254
+ const text = getTextPayload(segment);
255
+ return Math.floor(text.length / 2) * 11 + (text.length % 2) * 6;
256
+ }
257
+ case "byte":
258
+ return getByteValues(segment).length * 8;
259
+ case "kanji":
260
+ return Array.from(getTextPayload(segment)).length * 13;
261
+ default:
262
+ throw new InvalidModeError(`Unsupported mode: ${segment.mode}`);
263
+ }
264
+ }
265
+
266
+ function getSegmentBitLength(segment, version) {
267
+ if (segment.mode === "fnc1") {
268
+ validateSegment(segment);
269
+ return 4;
270
+ }
271
+
272
+ if (segment.mode === "eci") {
273
+ validateEciAssignmentNumber(segment.assignmentNumber);
274
+ return 4 + getEciDesignatorBitLength(segment.assignmentNumber);
275
+ }
276
+
277
+ validateSegment(segment);
278
+ return 4 +
279
+ getCharacterCountBitLength(version, segment.mode) +
280
+ getPayloadBitLength(segment);
281
+ }
282
+
283
+ function optimizeSegmentModes(text, version, allowKanji = true) {
284
+ const characters = Array.from(text);
285
+ if (characters.length === 0) {
286
+ return [{ mode: "byte", text: "" }];
287
+ }
288
+
289
+ const modes = allowKanji ? MODES : MODES.filter((mode) => mode !== "kanji");
290
+ const layers = Array.from({ length: characters.length + 1 }, () => new Map());
291
+ layers[0].set("start", {
292
+ cost: 0,
293
+ segmentCount: 0,
294
+ mode: null,
295
+ mod: 0,
296
+ prevKey: null
297
+ });
298
+
299
+ for (let index = 0; index < characters.length; index += 1) {
300
+ for (const [key, state] of layers[index]) {
301
+ for (const mode of modes) {
302
+ const character = characters[index];
303
+ if (!canEncodeCharacter(character, mode)) {
304
+ continue;
305
+ }
306
+
307
+ const sameSegment = state.mode === mode;
308
+ const payloadBits = sameSegment
309
+ ? getIncrementalPayloadBits(character, mode, state.mod)
310
+ : getIncrementalPayloadBits(character, mode, 0);
311
+ const overheadBits = sameSegment ? 0 : 4 + getCharacterCountBitLength(version, mode);
312
+ const nextMod = getNextMod(character, mode, sameSegment ? state.mod : 0);
313
+ const nextKey = `${mode}:${nextMod}`;
314
+ const candidate = {
315
+ cost: state.cost + overheadBits + payloadBits,
316
+ segmentCount: state.segmentCount + (sameSegment ? 0 : 1),
317
+ mode,
318
+ mod: nextMod,
319
+ prevKey: key
320
+ };
321
+
322
+ const current = layers[index + 1].get(nextKey);
323
+ if (!current || isBetterState(candidate, current)) {
324
+ layers[index + 1].set(nextKey, candidate);
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ let bestKey = null;
331
+ let bestState = null;
332
+ for (const [key, state] of layers[characters.length]) {
333
+ if (!bestState || isBetterState(state, bestState)) {
334
+ bestKey = key;
335
+ bestState = state;
336
+ }
337
+ }
338
+
339
+ if (!bestState) {
340
+ throw new Error("Unable to encode input text");
341
+ }
342
+
343
+ const assignments = new Array(characters.length);
344
+ for (let index = characters.length, key = bestKey; index > 0; index -= 1) {
345
+ const state = layers[index].get(key);
346
+ assignments[index - 1] = state.mode;
347
+ key = state.prevKey;
348
+ }
349
+
350
+ return coalesceAssignments(characters, assignments);
351
+ }
352
+
353
+ function isBetterState(candidate, current) {
354
+ return candidate.cost < current.cost ||
355
+ (candidate.cost === current.cost && candidate.segmentCount < current.segmentCount);
356
+ }
357
+
358
+ function coalesceAssignments(characters, assignments) {
359
+ const segments = [];
360
+ for (let index = 0; index < characters.length; index += 1) {
361
+ const mode = assignments[index];
362
+ const character = characters[index];
363
+ const last = segments.at(-1);
364
+ if (last?.mode === mode) {
365
+ last.text += character;
366
+ } else {
367
+ segments.push({ mode, text: character });
368
+ }
369
+ }
370
+ return segments;
371
+ }
372
+
373
+ function canEncodeCharacter(character, mode) {
374
+ switch (mode) {
375
+ case "numeric":
376
+ return /^[0-9]$/.test(character);
377
+ case "alphanumeric":
378
+ return ALPHANUMERIC_MAP.has(character);
379
+ case "kanji":
380
+ return canEncodeKanjiModeCharacter(character);
381
+ case "byte":
382
+ return true;
383
+ default:
384
+ throw new InvalidModeError(`Unsupported mode: ${mode}`);
385
+ }
386
+ }
387
+
388
+ function getIncrementalPayloadBits(character, mode, currentMod) {
389
+ switch (mode) {
390
+ case "numeric":
391
+ return currentMod === 0 ? 4 : 3;
392
+ case "alphanumeric":
393
+ return currentMod === 0 ? 6 : 5;
394
+ case "kanji":
395
+ return 13;
396
+ case "byte":
397
+ return encodeUtf8(character).length * 8;
398
+ default:
399
+ throw new InvalidModeError(`Unsupported mode: ${mode}`);
400
+ }
401
+ }
402
+
403
+ function getNextMod(character, mode, currentMod) {
404
+ switch (mode) {
405
+ case "numeric":
406
+ return (currentMod + 1) % 3;
407
+ case "alphanumeric":
408
+ return (currentMod + 1) % 2;
409
+ case "kanji":
410
+ case "byte":
411
+ return 0;
412
+ default:
413
+ throw new InvalidModeError(`Unsupported mode: ${mode}`);
414
+ }
415
+ }
416
+
417
+ function appendPayloadBits(buffer, segment) {
418
+ switch (segment.mode) {
419
+ case "numeric":
420
+ appendNumericBits(buffer, getTextPayload(segment));
421
+ break;
422
+ case "alphanumeric":
423
+ appendAlphanumericBits(buffer, getTextPayload(segment));
424
+ break;
425
+ case "byte":
426
+ for (const byte of getByteValues(segment)) {
427
+ buffer.append(byte, 8);
428
+ }
429
+ break;
430
+ case "kanji":
431
+ appendKanjiBits(buffer, getTextPayload(segment));
432
+ break;
433
+ default:
434
+ throw new InvalidModeError(`Unsupported mode: ${segment.mode}`);
435
+ }
436
+ }
437
+
438
+ function appendNumericBits(buffer, text) {
439
+ for (let i = 0; i < text.length; i += 3) {
440
+ const chunk = text.slice(i, i + 3);
441
+ const value = Number.parseInt(chunk, 10);
442
+ const bitLength = chunk.length === 3 ? 10 : chunk.length === 2 ? 7 : 4;
443
+ buffer.append(value, bitLength);
444
+ }
445
+ }
446
+
447
+ function appendAlphanumericBits(buffer, text) {
448
+ let index = 0;
449
+ for (; index + 1 < text.length; index += 2) {
450
+ const value = ALPHANUMERIC_MAP.get(text[index]) * 45 + ALPHANUMERIC_MAP.get(text[index + 1]);
451
+ buffer.append(value, 11);
452
+ }
453
+
454
+ if (index < text.length) {
455
+ buffer.append(ALPHANUMERIC_MAP.get(text[index]), 6);
456
+ }
457
+ }
458
+
459
+ function appendKanjiBits(buffer, text) {
460
+ for (const character of Array.from(text)) {
461
+ buffer.append(getKanjiModeValue(character), 13);
462
+ }
463
+ }
464
+
465
+ function getCharacterCount(segment) {
466
+ return segment.mode === "byte"
467
+ ? getByteValues(segment).length
468
+ : Array.from(getTextPayload(segment)).length;
469
+ }
470
+
471
+ function validateTextForMode(text, mode) {
472
+ assertText(text);
473
+
474
+ switch (mode) {
475
+ case "numeric":
476
+ if (!isNumeric(text)) {
477
+ throw new InvalidModeError("numeric mode can only encode decimal digits 0-9");
478
+ }
479
+ break;
480
+ case "alphanumeric":
481
+ if (!isAlphanumeric(text)) {
482
+ throw new InvalidModeError(`alphanumeric mode can only encode: ${ALPHANUMERIC_CHARSET}`);
483
+ }
484
+ break;
485
+ case "byte":
486
+ break;
487
+ case "kanji":
488
+ assertKanjiModeText(text);
489
+ break;
490
+ default:
491
+ throw new InvalidModeError(`Unsupported mode: ${mode}`);
492
+ }
493
+ }
494
+
495
+ function validateSegment(segment) {
496
+ if (!segment || typeof segment !== "object") {
497
+ throw new InvalidInputError("segment must be an object");
498
+ }
499
+
500
+ if (segment.mode === "fnc1") {
501
+ validateFnc1Segment(segment);
502
+ } else if (segment.mode === "eci") {
503
+ validateEciAssignmentNumber(segment.assignmentNumber);
504
+ } else if (segment.mode === "byte" && segment.bytes !== undefined) {
505
+ validateByteValues(segment.bytes, "byte segment");
506
+ } else {
507
+ validateTextForMode(segment.text, segment.mode);
508
+ }
509
+ }
510
+
511
+ function validateEciAssignmentNumber(value) {
512
+ if (!Number.isInteger(value) || value < 0 || value >= 1000000) {
513
+ throw new InvalidEciError(`ECI assignment number must be an integer from 0 to 999999; got ${value}`);
514
+ }
515
+ }
516
+
517
+ function getEciDesignatorBitLength(value) {
518
+ validateEciAssignmentNumber(value);
519
+ if (value < 2 ** 7) {
520
+ return 8;
521
+ }
522
+ if (value < 2 ** 14) {
523
+ return 16;
524
+ }
525
+ return 24;
526
+ }
527
+
528
+ function appendEciDesignatorBits(buffer, value) {
529
+ validateEciAssignmentNumber(value);
530
+ if (value < 2 ** 7) {
531
+ buffer.append(value, 8);
532
+ } else if (value < 2 ** 14) {
533
+ buffer.append(0b10, 2);
534
+ buffer.append(value, 14);
535
+ } else {
536
+ buffer.append(0b110, 3);
537
+ buffer.append(value, 21);
538
+ }
539
+ }
540
+
541
+ function assertText(text) {
542
+ if (typeof text !== "string") {
543
+ throw new InvalidInputError(`QR input must be a string; got ${typeof text}`);
544
+ }
545
+ }
546
+
547
+ function normalizeManualSegment(segment, index) {
548
+ if (!segment || typeof segment !== "object") {
549
+ throw new InvalidInputError(`segments[${index}] must be an object`);
550
+ }
551
+
552
+ switch (segment.mode) {
553
+ case "fnc1":
554
+ validateFnc1Segment(segment, `segments[${index}]`);
555
+ return { mode: "fnc1" };
556
+ case "eci":
557
+ validateEciAssignmentNumber(segment.assignmentNumber);
558
+ return { mode: "eci", assignmentNumber: segment.assignmentNumber };
559
+ case "numeric":
560
+ case "alphanumeric": {
561
+ const text = getManualTextData(segment, index);
562
+ validateTextForMode(text, segment.mode);
563
+ return { mode: segment.mode, text };
564
+ }
565
+ case "kanji": {
566
+ const text = getManualTextData(segment, index);
567
+ validateTextForMode(text, "kanji");
568
+ return { mode: "kanji", text };
569
+ }
570
+ case "byte": {
571
+ const data = getManualData(segment, index);
572
+ if (typeof data === "string") {
573
+ return { mode: "byte", text: data };
574
+ }
575
+ return { mode: "byte", bytes: toByteArray(data, `segments[${index}].data`) };
576
+ }
577
+ default:
578
+ throw new InvalidModeError(`segments[${index}].mode must be "fnc1", "eci", "numeric", "alphanumeric", "byte", or "kanji"`);
579
+ }
580
+ }
581
+
582
+ function validateControlSegments(segments) {
583
+ const fnc1Indexes = [];
584
+ const eciIndexes = [];
585
+
586
+ segments.forEach((segment, index) => {
587
+ if (segment.mode === "fnc1") {
588
+ fnc1Indexes.push(index);
589
+ } else if (segment.mode === "eci") {
590
+ eciIndexes.push(index);
591
+ }
592
+ });
593
+
594
+ if (fnc1Indexes.length > 1) {
595
+ throw new InvalidGs1Error("manual segments can include at most one fnc1 segment");
596
+ }
597
+ if (fnc1Indexes.length === 1 && fnc1Indexes[0] !== 0) {
598
+ throw new InvalidGs1Error("manual fnc1 segment must be the first segment");
599
+ }
600
+ if (fnc1Indexes.length === 1 && eciIndexes.length > 0) {
601
+ throw new InvalidGs1Error("FNC1 first position cannot be combined with ECI in this implementation");
602
+ }
603
+
604
+ return segments;
605
+ }
606
+
607
+ function validateFnc1Segment(segment, label = "fnc1 segment") {
608
+ if (
609
+ Object.hasOwn(segment, "data") ||
610
+ Object.hasOwn(segment, "text") ||
611
+ Object.hasOwn(segment, "bytes") ||
612
+ Object.hasOwn(segment, "assignmentNumber")
613
+ ) {
614
+ throw new InvalidGs1Error(`${label} must not include data, text, bytes, or assignmentNumber`);
615
+ }
616
+ }
617
+
618
+ function getManualTextData(segment, index) {
619
+ const data = getManualData(segment, index);
620
+ if (typeof data !== "string") {
621
+ throw new InvalidInputError(`segments[${index}].data must be a string for ${segment.mode} mode`);
622
+ }
623
+ return data;
624
+ }
625
+
626
+ function getManualData(segment, index) {
627
+ if (Object.hasOwn(segment, "data")) {
628
+ return segment.data;
629
+ }
630
+ if (Object.hasOwn(segment, "text")) {
631
+ return segment.text;
632
+ }
633
+ if (Object.hasOwn(segment, "bytes")) {
634
+ return segment.bytes;
635
+ }
636
+ throw new InvalidInputError(`segments[${index}] must include data or text`);
637
+ }
638
+
639
+ function getTextPayload(segment) {
640
+ if (typeof segment.text !== "string") {
641
+ throw new InvalidInputError(`${segment.mode} segment requires text data`);
642
+ }
643
+ return segment.text;
644
+ }
645
+
646
+ function getByteValues(segment) {
647
+ if (segment.bytes !== undefined) {
648
+ return validateByteValues(segment.bytes, "byte segment");
649
+ }
650
+ return encodeUtf8(getTextPayload(segment));
651
+ }
652
+
653
+ function validateByteValues(bytes, label) {
654
+ if (!Array.isArray(bytes)) {
655
+ throw new InvalidInputError(`${label} must contain byte values`);
656
+ }
657
+
658
+ for (let index = 0; index < bytes.length; index += 1) {
659
+ const byte = bytes[index];
660
+ if (!Number.isInteger(byte) || byte < 0 || byte > 255) {
661
+ throw new InvalidInputError(`${label}[${index}] must be an integer from 0 to 255; got ${byte}`);
662
+ }
663
+ }
664
+
665
+ return Array.from(bytes);
666
+ }