Coverage for rendercv/renderer/renderer.py: 89%
113 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-07 17:51 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-07 17:51 +0000
1"""
2The `rendercv.renderer.renderer` module contains the necessary functions for rendering
3$\\LaTeX$, PDF, Markdown, HTML, and PNG files from the `RenderCVDataModel` object.
4"""
6import importlib.resources
7import pathlib
8import re
9import shutil
10import subprocess
11import sys
12from typing import Optional
14import fitz
15import markdown
17from .. import data
18from . import templater
21def copy_theme_files_to_output_directory(
22 theme_name: str,
23 output_directory_path: pathlib.Path,
24):
25 """Copy the auxiliary files (all the files that don't end with `.j2.tex` and `.py`)
26 of the theme to the output directory. For example, a theme can have custom
27 fonts, and the $\\LaTeX$ needs it. If the theme is a custom theme, then it will be
28 copied from the current working directory.
30 Args:
31 theme_name (str): The name of the theme.
32 output_directory_path (pathlib.Path): Path to the output directory.
33 """
34 if theme_name in data.available_themes:
35 theme_directory_path = importlib.resources.files(
36 f"rendercv.themes.{theme_name}"
37 )
38 else:
39 # Then it means the theme is a custom theme. If theme_directory is not given
40 # as an argument, then look for the theme in the current working directory.
41 theme_directory_path = pathlib.Path.cwd() / theme_name
43 if not theme_directory_path.is_dir():
44 raise FileNotFoundError(
45 f"The theme {theme_name} doesn't exist in the current working"
46 " directory!"
47 )
49 for theme_file in theme_directory_path.iterdir():
50 dont_copy_files_with_these_extensions = [".j2.tex", ".py"]
51 # theme_file.suffix returns the latest part of the file name after the last dot.
52 # But we need the latest part of the file name after the first dot:
53 try:
54 suffix = re.search(r"\..*", theme_file.name)[0] # type: ignore
55 except TypeError:
56 suffix = ""
58 if suffix not in dont_copy_files_with_these_extensions:
59 if theme_file.is_dir():
60 shutil.copytree(
61 str(theme_file),
62 output_directory_path / theme_file.name,
63 dirs_exist_ok=True,
64 )
65 else:
66 shutil.copyfile(
67 str(theme_file), output_directory_path / theme_file.name
68 )
71def create_a_latex_file(
72 rendercv_data_model: data.RenderCVDataModel, output_directory: pathlib.Path
73) -> pathlib.Path:
74 """Render the $\\LaTeX$ file with the given data model and write it to the output
75 directory.
77 Args:
78 rendercv_data_model (dm.RenderCVDataModel): The data model.
79 output_directory (pathlib.Path): Path to the output directory.
81 Returns:
82 pathlib.Path: The path to the generated $\\LaTeX$ file.
83 """
84 # create output directory if it doesn't exist:
85 if not output_directory.is_dir():
86 output_directory.mkdir(parents=True)
88 jinja2_environment = templater.setup_jinja2_environment()
89 latex_file_object = templater.LaTeXFile(
90 rendercv_data_model,
91 jinja2_environment,
92 )
94 latex_file_name = f"{str(rendercv_data_model.cv.name).replace(' ', '_')}_CV.tex"
95 latex_file_path = output_directory / latex_file_name
96 latex_file_object.create_file(latex_file_path)
98 return latex_file_path
101def create_a_markdown_file(
102 rendercv_data_model: data.RenderCVDataModel, output_directory: pathlib.Path
103) -> pathlib.Path:
104 """Render the Markdown file with the given data model and write it to the output
105 directory.
107 Args:
108 rendercv_data_model (dm.RenderCVDataModel): The data model.
109 output_directory (pathlib.Path): Path to the output directory.
111 Returns:
112 pathlib.Path: The path to the rendered Markdown file.
113 """
114 # create output directory if it doesn't exist:
115 if not output_directory.is_dir():
116 output_directory.mkdir(parents=True)
118 jinja2_environment = templater.setup_jinja2_environment()
119 markdown_file_object = templater.MarkdownFile(
120 rendercv_data_model,
121 jinja2_environment,
122 )
124 markdown_file_name = f"{str(rendercv_data_model.cv.name).replace(' ', '_')}_CV.md"
125 markdown_file_path = output_directory / markdown_file_name
126 markdown_file_object.create_file(markdown_file_path)
128 return markdown_file_path
131def create_a_latex_file_and_copy_theme_files(
132 rendercv_data_model: data.RenderCVDataModel, output_directory: pathlib.Path
133) -> pathlib.Path:
134 """Render the $\\LaTeX$ file with the given data model in the output directory and
135 copy the auxiliary theme files to the output directory.
137 Args:
138 rendercv_data_model (dm.RenderCVDataModel): The data model.
139 output_directory (pathlib.Path): Path to the output directory.
141 Returns:
142 pathlib.Path: The path to the rendered $\\LaTeX$ file.
143 """
144 latex_file_path = create_a_latex_file(rendercv_data_model, output_directory)
145 copy_theme_files_to_output_directory(
146 rendercv_data_model.design.theme, output_directory
147 )
148 return latex_file_path
151def render_a_pdf_from_latex(
152 latex_file_path: pathlib.Path, local_latex_command: Optional[str] = None
153) -> pathlib.Path:
154 """Run TinyTeX with the given $\\LaTeX$ file to render the PDF.
156 Args:
157 latex_file_path (str): The path to the $\\LaTeX$ file.
159 Returns:
160 pathlib.Path: The path to the rendered PDF file.
161 """
162 # check if the file exists:
163 if not latex_file_path.is_file():
164 raise FileNotFoundError(f"The file {latex_file_path} doesn't exist!")
166 if local_latex_command:
167 executable = local_latex_command
169 # check if the command is working:
170 try:
171 subprocess.run(
172 [executable, "--version"],
173 stdout=subprocess.DEVNULL, # don't capture the output
174 stderr=subprocess.DEVNULL, # don't capture the error
175 )
176 except FileNotFoundError:
177 raise FileNotFoundError(
178 f"[blue]{executable}[/blue] isn't installed! Please install LaTeX and"
179 " try again (or don't use the"
180 " [bright_black]--use-local-latex-command[/bright_black] option)."
181 )
182 else:
183 tinytex_binaries_directory = (
184 pathlib.Path(__file__).parent / "tinytex-release" / "TinyTeX" / "bin"
185 )
187 executables = {
188 "win32": tinytex_binaries_directory / "windows" / "pdflatex.exe",
189 "linux": tinytex_binaries_directory / "x86_64-linux" / "pdflatex",
190 "darwin": tinytex_binaries_directory / "universal-darwin" / "pdflatex",
191 }
193 if sys.platform not in executables:
194 raise OSError(f"TinyTeX doesn't support the platform {sys.platform}!")
196 executable = executables[sys.platform]
198 # check if the executable exists:
199 if not executable.is_file():
200 raise FileNotFoundError(
201 f"The TinyTeX executable ({executable}) doesn't exist! If you are"
202 " cloning the repository, make sure to clone it recursively to get the"
203 " TinyTeX binaries. See the developer guide for more information."
204 )
206 # Before running LaTeX, make sure the PDF file is not open in another program,
207 # that wouldn't allow LaTeX to write to it. Remove the PDF file if it exists,
208 # if it's not removable, then raise an error:
209 pdf_file_path = latex_file_path.with_suffix(".pdf")
210 if pdf_file_path.is_file():
211 try:
212 pdf_file_path.unlink()
213 except PermissionError:
214 raise RuntimeError(
215 f"The PDF file {pdf_file_path} is open in another program and doesn't"
216 " allow RenderCV to rewrite it. Please close the PDF file."
217 )
219 # Run LaTeX to render the PDF:
220 command = [
221 executable,
222 str(latex_file_path.absolute()),
223 ]
224 with subprocess.Popen(
225 command,
226 cwd=latex_file_path.parent,
227 stdout=subprocess.PIPE, # capture the output
228 stderr=subprocess.DEVNULL, # don't capture the error
229 stdin=subprocess.DEVNULL, # don't allow LaTeX to ask for user input
230 ) as latex_process:
231 output = latex_process.communicate() # wait for the process to finish
232 if latex_process.returncode != 0:
233 if local_latex_command:
234 raise RuntimeError(
235 f"The local LaTeX command {local_latex_command} couldn't render"
236 " this LaTeX file into a PDF. Check out the log file"
237 f" {latex_file_path.with_suffix('.log')} in the output directory"
238 " for more information."
239 )
240 else:
241 raise RuntimeError(
242 "RenderCV's built-in TinyTeX binaries couldn't render this LaTeX"
243 " file into a PDF. This could be caused by one of two"
244 " reasons:\n\n1- The theme templates might have been updated in a"
245 " way RenderCV's TinyTeX cannot render. RenderCV's TinyTeX is"
246 " minified to keep the package size small. As a result, it doesn't"
247 " function like a general-purpose LaTeX distribution.\n2- Special"
248 " characters, like Greek or Chinese letters, that are not"
249 " compatible with the fonts used or RenderCV's TinyTeX might have"
250 " been used.\n\nHowever, this issue can be resolved by using your"
251 " own LaTeX distribution instead of the built-in TinyTeX. This can"
252 " be done with the '--use-local-latex-command' option, as shown"
253 " below:\n\nrendercv render --use-local-latex-command lualatex"
254 " John_Doe_CV.yaml\n\nIf you ensure that the generated LaTeX file"
255 " can be compiled by your local LaTeX distribution, RenderCV will"
256 " work successfully. You can debug the generated LaTeX file in"
257 " your LaTeX editor to resolve any bugs. Then, you can start using"
258 " RenderCV with your local LaTeX distribution.\n\nIf you can't"
259 " solve the problem, please open an issue on GitHub. Also, to see"
260 " the error, check out the log file"
261 f" {latex_file_path.with_suffix('.log')} in the output directory."
262 )
263 else:
264 try:
265 output = output[0].decode("utf-8")
266 except UnicodeDecodeError:
267 output = output[0].decode("latin-1")
269 if "Rerun to get" in output:
270 # Run TinyTeX again to get the references right:
271 subprocess.run(
272 command,
273 cwd=latex_file_path.parent,
274 stdout=subprocess.DEVNULL, # don't capture the output
275 stderr=subprocess.DEVNULL, # don't capture the error
276 stdin=subprocess.DEVNULL, # don't allow TinyTeX to ask for user input
277 )
279 return pdf_file_path
282def render_pngs_from_pdf(pdf_file_path: pathlib.Path) -> list[pathlib.Path]:
283 """Render a PNG file for each page of the given PDF file.
285 Args:
286 pdf_file_path (pathlib.Path): The path to the PDF file.
288 Returns:
289 list[pathlib.Path]: The paths to the rendered PNG files.
290 """
291 # check if the file exists:
292 if not pdf_file_path.is_file():
293 raise FileNotFoundError(f"The file {pdf_file_path} doesn't exist!")
295 # convert the PDF to PNG:
296 png_directory = pdf_file_path.parent
297 png_file_name = pdf_file_path.stem
298 png_files = []
299 pdf = fitz.open(pdf_file_path) # open the PDF file
300 for page in pdf: # iterate the pages
301 image = page.get_pixmap(dpi=300) # type: ignore
302 png_file_path = png_directory / f"{png_file_name}_{page.number+1}.png" # type: ignore
303 image.save(png_file_path)
304 png_files.append(png_file_path)
306 return png_files
309def render_an_html_from_markdown(markdown_file_path: pathlib.Path) -> pathlib.Path:
310 """Render an HTML file from a Markdown file with the same name and in the same
311 directory. It uses `rendercv/themes/main.j2.html` as the Jinja2 template.
313 Args:
314 markdown_file_path (pathlib.Path): The path to the Markdown file.
316 Returns:
317 pathlib.Path: The path to the rendered HTML file.
318 """
319 # check if the file exists:
320 if not markdown_file_path.is_file():
321 raise FileNotFoundError(f"The file {markdown_file_path} doesn't exist!")
323 # Convert the markdown file to HTML:
324 markdown_text = markdown_file_path.read_text(encoding="utf-8")
325 html_body = markdown.markdown(markdown_text)
327 # Get the title of the markdown content:
328 title = re.search(r"# (.*)\n", markdown_text)
329 if title is None:
330 title = ""
331 else:
332 title = title.group(1)
334 jinja2_environment = templater.setup_jinja2_environment()
335 html_template = jinja2_environment.get_template("main.j2.html")
336 html = html_template.render(html_body=html_body, title=title)
338 # Write html into a file:
339 html_file_path = markdown_file_path.parent / f"{markdown_file_path.stem}.html"
340 html_file_path.write_text(html, encoding="utf-8")
342 return html_file_path