carlos / contra Public

Clone
git clone https://amadan.net/carlos/contra

Plain git — no account needed to clone.

Download

Download this file

1"""The Emergency Kit oracle: read the PDF back the way a person would.
2
3Two questions, and the second is the one that matters:
4
5 1. Does the text extract? A document whose code cannot be selected or
6 searched is one somebody has to retype by hand from a photograph.
7 2. **Does the QR inside the rendered page actually scan?** Not the matrix
8 the encoder produced — the page as a reader draws it. Between those two
9 sit the coordinate flip, the module size, the quiet zone and whether the
10 squares land on the same grid. Every one of those can be wrong in a
11 document whose matrix is perfect.
12
13PyMuPDF renders, ZXing decodes, pypdf extracts. None of them is ours.
14
15Reads {pdf_base64, expect_code, expect_text[]} on stdin, writes a verdict.
16
17Needs: pip install --user pymupdf zxing-cpp pypdf pillow numpy
18"""
19
20import base64
21import io
22import json
23import sys
24
25import numpy as np
26import pymupdf
27import zxingcpp
28from PIL import Image
29from pypdf import PdfReader
30
31
32def check(case):
33 raw = base64.b64decode(case["pdf_base64"])
34 out = {"name": case.get("name", "")}
35
36 # 1. Structure and text, through a second reader.
37 reader = PdfReader(io.BytesIO(raw))
38 out["pages"] = len(reader.pages)
39 text = "\n".join(page.extract_text() or "" for page in reader.pages)
40 out["missing_text"] = [s for s in case.get("expect_text", []) if s not in text]
41 out["code_in_text"] = case["expect_code"] in text.replace("\n", "")
42
43 # 2. Render at print resolution and scan it, exactly as a phone would.
44 doc = pymupdf.open(stream=raw, filetype="pdf")
45 found = []
46 for page in doc:
47 pix = page.get_pixmap(dpi=case.get("dpi", 200))
48 img = Image.frombytes("RGB", (pix.width, pix.height), pix.samples).convert("L")
49 for r in zxingcpp.read_barcodes(np.array(img)):
50 found.append(r.text)
51 out["scanned"] = found
52 out["qr_scans"] = case["expect_code"] in found
53
54 # Exactly one QR per document: a second one would mean the two secrets had
55 # ended up on one sheet, which is the whole thing these documents are two
56 # of in order to prevent.
57 out["one_qr"] = len(found) == 1
58
59 out["ok"] = bool(
60 out["qr_scans"] and out["one_qr"] and out["code_in_text"] and not out["missing_text"]
61 )
62 return out
63
64
65def main():
66 cases = json.load(sys.stdin)
67 print(json.dumps([check(c) for c in cases]))
68
69
70if __name__ == "__main__":
71 main()
72