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

tapirscan

v1.5.0

Published

Orientation-aware barcode scanning with a Rust/WASM core and four effort modes

Readme

Tapirscan for JavaScript and TypeScript

Scan barcodes in the browser or Node with a Rust/WASM core. Each scan returns every decoded barcode with its text, format and source-image polygon / rect.

  • tapirscan/browser/medium (or /low, /high, /very-high) for web apps: scan files, <img>, <video>, canvases or bitmaps in a bundled worker, with no WASM setup. tapirscan/browser offers every mode and Turbo preset.
  • tapirscan/camera (preview) scans a live camera with one of those scanners.
  • tapirscan (the core) for Node and decoded pixels, scanning synchronously on the calling thread.

Try the live demo · Browser apps · Core API · WASM loading · All options · Results

npm install tapirscan

TypeScript declarations and WASM binaries are included. Your runtime must support WebAssembly SIMD; Node 20 or later is required.

Supported formats

All formats below are supported. A ✓ means the format is included in that keyword group; — means it is not included. retail is the default.

| Format identifier | retail | common1D | common | 1D | 2D | all | Scope | | --- | :---: | :---: | :---: | :---: | :---: | :---: | --- | | EAN13 | ✓ | ✓ | ✓ | ✓ | — | ✓ | EAN-13 | | UPCA | ✓ | ✓ | ✓ | ✓ | — | ✓ | UPC-A; normalized from zero-prefixed EAN-13 | | EAN8 | ✓ | ✓ | ✓ | ✓ | — | ✓ | EAN-8 | | UPCE | ✓ | ✓ | ✓ | ✓ | — | ✓ | UPC-E | | Code128 | — | ✓ | ✓ | ✓ | — | ✓ | Includes GS1-128 | | Code39 | — | ✓ | ✓ | ✓ | — | ✓ | Standard characters; full ASCII is Code39Extended | | ITF | — | ✓ | ✓ | ✓ | — | ✓ | Interleaved 2 of 5 | | Codabar | — | — | — | ✓ | — | ✓ | | | Code93 | — | — | — | ✓ | — | ✓ | | | QRCode | — | — | ✓ | — | ✓ | ✓ | Model 2; no Model 1 or rMQR | | DataMatrix | — | — | ✓ | — | ✓ | ✓ | ECC200 and DMRE | | PDF417 | — | — | — | — | ✓ | ✓ | Normal/Compact and Macro; no Micro PDF417 | | Aztec | — | — | — | — | ✓ | ✓ | Compact/full and Rune | | DataBar | — | — | — | ✓ | — | ✓ | Omnidirectional, Stacked, Stacked Omnidirectional | | DataBarExpanded | — | — | — | ✓ | — | ✓ | Expanded and Expanded Stacked | | MaxiCode | — | — | — | — | ✓ | ✓ | Modes 2–6 | | Code32 | — | — | — | — | — | — | Opt-in: Italian pharmacy number | | PZN | — | — | — | — | — | — | Opt-in: German PZN8/PZN7 | | MSIPlessey | — | — | — | — | — | — | Opt-in: mod-10 check digit required | | Industrial25 | — | — | — | — | — | — | Opt-in: Industrial (Standard) 2 of 5 | | Pharmacode | — | — | — | — | — | — | Opt-in: Laetus one-track Pharmacode | | DataBarLimited | — | — | — | ✓ | — | ✓ | | | MicroQRCode | — | — | — | — | ✓ | ✓ | M1–M4 | | Code39Extended | — | — | — | — | — | — | Opt-in: full-ASCII Code 39 | | IATA25 | — | — | — | — | — | — | Opt-in: IATA 2 of 5 | | Matrix25 | — | — | — | — | — | — | Opt-in: Matrix 2 of 5 |

Code32, PZN, MSIPlessey, Industrial25, Pharmacode, Code39Extended, IATA25 and Matrix25 require explicit selection; no group includes them, even all. See the format guide for variant details and EAN/UPC supplements.

Browser apps, React and Svelte

import { scan } from "tapirscan/browser/medium";

const result = await scan(file); // a File from <input type="file">
console.log(result.values); // e.g. ["4006381333931"]

The one-shot scan starts and stops a worker for every call. Reuse a scanner for several images or camera frames. Construction is synchronous: the worker and WASM load in the background, and the first scan waits for them.

import { Scanner } from "tapirscan/browser/medium";

const scanner = new Scanner({ formats: ["EAN13", "QRCode"] });
const result = await scanner.scan(image); // a file, <img>, canvas, video frame, ...
scanner.dispose(); // stops the worker

Entries: tapirscan/browser/low, /medium, /high and /very-high fix the mode and put one WASM file into the build. tapirscan/browser takes a mode or Turbo preset at run time and downloads only that one, but bundlers emit all eight files. Sizes: bundle one mode.

Camera scanning

tapirscan/camera runs a camera into a <video> and scans its frames with any browser scanner. It is a preview: its API may change in minor releases.

import { Scanner } from "tapirscan/browser/medium";
import { CameraScanner } from "tapirscan/camera";

const scanner = new Scanner({ formats: ["EAN13", "QRCode"] });
const camera = new CameraScanner(video, scanner, {
  onScan(result) {
    // Every scanned frame; result.barcodes is empty when nothing was read.
  },
  onError(error) {
    // The scanner failed or the camera ended; the camera is released.
  },
});
await camera.start(); // rejects if permission is denied or there is no camera
camera.stop(); // before dispose(), which would otherwise report through onError
scanner.dispose();
  • Frames: one frame is scanned at a time; slow devices skip frames instead of queueing them. No result arrives after stop(). scanInterval: 200 starts scans at least 200 ms apart, for fewer scans per second. Pausing the <video> pauses scanning, for example to freeze the frame of a detection, until it plays again; stop() also releases the camera.
  • Confirmed reads: onDetect(barcode) is called once per appearance of a barcode, after confirmations consecutive scans read it (default 1). The barcode counts as gone only after it is missing from at least 3 consecutive scans and for at least 500 ms, so a single blurred frame does not report it twice; it is reported again when it returns or the camera reopens. Barcodes with the same format and text count as one, so onDetect fires once while any of them stays in view. confirmations: 3 filters out a single misread; onScan still receives every scanned frame.
  • Region: region scans only part of each frame; see Scanning a region.
  • Choosing a camera: start() prefers the back camera and falls back to any camera. start({ facingMode: "user" }) prefers the front camera; start({ deviceId }) opens exactly that device. Calling start while running switches cameras, releasing the current one first because many phones open one camera at a time. CameraScanner.cameras() lists video inputs; browsers show labels only after permission, so list them after start().
  • Hidden pages: the camera is released while the page is hidden and reopened when it is shown. pauseWhenHidden: false keeps it open.
  • Torch: camera.torchAvailable and await camera.setTorch(true), which resolves false without a controllable torch (front cameras, most laptops, some browsers). The torch turns off when the camera reopens.
  • start() result: true once the camera is open; false when stop() or another start() cancelled it, or the page is hidden (the camera opens when it is shown). A start() waits for a camera request of an earlier call that is still pending, such as an unanswered permission prompt. Errors thrown by onScan or onDetect go to the global error handler (reportError) and do not stop scanning; without onError, a failure that stops scanning goes there too.
  • Other settings: constraints adds video constraints other than the camera choice; a width or height replaces the default 1280×720 ideal size. camera.track is the open MediaStreamTrack, for example for zoom. scanOptions applies to every scanned frame.
  • Requirements: HTTPS or localhost, and requestVideoFrameCallback (Firefox 132 or later). The controller sets muted and playsinline for iOS.

The controller never disposes the scanner. In React (add "use client"; at the top in Next.js):

import { useEffect, useRef, useState } from "react";
import { Scanner } from "tapirscan/browser/medium";
import { CameraScanner } from "tapirscan/camera";

export default function BarcodeScanner() {
  const video = useRef(null);
  const [barcodes, setBarcodes] = useState([]);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Select only the formats you need.
    const scanner = new Scanner({ formats: ["EAN13", "QRCode"] });
    const camera = new CameraScanner(video.current, scanner, {
      // Skip re-rendering for consecutive empty frames.
      onScan: ({ barcodes }) =>
        setBarcodes((shown) => (barcodes.length || shown.length ? barcodes : shown)),
      onError: (cause) => setError(cause.message),
    });
    camera.start().catch((cause) => setError(cause.message));
    return () => {
      camera.stop();
      scanner.dispose();
    };
  }, []);

  return (
    <>
      <video ref={video} />
      {error && <p role="alert">{error}</p>}
      {barcodes.map((barcode, i) => (
        <p key={i}>
          {barcode.format}: {barcode.text}
        </p>
      ))}
    </>
  );
}

