Skip to content

Commit 88ce674

Browse files
committed
Renamed build_ebook_v2.py -> build_ebook.py
1 parent 586758e commit 88ce674

File tree

2 files changed

+274
-386
lines changed

2 files changed

+274
-386
lines changed

build_ebook.py

Lines changed: 274 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,309 @@
1-
import subprocess
1+
#!/usr/bin/env python3
2+
3+
"""Generates epub and pdf from sources."""
4+
25
import datetime
3-
import os
6+
import pathlib
47
import re
8+
import shutil
9+
import subprocess
10+
11+
12+
class VTLogger:
13+
"""A logger"""
14+
def __init__(self, filename:str, log_to_file:bool=True) -> None:
15+
if log_to_file is True:
16+
self.log_file = open(filename, "w", encoding="utf-8")
17+
18+
def __del__(self) -> None:
19+
if self.log_file is not None:
20+
self.log_file.close()
21+
22+
def detail(self, message: str) -> None:
23+
"""Logs a detail message without printing it."""
24+
self._log(message, True)
25+
26+
def error(self, message: str) -> None:
27+
"""Logs an error."""
28+
message = f"Error: {message}"
29+
self._log(message)
30+
31+
def info(self, message: str) -> None:
32+
"""Logs an info message."""
33+
print(message)
34+
self._log(f"Info: {message}", True)
35+
36+
def warning(self, message: str) -> None:
37+
"""Logs an warning."""
38+
message = f"Warning: {message}"
39+
self._log(message)
40+
41+
def _log(self, message: str, no_print:bool=False) -> None:
42+
if no_print is False:
43+
print(message)
44+
45+
if self.log_file is not None:
46+
self.log_file.write(f"{message}\n")
47+
48+
class VTEMarkdownFile:
49+
"""Markdown file."""
50+
51+
def __init__(self, content: str, depth: int, prefix: str, title: str) -> None:
52+
self.content: str = content
53+
self.depth: str = depth
54+
self.prefix: str = prefix
55+
self.title: str = title
56+
57+
def __repr__(self) -> str:
58+
return (
59+
f"<VTEMarkdownFile depth: {self.depth}, prefix: '{self.prefix}',"
60+
f" title: '{self.title}', content: '{self.content}'>"
61+
)
62+
63+
def __str__(self) -> str:
64+
return (
65+
f"<VTEMarkdownFile depth: {self.depth}, prefix: '{self.prefix}',"
66+
f" title: '{self.title}', content: '{self.content}'>"
67+
)
68+
69+
class VTEBookBuilder:
70+
"""A 'Markdown' to 'epub' and 'pdf' converter."""
71+
72+
def __init__(self, logger: VTLogger) -> None:
73+
self.log = logger
74+
75+
def build_pdf_book(self, language: str, markdown_filepath: pathlib.Path) -> None:
76+
"""Builds a pdf file"""
77+
78+
self.log.info("Building 'pdf'...")
79+
80+
try:
81+
subprocess.check_output(
82+
[
83+
"xelatex",
84+
"--version",
85+
]
86+
)
87+
except (FileNotFoundError, subprocess.CalledProcessError) as error:
88+
log.error(error)
89+
self.log.warning("Please, install 'xelatex'!")
90+
91+
raise RuntimeError from error
92+
93+
try:
94+
subprocess.check_output(
95+
[
96+
"pandoc",
97+
markdown_filepath.as_posix(),
98+
"-V", "documentclass=report",
99+
"-t", "latex",
100+
"-s",
101+
"--toc",
102+
"--listings",
103+
"-H", "./ebook/listings-setup.tex",
104+
"-o", f"./ebook/Vulkan Tutorial {language}.pdf",
105+
"--pdf-engine=xelatex",
106+
]
107+
)
108+
except subprocess.CalledProcessError as error:
109+
log.error(error)
110+
self.log.warning("'pandoc' process failed!")
111+
112+
raise RuntimeError from error
113+
114+
def build_epub_book(self, language: str, markdown_filepath: pathlib.Path) -> None:
115+
"""Buids a epub file"""
116+
117+
self.log.info("Building 'epub'...")
118+
119+
try:
120+
subprocess.check_output(
121+
[
122+
"pandoc",
123+
markdown_filepath.as_posix(),
124+
"--toc",
125+
"-o", f"./ebook/Vulkan Tutorial {language}.epub",
126+
"--epub-cover-image=ebook/cover.png",
127+
]
128+
)
129+
except subprocess.CalledProcessError as error:
130+
log.error(error)
131+
self.log.warning("'pandoc' process failed!")
132+
133+
raise RuntimeError from error
134+
135+
def convert_svg_to_png(self, images_folder: str) -> list[pathlib.Path]:
136+
"""Converts *.svg images to *.png using Inkscape"""
137+
138+
self.log.info("Converting 'svg' images...")
139+
140+
pngs = list[pathlib.Path]()
141+
142+
for entry in pathlib.Path(images_folder).iterdir():
143+
if entry.suffix == ".svg":
144+
new_path = entry.with_suffix(".png")
145+
146+
try:
147+
subprocess.check_output(
148+
[
149+
"inkscape",
150+
f"--export-filename={new_path.as_posix()}",
151+
entry.as_posix()
152+
],
153+
stderr=subprocess.STDOUT
154+
)
155+
156+
pngs.append(new_path)
157+
except FileNotFoundError as error:
158+
self.log.error(error)
159+
self.log.warning("Install 'Inkscape' (https://www.inkscape.org)!")
160+
161+
raise RuntimeError from error
162+
163+
return pngs
164+
165+
def generate_joined_markdown(self, language: str, output_filename: pathlib.Path) -> None:
166+
"""Processes the markdown sources and produces a joined file."""
167+
168+
self.log.info(
169+
f"Generating a temporary 'Markdown' file: '{output_filename}'" \
170+
f" for language '{language}'..."
171+
)
172+
173+
md_files = self._collect_markdown_files_from_source(language)
174+
md_files = sorted(md_files, key=lambda file: file.prefix)
175+
176+
# Add title.
177+
current_date: str = datetime.datetime.now().strftime('%B %Y')
178+
179+
temp_markdown: str = (
180+
"% Vulkan Tutorial\n"
181+
"% Alexander Overvoorde\n"
182+
f"% {current_date} \n\n"
183+
)
184+
185+
def repl_hash(match):
186+
"""Calculates the proper `Markdown` heading depth (#)."""
187+
original_prefix = match.group(1)
188+
additional_prefix: str = "#" * entry.depth
189+
190+
return f"{original_prefix}{additional_prefix} "
191+
192+
for entry in md_files:
193+
# Add chapter title.
194+
content: str = f"# {entry.title}\n\n{entry.content}"
195+
196+
# Fix depth.
197+
if entry.depth > 0:
198+
content = re.sub(r"(#+) ", repl_hash, content)
5199

