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

1"""PDFBakerPage class. 

2 

3Individual page rendering and PDF conversion. 

4 

5Renders its SVG template with a fully merged configuration, 

6converts the result to PDF and returns the path of the new PDF file. 

7""" 

8 

9from pathlib import Path 

10from typing import Any 

11 

12from jinja2.exceptions import TemplateError, TemplateNotFound 

13 

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 

19 

20__all__ = ["PDFBakerPage"] 

21 

22 

23# pylint: disable=too-few-public-methods 

24class PDFBakerPage(LoggingMixin): 

25 """A single page of a document.""" 

26 

27 class Configuration(PDFBakerConfiguration): 

28 """PDFBakerPage configuration.""" 

29 

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 

38 

39 self.name = config_path.stem 

40 

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()) 

45 

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 

50 

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() 

66 

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 ) 

83 

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 ) 

89 

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()) 

94 

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 

103 

104 template_context = prepare_template_context( 

105 self.config, 

106 self.config.images_dir, 

107 ) 

108 

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" 

112 

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) 

123 

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