Erste Produktivversion
This commit is contained in:
210
tools/test-barcode.mjs
Normal file
210
tools/test-barcode.mjs
Normal file
@@ -0,0 +1,210 @@
|
||||
// Test des Strichcode-Decoders ohne Kamera.
|
||||
//
|
||||
// node tools/test-barcode.mjs
|
||||
//
|
||||
// Erzeugt synthetische EAN-13-, EAN-8- und UPC-A-Signale, verzerrt sie
|
||||
// (Unschärfe, Rauschen, unterschiedliche Skalierung, umgedreht) und
|
||||
// prüft, ob der Decoder sie zurückliest - und ob er auf leeren Flächen
|
||||
// und Rauschen schweigt.
|
||||
|
||||
import {
|
||||
checksumValid,
|
||||
decodeImage,
|
||||
decodeLine,
|
||||
} from "../web/html/js/barcode.js";
|
||||
|
||||
// Deterministischer Zufallszahlengenerator (xorshift32).
|
||||
//
|
||||
// Math.random() hätte hier einen Test ergeben, der mal durchgeht und mal
|
||||
// nicht - genau das ist beim ersten Gesamtdurchlauf passiert. Ein Test
|
||||
// mit wechselndem Ergebnis ist schlimmer als keiner: Man gewöhnt sich an,
|
||||
// ihn zu ignorieren.
|
||||
let _seed = 0x2f6f4e01;
|
||||
function rnd() {
|
||||
_seed ^= _seed << 13; _seed >>>= 0;
|
||||
_seed ^= _seed >> 17;
|
||||
_seed ^= _seed << 5; _seed >>>= 0;
|
||||
return _seed / 0x100000000;
|
||||
}
|
||||
function reseed(value) { _seed = value >>> 0 || 1; }
|
||||
|
||||
// --- Kodierer zum Gegentesten (nur im Test, nicht in der App) ---
|
||||
const L = ["0001101","0011001","0010011","0111101","0100011",
|
||||
"0110001","0101111","0111011","0110111","0001011"];
|
||||
const R_ = L.map((s) => [...s].map((b) => (b === "0" ? "1" : "0")).join(""));
|
||||
const G = R_.map((s) => [...s].reverse().join(""));
|
||||
const R = L.map((s) => [...s].map((b) => (b === "0" ? "1" : "0")).join(""));
|
||||
const PARITY = ["LLLLLL","LLGLGG","LLGGLG","LLGGGL","LGLLGG",
|
||||
"LGGLLG","LGGGLL","LGLGLG","LGLGGL","LGGLGL"];
|
||||
|
||||
function checkDigit(body) {
|
||||
const d = [...body].map(Number);
|
||||
let sum = 0;
|
||||
for (let i = d.length - 1, w = 3; i >= 0; i--, w = 4 - w) sum += d[i] * w;
|
||||
return (10 - (sum % 10)) % 10;
|
||||
}
|
||||
|
||||
function encodeEAN13(body12) {
|
||||
const code = body12 + checkDigit(body12);
|
||||
const first = Number(code[0]);
|
||||
const parity = PARITY[first];
|
||||
let bits = "101";
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const d = Number(code[i + 1]);
|
||||
bits += parity[i] === "L" ? L[d] : G[d];
|
||||
}
|
||||
bits += "01010";
|
||||
for (let i = 0; i < 6; i++) bits += R[Number(code[i + 7])];
|
||||
bits += "101";
|
||||
return { code, bits };
|
||||
}
|
||||
|
||||
function encodeEAN8(body7) {
|
||||
const code = body7 + checkDigit(body7);
|
||||
let bits = "101";
|
||||
for (let i = 0; i < 4; i++) bits += L[Number(code[i])];
|
||||
bits += "01010";
|
||||
for (let i = 4; i < 8; i++) bits += R[Number(code[i])];
|
||||
bits += "101";
|
||||
return { code, bits };
|
||||
}
|
||||
|
||||
/** Bitmuster -> Helligkeitszeile, mit Rand, Skalierung und Rauschen. */
|
||||
function toLine(bits, { scale = 3, quiet = null, noise = 0, blur = 0 } = {}) {
|
||||
// Ruhezone in MODULEN, nicht in Pixeln: Die Norm verlangt 7 bis 11
|
||||
// Module. Ein fester Pixelwert wäre bei großer Skalierung zu schmal
|
||||
// und würde einen Fehler vortäuschen, den es in der Wirklichkeit
|
||||
// nicht gibt.
|
||||
const margin = quiet ?? scale * 8;
|
||||
const px = [];
|
||||
for (let i = 0; i < margin; i++) px.push(255);
|
||||
for (const b of bits) {
|
||||
for (let s = 0; s < scale; s++) px.push(b === "1" ? 20 : 235);
|
||||
}
|
||||
for (let i = 0; i < margin; i++) px.push(255);
|
||||
|
||||
let out = px;
|
||||
if (blur) {
|
||||
out = px.map((_, i) => {
|
||||
let sum = 0, n = 0;
|
||||
for (let k = -blur; k <= blur; k++) {
|
||||
if (px[i + k] !== undefined) { sum += px[i + k]; n++; }
|
||||
}
|
||||
return sum / n;
|
||||
});
|
||||
}
|
||||
if (noise) {
|
||||
out = out.map((v) => Math.max(0, Math.min(255, v + (rnd() - 0.5) * noise)));
|
||||
}
|
||||
return Uint8Array.from(out.map(Math.round));
|
||||
}
|
||||
|
||||
function lineToImageData(line, height = 30) {
|
||||
const width = line.length;
|
||||
const data = new Uint8ClampedArray(width * height * 4);
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const p = (y * width + x) * 4;
|
||||
data[p] = data[p + 1] = data[p + 2] = line[x];
|
||||
data[p + 3] = 255;
|
||||
}
|
||||
}
|
||||
return { width, height, data };
|
||||
}
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
const check = (name, ok, extra = "") => {
|
||||
if (ok) { pass++; console.log(` ok ${name}`); }
|
||||
else { fail++; console.log(` FEHL ${name} ${extra}`); }
|
||||
};
|
||||
|
||||
console.log("--- Prüfziffer ---");
|
||||
check("EAN-13 gültig", checksumValid("4001234567890") === (checkDigit("400123456789") === 0));
|
||||
check("bekannte EAN-13", checksumValid("4006381333931"));
|
||||
check("verfälschte EAN-13 abgelehnt", !checksumValid("4006381333932"));
|
||||
check("EAN-8", checksumValid("96385074"));
|
||||
|
||||
console.log("\n--- Sauberes Signal ---");
|
||||
for (const body of ["400123456789", "978020137", "012345678", "590123412345".slice(0,12)]) {
|
||||
const b12 = body.padEnd(12, "0").slice(0, 12);
|
||||
const { code, bits } = encodeEAN13(b12);
|
||||
check(`EAN-13 ${code}`, decodeLine(toLine(bits)) === code, `-> ${decodeLine(toLine(bits))}`);
|
||||
}
|
||||
{
|
||||
const { code, bits } = encodeEAN8("9638507");
|
||||
check(`EAN-8 ${code}`, decodeLine(toLine(bits)) === code, `-> ${decodeLine(toLine(bits))}`);
|
||||
}
|
||||
|
||||
console.log("\n--- Umgedreht gehalten ---");
|
||||
{
|
||||
const { code, bits } = encodeEAN13("400123456789");
|
||||
const line = toLine(bits);
|
||||
check(`rückwärts ${code}`, decodeLine(Uint8Array.from([...line].reverse())) === code);
|
||||
}
|
||||
|
||||
console.log("\n--- Unscharf und verrauscht ---");
|
||||
for (const [blur, noise, scale] of [[1, 20, 3], [2, 30, 4], [1, 40, 5], [3, 25, 6]]) {
|
||||
const { code, bits } = encodeEAN13("426000123456");
|
||||
const got = decodeLine(toLine(bits, { blur, noise, scale }));
|
||||
check(`blur=${blur} noise=${noise} scale=${scale}`, got === code, `-> ${got}`);
|
||||
}
|
||||
|
||||
console.log("\n--- Knappe Ruhezone ---");
|
||||
{
|
||||
const { code, bits } = encodeEAN13("426000123456");
|
||||
// 6 Module Rand: unterschreitet die Norm, wird aber noch gelesen
|
||||
const ok6 = decodeLine(toLine(bits, { scale: 4, quiet: 24 }));
|
||||
check("6 Module Rand wird gelesen", ok6 === code, `-> ${ok6}`);
|
||||
// 2 Module Rand: wird abgelehnt. Das ist der Preis dafür, dass
|
||||
// Rauschen keine Fehltreffer erzeugt.
|
||||
const ok2 = decodeLine(toLine(bits, { scale: 4, quiet: 8 }));
|
||||
check("2 Module Rand wird abgelehnt", ok2 === null, `-> ${ok2}`);
|
||||
}
|
||||
|
||||
console.log("\n--- Ganzes Bild ---");
|
||||
{
|
||||
const { code, bits } = encodeEAN13("400123456789");
|
||||
const line = toLine(bits, { blur: 1, noise: 25, scale: 4 });
|
||||
check("decodeImage", decodeImage(lineToImageData(line)) === code);
|
||||
}
|
||||
|
||||
console.log("\n--- Kein Strichcode im Bild ---");
|
||||
{
|
||||
const flat = Uint8Array.from({ length: 400 }, () => 200);
|
||||
check("gleichmäßige Fläche -> null", decodeLine(flat) === null);
|
||||
|
||||
// Fehltreffer statistisch messen statt einmal zu würfeln.
|
||||
//
|
||||
// Eine einzelne Zeile kann in reinem Rauschen zufällig eine gültige
|
||||
// Kombination ergeben - Startzeichen, zwölf Ziffern, passende
|
||||
// Prüfziffer. Die Ruhezonenprüfung drückt das stark, ganz ausschließen
|
||||
// lässt es sich nicht.
|
||||
//
|
||||
// Entscheidend ist die Ebene darüber: decodeImage verlangt, dass ZWEI
|
||||
// Abtastlinien dasselbe Ergebnis liefern. Genau das ist im Sucher der
|
||||
// Anwendung im Einsatz.
|
||||
let lineHits = 0;
|
||||
const LINE_TRIES = 500;
|
||||
for (let i = 0; i < LINE_TRIES; i++) {
|
||||
reseed(0x1000 + i);
|
||||
const noise = Uint8Array.from({ length: 400 }, () => (rnd() * 255) | 0);
|
||||
if (decodeLine(noise)) lineHits++;
|
||||
}
|
||||
console.log(` info Einzelzeile auf Rauschen: ${lineHits}/${LINE_TRIES} Fehltreffer`);
|
||||
check(`Einzelzeile: Fehltrefferquote unter 2 %`, lineHits / LINE_TRIES < 0.02,
|
||||
`(${lineHits}/${LINE_TRIES})`);
|
||||
|
||||
let imageHits = 0;
|
||||
const IMAGE_TRIES = 200;
|
||||
for (let i = 0; i < IMAGE_TRIES; i++) {
|
||||
reseed(0x9000 + i);
|
||||
const noise = Uint8Array.from({ length: 400 }, () => (rnd() * 255) | 0);
|
||||
if (decodeImage(lineToImageData(noise))) imageHits++;
|
||||
}
|
||||
console.log(` info Ganzes Bild auf Rauschen: ${imageHits}/${IMAGE_TRIES} Fehltreffer`);
|
||||
check("Ganzes Bild: keine Fehltreffer auf Rauschen", imageHits === 0,
|
||||
`(${imageHits}/${IMAGE_TRIES})`);
|
||||
}
|
||||
|
||||
console.log(`\n${pass} bestanden, ${fail} fehlgeschlagen`);
|
||||
process.exit(fail ? 1 : 0);
|
||||
Reference in New Issue
Block a user