Coverage for src/pdfbaker/page.py: 84%
62 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-20 14:42 +1200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-20 14:42 +1200
1"""PDFBakerPage class.
3Individual page rendering and PDF conversion.
5Renders its SVG template with a fully merged configuration,
6converts the result to PDF and returns the path of the new PDF file.
7"""
9from pathlib import Path
10from typing import Any
12from jinja2.exceptions import TemplateError, TemplateNotFound
14from .config import PDFBakerConfiguration
15from .errors import ConfigurationError, SVGConversionError, SVGTemplateError
16from .logging import TRACE, LoggingMixin
17from .pdf import convert_svg_to_pdf
18from .render import create_env, prepare_template_context
20__all__ = ["PDFBakerPage"]
23# pylint: disable=too-few-public-methods
24class PDFBakerPage(LoggingMixin):
25 """A single page of a document."""
27 class Configuration(PDFBakerConfiguration):
28 """PDFBakerPage configuration."""
30 def __init__(
31 self,
32 page: "PDFBakerPage",
33 base_config: dict[str, Any],
34 config_path: Path,
35 ) -> None:
36 """Initialize page configuration (needs a template)."""
37 self.page = page
39 self.name = config_path.stem
41 self.page.log_trace_section("Loading page configuration: %s", config_path)
42 super().__init__(base_config, config_path)
43 self["page_number"] = page.number
44 self.page.log_trace(self.pretty())
46 self.templates_dir = self["directories"]["templates"]
47 self.images_dir = self["directories"]["images"]
48 self.build_dir = page.document.config.build_dir
49 self.dist_dir = page.document.config.dist_dir
51 if "template" not in self:
52 raise ConfigurationError(
53 f'Page "{self.name}" in document '
54 f'"{self.page.document.config.name}" has no template'
55 )
56 if isinstance(self["template"], dict) and "path" in self["template"]:
57 # Path was specified: relative to the config file
58 self.template = self.resolve_path(
59 self["template"]["path"], directory=self["directories"]["config"]
60 ).resolve()
61 else:
62 # Only name was specified: relative to the templates directory
63 self.template = self.resolve_path(
64 self["template"], directory=self.templates_dir
65 ).resolve()
67 def __init__(
68 self,
69 document: "PDFBakerDocument", # type: ignore # noqa: F821
70 page_number: int,
71 base_config: dict[str, Any],
72 config_path: Path | dict[str, Any],
73 ) -> None:
74 """Initialize a page."""
75 super().__init__()
76 self.document = document
77 self.number = page_number
78 self.config = self.Configuration(
79 page=self,
80 base_config=base_config,
81 config_path=config_path,
82 )
84 def process(self) -> Path:
85 """Render SVG template and convert to PDF."""
86 self.log_debug_subsection(
87 "Processing page %d: %s", self.number, self.config.name
88 )
90 self.log_debug("Loading template: %s", self.config.template)
91 if self.logger.isEnabledFor(TRACE):
92 with open(self.config.template, encoding="utf-8") as f:
93 self.log_trace_preview(f.read())
95 try:
96 jinja_env = create_env(self.config.template.parent)
97 template = jinja_env.get_template(self.config.template.name)
98 except TemplateNotFound as exc:
99 raise SVGTemplateError(
100 "Failed to load template for page "
101 f"{self.number} ({self.config.name}): {exc}"
102 ) from exc
104 template_context = prepare_template_context(
105 self.config,
106 self.config.images_dir,
107 )
109 self.config.build_dir.mkdir(parents=True, exist_ok=True)
110 output_svg = self.config.build_dir / f"{self.config.name}_{self.number:03}.svg"
111 output_pdf = self.config.build_dir / f"{self.config.name}_{self.number:03}.pdf"
113 self.log_debug("Rendering template...")
114 try:
115 rendered_template = template.render(**template_context)
116 with open(output_svg, "w", encoding="utf-8") as f:
117 f.write(rendered_template)
118 except TemplateError as exc:
119 raise SVGTemplateError(
120 f"Failed to render page {self.number} ({self.config.name}): {exc}"
121 ) from exc
122 self.log_trace_preview(rendered_template)
124 self.log_debug("Converting SVG to PDF: %s", output_svg)
125 svg2pdf_backend = self.config.get("svg2pdf_backend", "cairosvg")
126 try:
127 return convert_svg_to_pdf(
128 output_svg,
129 output_pdf,
130 backend=svg2pdf_backend,
131 )
132 except SVGConversionError as exc:
133 self.log_error(
134 "Failed to convert page %d (%s): %s",
135 self.number,
136 self.config.name,
137 exc,
138 )
139 raise