Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebff173443 | ||
|
|
2e619ed64d | ||
|
|
2eb9d1ecf8 | ||
|
|
9ef038ca98 |
@@ -4,6 +4,11 @@ Browser-App, die per Handykamera gebrauchte Server-RAM-Module erkennt und
|
|||||||
beim physischen Sortieren am Tisch anleitet: Modul vor die Kamera halten, die
|
beim physischen Sortieren am Tisch anleitet: Modul vor die Kamera halten, die
|
||||||
App nennt den Stapel.
|
App nennt den Stapel.
|
||||||
|
|
||||||
|
Vor dem Scannen wählt der Nutzer eine von drei Funktionen — Strichcode,
|
||||||
|
QR-Code oder Text erkennen (siehe „Funktionsweise" unten). Jede Funktion nutzt
|
||||||
|
ausschließlich ihre eigene Quelle; anders als früher weicht die App nicht mehr
|
||||||
|
automatisch auf Texterkennung aus, wenn kein Code gefunden wird.
|
||||||
|
|
||||||
Zwei Module gelten als zusammengehörig, wenn sie denselben Barcode-Inhalt
|
Zwei Module gelten als zusammengehörig, wenn sie denselben Barcode-Inhalt
|
||||||
tragen — ein Barcode ist exakt gelesen, das ist der zuverlässigste Maßstab.
|
tragen — ein Barcode ist exakt gelesen, das ist der zuverlässigste Maßstab.
|
||||||
Jeder Stapel führt dafür die Menge der Barcode-Zeichenketten, die bei seinen
|
Jeder Stapel führt dafür die Menge der Barcode-Zeichenketten, die bei seinen
|
||||||
@@ -51,16 +56,57 @@ keine Daten verlassen das Gerät.
|
|||||||
|
|
||||||
## Funktionsweise
|
## Funktionsweise
|
||||||
|
|
||||||
1. **Barcode zuerst.** Code-128 und DataMatrix werden aus dem Kamerabild
|
0. **Funktion wählen.** Vor dem ersten Scan (und jederzeit erneut über die
|
||||||
dekodiert (`zxing-wasm`). Das ist exakt, im Gegensatz zu Texterkennung.
|
antippbare Anzeige in der Scan-Ansicht) wählt der Nutzer eine von drei
|
||||||
Werden mehrere Barcodes im Bild gefunden, die sich widersprechen (nicht
|
Funktionen. Kennung, Beschriftung, gelesene Codearten und Verhalten stehen
|
||||||
dieselbe Teilenummer), gilt das als mehrdeutig — die App rät nicht,
|
ausschließlich in `src/scan-modes.js` — es gibt keine zweite Stelle, an der
|
||||||
sondern fragt nach. Das gilt nur für Barcodes mit einem bekannten
|
das steht:
|
||||||
Nummernschema; widersprechen sich mehrere Barcodes mit unbekanntem
|
|
||||||
Nummernschema, wird keine ihrer Teilenummern übernommen.
|
| Funktion | Liest | Laufende Suche | Texterkennung |
|
||||||
2. **Teilenummer-Decoder.** Aus einer Hersteller-PN wie `M386A8K40BM1-CRC4Y`
|
|---|---|---|---|
|
||||||
werden Kapazität, Bauform und Geschwindigkeit tabellengesteuert
|
| **Strichcode** | Code128, Code39, Code93, ITF, EAN-13, EAN-8, UPC-A, UPC-E, Codabar | ja | nein |
|
||||||
abgeleitet. Gelingt das, entfällt OCR vollständig, das Ergebnis ist grün.
|
| **QR-Code** | ausschließlich QRCode, MicroQRCode, RMQRCode | ja | nein |
|
||||||
|
| **Text erkennen** | keine Codes | nein | ja, auf Knopfdruck |
|
||||||
|
|
||||||
|
„QR-Code" liest bewusst **kein** DataMatrix — auch wenn die 2D-Codes auf
|
||||||
|
eigenen RAM-Etiketten DataMatrix sind. Der reguläre Arbeitsablauf läuft über
|
||||||
|
„Strichcode", weil dort die Teilenummer im Strichcode steht; „QR-Code" ist
|
||||||
|
für andere Ware gedacht. Diese Beschränkung ist eine ausdrückliche
|
||||||
|
Entscheidung, keine Lücke.
|
||||||
|
|
||||||
|
„Modul scannen" erscheint nur in „Text erkennen" — in „Strichcode" und
|
||||||
|
„QR-Code" sucht die App ohnehin laufend, ein Knopf ohne Texterkennung hätte
|
||||||
|
dort nichts zu tun. Der Zielrahmen hebt sich in „Strichcode" und „QR-Code"
|
||||||
|
hervor, sobald ein passender Code im Bild ist; in „Text erkennen" bleibt er
|
||||||
|
schlicht, weil dort nichts laufend erkannt wird. Ein Wechsel der Funktion
|
||||||
|
während des Sortierens (z. B. bei gemischter Ware) gilt sofort: laufende
|
||||||
|
Suche startet oder stoppt, der Knopf erscheint oder verschwindet.
|
||||||
|
|
||||||
|
Liegt beim Start eine gesicherte Sitzung vor, erscheint zuerst die Frage
|
||||||
|
nach dem Fortsetzen (siehe Punkt 5 unten) und erst danach die Funktionswahl
|
||||||
|
— die Kamera startet in jedem Fall erst, nachdem beides geklärt ist.
|
||||||
|
|
||||||
|
Technisch bekommen „Strichcode" und „QR-Code" eine Texterkennung
|
||||||
|
hineingereicht, die sofort leeren Text liefert, ohne Tesseract anzustoßen;
|
||||||
|
„Text erkennen" bekommt eine Barcode-Dekodierung hineingereicht, die sofort
|
||||||
|
eine leere Liste liefert (`buildRecognitionAdapters` in
|
||||||
|
`src/scan-recognition.js`). Die Erkennungs-Pipeline selbst (`src/pipeline.js`,
|
||||||
|
Punkte 1–4 unten) bleibt dabei unverändert und kennt keine Funktionen — die
|
||||||
|
drei Funktionen unterscheiden sich ausschließlich darin, welche Adapter ihr
|
||||||
|
übergeben werden.
|
||||||
|
1. **Barcode-Dekodierung** (nur in „Strichcode" und „QR-Code"). Die zur
|
||||||
|
Funktion gehörenden Codearten werden laufend aus dem Kamerabild dekodiert
|
||||||
|
(`zxing-wasm`, `decodeBarcodes` in `src/barcode.js` bekommt die Codearten
|
||||||
|
der gewählten Funktion übergeben). Das ist exakt, im Gegensatz zu
|
||||||
|
Texterkennung. Werden mehrere Barcodes im Bild gefunden, die sich
|
||||||
|
widersprechen (nicht dieselbe Teilenummer), gilt das als mehrdeutig — die
|
||||||
|
App rät nicht, sondern fragt nach. Das gilt nur für Barcodes mit einem
|
||||||
|
bekannten Nummernschema; widersprechen sich mehrere Barcodes mit
|
||||||
|
unbekanntem Nummernschema, wird keine ihrer Teilenummern übernommen.
|
||||||
|
2. **Teilenummer-Decoder** (nur relevant in „Strichcode"/„QR-Code" — „Text
|
||||||
|
erkennen" liest nie einen Barcode, siehe Punkt 0). Aus einer Hersteller-PN
|
||||||
|
wie `M386A8K40BM1-CRC4Y` werden Kapazität, Bauform und Geschwindigkeit
|
||||||
|
tabellengesteuert abgeleitet. Gelingt das, entfällt OCR vollständig, das Ergebnis ist grün.
|
||||||
Kennt der Decoder das Nummernschema nicht (siehe „Grenzen“ unten), ist das
|
Kennt der Decoder das Nummernschema nicht (siehe „Grenzen“ unten), ist das
|
||||||
allein noch kein Grund für Rot: Zum Sortieren muss keine Kapazität bekannt
|
allein noch kein Grund für Rot: Zum Sortieren muss keine Kapazität bekannt
|
||||||
sein, eine exakt gelesene Teilenummer genügt, um ein Modul wiederzuerkennen
|
sein, eine exakt gelesene Teilenummer genügt, um ein Modul wiederzuerkennen
|
||||||
@@ -72,17 +118,23 @@ keine Daten verlassen das Gerät.
|
|||||||
könnte, das über die Zuordnung entscheidet. Bei einem tatsächlich neuen
|
könnte, das über die Zuordnung entscheidet. Bei einem tatsächlich neuen
|
||||||
Modul läuft sie dagegen wie gehabt, denn dort liefert sie die lesbare
|
Modul läuft sie dagegen wie gehabt, denn dort liefert sie die lesbare
|
||||||
Beschriftung des neuen Stapels.
|
Beschriftung des neuen Stapels.
|
||||||
3. **OCR als Rückfallebene.** Nur wenn kein Barcode eindeutig lesbar war oder
|
3. **Texterkennung** (ausschließlich in „Text erkennen", ausgelöst durch
|
||||||
sein Nummernschema unbekannt ist (z. B. überklebtes/beschädigtes Etikett
|
„Modul scannen"). Anders als früher ist das kein automatischer Rückfall
|
||||||
oder ein noch nicht in `src/pn-tables.js` hinterlegter Hersteller). Das
|
vom Barcode-Weg mehr, sondern die einzige Quelle dieser Funktion — sie
|
||||||
Kamerabild wird dafür in Graustufen gewandelt und mit einem
|
liest ohnehin nie einen Barcode (siehe Punkt 0). Das Kamerabild wird dafür
|
||||||
**Otsu-Schwellwert** in Schwarz/Weiß aufbereitet: Der Schwellwert wird aus
|
in Graustufen gewandelt und mit einem **Otsu-Schwellwert** in Schwarz/Weiß
|
||||||
der Helligkeitsverteilung des gesamten Bildes bestimmt, nicht aus einer
|
aufbereitet: Der Schwellwert wird aus der Helligkeitsverteilung des
|
||||||
festen Kontrastspreizung zwischen hellstem und dunkelstem Pixel. Das macht
|
gesamten Bildes bestimmt, nicht aus einer festen Kontrastspreizung
|
||||||
die Aufbereitung robust gegen einzelne Lichtreflexe auf glänzenden
|
zwischen hellstem und dunkelstem Pixel. Das macht die Aufbereitung robust
|
||||||
Metalletiketten, die eine reine Min/Max-Spreizung leicht kippen würden.
|
gegen einzelne Lichtreflexe auf glänzenden Metalletiketten, die eine reine
|
||||||
Das OCR-Ergebnis wird anschließend gegen die bekannten Spec-Werte
|
Min/Max-Spreizung leicht kippen würden. Das OCR-Ergebnis wird anschließend
|
||||||
abgeglichen, was die typischen Verwechslungen (0/O, 1/I, 8/B, …) auflöst.
|
gegen die bekannten Spec-Werte abgeglichen, was die typischen
|
||||||
|
Verwechslungen (0/O, 1/I, 8/B, …) auflöst. Da in dieser Funktion nie eine
|
||||||
|
per Barcode gelesene Teilenummer vorliegt, entscheiden hier ausschließlich
|
||||||
|
die aus dem erkannten Text abgeleiteten technischen Angaben (Kapazität,
|
||||||
|
Bauform, Rank, Geschwindigkeit) über die Stapelzuordnung
|
||||||
|
(`specsCompatible` in `src/spec.js`) — anders als in „Strichcode"/
|
||||||
|
„QR-Code", wo meist die exakte Teilenummer trägt.
|
||||||
4. **Stapel-Zuweisung.** Grün (Barcode) und Gelb (OCR) laufen ohne Eingabe
|
4. **Stapel-Zuweisung.** Grün (Barcode) und Gelb (OCR) laufen ohne Eingabe
|
||||||
durch; nur bei roter Konfidenz (nichts Eindeutiges erkannt, widersprüchliche
|
durch; nur bei roter Konfidenz (nichts Eindeutiges erkannt, widersprüchliche
|
||||||
Barcodes oder widersprüchliche unbekannte Barcodes ohne jede OCR-Kapazität)
|
Barcodes oder widersprüchliche unbekannte Barcodes ohne jede OCR-Kapazität)
|
||||||
@@ -133,14 +185,19 @@ korrekt funktioniert.
|
|||||||
npm test
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
124 Tests, alle grün (Stand dieses Dokuments). Getestet werden ausschließlich
|
143 Tests, alle grün (Stand dieses Dokuments). Getestet werden ausschließlich
|
||||||
die reinen Module ohne Browser-Zugriff: Teilenummer-Decoder, Spec-Normalisierung
|
die reinen Module ohne Browser-Zugriff: Teilenummer-Decoder, Spec-Normalisierung
|
||||||
und toleranter Vergleich, OCR-Bildaufbereitung (Otsu-Schwellwert) und
|
und toleranter Vergleich, OCR-Bildaufbereitung (Otsu-Schwellwert) und
|
||||||
-Feldextraktion, Stapel-Zuweisung, die Erkennungs-Pipeline samt Zeitgrenzen
|
-Feldextraktion, Stapel-Zuweisung, die Erkennungs-Pipeline samt Zeitgrenzen
|
||||||
und Mehrdeutigkeitsbehandlung, sowie die Sitzungssicherung samt Prüfung eines
|
und Mehrdeutigkeitsbehandlung, die Sitzungssicherung samt Prüfung eines
|
||||||
wiederhergestellten Zustands. Kamera, Barcode-/OCR-Adapter selbst und
|
wiederhergestellten Zustands, die drei Scan-Funktionen (`scan-modes.js` —
|
||||||
Oberfläche laufen nur im echten Browser und werden dort manuell geprüft
|
Codearten je Funktion, kein DataMatrix in „QR-Code") sowie die je Funktion
|
||||||
(siehe „Am Gerät noch zu prüfen" unten) — `node --test` kennt kein DOM.
|
tatsächlich aufgerufenen Adapter (`scan-recognition.js` — Texterkennung läuft
|
||||||
|
nachweislich nie in „Strichcode"/„QR-Code", Barcode-Dekodierung nachweislich
|
||||||
|
nie in „Text erkennen"). Kamera, Barcode-/OCR-Adapter selbst, die
|
||||||
|
Auswahl-Oberfläche und die Verdrahtung in `main.js` laufen nur im echten
|
||||||
|
Browser und werden dort manuell geprüft (siehe „Am Gerät noch zu prüfen"
|
||||||
|
unten) — `node --test` kennt kein DOM.
|
||||||
|
|
||||||
## Produktion
|
## Produktion
|
||||||
|
|
||||||
@@ -189,7 +246,9 @@ worauf du bei der Veröffentlichung achten solltest.
|
|||||||
|
|
||||||
- Vanilla JavaScript (ES Modules), kein Framework
|
- Vanilla JavaScript (ES Modules), kein Framework
|
||||||
- [Vite](https://vite.dev/) als Entwicklungsserver und Bündler
|
- [Vite](https://vite.dev/) als Entwicklungsserver und Bündler
|
||||||
- [`zxing-wasm`](https://github.com/Sec-ant/zxing-wasm) für Code-128 und DataMatrix
|
- [`zxing-wasm`](https://github.com/Sec-ant/zxing-wasm) zur Barcode-Dekodierung;
|
||||||
|
welche Codearten gesucht werden, gibt die gewählte Funktion vor
|
||||||
|
(`src/scan-modes.js`)
|
||||||
- [`tesseract.js`](https://tesseract.projectnaptha.com/) für Texterkennung
|
- [`tesseract.js`](https://tesseract.projectnaptha.com/) für Texterkennung
|
||||||
- `node:test` für Tests, ohne zusätzliche Test-Bibliothek
|
- `node:test` für Tests, ohne zusätzliche Test-Bibliothek
|
||||||
|
|
||||||
@@ -202,15 +261,18 @@ worauf du bei der Veröffentlichung achten solltest.
|
|||||||
| `src/pn-decoder.js` | Teilenummer → Spec-Felder |
|
| `src/pn-decoder.js` | Teilenummer → Spec-Felder |
|
||||||
| `src/ocr-extract.js` | OCR-Rohtext → Spec-Felder |
|
| `src/ocr-extract.js` | OCR-Rohtext → Spec-Felder |
|
||||||
| `src/session.js` | Stapel halten, vorschlagen, buchen, umsortieren, zurücknehmen — inklusive der aus den Einträgen abgeleiteten, je Stapel bekannten Barcode-Inhalte (`stack.codes`), die vor dem Vergleich der technischen Angaben über die Zuordnung entscheiden, sofern diese Angaben nicht widersprechen |
|
| `src/session.js` | Stapel halten, vorschlagen, buchen, umsortieren, zurücknehmen — inklusive der aus den Einträgen abgeleiteten, je Stapel bekannten Barcode-Inhalte (`stack.codes`), die vor dem Vergleich der technischen Angaben über die Zuordnung entscheiden, sofern diese Angaben nicht widersprechen |
|
||||||
| `src/pipeline.js` | Barcode → Teilenummer → OCR → Ampelfarbe, inkl. Zeitgrenzen, Mehrdeutigkeitsbehandlung und Kurzschluss über einen bereits bekannten Barcode-Inhalt (`deps.isKnownCode`) |
|
| `src/pipeline.js` | Barcode → Teilenummer → OCR → Ampelfarbe, inkl. Zeitgrenzen, Mehrdeutigkeitsbehandlung und Kurzschluss über einen bereits bekannten Barcode-Inhalt (`deps.isKnownCode`) — unverändert; kennt keine Scan-Funktionen |
|
||||||
|
| `src/scan-modes.js` | Die drei Scan-Funktionen (Strichcode, QR-Code, Text erkennen): Kennung, deutsche Beschriftung, gelesene Codearten, laufende Suche ja/nein, Texterkennung ja/nein — einzige Quelle, sowohl für die Auswahl-Oberfläche als auch für `main.js` |
|
||||||
|
| `src/scan-recognition.js` | Baut die an `recognize()` übergebenen Adapter anhand der gewählten Funktion: in „Strichcode"/„QR-Code" liefert die Texterkennung sofort leeren Text ohne Tesseract anzustoßen, in „Text erkennen" liefert die Barcode-Dekodierung sofort eine leere Liste ohne `zxing-wasm` anzustoßen |
|
||||||
| `src/storage.js` | Absturzschutz der laufenden Sitzung: sichert und lädt aus `localStorage`, verwirft beim Laden jeden in sich unstimmigen Zustand vollständig (siehe unten) |
|
| `src/storage.js` | Absturzschutz der laufenden Sitzung: sichert und lädt aus `localStorage`, verwirft beim Laden jeden in sich unstimmigen Zustand vollständig (siehe unten) |
|
||||||
| `src/camera.js` | Kamerastart, Einzelbildaufnahme, Bilddatei-Ersatzweg |
|
| `src/camera.js` | Kamerastart, Einzelbildaufnahme, Bilddatei-Ersatzweg |
|
||||||
| `src/barcode.js` | Adapter zu `zxing-wasm` |
|
| `src/barcode.js` | Adapter zu `zxing-wasm`; `decodeBarcodes(imageData, formats)` bekommt die Codearten übergeben, ohne Angabe wie bisher (Code128 + DataMatrix) |
|
||||||
| `src/ocr.js` | Bildaufbereitung (Otsu) und Adapter zu `tesseract.js` |
|
| `src/ocr.js` | Bildaufbereitung (Otsu) und Adapter zu `tesseract.js` |
|
||||||
| `src/ui/scan-view.js` | Scan-Ansicht: Kamera-Vorschau, Scan-Knopf, Stapel-Leiste, „Zuletzt"-Zeile, Ersatzweg-Schaltfläche bei fehlender Kamera |
|
| `src/ui/scan-view.js` | Scan-Ansicht: Kamera-Vorschau, antippbare Anzeige der gewählten Funktion (Wechsel), Scan-Knopf (nur in „Text erkennen" sichtbar), Stapel-Leiste, „Zuletzt"-Zeile, Ersatzweg-Schaltfläche bei fehlender Kamera |
|
||||||
|
| `src/ui/mode-dialog.js` | Vollbild-Auswahl der Scan-Funktion vor dem Start und beim Wechsel: drei große Flächen, eine je Funktion aus `src/scan-modes.js` |
|
||||||
| `src/ui/result-overlay.js` | Kurze Treffer-Rückmeldung (grün/gelb), blendet sich nach kurzer Zeit selbst wieder aus |
|
| `src/ui/result-overlay.js` | Kurze Treffer-Rückmeldung (grün/gelb), blendet sich nach kurzer Zeit selbst wieder aus |
|
||||||
| `src/ui/ambiguous-dialog.js` | Rot-Dialog bei roter Konfidenz oder mehrdeutiger Stapelzuordnung |
|
| `src/ui/ambiguous-dialog.js` | Rot-Dialog bei roter Konfidenz oder mehrdeutiger Stapelzuordnung |
|
||||||
| `src/ui/resume-dialog.js` | Dialog beim Start: gesicherte Sitzung fortsetzen oder verwerfen |
|
| `src/ui/resume-dialog.js` | Dialog beim Start: gesicherte Sitzung fortsetzen oder verwerfen — läuft vor der Funktionswahl |
|
||||||
| `src/ui/session-list.js` | Sitzungsliste: Stapel-Übersicht, Umsortieren, Entfernen, Sitzung beenden |
|
| `src/ui/session-list.js` | Sitzungsliste: Stapel-Übersicht, Umsortieren, Entfernen, Sitzung beenden |
|
||||||
| `src/ui/describe-spec.js` | Gemeinsame Kurzbeschreibung eines Specs (Kapazität, Rank, Geschwindigkeit, Bauform), wahlweise mit Platzhaltern für fehlende Felder; von Treffer-Rückmeldung, „Zuletzt"-Zeile und Sitzungsliste gemeinsam genutzt |
|
| `src/ui/describe-spec.js` | Gemeinsame Kurzbeschreibung eines Specs (Kapazität, Rank, Geschwindigkeit, Bauform), wahlweise mit Platzhaltern für fehlende Felder; von Treffer-Rückmeldung, „Zuletzt"-Zeile und Sitzungsliste gemeinsam genutzt |
|
||||||
| `src/main.js` | Verdrahtung aller Module zur lauffähigen App |
|
| `src/main.js` | Verdrahtung aller Module zur lauffähigen App |
|
||||||
@@ -218,11 +280,11 @@ worauf du bei der Veröffentlichung achten solltest.
|
|||||||
| `server.py` | Auslieferung im Betrieb: statischer Server für `dist/` mit Passwortabfrage vor jeder Datei, plus passwortfreier Gesundheitspfad `/healthz`. Nur Python-Standardbibliothek, keine zusätzliche Abhängigkeit |
|
| `server.py` | Auslieferung im Betrieb: statischer Server für `dist/` mit Passwortabfrage vor jeder Datei, plus passwortfreier Gesundheitspfad `/healthz`. Nur Python-Standardbibliothek, keine zusätzliche Abhängigkeit |
|
||||||
| `.vch/deploy.yaml` | Bau- und Startbefehl sowie Gesundheitspfad für die Hosting-Umgebung |
|
| `.vch/deploy.yaml` | Bau- und Startbefehl sowie Gesundheitspfad für die Hosting-Umgebung |
|
||||||
|
|
||||||
`spec`, `pn-decoder`, `ocr-extract`, `session`, `pipeline` und `storage` sind
|
`spec`, `pn-decoder`, `ocr-extract`, `session`, `pipeline`, `storage`,
|
||||||
reine Funktionen ohne Browser-Zugriff (kein `window`, `document` oder
|
`scan-modes` und `scan-recognition` sind reine Funktionen ohne Browser-Zugriff
|
||||||
`localStorage` direkt) und deshalb vollständig mit `node:test` prüfbar.
|
(kein `window`, `document` oder `localStorage` direkt) und deshalb vollständig
|
||||||
`camera.js`, `barcode.js`, `ocr.js` und `src/ui/` brauchen einen echten
|
mit `node:test` prüfbar. `camera.js`, `barcode.js`, `ocr.js` und `src/ui/`
|
||||||
Browser und werden nur manuell geprüft.
|
brauchen einen echten Browser und werden nur manuell geprüft.
|
||||||
|
|
||||||
### Sitzungssicherung im Detail
|
### Sitzungssicherung im Detail
|
||||||
|
|
||||||
@@ -325,40 +387,68 @@ werden:
|
|||||||
- Datei-Auswahl als Ersatzweg (ohne Kamera) öffnet den nativen Dialog und
|
- Datei-Auswahl als Ersatzweg (ohne Kamera) öffnet den nativen Dialog und
|
||||||
hinterlässt keine leere Fläche im Layout.
|
hinterlässt keine leere Fläche im Layout.
|
||||||
|
|
||||||
|
**Funktionswahl (neu)**
|
||||||
|
- Erster Start ohne gesicherte Sitzung: Auswahl mit drei großen Flächen
|
||||||
|
(„Strichcode", „QR-Code", „Text erkennen") erscheint, *bevor* die Kamera
|
||||||
|
startet — kein Kamerabild sichtbar, solange keine Funktion gewählt ist.
|
||||||
|
- Erster Start *mit* gesicherter Sitzung: erst die Frage „Gesicherte Sitzung
|
||||||
|
gefunden" (fortsetzen/verwerfen), erst danach die Funktionswahl.
|
||||||
|
- „Strichcode" wählen: laufende Suche erkennt Code-128/Code-39/EAN/UPC/…,
|
||||||
|
kein „Modul scannen"-Knopf sichtbar, Zielrahmen hebt sich bei erkanntem
|
||||||
|
Code hervor.
|
||||||
|
- „QR-Code" wählen und ein Etikett mit dem eigenen DataMatrix-Code vor die
|
||||||
|
Kamera halten: **bewusst kein Treffer** (kein DataMatrix in dieser
|
||||||
|
Funktion) — Rahmen bleibt neutral. Einen echten QR-Code vor dieselbe
|
||||||
|
Funktion halten: Rahmen hebt sich hervor, Treffer wird gebucht.
|
||||||
|
- „Text erkennen" wählen: kein automatisches Hervorheben des Rahmens (er
|
||||||
|
bleibt durchgehend schlicht), „Modul scannen"-Knopf sichtbar und löst die
|
||||||
|
Texterkennung aus.
|
||||||
|
- Anzeige der gewählten Funktion in der Scan-Ansicht antippen: Auswahl
|
||||||
|
erscheint erneut, nach der Wahl gilt der Wechsel sofort — laufende Suche
|
||||||
|
startet/stoppt, Knopf erscheint/verschwindet, **ohne** die Seite neu zu
|
||||||
|
laden.
|
||||||
|
- Anzeige der gewählten Funktion aus rund einem Meter Entfernung lesbar.
|
||||||
|
|
||||||
**Barcode- und OCR-Erkennung**
|
**Barcode- und OCR-Erkennung**
|
||||||
- Samsung-Referenzmodul scannen: Code-128 *und* DataMatrix werden erkannt,
|
- Samsung-Referenzmodul in Funktion „Strichcode" scannen:
|
||||||
`M386A8K40BM1-CRC4Y` wird korrekt als Teilenummer übernommen, grüne
|
`M386A8K40BM1-CRC4Y` wird korrekt als Teilenummer übernommen, grüne
|
||||||
Rückmeldung mit `STAPEL A`, `64GB 4DRx4 PC4-2400 LRDIMM`.
|
Rückmeldung mit `STAPEL A`, `64GB 4DRx4 PC4-2400 LRDIMM`.
|
||||||
- Etikett ohne lesbaren Barcode fotografieren: OCR-Weg greift, die
|
- In Funktion „Text erkennen" ein Etikett fotografieren und „Modul scannen"
|
||||||
Otsu-Aufbereitung liefert auf einem echten, glänzenden Etikettenfoto
|
antippen: die Otsu-Aufbereitung liefert auf einem echten, glänzenden
|
||||||
tatsächlich brauchbaren Text (bisher nur an synthetischen Testbildern
|
Etikettenfoto tatsächlich brauchbaren Text (bisher nur an synthetischen
|
||||||
geprüft, nie an einem echten Foto).
|
Testbildern geprüft, nie an einem echten Foto).
|
||||||
- Ladezeit des WASM-Barcode-Moduls (`zxing-wasm`) beim allerersten Scan einer
|
- Ladezeit des WASM-Barcode-Moduls (`zxing-wasm`) beim allerersten Scan einer
|
||||||
Sitzung beobachten — bleibt sie deutlich unter der 10-Sekunden-Zeitgrenze
|
Sitzung in Funktion „Strichcode" oder „QR-Code" beobachten — bleibt sie
|
||||||
der Pipeline, und braucht ein zweiter, schnell nachfolgender Scan nicht
|
deutlich unter der 10-Sekunden-Zeitgrenze der Pipeline, und braucht ein
|
||||||
erneut die volle Ladezeit (Beleg, dass die Vorbereitung tatsächlich nur
|
zweiter, schnell nachfolgender Scan nicht erneut die volle Ladezeit (Beleg,
|
||||||
einmal läuft)?
|
dass die Vorbereitung tatsächlich nur einmal läuft)? In Funktion „Text
|
||||||
|
erkennen" darf dieses Laden dagegen gar nicht erst anlaufen, da dort nie
|
||||||
|
`decodeBarcodes` aufgerufen wird.
|
||||||
- Verbindung während des allerersten Scans einer Sitzung unterbrechen (WASM
|
- Verbindung während des allerersten Scans einer Sitzung unterbrechen (WASM
|
||||||
lädt per CDN), danach mit wiederhergestellter Verbindung erneut scannen:
|
lädt per CDN), danach mit wiederhergestellter Verbindung erneut scannen:
|
||||||
Das muss einen echten neuen Ladeversuch auslösen statt dauerhaft mit
|
Das muss einen echten neuen Ladeversuch auslösen statt dauerhaft mit
|
||||||
demselben Fehler zu scheitern.
|
demselben Fehler zu scheitern.
|
||||||
- Ladezeit des Tesseract-Arbeiters beim allerersten Scan einer Sitzung
|
- Ladezeit des Tesseract-Arbeiters beim ersten Antippen von „Modul scannen"
|
||||||
beobachten — bleibt sie auf einem normalen Handy deutlich unter der
|
in Funktion „Text erkennen" beobachten — bleibt sie auf einem normalen
|
||||||
20-Sekunden-Zeitgrenze der Pipeline?
|
Handy deutlich unter der 20-Sekunden-Zeitgrenze der Pipeline? In den
|
||||||
|
Funktionen „Strichcode"/„QR-Code" darf dieses Laden nie anlaufen, auch
|
||||||
|
nicht bei einem gänzlich unbekannten Etikett, da `runOcr` dort nie
|
||||||
|
aufgerufen wird.
|
||||||
- Verfügbarkeitsanzeige der Texterkennung: bleibt nach einem erfolgreichen
|
- Verfügbarkeitsanzeige der Texterkennung: bleibt nach einem erfolgreichen
|
||||||
Ladevorgang dauerhaft "verfügbar", springt nach einem erzwungenen
|
Ladevorgang dauerhaft "verfügbar", springt nach einem erzwungenen
|
||||||
Fehlschlag (z. B. Flugmodus beim ersten Laden) beim nächsten Erfolg wieder
|
Fehlschlag (z. B. Flugmodus beim ersten Laden) beim nächsten Erfolg wieder
|
||||||
darauf zurück?
|
darauf zurück?
|
||||||
- Stichprobe, ob die feste Zeichen-Whitelist der Texterkennung auf echten
|
- Stichprobe, ob die feste Zeichen-Whitelist der Texterkennung auf echten
|
||||||
Etiketten keine tatsächlich benötigten Zeichen ausschließt.
|
Etiketten keine tatsächlich benötigten Zeichen ausschließt.
|
||||||
- Bild ohne lesbaren Barcode und ohne verwertbaren OCR-Text: Rot-Dialog
|
- Funktion „Strichcode"/„QR-Code": kein passender Code im Bild führt zu
|
||||||
erscheint statt Fehler oder Absturz.
|
Rot-Dialog (statt Fehler oder Absturz) — hier ohne jeden OCR-Versuch.
|
||||||
- Modul mit unbekanntem Nummernschema (z. B. ein Hynix-Modul) scannen: grüne
|
Funktion „Text erkennen": kein verwertbarer OCR-Text führt ebenfalls zu
|
||||||
Rückmeldung statt Rot, auch wenn keine Kapazität abgeleitet werden kann.
|
Rot, hier ohne jeden Barcode-Versuch.
|
||||||
Dasselbe Modul ein zweites Mal scannen: landet auf demselben Stapel — und
|
- In Funktion „Strichcode" ein Modul mit unbekanntem Nummernschema (z. B. ein
|
||||||
die Rückmeldung erscheint spürbar schneller als beim ersten Scan, weil die
|
Hynix-Modul) scannen: grüne Rückmeldung statt Rot, auch wenn keine
|
||||||
Texterkennung diesmal übersprungen wird (nur am Gerät beobachtbar, da
|
Kapazität abgeleitet werden kann (die Texterkennung ist in dieser Funktion
|
||||||
`node --test` keine echte Ladezeit misst).
|
ohnehin stumm geschaltet und liefert nie eine Kapazität nach). Dasselbe
|
||||||
|
Modul ein zweites Mal scannen: landet auf demselben Stapel.
|
||||||
- Etikett mit zwei Barcodes (Teile- *und* Seriennummer) mehrfach scannen:
|
- Etikett mit zwei Barcodes (Teile- *und* Seriennummer) mehrfach scannen:
|
||||||
beide Scans landen auf demselben Stapel, obwohl die Seriennummer bei realen
|
beide Scans landen auf demselben Stapel, obwohl die Seriennummer bei realen
|
||||||
Modulen nie identisch ist — das belegt, dass die Teilenummer und nicht die
|
Modulen nie identisch ist — das belegt, dass die Teilenummer und nicht die
|
||||||
|
|||||||
+11
-3
@@ -25,8 +25,14 @@ async function ensureReady() {
|
|||||||
await readyPromise;
|
await readyPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bisheriges Verhalten (Code-128 und DataMatrix), falls der Aufrufer keine
|
||||||
|
// eigene Liste von Codearten angibt - main.js reicht seit den Scan-Funktionen
|
||||||
|
// (siehe scan-modes.js) stets eine eigene Liste durch, aber ohne Angabe
|
||||||
|
// bleibt der Adapter unveraendert nutzbar (z. B. fuer kuenftige Aufrufer).
|
||||||
|
const DEFAULT_FORMATS = ['Code128', 'DataMatrix'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dekodiert Code-128 und DataMatrix aus einem Einzelbild.
|
* Dekodiert Barcodes aus einem Einzelbild.
|
||||||
* Barcode-Dekodierung ist exakt - deshalb ist sie die bevorzugte Quelle
|
* Barcode-Dekodierung ist exakt - deshalb ist sie die bevorzugte Quelle
|
||||||
* gegenueber der fehleranfaelligen Texterkennung.
|
* gegenueber der fehleranfaelligen Texterkennung.
|
||||||
*
|
*
|
||||||
@@ -35,12 +41,14 @@ async function ensureReady() {
|
|||||||
* Ausgang und kein Fehler.
|
* Ausgang und kein Fehler.
|
||||||
*
|
*
|
||||||
* @param {ImageData} imageData Einzelbild aus der Kamera.
|
* @param {ImageData} imageData Einzelbild aus der Kamera.
|
||||||
|
* @param {string[]} [formats] Zu suchende Codearten (zxing-wasm-Formatnamen,
|
||||||
|
* siehe scan-modes.js). Ohne Angabe wie bisher: Code128 und DataMatrix.
|
||||||
* @returns {Promise<string[]>}
|
* @returns {Promise<string[]>}
|
||||||
*/
|
*/
|
||||||
export async function decodeBarcodes(imageData) {
|
export async function decodeBarcodes(imageData, formats = DEFAULT_FORMATS) {
|
||||||
await ensureReady();
|
await ensureReady();
|
||||||
const results = await readBarcodes(imageData, {
|
const results = await readBarcodes(imageData, {
|
||||||
formats: ['Code128', 'DataMatrix'],
|
formats,
|
||||||
tryHarder: true,
|
tryHarder: true,
|
||||||
maxNumberOfSymbols: 4,
|
maxNumberOfSymbols: 4,
|
||||||
});
|
});
|
||||||
|
|||||||
+79
-3
@@ -5,6 +5,8 @@ import { decodeBarcodes } from './barcode.js';
|
|||||||
import { runOcr, isOcrAvailable } from './ocr.js';
|
import { runOcr, isOcrAvailable } from './ocr.js';
|
||||||
import { recognize } from './pipeline.js';
|
import { recognize } from './pipeline.js';
|
||||||
import { normalizeToken } from './spec.js';
|
import { normalizeToken } from './spec.js';
|
||||||
|
import { getScanMode } from './scan-modes.js';
|
||||||
|
import { buildRecognitionAdapters } from './scan-recognition.js';
|
||||||
import {
|
import {
|
||||||
createSession, proposeAssignment, commitAssignment,
|
createSession, proposeAssignment, commitAssignment,
|
||||||
undoLast, moveEntry, removeEntry, nextStackId,
|
undoLast, moveEntry, removeEntry, nextStackId,
|
||||||
@@ -14,6 +16,7 @@ import { renderScanView } from './ui/scan-view.js';
|
|||||||
import { showResult } from './ui/result-overlay.js';
|
import { showResult } from './ui/result-overlay.js';
|
||||||
import { askForStack } from './ui/ambiguous-dialog.js';
|
import { askForStack } from './ui/ambiguous-dialog.js';
|
||||||
import { askResumeSession } from './ui/resume-dialog.js';
|
import { askResumeSession } from './ui/resume-dialog.js';
|
||||||
|
import { askForScanMode } from './ui/mode-dialog.js';
|
||||||
import { renderSessionList } from './ui/session-list.js';
|
import { renderSessionList } from './ui/session-list.js';
|
||||||
import { describeSpec } from './ui/describe-spec.js';
|
import { describeSpec } from './ui/describe-spec.js';
|
||||||
|
|
||||||
@@ -38,6 +41,14 @@ let busy = false;
|
|||||||
// Scan-Durchlaufs ueberschrieben werden.
|
// Scan-Durchlaufs ueberschrieben werden.
|
||||||
let cameraUnavailableMessage = null;
|
let cameraUnavailableMessage = null;
|
||||||
|
|
||||||
|
// Vor dem ersten Start der Kamera waehlt der Nutzer eine der drei Scan-
|
||||||
|
// Funktionen (siehe scan-modes.js); erst danach steht diese Kennung fest.
|
||||||
|
// Waehrend applyMode() noch nicht gelaufen ist, darf weder die Dauersuche
|
||||||
|
// noch ein Scan-Durchlauf ausgeloest werden - beide sind bis dahin durch das
|
||||||
|
// Auswahl-Overlay (searchPaused()) bzw. durch busy/den fehlenden Kamerastart
|
||||||
|
// ohnehin unerreichbar.
|
||||||
|
let currentModeId = null;
|
||||||
|
|
||||||
const view = renderScanView(app, {
|
const view = renderScanView(app, {
|
||||||
onCapture: () => processCapture(() => grabFrame(view.video)),
|
onCapture: () => processCapture(() => grabFrame(view.video)),
|
||||||
onUndo: () => {
|
onUndo: () => {
|
||||||
@@ -56,8 +67,44 @@ const view = renderScanView(app, {
|
|||||||
openSessionList();
|
openSessionList();
|
||||||
},
|
},
|
||||||
onPickFile: (file) => processCapture(() => imageDataFromFile(file)),
|
onPickFile: (file) => processCapture(() => imageDataFromFile(file)),
|
||||||
|
onChangeMode: () => {
|
||||||
|
// Waehrend eine Erkennung laeuft, bleibt auch der Funktionswechsel
|
||||||
|
// unerreichbar - aus demselben Grund wie bei Rueckgaengig/Sitzungsliste.
|
||||||
|
if (busy) return;
|
||||||
|
openModeDialog();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uebernimmt eine (neu gewaehlte) Scan-Funktion: Anzeige, Sichtbarkeit von
|
||||||
|
* "Modul scannen" und - bei einem Wechsel weg von laufender Codesuche - der
|
||||||
|
* Zielrahmen wird sofort zurueckgesetzt, statt auf den naechsten Durchlauf
|
||||||
|
* der Dauersuche zu warten.
|
||||||
|
* @param {string} modeId
|
||||||
|
*/
|
||||||
|
function applyMode(modeId) {
|
||||||
|
currentModeId = modeId;
|
||||||
|
const mode = getScanMode(modeId);
|
||||||
|
view.setMode(mode.label);
|
||||||
|
// "Modul scannen" hat nur dort etwas zu tun, wo es keine laufende Suche
|
||||||
|
// gibt (Funktion 3) - siehe Kommentar in scan-view.js/setCaptureVisible.
|
||||||
|
view.setCaptureVisible(!mode.continuousSearch);
|
||||||
|
if (!mode.continuousSearch) {
|
||||||
|
view.setFrameDetected(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oeffnet die Funktionswahl (auch fuer den Wechsel aus der laufenden
|
||||||
|
* Scan-Ansicht heraus, nicht nur beim Start). Solange das Overlay offen ist,
|
||||||
|
* pausiert die Dauersuche von selbst (siehe searchPaused()).
|
||||||
|
*/
|
||||||
|
async function openModeDialog() {
|
||||||
|
closeOverlays();
|
||||||
|
const modeId = await askForScanMode(app, { current: currentModeId });
|
||||||
|
applyMode(modeId);
|
||||||
|
}
|
||||||
|
|
||||||
/** Spiegelt den Sitzungsstand in die Oberflaeche und sichert ihn gegen Neuladen ab. */
|
/** Spiegelt den Sitzungsstand in die Oberflaeche und sichert ihn gegen Neuladen ab. */
|
||||||
function syncView() {
|
function syncView() {
|
||||||
view.setStacks(session.stacks);
|
view.setStacks(session.stacks);
|
||||||
@@ -98,9 +145,14 @@ async function processCapture(getFrame) {
|
|||||||
(stack) => Array.isArray(stack.codes) && stack.codes.includes(normalized),
|
(stack) => Array.isArray(stack.codes) && stack.codes.includes(normalized),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
// Welcher Adapter tatsaechlich etwas tut, haengt von der gewaehlten
|
||||||
|
// Scan-Funktion ab (siehe scan-modes.js/scan-recognition.js) - recognize()
|
||||||
|
// selbst (pipeline.js) bleibt unveraendert und kennt keine Funktionen.
|
||||||
|
const { decodeBarcodes: decode, runOcr: recognizeText } =
|
||||||
|
buildRecognitionAdapters(currentModeId, { decodeBarcodes, runOcr });
|
||||||
const { spec, source, confidence, codes } = await recognize(
|
const { spec, source, confidence, codes } = await recognize(
|
||||||
frame,
|
frame,
|
||||||
{ decodeBarcodes, runOcr, isKnownCode },
|
{ decodeBarcodes: decode, runOcr: recognizeText, isKnownCode },
|
||||||
);
|
);
|
||||||
|
|
||||||
const plan = proposeAssignment(targetSession, spec, codes);
|
const plan = proposeAssignment(targetSession, spec, codes);
|
||||||
@@ -218,6 +270,11 @@ function searchPaused() {
|
|||||||
* nichts tun.
|
* nichts tun.
|
||||||
*/
|
*/
|
||||||
async function runSearchAttempt() {
|
async function runSearchAttempt() {
|
||||||
|
// Funktion 3 (Text erkennen) hat keine laufende Suche - siehe scan-modes.js.
|
||||||
|
// Ein Wechsel dorthin setzt den Rahmen bereits in applyMode() zurueck;
|
||||||
|
// hier genuegt es, schlicht nichts zu tun.
|
||||||
|
if (!getScanMode(currentModeId).continuousSearch) return;
|
||||||
|
|
||||||
if (searchPaused()) {
|
if (searchPaused()) {
|
||||||
view.setFrameDetected(false);
|
view.setFrameDetected(false);
|
||||||
return;
|
return;
|
||||||
@@ -235,7 +292,7 @@ async function runSearchAttempt() {
|
|||||||
|
|
||||||
let codes;
|
let codes;
|
||||||
try {
|
try {
|
||||||
codes = await decodeBarcodes(region);
|
codes = await decodeBarcodes(region, getScanMode(currentModeId).barcodeFormats);
|
||||||
} catch {
|
} catch {
|
||||||
view.setFrameDetected(false);
|
view.setFrameDetected(false);
|
||||||
return;
|
return;
|
||||||
@@ -331,10 +388,14 @@ function openSessionList() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Startet die Kamera und, sobald sie brauchbare Bilder liefert, die Dauersuche. */
|
||||||
|
function startCameraAndSearch() {
|
||||||
startCamera(view.video)
|
startCamera(view.video)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Erst ab hier liefert die Kamera brauchbare Bilder - die Dauersuche
|
// Erst ab hier liefert die Kamera brauchbare Bilder - die Dauersuche
|
||||||
// laeuft fortan im Hintergrund, ohne dass der Nutzer etwas tun muss.
|
// laeuft fortan im Hintergrund, ohne dass der Nutzer etwas tun muss.
|
||||||
|
// runSearchAttempt() selbst haelt sich zurueck, wenn die gewaehlte
|
||||||
|
// Funktion keine laufende Suche vorsieht (Funktion 3).
|
||||||
continuousSearchLoop();
|
continuousSearchLoop();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -345,6 +406,7 @@ startCamera(view.video)
|
|||||||
view.setStatus(cameraUnavailableMessage, true);
|
view.setStatus(cameraUnavailableMessage, true);
|
||||||
view.setFilePickerVisible(true);
|
view.setFilePickerVisible(true);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bietet eine beim Start gefundene, gesicherte Sitzung zur Fortsetzung an,
|
* Bietet eine beim Start gefundene, gesicherte Sitzung zur Fortsetzung an,
|
||||||
@@ -367,4 +429,18 @@ async function initResumeOffer() {
|
|||||||
syncView();
|
syncView();
|
||||||
}
|
}
|
||||||
|
|
||||||
initResumeOffer();
|
/**
|
||||||
|
* Ablauf beim Start: erst - falls vorhanden - die Frage nach dem Fortsetzen
|
||||||
|
* einer gesicherten Sitzung, danach die Wahl der Scan-Funktion, und erst
|
||||||
|
* danach die Kamera. So sieht der Nutzer die Funktionswahl nie, bevor die
|
||||||
|
* Fortsetzen-Frage (falls noetig) beantwortet ist, und die Kamera startet
|
||||||
|
* nie, bevor feststeht, welche Codearten sie ueberhaupt suchen soll.
|
||||||
|
*/
|
||||||
|
async function boot() {
|
||||||
|
await initResumeOffer();
|
||||||
|
const modeId = await askForScanMode(app);
|
||||||
|
applyMode(modeId);
|
||||||
|
startCameraAndSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
boot();
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// Die drei Scan-Funktionen, die der Nutzer vor dem Scannen waehlt. Reines
|
||||||
|
// Modul ohne Browser-Zugriff (kein window/document/navigator) - einzige
|
||||||
|
// Quelle fuer Kennung, Beschriftung, Codearten und Verhalten je Funktion.
|
||||||
|
// Sowohl die Auswahl-Oberflaeche als auch main.js lesen ausschliesslich von
|
||||||
|
// hier - es darf keine zweite Stelle geben, an der Codearten oder
|
||||||
|
// Beschriftungen stehen.
|
||||||
|
//
|
||||||
|
// Ausdrueckliche Entscheidung des Auftraggebers (nicht abaendern): Funktion 2
|
||||||
|
// liest streng nur QR (QRCode, MicroQRCode, RMQRCode) - kein DataMatrix, auch
|
||||||
|
// wenn die 2D-Codes auf den eigenen RAM-Etiketten DataMatrix sind. Der
|
||||||
|
// Arbeitsablauf des Auftraggebers laeuft ueber Funktion 1 (Teilenummer im
|
||||||
|
// Strichcode); Funktion 2 dient anderer Ware.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ScanMode
|
||||||
|
* @property {string} id Kennung, intern verwendet (main.js, Speicherung der Wahl).
|
||||||
|
* @property {string} label Deutsche Beschriftung fuer die Oberflaeche.
|
||||||
|
* @property {string[]} barcodeFormats An decodeBarcodes() zu uebergebende
|
||||||
|
* Codearten (zxing-wasm-Formatnamen). Leer, wenn die Funktion keine
|
||||||
|
* Barcode-Dekodierung nutzt.
|
||||||
|
* @property {boolean} continuousSearch Ob waehrend die Kamera laeuft
|
||||||
|
* fortwaehrend nach Codes gesucht wird (statt nur auf Knopfdruck).
|
||||||
|
* @property {boolean} useOcr Ob die Texterkennung (Tesseract) genutzt wird.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {ScanMode[]} */
|
||||||
|
export const SCAN_MODES = [
|
||||||
|
{
|
||||||
|
id: 'barcode',
|
||||||
|
label: 'Strichcode',
|
||||||
|
// Eindimensionale Codes, wie sie auf Teilenummern-Etiketten vorkommen.
|
||||||
|
barcodeFormats: ['Code128', 'Code39', 'Code93', 'ITF', 'EAN13', 'EAN8', 'UPCA', 'UPCE', 'Codabar'],
|
||||||
|
continuousSearch: true,
|
||||||
|
useOcr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'qrcode',
|
||||||
|
label: 'QR-Code',
|
||||||
|
// Ausschliesslich QR-Varianten - bewusst kein DataMatrix, siehe Kommentar oben.
|
||||||
|
barcodeFormats: ['QRCode', 'MicroQRCode', 'RMQRCode'],
|
||||||
|
continuousSearch: true,
|
||||||
|
useOcr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ocr',
|
||||||
|
label: 'Text erkennen',
|
||||||
|
// Keine Codes - allein die Texterkennung, auf Knopfdruck.
|
||||||
|
barcodeFormats: [],
|
||||||
|
continuousSearch: false,
|
||||||
|
useOcr: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert die Funktion zu einer Kennung.
|
||||||
|
* @param {string} id
|
||||||
|
* @returns {ScanMode}
|
||||||
|
* @throws {Error} wenn die Kennung keiner der drei Funktionen entspricht -
|
||||||
|
* ein Programmfehler (main.js darf nie eine fremde Kennung durchreichen),
|
||||||
|
* kein regulaerer Ausgang.
|
||||||
|
*/
|
||||||
|
export function getScanMode(id) {
|
||||||
|
const mode = SCAN_MODES.find((candidate) => candidate.id === id);
|
||||||
|
if (!mode) {
|
||||||
|
throw new Error(`Unbekannte Scan-Funktion: ${id}`);
|
||||||
|
}
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Baut die Adapter fuer recognize() (pipeline.js) anhand der gewaehlten
|
||||||
|
// Scan-Funktion (siehe scan-modes.js). Reines Modul ohne Browser-Zugriff -
|
||||||
|
// direkt unter Node pruefbar. pipeline.js selbst bleibt unveraendert: die
|
||||||
|
// Funktionen unterscheiden sich ausschliesslich darin, welche Adapter
|
||||||
|
// hineingereicht werden.
|
||||||
|
//
|
||||||
|
// Funktionen 1/2 (laufende Codesuche): echte Barcode-Dekodierung mit den zur
|
||||||
|
// Funktion gehoerenden Codearten; die Texterkennung liefert sofort leeren
|
||||||
|
// Text, ohne dass runOcr ueberhaupt aufgerufen wird.
|
||||||
|
// Funktion 3 (Text erkennen): echte Texterkennung; die Barcode-Dekodierung
|
||||||
|
// liefert sofort eine leere Liste, ohne dass decodeBarcodes ueberhaupt
|
||||||
|
// aufgerufen wird.
|
||||||
|
|
||||||
|
import { getScanMode } from './scan-modes.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} modeId Kennung aus scan-modes.js.
|
||||||
|
* @param {{decodeBarcodes: Function, runOcr: Function}} adapters echte,
|
||||||
|
* browser-gebundene Adapter (aus barcode.js / ocr.js).
|
||||||
|
* @returns {{decodeBarcodes: Function, runOcr: Function}} an recognize()
|
||||||
|
* durchzureichende Adapter, passend zur gewaehlten Funktion.
|
||||||
|
*/
|
||||||
|
export function buildRecognitionAdapters(modeId, adapters) {
|
||||||
|
const mode = getScanMode(modeId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
decodeBarcodes: mode.useOcr
|
||||||
|
? async () => []
|
||||||
|
: (imageData) => adapters.decodeBarcodes(imageData, mode.barcodeFormats),
|
||||||
|
runOcr: mode.useOcr
|
||||||
|
? adapters.runOcr
|
||||||
|
: async () => '',
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -21,6 +21,19 @@ body {
|
|||||||
|
|
||||||
#app { display: flex; flex-direction: column; height: 100dvh; }
|
#app { display: flex; flex-direction: column; height: 100dvh; }
|
||||||
|
|
||||||
|
.mode-button {
|
||||||
|
min-height: 56px;
|
||||||
|
width: 100%;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: #1c2027;
|
||||||
|
color: var(--fg);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.camera { position: relative; flex: 1; overflow: hidden; background: #000; }
|
.camera { position: relative; flex: 1; overflow: hidden; background: #000; }
|
||||||
.camera video { width: 100%; height: 100%; object-fit: cover; }
|
.camera video { width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
|
||||||
@@ -115,5 +128,22 @@ body {
|
|||||||
.choices { display: grid; gap: 12px; width: 100%; max-width: 420px; }
|
.choices { display: grid; gap: 12px; width: 100%; max-width: 420px; }
|
||||||
.choices button { min-height: 64px; font-size: 20px; }
|
.choices button { min-height: 64px; font-size: 20px; }
|
||||||
|
|
||||||
|
/* Auswahl der Scan-Funktion vor dem Scannen (mode-dialog.js): drei grosse,
|
||||||
|
gleich gestaltete Flaechen statt schmaler Schaltflaechen. */
|
||||||
|
.mode-tile {
|
||||||
|
min-height: 96px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-tile-label { font-size: 24px; font-weight: 700; }
|
||||||
|
.mode-tile-hint { font-size: 14px; opacity: 0.8; }
|
||||||
|
|
||||||
|
.mode-tile-current { outline: 3px solid var(--fg); outline-offset: -3px; }
|
||||||
|
|
||||||
.status { padding: 8px 12px; font-size: 14px; color: var(--muted); }
|
.status { padding: 8px 12px; font-size: 14px; color: var(--muted); }
|
||||||
.status.warn { color: #f0b429; }
|
.status.warn { color: #f0b429; }
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { SCAN_MODES } from '../scan-modes.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kurzer Hinweis je Funktion, direkt aus ihren Merkmalen abgeleitet (siehe
|
||||||
|
* scan-modes.js) - keine zweite Stelle mit eigenem Text je Funktion.
|
||||||
|
*/
|
||||||
|
function describeMode(mode) {
|
||||||
|
if (mode.useOcr) return 'Auf Knopfdruck';
|
||||||
|
return 'Läuft von selbst, sobald ein Code im Bild ist';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vollbild-Auswahl der Scan-Funktion: drei grosse Flaechen, eine je Funktion
|
||||||
|
* aus scan-modes.js (einzige Quelle fuer Kennung und Beschriftung). Gebaut
|
||||||
|
* nach demselben Muster wie die anderen Vollbild-Dialoge (ambiguous-dialog.js,
|
||||||
|
* resume-dialog.js): Geschwister waehrend der Anzeige per inert unerreichbar,
|
||||||
|
* Fokus kehrt danach zurueck. Reine Darstellung - kennt weder Kamera noch
|
||||||
|
* Erkennung.
|
||||||
|
*
|
||||||
|
* Wird sowohl vor dem ersten Start der Kamera verwendet als auch fuer den
|
||||||
|
* Wechsel aus der laufenden Scan-Ansicht heraus (siehe main.js).
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} root
|
||||||
|
* @param {{current?: string}} [args] Kennung der aktuell gewaehlten Funktion,
|
||||||
|
* falls es sich um einen Wechsel handelt - hebt die passende Flaeche hervor.
|
||||||
|
* @returns {Promise<string>} Kennung der gewaehlten Funktion (siehe scan-modes.js).
|
||||||
|
*/
|
||||||
|
export function askForScanMode(root, { current } = {}) {
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'overlay';
|
||||||
|
overlay.style.background = 'var(--bg)';
|
||||||
|
overlay.setAttribute('role', 'dialog');
|
||||||
|
overlay.setAttribute('aria-modal', 'true');
|
||||||
|
overlay.setAttribute('aria-labelledby', 'mode-dialog-heading');
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<div class="stack-id" id="mode-dialog-heading" style="font-size:28px">Funktion wählen</div>
|
||||||
|
<div class="choices mode-choices" id="choices"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const choices = overlay.querySelector('#choices');
|
||||||
|
|
||||||
|
// Siehe ambiguous-dialog.js / resume-dialog.js: waehrend der Dialog offen
|
||||||
|
// ist, werden die Geschwisterelemente per inert unerreichbar gemacht und
|
||||||
|
// danach wieder freigegeben.
|
||||||
|
const previouslyFocused = document.activeElement;
|
||||||
|
const madeInert = [];
|
||||||
|
for (const sibling of root.children) {
|
||||||
|
if (!sibling.hasAttribute('inert')) {
|
||||||
|
sibling.setAttribute('inert', '');
|
||||||
|
madeInert.push(sibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root.appendChild(overlay);
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
const finish = (modeId) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
overlay.remove();
|
||||||
|
for (const sibling of madeInert) {
|
||||||
|
sibling.removeAttribute('inert');
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
previouslyFocused &&
|
||||||
|
typeof previouslyFocused.focus === 'function' &&
|
||||||
|
document.contains(previouslyFocused)
|
||||||
|
) {
|
||||||
|
previouslyFocused.focus();
|
||||||
|
} else {
|
||||||
|
root.focus?.();
|
||||||
|
}
|
||||||
|
resolve(modeId);
|
||||||
|
};
|
||||||
|
|
||||||
|
let firstButton = null;
|
||||||
|
for (const mode of SCAN_MODES) {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.className = 'action mode-tile';
|
||||||
|
if (mode.id === current) button.classList.add('mode-tile-current');
|
||||||
|
button.setAttribute('aria-pressed', String(mode.id === current));
|
||||||
|
button.innerHTML = `
|
||||||
|
<span class="mode-tile-label">${mode.label}</span>
|
||||||
|
<span class="mode-tile-hint"></span>
|
||||||
|
`;
|
||||||
|
button.querySelector('.mode-tile-hint').textContent = describeMode(mode);
|
||||||
|
button.addEventListener('click', () => finish(mode.id));
|
||||||
|
choices.appendChild(button);
|
||||||
|
firstButton = firstButton ?? button;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstButton?.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
+29
-3
@@ -4,14 +4,15 @@ import { FRAME_INSET } from '../camera.js';
|
|||||||
* Baut die Hauptansicht auf und liefert Aktualisierungsfunktionen zurueck.
|
* Baut die Hauptansicht auf und liefert Aktualisierungsfunktionen zurueck.
|
||||||
* Kennt weder Kamera noch Erkennung - alles kommt ueber die Rueckrufe.
|
* Kennt weder Kamera noch Erkennung - alles kommt ueber die Rueckrufe.
|
||||||
*/
|
*/
|
||||||
export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile }) {
|
export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile, onChangeMode }) {
|
||||||
root.innerHTML = `
|
root.innerHTML = `
|
||||||
|
<button class="mode-button" id="mode-button" type="button"></button>
|
||||||
<div class="camera">
|
<div class="camera">
|
||||||
<video id="preview" playsinline muted></video>
|
<video id="preview" playsinline muted></video>
|
||||||
<div class="frame"></div>
|
<div class="frame"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status" id="status"></div>
|
<div class="status" id="status"></div>
|
||||||
<button class="action capture" id="capture">Modul scannen</button>
|
<button class="action capture" id="capture" hidden>Modul scannen</button>
|
||||||
<button class="action secondary" id="pick-file-button" hidden>Bild auswählen</button>
|
<button class="action secondary" id="pick-file-button" hidden>Bild auswählen</button>
|
||||||
<div class="stacks" id="stacks"></div>
|
<div class="stacks" id="stacks"></div>
|
||||||
<div class="last">
|
<div class="last">
|
||||||
@@ -27,6 +28,8 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
|
|||||||
const fileEl = root.querySelector('#file');
|
const fileEl = root.querySelector('#file');
|
||||||
const pickFileButtonEl = root.querySelector('#pick-file-button');
|
const pickFileButtonEl = root.querySelector('#pick-file-button');
|
||||||
const frameEl = root.querySelector('.frame');
|
const frameEl = root.querySelector('.frame');
|
||||||
|
const captureEl = root.querySelector('#capture');
|
||||||
|
const modeButtonEl = root.querySelector('#mode-button');
|
||||||
|
|
||||||
// FRAME_INSET (camera.js) ist die einzige Quelle der Rahmenmasse - sowohl
|
// FRAME_INSET (camera.js) ist die einzige Quelle der Rahmenmasse - sowohl
|
||||||
// fuer diesen sichtbaren Rahmen als auch fuer den nativen Ausschnitt
|
// fuer diesen sichtbaren Rahmen als auch fuer den nativen Ausschnitt
|
||||||
@@ -36,8 +39,9 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
|
|||||||
`${FRAME_INSET.top * 100}% ${FRAME_INSET.right * 100}% ` +
|
`${FRAME_INSET.top * 100}% ${FRAME_INSET.right * 100}% ` +
|
||||||
`${FRAME_INSET.bottom * 100}% ${FRAME_INSET.left * 100}%`;
|
`${FRAME_INSET.bottom * 100}% ${FRAME_INSET.left * 100}%`;
|
||||||
|
|
||||||
root.querySelector('#capture').addEventListener('click', onCapture);
|
captureEl.addEventListener('click', onCapture);
|
||||||
root.querySelector('#undo').addEventListener('click', onUndo);
|
root.querySelector('#undo').addEventListener('click', onUndo);
|
||||||
|
modeButtonEl.addEventListener('click', onChangeMode);
|
||||||
// Klick auf die sichtbare Schaltflaeche ist selbst die Nutzergeste, die
|
// Klick auf die sichtbare Schaltflaeche ist selbst die Nutzergeste, die
|
||||||
// die versteckte Dateiauswahl oeffnen darf - anders als ein Aufruf aus dem
|
// die versteckte Dateiauswahl oeffnen darf - anders als ein Aufruf aus dem
|
||||||
// Code heraus, den Handy-Browser blockieren.
|
// Code heraus, den Handy-Browser blockieren.
|
||||||
@@ -74,6 +78,28 @@ export function renderScanView(root, { onCapture, onUndo, onOpenList, onPickFile
|
|||||||
lastEl.textContent = text;
|
lastEl.textContent = text;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zeigt die gewaehlte Scan-Funktion in der Ansicht an - antippbar, um zu
|
||||||
|
* wechseln (siehe onChangeMode). Beschriftung kommt vom Aufrufer, der sie
|
||||||
|
* seinerseits aus scan-modes.js liest; diese Ansicht kennt die Funktionen
|
||||||
|
* selbst nicht.
|
||||||
|
* @param {string} label
|
||||||
|
*/
|
||||||
|
setMode(label) {
|
||||||
|
modeButtonEl.textContent = `${label} ändern ⌄`;
|
||||||
|
modeButtonEl.setAttribute('aria-label', `Scan-Funktion wechseln (aktuell: ${label})`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Modul scannen" hat nur in Funktionen mit Texterkennung auf Knopfdruck
|
||||||
|
* etwas zu tun (Funktion 3) - in Funktionen mit laufender Codesuche sucht
|
||||||
|
* die App ohnehin durchgehend, ein Knopf dafuer waere wirkungslos.
|
||||||
|
* @param {boolean} visible
|
||||||
|
*/
|
||||||
|
setCaptureVisible(visible) {
|
||||||
|
captureEl.hidden = !visible;
|
||||||
|
},
|
||||||
|
|
||||||
/** @param {string} text @param {boolean} warn */
|
/** @param {string} text @param {boolean} warn */
|
||||||
setStatus(text, warn = false) {
|
setStatus(text, warn = false) {
|
||||||
statusEl.textContent = text;
|
statusEl.textContent = text;
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { SCAN_MODES, getScanMode } from '../src/scan-modes.js';
|
||||||
|
|
||||||
|
test('genau drei Funktionen in fester Reihenfolge', () => {
|
||||||
|
assert.equal(SCAN_MODES.length, 3);
|
||||||
|
assert.deepEqual(SCAN_MODES.map((mode) => mode.id), ['barcode', 'qrcode', 'ocr']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Funktion 1 (Strichcode): eindimensionale Codearten, laufende Suche, keine Texterkennung', () => {
|
||||||
|
const mode = getScanMode('barcode');
|
||||||
|
assert.equal(mode.label, 'Strichcode');
|
||||||
|
assert.deepEqual(mode.barcodeFormats, [
|
||||||
|
'Code128', 'Code39', 'Code93', 'ITF', 'EAN13', 'EAN8', 'UPCA', 'UPCE', 'Codabar',
|
||||||
|
]);
|
||||||
|
assert.equal(mode.continuousSearch, true);
|
||||||
|
assert.equal(mode.useOcr, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Funktion 2 (QR-Code): ausschliesslich QR-Varianten, kein DataMatrix', () => {
|
||||||
|
const mode = getScanMode('qrcode');
|
||||||
|
assert.equal(mode.label, 'QR-Code');
|
||||||
|
assert.deepEqual(mode.barcodeFormats, ['QRCode', 'MicroQRCode', 'RMQRCode']);
|
||||||
|
assert.ok(!mode.barcodeFormats.includes('DataMatrix'), 'Funktion 2 darf kein DataMatrix lesen');
|
||||||
|
assert.equal(mode.continuousSearch, true);
|
||||||
|
assert.equal(mode.useOcr, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Funktion 3 (Text erkennen): keine Codearten, keine laufende Suche, Texterkennung', () => {
|
||||||
|
const mode = getScanMode('ocr');
|
||||||
|
assert.equal(mode.label, 'Text erkennen');
|
||||||
|
assert.deepEqual(mode.barcodeFormats, []);
|
||||||
|
assert.equal(mode.continuousSearch, false);
|
||||||
|
assert.equal(mode.useOcr, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unbekannte Kennung wirft statt undefined zurueckzugeben', () => {
|
||||||
|
assert.throws(() => getScanMode('unbekannt'));
|
||||||
|
});
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { buildRecognitionAdapters } from '../src/scan-recognition.js';
|
||||||
|
import { recognize } from '../src/pipeline.js';
|
||||||
|
|
||||||
|
test('Funktion 1: Texterkennung wird nicht aufgerufen, auch wenn kein Code gefunden wird', async () => {
|
||||||
|
let ocrCalled = false;
|
||||||
|
const adapters = buildRecognitionAdapters('barcode', {
|
||||||
|
decodeBarcodes: async () => [],
|
||||||
|
runOcr: async () => { ocrCalled = true; return 'irgendein Text'; },
|
||||||
|
});
|
||||||
|
const result = await recognize({}, adapters);
|
||||||
|
assert.equal(ocrCalled, false, 'runOcr darf in Funktion 1 nie aufgerufen werden');
|
||||||
|
assert.equal(result.confidence, 'red');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Funktion 2: Texterkennung wird nicht aufgerufen, auch wenn kein Code gefunden wird', async () => {
|
||||||
|
let ocrCalled = false;
|
||||||
|
const adapters = buildRecognitionAdapters('qrcode', {
|
||||||
|
decodeBarcodes: async () => [],
|
||||||
|
runOcr: async () => { ocrCalled = true; return 'irgendein Text'; },
|
||||||
|
});
|
||||||
|
const result = await recognize({}, adapters);
|
||||||
|
assert.equal(ocrCalled, false, 'runOcr darf in Funktion 2 nie aufgerufen werden');
|
||||||
|
assert.equal(result.confidence, 'red');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Funktion 3: Barcode-Dekodierung wird nicht aufgerufen', async () => {
|
||||||
|
let barcodeCalled = false;
|
||||||
|
const adapters = buildRecognitionAdapters('ocr', {
|
||||||
|
decodeBarcodes: async () => { barcodeCalled = true; return []; },
|
||||||
|
runOcr: async () => '64GB 4DRx4 PC4-2400T-LD1-11-MC0 M386A8K40BM1-CRC4Y 1908',
|
||||||
|
});
|
||||||
|
const result = await recognize({}, adapters);
|
||||||
|
assert.equal(barcodeCalled, false, 'decodeBarcodes darf in Funktion 3 nie aufgerufen werden');
|
||||||
|
assert.equal(result.source, 'ocr');
|
||||||
|
assert.equal(result.confidence, 'yellow');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Funktion 1: ein gelesener Strichcode fuehrt zu Gruen', async () => {
|
||||||
|
const adapters = buildRecognitionAdapters('barcode', {
|
||||||
|
decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y'],
|
||||||
|
runOcr: async () => '',
|
||||||
|
});
|
||||||
|
const result = await recognize({}, adapters);
|
||||||
|
assert.equal(result.source, 'barcode');
|
||||||
|
assert.equal(result.confidence, 'green');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Funktion 3: kein brauchbarer Text fuehrt zu Rot, nicht zu Gruen', async () => {
|
||||||
|
const adapters = buildRecognitionAdapters('ocr', {
|
||||||
|
decodeBarcodes: async () => { throw new Error('darf nicht aufgerufen werden'); },
|
||||||
|
runOcr: async () => 'Made in Philippines',
|
||||||
|
});
|
||||||
|
const result = await recognize({}, adapters);
|
||||||
|
assert.equal(result.source, 'none');
|
||||||
|
assert.equal(result.confidence, 'red');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decodeBarcodes bekommt die Codearten der jeweiligen Funktion uebergeben', async () => {
|
||||||
|
let receivedFormats = null;
|
||||||
|
const adapters = buildRecognitionAdapters('qrcode', {
|
||||||
|
decodeBarcodes: async (_imageData, formats) => { receivedFormats = formats; return []; },
|
||||||
|
runOcr: async () => '',
|
||||||
|
});
|
||||||
|
await adapters.decodeBarcodes({});
|
||||||
|
assert.deepEqual(receivedFormats, ['QRCode', 'MicroQRCode', 'RMQRCode']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Funktion 3 uebergibt keine Codearten, weil decodeBarcodes gar nicht aufgerufen wird', async () => {
|
||||||
|
let called = false;
|
||||||
|
const adapters = buildRecognitionAdapters('ocr', {
|
||||||
|
decodeBarcodes: async () => { called = true; return []; },
|
||||||
|
runOcr: async () => '',
|
||||||
|
});
|
||||||
|
const codes = await adapters.decodeBarcodes({});
|
||||||
|
assert.deepEqual(codes, []);
|
||||||
|
assert.equal(called, false);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user