Good day
There is a way to read barcodes with a camera on Android and obtain the encoded value
Thanks in advance
Pablo
Read barcodes
Re: Read barcodes
Search for a Cordova plug-in, for sure exists a way to use ir on SB.
-
- Posts: 332
- Joined: Fri Sep 22, 2017 7:02 am
Re: Read barcodes
This can also be done easily with pure JavaScript.
⚠ For this to work, you need to edit index.html and move the entry:
to the very top of the <head> section.
Or use Peters HtmlPreprocessor instead of HeaderSection
Have fun!
⚠ For this to work, you need to edit index.html and move the entry:
Code: Select all
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js"></script>
Or use Peters HtmlPreprocessor instead of HeaderSection
Code: Select all
; HeaderSection
; <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js"></script>
; EndHeaderSection
;{ HtmlPreProcessor ( https://github.com/spiderbytes/HtmlPreprocessor )
;! <HtmlPreprocessor>
;! [
;! {
;! "search": "</title>",
;! "replace": "</title>\n\n<script type=\"text/javascript\" src=\"https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js\"></script>\"/>"
;! },
;! {
;! "search": "</body>",
;! "replace": "<noscript><p> </p><p align='center'>JavaScript ist nicht aktiviert</p></noscript>\n</body>"
;! }
;! ]
;! </HtmlPreprocessor>
;}

Code: Select all
; ─────────────────────────────────────────────────────────────────────────────
; SpiderBasic Projekt: QR-Camera-Scanner (iPhone/Safari-kompatibel, Debug)
; Created with ChatGPT5
; ─────────────────────────────────────────────────────────────────────────────
; Features:
; - Läuft auf iPhone/iPad (Safari 15+), Chrome/Edge/Firefox (Desktop/Mobile)
; - Nutzt BarcodeDetector, Fallback: jsQR (CDN)
; - Sichtbares Overlay: ROI (gelb) + QR-Polygon/BoundingBox (grün)
; - Debug-Panel: FPS, Frames, Auflösung, ROI, Fehler, Log
; - iOS-Spezial: playsinline, user-gesture Start, facingMode, Touch-Fix
; - HTTPS/localhost erforderlich für Kamera-Zugriff
; ─────────────────────────────────────────────────────────────────────────────
EnableExplicit
HeaderSection
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js"></script>
EndHeaderSection
; ============= UI & Styles =============
!(() => {
! const css = `
! *{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
! html,body{height:100%}
! body{margin:0;font:16px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;background:#0b1020;color:#e9eef7}
! .wrap{max-width:1100px;margin:0 auto;padding:16px}
! h1{margin:8px 0 12px;font-size:22px}
! .bar{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:12px}
! select,button,input,textarea{font:inherit;padding:10px 12px;border-radius:12px;border:1px solid #2a3355;background:#111832;color:#e9eef7}
! button{cursor:pointer}
! button[disabled]{opacity:.5;cursor:not-allowed}
! .grid{display:grid;grid-template-columns:1.2fr .8fr;gap:16px}
! @media (max-width:980px){.grid{grid-template-columns:1fr}}
! .stage{position:relative;border-radius:16px;overflow:hidden;background:#000}
! video{width:100%;display:block;background:#000;object-fit:cover;border-radius:16px}
! canvas.overlay{position:absolute;inset:0;pointer-events:none;z-index:2;display:block}
! .card{background:#0f1733;border:1px solid #202a55;border-radius:16px;padding:14px}
! .kv{display:grid;grid-template-columns:auto 1fr;gap:6px 10px;font-size:14px}
! .muted{opacity:.85}
! .ok{color:#7fffb5}
! .err{color:#ff8f8f}
! .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:pre-wrap;word-break:break-word}
! textarea#log{width:100%;min-height:140px;resize:vertical}
! .pill{padding:4px 8px;border-radius:999px;border:1px solid #2a3355;background:#0b122e}
! .pill.ok{border-color:#1a6f52}
! .pill.err{border-color:#6f1a1a}
! .note{font-size:13px}
! `;
! const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style);
!
! document.body.innerHTML = `
! <div class="wrap">
! <h1>📷 QR-Scanner – iOS/Safari Debug</h1>
!
! <div class="bar">
! <select id="camera"></select>
! <button id="start">Start</button>
! <button id="stop" disabled>Stop</button>
! <label class="muted"><input id="torch" type="checkbox" /> Taschenlampe</label>
! <label class="muted"><input id="debugToggle" type="checkbox" checked /> Debug sichtbar</label>
! <button id="copy" disabled>Kopieren</button>
! <span class="pill" id="pillNative">Native: ?</span>
! <span class="pill" id="pillFallback">Fallback: ?</span>
! <span class="pill" id="pillTorch">Torch: ?</span>
! </div>
!
! <div class="grid">
! <div class="stage">
! <video id="video" playsinline autoplay muted webkit-playsinline></video>
! <canvas id="overlay" class="overlay"></canvas>
! </div>
! <div class="card">
! <div class="kv">
! <div>State</div><div id="status" class="muted">Bereit. Tippe auf Start (iOS braucht Touch).</div>
! <div>Treffer</div><div id="result" class="mono"></div>
! <div>Frames gescannt</div><div id="statFrames">0</div>
! <div>FPS (Scan)</div><div id="statFps">0</div>
! <div>Auflösung</div><div id="statRes">0×0</div>
! <div>ROI</div><div id="statRoi">–</div>
! <div>Letzter Fehler</div><div id="statErr" class="err">–</div>
! </div>
! <hr style="opacity:.1;border:none;border-top:1px solid #202a55;margin:10px 0">
! <div class="muted" style="margin-bottom:6px">Debug-Log</div>
! <textarea id="log" class="mono" spellcheck="false"></textarea>
! <div class="note muted" style="margin-top:8px">
! Hinweis: iOS Safari zeigt teilweise keine Kameranamen vor Zustimmung. HTTPS/localhost zwingend.
! </div>
! </div>
! </div>
! </div>
! `;
!})();
; ============= Scanner-Logik & iOS-Fixes =============
!(() => {
! const $ = (id) => document.getElementById(id);
! const video = $('video'), overlay = $('overlay'), octx = overlay.getContext('2d');
! const cameraSel=$('camera'), btnStart=$('start'), btnStop=$('stop'), chkTorch=$('torch'), dbgToggle=$('debugToggle'), btnCopy=$('copy');
! const status=$('status'), result=$('result'), statFrames=$('statFrames'), statFps=$('statFps'), statRes=$('statRes'), statRoi=$('statRoi'), statErr=$('statErr');
! const pillNative=$('pillNative'), pillFallback=$('pillFallback'), pillTorch=$('pillTorch');
! const logEl=$('log');
!
! // iOS-Detection
! const ua = navigator.userAgent || '';
! const isiOS = /iPad|iPhone|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); // iPadOS
!
! // Prevent iOS from pausing video / wake tweaks
! document.addEventListener('visibilitychange', () => { if (document.hidden) stop(); }, {passive:true});
! document.body.addEventListener('touchstart', ()=>{}, {passive:true});
!
! // Internal state
! let stream=null, track=null, scanning=false, raf=0;
! let barcodeDetector = ('BarcodeDetector' in window) ? new BarcodeDetector({formats:['qr_code']}) : null;
! let jsqr=null; // fallback
! let workCanvas = document.createElement('canvas'); const wctx = workCanvas.getContext('2d', {willReadFrequently:true});
! let lastScanTs=0, frames=0, fps=0, lastFpsTs=performance.now();
! const SCAN_INTERVAL_MS= isiOS ? 120 : 80; // iOS etwas ruhiger
! const roiScale = 0.8;
!
! // Debug/log helpers
! const log = (...a) => {
! const line = a.map(x => (typeof x==='object'? JSON.stringify(x): String(x))).join(' ');
! const ts = new Date().toLocaleTimeString();
! logEl.value += `[${ts}] ${line}\n`;
! logEl.scrollTop = logEl.scrollHeight;
! };
! const setStatus = (msg, cls='muted') => { status.className=cls; status.textContent=msg; log(msg); };
! const setResult = (text) => {
! result.textContent = text||''; btnCopy.disabled=!text;
! if (text && window._PB_QR_Callback) { try{ window._PB_QR_Callback(text); }catch(e){ log('CB error', e); } }
! };
! const setPill = (el, ok, label) => {
! el.textContent = `${label}: ${ok ? 'ja' : 'nein'}`;
! el.className = `pill ${ok ? 'ok':'err'}`;
! };
! setPill(pillNative, !!barcodeDetector, 'Native');
! setPill(pillFallback, false, 'Fallback');
! setPill(pillTorch, false, 'Torch');
!
! // Overlay (CSS-Pixel korrekt, HiDPI)
! function drawOverlay({roi, polyPoints, bbox}) {
! const rect = video.getBoundingClientRect();
! const vwCSS = Math.max(1, Math.round(rect.width));
! const vhCSS = Math.max(1, Math.round(rect.height));
! const dpr = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
! overlay.width = Math.floor(vwCSS * dpr);
! overlay.height = Math.floor(vhCSS * dpr);
! overlay.style.width = vwCSS + 'px';
! overlay.style.height = vhCSS + 'px';
! octx.setTransform(dpr,0,0,dpr,0,0);
! octx.clearRect(0,0,vwCSS,vhCSS);
! if (!dbgToggle.checked) return;
!
! if (roi) {
! octx.beginPath(); octx.rect(roi.x, roi.y, roi.w, roi.h);
! octx.lineWidth=3; octx.strokeStyle='yellow'; octx.stroke();
! }
! if (polyPoints && polyPoints.length>=3) {
! octx.beginPath(); octx.moveTo(polyPoints[0].x, polyPoints[0].y);
! for (let i=1;i<polyPoints.length;i++) octx.lineTo(polyPoints[i].x, polyPoints[i].y);
! octx.closePath(); octx.lineWidth=4; octx.strokeStyle='lime'; octx.stroke();
! } else if (bbox) {
! octx.beginPath(); octx.rect(bbox.x, bbox.y, bbox.w, bbox.h);
! octx.lineWidth=4; octx.strokeStyle='lime'; octx.stroke();
! }
! }
!
! async function listCameras() {
! cameraSel.innerHTML='';
! let cams=[];
! try{
! const devices = await navigator.mediaDevices.enumerateDevices();
! cams = devices.filter(d=>d.kind==='videoinput');
! }catch(e){ log('enumerateDevices fail', e); }
! if (!cams.length) {
! const opt=document.createElement('option');
! opt.value=''; opt.textContent='(Kamera wird automatisch gewählt)';
! cameraSel.appendChild(opt);
! return;
! }
! for (const d of cams) {
! const opt=document.createElement('option');
! opt.value=d.deviceId||''; opt.textContent=d.label||'Kamera';
! cameraSel.appendChild(opt);
! }
! const back=cams.find(c=>/back|rear|umlad|rück/i.test(c.label||'')); if (back) cameraSel.value=back.deviceId||'';
! }
!
! async function ensurePermission() {
! try {
! // iOS: erst Permission holen, damit Labels sichtbar/Streams stabil
! await navigator.mediaDevices.getUserMedia({video:{facingMode:{ideal:'environment'}}, audio:false});
! } catch(e) { log('perm fail', e); }
! }
!
! async function loadFallbackIfNeeded(){
! if (!barcodeDetector && !jsqr){
! setStatus('Lade Fallback…','muted');
! await new Promise((res,rej)=>{
! const s=document.createElement('script');
! s.src='https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js';
! s.onload=()=>{ jsqr=window.jsQR; res(); };
! s.onerror=rej; document.head.appendChild(s);
! });
! setStatus('Fallback geladen.','ok'); setPill(pillFallback, true, 'Fallback');
! }
! }
!
! function buildConstraints(){
! // iOS mag deviceId weniger; facingMode bevorzugen
! const useDeviceId = !isiOS && cameraSel.value;
! const v = {
! facingMode: useDeviceId ? undefined : {ideal:'environment'},
! width: {ideal: 1280}, // konservativer für iOS Stabilität
! height: {ideal: 720},
! frameRate: {ideal: 30, max: 60}
! };
! if (useDeviceId) v.deviceId = {exact: cameraSel.value};
! return {video: v, audio:false};
! }
!
! async function start(){
! if (scanning) return;
! setStatus('Initialisiere…','muted');
! await ensurePermission();
! await listCameras();
! await loadFallbackIfNeeded();
!
! const constraints = buildConstraints();
! log('constraints', constraints);
! stream = await navigator.mediaDevices.getUserMedia(constraints);
! video.srcObject = stream;
! track = stream.getVideoTracks()[0];
!
! // iOS braucht oft echten "play()" nach User-Geste
! try { await video.play(); } catch(e) { log('video.play failed', e); }
!
! await new Promise(r=>{
! if (video.readyState>=2 && video.videoWidth) return r();
! const onLD=()=>{ video.removeEventListener('loadeddata', onLD); r(); };
! video.addEventListener('loadeddata', onLD, {once:true});
! });
!
! // Torch-Fähigkeit ermitteln (iOS i.d.R. false)
! try {
! const caps = track.getCapabilities?.()||{};
! setPill(pillTorch, !!caps.torch, 'Torch');
! if (caps.focusMode?.includes('continuous')) {
! await track.applyConstraints({advanced:[{focusMode:'continuous'}]});
! }
! } catch(e){ log('applyConstraints fail', e); }
!
! const vw=video.videoWidth, vh=video.videoHeight;
! statRes.textContent = `${vw}×${vh}`;
! btnStart.disabled=true; btnStop.disabled=false;
! scanning=true; frames=0; fps=0; lastFpsTs=performance.now(); lastScanTs=0;
! setStatus(barcodeDetector ? 'Scanner aktiv (native)…' : 'Scanner aktiv (Fallback)…','ok');
! tick();
! }
!
! function stop(){
! scanning=false; cancelAnimationFrame(raf);
! try{ track?.stop(); }catch{}; track=null;
! try{ stream?.getTracks().forEach(t=>t.stop()); }catch{}; stream=null;
! btnStart.disabled=false; btnStop.disabled=true;
! setStatus('Gestoppt.','muted');
! drawOverlay({roi:null, polyPoints:null, bbox:null});
! }
!
! async function setTorch(on){
! if (!track) return;
! try{
! const caps=track.getCapabilities?.()||{};
! if (caps.torch) {
! await track.applyConstraints({advanced:[{torch:!!on}]});
! } else if (caps.focusMode?.includes('continuous')) {
! await track.applyConstraints({advanced:[{focusMode:'continuous'}]});
! }
! }catch(e){ statErr.textContent=String(e); log('torch/focus fail', e); }
! }
!
! // Fallback-Scan (jsQR) mit korrekter Koordinaten-Mapping
! function scanFallback() {
! if (!jsqr) return { data: null, polyCSS: null, roiCSS: null };
! const vw = video.videoWidth || 1280;
! const vh = video.videoHeight || 720;
! const rect = video.getBoundingClientRect();
! const vwCSS = rect.width || vw;
! const vhCSS = rect.height || vh;
!
! const rw = Math.floor(vw * roiScale), rh = Math.floor(vh * roiScale);
! const rx = Math.floor((vw - rw) / 2), ry = Math.floor((vh - rh) / 2);
! const samples = [
! { sx: rx, sy: ry, sw: rw, sh: rh, dw: rw, dh: rh },
! { sx: rx, sy: ry, sw: rw, sh: rh, dw: Math.floor(rw*0.75), dh: Math.floor(rh*0.75) }
! ];
! const sxQD = vwCSS / vw, syQD = vhCSS / vh;
!
! let found=null, polyCSS=null;
! for (const s of samples) {
! workCanvas.width=s.dw; workCanvas.height=s.dh;
! wctx.drawImage(video, s.sx,s.sy,s.sw,s.sh, 0,0,s.dw,s.dh);
! const img=wctx.getImageData(0,0,s.dw,s.dh);
! const code=jsqr(img.data,img.width,img.height,{inversionAttempts:'attemptBoth'});
! if (code?.data){
! found=code.data;
! const scaleX=s.sw/s.dw, scaleY=s.sh/s.dh, off={x:s.sx,y:s.sy};
! const polyQ=[
! {x: off.x + code.location.topLeftCorner.x*scaleX, y: off.y + code.location.topLeftCorner.y*scaleY},
! {x: off.x + code.location.topRightCorner.x*scaleX, y: off.y + code.location.topRightCorner.y*scaleY},
! {x: off.x + code.location.bottomRightCorner.x*scaleX, y: off.y + code.location.bottomRightCorner.y*scaleY},
! {x: off.x + code.location.bottomLeftCorner.x*scaleX, y: off.y + code.location.bottomLeftCorner.y*scaleY}
! ];
! polyCSS = polyQ.map(p=>({x:p.x*sxQD, y:p.y*syQD}));
! break;
! }
! }
! const roiCSS = { x: rx*sxQD, y: ry*syQD, w: rw*sxQD, h: rh*syQD };
! return { data: found, polyCSS, roiCSS };
! }
!
! async function tick(now=performance.now()){
! if (!scanning) return;
! if (now - lastScanTs < SCAN_INTERVAL_MS) { raf=requestAnimationFrame(tick); return; }
! lastScanTs = now;
!
! const rect = video.getBoundingClientRect();
! const vw=video.videoWidth||0, vh=video.videoHeight||0;
! const vwCSS=Math.round(rect.width)||0, vhCSS=Math.round(rect.height)||0;
! if (!vw || !vh || !vwCSS || !vhCSS) { setStatus('Warte auf Videodaten…','muted'); raf=requestAnimationFrame(tick); return; }
!
! frames++; const dt=now-lastFpsTs;
! if (dt>=1000){ fps=Math.round((frames*1000)/dt); statFps.textContent=String(fps); frames=0; lastFpsTs=now; }
! statFrames.textContent = String(Number(statFrames.textContent||'0') + 1);
!
! let value=null, polyCSS=null, bboxCSS=null;
!
! // 1) Native (Safari 17+/Chrome)
! if (barcodeDetector) {
! try{
! const barcodes = await barcodeDetector.detect(video);
! if (barcodes?.length){
! const qr = barcodes.find(b=> (b.format||'').toLowerCase().includes('qr'));
! if (qr?.rawValue){
! value=qr.rawValue;
! if (qr.cornerPoints?.length){ polyCSS = qr.cornerPoints.map(p=>({x:p.x, y:p.y})); }
! else if (qr.boundingBox){ const bb=qr.boundingBox; bboxCSS={x:bb.x, y:bb.y, w:bb.width, h:bb.height}; }
! }
! }
! }catch(e){ statErr.textContent=String(e); log('native detect fail', e); }
! }
!
! // 2) Fallback
! let roiCSS=null;
! if (!value) {
! const fb=scanFallback();
! value=fb.data; polyCSS=fb.polyCSS; roiCSS=fb.roiCSS;
! }
!
! // 3) ROI immer zeigen (Display-Koordinaten)
! if (!roiCSS) {
! const rw=Math.floor(vwCSS*roiScale), rh=Math.floor(vhCSS*roiScale);
! const rx=Math.floor((vwCSS-rw)/2), ry=Math.floor((vhCSS-rh)/2);
! roiCSS={x:rx,y:ry,w:rw,h:rh};
! }
! statRoi.textContent = `${Math.round(roiCSS.w)}×${Math.round(roiCSS.h)} @ ${Math.round(roiCSS.x)},${Math.round(roiCSS.y)}`;
!
! // 4) Overlay zeichnen
! drawOverlay({roi:roiCSS, polyPoints:polyCSS, bbox:bboxCSS});
!
! if (value){ setStatus('QR erkannt ✅','ok'); setResult(value); }
! else { setStatus('Suche QR-Code…','muted'); }
!
! raf = requestAnimationFrame(tick);
! }
!
! // UI Events
! btnStart.addEventListener('click', start);
! btnStop .addEventListener('click', stop);
! chkTorch.addEventListener('change', e=> setTorch(e.target.checked));
! btnCopy.addEventListener('click', async ()=>{
! try{ await navigator.clipboard.writeText(result.textContent); setStatus('In Zwischenablage kopiert.','ok'); }
! catch(e){ setStatus('Kopieren blockiert – manuell markieren.','err'); }
! });
! cameraSel.addEventListener('change', ()=>{ if (scanning){ stop(); start(); } });
!
! // Initiale Kameraliste (nach Permission sichtbarer)
! (async () => {
! try { await listCameras(); } catch(e){ log('init list fail', e); }
! })();
!})();
; ============= Optionale PB-Bridge (Treffer zurück) =============
Procedure SetupQrCallback()
!window._PB_QR_Callback = function (s) {
! console.log('[PB] QR:', s);
!};
EndProcedure
SetupQrCallback()
; ─────────────────────────────────────────────────────────────────────────────
; ENDE
; ─────────────────────────────────────────────────────────────────────────────
-
- Posts: 332
- Joined: Fri Sep 22, 2017 7:02 am