|
|
/**
|
|
|
* 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
|
|
|
};
|