| 1 | """The QR oracle: two independent implementations, neither of them ours. |
| 2 | |
| 3 | The Emergency Kit's QR codes are the one artifact in this product that has to |
| 4 | be right years after it was printed, on a phone we have never seen, when the |
| 5 | person holding it has already lost everything else. "It looks like a QR code" |
| 6 | is not verification of that. |
| 7 | |
| 8 | So every symbol the JS encoder produces is checked twice here: |
| 9 | |
| 10 | 1. Decoded back to its payload by **ZXing** (zxing-cpp), which is the |
| 11 | library most phone scanners are descended from. This answers "will it |
| 12 | scan?". |
| 13 | 2. Compared **bit-for-bit** against Python's `qrcode` at the same version, |
| 14 | EC level and mask. This answers "is it the right symbol?", which |
| 15 | decoding alone does not — error correction can hide a real mistake in a |
| 16 | symbol that still reads. |
| 17 | |
| 18 | Neither tool is ours, and they were checked against each other before either |
| 19 | was trusted (test/qr.test.mjs's fixtures come from this file). |
| 20 | |
| 21 | Reads a JSON list of {text, ec, version, mask, modules} on stdin; writes a |
| 22 | JSON verdict per case on stdout. |
| 23 | |
| 24 | Needs: pip install --user zxing-cpp qrcode pillow numpy |
| 25 | """ |
| 26 | |
| 27 | import json |
| 28 | import sys |
| 29 | |
| 30 | import numpy as np |
| 31 | import qrcode |
| 32 | import zxingcpp |
| 33 | from PIL import Image |
| 34 | |
| 35 | EC = { |
| 36 | "L": qrcode.constants.ERROR_CORRECT_L, |
| 37 | "M": qrcode.constants.ERROR_CORRECT_M, |
| 38 | "Q": qrcode.constants.ERROR_CORRECT_Q, |
| 39 | "H": qrcode.constants.ERROR_CORRECT_H, |
| 40 | } |
| 41 | |
| 42 | |
| 43 | def to_image(modules, scale=8, quiet=4): |
| 44 | """Render a module matrix the way a printer would: dark on white, with the |
| 45 | quiet zone the specification requires. Without the quiet zone a correct |
| 46 | symbol simply does not decode, which is a real failure mode and not one we |
| 47 | want to discover on paper.""" |
| 48 | n = len(modules) |
| 49 | side = (n + quiet * 2) * scale |
| 50 | img = Image.new("L", (side, side), 255) |
| 51 | px = img.load() |
| 52 | for r in range(n): |
| 53 | for c in range(n): |
| 54 | if modules[r][c]: |
| 55 | for y in range(scale): |
| 56 | for x in range(scale): |
| 57 | px[(c + quiet) * scale + x, (r + quiet) * scale + y] = 0 |
| 58 | return img |
| 59 | |
| 60 | |
| 61 | def reference_matrix(text, ec, mask): |
| 62 | """The reference symbol, forced to alphanumeric mode. |
| 63 | |
| 64 | python-qrcode picks the most efficient mode for the data, so an all-digit |
| 65 | payload comes out in numeric mode — a different, equally valid symbol that |
| 66 | decodes to the same string. Our encoder is alphanumeric-only by design, so |
| 67 | comparing against an auto-selected reference would report a difference |
| 68 | that is not a defect. Forcing the mode compares like with like. |
| 69 | |
| 70 | Real CONTRA codes begin "A1-"/"C1-" and are never all digits, so this only |
| 71 | affects synthetic payloads in the sweep — but a test that is wrong on the |
| 72 | easy cases teaches you to ignore it on the hard ones.""" |
| 73 | q = qrcode.QRCode(error_correction=EC[ec], mask_pattern=mask, border=0) |
| 74 | q.add_data(qrcode.util.QRData(text.encode("ascii"), |
| 75 | mode=qrcode.util.MODE_ALPHA_NUM)) |
| 76 | q.make(fit=True) |
| 77 | return [[1 if cell else 0 for cell in row] for row in q.get_matrix()], q.version |
| 78 | |
| 79 | |
| 80 | def check(case): |
| 81 | text, ec, mask = case["text"], case["ec"], case["mask"] |
| 82 | modules = case["modules"] |
| 83 | out = {"text": text, "ec": ec, "mask": mask, "version": case["version"]} |
| 84 | |
| 85 | # 1. Does it scan? |
| 86 | decoded = zxingcpp.read_barcode(np.array(to_image(modules))) |
| 87 | out["decoded"] = None if decoded is None else decoded.text |
| 88 | out["scans"] = decoded is not None and decoded.text == text |
| 89 | |
| 90 | # 2. Is it the same symbol another encoder would have made? |
| 91 | try: |
| 92 | ref, ref_version = reference_matrix(text, ec, mask) |
| 93 | out["reference_version"] = ref_version |
| 94 | out["same_size"] = len(ref) == len(modules) |
| 95 | out["identical"] = out["same_size"] and all( |
| 96 | ref[r][c] == modules[r][c] for r in range(len(ref)) for c in range(len(ref)) |
| 97 | ) |
| 98 | if out["same_size"] and not out["identical"]: |
| 99 | out["differing_modules"] = sum( |
| 100 | ref[r][c] != modules[r][c] |
| 101 | for r in range(len(ref)) |
| 102 | for c in range(len(ref)) |
| 103 | ) |
| 104 | except Exception as exc: # noqa: BLE001 - the verdict carries the reason |
| 105 | out["identical"] = False |
| 106 | out["reference_error"] = str(exc) |
| 107 | |
| 108 | out["ok"] = bool(out["scans"] and out.get("identical")) |
| 109 | return out |
| 110 | |
| 111 | |
| 112 | def main(): |
| 113 | cases = json.load(sys.stdin) |
| 114 | print(json.dumps([check(c) for c in cases])) |
| 115 | |
| 116 | |
| 117 | if __name__ == "__main__": |
| 118 | main() |
| 119 | |