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

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""" 

5 

6import importlib.resources 

7import pathlib 

8import re 

9import shutil 

10import subprocess 

11import sys 

12from typing import Optional 

13 

14import fitz 

15import markdown 

16 

17from .. import data 

18from . import templater 

19 

20 

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. 

29 

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 

42 

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 ) 

48 

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 = "" 

57 

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 ) 

69 

70 

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. 

76 

77 Args: 

78 rendercv_data_model (dm.RenderCVDataModel): The data model. 

79 output_directory (pathlib.Path): Path to the output directory. 

80 

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) 

87 

88 jinja2_environment = templater.setup_jinja2_environment() 

89 latex_file_object = templater.LaTeXFile( 

90 rendercv_data_model, 

91 jinja2_environment, 

92 ) 

93 

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) 

97 

98 return latex_file_path 

99 

100 

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. 

106 

107 Args: 

108 rendercv_data_model (dm.RenderCVDataModel): The data model. 

109 output_directory (pathlib.Path): Path to the output directory. 

110 

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) 

117 

118 jinja2_environment = templater.setup_jinja2_environment() 

119 markdown_file_object = templater.MarkdownFile( 

120 rendercv_data_model, 

121 jinja2_environment, 

122 ) 

123 

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) 

127 

128 return markdown_file_path 

129 

130 

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. 

136 

137 Args: 

138 rendercv_data_model (dm.RenderCVDataModel): The data model. 

139 output_directory (pathlib.Path): Path to the output directory. 

140 

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 

149 

150 

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. 

155 

156 Args: 

157 latex_file_path (str): The path to the $\\LaTeX$ file. 

158 

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!") 

165 

166 if local_latex_command: 

167 executable = local_latex_command 

168 

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 ) 

186 

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 } 

192 

193 if sys.platform not in executables: 

194 raise OSError(f"TinyTeX doesn't support the platform {sys.platform}!") 

195 

196 executable = executables[sys.platform] 

197 

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 ) 

205 

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 ) 

218 

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

268 

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 ) 

278 

279 return pdf_file_path 

280 

281 

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. 

284 

285 Args: 

286 pdf_file_path (pathlib.Path): The path to the PDF file. 

287 

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!") 

294 

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) 

305 

306 return png_files 

307 

308 

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. 

312 

313 Args: 

314 markdown_file_path (pathlib.Path): The path to the Markdown file. 

315 

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!") 

322 

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) 

326 

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) 

333 

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) 

337 

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

341 

342 return html_file_path