You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/libs/barcode/barcode.ts

889 lines
26 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* Linear (1D) barcode encoder — own, zero-dependency.
*
* Implements the open symbology standards: ISO/IEC 15417 (Code 128),
* ISO/IEC 15420 (EAN-13 / EAN-8 / UPC-A / UPC-E), ISO/IEC 16388 (Code 39) and
* ISO/IEC 16390 (ITF / ITF-14). Every table below is published specification
* data transcribed from the standard, or derived from the standard's own
* construction rule. No npm dependency — `$libs` is zero-dep. JsBarcode and
* bwip-js were used to cross-check correctness, never imported.
*
* The output is a module lattice (`true` = dark), the same shape `$libs/qr`
* returns for a QR matrix: geometry-free data the renderer turns into SVG.
*/
// ── Public API types ────────────────────────────────────────────────────────
export type Symbology = 'code128' | 'ean13' | 'ean8' | 'upca' | 'upce' | 'code39' | 'itf' | 'itf14';
/** A half-open module range `[start, end)`. */
export interface BarcodeRange {
start: number;
end: number;
}
/**
* A run of human-readable text anchored in module space. `start` may be
* negative (the group sits in the left quiet zone, as EAN-13's first digit
* does) and `end` may exceed the symbol width (the right quiet zone).
*/
export interface BarcodeTextGroup {
text: string;
start: number;
end: number;
}
export interface BarcodeResult {
/** Module lattice, left→right. `true` = dark (bar). Excludes the quiet zone. */
modules: boolean[];
/** Symbol width in modules (= `modules.length`). */
size: number;
/** Quiet zone in modules per side — the symbology's spec minimum. */
quietZone: { start: number; end: number };
/**
* Module ranges whose bars extend below the baseline into the text band
* (EAN / UPC guard patterns). Renderers MUST honour them: the long guards
* are how a scanner finds the symbol's edges and centre.
*/
guards: BarcodeRange[];
/** Human-readable interpretation, anchored in module space. */
text: BarcodeTextGroup[];
/** The value as encoded — the input plus any computed check digit. */
value: string;
symbology: Symbology;
/** The check digit computed, when the symbology carries one. */
checkDigit?: string;
/** ITF-14 only: the symbol must be framed by a bearer bar. */
bearerBar?: boolean;
}
export type BarcodeErrorReason =
| 'empty'
| 'invalid-characters'
| 'invalid-length'
| 'invalid-check-digit';
/**
* A value that cannot be encoded in the requested symbology. Consumers treat
* this as a *state*, not a crash: a half-typed EAN is a normal editing step.
*/
export class BarcodeError extends Error {
readonly reason: BarcodeErrorReason;
readonly symbology: Symbology;
constructor(reason: BarcodeErrorReason, symbology: Symbology, message: string) {
super(message);
this.name = 'BarcodeError';
this.reason = reason;
this.symbology = symbology;
}
}
export interface EncodeOptions {
/** @default 'code128' */
symbology?: Symbology;
/** Quiet zone override, in modules. Defaults to the symbology's spec minimum. */
quietZone?: number;
}
// ── Shared primitives ───────────────────────────────────────────────────────
/**
* Wide-to-narrow ratio for the two-width symbologies (Code 39, ITF). The
* standards allow 2:1 … 3:1; 3:1 is the canonical nominal and what every
* renderer ships.
*/
const WIDE = 3;
/** Quiet zone minimums in modules, per the symbology's standard. */
const QUIET_ZONE: Record<Symbology, { start: number; end: number }> = {
code128: { start: 10, end: 10 },
ean13: { start: 11, end: 7 },
ean8: { start: 7, end: 7 },
upca: { start: 9, end: 9 },
upce: { start: 9, end: 7 },
code39: { start: 10, end: 10 },
itf: { start: 10, end: 10 },
itf14: { start: 10, end: 10 }
};
const DIGITS = /^[0-9]+$/;
/**
* The numeric symbologies, whose values are written with punctuation in the
* real world — an ISBN's hyphens, a GTIN's spaces. Code 128 and Code 39 are NOT
* here: a hyphen is encodable data there.
*/
const NUMERIC_SYMBOLOGIES = new Set<Symbology>(['ean13', 'ean8', 'upca', 'upce', 'itf', 'itf14']);
/** Separators dropped from a numeric value before encoding. */
const SEPARATORS = /[\s-]/g;
/** 9 digits + a base-11 check character. */
const ISBN10 = /^[0-9]{9}[0-9X]$/;
/**
* ISBN-10's own check digit: the ten characters weighted 10…1 must sum to a
* multiple of 11, where the check character `X` carries the value 10 (ISO 2108
* — the base-11 modulus is why the digit 10 needs a symbol at all).
*/
function isValidIsbn10(value: string): boolean {
let sum = 0;
for (let i = 0; i < 10; i++) {
sum += (value[i] === 'X' ? 10 : Number(value[i])) * (10 - i);
}
return sum % 11 === 0;
}
function fail(reason: BarcodeErrorReason, symbology: Symbology, message: string): never {
throw new BarcodeError(reason, symbology, message);
}
/** Expand a run-length width string (`'212222'`) into modules, starting with a bar. */
function widthsToModules(widths: string): string {
let out = '';
for (let i = 0; i < widths.length; i++) {
out += (i % 2 === 0 ? '1' : '0').repeat(Number(widths[i]));
}
return out;
}
/** Expand a narrow/wide element string (`'nnwwn'`) into modules, starting with a bar. */
function elementsToModules(elements: string, startsWithBar = true): string {
let out = '';
for (let i = 0; i < elements.length; i++) {
const dark = startsWithBar ? i % 2 === 0 : i % 2 === 1;
out += (dark ? '1' : '0').repeat(elements[i] === 'w' ? WIDE : 1);
}
return out;
}
/**
* The UPC/EAN mod-10 check digit: weight the digits alternately 3 and 1 from
* the RIGHT, sum, and take what is missing to the next multiple of 10.
*/
function mod10(digits: string): number {
let sum = 0;
for (let i = 0; i < digits.length; i++) {
const weight = (digits.length - i) % 2 === 1 ? 3 : 1;
sum += Number(digits[i]) * weight;
}
return (10 - (sum % 10)) % 10;
}
// ── Two-of-five bar code ────────────────────────────────────────────────────
/**
* The two-of-five code shared by Code 39's bars and ITF: exactly two of the
* five elements are wide, and the digit is the sum of the wide positions'
* weights `1 · 2 · 4 · 7 · 0` (so `0` is the 4+7 combination).
*/
const TWO_OF_FIVE = [
'nnwwn', // 0 → 4+7
'wnnnw', // 1 → 1+0
'nwnnw', // 2 → 2+0
'wwnnn', // 3 → 1+2
'nnwnw', // 4 → 4+0
'wnwnn', // 5 → 1+4
'nwwnn', // 6 → 2+4
'nnnww', // 7 → 7+0
'wnnwn', // 8 → 1+7
'nwnwn' // 9 → 2+7
];
// ── Code 39 (ISO/IEC 16388) ─────────────────────────────────────────────────
/** Character set in check-value order: value = index. */
const CODE39_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%';
/**
* Build a Code 39 character from the standard's construction rule rather than
* from a transcribed table — the rule cannot carry a typo.
*
* Nine elements alternate bar/space starting and ending with a bar
* (`b s b s b s b s b`). For the 40 alphanumerics: the five bars carry the
* two-of-five code, and the single wide space's position selects the group —
* left to right: `U–Z…` · `digits` · `A–J` · `K–T`. The last four characters
* (`$ / + %`) have all-narrow bars and three wide spaces instead.
*/
function code39FromGroup(spaceIndex: number, digit: number): string {
const bars = TWO_OF_FIVE[digit];
let out = '';
for (let i = 0; i < 4; i++) out += bars[i] + (i === spaceIndex ? 'w' : 'n');
return out + bars[4];
}
function code39Elements(value: number): string {
// $ / + % — all bars narrow, one narrow space among the four (moving right
// to left as the value grows).
if (value >= 39) {
const narrowSpace = 42 - value;
let out = '';
for (let i = 0; i < 4; i++) out += 'n' + (i === narrowSpace ? 'n' : 'w');
return out + 'n';
}
// group → the index of the wide space; digit → the two-of-five bar value.
if (value <= 9) return code39FromGroup(1, value);
if (value <= 19) return code39FromGroup(2, (value - 9) % 10);
if (value <= 29) return code39FromGroup(3, (value - 19) % 10);
return code39FromGroup(0, (value - 29) % 10);
}
/**
* `*` is the start/stop symbol — the 10th slot of the `U–Z…` group, which the
* alphabet itself does not carry (it is not an encodable character).
*/
const CODE39_START_STOP = code39FromGroup(0, 0);
function encodeCode39(value: string): { modules: string; value: string } {
const text = value.toUpperCase();
for (const ch of text) {
if (!CODE39_ALPHABET.includes(ch)) {
fail('invalid-characters', 'code39', `Code 39 cannot encode ${JSON.stringify(ch)}.`);
}
}
// Characters are separated by a narrow inter-character space.
const parts = [CODE39_START_STOP];
for (const ch of text) parts.push(code39Elements(CODE39_ALPHABET.indexOf(ch)));
parts.push(CODE39_START_STOP);
const modules = parts.map((p) => elementsToModules(p)).join('0');
// The `*` delimiters are structure, not data: ISO/IEC 16388 keeps the
// start/stop out of the human-readable interpretation, and every scanner
// returns the bare value.
return { modules, value: text };
}
// ── ITF (ISO/IEC 16390) ─────────────────────────────────────────────────────
function encodeItf(value: string, symbology: Symbology): { modules: string; check?: string } {
if (!DIGITS.test(value)) {
fail('invalid-characters', symbology, 'ITF encodes digits only.');
}
let digits = value;
let check: string | undefined;
if (symbology === 'itf14') {
if (digits.length !== 13 && digits.length !== 14) {
fail('invalid-length', symbology, 'ITF-14 needs 13 digits (14 with the check digit).');
}
const body = digits.slice(0, 13);
const expected = String(mod10(body));
if (digits.length === 14 && digits[13] !== expected) {
fail('invalid-check-digit', symbology, `Check digit should be ${expected}.`);
}
check = expected;
digits = body + expected;
} else if (digits.length % 2 !== 0) {
fail('invalid-length', symbology, 'ITF encodes digit PAIRS — the count must be even.');
}
// Start `nnnn`, then each pair interleaved (first digit in the bars, second
// in the spaces), then stop `wnn`.
let modules = elementsToModules('nnnn');
for (let i = 0; i < digits.length; i += 2) {
const bars = TWO_OF_FIVE[Number(digits[i])];
const spaces = TWO_OF_FIVE[Number(digits[i + 1])];
for (let k = 0; k < 5; k++) {
modules += '1'.repeat(bars[k] === 'w' ? WIDE : 1);
modules += '0'.repeat(spaces[k] === 'w' ? WIDE : 1);
}
}
modules += elementsToModules('wnn');
return { modules, check };
}
// ── EAN / UPC (ISO/IEC 15420) ───────────────────────────────────────────────
/** Left-hand odd-parity ("L") digit patterns. G and R derive from these. */
const EAN_L = [
'0001101',
'0011001',
'0010011',
'0111101',
'0100011',
'0110001',
'0101111',
'0111011',
'0110111',
'0001011'
];
/** Right-hand ("R") = the bitwise complement of L. */
const EAN_R = EAN_L.map((bits) => [...bits].map((b) => (b === '0' ? '1' : '0')).join(''));
/** Left-hand even-parity ("G") = R reversed. */
const EAN_G = EAN_R.map((bits) => [...bits].reverse().join(''));
/** Which parity the six left-hand digits use, indexed by EAN-13's first digit. */
const EAN13_PARITY = [
'LLLLLL',
'LLGLGG',
'LLGGLG',
'LLGGGL',
'LGLLGG',
'LGGLLG',
'LGGGLL',
'LGLGLG',
'LGLGGL',
'LGGLGL'
];
/**
* UPC-E parity for number system 0, indexed by the check digit. Number
* system 1 uses the inverse (`E` ↔ `O`). `O` = odd parity = the L table,
* `E` = even parity = the G table.
*/
const UPCE_PARITY = [
'EEEOOO',
'EEOEOO',
'EEOOEO',
'EEOOOE',
'EOEEOO',
'EOOEEO',
'EOOOEE',
'EOEOEO',
'EOEOOE',
'EOOEOE'
];
const EAN_GUARD = '101';
const EAN_CENTRE = '01010';
const UPCE_END = '010101';
/** EAN-13 / UPC-A share one geometry: 95 modules, 3 guards, two 42-module halves. */
function encodeEan13(digits: string): string {
const parity = EAN13_PARITY[Number(digits[0])];
let modules = EAN_GUARD;
for (let i = 0; i < 6; i++) {
const d = Number(digits[i + 1]);
modules += parity[i] === 'L' ? EAN_L[d] : EAN_G[d];
}
modules += EAN_CENTRE;
for (let i = 7; i < 13; i++) modules += EAN_R[Number(digits[i])];
return modules + EAN_GUARD;
}
function encodeEan8(digits: string): string {
let modules = EAN_GUARD;
for (let i = 0; i < 4; i++) modules += EAN_L[Number(digits[i])];
modules += EAN_CENTRE;
for (let i = 4; i < 8; i++) modules += EAN_R[Number(digits[i])];
modules += EAN_GUARD;
return modules;
}
/**
* Expand a 6-digit UPC-E body to its 12-digit UPC-A equivalent. The last
* digit of the body selects the suppression rule.
*/
function upceToUpca(numberSystem: string, body: string): string {
const [a, b, c, d, e, f] = body;
switch (f) {
case '0':
case '1':
case '2':
return `${numberSystem}${a}${b}${f}0000${c}${d}${e}`;
case '3':
return `${numberSystem}${a}${b}${c}00000${d}${e}`;
case '4':
return `${numberSystem}${a}${b}${c}${d}00000${e}`;
default:
return `${numberSystem}${a}${b}${c}${d}${e}0000${f}`;
}
}
function encodeUpce(numberSystem: string, body: string, check: number): string {
const parity = UPCE_PARITY[check];
let modules = EAN_GUARD;
for (let i = 0; i < 6; i++) {
// Number system 1 inverts the whole parity pattern.
const odd = numberSystem === '0' ? parity[i] === 'O' : parity[i] === 'E';
const d = Number(body[i]);
modules += odd ? EAN_L[d] : EAN_G[d];
}
return modules + UPCE_END;
}
// ── Code 128 (ISO/IEC 15417) ────────────────────────────────────────────────
/**
* The 107 symbol characters as bar/space width runs (bar, space, bar, space,
* bar, space). Values 0–102 are data, 103–105 the Start A/B/C characters and
* 106 the Stop — which carries a seventh element (13 modules instead of 11).
* Every data pattern sums to 11 modules with an even total bar width; the
* encoder's test asserts both invariants over the whole table.
*/
const CODE128_WIDTHS = [
'212222',
'222122',
'222221',
'121223',
'121322',
'131222',
'122213',
'122312',
'132212',
'221213',
'221312',
'231212',
'112232',
'122132',
'122231',
'113222',
'123122',
'123221',
'223211',
'221132',
'221231',
'213212',
'223112',
'312131',
'311222',
'321122',
'321221',
'312212',
'322112',
'322211',
'212123',
'212321',
'232121',
'111323',
'131123',
'131321',
'112313',
'132113',
'132311',
'211313',
'231113',
'231311',
'112133',
'112331',
'132131',
'113123',
'113321',
'133121',
'313121',
'211331',
'231131',
'213113',
'213311',
'213131',
'311123',
'311321',
'331121',
'312113',
'312311',
'332111',
'314111',
'221411',
'431111',
'111224',
'111422',
'121124',
'121421',
'141122',
'141221',
'112214',
'112412',
'122114',
'122411',
'142112',
'142211',
'241211',
'221114',
'413111',
'241112',
'134111',
'111242',
'121142',
'121241',
'114212',
'124112',
'124211',
'411212',
'421112',
'421211',
'212141',
'214121',
'412121',
'111143',
'111341',
'131141',
'114113',
'114311',
'411113',
'411311',
'113141',
'114131',
'311141',
'411131',
'211412',
'211214',
'211232',
'2331112'
];
const CODE128_START_A = 103;
const CODE128_START_B = 104;
const CODE128_START_C = 105;
const CODE128_CODE_A = 101;
const CODE128_CODE_B = 100;
const CODE128_CODE_C = 99;
const CODE128_STOP = 106;
function isDigitAt(text: string, i: number): boolean {
return i < text.length && text[i] >= '0' && text[i] <= '9';
}
/** Length of the digit run starting at `i`. */
function digitRun(text: string, i: number): number {
let n = 0;
while (isDigitAt(text, i + n)) n++;
return n;
}
/** Value of `ch` in subset A (control characters live at 64–95). */
function valueInA(code: number): number {
return code < 32 ? code + 64 : code - 32;
}
/**
* Build the Code 128 code-value sequence with automatic subset switching.
* Any valid switching choice decodes to the same data — the heuristic only
* decides how compact the symbol is: subset C (two digits per symbol
* character) for digit runs long enough to pay for the switch, A for control
* characters, B otherwise.
*/
function code128Values(text: string): number[] {
const values: number[] = [];
let mode: 'A' | 'B' | 'C';
const startRun = digitRun(text, 0);
if (startRun >= 4 || (startRun === text.length && startRun >= 2 && startRun % 2 === 0)) {
mode = 'C';
values.push(CODE128_START_C);
} else if (text.charCodeAt(0) < 32) {
mode = 'A';
values.push(CODE128_START_A);
} else {
mode = 'B';
values.push(CODE128_START_B);
}
let i = 0;
while (i < text.length) {
if (mode === 'C') {
if (isDigitAt(text, i) && isDigitAt(text, i + 1)) {
values.push(Number(text.slice(i, i + 2)));
i += 2;
continue;
}
// Out of digit pairs — leave subset C.
const next: 'A' | 'B' = text.charCodeAt(i) < 32 ? 'A' : 'B';
values.push(next === 'A' ? CODE128_CODE_A : CODE128_CODE_B);
mode = next;
continue;
}
const run = digitRun(text, i);
const evenRun = run - (run % 2);
if (evenRun >= 6 || (evenRun >= 4 && i + run === text.length)) {
values.push(CODE128_CODE_C);
mode = 'C';
continue;
}
const code = text.charCodeAt(i);
if (mode === 'B' && code < 32) {
values.push(CODE128_CODE_A);
mode = 'A';
continue;
}
if (mode === 'A' && code > 95) {
values.push(CODE128_CODE_B);
mode = 'B';
continue;
}
values.push(mode === 'A' ? valueInA(code) : code - 32);
i++;
}
return values;
}
function encodeCode128(text: string): string {
for (let i = 0; i < text.length; i++) {
if (text.charCodeAt(i) > 127) {
fail(
'invalid-characters',
'code128',
`Code 128 encodes ASCII 0–127; ${JSON.stringify(text[i])} is outside it.`
);
}
}
const values = code128Values(text);
// Checksum: the start value plus each subsequent value weighted by its
// position, modulo 103.
let sum = values[0];
for (let i = 1; i < values.length; i++) sum += values[i] * i;
values.push(sum % 103);
values.push(CODE128_STOP);
return values.map((v) => widthsToModules(CODE128_WIDTHS[v])).join('');
}
// ── Text placement ──────────────────────────────────────────────────────────
/** Centre a text group over a module range. */
function group(text: string, start: number, end: number): BarcodeTextGroup {
return { text, start, end };
}
/**
* One group per character, each centred over its own module cell. EAN / UPC
* print the HRI digit-under-digit: every digit sits under the 7 modules that
* encode it, which is what gives the family its look (and lets a human check a
* misread digit against its bars).
*/
function cells(text: string, start: number, cell: number): BarcodeTextGroup[] {
return [...text].map((ch, i) => group(ch, start + i * cell, start + (i + 1) * cell));
}
// ── Public API ──────────────────────────────────────────────────────────────
/**
* Encode `value` into a module lattice for the requested symbology.
* Throws {@link BarcodeError} when the value cannot be encoded — callers are
* expected to treat that as a state (a half-typed EAN), not as a crash.
*
* Two input conveniences, both scoped to the numeric symbologies:
* separators are tolerated (an ISBN travels hyphenated, a GTIN spaced), and a
* 10-character value in `ean13` is read as an ISBN-10 and converted.
*/
export function encode(value: string, options: EncodeOptions = {}): BarcodeResult {
const symbology = options.symbology ?? 'code128';
if (!value) fail('empty', symbology, 'Nothing to encode.');
// In Code 128 / Code 39 a hyphen or a space is DATA and must survive; in the
// numeric symbologies it is punctuation the value was written with.
let input = NUMERIC_SYMBOLOGIES.has(symbology) ? value.replace(SEPARATORS, '') : value;
if (!input) fail('empty', symbology, 'Nothing to encode once the separators are dropped.');
// ISBN-10 → Bookland EAN-13. An ISBN barcode IS an EAN-13 (ISO 2108), so
// this is an input profile, never a symbology of its own: the 10-digit form
// carries a base-11 check digit (possibly `X`) that we validate BEFORE
// converting — a mistyped ISBN-10 would otherwise become a perfectly valid
// EAN-13 pointing at a different book, the costliest failure in this domain.
if (symbology === 'ean13' && input.length === 10) {
// Only the check character can be a letter, so this is a no-op elsewhere.
input = input.toUpperCase();
if (!ISBN10.test(input)) {
fail(
'invalid-characters',
symbology,
'A 10-character value is read as an ISBN-10: 9 digits plus a check digit (0-9 or X).'
);
}
if (!isValidIsbn10(input)) {
fail('invalid-check-digit', symbology, 'The ISBN-10 check digit does not validate (mod 11).');
}
input = `978${input.slice(0, 9)}`;
}
// From here on the normalised form IS the value: everything below encodes it
// and reports it back through `result.value`.
value = input;
let bits = '';
let encoded = value;
let checkDigit: string | undefined;
let guards: BarcodeRange[] = [];
let text: BarcodeTextGroup[] = [];
let bearerBar: boolean | undefined;
switch (symbology) {
case 'code128': {
bits = encodeCode128(value);
text = [group(value, 0, bits.length)];
break;
}
case 'code39': {
const res = encodeCode39(value);
bits = res.modules;
encoded = res.value;
text = [group(res.value, 0, bits.length)];
break;
}
case 'itf':
case 'itf14': {
const res = encodeItf(value, symbology);
bits = res.modules;
checkDigit = res.check;
encoded = checkDigit ? value.slice(0, 13) + checkDigit : value;
text = [group(encoded, 0, bits.length)];
bearerBar = symbology === 'itf14';
break;
}
case 'ean13':
case 'upca': {
const body = symbology === 'upca' ? `0${value}` : value;
if (!DIGITS.test(value)) fail('invalid-characters', symbology, 'Digits only.');
const want = symbology === 'upca' ? [11, 12] : [12, 13];
if (!want.includes(value.length)) {
fail(
'invalid-length',
symbology,
`${symbology === 'upca' ? 'UPC-A' : 'EAN-13'} needs ${want[0]} digits (${want[1]} with the check digit).`
);
}
const withoutCheck = body.slice(0, 12);
const expected = String(mod10(withoutCheck));
if (body.length === 13 && body[12] !== expected) {
fail('invalid-check-digit', symbology, `Check digit should be ${expected}.`);
}
checkDigit = expected;
const digits = withoutCheck + expected;
bits = encodeEan13(digits);
// 101 · 6×7 · 01010 · 6×7 · 101 = 95 modules.
guards = [
{ start: 0, end: 3 },
{ start: 45, end: 50 },
{ start: 92, end: 95 }
];
encoded = symbology === 'upca' ? digits.slice(1) : digits;
text =
symbology === 'upca'
? [
// The number-system digit prints outside the symbol, its own
// cell being the first of the left half; the check digit does
// the same on the right.
group(digits[1], -9, 0),
...cells(digits.slice(2, 7), 10, 7),
...cells(digits.slice(7, 12), 50, 7),
group(digits[12], 95, 104)
]
: [
// EAN-13's first digit is encoded in the PARITY of the left
// half, not in bars of its own, so it prints in the quiet zone.
group(digits[0], -11, 0),
...cells(digits.slice(1, 7), 3, 7),
...cells(digits.slice(7), 50, 7)
];
break;
}
case 'ean8': {
if (!DIGITS.test(value)) fail('invalid-characters', symbology, 'Digits only.');
if (value.length !== 7 && value.length !== 8) {
fail('invalid-length', symbology, 'EAN-8 needs 7 digits (8 with the check digit).');
}
const withoutCheck = value.slice(0, 7);
const expected = String(mod10(withoutCheck));
if (value.length === 8 && value[7] !== expected) {
fail('invalid-check-digit', symbology, `Check digit should be ${expected}.`);
}
checkDigit = expected;
const digits = withoutCheck + expected;
bits = encodeEan8(digits);
// 101 · 4×7 · 01010 · 4×7 · 101 = 67 modules.
guards = [
{ start: 0, end: 3 },
{ start: 31, end: 36 },
{ start: 64, end: 67 }
];
encoded = digits;
text = [...cells(digits.slice(0, 4), 3, 7), ...cells(digits.slice(4), 36, 7)];
break;
}
case 'upce': {
if (!DIGITS.test(value)) fail('invalid-characters', symbology, 'Digits only.');
if (value.length !== 6 && value.length !== 7 && value.length !== 8) {
fail(
'invalid-length',
symbology,
'UPC-E needs 6 digits (7 with the number system, 8 with the check digit too).'
);
}
// 6 digits → number system 0 implied. 7+ → the first digit is the
// number system and must be 0 or 1.
const numberSystem = value.length === 6 ? '0' : value[0];
if (numberSystem !== '0' && numberSystem !== '1') {
fail('invalid-characters', symbology, 'UPC-E number system must be 0 or 1.');
}
const body = value.length === 6 ? value : value.slice(1, 7);
const upca = upceToUpca(numberSystem, body);
const expected = String(mod10(upca));
if (value.length === 8 && value[7] !== expected) {
fail('invalid-check-digit', symbology, `Check digit should be ${expected}.`);
}
checkDigit = expected;
bits = encodeUpce(numberSystem, body, Number(expected));
// 101 · 6×7 · 010101 = 51 modules. The end guard is long; the start
// guard too — the middle guard does not exist in UPC-E.
guards = [
{ start: 0, end: 3 },
{ start: 45, end: 51 }
];
encoded = numberSystem + body + expected;
text = [group(numberSystem, -9, 0), ...cells(body, 3, 7), group(expected, 51, 58)];
break;
}
}
const spec = QUIET_ZONE[symbology];
const quietZone =
options.quietZone === undefined
? { ...spec }
: { start: options.quietZone, end: options.quietZone };
return {
modules: [...bits].map((b) => b === '1'),
size: bits.length,
quietZone,
guards,
text,
value: encoded,
symbology,
...(checkDigit === undefined ? {} : { checkDigit }),
...(bearerBar === undefined ? {} : { bearerBar })
};
}
/** The symbologies this encoder supports, in catalog order. */
export const SYMBOLOGIES: readonly Symbology[] = [
'code128',
'ean13',
'ean8',
'upca',
'upce',
'code39',
'itf',
'itf14'
];
/** Internals exposed for the test suite's structural assertions. */
export const __internals = {
CODE128_WIDTHS,
CODE39_ALPHABET,
CODE39_START_STOP,
TWO_OF_FIVE,
EAN_L,
EAN_G,
EAN_R,
EAN13_PARITY,
UPCE_PARITY,
code39Elements,
upceToUpca,
mod10
};

Powered by TurnKey Linux.