91 lines
5.2 KiB
Python
91 lines
5.2 KiB
Python
"""Independent PDF retention/bounds and Poppler contact-sheet proof for 18 letters.
|
|
|
|
Keeps the original four-letter corpus and all fidelity tolerances untouched.
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import unicodedata
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
import pdfplumber
|
|
from PIL import Image, ImageDraw
|
|
|
|
|
|
def normalize(text):
|
|
return re.sub(r"\s+", "", unicodedata.normalize("NFKC", text)).casefold()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("directory", type=Path)
|
|
parser.add_argument("--poppler", type=Path, required=True)
|
|
args = parser.parse_args()
|
|
source = json.loads((args.directory / "measurements.json").read_text(encoding="utf-8"))
|
|
fixtures = []
|
|
for fixture in source["fixtures"]:
|
|
directory = args.directory / fixture["fixture"]
|
|
filename = directory / "export.pdf"
|
|
failures, pages, markers, texts = [], [], [], []
|
|
columns = [[], []]
|
|
sidebar = fixture["measured"]["pageDisplay"] == "grid"
|
|
with pdfplumber.open(filename) as document:
|
|
for number, page in enumerate(document.pages, 1):
|
|
text = page.extract_text() or ""
|
|
texts.append(text)
|
|
if sidebar:
|
|
# PDF line extraction otherwise interleaves separate sidebar/body columns.
|
|
boundary = (210 - 68 if fixture["settings"]["themeId"] == "nordic" else 68) * 72 / 25.4
|
|
for index, bounds in enumerate(((0, 0, boundary, page.height), (boundary, 0, page.width, page.height))):
|
|
columns[index].append(page.crop(bounds).extract_text() or "")
|
|
markers.extend(re.findall(r"\b[SEBPR]\d{2}\b", text))
|
|
words = page.extract_words()
|
|
outside = [word for word in words if word["x0"] < -1.42 or word["top"] < -1.42
|
|
or word["x1"] > page.width + 1.42 or word["bottom"] > page.height + 1.42]
|
|
if outside or not words:
|
|
failures.append(f"page {number}: {len(outside)} outside words; {len(words)} total words")
|
|
if abs(page.width * 25.4 / 72 - 210) > .5 or abs(page.height * 25.4 / 72 - 297) > .5:
|
|
failures.append(f"page {number}: unexpected A4 dimensions")
|
|
pages.append({"page": number, "widthPt": page.width, "heightPt": page.height, "words": len(words), "outsideWords": outside})
|
|
combined = normalize("\n".join("\n".join(column) for column in columns) if sidebar else "\n".join(texts))
|
|
for text in fixture["expectedParagraphs"] + [fixture["expectedRecipient"], fixture["expectedSender"], fixture["expectedSubject"]]:
|
|
if normalize(text) not in combined:
|
|
failures.append("Missing or changed source text: " + text[:90])
|
|
if Counter(markers) != Counter(fixture["expectedMarkers"]):
|
|
failures.append("PDF/source marker multisets differ")
|
|
if len(pages) != fixture["previewPages"]:
|
|
failures.append("PDF/preview page count differs")
|
|
for page in range(1, len(pages) + 1):
|
|
subprocess.run([str(args.poppler), "-f", str(page), "-l", str(page), "-singlefile", "-scale-to", "1400", "-png", str(filename), str(directory / f"poppler-{page}")], check=True, capture_output=True)
|
|
fixtures.append({"fixture": fixture["fixture"], "pass": not failures, "failures": failures,
|
|
"pdfSha256": hashlib.sha256(filename.read_bytes()).hexdigest(), "pages": pages,
|
|
"expectedMarkers": len(fixture["expectedMarkers"]), "actualMarkers": len(markers)})
|
|
report = {"total": len(fixtures), "passed": sum(item["pass"] for item in fixtures),
|
|
"pages": sum(len(item["pages"]) for item in fixtures), "fixtures": fixtures,
|
|
"qualification": "Finite local synthetic retention, page dimensions/count and physical word bounds; sidebar text extracted by physical columns to avoid line interleaving. Not human print acceptance."}
|
|
(args.directory / "pdf-proof.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
for language in ("short-en", "long-nb"):
|
|
for edge in ("first", "last"):
|
|
sheet = Image.new("RGB", (900, 1365), "#e5e7eb")
|
|
draw = ImageDraw.Draw(sheet)
|
|
for index, fixture in enumerate([item for item in fixtures if item["fixture"].startswith(language + "-")]):
|
|
number = 1 if edge == "first" else len(fixture["pages"])
|
|
with Image.open(args.directory / fixture["fixture"] / f"poppler-{number}.png") as image:
|
|
image.thumbnail((286, 414))
|
|
x, y = (index % 3) * 300 + 7, (index // 3) * 455 + 32
|
|
sheet.paste(image, (x, y))
|
|
draw.text((x, y - 25), f"{fixture['fixture']} p{number}/{len(fixture['pages'])}", fill="#111827")
|
|
sheet.save(args.directory / f"contact-{language}-{edge}.png")
|
|
print(f"Matching letter physical proof: {report['passed']}/{report['total']} documents, {report['pages']} pages")
|
|
for item in fixtures:
|
|
if not item["pass"]:
|
|
print(item["fixture"], item["failures"])
|
|
raise SystemExit(0 if report["passed"] == report["total"] else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|