Skip to main content

Tapirscan for Python

scan(image) returns every decoded barcode: result.values gives the decoded strings and result.barcodes pairs each one with its format and source-image polygon / rect. Both are empty when nothing is decoded.

Try the live demo · Quick start · Functions · All options · Results

import tapirscan

result = tapirscan.scan(image)
print(result.values)
for barcode in result.barcodes:
    print(barcode.text, barcode.format, barcode.rect)

image can be a Pillow image, a NumPy array or a PyTorch tensor. Use inspect instead of scan for timing, unread regions and diagnostics.

Installation

Python 3.10+ is required. Platform wheels bundle the native library with all four effort modes:

pip install tapirscan

Building wheels from source requires a full checkout and a matching native library with its native-build.json sidecar. See development for build commands and using a prebuilt library through TAPIRSCAN_NATIVE_DIR.

Switching from pyzbar or zxing-cpp: see the migration guides.

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.

Quick start

Tapirscan has no image-library dependencies; install the ones your inputs need.

TIFF with tifffile

pip install tapirscan tifffile
import tifffile
import tapirscan

pixels = tifffile.imread("label.tif", key=0)  # NumPy array: first TIFF page
result = tapirscan.scan(pixels)
print(result.values)

This assumes an 8-bit grayscale or RGB image. Defaults are Medium effort and the retail formats (EAN13, UPCA, EAN8, UPCE).

JPEG with Pillow

pip install tapirscan pillow
from PIL import Image
import tapirscan

with Image.open("label.jpg") as image:
    result = tapirscan.scan(image, mode="high", formats="1D")

for barcode in result.barcodes:
    print(barcode.text, barcode.format, barcode.rect)

formats="1D" enables linear formats. "retail" selects EAN13, UPCA, EAN8 and UPCE; "common1D" adds Code128, Code39 and ITF; "common" adds QRCode and DataMatrix; "2D" and "all" are also available. See format and keyword-group matrix.

PyTorch tensors

pip install tapirscan torch
import torch

tensor = torch.from_numpy(pixels)  # the array from the TIFF example
result = tapirscan.scan(tensor)
print(result.values)

GPU tensors and tensors with requires_grad=True work directly: Tapirscan copies the pixels to CPU without changing your tensor or its autograd graph. Decoding runs on CPU. See array and tensor inputs.

