"""WeasyPrint engine. Default engine for documents we generate ourselves. It implements CSS Paged Media properly, which is what makes counters, running headers and repeated table headers work, and it uses far less memory than a headless browser. It does not execute JavaScript. That is intentional. """ from __future__ import annotations import asyncio import logging from pathlib import Path from ..models import ConvertRequest from ..pdf.styles import build_page_css from .base import ChunkRender, RenderEngine logger = logging.getLogger(__name__) class WeasyPrintEngine(RenderEngine): name = "weasyprint" @property def supports_anchor_pages(self) -> bool: return True async def available(self) -> bool: try: import weasyprint # noqa: F401 except Exception as exc: # noqa: BLE001 - reported, never silent logger.error("WeasyPrint is not importable", exc_info=exc) return False return True async def render_chunk( self, html: str, base_url: str | None, request: ConvertRequest, output_path: Path, total_pages: int | None = None, asset_gate=None, ) -> ChunkRender: return await asyncio.to_thread( self._render_blocking, html, base_url, request, output_path, total_pages, asset_gate ) def _render_blocking( self, html: str, base_url: str | None, request: ConvertRequest, output_path: Path, total_pages: int | None, asset_gate, ) -> ChunkRender: from weasyprint import CSS, HTML kwargs = {"string": html, "base_url": base_url} if asset_gate is not None: kwargs["url_fetcher"] = asset_gate.weasy_fetcher() stylesheets = [] if total_pages is not None: # Only used when the page total is known up front, which happens on # the second pass of an unchunked document. stylesheets.append( CSS( string=build_page_css( request.page, request.page_numbers, total_pages=total_pages, outline=request.outline, ) ) ) document = HTML(**kwargs).render(stylesheets=stylesheets or None) write_kwargs = {} if request.pdf_profile: write_kwargs["pdf_variant"] = request.pdf_profile document.write_pdf(target=str(output_path), **write_kwargs) return ChunkRender( path=output_path, page_count=len(document.pages), anchor_pages=self._anchor_pages(document), ) @staticmethod def _anchor_pages(document) -> dict[str, int]: """Map anchor name to the zero based page index it landed on.""" anchors: dict[str, int] = {} for index, page in enumerate(document.pages): page_anchors = getattr(page, "anchors", None) if not page_anchors: continue for name in page_anchors: anchors.setdefault(name, index) if not anchors: logger.warning("WeasyPrint returned no anchors, table of contents page numbers may be missing") return anchors