Coverage for src/pdfbaker/page.py: 84%
61 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-20 04:55 +1200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-20 04:55 +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.log_trace(self.pretty())
45 self.templates_dir = self["directories"]["templates"]
46 self.images_dir = self["directories"]["images"]
47 self.build_dir = page.document.config.build_dir
48 self.dist_dir = page.document.config.dist_dir
50 if "template" not in self:
51 raise ConfigurationError(
52 f'Page "{self.name}" in document '
53 f'"{self.page.document.config.name}" has no template'
54 )
55 if isinstance(self["template"], dict) and "path" in self["template"]:
56 # Path was specified: relative to the config file
57 self.template = self.resolve_path(
58 self["template"]["path"], directory=self["directories"]["config"]
59 ).resolve()
60 else:
61 # Only name was specified: relative to the templates directory
62 self.template = self.resolve_path(
63 self["template"], directory=self.templates_dir
64 ).resolve()
66 def __init__(
67 self,
68 document: "PDFBakerDocument", # type: ignore # noqa: F821
69 page_number: int,
70 base_config: dict[str, Any],
71 config_path: Path | dict[str, Any],
72 ) -> None:
73 """Initialize a page."""
74 super().__init__()
75 self.document = document
76 self.number = page_number
77 self.config = self.Configuration(
78 page=self,
79 base_config=base_config,
80 config_path=config_path,
81 )
83 def process(self) -> Path:
84 """Render SVG template and convert to PDF."""
85 self.log_debug_subsection(
86 "Processing page %d: %s", self.number, self.config.name
87 )
89 self.log_debug("Loading template: %s", self.config.template)
90 if self.logger.isEnabledFor(TRACE):
91 with open(self.config.template, encoding="utf-8") as f:
92 self.log_trace_preview(f.read())
94 try:
95 jinja_env = create_env(self.config.template.parent)
96 template = jinja_env.get_template(self.config.template.name)
97 except TemplateNotFound as exc:
98 raise SVGTemplateError(
99 "Failed to load template for page "
100 f"{self.number} ({self.config.name}): {exc}"
101 ) from exc
103 template_context = prepare_template_context(
104 self.config,
105 self.config.images_dir,
106 )
108 self.config.build_dir.mkdir(parents=True, exist_ok=True)
109 output_svg = self.config.build_dir / f"{self.config.name}_{self.number:03}.svg"
110 output_pdf = self.config.build_dir / f"{self.config.name}_{self.number:03}.pdf"
112 self.log_debug("Rendering template...")
113 try:
114 rendered_template = template.render(**template_context)
115 with open(output_svg, "w", encoding="utf-8") as f:
116 f.write(rendered_template)
117 except TemplateError as exc:
118 raise SVGTemplateError(
119 f"Failed to render page {self.number} ({self.config.name}): {exc}"
120 ) from exc
121 self.log_trace_preview(rendered_template)
123 self.log_debug("Converting SVG to PDF: %s", output_svg)
124 svg2pdf_backend = self.config.get("svg2pdf_backend", "cairosvg")
125 try:
126 return convert_svg_to_pdf(
127 output_svg,
128 output_pdf,
129 backend=svg2pdf_backend,
130 )
131 except SVGConversionError as exc:
132 self.log_error(
133 "Failed to convert page %d (%s): %s",
134 self.number,
135 self.config.name,
136 exc,
137 )
138 raise