148 lines
6.2 KiB
Python
148 lines
6.2 KiB
Python
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()
|