Fast scanning

  • Select only the formats you need. Each enabled 2D format searches the whole image; formats="all" is for exploration.
  • Reuse a Scanner for more than one image, and use mode="low" for camera frames.
  • Inputs, cheapest first: contiguous uint8 NumPy arrays (RGBA, then RGB or gray, a small extra copy for large frames); uint8 tensors; float arrays and tensors (validated and rounded, on several threads for large arrays); Pillow images (one tobytes() copy). CUDA tensors are converted on the GPU and transferred once, so scan tensors where they already are; moving a CPU tensor to the GPU only for scanning does not pay off for uint8 and saves little for large float tensors. Do not convert RGBA to gray for scanning; for OpenCV, see below.
  • Large photos: when the barcodes are large in the frame, let Pillow decode a JPEG at reduced size before it loads: image = Image.open(path), then image.draft("RGB", (image.width // 2, image.height // 2)). A 12 MP photo with formats="all" then scans several times faster. Check that your smallest codes still read, and multiply coordinates by the factor to map them back to the original.
  • Batches: tapirscan.scan_many(images, ...) scans on all CPU cores with one scanner per thread, reading a generator only a few images ahead. It already keeps every core busy, so its scanners run each scan on one thread (parallel=False).
  • Threads and servers: scanning releases the GIL, so one scanner per thread scales with CPU cores. Create one per worker thread, for example with threading.local() or a ThreadPoolExecutor initializer. In FastAPI and other async frameworks, scan in a sync endpoint or thread pool so the event loop keeps running. With multiprocessing, create the scanner inside each process. Scans with several formats also use extra threads internally; pass parallel=False when you already run one scanner per core.
import threading
from concurrent.futures import ThreadPoolExecutor

import tapirscan

local = threading.local()


def scan(image):
    if not hasattr(local, "scanner"):
        local.scanner = tapirscan.Scanner(formats=["EAN13", "QRCode"])
    return local.scanner.scan(image).values


with ThreadPoolExecutor() as pool:
    results = list(pool.map(scan, images))

In asyncio code, scan in a worker thread so the event loop keeps running:

result = await asyncio.to_thread(scanner.scan, image)

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

Functions

# One image, with automatic cleanup:
result = tapirscan.scan(image, mode="medium", formats=["EAN13"])

# Reuse native initialization across images:
with tapirscan.Scanner(mode="high", formats="1D") as scanner:
    result = scanner.scan(image)
    print(result.values)

# Many images on all CPU cores, results in input order:
results = tapirscan.scan_many(images, formats=["EAN13", "QRCode"])

Signatures (all settings are optional). inspect and scanner.inspect take the same arguments as their scan counterparts and return InspectionResult:

scan(image, *, mode="medium", formats=None, ean_add_on_policy="ignore", parallel=None,
     layout="auto", value_range="auto", color_order="RGB", library_dir=None) -> ScanResult
Scanner(mode="medium", *, formats=None, ean_add_on_policy="ignore", parallel=None,
        library_dir=None)
scanner.scan(image, *, formats=None,
             layout="auto", value_range="auto", color_order="RGB") -> ScanResult
scanner.close()
scan_many(images, *, mode="medium", formats=None, ean_add_on_policy="ignore",
          parallel=False, layout="auto", value_range="auto", color_order="RGB",
          library_dir=None, workers=None) -> list[ScanResult]

scan_many scans on workers threads (default: the CPU count), each with its own scanner, and returns results in input order. If an image fails, its error is raised after the other scans finish.

Use the context manager or call close() in a finally block; repeated close() is safe and results stay valid afterwards. Calls on one scanner run one at a time; separate scanners can run concurrently (threads and servers). scanner.mode and scanner.formats are read-only; create another scanner to change effort. Per-call formats apply to that call only; None uses the scanner's selection. A scanner copies each image into a pixel buffer it reuses, as large as the largest image so far, until close().

All options

Option Where Default Meaning
mode Creation / one-shot "medium" "low", "medium", "high", "very-high".
formats Creation / scan retail An identifier, "retail", "common1D", "common", "1D", "2D", "all", or a nonempty list/tuple of identifiers.
ean_add_on_policy Creation / one-shot "ignore" "ignore", "read", "require"; see supplements.
parallel Creation / one-shot None Run one scan's formats on separate threads; results are identical. None is on unless TAPIRSCAN_THREADS is 0 or 1. scan_many defaults to False.
layout Scan, arrays/tensors only "auto" "HW", "HWC", "CHW"; specify when the channel position is ambiguous.
value_range Scan, arrays/tensors only "auto" "0_1" or "0_255". Auto uses [0,1] for floats and [0,255] for integers, independent of image contents.
color_order Scan, arrays/tensors only "RGB" "BGR" for OpenCV BGR/BGRA pixels. Grayscale is unaffected.
library_dir Creation / one-shot Bundled library Directory of a custom native build. Lookup order: this argument, TAPIRSCAN_LIBRARY_DIR, then the wheel's library.

Effort modes tune every format; see effort levels. Format group exports: retail_formats, common_linear_formats, common_formats, linear_formats, matrix_formats. Resizing, cropping, rotation and camera capture are up to the caller.

Image input

Array and tensor inputs

One HW/HWC/CHW image, optionally with a leading batch axis of size one, with 1, 3 or 4 channels. NumPy views and noncontiguous tensors are accepted. Colors default to RGB/RGBA. Booleans become black/white. NaN, infinity and out-of-range values are rejected rather than clipped: scale higher-bit-depth intensities and undo mean/std normalization first. Floats stored in byte units need value_range="0_255".

Scanning always runs on the CPU. Large float NumPy arrays are converted on several threads. CUDA tensors are validated and converted to bytes on the GPU, so only the final 8-bit pixels are transferred; MPS and other device tensors are copied to CPU first, which adds latency. Sparse, quantized, complex and meta tensors are rejected.

OpenCV

import cv2
import tapirscan

image = cv2.imread("label.jpg")
if image is None:
    raise ValueError("Could not load label.jpg")
result = tapirscan.scan(image, color_order="BGR")

OpenCV is not a dependency. color_order="BGR" does not modify your array; it reorders the channels in a copy. Converting with OpenCV is faster: for a 12 MP frame, scanner.scan(cv2.cvtColor(image, cv2.COLOR_BGR2RGBA)) is about twice as fast as color_order="BGR". For a camera or a video file (cv2.VideoCapture("clip.mp4")), reuse one scanner:

capture = cv2.VideoCapture(0)
with tapirscan.Scanner(mode="low", formats=["EAN13", "QRCode"]) as scanner:
    while True:
        ok, frame = capture.read()
        if not ok:
            break
        rgba = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA)
        for barcode in scanner.scan(rgba).barcodes:
            print(barcode.text, barcode.rect)

Pillow

Pillow I, F and I;16* images are read as intensities in [0,255], matching Pillow's usual conversion convention; scale higher-bit-depth images explicitly. layout, value_range and color_order do not apply to Pillow images.

Raw pixels

image = tapirscan.PixelImage(pixels, width=640, height=480, channels=3)
result = tapirscan.scan(image)

pixels contains decoded bytes, not a JPEG/PNG file.

PixelImage field Default Meaning
data Required, positional bytes, bytearray, or a contiguous byte memoryview.
width, height Required, keyword-only Integer dimensions, at least 3 pixels each.
channels 1 1 grayscale, 3 RGB, 4 RGBA. Alpha is ignored.
stride width * channels Bytes between row starts. Larger values allow padding.

Storage must cover (height - 1) * stride + width * channels bytes.

Limits

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. Images may have at most 65,535 pixels per side, 256 megapixels and 1 GiB of pixel data; this is checked before any conversion or device transfer. Coordinates start at the top left. If you resize before scanning, map coordinates back when drawing on the original.

Results

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

Field/method Meaning
result.values List of decoded strings, one per barcode.
result.barcodes Tuple of immutable Barcode objects.
result.best Highest-support barcode (first on ties), or None.
result.as_dict() JSON-compatible dictionary of the public fields, without diagnostics.
result.image inspect: ImageSize(width, height) of the supplied pixels.
result.mode inspect: effort mode used.
result.elapsed_ms inspect: native scan time, excluding image conversion.
result.undecoded inspect: regions without a decode.
result.diagnostics inspect: engine evidence.
barcode.text, .format Decoded text and format identifier.
barcode.polygon Tuple of four Point(x, y) source-image coordinates.
barcode.rect Enclosing integer Rect(left, top, width, height).
barcode.support Reader-specific ranking evidence used by best.
barcode.payload_bytes Decoded payload bytes, when available; otherwise None.
barcode.gs1 GS1 indicator, or None if not supplied by the reader.
barcode.reader_initialization Data Matrix, PDF417, Aztec and MaxiCode reader-initialization flag; None for other formats.
barcode.structured_append StructuredAppend(index, count, id, parity), or None; index is one-based.
barcode.ean_add_on EAN/UPC supplement, with ean_add_on_policy "read" or "require".
barcode.codabar_start_stop Codabar start and stop characters, such as "AB", which text omits; None otherwise.
barcode.content Structured content of text, or None for plain text.

Separate labels with the same value stay separate entries. barcode.as_dict() exports a single read, and json.dumps(result.as_dict()) works directly. Each exported barcode has a content entry, {"kind", "fields": [{"name", "value"}]} or None for plain text. Results pickle and export without the native library.

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.

payload_bytes 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.

Exported types include ScanResult, InspectionResult, Barcode, Format, FormatSelection, Mode, Layout, ValueRange, ColorOrder, ImageInput and PixelImage. The package includes py.typed.

Structured content

barcode.content holds the named fields of GS1, Wi-Fi, contact, payment and similar payloads, or None for plain text. Scanning parses it, a few microseconds per decoded barcode. tapirscan.parse_content(text) parses any str, and gs1=True reads it as a GS1 element string.

content = barcode.content  # or tapirscan.parse_content(text, gs1=True)
if content and content.kind == "GS1":
    gtin, expiry = content.get("01"), content.get("17")

Kinds and fields: structured content.

EAN/UPC supplements

Set ean_add_on_policy 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.ean_add_on; 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 ("Unknown" when unavailable). They can overlap or be false candidates, and an empty list does not prove that every barcode was found.

Diagnostics

report = tapirscan.inspect(image)
regions = report.diagnostics.regions
if regions is not None:
    print(regions.proposals, regions.search_windows, regions.candidates)

proposals and search_windows are None when a reader does not expose them, and empty tuples when it found nothing. report.diagnostics.barcodes lists per-read evidence; region types are in tapirscan.results. report.diagnostics.to_raw_dict() returns the engine's own JSON evidence, whose fields vary by reader and may change between releases. Candidate indices inside a recovery crop are local to that crop, not identifiers for tracking between frames.

Errors

Invalid input raises ValueError or TypeError. Native failures raise ScannerError with a message, the native .status number and a .code shared with JavaScript: "invalid_input", "disposed" or "engine". Scanning after close() raises RuntimeError. A custom native library from library_dir must implement native ABI 7; mismatches are reported with rebuild instructions.

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.

Metadata

Release files for tapirscan 1.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for tapirscan 1.5.0
File
tapirscan-1.5.0-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
tapirscan-1.5.0-py3-none-manylinux_2_28_x86_64.whl Python 3 none Linux glibc 2.28+ x86-64 Details
tapirscan-1.5.0-py3-none-manylinux_2_28_aarch64.whl Python 3 none Linux glibc 2.28+ ARM64 Details
tapirscan-1.5.0-py3-none-macosx_11_0_x86_64.whl Python 3 none macOS 11.0+ x86-64 Details
tapirscan-1.5.0-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details

Total release size: 14.8 MB

Release files / tapirscan-1.5.0-py3-none-win_amd64.whl

Download URL tapirscan-1.5.0-py3-none-win_amd64.whl
Size 3.0 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
7e76e2142478c9ad94a9dfc8e7a5580996fa3f174d569e6addcc41e94e6651c6
BLAKE2b-256 checksum
How to use checksums
b97a25ef2ec8858abfaeeda3682c2f3902e90008cafc0f65e2d50276ce2a3940
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / tapirscan-1.5.0-py3-none-manylinux_2_28_x86_64.whl

Download URL tapirscan-1.5.0-py3-none-manylinux_2_28_x86_64.whl
Size 3.2 MB
Tags Linux glibc 2.28+ x86-64 Python 3
SHA-256 checksum
How to use checksums
ea11751223674bedb6d42b6f4a1ddd4f053c27a7ba8fdce12de4f88e2e795f5a
BLAKE2b-256 checksum
How to use checksums
58d6382c5e758f33f16c9d1df8761e60160631e9c2d973064c7e518fe632f11a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / tapirscan-1.5.0-py3-none-manylinux_2_28_aarch64.whl

Download URL tapirscan-1.5.0-py3-none-manylinux_2_28_aarch64.whl
Size 2.9 MB
Tags Linux glibc 2.28+ ARM64 Python 3
SHA-256 checksum
How to use checksums
5b0404d5fef4a1ced8fdab5edf9b03c0efa358f6313798d4e99cd736dad375c7
BLAKE2b-256 checksum
How to use checksums
4ca3f63c0f2f941034ac2966dac106f57cf6213c49c212cef42868f88aefbe1a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / tapirscan-1.5.0-py3-none-macosx_11_0_x86_64.whl

Download URL tapirscan-1.5.0-py3-none-macosx_11_0_x86_64.whl
Size 3.0 MB
Tags Python 3 macOS 11.0+ x86-64
SHA-256 checksum
How to use checksums
e3d5dcc1beccd7d804bdde7f738e7cf25b67f6429330be51bb0e473e73125968
BLAKE2b-256 checksum
How to use checksums
ab4308e38ffbaa3bfc705c24140ef0a975345d21d111ebbd1353936466b01e72
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / tapirscan-1.5.0-py3-none-macosx_11_0_arm64.whl

Download URL tapirscan-1.5.0-py3-none-macosx_11_0_arm64.whl
Size 2.7 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
bf54f4df1556d094fc95a7981003b2df262431c0b88d93bdef63b2a849a67a07
BLAKE2b-256 checksum
How to use checksums
46726561ecdfa6585b4a7ce5588598a2c21986a1b2b9dae77d6029d428f844b2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Security for tapirscan 1.5.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 5 files in this release have been uploaded with provenance metadata.

Select from 5 files

Attestation for tapirscan-1.5.0-py3-none-win_amd64.whl

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page