Python
Scan images and webcams, generate barcodes, parse GS1 and driver licenses, and run the REST API with the qrgen-sdk Python package.
Note: This page is generated from
packages/python/README.md. Source code:packages/python.
Open-source (Apache-2.0) barcode, QR code and ID scanning for Python. Decode 26 symbologies from images and webcams, generate SVG/PNG barcodes, parse GS1, AAMVA driver licenses and QR payloads (Wi-Fi, vCard, payments…), and run a REST API with the standard library only.
It is the Python member of the QRGen family. Symbology ids, the result JSON, parsers and the REST API follow the shared specification, so results are interchangeable with the JavaScript, iOS, Android, Flutter, React Native and .NET packages. Decoding and encoding use zxing-cpp.
Install
pip install qrgen-sdk # scan, generate, parse, REST server
pip install "qrgen-sdk[camera]" # + webcam / Raspberry Pi camera (OpenCV, numpy)
Until the package is published on PyPI, install it from GitHub:
pip install "git+https://github.com/Rockyljewell/QR-GEN.git#subdirectory=packages/python"
pip install "qrgen-sdk[camera] @ git+https://github.com/Rockyljewell/QR-GEN.git#subdirectory=packages/python"
Requirements: Python 3.9+, zxing-cpp>=3.1 and Pillow (installed automatically).
zxing-cpp 3.1 ships wheels for Python 3.10+ on Linux x86-64/aarch64 (glibc 2.28+), macOS and
Windows; on Python 3.9 or other platforms pip builds it from source, which needs CMake and a
C++17 compiler.
Quick start
import qrgen_sdk
# Generate
svg = qrgen_sdk.generate("https://example.com") # SVG string
png = qrgen_sdk.generate("https://example.com", format="png", scale=8) # PNG bytes
qrgen_sdk.save("(01)09501101530003(17)250101(10)ABC123", "label.png",
symbology="code128", gs1=True, hrt=True)
# Scan: a path, bytes, a file object, a PIL image or a numpy (OpenCV) array
for code in qrgen_sdk.scan("label.png", symbologies=["qr", "retail", "code128"]):
print(code.symbology, code.data) # code128 (01)09501101530003(17)250101(10)ABC123
print(code.to_dict()) # SPEC section 2 JSON (camelCase)
print(qrgen_sdk.parse_content(code.data)) # {"type": "gs1", "gs1": {...}}
Scanning options
qrgen_sdk.scan(image, symbologies=None, try_harder=True, max_results=None) -> list[Barcode]
qrgen_sdk.scan_file(path, **same_options)
symbologies: ids, aliases or groups ("qr","QRCode","ean-13","retail","2d","qr,ean13"…).Nonemeansall.try_harder: also search rotated, downscaled and inverted variants and retry with a second binarizer. UseFalsefor speed.- GS1 data comes back in HRI form (
"(01)...(10)...") withis_gs1=True. - UPC-A is reported as 12 digits and UPC-E as 8 digits; books report as
ean13unless you ask only forisbn.
Barcode fields: data, symbology, symbology_name, raw_bytes, content_type,
is_gs1, location (Quad of Points), frame_size, orientation, ec_level,
symbology_identifier, timestamp. Barcode.to_dict() returns the camelCase JSON
shape shared by every QRGen platform; Barcode.from_dict() reads it back.
Generating options
qrgen_sdk.generate(data, symbology="qr", format="svg", scale=4, ec_level=None,
gs1=False, hrt=False, margin=True, foreground=None, background=None)
qrgen_sdk.save(data, "out.svg", **options) # format from the extension
- Writable: qr, micro-qr, rmqr, data-matrix, aztec, pdf417, micro-pdf417, maxicode, ean13, ean8, upca, upce, isbn, code128, code39, code93, codabar, itf, itf14, databar, databar-expanded, databar-limited, code32, pzn, telepen, dx-film-edge.
format:svg(returnsstr) orpng(returnsbytes);jpeg,webp,bmp,gifandtiffwork too.ec_level:L/M/Q/Hfor QR codes;0-8or a percentage for PDF417/Aztec.foreground/background:#rrggbb,#rrggbbaaor"transparent".datamay bebytesfor binary 2D codes.
Invalid input raises qrgen_sdk.GenerateError (a ValueError).
Parsers
Pure Python, never raise, and return dicts with the SPEC’s camelCase keys so
json.dumps() matches the other platforms.
from qrgen_sdk import parse_content, parse_gs1, parse_aamva
parse_content("WIFI:T:WPA;S:Home;P:secret;;")
# {'type': 'wifi', 'ssid': 'Home', 'password': 'secret', 'security': 'WPA', 'hidden': False}
parse_gs1("(01)09501101530003(17)250101(10)ABC123")["values"]
# {'01': '09501101530003', '17': '250101', '10': 'ABC123'}
parse_gs1("https://id.gs1.org/01/09501101530003/10/ABC123?17=250101") # Digital Link
parse_gs1("]C1" "0109501101530003" "10ABC123\x1d" "3103000195") # raw + GS
# element 3103 -> {"ai": "3103", "title": "NET WEIGHT (kg)", "value": "000195", "number": 0.195}
license = parse_aamva(pdf417_text) # North American DL/ID cards
license["fullName"], license["dateOfBirth"], license["age"], license["isUnder21"]
parse_contenttypes:url,gs1-digital-link,email(mailto:, MATMSG, SMTP),phone,sms,wifi(with backslash escapes),geo,contact(vCard, MECARD),event(VEVENT),payment(EPC/SEPABCD,bitcoin:,ethereum:,upi://pay,payto://and other coins asother),product(GTIN with check digit),gs1,aamvaandtext. Passsymbology=as a hint (for example to read 8 digits as UPC-E).parse_gs1knows 750+ AIs (counting decimal variants) (00-03, 10-17, 20-22, 235, 240-243, 250-255, 30, 310n-369n, 37, 390n-395n, 400-403, 410-417, 420-427, 4300-4333, 7001-7259, 8001-8200, 90-99) with fixed/variable lengths, dates as ISOdate(day00= last day of the month) and decimal AIs asnumber.parse_aamva(data, today=date(...))handles AAMVA versions 1-10, legacyDAA/DCTnames, US (MMDDCCYY) vs Canadian (CCYYMMDD) dates, ZIP+4, and computesage,isExpiredandisUnder21against an injectabletoday.
Command line
The console script is qrgen-py (so it does not clash with the Node qrgen CLI);
qrgen-sdk and python -m qrgen_sdk are aliases.
qrgen-py scan photo.jpg shelf.png --symbologies qr,ean13 # "symbology<TAB>data"
qrgen-py scan photo.jpg --json --parse # SPEC JSON + parsed content
qrgen-py generate "https://example.com" -o qr.svg
qrgen-py generate "(01)09501101530003(10)ABC" --symbology code128 --gs1 --hrt -o label.png
qrgen-py generate "hello" --format png --scale 8 > hello.png
qrgen-py parse "WIFI:T:WPA;S:Home;P:secret;;"
qrgen-py parse --unescape '@\n\x1e\rANSI 636014100002DL...' # AAMVA with escapes
qrgen-py camera --mode batch --symbologies retail
qrgen-py serve --port 8080
qrgen-py symbologies
Exit codes: 0 success, 1 nothing found / not parseable, 2 error.
Webcam scanning
from qrgen_sdk.camera import Camera, scan_camera
# One-liner: preview window, highlight quads, prints every new code
scan_camera(on_scan=lambda codes: print([c.data for c in codes]))
# Full control
with Camera(device=0, symbologies=["qr", "ean13"], mode="continuous",
duplicate_filter=1000, width=1280, height=720) as cam:
for frame, result in cam.frames():
for code in result.new: # passed the duplicate filter
print(code.symbology, code.data)
mode:single(stop after the first scan),continuous, orbatch(tracks many codes with stable ids;result.trackedholdsTrackedBarcodes withid,first_seen,last_seen,count, dropped after 500 ms unseen).duplicate_filter: ms before the same symbology+data is reported again;0reports every frame,-1once per session.scan_area=(x, y, w, h)(0..1) restricts decoding to a region of interest.Camera.process(frame)works on any BGR/grayscale numpy frame, so you can plug in picamera2, GStreamer, RTSP streams or video files.DuplicateFilterandBatchTracker(inqrgen_sdk.tracking) are plain Python classes with an injectable clock.- With omitted
now_ms, duplicate/tracking windows use monotonic elapsed time; barcode and track timestamps remain milliseconds since the Unix epoch. Explicitnow_mssupplies both timeouts and timestamps. Mixing default and injected calls rebases retained history using timestamp differences at each transition, preserving the previous behavior; use a consistent timestamp basis when mixing clocks.
Optional spatial observations
Batch tracking aggregates equal symbology/payload values by default. To keep separate observations of identical codes at different positions, opt in explicitly:
with Camera(mode="batch", spatial_tracking=True, track_max_jump=3.0) as cam:
for frame, result in cam.frames():
for observed in result.tracked:
print(observed.id, observed.data, observed.count)
For external frame sources, use BatchTracker(timeout_ms=500, spatial=True, max_jump=3.0). Each record must have finite corner coordinates, a nonzero extent,
and a consistent positive frame_size. Narrow linear-code quads are supported.
Invalid records or a negative/nonfinite movement gate raise ValueError; a rejected
update leaves tracker state and clock history unchanged. The options are constructor
policy; spatial, max_jump and max_association_work are read-only.
Within each exact symbology/payload group, spatial tracking maximizes one-to-one
matches inside the movement gate, then minimizes total center movement. The gate
is max(detection width, height, 1) * max_jump source pixels. IDs are opaque strings,
fresh after expiry, coordinate resets and clear() within that tracker object.
An observed track’s count is the number of frames matched to it.
Supply tracker.update(codes, frame_size=Size(width, height)) when a frame’s size is
known, including blank frames. Records must carry the same positive size; the
argument does not fill missing record metadata. A size change removes old spatial
tracks before matching. Camera.process supplies full-frame size automatically and
keeps ROI coordinates in that space. Successful camera opens and closes reset only
spatial history; removed tracks are reported once in the next successful
FrameResult, even if intervening frame processing fails. Idempotent or failed
opens do not reset it. When directly processing different same-size sources, call
tracker.clear() yourself. A gap without frames preserves tracks until their normal
timeout is checked on the next update.
result.new and on_scan remain payload duplicate-filter notifications; use
tracked, added or on_track for observed tracks. Resets do not clear payload
report history. Spatial IDs do not establish persistent physical identity through
crossings, camera motion, occlusion or leaving view. Decoder duplicates/misses can
also affect observed counts. These tracks and frame counts are not confirmed
receiving quantities. Dense repeated payload groups require more assignment work;
max_results limits each decoded frame, not the number of retained unseen tracks.
The default max_association_work=250_000 conservatively sums n*n*(m+n) over
each exact symbology/payload group with n detections and m > 0 surviving prior
tracks. It includes prior columns outside the movement gate. New-only, expired and
coordinate-reset groups do not spend assignment units. A frame over this budget
raises ValueError before allocating matching matrices or changing clocks, tracks,
events, queued source removals or Camera duplicate-report history. There is no
truncation or fallback matching policy. The limit must be a positive integer and is
an assignment-work bound, not a whole-frame time guarantee. For large offline
workloads, explicitly raise BatchTracker(max_association_work=...) or
Camera(track_max_association_work=...); measure performance for your hardware.
- The
cameraextra installsopencv-python-headless, which has no GUI. For a preview window installopencv-pythoninstead; without GUI support scanning continues and a warning is printed. Pressqor Esc to quit the window.
REST server
A dependency-free implementation of SPEC section 6 (http.server.ThreadingHTTPServer,
CORS enabled, JSON errors).
qrgen-py serve --port 8080 # localhost only
qrgen-py serve --host 0.0.0.0 --port 8080 # reachable from other machines
curl -s localhost:8080/health
curl -s -X POST --data-binary @photo.png -H "Content-Type: image/png" "localhost:8080/v1/scan?symbologies=qr,ean13"
curl -s -X POST -H "Content-Type: application/json" \
-d '{"image":"data:image/png;base64,iVBOR...","symbologies":["qr"],"tryHarder":true}' localhost:8080/v1/scan
curl -s "localhost:8080/v1/generate?data=hello&format=png&scale=8" -o hello.png
curl -s -X POST -H "Content-Type: application/json" -d '{"data":"hello","symbology":"qr","format":"svg"}' localhost:8080/v1/generate
curl -s -X POST -H "Content-Type: application/json" -d '{"data":"WIFI:T:WPA;S:Home;P:x;;"}' localhost:8080/v1/parse
| Method | Path | Notes |
|---|---|---|
| GET | /health |
{"ok": true, "version": "1.0.0"} |
| GET | /v1/symbologies |
{"read": [...], "write": [...]} |
| POST | /v1/scan |
raw image bytes or JSON {image, symbologies, tryHarder, maxResults}; ?symbologies= |
| GET/POST | /v1/generate |
data, symbology, format, scale, ecLevel, gs1, hrt, margin, foreground, background |
| POST | /v1/parse |
JSON {data} (or a text body); type: "gs1"/"aamva" runs a specific parser |
Errors look like {"error": {"code": "bad-request", "message": "..."}}. Embed it with
qrgen_sdk.server.create_server(host, port) or start_in_thread(). The server has no
authentication; put it behind a reverse proxy before exposing it publicly.
Linux and Raspberry Pi
- Wheels: zxing-cpp publishes manylinux wheels (glibc 2.28+) for x86-64 and aarch64 on
Python 3.10+, so
pip install qrgen-sdkworks on 64-bit Raspberry Pi OS (Bookworm) and Ubuntu without a compiler. On 32-bit (armv7) systems or Python 3.9, pip builds zxing-cpp from source:sudo apt install cmake g++ python3-devfirst (it takes a while on a Pi), or use a 64-bit OS. - Virtual environments: Raspberry Pi OS and recent Debian/Ubuntu block system-wide
pip installs. Use
python3 -m venv ~/qrgen && ~/qrgen/bin/pip install "qrgen-sdk[camera]". To reuse the distro’s OpenCV and picamera2, create the venv with--system-site-packagesandsudo apt install python3-opencv python3-picamera2. - USB webcams use V4L2:
qrgen-py camera --device 0(or/dev/video0). Add your user to thevideogroup if you getcamera-in-use/permission errors (sudo usermod -aG video $USER, then log in again).v4l2-ctl --list-deviceslists cameras. - Raspberry Pi Camera Module (libcamera): run
qrgen-py camera --device picamera2(uses picamera2), or wrap OpenCV withlibcamerify qrgen-py camera. - Headless / SSH: use
--no-window(orshow_window=False) and--jsonto stream results;qrgen-py camera --no-window --json | jq .data. - Performance: on a Pi 4/5 use 640x480 or 1280x720 capture (
--width 1280 --height 720), narrow--symbologies, and set a--scan-area. Camera frames use a fast decoder configuration by default (try_harder=False). - Run as a service:
qrgen-py serve --host 0.0.0.0under systemd gives every device on the network a barcode API.
Status
Version 1.0.0.
- Tested (pytest, 220 tests, Linux x86-64, Python 3.11, zxing-cpp 3.1.1): symbology ids/aliases/groups and zxing-cpp mapping; generate -> scan round trips through real PNGs for 26 symbologies (including GS1 Code 128, QR and Data Matrix); the SPEC GS1 and AAMVA vectors plus many parser cases; duplicate filter and batch tracker; camera frame processing on synthetic frames and a generated video file; the REST server on an ephemeral port; the CLI.
- Not tested here: physical webcams, Raspberry Pi hardware and picamera2, the OpenCV preview window, Python versions other than 3.11 (3.9+ is supported by design), Windows and macOS.
- Not yet on PyPI; install from GitHub as shown above.
License
Apache-2.0. See LICENSE and NOTICE.
Created by Rockyljewell.
Reversed-reflectance decoding
The default scan(..., try_harder=True) also finds reversed-reflectance
(light-on-dark) codes. zxing-cpp searches QR, Micro QR, rMQR, Data Matrix and Aztec
in both polarities within the normal pass. PDF417, MaxiCode and linear codes need
an explicit pass over an inverted copy, and it runs only when the normal pass found
nothing. An image holding only inverted codes is decoded in full, up to
max_results, and identical payloads at separate positions remain separate
detections. A mixed-polarity image (a normal code next to an inverted linear code)
returns the normal code only; crop and scan the inverted region separately if you
need both. The alternate-binarizer retry also applies to the inverted pass. Set
try_harder=False, or low-level decode(..., try_invert=False), to skip inversion.
Caller PIL images and NumPy arrays are not modified.