408fde440d
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.
61 lines
1.9 KiB
Python
61 lines
1.9 KiB
Python
"""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
|