feat: publish unlisted GTIN tool
This commit is contained in:
343
site/gtin/js/barcode.js
Normal file
343
site/gtin/js/barcode.js
Normal file
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* 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 `<rect x="${links + start}" y="0" width="${breite}" height="${hoehe}" fill="#000000"/>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
const grundlinie = balkenhoehe + GUARD_ZUSATZ_MODULE + TEXT_GROESSE_MODULE;
|
||||
const text = symbol.textgruppen
|
||||
.map(
|
||||
(gruppe) =>
|
||||
`<text x="${links + gruppe.start + gruppe.breite / 2}" y="${grundlinie}" ` +
|
||||
`font-size="${TEXT_GROESSE_MODULE}" text-anchor="middle" ` +
|
||||
`textLength="${gruppe.breite}" lengthAdjust="spacing" fill="#000000">${gruppe.text}</text>`,
|
||||
)
|
||||
.join('');
|
||||
const fuehrend = symbol.fuehrendeZiffer
|
||||
? `<text x="${links - 4}" y="${grundlinie}" font-size="${TEXT_GROESSE_MODULE}" ` +
|
||||
`text-anchor="middle" fill="#000000">${symbol.fuehrendeZiffer}</text>`
|
||||
: '';
|
||||
|
||||
const beschriftung = `${symbol.typ}-Barcode ${symbol.gtin}`;
|
||||
const markup =
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="${breitePx}" height="${hoehePx}" ` +
|
||||
`viewBox="0 0 ${gesamtModule} ${hoeheModule}" preserveAspectRatio="xMidYMid meet" ` +
|
||||
`role="img" aria-label="${beschriftung}" focusable="false">` +
|
||||
`<title>${beschriftung}</title>` +
|
||||
`<rect x="0" y="0" width="${gesamtModule}" height="${hoeheModule}" fill="#ffffff"/>` +
|
||||
`<g shape-rendering="crispEdges">${rechtecke}</g>` +
|
||||
`<g font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">${fuehrend}${text}</g>` +
|
||||
`</svg>`;
|
||||
|
||||
return { markup, breite: breitePx, hoehe: hoehePx, modulbreite: modulbreitePx, symbol };
|
||||
}
|
||||
Reference in New Issue
Block a user