Skip to content

Commit e23dcff

Browse files
committed
More work on kepubify
1 parent 9c5d1c0 commit e23dcff

1 file changed

Lines changed: 74 additions & 4 deletions

File tree

src/calibre/ebooks/oeb/polish/kepubify.py

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,30 @@
1212
# * Cover marking in the OPF
1313
# * Markup cleanup (remove various things that trip up the Kobo renderer)
1414

15+
import os
1516
import re
17+
import sys
18+
from concurrent.futures import ThreadPoolExecutor
1619

1720
from lxml import etree
1821

1922
from calibre.ebooks.metadata import authors_to_string
20-
from calibre.ebooks.oeb.base import XHTML, XPath, escape_cdata
23+
from calibre.ebooks.oeb.base import OEB_DOCS, XHTML, XPath, escape_cdata
2124
from calibre.ebooks.oeb.parse_utils import barename, merge_multiple_html_heads_and_bodies
25+
from calibre.ebooks.oeb.polish.container import get_container
2226
from calibre.ebooks.oeb.polish.cover import find_cover_image, find_cover_image3, find_cover_page
2327
from calibre.ebooks.oeb.polish.parsing import parse
2428
from calibre.ebooks.oeb.polish.tts import lang_for_elem
2529
from calibre.ebooks.oeb.polish.utils import extract, insert_self_closing
2630
from calibre.spell.break_iterator import sentence_positions
31+
from calibre.srv.render_book import Profiler, calculate_number_of_workers
2732
from calibre.utils.localization import canonicalize_lang, get_lang
2833

2934
KOBO_CSS_CLASS = 'kobostylehacks'
3035
OUTER_DIV_ID = 'book-columns'
3136
INNER_DIV_ID = 'book-inner'
3237
KOBO_SPAN_CLASS = 'koboSpan'
38+
DUMMY_TITLE_PAGE_NAME = 'kobo-title-page-generated-by-calibre'
3339
SKIPPED_TAGS = frozenset((
3440
'', 'script', 'style', 'atom', 'pre', 'audio', 'video', 'svg', 'math'
3541
))
@@ -224,6 +230,16 @@ def kepubify_html_data(raw: str | bytes, metadata_lang: str = 'en'):
224230
return root
225231

226232

233+
def kepubify_html_path(path: str, metadata_lang: str = 'en'):
234+
with open(path, 'r+b') as f:
235+
raw = f.read()
236+
root = kepubify_html_data(raw)
237+
raw = serialize_html(root)
238+
f.seek(0)
239+
f.truncate()
240+
f.write(raw)
241+
242+
227243
def is_probably_a_title_page(root):
228244
for title in XPath('//h:title')(root):
229245
if title.text:
@@ -259,7 +275,7 @@ def add_dummy_title_page(container, cover_image_name):
259275
__CONTENT__
260276
</div></div></body></html>
261277
'''
262-
titlepage_name = container.add_file('kobo-title-page-generated-by-calibre.html', modify_name_if_needed=True)
278+
titlepage_name = container.add_file(f'{DUMMY_TITLE_PAGE_NAME}.html', modify_name_if_needed=True)
263279
if cover_image_name:
264280
cover_href = container.name_to_href(cover_image_name, titlepage_name)
265281
html = html.replace('__CONTENT__', f'<img src="{cover_href}" alt="cover" style="height: 100%" />')
@@ -272,6 +288,15 @@ def add_dummy_title_page(container, cover_image_name):
272288
''')
273289
with container.open(titlepage_name, 'w') as f:
274290
f.write(html)
291+
container.apply_unique_properties(titlepage_name, 'calibre:title-page')
292+
293+
294+
def remove_dummy_title_page(container):
295+
for name, is_linear in container.spine_names():
296+
if is_linear:
297+
if DUMMY_TITLE_PAGE_NAME in name:
298+
container.remove_item(name)
299+
break
275300

276301

277302
def first_spine_item_is_probably_cover(container) -> bool:
@@ -285,10 +310,55 @@ def first_spine_item_is_probably_cover(container) -> bool:
285310
return False
286311

287312

288-
def kepubify_container(container):
289-
lang = container.mi.language
313+
def kepubify_container(container, max_workers=0):
314+
remove_dummy_title_page(container)
315+
metadata_lang = container.mi.language
290316
cover_image_name = find_cover_image(container) or find_cover_image3(container)
291317
if cover_image_name:
292318
container.apply_unique_properties(cover_image_name, 'cover-image')
293319
if not find_cover_page(container) and not first_spine_item_is_probably_cover(container):
294320
add_dummy_title_page(container, cover_image_name)
321+
names_that_need_work = tuple(name for name, mt in container.mime_map.items() if mt in OEB_DOCS)
322+
num_workers = calculate_number_of_workers(names_that_need_work, container, max_workers)
323+
paths = tuple(map(container.name_to_abspath, names_that_need_work))
324+
if num_workers < 2:
325+
for path in paths:
326+
kepubify_html_path(path, metadata_lang)
327+
else:
328+
with ThreadPoolExecutor(max_workers=num_workers) as executor:
329+
futures = tuple(executor.submit(kepubify_html_path, path, metadata_lang) for path in paths)
330+
for future in futures:
331+
future.result()
332+
333+
334+
def profile():
335+
from calibre.ptempfile import TemporaryDirectory
336+
path = sys.argv[-1]
337+
with TemporaryDirectory() as tdir, Profiler():
338+
main(path, max_workers=1)
339+
340+
341+
def develop():
342+
from zipfile import ZipFile
343+
344+
from calibre.ptempfile import TemporaryDirectory
345+
path = sys.argv[-1]
346+
with TemporaryDirectory() as tdir:
347+
outpath = main(path, max_workers=1)
348+
with ZipFile(outpath) as zf:
349+
zf.extractall(tdir)
350+
print('Extracted to:', tdir)
351+
input('Press Enter to quit')
352+
353+
354+
def main(path, max_workers=0):
355+
container = get_container(path, tweak_mode=True)
356+
kepubify_container(container, max_workers=max_workers)
357+
base, ext = os.path.splitext(path)
358+
outpath = base + '.kepub'
359+
container.commit(output=outpath)
360+
return outpath
361+
362+
363+
if __name__ == '__main__':
364+
main(sys.argv[-1])

0 commit comments

Comments
 (0)