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.

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
PresetIntended forCost
high_performanceClean, machine-generated imagesFastest, skips recovery passes
normal_qualityDefault. Regular scans and exportsBalanced
high_qualityCamera captures, degraded scansModerate
max_qualityRetry pass on images that already failedSlowest 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 imagehigh_performancenormal_qualityhigh_qualitymax_quality
Cleanread, 35 msread, 50 msread, 45 msread, 106 ms
Rotated 12°, blurredmiss, 15 msread, 74 msread, 67 msread, 157 ms
Gaussian noise (σ 40)read, 31 msread, 49 msread, 52 msread, 123 ms
Half resolutionread, 22 msread, 37 msread, 35 msread, 86 ms
Low contrastread, 24 msread, 38 msread, 43 msread, 112 ms
Heavy blur (σ 2.2)miss, 5 msmiss, 25 msmiss, 85 msmiss, 180 ms

Four things fall out of this table, and they are more useful than the individual numbers:

  1. 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.
  2. 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_performance is the wrong default for any mobile-sourced pipeline.
  3. max_quality cost three times the time of normal_quality and read nothing extra. It earns its place as a second-attempt path, not a default.
  4. Once the module grid is gone, no preset recovers it. The heavy blur row is a miss across the board, and max_quality spent 180 ms proving it. That is a capture problem with a capture fix: more pixels, better focus, larger x_dimension at 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.

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:

PropertyValuesWhen to reach for it
deconvolutionFAST, NORMAL, SLOWMotion blur and out-of-focus captures
barcode_qualityHIGH, NORMAL, LOWPrint defects, thermal fade, fax artifacts
x_dimensionAUTO, SMALL, NORMAL, LARGE, USE_MINIMAL_X_DIMENSIONDense barcodes, or a fixed known module size
minimal_x_dimensionfloatPair with USE_MINIMAL_X_DIMENSION to set a floor
complex_backgroundAUTO, DISABLED, ENABLEDBarcode printed over artwork or a patterned form
inverse_imageAUTO, DISABLED, ENABLEDWhite-on-black barcodes, screen captures
allow_incorrect_barcodesboolDiagnostics 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 1.

Page 2.

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_id is an integer, not a string, on both the generation and recognition side. The same object also exposes macro_pdf_417_file_name, macro_pdf_417_sender, macro_pdf_417_addressee, macro_pdf_417_time_stamp, and macro_pdf_417_checksum when 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 DecodeType to PDF417 unless you genuinely expect mixed symbologies. Use MACRO_PDF417, COMPACT_PDF417, or MICRO_PDF417 when 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_dimension costs nothing and solves more misses than any reader setting. The PDF417 generation article covers those parameters.
  • Set timeout on 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

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

  1. 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.
  2. Which recognition preset should I use for PDF417? Start with normal_quality, which is the default. Move to high_quality for camera captures and scans, and reserve max_quality for retry passes because it is several times slower without reading much more.
  3. 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.
  4. Can Aspose.BarCode read a Macro PDF417 message split across several images? Yes. Read each segment with DecodeType.MACRO_PDF417 and use macro_pdf_417_file_id, macro_pdf_417_segment_id, and macro_pdf_417_segments_count on the extended result to reassemble the payload in order.
  5. How do I know whether a decoded result can be trusted? Check reading_quality and confidence on the BarCodeResult, and use the region quadrangle to confirm the barcode was found where you expected it in the image.
  6. 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.

Read More