121 lines
5.3 KiB
Python
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()
|