In Svelte 5 and SvelteKit:

<script>
  import { Scanner } from "tapirscan/browser/medium";
  import { onDestroy } from "svelte";

  const scanner = new Scanner();
  onDestroy(() => scanner.dispose());

  let barcodes = $state.raw([]);
  let error = $state(null);
  let latest = 0; // scans can finish out of order; keep only the newest
  async function onchange(event) {
    const file = event.currentTarget.files?.[0];
    if (!file) return;
    const request = ++latest;
    try {
      const found = await scanner.scan(file);
      if (request === latest) [barcodes, error] = [found.barcodes, null];
    } catch (cause) {
      if (request === latest) [barcodes, error] = [[], cause.message];
    }
  }
</script>

<input type="file" accept="image/*" {onchange} />
{#if error}<p role="alert">{error}</p>{/if}
{#each barcodes as barcode}<p>{barcode.format}: {barcode.text}</p>{/each}

Drawing results over the video

polygon is in frame pixels (videoWidth × videoHeight). A canvas with the frame's pixel size, laid over the video with the same CSS box and object-fit, needs no coordinate conversion:

<div style="position: relative">
  <video style="display: block; width: 100%"></video>
  <canvas
    style="position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none"
  ></canvas>
</div>
const overlay = document.querySelector("canvas");
function draw({ barcodes }) {
  overlay.width = video.videoWidth; // also clears the canvas
  overlay.height = video.videoHeight;
  const context = overlay.getContext("2d");
  context.lineWidth = 4;
  context.strokeStyle = "lime";
  for (const { polygon } of barcodes) {
    context.beginPath();
    for (const [x, y] of polygon) context.lineTo(x, y);
    context.closePath();
    context.stroke();
  }
}
// new CameraScanner(video, scanner, { onScan: draw, onError })

Give the canvas the video's object-fit and any mirroring transform (for example scaleX(-1) on a front camera preview).

Scanning a region

region scans only part of each frame, given in fractions of the frame's width and height, and ignores barcodes outside it, such as neighbors on a shelf. Results keep whole-frame coordinates, so an overlay draws them as before.

// The middle 60% horizontally and 40% vertically, as a wide viewfinder:
const camera = new CameraScanner(video, scanner, {
  region: { left: 0.2, top: 0.3, width: 0.6, height: 0.4 },
  onDetect,
  onError,
});

The fractions apply to whatever size the camera delivers, so a region that is square on a landscape 1280×720 frame, such as { left: 0.33, top: 0.2, width: 0.34, height: 0.6 }, is not square on a portrait 720×1280 one.

For a single image, draw its region into a canvas, scan the canvas and add the region's left and top to each barcode's polygon and rect.

Camera picker and zoom

await camera.start(); // labels are only visible after permission
const select = document.querySelector("select");
select.replaceChildren(
  ...(await CameraScanner.cameras()).map(
    (c, i) => new Option(c.label || `Camera ${i + 1}`, c.deviceId),
  ),
);
select.onchange = () => camera.start({ deviceId: select.value }); // switches cameras

const zoom = camera.track?.getCapabilities?.().zoom; // undefined where unsupported
if (zoom) await camera.track.applyConstraints({ advanced: [{ zoom: Math.min(2, zoom.max) }] });

Store select.value (for example in localStorage) to reopen the same camera next time; start() rejects if that device is gone, so fall back to start().

Recorded video

Scan a video file or stream in a <video> element with your own frame loop. Requesting the next frame only after a scan finishes skips the frames in between, as CameraScanner does:

const video = document.querySelector("video"); // <video src="clip.mp4" muted playsinline>
video.requestVideoFrameCallback(async function onFrame() {
  const { barcodes } = await scanner.scan(video);
  for (const barcode of barcodes) console.log(video.currentTime, barcode.text);
  if (!video.ended) video.requestVideoFrameCallback(onFrame);
});
await video.play();

Hosting, Content Security Policy and static files: browser deployment. Switching from ZXing, ZBar, Quagga or html5-qrcode: see the migration guides.

Bundlers: Next.js (Turbopack or webpack) and Vite bundle the worker and WASM files with no configuration. Angular production builds need the worker option and a copied WASM asset; see Angular. The Vite 6 and 7 development servers (including SvelteKit on them) need one line, or the scanner reports that its worker failed to start:

// vite.config.js
export default defineConfig({
  // ...your plugins
  optimizeDeps: { exclude: ["tapirscan"] },
});
  • Sources: a File or Blob (any image the browser decodes), <img>, <video> (its current frame), <canvas>, OffscreenCanvas, ImageBitmap, VideoFrame, ImageData, or decoded pixels as in the core API. Inputs are not modified or transferred, unless you pass { transfer: true }. Images and canvases are composited onto white, so transparent areas count as background; ImageData and pixel buffers are scanned as given, with alpha ignored.
  • Methods: scan and inspect work as in the core API but return promises. Each call captures its image and options when it starts, so buffers and option objects can be reused immediately. Calls reach the worker in call order.
  • Options: listed under all options; loadWasm needs the core. worker is for bundlers that do not emit workers from packages, such as Angular. transfer: true saves one full copy per call when the pixels span their whole, non-shared ArrayBuffer; other inputs are copied as usual.
  • Lifecycle: scanner.ready resolves once loaded; awaiting it is optional, because loading errors also reject every scan. During server rendering the constructor does nothing and scans reject. dispose() stops the worker and immediately rejects every unfinished scan; returned results stay valid.
  • Errors: unknown option names and invalid wasmBaseUrl values throw in the constructor; invalid option values and scan arguments reject with TypeError; engine failures reject with ScannerError. Browser image decoding and loading can also fail.
  • Requirements: module workers, OffscreenCanvas and WebAssembly SIMD: Chrome 91, Firefox 114, Safari 16.4 or later. The camera loop also needs requestVideoFrameCallback. Images may have at most 65,535 pixels per side and 256 megapixels.
  • Support check: browserSupported() from tapirscan/support, which imports nothing, tells whether this browser can run a scanner, before any download (wasmSupported() checks WebAssembly SIMD alone, for the core). A scanner created where it cannot starts no worker and rejects with ScannerError code "unsupported".

Fast scanning in the browser:

  • Select only the formats you need; each enabled 2D format searches the whole image.
  • Reuse one Scanner. For camera loops, request 720p or 1080p frames, and use tapirscan/browser/low or a Turbo preset if the preview stutters.
  • Inputs from cheapest: decoded pixels with { transfer: true }, decoded pixels or ImageData, <video>/canvas sources (a canvas readback each), then File/Blob (includes image decoding). Pass pixels as you have them (RGBA is cheapest for the retail formats); do not convert color.

See fast end-to-end scanning for measured costs.

The camera example is a complete page: from this directory (or the installed package), run python3 -m http.server and open /examples/camera.html on localhost.

Core API

The core scans decoded pixels, for example a canvas's ImageData:

import { scan } from "tapirscan";

const image = context.getImageData(0, 0, canvas.width, canvas.height);
const result = await scan(image);
console.log(result.values); // e.g. ["4006381333931"]

The one-shot helper creates and disposes a scanner on every call. Defaults are Medium effort and retail formats. In browsers the core needs its WASM assets served; Node loads the packaged files automatically.

Reuse a scanner for repeated images. Its scans are synchronous:

import { Scanner } from "tapirscan";

const scanner = await Scanner.create({ mode: "high", formats: "1D" });
try {
  const result = scanner.scan(image);
  for (const barcode of result.barcodes) {
    console.log(barcode.text, barcode.format, barcode.polygon);
  }
} finally {
  scanner.dispose();
}

Settings also work with the helper: await scan(image, { mode: "high", formats: "1D" }). For unread regions, timing and diagnostics, call inspect instead of scan.

The core scans synchronously, so a long scan on the main thread blocks the page. Use tapirscan/browser, or create one core scanner inside your own worker and transfer frame buffers to it; configure loadWasm inside that worker.

| Function | Returns | Behavior | | ------------------------------------- | --------------------------- | ---------------------------------------------------------------------------- | | scan(image, options = {}) | Promise<ScanResult> | One image; creates and disposes a scanner. | | inspect(image, options = {}) | Promise<InspectionResult> | Like scan, with timing, unread regions and diagnostics. | | Scanner.create(options = {}) | Promise<Scanner> | A reusable scanner with fixed mode, default formats and supplement policy. | | scanner.scan(image, { formats }) | ScanResult | Synchronous scan; formats optionally overrides the defaults for this call. | | scanner.inspect(image, { formats }) | InspectionResult | Synchronous inspection. | | scanner.dispose() | void | Releases WASM memory. Repeated disposal is safe; do not scan after disposal. |

Create another scanner to change effort or the supplement policy. Per-call formats such as scanner.scan(image, { formats: "QRCode" }) apply only to that call; scanner.formats exposes the defaults. Results stay valid after disposal.

Experimental Turbo presets

For faster 1D barcode scanning, opt into a Turbo preset instead of a mode. The core and tapirscan/browser accept it; in the browser it suits camera loops:

import { Scanner } from "tapirscan/browser";

const scanner = new Scanner({ experimentalTurbo: 2, formats: "retail" });
const result = await scanner.scan(video);

Accepted values are 2, 4, 8 and 16. Each number is the targeted speedup over Low; the actual speedup varies with images and devices and is not guaranteed. Higher presets do less work and can miss more barcodes, including clean symbols placed close together. Start with 2 and check detection on your own inputs. The presets speed up EAN-13, UPC-A, EAN-8, UPC-E, Code 128, Code 39 and ITF. Other formats stay readable when selected, but do not get faster, and mixed-format scans still pay for the enabled 2D readers. See Turbo behavior and limitations.

experimentalTurbo and mode are mutually exclusive, and Turbo requires eanAddOnPolicy: "ignore". A Turbo scanner has no effort mode: inspect results report experimentalTurbo and omit mode. The core scanner also exposes scanner.experimentalTurbo and leaves scanner.mode undefined; the browser Scanner has no configuration properties.

Stability: this option, its presets and their asset imports may change or be removed in a minor release. Pin the exact package version if you rely on them.

tapirscan/browser loads the preset's WASM by itself. With the core in Vite or SvelteKit, load the matching asset as in WASM loading: tapirscan/wasm/experimental-turbo2.wasm, -turbo4, -turbo8 or -turbo16.

WASM loading

In Node, the default loader reads assets from the installed package. Decode your image with an image library first, then pass grayscale, RGB or RGBA bytes, for example with sharp:

import sharp from "sharp";
import { Scanner } from "tapirscan";

const scanner = await Scanner.create({ formats: ["EAN13", "QRCode"] });
for (const file of files) {
  const { data, info } = await sharp(file).raw().toBuffer({ resolveWithObject: true });
  const result = scanner.scan({
    data,
    width: info.width,
    height: info.height,
    channels: info.channels,
  });
  console.log(file, result.values);
}
scanner.dispose();

Filenames, URLs and encoded JPEG/PNG bytes are not scan inputs.

Vite and SvelteKit

tapirscan/browser needs no WASM setup. To use the core directly, import the WASM asset URL so Vite includes it in development and production builds, including apps deployed under a base path:

import { Scanner } from "tapirscan";
import mediumWasmUrl from "tapirscan/wasm/medium.wasm?url";

// Load once; reuse these bytes if you create more than one scanner.
const response = await fetch(mediumWasmUrl);
if (!response.ok) throw new Error(`WASM load failed: ${response.status}`);
const bytes = await response.arrayBuffer();
const scanner = await Scanner.create({ loadWasm: async () => bytes });

The imports are tapirscan/wasm/low.wasm, medium.wasm, high.wasm and very-high.wasm; match the asset to mode (the example uses the default Medium). A mismatched asset is an error. ?url is Vite syntax. In SvelteKit, create core scanners in browser code such as onMount, not during server rendering.

Other browser setups

The default loader fetches assets relative to the module. If your bundler relocates modules, copy the WASM files to your public directory as part of your build, so package upgrades cannot leave stale assets:

mkdir -p public/tapirscan
cp node_modules/tapirscan/wasm/*.wasm public/tapirscan/

Then point the scanner at that directory:

const scanner = await Scanner.create({ wasmBaseUrl: "/tapirscan/" });

wasmBaseUrl accepts a string or URL, with or without a trailing slash. Relative URLs resolve against the page or worker in browsers and the package module in Node. For authenticated requests or custom storage, use loadWasm: async (url) => arrayBuffer; it receives URLs resolved against wasmBaseUrl. The default loader caches loaded assets within a module instance; custom loaders manage their own caching. Each scanner owns its own WASM instance.

All options

| Option | Where | Default | Meaning | | ------------------- | ---------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | mode | Creation¹ | "medium" | "low", "medium", "high", "very-high". | | formats | Creation / scan | "retail" | An identifier, "retail", "common1D", "common", "1D", "2D", "all", or a nonempty array. Per-call formats override the defaults. | | eanAddOnPolicy | Creation | "ignore" | "ignore", "read", "require"; see supplements. | | wasmBaseUrl | Creation | Module-relative assets | Directory URL for the packaged WASM files. | | loadWasm | Creation | Module-relative loader | (url: URL) => Promise<ArrayBuffer>. | | experimentalTurbo | Creation¹ | Unset | Experimental: 2, 4, 8 or 16. Mutually exclusive with mode. | | worker | Browser creation | The entry's worker | () => Worker running an entry's worker module, such as tapirscan/browser/medium/worker. | | transfer | Browser scan | false | true hands a decoded-pixel buffer to the worker instead of copying it; the buffer is detached. |

¹ Core and tapirscan/browser only; single-mode entries reject it.

"retail" selects EAN13, UPCA, EAN8 and UPCE. "common1D" adds Code128, Code39 and ITF; "common" adds QRCode and DataMatrix. "1D" and "2D" enable linear or 2D formats. The tapirscan entry exports retailFormats, commonLinearFormats, commonFormats, linearFormats and matrixFormats to compose custom selections; the browser entries do not. See format and keyword-group matrix for identifiers and variants.

Image input

The core accepts ImageData (or an object with its data, width and height) or an explicit pixel buffer:

| Field | Type | Meaning | | ----------------- | ------------- | ------------------------------------------------------------------------------------ | | data | Uint8Array | Decoded pixels. ImageData uses Uint8ClampedArray with tightly packed RGBA. | | width, height | number | Integer dimensions, at least 3 pixels each. | | channels | 1 \| 3 \| 4 | Grayscale, RGB or RGBA. Alpha is ignored. Required for explicit buffers. | | stride | number | Bytes between row starts; defaults to width × channels. Larger values allow padding. |

Input is limited to 65,535 pixels per side, 256 megapixels and 1 GiB of addressed pixels. Images above 32 megapixels are scanned as a reduced copy; results stay in source coordinates, but barcodes that are small in such an image may be missed. Such images are copied into WASM a few rows at a time, so only the reduction is held, not the whole image. Keep the buffer unchanged during the call.

A scanner's WASM memory grows to fit the largest image it has scanned (about 50 MB after a 12-megapixel photo) and is released only by dispose(). On phones, where browsers close memory-hungry tabs, scan occasional large photos with a separate scanner and dispose it afterwards; keep one long-lived scanner for camera frames. Coordinates start at the top left, x rightward and y downward. If you resize or rotate before scanning, map coordinates back yourself.

Results

scan returns a ScanResult; inspect returns an InspectionResult with the same fields plus the ones marked inspect.

| Field | Type | Meaning | | ------------------------------ | -------------------------------------------------------------- | --------------------------------------------------------------------------- | | result.values | readonly string[] | Decoded strings, one per barcode. | | result.barcodes | readonly Barcode[] | Decoded barcodes with format and geometry. | | result.best | Barcode \| undefined | Highest-support barcode, first on ties. | | result.image | { width, height } | inspect: dimensions of the supplied pixels. | | result.mode | Mode \| undefined | inspect: effort mode used; absent for Turbo presets. | | result.elapsedMs | number | inspect: scan time, excluding image loading and scanner creation. | | result.undecoded | readonly UndecodedRegion[] | inspect: regions without a decode. | | result.diagnostics | Diagnostics | inspect: engine evidence. | | barcode.text | string | Decoded text. | | barcode.format | Format | Symbology identifier. | | barcode.polygon | Quad | Four [x, y] corners in input-image coordinates. | | barcode.rect | { left: number, top: number, width: number, height: number } | Enclosing integer rectangle. | | barcode.support | number | Reader-specific ranking evidence used by best. | | barcode.payloadBytes | readonly number[] \| undefined | Decoded payload bytes, when available. | | barcode.gs1 | boolean \| undefined | GS1 indicator, when supplied by the reader. | | barcode.readerInitialization | boolean \| undefined | Data Matrix, PDF417, Aztec and MaxiCode reader-initialization flag. | | barcode.structuredAppend | StructuredAppend \| undefined | Multipart metadata: one-based index, count, optional id and parity. | | barcode.eanAddOn | string \| undefined | EAN/UPC supplement, with eanAddOnPolicy "read" or "require". | | barcode.codabarStartStop | string \| undefined | Codabar start and stop characters, such as "AB"; text omits them. | | barcode.content | Content \| undefined | Structured content of text; absent for plain text. |

Separate labels with the same value stay separate entries. Results are deeply frozen; use structuredClone(result) for a mutable copy. JSON.stringify(result) gives plain JSON.

support is an uncalibrated, reader-specific ranking heuristic, not a probability, and is not comparable across formats. In a mixed-format image, best is therefore not necessarily the most reliable read; select by format or payload when your application knows what it expects. Checksums reduce wrong reads but cannot rule them out.

payloadBytes is supplied by QR Code, Micro QR, Data Matrix, Aztec, PDF417 and MaxiCode, and by the Code 39 variants (Code32, PZN, Code39Extended) as the raw Code 39 text. It holds decoded data bytes before character-set interpretation, not raw codewords; Aztec Rune gives its value as decimal ASCII. Encoding text as UTF-8 does not reconstruct these bytes. Use Uint8Array.from(barcode.payloadBytes) for a byte buffer.

All types (ScanResult, InspectionResult, Barcode, ScannerOptions, ScanOptions, Format and others) are exported from tapirscan and every tapirscan/browser entry. Each entry's ScannerOptions lists the options it accepts.

Structured content

A barcode whose text has a recognized structure (GS1, Wi-Fi, contact, payment and similar payloads) carries its named fields as content.

const { content } = result.barcodes[0];
if (content?.kind === "WiFi") {
  const ssid = content.fields.find((f) => f.name === "ssid")?.value;
}

Kinds and fields: structured content.

EAN/UPC supplements

Set eanAddOnPolicy when creating a scanner or calling one-shot scan or inspect.

| Policy | Behavior | | ----------- | ------------------------------------------------------------------------------------------------ | | "ignore" | Decode the main barcode without reading its supplement. | | "read" | Try reading the two- or five-digit supplement; keep the main barcode if none is readable. | | "require" | Return an EAN/UPC barcode only when its supplement is readable. Other formats remain unaffected. |

The supplement appears in barcode.eanAddOn; barcode.text is the main payload, and polygon and rect describe the main barcode. Reading supplements adds decoding work. Retail reads rejected by "require" appear in result.undecoded.

Undecoded regions

inspect results list undecoded regions: localized proposals without an accepted decode, each with a source-image polygon and a format hint. They can overlap or be false candidates, and an empty list does not prove that every barcode was found.

Diagnostics

const report = scanner.inspect(image);
console.log(report.diagnostics.regions.proposals, report.diagnostics.regions.searchWindows);
console.log(report.diagnostics.scan.barcodes);

diagnostics.regions has a stable shape: proposals and searchWindows are null when a reader does not expose them, and empty arrays when it found nothing. The other fields are the engine's own evidence; they vary by reader and may change between releases. Candidate indices inside recovery crops are local to the crop, not identifiers for tracking between frames.

Errors

Invalid options throw TypeError. Engine and validation failures throw the exported ScannerError with .message and a .code:

| Code | Meaning | | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | "invalid_input" | The engine rejected the image or options. | | "disposed" | The scanner was disposed. | | "engine" | Internal scanner failure. | | "capacity" | Too many live scanners in this WASM instance. | | "unsupported" | No WebAssembly SIMD (or, in browsers, module workers, OffscreenCanvas or createImageBitmap). | | "abi_shape", "abi_version", "abi_mode", "abi_turbo" | The loaded WASM file does not match this package or mode. | | "invalid_output" | The engine returned unreadable output. | | "core_<n>" | An unexpected engine status n. |

Loader and fetch errors reject Scanner.create and the one-shot helpers. Dispose reusable scanners in finally.

License

Tapirscan is dual-licensed under MIT OR Apache-2.0, at your option. See the full license texts. Third-party components retain their own licenses and notices.