Files
jobtrackingapp/docs/cv-import/benchmarks/generate.py
T

121 lines
5.3 KiB
Python

"""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()