Read barcodes

Just starting out? Need help? Post your questions and find answers here.
pabloecipi
Posts: 1
Joined: Fri Aug 22, 2025 8:21 pm

Read barcodes

Post by pabloecipi »

Good day
There is a way to read barcodes with a camera on Android and obtain the encoded value

Thanks in advance
Pablo
User avatar
Caronte3D
Posts: 189
Joined: Sat Nov 23, 2019 5:21 pm
Location: Some Universe

Re: Read barcodes

Post by Caronte3D »

Search for a Cordova plug-in, for sure exists a way to use ir on SB.
Dirk Geppert
Posts: 332
Joined: Fri Sep 22, 2017 7:02 am

Re: Read barcodes

Post by Dirk Geppert »

This can also be done easily with pure JavaScript.

⚠ 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>
to the very top of the <head> section.
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>&nbsp;</p><p align='center'>JavaScript ist nicht aktiviert</p></noscript>\n</body>"
;!      }
;!    ]
;!  </HtmlPreprocessor>

;}

Have fun! :D

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
; ─────────────────────────────────────────────────────────────────────────────

Dirk Geppert
Posts: 332
Joined: Fri Sep 22, 2017 7:02 am

Re: Read barcodes

Post by Dirk Geppert »

Post Reply