Skip to content

Commit d51734e

Browse files
committed
Merge branch 'H-G-Hristov-hgh/added-build_ebook_v2.py'
2 parents 1db3e81 + 9ee1426 commit d51734e

File tree

2 files changed

+227
-92
lines changed

2 files changed

+227
-92
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
*.html
22
ads.txt
3+
4+
**/_out/*
5+
**/.vscode/*
6+
.DS_Store
7+
build_ebook.log
8+
temp_ebook.md

build_ebook.py

100644100755
Lines changed: 221 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,241 @@
1-
import subprocess
2-
import datetime
3-
import os
4-
import re
5-
6-
7-
def create_ebook(path):
8-
9-
name_path = path
10-
print('\n Creating \"' + name_path + '\" ebook')
11-
# Recursively gather all markdown files in the right order
12-
markdownFiles = []
13-
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)
18-
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:])
26-
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-
})
33-
34-
markdownFiles.sort(key=lambda entry: entry['filename'])
1+
"""Generate EPUB and PDF ebooks from sources."""
352

36-
# Create concatenated document
37-
print('processing markdown...')
38-
39-
allMarkdown = ''
40-
41-
for entry in markdownFiles:
42-
contents = entry['contents']
43-
44-
# Add title
45-
contents = '# ' + entry['title'] + '\n\n' + contents
46-
47-
# Fix image links
48-
contents = re.sub(r'\/images\/', 'images/', contents)
49-
contents = re.sub(r'\.svg', '.png', contents)
50-
51-
# Fix remaining relative links (e.g. code files)
52-
contents = re.sub(
53-
r'\]\(\/', '](https://vulkan-tutorial.com/', contents)
54-
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]
3+
from datetime import datetime
4+
import json
5+
import logging
6+
from pathlib import Path
7+
import re
8+
from tempfile import TemporaryDirectory
9+
import subprocess
10+
from dataclasses import dataclass
11+
from subprocess import CalledProcessError
12+
from re import Match
13+
import shutil
14+
15+
logging.basicConfig(
16+
format="%(asctime)s %(levelname)-8s %(message)s",
17+
level=logging.INFO,
18+
datefmt="%Y-%m-%d %H:%M:%S",
19+
)
6120

62-
return '](#' + target + ')'
6321

64-
contents = re.sub(r'\]\(!([^)]+)\)', repl, contents)
22+
def convert_images(images_dir: Path, converted_image_dir: Path) -> None:
23+
"""Convert all SVG images to PNGs."""
24+
25+
if not converted_image_dir.exists():
26+
converted_image_dir.mkdir()
27+
28+
for source_file in images_dir.glob("*"):
29+
if source_file.suffix == ".svg":
30+
dest_file = converted_image_dir / source_file.with_suffix(".png").name
6531

66-
allMarkdown += contents + '\n\n'
32+
try:
33+
subprocess.check_output(
34+
[
35+
"inkscape",
36+
f"--export-filename={dest_file.as_posix()}",
37+
source_file.as_posix(),
38+
],
39+
stderr=subprocess.STDOUT,
40+
)
41+
except FileNotFoundError:
42+
raise RuntimeError(
43+
f"failed to convert {source_file.name} to {dest_file.name}: "
44+
"inkscape not installed"
45+
)
46+
except CalledProcessError as e:
47+
raise RuntimeError(
48+
f"failed to convert {source_file.name} to {dest_file.name}: "
49+
f"inkscape failed: {e.output.decode()}"
50+
)
51+
else:
52+
shutil.copy(source_file, converted_image_dir / source_file.name)
6753

68-
# Add title
69-
dateNow = datetime.datetime.now()
54+
return converted_image_dir
7055

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

75-
allMarkdown = metadata + allMarkdown
57+
@dataclass
58+
class MarkdownChapter:
59+
title: str
60+
depth: int
61+
contents: str
7662

77-
with open('ebook.md', 'w') as f:
78-
f.write(allMarkdown)
7963

80-
# Building PDF
81-
print('building pdf...')
64+
def find_markdown_chapters(markdown_dir: Path) -> list[Path]:
65+
"""Find all Markdown files and interpret them as chapters."""
8266

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'])
67+
markdown_entries = list(markdown_dir.rglob("*"))
68+
markdown_entries.sort()
8569

86-
print('building epub...')
70+
markdown_chapters = []
8771

88-
subprocess.check_output(
89-
['pandoc', 'ebook.md', '--toc', '-o', 'ebook/Vulkan Tutorial ' + name_path + '.epub', '--epub-cover-image=ebook/cover.png'])
72+
for markdown_path in markdown_entries:
73+
# Skip privacy policy (regardless of language)
74+
if markdown_path.name.startswith("95_"):
75+
continue
76+
77+
title = markdown_path.stem.partition("_")[-1].replace("_", " ")
78+
depth = len(markdown_path.relative_to(markdown_dir).parts) - 1
79+
80+
markdown_chapters.append(
81+
MarkdownChapter(
82+
title=title,
83+
depth=depth,
84+
contents=markdown_path.read_text() if markdown_path.is_file() else "",
85+
)
86+
)
87+
88+
return markdown_chapters
89+
90+
91+
def generate_markdown_preface() -> str:
92+
current_date = datetime.now().strftime("%B %Y")
93+
94+
return "\n".join(
95+
[
96+
"% Vulkan Tutorial",
97+
"% Alexander Overvoorde",
98+
f"% {current_date}",
99+
]
100+
)
101+
102+
103+
def generate_markdown_chapter(
104+
chapter: MarkdownChapter, converted_image_dir: Path
105+
) -> str:
106+
contents = f"# {chapter.title}\n\n{chapter.contents}"
107+
108+
# Adjust titles based on depth of chapter itself
109+
if chapter.depth > 0:
110+
111+
def adjust_title_depth(match: Match) -> str:
112+
return ("#" * chapter.depth) + match.group(0)
113+
114+
contents = re.sub(r"#+ ", adjust_title_depth, contents)
115+
116+
# Fix image links
117+
contents = contents.replace("/images/", f"{converted_image_dir.as_posix()}/")
118+
contents = contents.replace(".svg", ".png")
119+
120+
# Fix remaining relative links
121+
contents = contents.replace("(/code", "(https://vulkan-tutorial.com/code")
122+
contents = contents.replace("(/resources", "(https://vulkan-tutorial.com/resources")
123+
124+
# Fix chapter references
125+
def fix_chapter_reference(match: Match) -> str:
126+
target = match.group(1).lower().replace("_", "-").split("/")[-1]
127+
return f"](#{target})"
128+
129+
contents = re.sub(r"\]\(!([^)]+)\)", fix_chapter_reference, contents)
130+
131+
return contents
132+
133+
134+
def compile_full_markdown(
135+
markdown_dir: Path, markdown_file: Path, converted_image_dir: Path
136+
) -> Path:
137+
"""Combine Markdown source files into one large file."""
138+
139+
markdown_fragments = [generate_markdown_preface()]
140+
141+
for chapter in find_markdown_chapters(markdown_dir):
142+
markdown_fragments.append(
143+
generate_markdown_chapter(chapter, converted_image_dir)
144+
)
90145

91-
# Clean up
92-
os.remove('ebook.md')
146+
markdown_file.write_text("\n\n".join(markdown_fragments))
93147

148+
return markdown_file
94149

95-
# Convert all SVG images to PNG for pandoc
96-
print('converting svgs...')
97150

98-
generatedPngs = []
151+
def build_pdf(markdown_file: Path, pdf_file: Path) -> Path:
152+
"""Build combined Markdown file into a PDF."""
99153

100-
for fn in os.listdir('images'):
101-
parts = fn.split('.')
154+
try:
155+
subprocess.check_output(["xelatex", "--version"])
156+
except FileNotFoundError:
157+
raise RuntimeError(f"failed to build {pdf_file}: xelatex not installed")
102158

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')
159+
try:
160+
subprocess.check_output(
161+
[
162+
"pandoc",
163+
markdown_file.as_posix(),
164+
"-V",
165+
"documentclass=report",
166+
"-t",
167+
"latex",
168+
"-s",
169+
"--toc",
170+
"--listings",
171+
"-H",
172+
"ebook/listings-setup.tex",
173+
"-o",
174+
pdf_file.as_posix(),
175+
"--pdf-engine=xelatex",
176+
]
177+
)
178+
except CalledProcessError as e:
179+
raise RuntimeError(
180+
f"failed to build {pdf_file}: pandoc failed: {e.output.decode()}"
181+
)
182+
183+
return pdf_file
107184

108-
create_ebook('en')
109-
create_ebook('fr')
110185

111-
for fn in generatedPngs:
112-
os.remove(fn)
186+
def build_epub(markdown_file: Path, epub_file: Path) -> Path:
187+
try:
188+
subprocess.check_output(
189+
[
190+
"pandoc",
191+
markdown_file.as_posix(),
192+
"--toc",
193+
"-o",
194+
epub_file.as_posix(),
195+
"--epub-cover-image=ebook/cover.png",
196+
]
197+
)
198+
except CalledProcessError as e:
199+
raise RuntimeError(
200+
f"failed to build {epub_file}: pandoc failed: {e.output.decode()}"
201+
)
202+
203+
return epub_file
204+
205+
206+
def main() -> None:
207+
"""Build ebooks."""
208+
with TemporaryDirectory() as raw_out_dir:
209+
out_dir = Path(raw_out_dir)
210+
211+
logging.info("converting svg images to png...")
212+
converted_image_dir = convert_images(
213+
Path("images"), out_dir / "converted_images"
214+
)
215+
216+
languages = json.loads(Path("config.json").read_text())["languages"].keys()
217+
logging.info(f"building ebooks for languages {'/'.join(languages)}")
218+
219+
for lang in languages:
220+
logging.info(f"{lang}: generating markdown...")
221+
markdown_file = compile_full_markdown(
222+
Path(lang), out_dir / f"{lang}.md", converted_image_dir
223+
)
224+
225+
logging.info(f"{lang}: building pdf...")
226+
pdf_file = build_pdf(markdown_file, out_dir / f"{lang}.pdf")
227+
228+
logging.info(f"{lang}: building epub...")
229+
epub_file = build_epub(markdown_file, out_dir / f"{lang}.epub")
230+
231+
shutil.copy(pdf_file, f"ebook/Vulkan Tutorial {lang}.pdf")
232+
shutil.copy(epub_file, f"ebook/Vulkan Tutorial {lang}.epub")
233+
234+
logging.info("done")
235+
236+
237+
if __name__ == "__main__":
238+
try:
239+
main()
240+
except RuntimeError as e:
241+
logging.error(str(e))

0 commit comments

Comments
 (0)