ЧЗ scanner: switch decoder to zxing-wasm (ZXing-C++) for reliable DataMatrix
The pure-JS @zxing/library locator is weak on real ЧЗ codes (decoded 1/4 of the test photos with correct grayscale input; libdmtx and zxing-wasm read 3/4 — the 4th is physically undecodable). Symptom: scanner takes very long to even find a code, 2 of 4 never read. - Bundle zxing-wasm reader as an IIFE (frontend/lib/zxing-wasm.js, global ZXingWASM) + zxing_reader.wasm; drop frontend/lib/zxing.js. Init fetches the .wasm as an arrayBuffer and hands it over as wasmBinary (no streaming/MIME/CDN). - ChZ.zxReady()/ChZ.decodeImageData() (chzn.js): decode an ImageData -> raw GS1 bytes (result.bytes keeps the GS separators) with tryHarder/tryRotate/tryInvert. - Scanner (main.js) rewritten to a canvas loop: crop the central ROI, upscale to 540px, decode each frame — full-frame search was the slow/unreliable part. Photo path decodes the picked image the same way. Not yet verified on-device (WebView WASM instantiation); decoder logic validated in a Node harness against the 4 sample photos.
This commit is contained in:
parent
8ae2838320
commit
5f30af1cf7
6 changed files with 2190 additions and 37 deletions
|
|
@ -18,7 +18,7 @@
|
|||
<script src="lib/chart.min.js"></script>
|
||||
<script src="lib/chartjs-plugin-zoom.min.js"></script>
|
||||
<script src="lib/jszip.min.js"></script>
|
||||
<script src="lib/zxing.js"></script>
|
||||
<script src="lib/zxing-wasm.js"></script>
|
||||
<script src="js/db.js"></script>
|
||||
<script src="js/bmr.js"></script>
|
||||
<script src="js/tilecache.js"></script>
|
||||
|
|
|
|||
|
|
@ -270,9 +270,40 @@ let ChZ = (function () {
|
|||
return prefill;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// DataMatrix decoding via bundled zxing-wasm (ZXing-C++). Far better at
|
||||
// LOCATING codes than the pure-JS port. The .wasm is fetched once as an
|
||||
// arrayBuffer and handed to the module (wasmBinary) — no streaming/MIME issues.
|
||||
// ------------------------------------------------------------------
|
||||
let _zxReadyP = null;
|
||||
function zxReady() {
|
||||
if (_zxReadyP) return _zxReadyP;
|
||||
_zxReadyP = (async () => {
|
||||
if (typeof ZXingWASM === "undefined") throw new Error("ZXingWASM не загружен (пересобери приложение)");
|
||||
let buf = await (await fetch("lib/zxing_reader.wasm")).arrayBuffer();
|
||||
await ZXingWASM.prepareZXingModule({ overrides: { wasmBinary: new Uint8Array(buf) }, fireImmediately: true });
|
||||
return true;
|
||||
})();
|
||||
return _zxReadyP;
|
||||
}
|
||||
|
||||
// Decode one ImageData -> raw GS1 code string (with GS 0x1D), or null.
|
||||
async function decodeImageData(imageData, formats) {
|
||||
let res = await ZXingWASM.readBarcodes(imageData, {
|
||||
formats: formats || ["DataMatrix"],
|
||||
tryHarder: true, tryRotate: true, tryInvert: true, maxNumberOfSymbols: 1,
|
||||
});
|
||||
if (!res || !res.length || !res[0].isValid) return null;
|
||||
let r = res[0];
|
||||
// raw payload from bytes keeps the GS separators; fall back to text
|
||||
if (r.bytes && r.bytes.length) return String.fromCharCode.apply(null, r.bytes);
|
||||
return r.text || null;
|
||||
}
|
||||
|
||||
return {
|
||||
getSession, setSession, hasSession, importFromText, refresh, ensureSid,
|
||||
check, nutritionInfo, normalizeCode, extractGtin, parseCard, resolve,
|
||||
zxReady, decodeImageData,
|
||||
_raw: raw,
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -1844,16 +1844,16 @@ function showAddProductModal(onSaved, prefill) {
|
|||
});
|
||||
}
|
||||
|
||||
// Честный знак scanner: camera + photo/manual, decoded by bundled ZXing
|
||||
// (frontend/lib/zxing.js) — fully offline, no Google Play Services (the native
|
||||
// BarcodeDetector needs GMS and is absent on de-Googled ROMs). On a decoded
|
||||
// DataMatrix -> ChZ.resolve -> prefill the new-product modal.
|
||||
// Честный знак scanner: camera + photo/manual, decoded by bundled zxing-wasm
|
||||
// (ZXing-C++: frontend/lib/zxing-wasm.js + zxing_reader.wasm) — fully offline,
|
||||
// no Google Play Services, and far better at LOCATING a DataMatrix than the
|
||||
// pure-JS port. Live frames are cropped to the central ROI and upscaled for
|
||||
// speed/reliability. On decode -> ChZ.resolve -> prefill the new-product modal.
|
||||
function showChznScanModal(onSaved) {
|
||||
if (!ChZ.hasSession()) {
|
||||
appAlert("Сессия Честного знака не импортирована.\nНастройки → Честный знак → Импорт сессии (файл chzn_session.json из tools/chzn_export.sh).");
|
||||
return;
|
||||
}
|
||||
let hasZX = (typeof ZXing !== "undefined" && ZXing.BrowserDatamatrixCodeReader);
|
||||
|
||||
let ov = document.createElement("div");
|
||||
ov.className = "modal-overlay";
|
||||
|
|
@ -1864,7 +1864,7 @@ function showChznScanModal(onSaved) {
|
|||
<h3>Скан «Честный знак»</h3>
|
||||
<div style="position:relative;background:#000;border-radius:8px;overflow:hidden;aspect-ratio:1/1;max-height:60vh">
|
||||
<video id="cz-video" playsinline muted style="width:100%;height:100%;object-fit:cover"></video>
|
||||
<div style="position:absolute;inset:15%;border:2px solid rgba(255,255,255,.7);border-radius:8px;pointer-events:none"></div>
|
||||
<div style="position:absolute;inset:14%;border:2px solid rgba(255,255,255,.7);border-radius:8px;pointer-events:none"></div>
|
||||
</div>
|
||||
<div id="cz-status" style="margin:10px 0;font-size:13px;color:var(--muted);min-height:18px">Запуск камеры...</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
|
|
@ -1875,21 +1875,15 @@ function showChznScanModal(onSaved) {
|
|||
</div>`;
|
||||
document.body.appendChild(ov);
|
||||
|
||||
let controls = null, busy = false;
|
||||
let stream = null, scanning = false, busy = false;
|
||||
let base = winStack.length;
|
||||
let statusEl = ov.querySelector("#cz-status");
|
||||
let video = ov.querySelector("#cz-video");
|
||||
|
||||
let reader = null;
|
||||
if (hasZX) {
|
||||
let hints = new Map();
|
||||
hints.set(ZXing.DecodeHintType.TRY_HARDER, true); // better on curved/low-contrast codes
|
||||
reader = new ZXing.BrowserDatamatrixCodeReader(hints, { delayBetweenScanAttempts: 120 });
|
||||
}
|
||||
let canvas = document.createElement("canvas");
|
||||
let cctx = canvas.getContext("2d", { willReadFrequently: true });
|
||||
|
||||
function stopCamera() {
|
||||
try { if (controls) controls.stop(); } catch {}
|
||||
controls = null;
|
||||
scanning = false;
|
||||
try {
|
||||
let s = video && video.srcObject;
|
||||
if (s && s.getTracks) s.getTracks().forEach(t => t.stop());
|
||||
|
|
@ -1906,8 +1900,7 @@ function showChznScanModal(onSaved) {
|
|||
|
||||
async function onDecoded(rawVal) {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try { if (controls) controls.stop(); } catch {}
|
||||
busy = true; scanning = false;
|
||||
statusEl.textContent = "Код распознан, запрашиваю КБЖУ...";
|
||||
try {
|
||||
let prefill = await ChZ.resolve(rawVal);
|
||||
|
|
@ -1917,20 +1910,51 @@ function showChznScanModal(onSaved) {
|
|||
} catch (e) {
|
||||
busy = false;
|
||||
statusEl.textContent = (e.notFound ? "Код не найден в Честном знаке. " : "Ошибка: " + e.message + ". ") + "Наведите ещё раз.";
|
||||
if (reader) startCamera(); // resume continuous scan
|
||||
if (stream) { scanning = true; tick(); }
|
||||
}
|
||||
}
|
||||
|
||||
// Central ROI of the current video frame, upscaled to a square -> ImageData.
|
||||
// Cropping to the guide box removes clutter and makes the code larger, which
|
||||
// is exactly what the locator needs (full-frame search was slow/unreliable).
|
||||
function grabROI() {
|
||||
let vw = video.videoWidth, vh = video.videoHeight;
|
||||
if (!vw || !vh) return null;
|
||||
let side = Math.min(vw, vh) * 0.72;
|
||||
let sx = (vw - side) / 2, sy = (vh - side) / 2;
|
||||
let out = 540;
|
||||
canvas.width = out; canvas.height = out;
|
||||
cctx.drawImage(video, sx, sy, side, side, 0, 0, out, out);
|
||||
return cctx.getImageData(0, 0, out, out);
|
||||
}
|
||||
|
||||
async function tick() {
|
||||
if (busy || !scanning) return;
|
||||
let img = grabROI();
|
||||
if (img) {
|
||||
try {
|
||||
let raw = await ChZ.decodeImageData(img, ["DataMatrix"]);
|
||||
if (raw) { onDecoded(raw); return; }
|
||||
} catch (e) { /* keep scanning */ }
|
||||
}
|
||||
if (scanning) setTimeout(tick, 100);
|
||||
}
|
||||
|
||||
async function startCamera() {
|
||||
if (!reader) { statusEl.textContent = "Декодер ZXing не загрузился — пересобери приложение."; return; }
|
||||
try {
|
||||
statusEl.textContent = "Загрузка декодера...";
|
||||
await ChZ.zxReady();
|
||||
} catch (e) {
|
||||
statusEl.textContent = "Декодер не загрузился: " + (e.message || e);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: "environment" } } });
|
||||
video.srcObject = stream;
|
||||
await video.play();
|
||||
scanning = true;
|
||||
statusEl.textContent = "Наведите на код Честного знака...";
|
||||
controls = await reader.decodeFromConstraints(
|
||||
{ video: { facingMode: { ideal: "environment" } } },
|
||||
video,
|
||||
(result) => { if (result && !busy) onDecoded(result.getText()); }
|
||||
// per-frame NotFoundException is normal — ignored
|
||||
);
|
||||
tick();
|
||||
} catch (e) {
|
||||
statusEl.textContent = "Камера недоступна (" + (e.name || e.message) + "). Выберите фото или введите код.";
|
||||
}
|
||||
|
|
@ -1938,16 +1962,18 @@ function showChznScanModal(onSaved) {
|
|||
|
||||
ov.querySelector("#cz-photo").onclick = () => {
|
||||
pickFile("image/*", async (buf) => {
|
||||
if (!reader) { statusEl.textContent = "Декодер ZXing не загрузился."; return; }
|
||||
let url = URL.createObjectURL(new Blob([buf]));
|
||||
try {
|
||||
let result = await reader.decodeFromImageUrl(url);
|
||||
onDecoded(result.getText());
|
||||
} catch (e) {
|
||||
statusEl.textContent = (e && e.name === "NotFoundException")
|
||||
? "Код на фото не распознан. Попробуйте более резкое фото."
|
||||
: "Ошибка распознавания: " + (e.message || e);
|
||||
} finally { URL.revokeObjectURL(url); }
|
||||
await ChZ.zxReady();
|
||||
let bmp = await createImageBitmap(new Blob([buf]));
|
||||
let c = document.createElement("canvas");
|
||||
c.width = bmp.width; c.height = bmp.height;
|
||||
let x = c.getContext("2d", { willReadFrequently: true });
|
||||
x.drawImage(bmp, 0, 0);
|
||||
let img = x.getImageData(0, 0, bmp.width, bmp.height);
|
||||
let raw = await ChZ.decodeImageData(img, ["DataMatrix", "QRCode"]);
|
||||
if (raw) onDecoded(raw);
|
||||
else statusEl.textContent = "Код на фото не распознан. Попробуйте более резкое/крупное фото.";
|
||||
} catch (e) { statusEl.textContent = "Ошибка распознавания: " + (e.message || e); }
|
||||
}, true);
|
||||
};
|
||||
ov.querySelector("#cz-manual").onclick = async () => {
|
||||
|
|
|
|||
2097
frontend/lib/zxing-wasm.js
Normal file
2097
frontend/lib/zxing-wasm.js
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
BIN
frontend/lib/zxing_reader.wasm
Normal file
BIN
frontend/lib/zxing_reader.wasm
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue