Generating a PDF417 barcode takes four lines of Python. Reading one back from a phone photo, a 200 DPI scan, or a boarding pass that spent a day in someone’s pocket is where the real work starts, and it is where most PDF417 integrations actually fail. In this article, you will learn how to read PDF417 barcodes in Python when the source image is imperfect. If you are looking for the generation side instead, the earlier walkthrough on how to generate PDF417 barcodes using Python covers creating basic and custom PDF417 symbols, including row and column control, and the Python barcode reader guide covers general-purpose scanning across symbologies. What follows here goes narrow and deep on PDF417 recognition.
Every snippet below was run against Aspose.BarCode for Python via .NET 26.7 on Linux with Python 3.12. The timings and read/miss results are measured, not estimated, and the script that produced them is at the end so you can rerun it on your own images.
What Actually Breaks PDF417 Recognition
PDF417 is a stacked symbology: rows of codewords, each codeword built from four bars and four spaces spanning 17 modules. That structure carries Reed-Solomon error correction, so a torn corner or a coffee stain is usually survivable. What is not survivable is losing the module grid itself.
Three things destroy the grid, in rough order of how often they show up in production:
- Effective module width falling below two pixels. This is the dominant failure. It happens through downscaling, through camera distance, and through blur, which spreads one module’s ink across its neighbours. A barcode generated at three pixels per module and then blurred is functionally a barcode at one pixel per module.
- Perspective and rotation. A barcode photographed at an angle has rows that are no longer parallel scan lines. The detector handles this, but it costs a detection pass that the fastest preset skips.
- Contrast collapse. Thermal paper that has faded, a screen photographed under glare, or a scan with the brightness pinned. Less fatal than it looks, as the numbers below show.
The practical consequence is that recognition settings are a recovery mechanism, not a substitute for capture quality. They buy you back the middle band of image quality. They do not buy back the bottom.
Set Up the Python PDF417 Reader
Install the library from PyPI:
pip install aspose-barcode-for-python-via-net
The recognition API lives in aspose.barcode.barcoderecognition. Two classes matter for this article: BarCodeReader, which drives detection and decoding, and QualitySettings, which controls how hard the reader tries.
Read a PDF417 Barcode in Python
The minimum viable read is two arguments and a loop:
from aspose.barcode import barcoderecognition as rec
reader = rec.BarCodeReader("clean.png", rec.DecodeType.PDF417)
for result in reader.read_bar_codes():
print("Code text:", result.code_text)
print("Symbology:", result.code_type_name)
print("Reading quality:", result.reading_quality)

Read a PDF417 Barcode in Python.
Output on a clean image:
Code text: PASSENGER|KHAN/M|SEAT14C|LHE-DXB
Symbology: Pdf417
Reading quality: 100.0
The second argument is not optional in practice. Leave it out and BarCodeReader falls back to DecodeType.ALL_SUPPORTED_TYPES, which runs detection for more than sixty symbologies against every candidate region in the image. On a PDF417-only pipeline that is pure waste, and it opens the door to a linear symbology false-positive inside the PDF417 row structure. Name the type.
If your input arrives as bytes from an upload rather than a file on disk, pass a stream and set a timeout so a pathological image cannot stall a worker:
import io
from aspose.barcode import barcoderecognition as rec
def decode_pdf417(image_bytes, timeout_ms=2000):
reader = rec.BarCodeReader(io.BytesIO(image_bytes), rec.DecodeType.PDF417)
reader.timeout = timeout_ms
return [r.code_text for r in reader.read_bar_codes()]
Compare PDF417 Recognition Quality Settings in Python
QualitySettings ships four presets worth knowing. Assigning one to the reader is a single line:
reader = rec.BarCodeReader("scan.png", rec.DecodeType.PDF417)
reader.quality_settings = rec.QualitySettings.high_quality
| Preset | Intended for | Cost |
|---|---|---|
high_performance | Clean, machine-generated images | Fastest, skips recovery passes |
normal_quality | Default. Regular scans and exports | Balanced |
high_quality | Camera captures, degraded scans | Moderate |
max_quality | Retry pass on images that already failed | Slowest by a wide margin |
To find out what those descriptions mean in practice, I generated a PDF417 at three pixels per module, degraded it six ways with Pillow, and ran every preset against every variant. Median of three runs:
| Source image | high_performance | normal_quality | high_quality | max_quality |
|---|---|---|---|---|
| Clean | read, 35 ms | read, 50 ms | read, 45 ms | read, 106 ms |
| Rotated 12°, blurred | miss, 15 ms | read, 74 ms | read, 67 ms | read, 157 ms |
| Gaussian noise (σ 40) | read, 31 ms | read, 49 ms | read, 52 ms | read, 123 ms |
| Half resolution | read, 22 ms | read, 37 ms | read, 35 ms | read, 86 ms |
| Low contrast | read, 24 ms | read, 38 ms | read, 43 ms | read, 112 ms |
| Heavy blur (σ 2.2) | miss, 5 ms | miss, 25 ms | miss, 85 ms | miss, 180 ms |
Four things fall out of this table, and they are more useful than the individual numbers:
- Noise and low contrast are not the problem people think they are. Reed-Solomon plus the detector absorbed heavy Gaussian noise and an 82% contrast reduction on the fastest preset. Stop tuning for these.
- Rotation plus blur is the case that separates the presets. It is also the exact profile of a handheld phone capture, which is why
high_performanceis the wrong default for any mobile-sourced pipeline. max_qualitycost three times the time ofnormal_qualityand read nothing extra. It earns its place as a second-attempt path, not a default.- Once the module grid is gone, no preset recovers it. The heavy blur row is a miss across the board, and
max_qualityspent 180 ms proving it. That is a capture problem with a capture fix: more pixels, better focus, largerx_dimensionat generation time.
Tune QualitySettings to Read Blurred PDF417 Barcodes in Python
A preset is a bundle of individual properties, and you can start from a preset and override one. This is where the real gains live for a known, repeatable defect.
The rotated-and-blurred image above is a good demonstration. Under high_performance it misses. Enable deconvolution on top of that same preset and it reads:
from aspose.barcode import barcoderecognition as rec
reader = rec.BarCodeReader("rotated.png", rec.DecodeType.PDF417)
quality = rec.QualitySettings.high_performance
quality.deconvolution = rec.DeconvolutionMode.NORMAL
reader.quality_settings = quality
for result in reader.read_bar_codes():
print(result.code_text)

