/** * EAN-Codierung und SVG-Erzeugung. * * Die Datei ist bewusst frei von DOM-Zugriffen: sie liefert eine reine * Bitfolge (1 = Balken, 0 = Luecke) und daraus ein SVG-Markup. Die * Pruefzifferpruefung aus gtin.js wird vor jeder Codierung wiederholt, damit * kein Barcode fuer eine inkonsistente Nummer entstehen kann. */ import { gtinGueltig } from './gtin.js'; /** L-Code (ungerade Paritaet), linke Haelfte. */ const L_CODE = [ '0001101', '0011001', '0010011', '0111101', '0100011', '0110001', '0101111', '0111011', '0110111', '0001011', ]; /** G-Code (gerade Paritaet), linke Haelfte bei EAN-13. */ const G_CODE = [ '0100111', '0110011', '0011011', '0100001', '0011101', '0111001', '0000101', '0010001', '0001001', '0010111', ]; /** R-Code, rechte Haelfte (Komplement des L-Codes). */ const R_CODE = [ '1110010', '1100110', '1101100', '1000010', '1011100', '1001110', '1010000', '1000100', '1001000', '1110100', ]; /** * Paritaetsmuster der linken Gruppe in Abhaengigkeit der ersten Ziffer * (EAN-13). Die erste Ziffer wird selbst nicht als Balken codiert. */ const EAN13_PARITAET = [ 'LLLLLL', 'LLGLGG', 'LLGGLG', 'LLGGGL', 'LGLLGG', 'LGGLLG', 'LGGGLL', 'LGLGLG', 'LGLGGL', 'LGGLGL', ]; const START_UND_ENDE = '101'; const MITTE = '01010'; /** Ruhezonen in Modulen (links/rechts). */ export const QUIETZONE = { 'EAN-8': { links: 7, rechts: 7 }, 'EAN-13': { links: 11, rechts: 7 }, }; /** Standardhoehe der Balken in Modulen. */ export const BALKENHOEHE_MODULE = 62; /** Verlaengerung der Rand- und Mittelbalken nach unten, in Modulen. */ export const GUARD_ZUSATZ_MODULE = 6; /** Reservierte Hoehe fuer die Klartextzeile, in Modulen. */ export const TEXT_HOEHE_MODULE = 13; /** Schriftgroesse des Klartexts, in Modulen. */ export const TEXT_GROESSE_MODULE = 9; /** * @typedef {object} Textgruppe * @property {string} text anzuzeigende Ziffern * @property {number} start Startposition im Symbol (Modulindex) * @property {number} breite Breite der Gruppe in Modulen */ /** * @typedef {object} EanSymbol * @property {string} gtin validierte GTIN * @property {'EAN-8' | 'EAN-13'} typ Barcode-Typ * @property {string} bits Bitfolge (1 = Balken) * @property {number} module Anzahl der Symbolmodule ohne Ruhezonen * @property {{ links: number, rechts: number }} quietzone Ruhezonen in Modulen * @property {Textgruppe[]} textgruppen Klartext unter dem Symbol * @property {string | null} fuehrendeZiffer erste Ziffer (nur EAN-13) */ /** * Codiert eine vollstaendige GTIN als EAN-8 oder EAN-13. * * @param {unknown} gtin vollstaendige GTIN (8 oder 13 Ziffern) * @returns {EanSymbol} * @throws {Error} wenn die GTIN ungueltig ist oder die Codierung nicht aufgeht */ export function kodiereEan(gtin) { const wert = String(gtin ?? ''); if (!gtinGueltig(wert)) { throw new Error(`Ungültige GTIN: ${wert}`); } const typ = wert.length === 8 ? 'EAN-8' : 'EAN-13'; /** @type {string} */ let links; /** @type {string} */ let rechts; /** @type {Textgruppe[]} */ let textgruppen; let fuehrendeZiffer = null; if (typ === 'EAN-8') { links = ''; for (let i = 0; i < 4; i += 1) links += L_CODE[Number(wert[i])]; rechts = ''; for (let i = 4; i < 8; i += 1) rechts += R_CODE[Number(wert[i])]; textgruppen = [ { text: wert.slice(0, 4), start: 3, breite: 28 }, { text: wert.slice(4, 8), start: 36, breite: 28 }, ]; } else { const paritaet = EAN13_PARITAET[Number(wert[0])]; links = ''; for (let i = 1; i <= 6; i += 1) { const ziffer = Number(wert[i]); links += paritaet[i - 1] === 'L' ? L_CODE[ziffer] : G_CODE[ziffer]; } rechts = ''; for (let i = 7; i < 13; i += 1) rechts += R_CODE[Number(wert[i])]; textgruppen = [ { text: wert.slice(1, 7), start: 3, breite: 42 }, { text: wert.slice(7, 13), start: 50, breite: 42 }, ]; fuehrendeZiffer = wert[0]; } const bits = START_UND_ENDE + links + MITTE + rechts + START_UND_ENDE; const erwartet = typ === 'EAN-8' ? 67 : 95; if (bits.length !== erwartet || !/^[01]+$/.test(bits)) { throw new Error(`EAN-Codierung fehlerhaft: ${bits.length} statt ${erwartet} Module.`); } return { gtin: wert, typ, bits, module: bits.length, quietzone: QUIETZONE[typ], textgruppen, fuehrendeZiffer, }; } /** * Groesste ganzzahlige Modulbreite, die in die verfuegbare Breite passt. * * @param {number} verfuegbareBreitePx * @param {number} moduleGesamt Anzahl Module inklusive Ruhezonen * @returns {number} ganzzahlige Modulbreite in Pixeln, mindestens 1 */ export function modulbreite(verfuegbareBreitePx, moduleGesamt) { if (!Number.isFinite(verfuegbareBreitePx) || moduleGesamt <= 0) return 1; return Math.max(1, Math.floor(verfuegbareBreitePx / moduleGesamt)); } /** * Maximale Balkenhoehe (in Modulen) fuer eine verfuegbare Anzeigehoehe. * * @param {number} verfuegbareHoehePx * @param {number} modulbreitePx * @param {number} [minimum] kleinste sinnvolle Balkenhoehe in Modulen * @returns {number} */ export function balkenhoeheFuerHoehe(verfuegbareHoehePx, modulbreitePx, minimum = 30) { if (!Number.isFinite(verfuegbareHoehePx) || verfuegbareHoehePx <= 0 || modulbreitePx <= 0) { return BALKENHOEHE_MODULE; } const reserviert = GUARD_ZUSATZ_MODULE + TEXT_HOEHE_MODULE; const moeglich = Math.floor(verfuegbareHoehePx / modulbreitePx) - reserviert; return Math.max(minimum, moeglich); } /** Kleinste sinnvolle Balkenhoehe in Modulen. */ export const MIN_BALKENHOEHE_MODULE = 30; /** * Waehlt Modulbreite und Balkenhoehe fuer eine verfuegbare Flaeche. * * Gesucht ist die groesste ganzzahlige Modulbreite, mit der der komplette * Barcode (Balken, Guard-Zusatz und Klartextzeile) sowohl in die Breite als * auch in die Hoehe passt. So fuellt der Barcode die Anzeige bestmoeglich aus, * ohne abgeschnitten zu werden; die Modulbreite bleibt ganzzahlig und damit * gestochen scharf. * * @param {number} verfuegbareBreitePx * @param {number} verfuegbareHoehePx * @param {number} moduleGesamt Anzahl der Module inklusive Ruhezonen * @param {{ minimumBalkenhoehe?: number, maximumBalkenhoehe?: number }} [optionen] * @returns {{ modulbreite: number, balkenhoeheModule: number }} */ export function barcodeGroesse(verfuegbareBreitePx, verfuegbareHoehePx, moduleGesamt, optionen = {}) { const minimum = Math.max( 1, Math.round(optionen.minimumBalkenhoehe ?? MIN_BALKENHOEHE_MODULE), ); const maximum = Math.max( minimum, Math.round(optionen.maximumBalkenhoehe ?? BALKENHOEHE_MODULE), ); const reserviert = GUARD_ZUSATZ_MODULE + TEXT_HOEHE_MODULE; const breite = Number.isFinite(verfuegbareBreitePx) && verfuegbareBreitePx > 0 ? Math.floor(verfuegbareBreitePx) : 320; const hoehe = Number.isFinite(verfuegbareHoehePx) && verfuegbareHoehePx > 0 ? Math.floor(verfuegbareHoehePx) : 320; const module = Number.isFinite(moduleGesamt) && moduleGesamt > 0 ? Math.floor(moduleGesamt) : 1; const groessteBreite = Math.max(1, Math.floor(breite / module)); for (let kandidat = groessteBreite; kandidat >= 1; kandidat -= 1) { const platz = Math.floor(hoehe / kandidat) - reserviert; if (kandidat === 1 || platz >= minimum) { return { modulbreite: kandidat, balkenhoeheModule: Math.min(maximum, Math.max(minimum, platz)), }; } } return { modulbreite: 1, balkenhoeheModule: minimum }; } /** * Zerlegt die Bitfolge in zusammenhaengende Balken. * * @param {string} bits * @returns {{ start: number, breite: number }[]} */ function balkenLaeufe(bits) { /** @type {{ start: number, breite: number }[]} */ const laeufe = []; let start = -1; for (let i = 0; i < bits.length; i += 1) { if (bits[i] === '1') { if (start === -1) start = i; } else if (start !== -1) { laeufe.push({ start, breite: i - start }); start = -1; } } if (start !== -1) laeufe.push({ start, breite: bits.length - start }); return laeufe; } /** * Erzeugt das SVG-Markup fuer einen EAN-Barcode. * * Alle Laengen werden in ganzen Modulen gerechnet; die Pixelbreite ergibt sich * aus einem ganzzahligen Vielfachen der Modulbreite. Dadurch entstehen harte * Kanten ohne Teilpixel. * * @param {unknown} gtin vollstaendige GTIN * @param {{ modulbreite?: number, balkenhoeheModule?: number }} [optionen] * @returns {{ markup: string, breite: number, hoehe: number, modulbreite: number, symbol: EanSymbol }} */ export function erzeugeSvgMarkup(gtin, optionen = {}) { const symbol = kodiereEan(gtin); const modulbreitePx = Math.max(1, Math.round(optionen.modulbreite ?? 3)); const balkenhoehe = Math.max( 20, Math.round(optionen.balkenhoeheModule ?? BALKENHOEHE_MODULE), ); const { links, rechts } = symbol.quietzone; const gesamtModule = links + symbol.module + rechts; const hoeheModule = balkenhoehe + GUARD_ZUSATZ_MODULE + TEXT_HOEHE_MODULE; const breitePx = gesamtModule * modulbreitePx; const hoehePx = hoeheModule * modulbreitePx; const linkeGruppeEnde = 3 + (symbol.typ === 'EAN-8' ? 28 : 42); const guardBereiche = [ { start: 0, ende: 3 }, { start: linkeGruppeEnde, ende: linkeGruppeEnde + 5 }, { start: symbol.module - 3, ende: symbol.module }, ]; const istGuard = (/** @type {number} */ start, /** @type {number} */ breite) => guardBereiche.some((bereich) => start >= bereich.start && start + breite <= bereich.ende); const rechtecke = balkenLaeufe(symbol.bits) .map(({ start, breite }) => { const hoehe = balkenhoehe + (istGuard(start, breite) ? GUARD_ZUSATZ_MODULE : 0); return ``; }) .join(''); const grundlinie = balkenhoehe + GUARD_ZUSATZ_MODULE + TEXT_GROESSE_MODULE; const text = symbol.textgruppen .map( (gruppe) => `${gruppe.text}`, ) .join(''); const fuehrend = symbol.fuehrendeZiffer ? `${symbol.fuehrendeZiffer}` : ''; const beschriftung = `${symbol.typ}-Barcode ${symbol.gtin}`; const markup = `` + `${beschriftung}` + `` + `${rechtecke}` + `${fuehrend}${text}` + ``; return { markup, breite: breitePx, hoehe: hoehePx, modulbreite: modulbreitePx, symbol }; }