200+
# Fix image links.
201+
content = re.sub(r"\/images\/", "images/", content)
202+
content = re.sub(r"\.svg", ".png", content)
6203

7-
def create_ebook(path):
204+
# Fix remaining relative links (e.g. code files).
205+
content = re.sub(r"\]\(\/", "](https://vulkan-tutorial.com/", content)
8206

9-
name_path = path
10-
print('\n Creating \"' + name_path + '\" ebook')
11-
# Recursively gather all markdown files in the right order
12-
markdownFiles = []
207+
# Fix chapter references.
208+
def repl(match):
209+
target = match.group(1)
210+
target = target.lower()
211+
target = re.sub("_", "-", target)
212+
target = target.split("/")[-1]
13213

14-
for root, subdirs, files in os.walk(name_path):
15-
for fn in files:
16-
if 'md' in fn and 'ebook.md' not in fn:
17-
path = os.path.join(root, fn)
214+
return f"](#{target})"
18215

19-
# "02_Development_environment.md" -> "Development environment"
20-
# "02_Development_environment.md" -> "02_Development_environment"
21-
title = fn.split('.')[0]
22-
# "02_Development_environment" -> "02 Development environment"
23-
title = title.replace('_', ' ')
24-
# "02 Development environment" -> "Development environment"
25-
title = ' '.join(title.split(' ')[1:])
216+
content = re.sub(r"\]\(!([^)]+)\)", repl, content)
26217

27-
with open(path, 'r') as f:
28-
markdownFiles.append({
29-
'title': title,
30-
'filename': os.path.join(root, fn),
31-
'contents': f.read()
32-
})
218+
temp_markdown += content + "\n\n"
33219

34-
markdownFiles.sort(key=lambda entry: entry['filename'])
220+
log.info("Writing markdown file...")
35221

36-
# Create concatenated document
37-
print('processing markdown...')
222+
with open(output_filename, "w", encoding="utf-8") as file:
223+
file.write(temp_markdown)
38224

39-
allMarkdown = ''
225+
def _collect_markdown_files_from_source(
226+
self,
227+
directory_path: pathlib.Path,
228+
current_depth: int=int(0),
229+
parent_prefix: str=str(),
230+
markdown_files: list[VTEMarkdownFile]=None
231+
) -> list[VTEMarkdownFile]:
232+
"""Traverses the directory tree, processes `Markdown` files."""
233+
if markdown_files is None:
234+
markdown_files = list[VTEMarkdownFile]()
40235

41-
for entry in markdownFiles:
42-
contents = entry['contents']
236+
for entry in pathlib.Path(directory_path).iterdir():
237+
title_tokens = entry.stem.replace("_", " ").split(" ")
238+
prefix = f"{parent_prefix}{title_tokens[0]}."
43239

44-
# Add title
45-
contents = '# ' + entry['title'] + '\n\n' + contents
240+
if entry.is_dir() is True:
241+
log.detail(f"Processing directory: {entry}")
46242

