#!/usr/bin/env python3 """Measure how legible each page of the photocopied manual is. check_legibility.py copied.pdf [crisp.pdf] OCRs every page (tesseract) and scores it against the PDF's own invisible text layer, which is the known-correct text: the score is the share of true words that OCR recovers. If a crisp render is given, its score is the baseline (OCR is imperfect on monospace tables), and a page is judged by how much the copying cost it. Needs tesseract and poppler's pdftotext/pdftoppm. Development tool; the build does not run it. """ import difflib import os import re import subprocess import sys import tempfile from concurrent.futures import ThreadPoolExecutor from pathlib import Path def words(text): return re.findall(r"[A-Za-z0-9][A-Za-z0-9'.\-]*", text) def ocr_scores(pdf, dpi=130): """Return {page: (recall, truth_word_count)} for each page of pdf.""" with tempfile.TemporaryDirectory() as tmp: tmp = Path(tmp) subprocess.run(["pdftoppm", "-r", str(dpi), "-gray", "-png", pdf, str(tmp / "p")], check=True) images = sorted(tmp.glob("p-*.png")) def score(img): n = int(img.stem.split("-")[1]) truth = words(subprocess.run(["pdftotext", "-f", str(n), "-l", str(n), pdf, "-"], capture_output=True, text=True, check=True).stdout) got = words(subprocess.run(["tesseract", str(img), "-", "--psm", "6"], capture_output=True, text=True, env={**os.environ, "OMP_THREAD_LIMIT": "1"}).stdout) m = difflib.SequenceMatcher(None, truth, got, autojunk=False) hit = sum(b.size for b in m.get_matching_blocks()) return n, (hit / max(1, len(truth)), len(truth)) with ThreadPoolExecutor(4) as pool: return dict(pool.map(score, images)) if __name__ == "__main__": copied = ocr_scores(sys.argv[1]) crisp = ocr_scores(sys.argv[2]) if len(sys.argv) > 2 else {} print("page words crisp copied loss") losses = [] for n in sorted(copied): c, w = copied[n] b = crisp.get(n, (1.0, w))[0] losses.append(b - c) flag = " <-- check" if b - c > 0.10 else "" print(f"{n:4d} {w:6d} {b:5.0%} {c:5.0%} {b - c:+5.0%}{flag}") tot = sum(w for _, w in copied.values()) print(f"\nmean copied recall {sum(c * w for c, w in copied.values()) / tot:.1%}" + (f", mean crisp {sum(c * w for c, w in crisp.values()) / tot:.1%}" if crisp else ""))