npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cam2qr

v1.4.0

Published

Zero-dependency TypeScript QR code scanning for the browser — independent from-spec detection and decoding, camera included.

Readme

cam2qr

Zero-dependency TypeScript QR code scanning for the browser, camera included.

CI npm license: MIT

The entire pipeline (adaptive binarization, finder-pattern location, perspective correction, Reed-Solomon error correction, segment decoding) is implemented independently from the ISO/IEC 18004 specification. Not a port or a wrapper of an existing scanner. Full docs and a how-it-works deep dive live at cam2qr.com.

  • Zero runtime dependencies. One package, nothing else in your lockfile. 13.4 kB brotli for the full camera scanner; 9.5 kB if you only import decode().
  • Layered. Use decode(imageData) as a pure function (works in Node), QrScanner for batteries-included camera scanning, or the cam2qr/react, cam2qr/vue, and cam2qr/svelte adapters.
  • Fast, off the main thread. ~0.6 ms per clean frame; live scanning decodes in a module Web Worker by default with automatic main-thread fallback.
  • Robust. Decodes 100% of every distortion sweep (noise, blur, rotation, perspective, low contrast, inversion) in the public benchmark against jsQR and @zxing/library.
  • Typed errors, rich results. CameraError.code tells you why the camera failed; results carry corner points, version, EC level, corrected-codeword counts, raw bytes, segments, and a parsed-content classification (URL / WiFi / vCard / geo / tel / sms / email / GS1 element strings).

Install

npm install cam2qr

Live camera scanning

import { QrScanner } from 'cam2qr';

const scanner = new QrScanner(videoElement, {
  camera: { facing: 'environment' },
  onDecode(result) {
    console.log(result.text, result.content, result.cornerPoints);
  },
  onError(error) {
    if (error.code === 'permission-denied') showPermissionHelp();
  },
});

await scanner.start();          // asks for permission, begins scanning
scanner.setTorch(true);         // resolves false when unsupported, no throw
await scanner.setCamera({ facing: 'user' });
scanner.update({ tryHarder: true });
scanner.stop();                 // releases the camera
scanner.destroy();              // full teardown (worker, listeners)

Scanner options (all optional)

| Option | Default | Meaning | | --- | --- | --- | | camera.facing | 'environment' | 'environment' (rear) or 'user' (front) | | camera.deviceId | – | exact camera from listCameras() (overrides facing) | | camera.resolution | 1280×720 | ideal capture resolution | | maxScansPerSecond | 15 | decode attempts per second (battery/CPU dial) | | scanRegion | full frame | sub-rectangle (or video => region) to decode; a big CPU saver | | useWorker | true | decode in a Web Worker when available | | useNativeDetector | false | use the browser's BarcodeDetector when present, falling back to our engine when missing or failing; native results carry placeholder codec metadata (version: 0, mask: -1, zero ecc, EC level 'M') | | pauseOnHidden | true | pause while the tab is hidden | | tryInverted | true | also try light-on-dark codes | | tryHarder | false | extra passes: more finder triples + a 2× low-pass (blur recovery) | | maxDownscale | 2 | downscale huge frames by up to this factor before decoding | | dedupeWindowMs | 1500 | quiet period before the same payload fires again, timed from when it was last seen — see below (0 = off; tracked per payload) | | stopOnDecode | false | stop the camera after the first result | | multiple | false | decode every code in the frame; each symbol fires its own decode event | | structuredAppend | 'reassemble' | join multi-symbol sequences into one decode event (parity-checked); 'individual' fires each symbol separately | | onDecode / onError | – | callbacks; scanner.on('decode' \| 'detect' \| 'error' \| 'start' \| 'stop', …) also works | | onDetect | – | fires every scanned frame with located symbol candidates (or null); corner points arrive before/regardless of a successful decode, for live outline overlays |

Also on the instance: pause() / resume(), setZoom(level), getCapabilities(), listCameras() (exported standalone).

How deduping works

dedupeWindowMs is timed from when a payload was last seen, not from when it last fired, and every frame the code stays in view refreshes that timer. So a code held steadily in front of the camera fires onDecode exactly once, however long you hold it — it has to leave the frame for longer than the window before it can fire again.

That is what you want for scanning a series of items (each new code fires immediately, and one lingering code cannot spam you). It does mean two things are not what a first reading suggests:

  • Holding one code in frame does not re-fire every dedupeWindowMs.
  • Re-scanning the same item requires it to leave the frame for a full window — pointing away and back is enough.

Set dedupeWindowMs: 0 to fire on every decoded frame and dedupe yourself.

Live outline overlays

