Initial commit: boarding pass to AirTrail pipeline

Photograph/screenshot a boarding pass, decode its BCBP barcode (PDF417/
Aztec/QR/DataMatrix), fetch the historical flight track from FlightAware
the day after the flight, stage it for review, and write it into AirTrail
via its REST API on approval. ntfy notifications carry signed
Approve/Deny/Investigate actions.
This commit is contained in:
2026-07-04 22:21:02 +10:00
commit 408fde440d
34 changed files with 11657 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
"""PDF417 barcode decoding for boarding pass photos.
zxing-cpp is used as the primary (and only) decoder: it ships prebuilt wheels
for common platforms and has more reliable PDF417 support on real-world
skewed/glare phone photos than pyzbar/zbar. Kept behind this single function
so the library can be swapped later without touching call sites, per the
brief's note that decoder reliability should be validated against real
captures rather than assumed.
"""
import io
import zxingcpp
from PIL import Image, ImageOps
MIN_DIMENSION = 800
MAX_DIMENSION = 3000
# IATA Resolution 792 (BCBP) permits any of these 2D symbologies - PDF417 is
# typical for printed passes, Aztec/QR/DataMatrix are common for on-screen
# mobile passes (e.g. Emirates' app uses Aztec).
BARCODE_FORMATS = (
zxingcpp.BarcodeFormat.PDF417
| zxingcpp.BarcodeFormat.Aztec
| zxingcpp.BarcodeFormat.QRCode
| zxingcpp.BarcodeFormat.DataMatrix
)
def _prepare(img: Image.Image) -> Image.Image:
img = ImageOps.exif_transpose(img)
img = img.convert("L")
w, h = img.size
if min(w, h) < MIN_DIMENSION:
scale = MIN_DIMENSION / min(w, h)
img = img.resize((int(w * scale), int(h * scale)), Image.LANCZOS)
elif max(w, h) > MAX_DIMENSION:
scale = MAX_DIMENSION / max(w, h)
img = img.resize((int(w * scale), int(h * scale)), Image.LANCZOS)
return img
def decode_boarding_pass(image_bytes: bytes) -> str | None:
try:
img = Image.open(io.BytesIO(image_bytes))
img = _prepare(img)
except Exception:
return None
results = zxingcpp.read_barcodes(img, formats=BARCODE_FORMATS)
if results:
return results[0].text
# One retry with autocontrast - cheap, catches glare/low-contrast photos.
contrasted = ImageOps.autocontrast(img)
results = zxingcpp.read_barcodes(contrasted, formats=BARCODE_FORMATS)
if results:
return results[0].text
return None