"""Generation of the page stylesheet injected into the source document.""" from __future__ import annotations import re from ..models import PageNumbers, PageSettings NAMED_SIZE = re.compile(r"^[A-Za-z][A-Za-z0-9]*$") MARGIN_BOXES = { "top-left": "@top-left", "top-center": "@top-center", "top-right": "@top-right", "bottom-left": "@bottom-left", "bottom-center": "@bottom-center", "bottom-right": "@bottom-right", } PLACEHOLDER = re.compile(r"(\{page\}|\{pages\})") def page_size_value(page: PageSettings) -> str: fmt = page.format.strip() if NAMED_SIZE.match(fmt): return f"{fmt} {page.orientation}" # Explicit dimensions already carry the orientation. return fmt def margin_shorthand(page: PageSettings) -> str: m = page.margin return f"{m.top} {m.right} {m.bottom} {m.left}" def css_content_value(fmt: str, total_pages: int | None) -> str: """Turn "{page} / {pages}" into a CSS content value. When total_pages is known the total is written as a literal, otherwise the CSS counter(pages) is used. """ parts: list[str] = [] for token in PLACEHOLDER.split(fmt): if token == "{page}": parts.append("counter(page)") elif token == "{pages}": parts.append(str(total_pages) if total_pages is not None else "counter(pages)") elif token: escaped = token.replace("\\", "\\\\").replace('"', '\\"') parts.append(f'"{escaped}"') return " ".join(parts) if parts else '""' def build_page_css( page: PageSettings, page_numbers: PageNumbers | None = None, total_pages: int | None = None, outline: bool = True, ) -> str: """Stylesheet applied on top of the document styles.""" rules = [ "@page {", f" size: {page_size_value(page)};", f" margin: {margin_shorthand(page)};", ] if page_numbers is not None and page_numbers.enabled: box = MARGIN_BOXES[page_numbers.position] rules.append(f" {box} {{") rules.append(f" content: {css_content_value(page_numbers.format, total_pages)};") rules.append(" font-size: 9pt;") rules.append(" color: #444;") rules.append(" }") rules.append("}") if not outline: rules.append("h1, h2, h3, h4, h5, h6 { bookmark-level: none; }") return "\n".join(rules) def build_overlay_css( width_pt: float, height_pt: float, page: PageSettings, page_numbers: PageNumbers, total_pages: int, ) -> str: """Stylesheet for the transparent numbering layer merged onto the final PDF. The size comes from the produced PDF itself, so the overlay always matches even when the source document declares its own @page size. """ box = MARGIN_BOXES[page_numbers.position] reset = "" if page_numbers.start_at != 1: reset = f"body {{ counter-reset: page {page_numbers.start_at - 1}; }}\n" return ( f"@page {{\n" f" size: {width_pt:.2f}pt {height_pt:.2f}pt;\n" f" margin: {margin_shorthand(page)};\n" f" {box} {{\n" f" content: {css_content_value(page_numbers.format, total_pages)};\n" f" font-size: 9pt;\n" f" color: #444;\n" f" }}\n" f"}}\n" f"{reset}" f"body {{ margin: 0; }}\n" f".pdf-page-slot {{ height: 1px; break-after: page; }}\n" f".pdf-page-slot:last-child {{ break-after: auto; }}\n" )