1212# * Cover marking in the OPF
1313# * Markup cleanup (remove various things that trip up the Kobo renderer)
1414
15+ import os
1516import re
17+ import sys
18+ from concurrent .futures import ThreadPoolExecutor
1619
1720from lxml import etree
1821
1922from 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
2124from calibre .ebooks .oeb .parse_utils import barename , merge_multiple_html_heads_and_bodies
25+ from calibre .ebooks .oeb .polish .container import get_container
2226from calibre .ebooks .oeb .polish .cover import find_cover_image , find_cover_image3 , find_cover_page
2327from calibre .ebooks .oeb .polish .parsing import parse
2428from calibre .ebooks .oeb .polish .tts import lang_for_elem
2529from calibre .ebooks .oeb .polish .utils import extract , insert_self_closing
2630from calibre .spell .break_iterator import sentence_positions
31+ from calibre .srv .render_book import Profiler , calculate_number_of_workers
2732from calibre .utils .localization import canonicalize_lang , get_lang
2833
2934KOBO_CSS_CLASS = 'kobostylehacks'
3035OUTER_DIV_ID = 'book-columns'
3136INNER_DIV_ID = 'book-inner'
3237KOBO_SPAN_CLASS = 'koboSpan'
38+ DUMMY_TITLE_PAGE_NAME = 'kobo-title-page-generated-by-calibre'
3339SKIPPED_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+
227243def 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
277302def 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