47-
# Fix image links
48-
contents = re.sub(r'\/images\/', 'images/', contents)
49-
contents = re.sub(r'\.svg', '.png', contents)
243+
title = " ".join(title_tokens[1:])
50244

51-
# Fix remaining relative links (e.g. code files)
52-
contents = re.sub(
53-
r'\]\(\/', '](https://vulkan-tutorial.com/', contents)
245+
markdown_files.append(VTEMarkdownFile("", current_depth, prefix, title))
54246

55-
# Fix chapter references
56-
def repl(m):
57-
target = m.group(1)
58-
target = target.lower()
59-
target = re.sub('_', '-', target)
60-
target = target.split('/')[-1]
247+
self._collect_markdown_files_from_source(
248+
entry,
249+
(current_depth + 1),
250+
prefix, markdown_files
251+
)
252+
else:
253+
log.detail(f"Processing: {entry}")
61254

62-
return '](#' + target + ')'
255+
title = " ".join(title_tokens[1:])
63256

64-
contents = re.sub(r'\]\(!([^)]+)\)', repl, contents)
257+
with open(entry, "r", encoding="utf-8") as file:
258+
content = file.read()
259+
markdown_files.append(VTEMarkdownFile(content, current_depth, prefix, title))
65260

66-
allMarkdown += contents + '\n\n'
261+
return markdown_files
67262

68-
# Add title
69-
dateNow = datetime.datetime.now()
70263

71-
metadata = '% Vulkan Tutorial\n'
72-
metadata += '% Alexander Overvoorde\n'
73-
metadata += '% ' + dateNow.strftime('%B %Y') + '\n\n'
264+
###############################################################################
74265

75-
allMarkdown = metadata + allMarkdown
76266

77-
with open('ebook.md', 'w') as f:
78-
f.write(allMarkdown)
267+
if __name__ == "__main__":
79268

80-
# Building PDF
81-
print('building pdf...')
269+
out_dir = pathlib.Path("./_out")
270+
if not out_dir.exists():
271+
out_dir.mkdir()
82272

83-
subprocess.check_output(['pandoc', 'ebook.md', '-V', 'documentclass=report', '-t', 'latex', '-s',
84-
'--toc', '--listings', '-H', 'ebook/listings-setup.tex', '-o', 'ebook/Vulkan Tutorial ' + name_path + '.pdf', '--pdf-engine=xelatex'])
273+
log = VTLogger(f"{out_dir.as_posix()}/build_ebook.log")
274+
eBookBuilder = VTEBookBuilder(log)
85275

86-
print('building epub...')
276+
log.info("--- Exporting ebooks:")
87277

88-
subprocess.check_output(
89-
['pandoc', 'ebook.md', '--toc', '-o', 'ebook/Vulkan Tutorial ' + name_path + '.epub', '--epub-cover-image=ebook/cover.png'])
278+
generated_pngs = eBookBuilder.convert_svg_to_png("./images")
90279

91-
# Clean up
92-
os.remove('ebook.md')
280+
LANGUAGES = [ "en", "fr" ]
281+
OUTPUT_MARKDOWN_FILEPATH = pathlib.Path(f"{out_dir.as_posix()}/temp_ebook.md")
93282

283+
for lang in LANGUAGES:
284+
eBookBuilder.generate_joined_markdown(f"./{lang}", OUTPUT_MARKDOWN_FILEPATH)
94285

95-
# Convert all SVG images to PNG for pandoc
96-
print('converting svgs...')
286+
try:
287+
eBookBuilder.build_epub_book(lang, OUTPUT_MARKDOWN_FILEPATH)
288+
eBookBuilder.build_pdf_book(lang, OUTPUT_MARKDOWN_FILEPATH)
289+
except RuntimeError as runtimeError:
290+
log.error("Termininating...")
97291

98-
generatedPngs = []
292+
# Clean up.
293+
if OUTPUT_MARKDOWN_FILEPATH.exists():
294+
OUTPUT_MARKDOWN_FILEPATH.unlink()
99295

100-
for fn in os.listdir('images'):
101-
parts = fn.split('.')
296+
log.info("Cleaning up...")
102297

103-
if parts[1] == 'svg':
104-
subprocess.check_output(['inkscape', '--export-filename=images/' +
105-
parts[0] + '.png', 'images/' + fn], stderr=subprocess.STDOUT)
106-
generatedPngs.append('images/' + parts[0] + '.png')
298+
# Clean up temporary files.
299+
for png_path in generated_pngs:
300+
try:
301+
png_path.unlink()
302+
except FileNotFoundError as fileError:
303+
log.error(fileError)
107304

108-
create_ebook('en')
109-
create_ebook('fr')
305+
# Comment out to view log file.
306+
if out_dir.exists():
307+
shutil.rmtree(out_dir)
110308

111-
for fn in generatedPngs:
112-
os.remove(fn)
309+
log.info("---- DONE!")

0 commit comments

Comments
 (0)