When building the Sunasty Web QR Scanner and its companion open-source Chrome extension sunasty-qr-scanner, we noticed a recurring failure mode with standard JavaScript QR decoders like jsQR: passing a full-resolution desktop screenshot (e.g. 2560×1440 on a HiDPI display) containing a small 140×140 QR code in one corner frequently returns null.
1. Why Single-Pass Full-Frame Scans Fail
Pure JavaScript QR decoders locate finder patterns (the nested 1:1:3:1:1 square ratios) after converting the RGBA buffer into a binary bit matrix. When the QR code occupies less than 2% of the frame area:
- Downscaling the full viewport blurs high-version QR modules beyond the Nyquist limit.
- Keeping native resolution increases the adaptive threshold block size relative to the modules, merging adjacent dark dots.
- Translucent overlays reduce luminance contrast below detection margins.
2. Multi-Scale Overlapping Grid Pipeline
To achieve fast recognition on normal images while recovering small codes on dense screenshots, we run a cascaded pipeline:
-
Pass 1 (Normalized Full Frame): Scan full image capped at
1024 px. Succeeds in ~8 ms for typical images. -
Pass 2 (Native Resolution): If source exceeds
1024 px, scan at native 1:1 resolution. - Pass 3 (Overlapping 3x3 Spatial Grid): Divide image into 9 overlapping regions (each covering 48% width/height, stepped at 0%, 26%, 52%). The 22% overlap ensures any code straddling a boundary remains wholly inside at least one tile.
- Pass 4 (Luminance Binarization): Apply a hard luminance threshold at $Y = 128$ ($Y = 0.299R + 0.587G + 0.114B$) to rescue washed-out graphics.
export function decodeQrMultiScale(sourceCanvas, jsQR) {
const ctx = sourceCanvas.getContext('2d', { willReadFrequently: true });
const { width, height } = sourceCanvas;
// Pass 1: Direct scan
const fullData = ctx.getImageData(0, 0, width, height);
const directHit = jsQR(fullData.data, width, height, {
inversionAttempts: 'attemptBoth'
});
if (directHit && directHit.data) return directHit.data;
// Pass 2: Overlapping 3x3 sub-regions for small QR codes
const tileCanvas = document.createElement('canvas');
const tileCtx = tileCanvas.getContext('2d', { willReadFrequently: true });
const offsets = [0, 0.26, 0.52];
const span = 0.48;
for (const ox of offsets) {
for (const oy of offsets) {
const sx = Math.floor(width * ox);
const sy = Math.floor(height * oy);
const sw = Math.floor(width * span);
const sh = Math.floor(height * span);
tileCanvas.width = Math.min(1200, Math.round(sw * 1.5));
tileCanvas.height = Math.min(1200, Math.round(sh * 1.5));
tileCtx.imageSmoothingEnabled = false;
tileCtx.drawImage(sourceCanvas, sx, sy, sw, sh, 0, 0, tileCanvas.width, tileCanvas.height);
const tileImg = tileCtx.getImageData(0, 0, tileCanvas.width, tileCanvas.height);
const hit = jsQR(tileImg.data, tileCanvas.width, tileCanvas.height, {
inversionAttempts: 'attemptBoth'
});
if (hit && hit.data) return hit.data;
}
}
return null;
}
3. Client-Side RFC 6238 TOTP Generation (Web Crypto API)
When decoding otpauth://totp/... setup QR codes, computing 2FA codes locally prevents transmitting authentication secrets across the network. Using the W3C Web Cryptography API (crypto.subtle):
export async function generateTotp(secretBase32, timestampMs = Date.now(), period = 30, digits = 6) {
const keyBytes = decodeBase32(secretBase32);
const counter = Math.floor(timestampMs / 1000 / period);
const counterBuffer = new ArrayBuffer(8);
const counterView = new DataView(counterBuffer);
counterView.setBigUint64(0, BigInt(counter));
const cryptoKey = await crypto.subtle.importKey(
'raw',
keyBytes,
{ name: 'HMAC', hash: 'SHA-1' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', cryptoKey, counterBuffer);
const sigBytes = new Uint8Array(signature);
const offset = sigBytes[sigBytes.length - 1] & 0x0f;
const binary =
((sigBytes[offset] & 0x7f) << 24) |
((sigBytes[offset + 1] & 0xff) << 16) |
((sigBytes[offset + 2] & 0xff) << 8) |
(sigBytes[offset + 3] & 0xff);
const otp = binary % (10 ** digits);
return String(otp).padStart(digits, '0');
}
Interactive demonstration and tools available on Sunasty.
Top comments (0)