Files
ubuntu-based-kiosk/library/quickadd/tests/gen_photos.py
Claude 5fc7450af7 library: scan with a phone camera (Quick Add)
A 'Scan with camera' button opens the phone's own camera app; the photo is shrunk
on the phone and read on the server with zbarimg, then goes through the normal add
flow (also available on the sticker step). Works over plain HTTP on iPhone and
Android - no HTTPS needed.

- /api/scan: JPEG/PNG only (checked by magic bytes before ImageMagick sees it),
  12 MB cap, login required, 20 s timeout, temp files cleaned up
- UPC-A reads (zbar reports 13 digits with a leading zero) normalised to the 12
  digits printed on the item so duplicate checks match
- tests/gen_photos.py + tests/check_scan.py: phone-like photos (tilt, blur, noise,
  low light, upside down, EXIF rotation, small, UPC, Code 39 sticker); 10/10 read
  at the size the page sends

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013wi3ZSot4XbUViwRFgLpRf
2026-11-06 13:12:58 +00:00

78 lines
4.0 KiB
Python

"""Generate phone-photo-like test images containing real barcodes."""
import random, io, os
import zxingcpp
from PIL import Image, ImageDraw, ImageFilter, ImageEnhance
OUT = os.environ.get("PHOTO_DIR", "/tmp/quickadd-photos")
os.makedirs(OUT, exist_ok=True)
random.seed(7)
def barcode(fmt, text, scale=4, height=90):
bc = zxingcpp.write_barcode(fmt, text, width=0, height=height, quiet_zone=10)
img = Image.fromarray(bc.__array__() if hasattr(bc, "__array__") else bc).convert("RGB")
return img.resize((img.width * scale, img.height * scale), Image.NEAREST)
def scene(code_img, w=3024, h=4032, tilt=0, blur=0.0, contrast=1.0, bright=1.0, noise=0, quality=72, exif=None, size_frac=0.45):
"""Paste the barcode (on a white label) onto a coloured, busy 'book cover'."""
bg = Image.new("RGB", (w, h), (random.randint(20, 90), random.randint(30, 80), random.randint(60, 140)))
d = ImageDraw.Draw(bg)
for _ in range(60): # clutter: stripes, blobs, text-ish bars
x, y = random.randint(0, w), random.randint(0, h)
d.rectangle([x, y, x + random.randint(40, 600), y + random.randint(8, 60)],
fill=tuple(random.randint(0, 255) for _ in range(3)))
label = code_img.copy()
k = (w * size_frac) / label.width
label = label.resize((int(label.width * k), int(label.height * k)), Image.LANCZOS)
pad = Image.new("RGB", (label.width + 60, label.height + 60), "white")
pad.paste(label, (30, 30))
if tilt:
pad = pad.rotate(tilt, expand=True, resample=Image.BICUBIC, fillcolor="white")
bg.paste(pad, ((w - pad.width) // 2 + random.randint(-80, 80), (h - pad.height) // 2 + random.randint(-300, 300)))
if blur:
bg = bg.filter(ImageFilter.GaussianBlur(blur))
bg = ImageEnhance.Contrast(bg).enhance(contrast)
bg = ImageEnhance.Brightness(bg).enhance(bright)
if noise:
px = bg.load()
for _ in range(noise):
x, y = random.randint(0, w - 1), random.randint(0, h - 1)
v = random.randint(0, 255); px[x, y] = (v, v, v)
buf = io.BytesIO()
kw = {"quality": quality}
if exif is not None:
e = Image.Exif(); e[0x0112] = exif; kw["exif"] = e
bg.save(buf, "JPEG", **kw)
return buf.getvalue()
E13 = zxingcpp.BarcodeFormat.EAN13
UPCA = zxingcpp.BarcodeFormat.UPCA
C39 = zxingcpp.BarcodeFormat.Code39
cases = {
# name: (expected, bytes)
"isbn_clean": ("9780345339683", scene(barcode(E13, "9780345339683"))),
"isbn_tilt8": ("9780345339683", scene(barcode(E13, "9780345339683"), tilt=8, blur=1.2)),
"isbn_tilt_neg12": ("9780441172719", scene(barcode(E13, "9780441172719"), tilt=-12, blur=1.5, contrast=0.8)),
"isbn_dim_noisy": ("9780345339683", scene(barcode(E13, "9780345339683"), blur=1.4, contrast=0.7, bright=0.7, noise=60000, quality=55)),
"isbn_small": ("9780441172719", scene(barcode(E13, "9780441172719"), size_frac=0.22, blur=1.0)),
"isbn_exif_rot90": ("9780345339683", scene(barcode(E13, "9780345339683").rotate(90, expand=True), exif=6)),
"isbn_upside_down": ("9780345339683", scene(barcode(E13, "9780345339683").rotate(180, expand=True))),
"upc_a": ("036000291452", scene(barcode(UPCA, "036000291452"), tilt=5, blur=1.0)),
}
for name, (exp, data) in cases.items():
open(f"{OUT}/{name}.jpg", "wb").write(data)
open(f"{OUT}/{name}.expect", "w").write(exp)
# sticker (Code 39, 7 digits) and a photo with no barcode at all
open(f"{OUT}/sticker_c39.jpg", "wb").write(scene(barcode(C39, "0000042", scale=3), tilt=4, blur=1.0, size_frac=0.5))
open(f"{OUT}/sticker_c39.expect", "w").write("0000042")
blank = Image.new("RGB", (3024, 4032), (90, 60, 40)); ImageDraw.Draw(blank).rectangle([400, 900, 2600, 1300], fill=(200, 180, 60))
b = io.BytesIO(); blank.save(b, "JPEG", quality=80); open(f"{OUT}/no_barcode.jpg", "wb").write(b.getvalue())
print("wrote", len(os.listdir(OUT)) // 2, "photos to", OUT)
for n in sorted(os.listdir(OUT)):
if n.endswith(".jpg"):
print(f" {n:22s} {os.path.getsize(os.path.join(OUT, n)) / 1e6:5.1f} MB")