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