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.0.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 (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).

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
  },
});

Camera error codes

start() rejects (and the error event fires) 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.

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()).

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; everything is torn down when the component's scope disposes.

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.

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