onDetect reports corner points in video pixel coordinates. A canvas sized to videoWidth×videoHeight and CSS-stretched exactly like the video can draw them directly (see demo/); for overlays positioned in CSS pixels, videoToElementCoordinates(points, video) maps them, accounting for object-fit letterboxing/cropping.

const scanner = new QrScanner(video, {
  onDetect(detections) {
    clearOverlay();
    for (const d of detections ?? []) drawPolygon(d.cornerPoints); // tracks while you aim
  },
  onDecode(result) {
    flashPolygon(result.cornerPoints); // confirmed decode
  },
});

Error codes

start() rejects with a CameraError whose code is one of permission-denied, camera-not-found, camera-in-use, insecure-context (camera needs HTTPS or localhost), unsupported, stream-failed.

The error event carries either typed flavor, so you can tell a camera fault from a decode fault: a CameraError for camera/stream problems (the codes above), or a DecodeError when the decode runner itself fails — a frame that failed to decode or the Web Worker crashing (code: 'runner-failed'). Match on error.name:

scanner.on('error', (error) => {
  if (error.name === 'CameraError' && error.code === 'permission-denied') showPermissionHelp();
  // DecodeError (e.g. 'runner-failed') means decoding hiccuped, not the camera —
  // the scanner keeps running (a failed worker transparently falls back inline).
});

One-shot decoding

import { decode } from 'cam2qr';

const result = decode(imageData, { tryHarder: true }); // QrResult | null

Takes any { data, width, height } RGBA buffer (a canvas ImageData, a decoded PNG in Node, …). Options: tryInverted, tryHarder, maxDownscale, parseContent. decode() returns null when no decodable code is present and only throws on genuinely invalid input.

Multiple codes per frame

import { decodeAll } from 'cam2qr';

const results = decodeAll(imageData); // QrResult[]: every decodable symbol, deduplicated

decodeAll always runs the full pass plan (all scales, plus the inverted pass), so it can find a dark-on-light and a light-on-dark code in the same frame. On the scanner, set multiple: true instead.

Locate without decoding

import { detect } from 'cam2qr';

const candidates = detect(imageData); // Detection[]: { cornerPoints, moduleSize }

Cheaper than a decode and useful for framing feedback; candidates are plausibility-ranked and may include finder-like decoys that would not survive a decode.

The result

interface QrResult {
  text: string;                    // decoded, charset-aware (ECI/UTF-8/Shift-JIS kanji)
  bytes: Uint8Array;               // raw payload bytes
  content?: ParsedContent;         // url | wifi | vcard | geo | tel | sms | email | gs1 | text
  cornerPoints: [Point, Point, Point, Point]; // symbol outline in image pixels
  moduleSize: number;              // measured pixels per module
  version: number;                 // 1–40
  errorCorrectionLevel: 'L' | 'M' | 'Q' | 'H';
  mask: number;
  segments: Segment[];             // per-mode payload breakdown (incl. kanji)
  ecc: { blocks: number; codewordsCorrected: number }; // damage signal
  structuredAppend?: { index: number; total: number; parity: number };
  fnc1?: { position: 'first' } | { position: 'second'; applicationIndicator: string };
}

GS1 symbols (FNC1 in first position) get their element strings split into application identifiers: content becomes { type: 'gs1', elements: [{ ai: '01', value: '0401…' }, …] }, with GS separators and alphanumeric % escapes handled per spec.

Structured-append sequences (one payload split across up to 16 symbols) are reassembled by the scanner by default: parts are withheld and a single decode event fires with the joined payload once every symbol has been seen and the parity byte checks out. Show all symbols at once with multiple: true, or pan across them within 30 s. decode()/decodeAll() stay pure and return the parts with their structuredAppend headers.

React

import { useQrScanner } from 'cam2qr/react';

function Scanner() {
  const { videoRef, result, error, isScanning, scanner } = useQrScanner({
    onDecode: (r) => console.log(r.text),
  });
  return <video ref={videoRef} />;
}

The hook starts the camera when the video mounts, releases it on unmount, and re-renders on results/errors. Pass enabled: false to keep the camera off; use scanner for imperative control (torch, camera switching, update()).

Live-tunable options are applied as they change, so you can drive them straight from state:

useQrScanner({ dedupeWindowMs, maxScansPerSecond, tryHarder, scanRegion });

That covers maxScansPerSecond, scanRegion, tryInverted, tryHarder, maxDownscale, dedupeWindowMs, stopOnDecode, multiple and structuredAppend — the same set scanner.update() accepts.

Everything else is fixed when the scanner is created, because changing it means reacquiring the camera. In particular, changing camera on a later render does not switch cameras — switch imperatively instead:

