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"{text}" if run.italic: text = f"{text}" 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()