"""Generate deterministic, nonpersonal PDF/DOCX benchmark documents and source hashes.""" import argparse from hashlib import sha256 from io import BytesIO import json from pathlib import Path import re from docx import Document from docx.oxml.ns import qn from PIL import Image, ImageDraw, ImageFont from reportlab import __file__ as reportlab_file from reportlab.lib.utils import ImageReader from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfgen import canvas FONT_PATH = Path(reportlab_file).parent / "fonts/Vera.ttf" HEADINGS = {"professional summary", "work experience", "skills", "education", "certifications", "projects", "languages", "interests", "courses", "awards", "publications", "organisations", "references", "volunteer experience", "notes", "utdanning", "språk", "arbeidserfaring"} def generate_case(case, output): identifier = case["id"] if not re.fullmatch(r"[a-z0-9-]+", identifier): raise ValueError("Fixture IDs may contain lowercase letters, numbers and hyphens only.") output.mkdir(parents=True, exist_ok=True) pdf = output / (identifier + ".pdf") docx = output / (identifier + ".docx") if pdf.exists() or docx.exists(): raise FileExistsError("Preserve prior fixtures and select a fresh output directory.") pdfmetrics.registerFont(TTFont("CvImportBenchmark", str(FONT_PATH))) document = canvas.Canvas(str(pdf), pagesize=(595, 842), invariant=1) document.setTitle("Synthetic CV import benchmark: " + identifier) document.setAuthor("Jobjakt synthetic fixture") document.setFont("CvImportBenchmark", 10) layout = case.get("layout", "single") lines = case["lines"] if layout == "scan": image = Image.new("RGB", (1000, 1400), "white") draw = ImageDraw.Draw(image) font = ImageFont.truetype(str(FONT_PATH), 22) draw.multiline_text((60, 60), "\n".join(lines), font=font, fill="black", spacing=12) buffer = BytesIO() image.save(buffer, format="PNG") document.drawImage(ImageReader(buffer), 20, 20, width=555, height=777) document.save() return {"pdf": str(pdf)} x, y = 40, 800 split = max(1, len(lines) // 2) for index, line in enumerate(lines): if line == "PAGE_BREAK" or y < 45: document.showPage() document.setFont("CvImportBenchmark", 10) x, y = 40, 800 if line == "PAGE_BREAK": continue if layout == "columns" and index == split: x, y = 305, 800 if layout == "table" and "|" in line: cells = [part.strip() for part in line.split("|")] width = 515 / len(cells) for column, cell in enumerate(cells): document.rect(40 + column * width, y - 5, width, 21) document.drawString(44 + column * width, y + 2, cell) else: # Long names deliberately exercise wrapping without changing the source value. words = line.split() width = 250 if layout == "columns" else 515 wrapped = "" for word in words: if wrapped and pdfmetrics.stringWidth(wrapped + " " + word, "CvImportBenchmark", 10) > width: document.drawString(x, y, wrapped) y -= 14 wrapped = word else: wrapped = (wrapped + " " + word).strip() document.drawString(x, y, wrapped) y -= 24 document.save() word = Document() if layout == "columns": columns = word.sections[0]._sectPr.find(qn("w:cols")) columns.set(qn("w:num"), "2") for line in lines: if line == "PAGE_BREAK": word.add_page_break() elif layout == "table" and "|" in line: cells = [part.strip() for part in line.split("|")] table = word.add_table(rows=1, cols=len(cells)) for cell, text in zip(table.rows[0].cells, cells): cell.text = text else: paragraph = word.add_paragraph(line) if line.casefold() in HEADINGS: paragraph.style = word.styles["Heading 2"] word.save(docx) return {"pdf": str(pdf), "docx": str(docx)} def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--corpus", type=Path, default=Path(__file__).with_name("corpus.json")) parser.add_argument("--output", required=True, type=Path) args = parser.parse_args() if args.output.exists(): raise FileExistsError("Select a fresh output directory.") corpus = json.loads(args.corpus.read_text(encoding="utf-8")) manifest = {"corpusSha256": sha256(args.corpus.read_bytes()).hexdigest(), "documents": []} for case in corpus: for format_name, path in generate_case(case, args.output).items(): file = Path(path) manifest["documents"].append({"caseId": case["id"], "split": case["split"], "format": format_name, "file": file.name, "sha256": sha256(file.read_bytes()).hexdigest(), "bytes": file.stat().st_size}) (args.output / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8") print(json.dumps({"documents": len(manifest["documents"]), "manifest": str(args.output / "manifest.json")})) if __name__ == "__main__": main()