Skip to content
Jaime Poch edited this page Sep 22, 2022 · 1 revision

Welcome to the html5-qrcode wiki!

<!doctype html>

<title>Simple Document Scanner</title> <style> :root{ --accent:#ff6ea1; --bg1:linear-gradient(135deg,#fffaf0,#ffeef8); --card-bg:rgba(255,255,255,0.95); } *{box-sizing:border-box;margin:0;padding:0} html,body{height:100%;font-family:system-ui,-apple-system,Segoe UI,Roboto,'Helvetica Neue',Arial;background:var(--bg1);display:flex;align-items:center;justify-content:center;padding:18px} .card{width:100%;max-width:980px;background:var(--card-bg);border-radius:14px;padding:18px;box-shadow:0 12px 30px rgba(0,0,0,0.12);overflow:hidden} .top{display:flex;gap:12px;align-items:center;justify-content:space-between;margin-bottom:12px} h1{font-size:20px;color:var(--accent);letter-spacing:0.6px} .controls{display:flex;gap:8px;flex-wrap:wrap;align-items:center} button,input[type=file]{padding:8px 12px;border-radius:10px;border:1px solid rgba(0,0,0,0.06);background:white;cursor:pointer} .btn-primary{background:var(--accent);color:white;border:none} .stage{display:flex;gap:12px} .viewer{flex:1;min-height:420px;background:linear-gradient(180deg,#fff,#fff7ff);border-radius:10px;padding:10px;position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden} video,canvas,img{max-width:100%;max-height:100%;display:block} .overlay{ position:absolute;left:0;top:0;right:0;bottom:0; pointer-events:auto; } .crop-box{ position:absolute;border:2px dashed rgba(255,110,150,0.85);border-radius:8px; touch-action:none; background:linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02)); } .handle{ width:18px;height:18px;border-radius:4px;background:var(--accent);position:absolute;box-shadow:0 3px 8px rgba(0,0,0,0.18) } .handle.tl{left:-9px;top:-9px;cursor:nwse-resize} .handle.tr{right:-9px;top:-9px;cursor:nesw-resize} .handle.bl{left:-9px;bottom:-9px;cursor:nesw-resize} .handle.br{right:-9px;bottom:-9px;cursor:nwse-resize} .side-controls{width:360px;max-width:40%;padding:12px;display:flex;flex-direction:column;gap:10px} .slider-row{display:flex;gap:8px;align-items:center} label{font-size:13px;color:#444} input[type=range]{flex:1} .small{font-size:12px;color:#666} .actions{display:flex;gap:8px;flex-wrap:wrap} .notice{font-size:13px;color:#666;margin-top:6px} @media (max-width:880px){.stage{flex-direction:column}.side-controls{width:100%;max-width:none}} </style>

Document Scanner — Quick Scan

Camera Capture Download Scan
<div class="stage">
  <div class="viewer" id="viewer">
    <video id="video" autoplay playsinline style="display:none"></video>
    <img id="sourceImg" alt="source" style="display:none;max-width:100%;height:auto">
    <canvas id="preview" style="display:none"></canvas>

    <!-- interactive overlay for cropping -->
    <div class="overlay" id="overlay" style="display:none">
      <div class="crop-box" id="cropBox" style="left:8%;top:10%;width:84%;height:80%">
        <div class="handle tl"></div>
        <div class="handle tr"></div>
        <div class="handle bl"></div>
        <div class="handle br"></div>
      </div>
    </div>
  </div>

  <div class="side-controls">
    <div>
      <label class="small">Preview / Controls</label>
      <div class="notice">Use camera or upload image. Drag crop corners to frame the document. Click Capture to make a scan; use sliders to tidy it up.</div>
    </div>

    <div class="slider-row">
      <label>Rotate</label>
      <input id="rotate" type="range" min="-180" max="180" value="0">
      <span id="rotVal" class="small">0°</span>
    </div>

    <div class="slider-row">
      <label>Brightness</label>
      <input id="brightness" type="range" min="-100" max="100" value="0">
      <span id="bVal" class="small">0</span>
    </div>

    <div class="slider-row">
      <label>Contrast</label>
      <input id="contrast" type="range" min="-100" max="100" value="0">
      <span id="cVal" class="small">0</span>
    </div>

    <div class="slider-row">
      <label>Sharpen</label>
      <input id="sharpen" type="range" min="0" max="100" value="0">
      <span id="sVal" class="small">0</span>
    </div>

    <div>
      <label class="small">Actions</label>
      <div class="actions">
        <button id="applyFilters" class="btn-primary">Apply & Preview</button>
        <button id="autoCenter">Auto-center crop</button>
        <button id="reset">Reset</button>
      </div>
    </div>

    <div style="margin-top:8px">
      <label class="small">Output</label>
      <div class="notice">After preview, click <strong>Download Scan</strong> to save PNG (high-res export).</div>
    </div>
  </div>
</div>
<script> /* Simple Document Scanner JS - Supports camera or file upload - Axis-aligned crop box (draggable corners / move) - Rotate, brightness, contrast, simple sharpen - Export PNG */ // elements const video = document.getElementById('video'); const sourceImg = document.getElementById('sourceImg'); const preview = document.getElementById('preview'); const overlay = document.getElementById('overlay'); const cropBox = document.getElementById('cropBox'); const fileInput = document.getElementById('fileInput'); const startCam = document.getElementById('startCam'); const captureBtn = document.getElementById('capture'); const downloadBtn = document.getElementById('download'); const applyFiltersBtn = document.getElementById('applyFilters'); const autoCenterBtn = document.getElementById('autoCenter'); const resetBtn = document.getElementById('reset'); const rotateRange = document.getElementById('rotate'); const brightnessRange = document.getElementById('brightness'); const contrastRange = document.getElementById('contrast'); const sharpenRange = document.getElementById('sharpen'); const rotVal = document.getElementById('rotVal'); const bVal = document.getElementById('bVal'); const cVal = document.getElementById('cVal'); const sVal = document.getElementById('sVal'); let stream = null; let currentImage = null; // HTMLImageElement or Video frame let lastPreviewDataURL = null; // show/hide helpers function showViewerElement(el){ video.style.display='none'; sourceImg.style.display='none'; preview.style.display='none'; overlay.style.display='none'; el.style.display='block'; } // start camera startCam.addEventListener('click', async ()=>{ if(stream){ stopCamera(); showViewerElement(video); return; } try{ stream = await navigator.mediaDevices.getUserMedia({video:{facingMode:'environment'},audio:false}); video.srcObject = stream; showViewerElement(video); overlay.style.display='block'; }catch(e){ alert('Camera access nahi mila: ' + e.message); } }); function stopCamera(){ if(stream){ stream.getTracks().forEach(t=>t.stop()); stream=null; } video.srcObject = null; } // file upload fileInput.addEventListener('change', (e)=>{ const f = e.target.files[0]; if(!f) return; const url = URL.createObjectURL(f); sourceImg.onload = ()=>{ URL.revokeObjectURL(url); fitInitialCrop(); showViewerElement(sourceImg); overlay.style.display='block'; currentImage = sourceImg; }; sourceImg.src = url; }); // capture from camera or from image -> draw on canvas for editing captureBtn.addEventListener('click', ()=>{ const canvas = preview; const ctx = canvas.getContext('2d'); // pick source: video if active, else sourceImg if(stream && video.style.display!=='none'){ canvas.width = video.videoWidth; canvas.height = video.videoHeight; ctx.drawImage(video,0,0,canvas.width,canvas.height); } else if(sourceImg && sourceImg.style.display!=='none'){ canvas.width = sourceImg.naturalWidth; canvas.height = sourceImg.naturalHeight; ctx.drawImage(sourceImg,0,0,canvas.width,canvas.height); } else { alert('Pehle camera chalao ya image upload karo.'); return; } currentImage = canvas; // use the canvas as source for further ops fitInitialCrop(); showViewerElement(preview); overlay.style.display='block'; }); // crop box interactions (dragging and resizing) let dragging = false, dragType = null, start = {}, startBox = {}; const handles = { tl: cropBox.querySelector('.handle.tl'), tr: cropBox.querySelector('.handle.tr'), bl: cropBox.querySelector('.handle.bl'), br: cropBox.querySelector('.handle.br'), }; function getRect(el){ return el.getBoundingClientRect(); } function clientToOverlay(x,y){ const o = overlay.getBoundingClientRect(); return {x: x - o.left, y: y - o.top}; } function startDrag(evt, type){ evt.preventDefault(); dragging = true; dragType = type; const p = (evt.touches ? evt.touches[0] : evt); start = clientToOverlay(p.clientX, p.clientY); startBox = {left: cropBox.offsetLeft, top: cropBox.offsetTop, width: cropBox.offsetWidth, height: cropBox.offsetHeight}; window.addEventListener('mousemove', onDrag); window.addEventListener('touchmove', onDrag, {passive:false}); window.addEventListener('mouseup', endDrag); window.addEventListener('touchend', endDrag); } function onDrag(evt){ if(!dragging) return; evt.preventDefault(); const p = (evt.touches ? evt.touches[0] : evt); const cur = clientToOverlay(p.clientX, p.clientY); const dx = cur.x - start.x; const dy = cur.y - start.y; // move or resize if(dragType === 'move'){ let nl = startBox.left + dx, nt = startBox.top + dy; // clamp nl = Math.max(0, Math.min(nl, overlay.clientWidth - startBox.width)); nt = Math.max(0, Math.min(nt, overlay.clientHeight - startBox.height)); cropBox.style.left = nl + 'px'; cropBox.style.top = nt + 'px'; } else { // resize via corners let l = startBox.left, t = startBox.top, w = startBox.width, h = startBox.height; if(dragType === 'tl'){ l = startBox.left + dx; t = startBox.top + dy; w = startBox.width - dx; h = startBox.height - dy; } if(dragType === 'tr'){ t = startBox.top + dy; w = startBox.width + dx; h = startBox.height - dy; } if(dragType === 'bl'){ l = startBox.left + dx; w = startBox.width - dx; h = startBox.height + dy; } if(dragType === 'br'){ w = startBox.width + dx; h = startBox.height + dy; } // enforce minimum const minSize = 40; if(w < minSize) w = minSize; if(h < minSize) h = minSize; // clamp within overlay if(l < 0){ w += l; l = 0; } if(t < 0){ h += t; t = 0; } if(l + w > overlay.clientWidth) w = overlay.clientWidth - l; if(t + h > overlay.clientHeight) h = overlay.clientHeight - t; cropBox.style.left = l + 'px'; cropBox.style.top = t + 'px'; cropBox.style.width = w + 'px'; cropBox.style.height = h + 'px'; } } function endDrag(){ dragging = false; dragType = null; window.removeEventListener('mousemove', onDrag); window.removeEventListener('touchmove', onDrag); window.removeEventListener('mouseup', endDrag); window.removeEventListener('touchend', endDrag); } // attach handlers cropBox.addEventListener('mousedown', (e)=>{ if(e.target.classList.contains('handle')) return; startDrag(e,'move') }); cropBox.addEventListener('touchstart', (e)=>{ if(e.target.classList.contains('handle')) return; startDrag(e,'move') }, {passive:false}); handles.tl.addEventListener('mousedown',(e)=>startDrag(e,'tl')); handles.tr.addEventListener('mousedown',(e)=>startDrag(e,'tr')); handles.bl.addEventListener('mousedown',(e)=>startDrag(e,'bl')); handles.br.addEventListener('mousedown',(e)=>startDrag(e,'br')); handles.tl.addEventListener('touchstart',(e)=>startDrag(e,'tl'),{passive:false}); handles.tr.addEventListener('touchstart',(e)=>startDrag(e,'tr'),{passive:false}); handles.bl.addEventListener('touchstart',(e)=>startDrag(e,'bl'),{passive:false}); handles.br.addEventListener('touchstart',(e)=>startDrag(e,'br'),{passive:false}); // fit crop initial function fitInitialCrop(){ // show overlay and set crop box to a good default overlay.style.display='block'; const view = (currentImage && currentImage.tagName === 'CANVAS') ? preview : (currentImage || sourceImg); // wait for layout requestAnimationFrame(()=>{ const vw = overlay.clientWidth, vh = overlay.clientHeight; cropBox.style.left = (vw*0.06)+'px'; cropBox.style.top = (vh*0.06)+'px'; cropBox.style.width = (vw*0.88)+'px'; cropBox.style.height = (vh*0.88)+'px'; }); } // apply filters and show preview in preview canvas function applyFiltersToPreview(){ // source: preview canvas if capturing used, else image element let srcCanvas, srcW, srcH; if(currentImage && currentImage.tagName === 'CANVAS'){ srcCanvas = currentImage; srcW = srcCanvas.width; srcH = srcCanvas.height; } else if(sourceImg && sourceImg.src){ // draw image into an offscreen canvas srcW = sourceImg.naturalWidth; srcH = sourceImg.naturalHeight; const off = document.createElement('canvas'); off.width = srcW; off.height = srcH; const octx = off.getContext('2d'); octx.drawImage(sourceImg,0,0); srcCanvas = off; } else if(video && video.style.display!=='none' && stream){ srcW = video.videoWidth; srcH = video.videoHeight; const off = document.createElement('canvas'); off.width = srcW; off.height = srcH; off.getContext('2d').drawImage(video,0,0); srcCanvas = off; } else { alert('Koi source nahi hai.'); return; } // compute crop area relative to overlay and source image natural size const ov = overlay.getBoundingClientRect(); const imgRect = getContentRectForSource(srcCanvas); // overlay->image scaling factors const sx = srcCanvas.width / imgRect.width; const sy = srcCanvas.height / imgRect.height; const cbLeft = cropBox.offsetLeft; const cbTop = cropBox.offsetTop; const cbW = cropBox.offsetWidth; const cbH = cropBox.offsetHeight; // map overlay coordinates to source canvas coordinates const sx0 = (cbLeft - imgRect.left) * sx; const sy0 = (cbTop - imgRect.top) * sy; const sW = cbW * sx; const sH = cbH * sy; // prepare output canvas const out = document.createElement('canvas'); out.width = Math.round(sW); out.height = Math.round(sH); const outCtx = out.getContext('2d'); // apply rotation, brightness, contrast, sharpen // We'll draw source into a temporary canvas then read pixels and apply simple filters const temp = document.createElement('canvas'); temp.width = srcCanvas.width; temp.height = srcCanvas.height; const tctx = temp.getContext('2d'); tctx.drawImage(srcCanvas,0,0); // extract the cropped image data const imgData = tctx.getImageData(Math.round(sx0), Math.round(sy0), Math.max(1,Math.round(sW)), Math.max(1,Math.round(sH))); // apply brightness/contrast and simple sharpen const brightness = parseInt(brightnessRange.value,10) || 0; const contrast = parseInt(contrastRange.value,10) || 0; const sharpen = parseInt(sharpenRange.value,10) || 0; const rot = parseFloat(rotateRange.value) || 0; // brightness & contrast adjustment // formula: new = ((old-128)*contrastFactor)+128 + brightness const cFactor = (259 * (contrast + 255)) / (255 * (259 - contrast)); const data = imgData.data; for(let i=0;i 0){ // simple unsharp-like effect: mix original with Laplacian-like kernel const tmpData = new Uint8ClampedArray(data); // copy const w = imgData.width, h = imgData.height; // kernel for subtle sharpen const k = [ 0, -1, 0, -1, 5, -1, 0, -1, 0 ]; const kSum = 1; for(let y=1;y 0.001){ // convert degrees to radians const r = rot * Math.PI / 180; // create temp final to rotate around center const temp2 = document.createElement('canvas'); temp2.width = final.width; temp2.height = final.height; const t2 = temp2.getContext('2d'); t2.translate(final.width/2, final.height/2); t2.rotate(r); t2.drawImage(out, -out.width/2, -out.height/2); // copy back fctx.drawImage(temp2,0,0); } else { fctx.drawImage(out,0,0); } showViewerElement(preview); overlay.style.display='none'; lastPreviewDataURL = preview.toDataURL('image/png'); } // helper: get displayed content rect (centered with aspect fit) inside viewer overlay function getContentRectForSource(srcCanvas){ // overlay size const ov = overlay.getBoundingClientRect(); const vw = ov.width, vh = ov.height; // source aspect const sw = srcCanvas.width, sh = srcCanvas.height; const sar = sw / sh; const varr = vw / vh; let drawW, drawH, left, top; if(sar > varr){ drawW = vw; drawH = vw / sar; left = 0; top = (vh - drawH)/2; } else { drawH = vh; drawW = vh * sar; top = 0; left = (vw - drawW)/2; } // returning coordinates relative to overlay container return {left, top, width: drawW, height: drawH}; } // auto-center crop (fit to center area) autoCenterBtn.addEventListener('click', ()=>{ fitInitialCrop(); }); // apply & preview applyFiltersBtn.addEventListener('click', applyFiltersToPreview); // download downloadBtn.addEventListener('click', ()=>{ if(!lastPreviewDataURL){ // try to generate one from current crop applyFiltersToPreview(); if(!lastPreviewDataURL) { alert('Preview generate nahi hua.'); return; } } const a = document.createElement('a'); a.href = lastPreviewDataURL; a.download = 'scan-'+Date.now()+'.png'; document.body.appendChild(a); a.click(); a.remove(); }); // reset resetBtn.addEventListener('click', ()=>{ brightnessRange.value = 0; contrastRange.value=0; sharpenRange.value=0; rotateRange.value=0; rotVal.textContent = '0°'; bVal.textContent='0'; cVal.textContent='0'; sVal.textContent='0'; lastPreviewDataURL = null; overlay.style.display = (sourceImg.src || stream) ? 'block' : 'none'; // show source if available if(sourceImg.src) showViewerElement(sourceImg); else if(stream) showViewerElement(video); }); // live value displays rotateRange.addEventListener('input', ()=>rotVal.textContent = rotateRange.value + '°'); brightnessRange.addEventListener('input', ()=>bVal.textContent = br

Clone this wiki locally