Source code for prescription.renderers.pdf

"""PDFRenderer — reportlab-based PDF output.

Kramer Harrison, 2026
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from optiland.prescription.renderers.base import BaseRenderer

if TYPE_CHECKING:
    import os

    from optiland.prescription.document import Document

import optiland as _optiland_pkg

_VERSION = getattr(_optiland_pkg, "__version__", "")

_MARGIN_H = 25 * 2.8346  # 25 mm in points
_MARGIN_V = 20 * 2.8346  # 20 mm in points
_HEADER_Y_OFFSET = 10  # points above top margin
_FOOTER_Y_OFFSET = 10  # points below bottom margin


[docs] class PDFRenderer(BaseRenderer): """Renders a Document to PDF using the 'reportlab' library.""" def __init__(self) -> None: try: from reportlab.lib import colors # noqa: F401 except ImportError as exc: raise ImportError( "PDFRenderer requires the 'reportlab' package. " "Install it with: pip install reportlab" ) from exc
[docs] def render(self, document: Document) -> str: """Not meaningful for PDF; returns a description string.""" return f"<PDFDocument: {document.title}>"
[docs] def write(self, document: Document, path: str | os.PathLike) -> None: from reportlab.lib.pagesizes import A4 from reportlab.platypus import SimpleDocTemplate path_str = str(path) page_w, _ = A4 content_w = page_w - 2 * _MARGIN_H doc = SimpleDocTemplate( path_str, pagesize=A4, leftMargin=_MARGIN_H, rightMargin=_MARGIN_H, topMargin=_MARGIN_V + 18, bottomMargin=_MARGIN_V + 18, ) story = self._build_story(document, content_w) _meta = {"title": document.title, "generated_at": document.generated_at} def _on_page(canvas: object, doc: object) -> None: # type: ignore[override] self._draw_header_footer(canvas, doc, _meta) # type: ignore[arg-type] doc.build(story, onFirstPage=_on_page, onLaterPages=_on_page)
def _build_story(self, document: Document, content_w: float) -> list: from reportlab.lib import colors from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet from reportlab.platypus import HRFlowable, Paragraph, Spacer styles = getSampleStyleSheet() title_style = ParagraphStyle( "PrescriptionTitle", parent=styles["Normal"], fontName="Helvetica-Bold", fontSize=16, leading=20, spaceAfter=6, ) heading_style = ParagraphStyle( "SectionHeading", parent=styles["Normal"], fontName="Helvetica-Bold", fontSize=11, leading=14, spaceBefore=10, spaceAfter=4, ) body_style = ParagraphStyle( "Body", parent=styles["Normal"], fontName="Helvetica", fontSize=9, ) italic_style = ParagraphStyle( "Italic", parent=styles["Normal"], fontName="Helvetica-Oblique", fontSize=8, textColor=colors.grey, ) story: list = [] story.append(Paragraph(document.title, title_style)) story.append(Paragraph(f"Generated: {document.generated_at}", body_style)) story.append(Spacer(1, 6)) story.append(HRFlowable(width="100%", color=colors.darkblue, thickness=1.5)) story.append(Spacer(1, 8)) for section in document.sections: story.append(Paragraph(section.title, heading_style)) story.append( HRFlowable(width="100%", color=colors.lightgrey, thickness=0.5) ) story.append(Spacer(1, 4)) for block in section.blocks: story.extend( self._render_block( block, content_w, body_style, italic_style, colors ) ) return story def _render_block( self, block: object, content_w: float, body_style: object, italic_style: object, colors: object, ) -> list: from reportlab.platypus import HRFlowable, Paragraph, Spacer from optiland.prescription.document import ( KeyValueBlock, SeparatorBlock, TableBlock, TextBlock, ) result: list = [] if isinstance(block, KeyValueBlock): result.append(self._kv_table(block, content_w)) result.append(Spacer(1, 4)) elif isinstance(block, TableBlock): result.append(self._data_table(block, content_w)) result.append(Spacer(1, 6)) elif isinstance(block, TextBlock): result.append(Paragraph(block.text, italic_style)) # type: ignore[arg-type] result.append(Spacer(1, 4)) elif isinstance(block, SeparatorBlock): result.append( HRFlowable(width="100%", color=colors.lightgrey) # type: ignore[union-attr] ) result.append(Spacer(1, 4)) return result @staticmethod def _kv_table(block: object, content_w: float) -> object: from reportlab.lib import colors from reportlab.lib.styles import ParagraphStyle from reportlab.platypus import Paragraph, Table, TableStyle from optiland.prescription.document import KeyValueBlock assert isinstance(block, KeyValueBlock) key_style = ParagraphStyle( "KVKey", fontName="Helvetica", fontSize=9, textColor=colors.grey ) val_style = ParagraphStyle("KVVal", fontName="Courier", fontSize=8) data = [ [Paragraph(k, key_style), Paragraph(v, val_style)] for k, v in block.rows ] col_w = [content_w * 0.45, content_w * 0.55] tbl = Table(data, colWidths=col_w) tbl.setStyle( TableStyle( [ ("VALIGN", (0, 0), (-1, -1), "TOP"), ("LEFTPADDING", (0, 0), (-1, -1), 4), ("RIGHTPADDING", (0, 0), (-1, -1), 4), ("TOPPADDING", (0, 0), (-1, -1), 2), ("BOTTOMPADDING", (0, 0), (-1, -1), 2), ] ) ) return tbl @staticmethod def _data_table(block: object, content_w: float) -> object: from reportlab.lib import colors from reportlab.lib.styles import ParagraphStyle from reportlab.platypus import Paragraph, Table, TableStyle from optiland.prescription.document import TableBlock assert isinstance(block, TableBlock) hdr_style = ParagraphStyle( "TblHdr", fontName="Helvetica-Bold", fontSize=8, textColor=colors.white, ) cell_style = ParagraphStyle("TblCell", fontName="Courier", fontSize=7) n_cols = len(block.headers) col_w = [content_w / n_cols] * n_cols data: list[list] = [[Paragraph(h, hdr_style) for h in block.headers]] for row in block.rows: data.append([Paragraph(cell, cell_style) for cell in row]) tbl = Table(data, colWidths=col_w, repeatRows=1) row_commands = [] for i in range(1, len(data)): if i % 2 == 0: row_commands.append(("BACKGROUND", (0, i), (-1, i), colors.whitesmoke)) tbl.setStyle( TableStyle( [ ("BACKGROUND", (0, 0), (-1, 0), colors.darkblue), ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), ("GRID", (0, 0), (-1, -1), 0.25, colors.lightgrey), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("LEFTPADDING", (0, 0), (-1, -1), 3), ("RIGHTPADDING", (0, 0), (-1, -1), 3), ("TOPPADDING", (0, 0), (-1, -1), 2), ("BOTTOMPADDING", (0, 0), (-1, -1), 2), *row_commands, ] ) ) return tbl @staticmethod def _draw_header_footer(canvas: object, doc: object, meta: dict) -> None: from reportlab.lib import colors from reportlab.lib.pagesizes import A4 c = canvas # type: ignore[assignment] page_w, page_h = A4 c.saveState() # type: ignore[union-attr] c.setFont("Helvetica", 7) # type: ignore[union-attr] c.setFillColor(colors.grey) # type: ignore[union-attr] header_y = page_h - _MARGIN_V + _HEADER_Y_OFFSET c.drawString(_MARGIN_H, header_y, meta["title"]) # type: ignore[union-attr] c.drawRightString( # type: ignore[union-attr] page_w - _MARGIN_H, header_y, f"Generated: {meta['generated_at']}", ) c.line( # type: ignore[union-attr] _MARGIN_H, header_y - 2, page_w - _MARGIN_H, header_y - 2 ) footer_y = _MARGIN_V - _FOOTER_Y_OFFSET c.drawString( # type: ignore[union-attr] _MARGIN_H, footer_y, f"Optiland v{_VERSION}" ) page_num = getattr(doc, "page", "?") # type: ignore[union-attr] c.drawRightString( # type: ignore[union-attr] page_w - _MARGIN_H, footer_y, f"Page {page_num}" ) c.line( # type: ignore[union-attr] _MARGIN_H, footer_y + 8, page_w - _MARGIN_H, footer_y + 8 ) c.restoreState() # type: ignore[union-attr]