Tune QualitySettings to Read Blurred PDF417 Barcodes in Python.
Measured: miss in 12 ms without the override, read in 54 ms with it. One property flipped the outcome, and the result still came back faster than the max_quality preset would have.
The properties worth knowing:
| Property | Values | When to reach for it |
|---|---|---|
deconvolution | FAST, NORMAL, SLOW | Motion blur and out-of-focus captures |
barcode_quality | HIGH, NORMAL, LOW | Print defects, thermal fade, fax artifacts |
x_dimension | AUTO, SMALL, NORMAL, LARGE, USE_MINIMAL_X_DIMENSION | Dense barcodes, or a fixed known module size |
minimal_x_dimension | float | Pair with USE_MINIMAL_X_DIMENSION to set a floor |
complex_background | AUTO, DISABLED, ENABLED | Barcode printed over artwork or a patterned form |
inverse_image | AUTO, DISABLED, ENABLED | White-on-black barcodes, screen captures |
allow_incorrect_barcodes | bool | Diagnostics only, never production |
That last one deserves a warning. allow_incorrect_barcodes returns symbols that failed checksum validation. It is a debugging aid for answering “did the detector even find something here?” Shipping it means shipping decoded strings that are wrong rather than absent, which is strictly worse than a miss.
A tiering strategy that works well in production: try normal_quality first, and on a miss retry once with high_quality plus deconvolution = SLOW. You pay the expensive path only for the images that need it.
Validate PDF417 Decoding Results in Python
A decoded string is not automatically a correct string. Three fields on BarCodeResult help:
for result in reader.read_bar_codes():
print("Text:", result.code_text)
print("Reading quality:", result.reading_quality)
print("Confidence:", result.confidence)
print("Region:", [(p.x, p.y) for p in result.region.points])
Text: PASSENGER|KHAN/M|SEAT14C|LHE-DXB
Reading quality: 100.0
Confidence: 100
Region: [(7, 6), (261, 7), (261, 276), (7, 277)]
Reassemble a Macro PDF417 Split Across Images
Macro PDF417 splits one logical message across several symbols, which shows up in shipping manifests and multi-page documents. The segment metadata survives the round trip, so reassembly is deterministic:
from aspose.barcode import barcoderecognition as rec
segments = {}
total = None
for path in ["page1.png", "page2.png"]:
reader = rec.BarCodeReader(path, rec.DecodeType.MACRO_PDF417)
for result in reader.read_bar_codes():
meta = result.extended.pdf417
segments[meta.macro_pdf_417_segment_id] = result.code_text
total = meta.macro_pdf_417_segments_count
print(f"{path}: segment {meta.macro_pdf_417_segment_id + 1} of {total}, "
f"file id {meta.macro_pdf_417_file_id}")
if total and len(segments) == total:
payload = "".join(segments[i] for i in sorted(segments))
print("Reassembled:", payload)
else:
print("Incomplete: missing segments")

Page 1.