scanner?.setCamera({ facing: 'user' }); // or { deviceId } from listCameras()

useWorker, useNativeDetector and pauseOnHidden are mount-time config; to change one, remount the scanner (e.g. with a key).

Pass paused to suspend and resume decoding without releasing the camera — no black-flash or permission re-prompt, unlike toggling enabled. It is reactive and correct from the first render, so paused: true starts the scanner suspended (the stream still warms up) and flips live as the prop changes:

const { videoRef } = useQrScanner({ paused: !isSheetOpen });

Vue

<script setup>
import { useQrScanner } from 'cam2qr/vue';

const { videoRef, result, error, isScanning, scanner } = useQrScanner({
  onDecode: (r) => console.log(r.text),
});
</script>

<template>
  <video ref="videoRef"></video>
  <p v-if="result">{{ result.text }}</p>
</template>

Same lifecycle as the React hook, with refs instead of state. enabled accepts a Ref to toggle the camera reactively; paused (also boolean | Ref<boolean>) suspends and resumes decoding while keeping the camera stream warm — no black-flash, unlike enabled. Everything is torn down when the component's scope disposes.

The composable takes its options object once, so unlike the React hook it does not re-read them per render: tune a running scanner with scanner.value?.update({ … }).

Svelte

<script>
  import { createQrScanner } from 'cam2qr/svelte';

  const { video, result, isScanning } = createQrScanner({
    onDecode: (r) => console.log(r.text),
  });
</script>

<video use:video></video>
{#if $result}<p>{$result.text}</p>{/if}

Readable stores plus an action (built on svelte/store only, so Svelte 3, 4, and 5 all work). The camera starts when the <video> mounts and is released when it unmounts, so you can gate the element with {#if} to toggle scanning. To suspend decoding without releasing the camera, pass paused — a boolean, or a store to toggle reactively:

<script>
  import { writable } from 'svelte/store';
  const paused = writable(false);
  const { video, result } = createQrScanner({ paused });
</script>

<video use:video></video>
<button on:click={() => paused.update((p) => !p)}>Toggle</button>

Bundlers & legacy resolvers (CRA, TS < 5)

The subpath entries (cam2qr/react, cam2qr/vue, cam2qr/svelte) work out of the box with modern bundlers and TypeScript's bundler/node16/nodenext resolution via the package exports map. For toolchains still on classic moduleResolution: node — TypeScript < 5, Create React App / react-scripts, older Jest/Metro resolvers — the package also ships directory stubs (react/package.json, …) that point those resolvers at the same build, so import { useQrScanner } from 'cam2qr/react' resolves its types and its module without any declare module shim or Jest moduleNameMapper. No configuration is needed; the root decode/QrScanner entry never depended on this.

Node and other runtimes

decode(), decodeAll(), and detect() are pure functions over RGBA bytes with no DOM or Node API usage, so they run in browsers, workers, Node, and Hermes. The camera layer is browser-only by nature. For React Native, docs/react-native.md sketches the adapter approach over react-native-vision-camera.

Benchmarks

docs/benchmarks.md is regenerated by pnpm compare, which feeds the same seeded synthetic frames to cam2qr, jsQR, and @zxing/library (both are devDependencies used as measurement baselines only). Snapshot from the current run: cam2qr with tryHarder decodes 100% of every distortion sweep (jsQR drops to 60% on noise and 48% on low contrast; zxing to 20% on noise and 0% on inverted codes), at ~0.6 ms per clean small frame: ~5× faster than jsQR, on par with zxing. The harness asserts cam2qr's detection-rate floors, so quality regressions fail CI.

There is also a regression corpus in test/fixtures/corpus, where every image must decode, enforced by the main test suite. "Fails to scan" reports become corpus images; see its README.

How it's tested

Every decoder stage has unit tests (GF(256), Reed-Solomon with injected errors, BCH format/version, masks, bitstream). Round-trip tests generate symbols with an external generator and decode them across all 40 versions × 4 EC levels × 8 masks. A pure-TS software renderer produces distorted images (rotation, perspective, noise, blur, lighting gradients, inversion) for detection tests. The scanner loop is integration-tested against fake camera hardware, and Playwright feeds a generated video to Chromium's fake camera to verify the shipped bundle end to end.

Demo

pnpm demo builds the library and serves a scanner page (Vite, port 5183) that runs against the built artifacts: camera picker, torch, pause, tryHarder toggle, decode outline overlay, and parsed-content display.

Contributing

Bug reports with a sample image are especially valuable, since every "fails to scan" report can become a permanent regression test. See CONTRIBUTING.md for setup, commands, architecture, and conventions.

License

MIT