render_v3_pdf.py
#!/usr/bin/env python3
"""Render a positioning-paper markdown file to a shareable PDF.
Usage (from repo root):
python3 docs/whitepapers/render_v3_pdf.py
-> renders the v3.1 OKI paper to its committed PDF path (every default
below tracks the current external edition)
python3 docs/whitepapers/render_v3_pdf.py --help
-> the CLI added 2026-09-02 (WS-1b) so the same renderer produces the
platform whitepaper r3.6 PDF from docs/MIZOKI_3.5_WHITEPAPER_r3.6_SEP2026.md;
the exact invocation is in docs/whitepapers/README.md.
Dependency: docs/whitepapers/requirements.txt (reportlab, pinned).
"""
from __future__ import annotations
import argparse
import re
from pathlib import Path
from reportlab.lib.colors import Color, HexColor, white, black
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT, TA_RIGHT
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.platypus import (
KeepTogether,
ListFlowable,
ListItem,
PageBreak,
Paragraph,
SimpleDocTemplate,
Spacer,
Table,
TableStyle,
HRFlowable,
)
NAVY = HexColor("#0B1F33")
GOLD = HexColor("#B0893E")
RULE = HexColor("#D6D1C4")
MUTED = HexColor("#4A5560")
ROW_ALT = HexColor("#F4F1EA")
BODY = HexColor("#1A1A1A")
ROOT = Path(__file__).resolve().parent
MD_PATH = ROOT / "MIZ_OKI_3.5_Operating_Knowledge_Intelligence_Whitepaper.md"
PDF_PATH = ROOT / "MIZ_OKI_3.5_Operating_Knowledge_Intelligence_Whitepaper.pdf"
# Defaults for the v3.1 external OKI paper. Update with its source edition.
DEFAULT_KICKER = "MIZ OKI 3.5"
DEFAULT_TITLE = "Operating Knowledge Intelligence"
DEFAULT_SUBTITLE = "External whitepaper for governed enterprise decisions."
DEFAULT_VERSION_LABEL = "v3.1"
DEFAULT_DATE_LABEL = "23 September 2026"
DEFAULT_SUPERSEDES_LABEL = "v3.0 (13 August 2026); July v2.0"
DEFAULT_COVER_LINES = (
"Reconciled to MIZOKICloudRun measured state",
"Platform ceiling: built, pre-benchmark",
"Stage 3 recommend-only is the fleet default",
)
DEFAULT_HEADER_LEFT = "MIZ OKI 3.5 | Operating Knowledge Intelligence"
DEFAULT_FOOTER_NOTE = "built, pre-benchmark · External whitepaper"
DEFAULT_TAGLINE = "Evidence. Authority. Accountable outcomes."
DEFAULT_DOC_TITLE = "MIZ OKI 3.5 -- Operating Knowledge Intelligence"
DEFAULT_AUTHOR = "Media Intelligence"
BODY_START_CHOICES = ("first-h2", "after-rule")
def _styles() -> dict[str, ParagraphStyle]:
base = getSampleStyleSheet()
return {
"cover_kicker": ParagraphStyle(
"cover_kicker",
parent=base["Normal"],
fontName="Times-Italic",
fontSize=11,
textColor=GOLD,
alignment=TA_LEFT,
spaceAfter=10,
tracking=1,
),
"cover_title": ParagraphStyle(
"cover_title",
parent=base["Normal"],
fontName="Times-Bold",
fontSize=28,
leading=34,
textColor=white,
alignment=TA_LEFT,
spaceAfter=8,
),
"cover_sub": ParagraphStyle(
"cover_sub",
parent=base["Normal"],
fontName="Times-Roman",
fontSize=13,
leading=18,
textColor=HexColor("#E8E4D9"),
alignment=TA_LEFT,
spaceAfter=6,
),
"cover_meta": ParagraphStyle(
"cover_meta",
parent=base["Normal"],
fontName="Helvetica",
fontSize=9,
leading=13,
textColor=HexColor("#C9C3B4"),
alignment=TA_LEFT,
spaceAfter=3,
),
"h1": ParagraphStyle(
"h1",
parent=base["Normal"],
fontName="Times-Bold",
fontSize=16,
keepWithNext=True,
leading=20,
textColor=NAVY,
spaceBefore=16,
spaceAfter=8,
),
"h2": ParagraphStyle(
"h2",
parent=base["Normal"],
fontName="Times-Bold",
fontSize=12.5,
keepWithNext=True,
leading=16,
textColor=NAVY,
spaceBefore=12,
spaceAfter=6,
),
"body": ParagraphStyle(
"body",
parent=base["Normal"],
fontName="Times-Roman",
fontSize=10.2,
leading=14.4,
textColor=BODY,
alignment=TA_JUSTIFY,
spaceAfter=8,
),
"quote": ParagraphStyle(
"quote",
parent=base["Normal"],
fontName="Times-Italic",
fontSize=9.6,
leading=13.4,
textColor=MUTED,
leftIndent=18,
rightIndent=10,
spaceAfter=8,
),
"bullet": ParagraphStyle(
"bullet",
parent=base["Normal"],
fontName="Times-Roman",
fontSize=10.2,
leading=14.4,
textColor=BODY,
leftIndent=14,
bulletIndent=0,
spaceAfter=3,
),
"bullet_nested": ParagraphStyle(
"bullet_nested",
parent=base["Normal"],
fontName="Times-Roman",
fontSize=9.8,
leading=13.8,
textColor=BODY,
leftIndent=30,
bulletIndent=16,
spaceAfter=3,
),
"table_cell": ParagraphStyle(
"table_cell",
parent=base["Normal"],
fontName="Helvetica",
fontSize=7.8,
leading=10.4,
textColor=BODY,
),
"table_head": ParagraphStyle(
"table_head",
parent=base["Normal"],
fontName="Helvetica-Bold",
fontSize=7.8,
leading=10.4,
textColor=white,
),
"caption": ParagraphStyle(
"caption",
parent=base["Normal"],
fontName="Helvetica-Oblique",
fontSize=8,
leading=11,
textColor=MUTED,
spaceAfter=10,
),
}
def _inline(text: str) -> str:
text = text.replace("&", "&").replace("<", "<").replace(">", ">")
text = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", text)
# Single-asterisk italics (added 2026-09-02; the OKI paper carries none, so its
# render is unchanged). Runs only after bold so `**` pairs are never split.
text = re.sub(r"(?<![*\w])\*(?!\*)([^*\n]+?)\*(?![*\w])", r"<i>\1</i>", text)
text = re.sub(r"`([^`]+)`", r"<font face='Courier' size='8.5'>\1</font>", text)
text = re.sub(
r"\[([^\]]+)\]\((https?://[^)]+)\)",
lambda m: '<link href="' + m.group(2).replace('"', '"') + '" color="#0B5964">' + m.group(1) + '</link>',
text,
)
text = text.replace(" -- ", " — ")
return text
def _cover_version_line(version_label: str, date_label: str) -> str:
# "v3.0" -> "Version 3.0 · <date>" (the pre-CLI cover string); any other label
# ("Revision 3.6") is used as written.
if re.fullmatch(r"v\d[\d.]*", version_label):
return f"Version {version_label[1:]} · {date_label}"
return f"{version_label} · {date_label}"
def build_settings(args: argparse.Namespace) -> dict:
"""Resolve CLI arguments into the strings the render bakes in. With no
arguments the labels describe the current external edition (verified by
tests/governance/test_platform_whitepaper_r36.py)."""
version_label = args.version_label
date_label = args.date_label
cover_lines = [_cover_version_line(version_label, date_label)]
cover_lines += list(args.cover_line) if args.cover_line else [DEFAULT_COVER_LINES[0]]
cover_lines.append(f"Supersedes {args.supersedes_label}")
if not args.cover_line:
cover_lines += list(DEFAULT_COVER_LINES[1:])
subject = args.subject or (
f"External whitepaper {version_label}, reconciled {date_label}"
)
return {
"input": Path(args.input).resolve(),
"output": Path(args.output).resolve(),
"kicker": args.kicker,
"title": args.title,
"subtitle": args.subtitle,
"cover_lines": cover_lines,
"header_left": args.header_left,
"header_right": f"{version_label} | {date_label}",
"footer_note": args.footer_note,
"tagline": args.tagline,
"doc_title": args.doc_title,
"author": args.author,
"subject": subject,
"body_start": args.body_start,
}
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
p = argparse.ArgumentParser(
description="Render a positioning-paper markdown file to PDF. With no "
"arguments, renders the v3.1 OKI paper exactly as before the CLI existed.",
)
p.add_argument("--input", default=str(MD_PATH), help="markdown source (default: the v3.0 OKI paper)")
p.add_argument("--output", default=str(PDF_PATH), help="PDF to write (default: the committed OKI PDF)")
p.add_argument("--kicker", default=DEFAULT_KICKER, help="small italic line above the cover title")
p.add_argument("--title", default=DEFAULT_TITLE, help="cover title")
p.add_argument("--subtitle", default=DEFAULT_SUBTITLE, help="cover subtitle")
p.add_argument("--version-label", default=DEFAULT_VERSION_LABEL,
help="e.g. 'v3.0' or 'Revision 3.6'; appears in the running header and the cover")
p.add_argument("--date-label", default=DEFAULT_DATE_LABEL, help="e.g. '13 August 2026'")
p.add_argument("--supersedes-label", default=DEFAULT_SUPERSEDES_LABEL,
help="what this edition supersedes; rendered as 'Supersedes <label>' on the cover")
p.add_argument("--cover-line", action="append", default=None,
help="extra cover line (repeatable). Replaces the default cover lines "
"between the version line and the supersedes line.")
p.add_argument("--header-left", default=DEFAULT_HEADER_LEFT, help="running header, left")
p.add_argument("--footer-note", default=DEFAULT_FOOTER_NOTE, help="running footer, left")
p.add_argument("--tagline", default=DEFAULT_TAGLINE, help="italic line at the foot of the cover")
p.add_argument("--doc-title", default=DEFAULT_DOC_TITLE, help="PDF metadata title")
p.add_argument("--author", default=DEFAULT_AUTHOR, help="PDF metadata author")
p.add_argument("--subject", default=None,
help="PDF metadata subject (default: derived from version/date labels)")
p.add_argument("--body-start", choices=BODY_START_CHOICES, default="first-h2",
help="where the body begins: 'first-h2' skips everything before the first "
"'## ' line (OKI paper); 'after-rule' skips a '# / ## / ### / **meta**' "
"title block up to the first '---' rule (platform whitepaper r3.6), so "
"the cover comes from the labels above instead of the markdown title block")
return p.parse_args(argv)
def _header_footer(canvas, doc) -> None:
canvas.saveState()
page = canvas.getPageNumber()
if page == 1:
canvas.restoreState()
return
s = doc.mizoki_settings
canvas.setFillColor(NAVY)
canvas.rect(0, letter[1] - 36, letter[0], 36, fill=1, stroke=0)
canvas.setFillColor(GOLD)
canvas.rect(0, letter[1] - 38, letter[0], 2, fill=1, stroke=0)
canvas.setFillColor(white)
canvas.setFont("Times-Roman", 8)
canvas.drawString(0.85 * inch, letter[1] - 24, s["header_left"])
canvas.drawRightString(letter[0] - 0.85 * inch, letter[1] - 24, s["header_right"])
canvas.setFillColor(RULE)
canvas.rect(0, 0, letter[0], 32, fill=1, stroke=0)
canvas.setFillColor(NAVY)
canvas.setFont("Helvetica", 8)
canvas.drawString(0.85 * inch, 14, s["footer_note"])
canvas.drawRightString(letter[0] - 0.85 * inch, 14, str(page - 1))
canvas.restoreState()
def _cover(styles: dict[str, ParagraphStyle], story: list, s: dict) -> None:
# Drawn as a full-bleed first page via a spacer + overlay in onFirstPage.
story.append(Spacer(1, 1.6 * inch))
story.append(Paragraph(_inline(s["kicker"]), styles["cover_kicker"]))
story.append(Paragraph(_inline(s["title"]), styles["cover_title"]))
story.append(Spacer(1, 0.15 * inch))
story.append(Paragraph(_inline(s["subtitle"]), styles["cover_sub"]))
story.append(Spacer(1, 0.35 * inch))
story.append(
HRFlowable(width="40%", thickness=1.5, color=GOLD, spaceAfter=16, hAlign="LEFT")
)
for line in s["cover_lines"]:
story.append(Paragraph(_inline(line), styles["cover_meta"]))
story.append(PageBreak())
def _on_first_page(canvas, doc) -> None:
canvas.saveState()
canvas.setFillColor(NAVY)
canvas.rect(0, 0, letter[0], letter[1], fill=1, stroke=0)
canvas.setFillColor(GOLD)
canvas.rect(0, letter[1] - 8, letter[0], 8, fill=1, stroke=0)
canvas.rect(0, 0, letter[0], 8, fill=1, stroke=0)
canvas.setFillColor(HexColor("#132A44"))
canvas.rect(0, 2.1 * inch, letter[0], 0.02 * inch, fill=1, stroke=0)
canvas.setFillColor(GOLD)
canvas.setFont("Times-Italic", 9)
canvas.drawString(0.85 * inch, 0.55 * inch, doc.mizoki_settings["tagline"])
canvas.restoreState()
def _parse_table(lines: list[str], styles: dict[str, ParagraphStyle]):
rows = []
for line in lines:
cells = [c.strip() for c in line.strip().strip("|").split("|")]
if all(set(c) <= set("-: ") and c for c in cells):
continue
rows.append(cells)
if not rows:
return None
width = letter[0] - 1.7 * inch
n = len(rows[0])
col_w = [width / n] * n
# Status table: give first col more room
if n == 3:
col_w = [width * 0.34, width * 0.28, width * 0.38]
data = []
for i, row in enumerate(rows):
st = styles["table_head"] if i == 0 else styles["table_cell"]
data.append([Paragraph(_inline(c), st) for c in row])
tbl = Table(data, colWidths=col_w, repeatRows=1)
style_cmds = [
("BACKGROUND", (0, 0), (-1, 0), NAVY),
("TEXTCOLOR", (0, 0), (-1, 0), white),
("VALIGN", (0, 0), (-1, -1), "TOP"),
("LEFTPADDING", (0, 0), (-1, -1), 5),
("RIGHTPADDING", (0, 0), (-1, -1), 5),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("GRID", (0, 0), (-1, -1), 0.3, RULE),
]
for r in range(1, len(data)):
if r % 2 == 0:
style_cmds.append(("BACKGROUND", (0, r), (-1, r), ROW_ALT))
tbl.setStyle(TableStyle(style_cmds))
return tbl
def build(settings: dict | None = None) -> Path:
s = settings or build_settings(parse_args([]))
styles = _styles()
text = s["input"].read_text(encoding="utf-8")
# Drop the markdown title block; cover page carries it.
lines = text.splitlines()
story: list = []
_cover(styles, story, s)
i = 0
if s["body_start"] == "after-rule":
# skip the '# / ## / ### / **meta**' title block up to and including the first '---'
while i < len(lines) and lines[i].strip() != "---":
i += 1
i += 1
else:
# skip leading H1 and bold meta lines until first ##
while i < len(lines) and not lines[i].startswith("## "):
i += 1
para_buf: list[str] = []
bullets: list[tuple[int, str]] = []
table_buf: list[str] = []
quote_buf: list[str] = []
def flush_para() -> None:
nonlocal para_buf
if para_buf:
story.append(Paragraph(_inline(" ".join(para_buf)), styles["body"]))
para_buf = []
def flush_quote() -> None:
nonlocal quote_buf
if quote_buf:
story.append(Paragraph(_inline(" ".join(quote_buf)), styles["quote"]))
quote_buf = []
def flush_bullets() -> None:
nonlocal bullets
if not bullets:
return
items = []
for level, b in bullets:
if level == 0:
items.append(ListItem(Paragraph(_inline(b), styles["bullet"]),
leftIndent=12, bulletColor=GOLD))
else:
items.append(ListItem(Paragraph(_inline(b), styles["bullet_nested"]),
leftIndent=28, bulletColor=GOLD, value="–"))
story.append(
ListFlowable(
items,
bulletType="bullet",
start="•",
leftIndent=18,
bulletFontName="Times-Roman",
bulletFontSize=10,
spaceAfter=8,
)
)
bullets = []
def flush_table() -> None:
nonlocal table_buf
if table_buf:
tbl = _parse_table(table_buf, styles)
if tbl is not None:
story.append(tbl)
story.append(Spacer(1, 10))
table_buf = []
def flush_all() -> None:
flush_para()
flush_quote()
flush_bullets()
flush_table()
while i < len(lines):
raw = lines[i]
line = raw.rstrip()
i += 1
if line.startswith("|"):
flush_para()
flush_quote()
flush_bullets()
table_buf.append(line)
continue
else:
flush_table()
if not line:
flush_all()
continue
if line.strip() == "---":
flush_all()
continue
if line.startswith("## "):
flush_all()
title = line[3:].strip()
story.append(Paragraph(_inline(title), styles["h1"]))
rule = HRFlowable(width="100%", thickness=0.6, color=GOLD, spaceAfter=8, spaceBefore=0)
rule.keepWithNext = True
story.append(rule)
continue
if line.startswith("### "):
flush_all()
story.append(Paragraph(_inline(line[4:].strip()), styles["h2"]))
continue
if line.startswith("#### "):
# h4: a bold run-in paragraph (added 2026-09-02)
flush_all()
story.append(Paragraph("<b>" + _inline(line[5:].strip()) + "</b>", styles["body"]))
continue
if line.startswith(">"):
# blockquote: indented italic paragraph (added 2026-09-02)
flush_para()
flush_bullets()
quote_buf.append(line.lstrip(">").strip())
continue
stripped = line.lstrip(" ")
indent = len(line) - len(stripped)
if stripped.startswith("- "):
flush_para()
flush_quote()
flush_table()
bullets.append((1 if indent >= 2 else 0, stripped[2:].strip()))
continue
if re.match(r"^\d+\.\s", stripped):
flush_para()
flush_quote()
flush_table()
flush_bullets()
story.append(Paragraph(_inline(stripped), styles["body"]))
continue
para_buf.append(line.strip())
flush_all()
doc = SimpleDocTemplate(
str(s["output"]),
pagesize=letter,
leftMargin=0.85 * inch,
rightMargin=0.85 * inch,
topMargin=0.7 * inch,
bottomMargin=0.55 * inch,
title=s["doc_title"],
author=s["author"],
subject=s["subject"],
)
doc.mizoki_settings = s
doc.build(story, onFirstPage=_on_first_page, onLaterPages=_header_footer)
print(f"wrote {s['output']} ({s['output'].stat().st_size} bytes)")
return s["output"]
if __name__ == "__main__":
build(build_settings(parse_args()))