feat: build Learning Planner evidence project
This commit is contained in:
147
scripts/build_evidence_pdfs.py
Normal file
147
scripts/build_evidence_pdfs.py
Normal file
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from xml.sax.saxutils import escape
|
||||
|
||||
from docx import Document
|
||||
from docx.oxml.ns import qn
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.enums import TA_CENTER
|
||||
from reportlab.lib.pagesizes import LETTER
|
||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.platypus import Image, PageBreak, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DOCS = ROOT / "evidence-documents"
|
||||
ASSETS = ROOT / "work" / "pdf-assets"
|
||||
|
||||
|
||||
def footer(canvas, doc):
|
||||
canvas.saveState()
|
||||
canvas.setFont("Helvetica-Bold", 7.5)
|
||||
canvas.setFillColor(colors.HexColor("#6F6D82"))
|
||||
canvas.drawString(0.8 * inch, 10.55 * inch, "ENGINEERING COMPETENCY · LEARNING PLANNER")
|
||||
canvas.setFont("Helvetica", 8)
|
||||
canvas.drawRightString(7.7 * inch, 0.42 * inch, f"Learning Planner · Competency evidence · {doc.page}")
|
||||
canvas.restoreState()
|
||||
|
||||
|
||||
def styles():
|
||||
base = getSampleStyleSheet()
|
||||
return {
|
||||
"body": ParagraphStyle("Body", parent=base["BodyText"], fontName="Helvetica", fontSize=9.7, leading=12.3, textColor=colors.HexColor("#212037"), spaceAfter=5),
|
||||
"h2": ParagraphStyle("H2", parent=base["Heading2"], fontName="Helvetica-Bold", fontSize=12.2, leading=14, textColor=colors.HexColor("#2E74B5"), spaceBefore=8, spaceAfter=5, keepWithNext=True),
|
||||
"title": ParagraphStyle("Title", parent=base["Title"], fontName="Helvetica-Bold", fontSize=22, leading=24, textColor=colors.HexColor("#212037"), alignment=0, spaceAfter=4),
|
||||
"appendix_title": ParagraphStyle("AppendixTitle", parent=base["Heading1"], fontName="Helvetica-Bold", fontSize=15, leading=18, textColor=colors.HexColor("#212037"), spaceAfter=6),
|
||||
"subtitle": ParagraphStyle("Subtitle", parent=base["BodyText"], fontName="Helvetica", fontSize=11.2, leading=14, textColor=colors.HexColor("#6F6D82"), spaceAfter=10),
|
||||
"kicker": ParagraphStyle("Kicker", parent=base["BodyText"], fontName="Helvetica-Bold", fontSize=8.3, leading=10, textColor=colors.HexColor("#6257D9"), spaceAfter=2),
|
||||
"caption": ParagraphStyle("Caption", parent=base["BodyText"], fontName="Helvetica-Oblique", fontSize=8.5, leading=11, textColor=colors.HexColor("#6F6D82"), alignment=TA_CENTER, spaceBefore=4),
|
||||
}
|
||||
|
||||
|
||||
def rich_text(paragraph) -> str:
|
||||
chunks = []
|
||||
for run in paragraph.runs:
|
||||
text = escape(run.text)
|
||||
if not text:
|
||||
continue
|
||||
if run.bold:
|
||||
text = f"<b>{text}</b>"
|
||||
if run.italic:
|
||||
text = f"<i>{text}</i>"
|
||||
chunks.append(text)
|
||||
return "".join(chunks) or escape(paragraph.text)
|
||||
|
||||
|
||||
def has_page_break(paragraph) -> bool:
|
||||
return bool(paragraph._p.xpath('.//w:br[@w:type="page"]'))
|
||||
|
||||
|
||||
def paragraph_image(paragraph, document, asset_dir: Path):
|
||||
blips = paragraph._p.xpath(".//a:blip")
|
||||
if not blips:
|
||||
return None
|
||||
rid = blips[0].get(qn("r:embed"))
|
||||
part = document.part.related_parts[rid]
|
||||
extension = Path(part.partname).suffix or ".png"
|
||||
path = asset_dir / f"image-{len(list(asset_dir.glob('image-*'))) + 1}{extension}"
|
||||
path.write_bytes(part.blob)
|
||||
image = Image(str(path))
|
||||
max_width, max_height = 6.25 * inch, 7.55 * inch
|
||||
scale = min(max_width / image.imageWidth, max_height / image.imageHeight)
|
||||
image.drawWidth = image.imageWidth * scale
|
||||
image.drawHeight = image.imageHeight * scale
|
||||
image.hAlign = "CENTER"
|
||||
return image
|
||||
|
||||
|
||||
def convert(docx_path: Path):
|
||||
document = Document(docx_path)
|
||||
asset_dir = ASSETS / docx_path.stem
|
||||
asset_dir.mkdir(parents=True, exist_ok=True)
|
||||
pdf_path = docx_path.with_suffix(".pdf")
|
||||
story = []
|
||||
style = styles()
|
||||
|
||||
for paragraph in document.paragraphs:
|
||||
if has_page_break(paragraph):
|
||||
story.append(PageBreak())
|
||||
continue
|
||||
|
||||
image = paragraph_image(paragraph, document, asset_dir)
|
||||
if image is not None:
|
||||
story.append(Spacer(1, 3))
|
||||
story.append(image)
|
||||
continue
|
||||
|
||||
text = paragraph.text.strip()
|
||||
if not text:
|
||||
story.append(Spacer(1, 3))
|
||||
continue
|
||||
|
||||
first_size = next((run.font.size.pt for run in paragraph.runs if run.font.size), None)
|
||||
if paragraph.style.name == "Heading 2":
|
||||
story.append(Paragraph(rich_text(paragraph), style["h2"]))
|
||||
elif first_size and first_size >= 20:
|
||||
story.append(Paragraph(rich_text(paragraph), style["title"]))
|
||||
elif text.startswith("SCREENSHOT APPENDIX"):
|
||||
story.append(Paragraph(escape(text), style["kicker"]))
|
||||
elif first_size and first_size >= 14:
|
||||
story.append(Paragraph(rich_text(paragraph), style["appendix_title"]))
|
||||
elif text == "COMPETENCY EVIDENCE BRIEF":
|
||||
story.append(Paragraph(escape(text), style["kicker"]))
|
||||
elif first_size and first_size >= 11:
|
||||
story.append(Paragraph(rich_text(paragraph), style["subtitle"]))
|
||||
elif text.startswith("Figure "):
|
||||
story.append(Paragraph(escape(text), style["caption"]))
|
||||
elif paragraph._p.xpath(".//w:shd"):
|
||||
box = Table([[Paragraph(rich_text(paragraph), style["body"])]], colWidths=[6.2 * inch])
|
||||
box.setStyle(TableStyle([
|
||||
("BACKGROUND", (0, 0), (-1, -1), colors.HexColor("#F2F4F7")),
|
||||
("BOX", (0, 0), (-1, -1), 0.5, colors.HexColor("#D9DCE4")),
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 10),
|
||||
("RIGHTPADDING", (0, 0), (-1, -1), 10),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 7),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
|
||||
]))
|
||||
story.extend([Spacer(1, 3), box, Spacer(1, 5)])
|
||||
else:
|
||||
story.append(Paragraph(rich_text(paragraph), style["body"]))
|
||||
|
||||
pdf = SimpleDocTemplate(
|
||||
str(pdf_path), pagesize=LETTER, rightMargin=0.8 * inch, leftMargin=0.8 * inch,
|
||||
topMargin=0.72 * inch, bottomMargin=0.65 * inch, title=document.core_properties.title,
|
||||
author="Learning Planner project", subject=document.core_properties.subject,
|
||||
)
|
||||
pdf.build(story, onFirstPage=footer, onLaterPages=footer)
|
||||
|
||||
|
||||
def main():
|
||||
ASSETS.mkdir(parents=True, exist_ok=True)
|
||||
for path in sorted(DOCS.glob("0*-*.docx")):
|
||||
convert(path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user