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

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.log_trace(self.pretty()) 

44 

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 

49 

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

65 

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 ) 

82 

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 ) 

88 

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

93 

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 

102 

103 template_context = prepare_template_context( 

104 self.config, 

105 self.config.images_dir, 

106 ) 

107 

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" 

111 

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) 

122 

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