Page 2.
Output:
Data\pdf417\page1.png: segment 1 of 2, file id 42
Data\pdf417\page2.png: segment 2 of 2, file id 42
Reassembled: SEGMENT-A-DATASEGMENT-B-DATA
Note that
macro_pdf_417_file_idis an integer, not a string, on both the generation and recognition side. The same object also exposesmacro_pdf_417_file_name,macro_pdf_417_sender,macro_pdf_417_addressee,macro_pdf_417_time_stamp, andmacro_pdf_417_checksumwhen the encoder populated them.
Benchmark PDF417 Recognition on Your Own Images
Preset choice should be a measurement, not a guess. Point this at a folder of representative captures from your actual pipeline and it will tell you which preset earns its cost:
import time, statistics, pathlib
from aspose.barcode import barcoderecognition as rec
PRESETS = ["high_performance", "normal_quality", "high_quality", "max_quality"]
def probe(path, preset, reps=3):
times, found = [], False
for _ in range(reps):
reader = rec.BarCodeReader(str(path), rec.DecodeType.PDF417)
reader.quality_settings = getattr(rec.QualitySettings, preset)
start = time.perf_counter()
results = list(reader.read_bar_codes())
times.append((time.perf_counter() - start) * 1000)
found = found or bool(results)
return found, statistics.median(times)
for image in sorted(pathlib.Path("samples").glob("*.png")):
row = []
for preset in PRESETS:
found, ms = probe(image, preset)
row.append(f"{preset}: {'read' if found else 'miss'} {ms:.0f}ms")
print(f"{image.name:28} " + " | ".join(row))
clean.png high_performance: read 32ms | normal_quality: read 48ms | high_quality: read 51ms | max_quality: read 130ms
faded_thermal.png high_performance: read 26ms | normal_quality: read 43ms | high_quality: read 46ms | max_quality: read 144ms
half_resolution.png high_performance: read 27ms | normal_quality: read 41ms | high_quality: read 41ms | max_quality: read 89ms
heavy_blur.png high_performance: miss 7ms | normal_quality: miss 24ms | high_quality: miss 69ms | max_quality: miss 180ms
noisy_scan.png high_performance: read 26ms | normal_quality: read 42ms | high_quality: read 47ms | max_quality: read 116ms
Page1.png high_performance: read 21ms | normal_quality: read 34ms | high_quality: read 38ms | max_quality: read 90ms
Page2.png high_performance: read 21ms | normal_quality: read 33ms | high_quality: read 38ms | max_quality: read 89ms
rotated.png high_performance: miss 16ms | normal_quality: read 43ms | high_quality: read 57ms | max_quality: read 147ms
Run it once against fifty real captures and the preset debate ends.
Practical Guidelines
- Restrict
DecodeTypetoPDF417unless you genuinely expect mixed symbologies. UseMACRO_PDF417,COMPACT_PDF417, orMICRO_PDF417when you know the variant. - Crop before you decode. Passing a full A4 scan when the barcode occupies 5% of it wastes detection time and adds false-positive surface.
- Keep intermediate files lossless. PNG or TIFF. JPEG ringing at low quality settings attacks exactly the bar edges the decoder needs.
- Tier your retries rather than defaulting to the slowest preset.
- Fix generation before tuning recognition. If you control the encoder, raising
x_dimensioncosts nothing and solves more misses than any reader setting. The PDF417 generation article covers those parameters. - Set
timeouton any reader that touches user-supplied images.
Get a Free License
The evaluation build watermarks decoded text for symbologies other than Code 39, which will interfere with any round-trip testing you do. A free temporary license removes that for evaluation.
Free Additional Resources
- Barcode Reading Quality Options and Presets
- Special Barcode Reading Cases
- API Reference
- Free Online Barcode Tools
Conclusion
Reading PDF417 barcodes in Python is a two-line operation on clean images and an engineering problem on real ones. The measurements above point to a simple posture: name your DecodeType, default to normal_quality, retry once with high_quality and deconvolution enabled, and validate every result before acting on it. Save the expensive presets for the retry path, and when a whole class of images fails at every setting, fix the capture instead of the reader. Questions are welcome on the free support forum.
FAQs
- Why does the same PDF417 barcode read in one image and fail in another? Recognition depends on how many pixels each module occupies after capture. Blur, downscaling, and JPEG compression all shrink the effective module width, and once a module drops below roughly two clean pixels, no preset will recover it.
- Which recognition preset should I use for PDF417?
Start with
normal_quality, which is the default. Move tohigh_qualityfor camera captures and scans, and reservemax_qualityfor retry passes because it is several times slower without reading much more. - Does restricting DecodeType to PDF417 actually make reading faster?
Yes. The default is
DecodeType.ALL_SUPPORTED_TYPES, which runs the detector for every supported symbology. Naming PDF417 explicitly removes that work and also removes the chance of a false positive from another symbology. - Can Aspose.BarCode read a Macro PDF417 message split across several images?
Yes. Read each segment with
DecodeType.MACRO_PDF417and usemacro_pdf_417_file_id,macro_pdf_417_segment_id, andmacro_pdf_417_segments_counton the extended result to reassemble the payload in order. - How do I know whether a decoded result can be trusted?
Check
reading_qualityandconfidenceon theBarCodeResult, and use theregionquadrangle to confirm the barcode was found where you expected it in the image. - Can it read PDF417 from a photo of a driver’s license or boarding pass? It can, provided the capture is in focus and the barcode is not clipped. Crop to the barcode region before reading, and prefer PNG over JPEG for intermediate files.
