Coverage for src / rendercv / renderer / pdf_png.py: 100%
71 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-25 13:49 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-25 13:49 +0000
1import atexit
2import functools
3import pathlib
4import shutil
5import tempfile
6import tomllib
8import rendercv_fonts
9import typst
11from rendercv.exception import RenderCVInternalError
12from rendercv.schema.models.rendercv_model import RenderCVModel
14from .path_resolver import resolve_rendercv_file_path
17def generate_pdf(
18 rendercv_model: RenderCVModel, typst_path: pathlib.Path | None
19) -> pathlib.Path | None:
20 """Compile Typst source to PDF using typst-py compiler.
22 Why:
23 PDF is the primary output format for CVs. Typst compilation produces
24 high-quality PDFs with proper fonts, layout, and typography from the
25 intermediate Typst markup.
27 Args:
28 rendercv_model: CV model for path resolution and photo handling.
29 typst_path: Path to Typst source file to compile.
31 Returns:
32 Path to generated PDF file, or None if generation disabled.
33 """
34 if rendercv_model.settings.render_command.dont_generate_pdf or typst_path is None:
35 return None
36 pdf_path = resolve_rendercv_file_path(
37 rendercv_model, rendercv_model.settings.render_command.pdf_path
38 )
39 typst_compiler = get_typst_compiler(
40 rendercv_model._input_file_path, typst_path.parent
41 )
42 copy_photo_next_to_typst_file(rendercv_model, typst_path)
43 typst_compiler.compile(input=typst_path, format="pdf", output=pdf_path)
45 return pdf_path
48def generate_png(
49 rendercv_model: RenderCVModel, typst_path: pathlib.Path | None
50) -> list[pathlib.Path] | None:
51 """Compile Typst source to PNG images using typst-py compiler.
53 Why:
54 PNG format enables CV preview in web applications and README files.
55 Multi-page CVs produce multiple PNG files with sequential numbering.
57 Args:
58 rendercv_model: CV model for path resolution and photo handling.
59 typst_path: Path to Typst source file to compile.
61 Returns:
62 List of paths to generated PNG files, or None if generation disabled.
63 """
64 if rendercv_model.settings.render_command.dont_generate_png or typst_path is None:
65 return None
66 png_path = resolve_rendercv_file_path(
67 rendercv_model, rendercv_model.settings.render_command.png_path
68 )
70 pattern = f"{png_path.stem}_*.png"
71 for existing_png_file in png_path.parent.glob(pattern):
72 if existing_png_file.is_file():
73 existing_png_file.unlink()
75 typst_compiler = get_typst_compiler(
76 rendercv_model._input_file_path, typst_path.parent
77 )
78 copy_photo_next_to_typst_file(rendercv_model, typst_path)
79 png_files_bytes = typst_compiler.compile(input=typst_path, format="png")
81 if not isinstance(png_files_bytes, list):
82 png_files_bytes = [png_files_bytes]
84 png_files = []
85 for i, png_file_bytes in enumerate(png_files_bytes):
86 if png_file_bytes is None:
87 raise RenderCVInternalError("Typst compiler returned None for PNG bytes")
88 png_file = png_path.parent / (png_path.stem + f"_{i + 1}.png")
89 png_file.write_bytes(png_file_bytes)
90 png_files.append(png_file)
92 return png_files if png_files else None
95def copy_photo_next_to_typst_file(
96 rendercv_model: RenderCVModel, typst_path: pathlib.Path
97) -> None:
98 """Copy CV photo to Typst file directory for compilation.
100 Why:
101 Typst compiler resolves image paths relative to source file location.
102 Copying photo ensures compilation succeeds regardless of original
103 photo location.
105 Args:
106 rendercv_model: CV model containing photo path.
107 typst_path: Path to Typst source file.
108 """
109 photo_path = rendercv_model.cv.photo
110 if isinstance(photo_path, pathlib.Path):
111 copy_to = typst_path.parent / photo_path.name
112 if photo_path != copy_to:
113 shutil.copy(photo_path, copy_to)
116def read_version_from_typst_toml(typst_toml_path: pathlib.Path) -> str:
117 """Read the version field from a typst.toml file.
119 Why:
120 Multiple bundled Typst packages need their version extracted for
121 directory layout. Centralizes the parsing logic.
123 Args:
124 typst_toml_path: Path to the typst.toml file.
126 Returns:
127 The version string.
128 """
129 data = tomllib.loads(typst_toml_path.read_text(encoding="utf-8"))
130 try:
131 return data["package"]["version"]
132 except KeyError as e:
133 message = f"Could not find version in {typst_toml_path}"
134 raise RenderCVInternalError(message) from e
137def install_bundled_typst_package(
138 bundled_path: pathlib.Path,
139 package_name: str,
140 temp_dir: pathlib.Path,
141 typ_files: list[str],
142) -> None:
143 """Copy a bundled Typst package into the temporary package cache.
145 Why:
146 The Typst compiler expects packages in a directory structure of
147 preview/{name}/{version}/. This copies the required files from
148 a bundled package into that layout.
150 Args:
151 bundled_path: Path to the bundled package directory.
152 package_name: Name of the Typst package (used in directory structure).
153 temp_dir: Root of the temporary package cache.
154 typ_files: List of .typ filenames to copy alongside typst.toml.
155 """
156 version = read_version_from_typst_toml(bundled_path / "typst.toml")
157 package_directory = temp_dir / "preview" / package_name / version
158 package_directory.mkdir(parents=True)
159 shutil.copy2(bundled_path / "typst.toml", package_directory / "typst.toml")
160 for typ_file in typ_files:
161 shutil.copy2(bundled_path / typ_file, package_directory / typ_file)
164@functools.lru_cache(maxsize=1)
165def get_package_path() -> pathlib.Path:
166 """Set up local Typst package resolution from bundled Typst packages.
168 Why:
169 Bundled Typst packages (rendercv, fontawesome) are shipped inside the
170 Python package so that PDF compilation works without downloading from
171 Typst Universe. The Typst compiler expects packages in a directory
172 structure of preview/{name}/{version}/, so this creates a temporary
173 directory with that layout.
175 Returns:
176 Path to temporary package cache directory.
177 """
178 temp_dir = pathlib.Path(tempfile.mkdtemp(prefix="rendercv-pkg-"))
179 atexit.register(shutil.rmtree, str(temp_dir), True)
181 renderer_dir = pathlib.Path(__file__).parent
183 install_bundled_typst_package(
184 bundled_path=renderer_dir / "rendercv_typst",
185 package_name="rendercv",
186 temp_dir=temp_dir,
187 typ_files=["lib.typ"],
188 )
190 install_bundled_typst_package(
191 bundled_path=renderer_dir / "typst_fontawesome",
192 package_name="fontawesome",
193 temp_dir=temp_dir,
194 typ_files=[
195 "lib.typ",
196 "lib-impl.typ",
197 "lib-gen-func.typ",
198 "lib-gen-map.typ",
199 ],
200 )
202 return temp_dir
205@functools.lru_cache(maxsize=1)
206def get_typst_compiler(
207 input_file_path: pathlib.Path | None,
208 root: pathlib.Path,
209) -> typst.Compiler:
210 """Create cached Typst compiler with font paths configured.
212 Why:
213 Compiler initialization is expensive. Caching enables reuse across
214 all compilations. The source file is passed per compile() call, so
215 the compiler survives output filename changes (e.g., when cv.name
216 changes). Font paths include package fonts and optional user fonts
217 from input file directory.
219 Args:
220 input_file_path: Original input file path for relative font resolution.
221 root: Root directory for Typst project. Must contain the input file.
223 Returns:
224 Configured Typst compiler instance.
225 """
226 return typst.Compiler(
227 root=root,
228 font_paths=[
229 *rendercv_fonts.paths_to_font_folders,
230 (
231 input_file_path.parent / "fonts"
232 if input_file_path
233 else pathlib.Path.cwd() / "fonts"
234 ),
235 ],
236 package_path=get_package_path(),
237 )