diff --git a/README.md b/README.md index 2c3f9a2..7815f5c 100755 --- a/README.md +++ b/README.md @@ -50,18 +50,32 @@ Das Modul bietet drei Modi für die mobile Barcode-Erfassung: ## Barcode-Unterstützung -Das Modul sucht Barcodes in folgender Reihenfolge: +Das Modul sucht Barcodes/Artikelnummern in folgender Reihenfolge: 1. Produkt-Barcode (`llx_product.barcode`) 2. Lieferanten-Barcode (`llx_product_fournisseur_price.barcode`) -3. Produkt-Referenz (`llx_product.ref`) +3. **Lieferanten-Artikelnummer** (`llx_product_fournisseur_price.ref_fourn`) ← **NEU ab v9.0** +4. Produkt-Referenz (`llx_product.ref`) -Unterstützte Barcode-Formate (Quagga2): -- **Code 128** (empfohlen für alphanumerische Codes, höchste Priorität) -- **Code 39** (alphanumerisch, zweite Priorität) -- **EAN-13** (nur Ziffern, dritte Priorität) -- **EAN-8** (nur Ziffern, niedrigste Priorität) +### Barcode-Formate (ZXing-JS, ab v8.3) -**WICHTIG:** Reader-Reihenfolge optimiert für alphanumerische Codes! CODE128/CODE39 haben Vorrang vor EAN-Readern, um Fehlerkennungen bei Herstellercodes wie "P20260030" zu vermeiden. +**Unterstützte Formate - wählbar per Dropdown:** +- **Code 128** - alphanumerisch (1D-Barcode) +- **QR-Code** - 2D-Matrix-Code +- **DataMatrix** - 2D-Matrix-Code + +**Typ-Umschaltung ohne Kamera-Neustart:** +Der Barcode-Typ kann während des Scannens gewechselt werden. Die Kamera bleibt aktiv, nur der Decoder wird neu initialisiert. Die Auswahl wird persistent in localStorage gespeichert. + +### OCR-Modus für Artikelnummern (ab v8.5, optimiert in v9.1) + +**Für Etiketten OHNE Barcode:** +- Foto-basierte Texterkennung mit Tesseract.js +- Variable Ziffern-Anzahl (5-12 stellig) per Dropdown wählbar +- Erkennt **zusammenhängende** N-stellige Zahlen (z.B. 7-stellige Artikelnummer) +- Mehrere Treffer → Auswahl-Dialog +- Keine Treffer → Fallback auf manuelle Produktsuche + +**Wichtig:** OCR erkennt nur **isolierte** Zahlenblöcke mit exakt N Ziffern. Keine Subsequenzen oder Zusammensetzungen aus längeren Zahlen! ## Installation @@ -186,8 +200,9 @@ handybarcodescanner/ ``` ### Verwendete Bibliotheken -- [Quagga2](https://github.com/ericblade/quagga2) - Browser-basierte Barcode-Erkennung -- [JsBarcode](https://github.com/lindell/JsBarcode) - Barcode-Generierung für Druck +- [ZXing-JS](https://github.com/zxing-js/library) v0.21.3 - Browser-basierte Barcode-Erkennung (1D + 2D) +- [Tesseract.js](https://github.com/naptha/tesseract.js) v5.1.0 - OCR (Optical Character Recognition) für Artikelnummern +- [JsBarcode](https://github.com/lindell/JsBarcode) v3.11.6 - Barcode-Generierung für Druck - [ZXing](https://github.com/zxing/zxing) - Barcode-Generierung in Android App ### PWA (Progressive Web App) @@ -203,7 +218,15 @@ Das Modul ist als PWA konzipiert mit: - Firefox auf Android: Nur Shortcuts, kein echter Standalone-Modus ⚠️ - Safari auf iOS: PWA-Unterstützung mit Einschränkungen -**Versionsverwaltung:** Bei Updates CACHE_NAME in `sw.js` + `?v=` in `pwa.php` synchron erhöhen! +**Versionsverwaltung:** Bei Updates IMMER synchron erhöhen: +- `CACHE_NAME` in `sw.js` (z.B. `scanner-v9.1`) +- `?v=` Parameter in `pwa.php` für CSS und JS (z.B. `?v=91`) +- `$this->version` in `modHandyBarcodeScanner.class.php` (z.B. `9.1`) + +**Aktuelle Version: 9.1** +- v9.1: OCR-Feature Bug-Fix (extractDigitSequences: nur zusammenhängende Zahlenblöcke) +- v9.0: OCR-Feature für Artikelnummern-Erkennung +- v8.3: ZXing-JS Integration (Code 128, QR, DataMatrix) ### Brother PT-E560BT Android App diff --git a/android-app/BrotherPrintHelper-v2.apk b/android-app/BrotherPrintHelper-v2.apk new file mode 100755 index 0000000..f538458 Binary files /dev/null and b/android-app/BrotherPrintHelper-v2.apk differ diff --git a/core/modules/modHandyBarcodeScanner.class.php b/core/modules/modHandyBarcodeScanner.class.php index 37513b3..1e900cb 100755 --- a/core/modules/modHandyBarcodeScanner.class.php +++ b/core/modules/modHandyBarcodeScanner.class.php @@ -76,7 +76,7 @@ class modHandyBarcodeScanner extends DolibarrModules $this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@handybarcodescanner' // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z' - $this->version = '8.2'; + $this->version = '9.1'; // Url to the file with your last numberversion of this module //$this->url_last_version = 'http://www.example.com/versionmodule.txt'; diff --git a/js/scanner.js b/js/scanner.js index 091a81f..e16f018 100755 --- a/js/scanner.js +++ b/js/scanner.js @@ -44,9 +44,18 @@ let currentProduct = null; let selectedSupplier = null; let allSuppliers = []; - let quaggaInitialized = false; + let scannerInitialized = false; let openDialogCount = 0; // Zaehlt offene Dialoge + // ZXing Scanner State + let currentBarcodeType = 'CODE_128'; // Aktuell gewahlter Barcode-Typ + let currentScanner = null; // ZXing BrowserMultiFormatReader instance + + // OCR State (Tesseract.js) + let ocrWorker = null; // Singleton Worker + let ocrInitialized = false; + let currentDigitCount = 8; // Anzahl Ziffern für OCR-Erkennung + // Persistente Einstellungen - eine zentrale Config statt einzelner Keys const STORAGE_KEY = 'hbs_config'; function loadConfig() { @@ -133,6 +142,49 @@ window._showProductSearchModal = showProductSearchModal; window._showFreetextModal = showFreetextModal; window._showOrdersPanel = showOrdersPanel; + + // History-Button Event-Handler + const historyBtn = document.getElementById('ctrl-history'); + if (historyBtn) { + historyBtn.addEventListener('click', showBarcodeHistory); + } + + // Barcode-Typ-Dropdown Event-Handler + const typeSelect = document.getElementById('barcode-type-select'); + if (typeSelect) { + // Gespeicherten Typ laden + const savedType = loadConfig().barcodeType || 'CODE_128'; + typeSelect.value = savedType; + currentBarcodeType = savedType; + + // Initiale Scan-Region-Farbe setzen + updateScanRegionColor(savedType); + + // OCR-Digit-Selector initial ein/ausblenden + const digitSelector = document.getElementById('ocr-digit-selector'); + if (digitSelector) { + digitSelector.classList.toggle('hidden', savedType !== 'OCR'); + } + + // Event-Listener fuer Typ-Wechsel + typeSelect.addEventListener('change', function() { + switchBarcodeType(this.value); + }); + } + + // OCR Digit-Count Dropdown Handler + const digitSelect = document.getElementById('ocr-digit-count'); + if (digitSelect) { + const savedCount = loadConfig().ocrDigitCount || 8; + digitSelect.value = savedCount; + currentDigitCount = savedCount; + + digitSelect.addEventListener('change', function() { + currentDigitCount = parseInt(this.value); + saveConfig('ocrDigitCount', currentDigitCount); + showToast('OCR: ' + currentDigitCount + ' Ziffern', 'success'); + }); + } } // Check camera support - just log warnings, never disable button @@ -143,9 +195,9 @@ console.warn('HandyBarcodeScanner: HTTPS empfohlen fuer Kamerazugriff'); } - // Check if Quagga is loaded - if (typeof Quagga === 'undefined') { - console.error('HandyBarcodeScanner: Quagga nicht geladen'); + // Check if ZXing is loaded + if (typeof ZXing === 'undefined') { + console.error('HandyBarcodeScanner: ZXing nicht geladen'); } // Check mediaDevices @@ -224,10 +276,17 @@ return; } - // Check if Quagga is available - if (typeof Quagga === 'undefined') { - showToast('Quagga Bibliothek nicht geladen', 'error'); - return; + // Check if libraries are available + if (currentBarcodeType === 'OCR') { + if (typeof Tesseract === 'undefined') { + showToast('Tesseract.js Bibliothek nicht geladen', 'error'); + return; + } + } else { + if (typeof ZXing === 'undefined') { + showToast('ZXing Bibliothek nicht geladen', 'error'); + return; + } } // Disable button while initializing @@ -240,8 +299,14 @@ // Permission granted - stop the test stream stream.getTracks().forEach(track => track.stop()); - // Now start Quagga - initQuagga(); + // OCR-Modus oder Barcode-Modus? + if (currentBarcodeType === 'OCR') { + // OCR-Modus: Kamera-Preview + "Foto aufnehmen" Button + startCameraPreview(); + } else { + // Barcode-Modus: ZXing Video-Stream + currentScanner = initZXing(currentBarcodeType); + } }) .catch(function(err) { console.error('Camera permission error:', err); @@ -260,68 +325,63 @@ }); } - function initQuagga() { - Quagga.init({ - inputStream: { - name: "Live", - type: "LiveStream", - target: elements.videoContainer, - constraints: { - facingMode: "environment", - width: { min: 640, ideal: 1280, max: 1920 }, - height: { min: 480, ideal: 720, max: 1080 } - }, - area: { // Scan-Bereich: groesserer Bereich fuer kleine Barcodes - top: "10%", - right: "5%", - left: "5%", - bottom: "10%" + function getBarcodeFormat(type) { + const formats = { + 'CODE_128': ZXing.BarcodeFormat.CODE_128, + 'QR_CODE': ZXing.BarcodeFormat.QR_CODE, + 'DATA_MATRIX': ZXing.BarcodeFormat.DATA_MATRIX, + 'CODE_39': ZXing.BarcodeFormat.CODE_39, + 'EAN_13': ZXing.BarcodeFormat.EAN_13, + 'EAN_8': ZXing.BarcodeFormat.EAN_8 + }; + return formats[type] || ZXing.BarcodeFormat.CODE_128; + } + + function initZXing(format) { + format = format || currentBarcodeType; + + try { + const hints = new Map(); + const possibleFormats = [getBarcodeFormat(format)]; + hints.set(ZXing.DecodeHintType.POSSIBLE_FORMATS, possibleFormats); + + const codeReader = new ZXing.BrowserMultiFormatReader(hints); + + codeReader.decodeFromVideoDevice( + undefined, // Auto-select Kamera + elements.video, + (result, err) => { + if (result) { + onBarcodeDetected(result); + } + // Errors werden ignoriert (passiert bei jedem Frame ohne Code) } - }, - decoder: { - readers: [ - "code_128_reader", // Best for alphanumeric codes - "code_39_reader", // Also good for alphanumeric (e.g. P20260030) - "ean_reader", // Numeric only - lower priority - "ean_8_reader" // Numeric only - lower priority - ], - multiple: false - }, - locate: true, - locator: { - patchSize: "medium", // medium = besser fuer kleine Barcodes - halfSample: false // volle Aufloesung = bessere Genauigkeit - }, - numOfWorkers: navigator.hardwareConcurrency || 4, - frequency: 20 // haeufiger scannen fuer bessere Erkennung - }, function(err) { - if (err) { - console.error('Quagga init error:', err); + ).then(() => { + // Scanner erfolgreich gestartet + scannerInitialized = true; + isScanning = true; + elements.startBtn.disabled = false; + elements.startBtn.classList.add('hidden'); + elements.stopBtn.classList.remove('hidden'); + elements.videoContainer.classList.add('scanning'); + + // Timeout-Timer starten + startScanTimeout(); + }).catch((err) => { + console.error('ZXing init error:', err); elements.startBtn.disabled = false; elements.startBtn.textContent = CONFIG.lang.startScan || 'Scan starten'; showToast(CONFIG.lang.cameraError || 'Kamerafehler', 'error'); - return; - } + }); - // Success - start scanning - Quagga.start(); - quaggaInitialized = true; - isScanning = true; + return codeReader; + } catch (e) { + console.error('ZXing initialization error:', e); elements.startBtn.disabled = false; - elements.startBtn.classList.add('hidden'); - elements.stopBtn.classList.remove('hidden'); - elements.videoContainer.classList.add('scanning'); - - // Quagga-Canvas darf keine Klicks abfangen - const canvases = elements.videoContainer.querySelectorAll('canvas'); - canvases.forEach(function(c) { c.style.pointerEvents = 'none'; }); - - // Register detection handler - Quagga.onDetected(onBarcodeDetected); - - // Timeout-Timer starten - startScanTimeout(); - }); + elements.startBtn.textContent = CONFIG.lang.startScan || 'Scan starten'; + showToast('Scanner konnte nicht initialisiert werden', 'error'); + return null; + } } // Timeout-Hinweis wenn nichts erkannt wird @@ -339,18 +399,29 @@ function stopScanner() { clearTimeout(scanTimeoutTimer); - if (quaggaInitialized) { + // OCR-Modus: Video-Stream manuell stoppen + if (currentBarcodeType === 'OCR') { + const video = elements.video; + if (video && video.srcObject) { + video.srcObject.getTracks().forEach(track => track.stop()); + video.srcObject = null; + } + // Button-Click-Handler zurücksetzen + elements.startBtn.onclick = null; + } + + if (currentScanner && scannerInitialized) { try { - Quagga.offDetected(onBarcodeDetected); - Quagga.stop(); + currentScanner.reset(); } catch (e) { - console.log('Quagga stop error:', e); + console.log('Scanner stop error:', e); } } isScanning = false; isPaused = false; - quaggaInitialized = false; + scannerInitialized = false; + currentScanner = null; elements.startBtn.classList.remove('hidden'); elements.startBtn.disabled = false; elements.startBtn.textContent = CONFIG.lang.startScan || 'Scannen'; @@ -359,19 +430,12 @@ } // Scanner pausieren (bei Dialog-Oeffnung) + // ZXing hat kein natives pause() - wir nutzen ein Flag und ignorieren Detections function pauseScanner() { if (!isScanning || isPaused) return; openDialogCount++; - - if (quaggaInitialized) { - try { - Quagga.pause(); - isPaused = true; - console.log('Scanner paused, dialogs:', openDialogCount); - } catch (e) { - console.log('Quagga pause error:', e); - } - } + isPaused = true; + console.log('Scanner paused, dialogs:', openDialogCount); } // Scanner fortsetzen (wenn alle Dialoge geschlossen) @@ -385,29 +449,376 @@ if (!isScanning || !isPaused) return; - if (quaggaInitialized) { - try { - Quagga.start(); - isPaused = false; - console.log('Scanner resumed'); - startScanTimeout(); - } catch (e) { - console.log('Quagga resume error:', e); - } - } + isPaused = false; + console.log('Scanner resumed'); + startScanTimeout(); } // Globale Funktionen fuer PWA window._pauseScanner = pauseScanner; window._resumeScanner = resumeScanner; + // ===== OCR-Funktionen (Tesseract.js) ===== + + // Kamera-Preview starten (ohne Barcode-Detection) + function startCameraPreview() { + const video = elements.video; + + navigator.mediaDevices.getUserMedia({ + video: { facingMode: 'environment' } + }) + .then(function(stream) { + video.srcObject = stream; + video.play(); + + isScanning = true; + scannerInitialized = true; + // OCR: Start-Button NICHT verstecken, sondern als "Foto aufnehmen" nutzen + elements.startBtn.classList.remove('hidden'); + elements.startBtn.textContent = 'Foto aufnehmen'; + elements.stopBtn.classList.remove('hidden'); + elements.videoContainer.classList.add('scanning'); + elements.startBtn.disabled = false; + + // Button-Click auf Snapshot-Capture umstellen + elements.startBtn.onclick = captureOCRSnapshot; + + console.log('OCR Kamera-Preview gestartet'); + }) + .catch(function(err) { + console.error('OCR Camera error:', err); + showToast('Kamerafehler: ' + err.message, 'error'); + elements.startBtn.disabled = false; + elements.startBtn.textContent = 'Scannen'; + }); + } + + // Foto vom Video-Stream aufnehmen und OCR durchführen + function captureOCRSnapshot() { + const video = elements.video; + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + // Canvas-Größe = Video-Größe + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + // Video-Frame auf Canvas zeichnen + context.drawImage(video, 0, 0, canvas.width, canvas.height); + + // Canvas zu Blob konvertieren + canvas.toBlob(function(blob) { + if (blob) { + performOCR(blob); + } else { + showToast('Fehler beim Erstellen des Fotos', 'error'); + } + }, 'image/jpeg', 0.9); + } + + // Tesseract Worker initialisieren (Singleton, Lazy) + async function initOCRWorker() { + if (ocrInitialized && ocrWorker) { + return ocrWorker; + } + + try { + console.log('Initialisiere Tesseract Worker...'); + ocrWorker = await Tesseract.createWorker('eng'); + + // Nur Ziffern erkennen, einfache Einstellungen + await ocrWorker.setParameters({ + tessedit_char_whitelist: '0123456789' + // Keine PSM/OEM - Tesseract-Defaults verwenden + }); + + ocrInitialized = true; + console.log('Tesseract Worker bereit'); + return ocrWorker; + } catch (e) { + console.error('OCR Worker Init Error:', e); + showToast('OCR-Fehler: ' + e.message, 'error'); + throw e; + } + } + + // OCR durchführen mit Timeout + async function performOCR(imageBlob) { + // Loading-Overlay anzeigen + const overlay = document.createElement('div'); + overlay.className = 'ocr-loading-overlay'; + overlay.innerHTML = '