Skip to content

Feature request sponsorship goals

minhaz edited this page Oct 11, 2023 · 7 revisions

Update The author is not taking any feature requests at this point - please do not sponsor for feature requests.

Hi, thanks for taking time to support me!

I work on this library outside of original work hours, it get's hard to balance time between supporting this project, fixing known issues and adding new features. After struggling through bandwidth issue - I feel a nice incentive to support feature requests for this project would be via sponsorships.

If you feel this project adds commercial value to your project and your project would benefit from certain feature requests listed here please support the sponsorship goal for the same.

If your feature request is not listed, please reach out to me at [email protected].

I'd continue to support and build these features, but achieving the sponsorship goals would help speedup things. Read more on Why should you support html5-qrcode project by sponsoring

Either of these works!

Feature requests (Author not taking any FR at this point)

Move file selection into the camera view itself

Today in html5-qrcode (when using Html5QrcodeScanner) file based scanning and camera based scanning are disjoint.

In this feature request, the plan is to merge them into single option so that users can scan using camera or choose to just drop and image or directly select an image without doing "Stop scanning" first.

This would enhance the user experience very well.

This could be up to 2 weeks effort once started.

Inline switch camera UI in Html5QrcodeScanner class.

Today users need to do Stop Scanning > Select Camera. Change this to Switch Camera so that users can switch camera directly in single step (just like native camera on devices).

In this feature, I'll also support a dropdown internally to select the camera from list.

Multi language support

Add support languages outside of English.

This is a very important feature and on my radar. Require technical and API changes.

Support Zoom and Crop in file based scanning

Several file based scanning fails today due to small viewport size and lack of support for any zooming or cropping on the selected image file. The plan is to add support for this in Html5QrcodeScanner.

High resolution video support

To support scanning small QR or barcodes.

Support different camera aspect ratio with UI toggle

Easy to configure aspect ratio toggle in UI like in native cameras.

Improve scanner quality.

Some codes don't work very well today. This require debugging and making improvements to core algorithms.

Add QR code generation support

Title says it all!

Add barcode generation support.

Same as above.

<!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