1#!/usr/local/bin/python3.8 2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai 3 4 5__license__ = 'GPL v3' 6__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' 7__docformat__ = 'restructuredtext en' 8 9import os, ast, json 10 11from calibre.utils.config import config_dir, prefs, tweaks 12from calibre.utils.lock import ExclusiveFile 13from calibre import sanitize_file_name 14from calibre.customize.conversion import OptionRecommendation 15from calibre.customize.ui import available_output_formats 16 17 18config_dir = os.path.join(config_dir, 'conversion') 19 20 21def name_to_path(name): 22 return os.path.join(config_dir, sanitize_file_name(name)+'.py') 23 24 25def save_defaults(name, recs): 26 path = name_to_path(name) 27 raw = recs.serialize() 28 29 os.makedirs(config_dir, exist_ok=True) 30 31 with lopen(path, 'wb'): 32 pass 33 with ExclusiveFile(path) as f: 34 f.write(raw) 35 36 37def load_defaults(name): 38 path = name_to_path(name) 39 40 os.makedirs(config_dir, exist_ok=True) 41 42 if not os.path.exists(path): 43 open(path, 'wb').close() 44 with ExclusiveFile(path) as f: 45 raw = f.read() 46 r = GuiRecommendations() 47 if raw: 48 r.deserialize(raw) 49 return r 50 51 52def save_specifics(db, book_id, recs): 53 raw = recs.serialize() 54 db.new_api.set_conversion_options({book_id: raw}, fmt='PIPE') 55 56 57def load_specifics(db, book_id): 58 raw = db.conversion_options(book_id, 'PIPE') 59 r = GuiRecommendations() 60 if raw: 61 r.deserialize(raw) 62 return r 63 64 65def delete_specifics(db, book_id): 66 db.delete_conversion_options(book_id, 'PIPE') 67 68 69class GuiRecommendations(dict): 70 71 def __new__(cls, *args): 72 dict.__new__(cls) 73 obj = super().__new__(cls, *args) 74 obj.disabled_options = set() 75 return obj 76 77 def to_recommendations(self, level=OptionRecommendation.LOW): 78 ans = [] 79 for key, val in self.items(): 80 ans.append((key, val, level)) 81 return ans 82 83 def __str__(self): 84 ans = ['{'] 85 for key, val in self.items(): 86 ans.append('\t'+repr(key)+' : '+repr(val)+',') 87 ans.append('}') 88 return '\n'.join(ans) 89 90 def serialize(self): 91 ans = json.dumps(self, indent=2, ensure_ascii=False) 92 if isinstance(ans, str): 93 ans = ans.encode('utf-8') 94 return b'json:' + ans 95 96 def deserialize(self, raw): 97 try: 98 if raw.startswith(b'json:'): 99 d = json.loads(raw[len(b'json:'):]) 100 else: 101 d = ast.literal_eval(raw) 102 except Exception: 103 pass 104 else: 105 if d: 106 self.update(d) 107 from_string = deserialize 108 109 def merge_recommendations(self, get_option, level, options, 110 only_existing=False): 111 for name in options: 112 if only_existing and name not in self: 113 continue 114 opt = get_option(name) 115 if opt is None: 116 continue 117 if opt.level == OptionRecommendation.HIGH: 118 self[name] = opt.recommended_value 119 self.disabled_options.add(name) 120 elif opt.level > level or name not in self: 121 self[name] = opt.recommended_value 122 123 def as_dict(self): 124 return { 125 'options': self.copy(), 126 'disabled': tuple(self.disabled_options) 127 } 128 129 130def get_available_formats_for_book(db, book_id): 131 available_formats = db.new_api.formats(book_id) 132 return {x.lower() for x in available_formats} 133 134 135class NoSupportedInputFormats(Exception): 136 137 def __init__(self, available_formats): 138 Exception.__init__(self) 139 self.available_formats = available_formats 140 141 142def get_supported_input_formats_for_book(db, book_id): 143 from calibre.ebooks.conversion.plumber import supported_input_formats 144 available_formats = get_available_formats_for_book(db, book_id) 145 input_formats = {x.lower() for x in supported_input_formats()} 146 input_formats = sorted(available_formats.intersection(input_formats)) 147 if not input_formats: 148 raise NoSupportedInputFormats(tuple(x for x in available_formats if x)) 149 return input_formats 150 151 152def get_preferred_input_format_for_book(db, book_id): 153 recs = load_specifics(db, book_id) 154 if recs: 155 return recs.get('gui_preferred_input_format', None) 156 157 158def sort_formats_by_preference(formats, prefs): 159 uprefs = {x.upper():i for i, x in enumerate(prefs)} 160 161 def key(x): 162 return uprefs.get(x.upper(), len(prefs)) 163 164 return sorted(formats, key=key) 165 166 167def get_input_format_for_book(db, book_id, pref=None): 168 ''' 169 Return (preferred input format, list of available formats) for the book 170 identified by book_id. Raises an error if the book has no input formats. 171 172 :param pref: If None, the format used as input for the last conversion, if 173 any, on this book is used. If not None, should be a lowercase format like 174 'epub' or 'mobi'. If you do not want the last converted format to be used, 175 set pref=False. 176 ''' 177 if pref is None: 178 pref = get_preferred_input_format_for_book(db, book_id) 179 if hasattr(pref, 'lower'): 180 pref = pref.lower() 181 input_formats = get_supported_input_formats_for_book(db, book_id) 182 input_format = pref if pref in input_formats else \ 183 sort_formats_by_preference( 184 input_formats, prefs['input_format_order'])[0] 185 return input_format, input_formats 186 187 188def get_output_formats(preferred_output_format): 189 all_formats = {x.upper() for x in available_output_formats()} 190 all_formats.discard('OEB') 191 pfo = preferred_output_format.upper() if preferred_output_format else '' 192 restrict = tweaks['restrict_output_formats'] 193 if restrict: 194 fmts = [x.upper() for x in restrict] 195 if pfo and pfo not in fmts and pfo in all_formats: 196 fmts.append(pfo) 197 else: 198 fmts = list(sorted(all_formats, 199 key=lambda x:{'EPUB':'!A', 'AZW3':'!B', 'MOBI':'!C'}.get(x.upper(), x))) 200 return fmts 201 202 203def get_sorted_output_formats(preferred_fmt=None): 204 preferred_output_format = (preferred_fmt or prefs['output_format']).upper() 205 fmts = get_output_formats(preferred_output_format) 206 try: 207 fmts.remove(preferred_output_format) 208 except Exception: 209 pass 210 fmts.insert(0, preferred_output_format) 211 return fmts 212 213 214OPTIONS = { 215 'input': { 216 'comic': ( 217 'colors', 'dont_normalize', 'keep_aspect_ratio', 'right2left', 'despeckle', 'no_sort', 'no_process', 'landscape', 218 'dont_sharpen', 'disable_trim', 'wide', 'output_format', 'dont_grayscale', 'comic_image_size', 'dont_add_comic_pages_to_toc'), 219 220 'docx': ('docx_no_cover', 'docx_no_pagebreaks_between_notes', 'docx_inline_subsup'), 221 222 'fb2': ('no_inline_fb2_toc',), 223 224 'pdf': ('no_images', 'unwrap_factor'), 225 226 'rtf': ('ignore_wmf',), 227 228 'txt': ('paragraph_type', 'formatting_type', 'markdown_extensions', 'preserve_spaces', 'txt_in_remove_indents'), 229 }, 230 231 'pipe': { 232 'debug': ('debug_pipeline',), 233 234 'heuristics': ( 235 'enable_heuristics', 'markup_chapter_headings', 236 'italicize_common_cases', 'fix_indents', 'html_unwrap_factor', 237 'unwrap_lines', 'delete_blank_paragraphs', 'format_scene_breaks', 238 'replace_scene_breaks', 'dehyphenate', 'renumber_headings'), 239 240 'look_and_feel': ( 241 'change_justification', 'extra_css', 'base_font_size', 242 'font_size_mapping', 'line_height', 'minimum_line_height', 243 'embed_font_family', 'embed_all_fonts', 'subset_embedded_fonts', 244 'smarten_punctuation', 'unsmarten_punctuation', 245 'disable_font_rescaling', 'insert_blank_line', 246 'remove_paragraph_spacing', 'remove_paragraph_spacing_indent_size', 247 'insert_blank_line_size', 'input_encoding', 'filter_css', 248 'expand_css', 'asciiize', 'keep_ligatures', 'linearize_tables', 249 'transform_css_rules', 'transform_html_rules'), 250 251 'metadata': ('prefer_metadata_cover',), 252 253 'page_setup': ( 254 'margin_top', 'margin_left', 'margin_right', 'margin_bottom', 255 'input_profile', 'output_profile'), 256 257 'search_and_replace': ( 258 'search_replace', 'sr1_search', 'sr1_replace', 'sr2_search', 'sr2_replace', 'sr3_search', 'sr3_replace'), 259 260 'structure_detection': ( 261 'chapter', 'chapter_mark', 'start_reading_at', 262 'remove_first_image', 'remove_fake_margins', 'insert_metadata', 263 'page_breaks_before'), 264 265 'toc': ( 266 'level1_toc', 'level2_toc', 'level3_toc', 267 'toc_threshold', 'max_toc_links', 'no_chapters_in_toc', 268 'use_auto_toc', 'toc_filter', 'duplicate_links_in_toc',), 269 }, 270 271 'output': { 272 'azw3': ('prefer_author_sort', 'toc_title', 'mobi_toc_at_start', 'dont_compress', 'no_inline_toc', 'share_not_sync',), 273 274 'docx': ( 275 'docx_page_size', 'docx_custom_page_size', 'docx_no_cover', 'docx_no_toc', 276 'docx_page_margin_left', 'docx_page_margin_top', 'docx_page_margin_right', 277 'docx_page_margin_bottom', 'preserve_cover_aspect_ratio',), 278 279 'epub': ( 280 'dont_split_on_page_breaks', 'flow_size', 'no_default_epub_cover', 281 'no_svg_cover', 'epub_inline_toc', 'epub_toc_at_end', 'toc_title', 282 'preserve_cover_aspect_ratio', 'epub_flatten', 'epub_version'), 283 284 'fb2': ('sectionize', 'fb2_genre'), 285 286 'htmlz': ('htmlz_css_type', 'htmlz_class_style', 'htmlz_title_filename'), 287 288 'lrf': ( 289 'wordspace', 'header', 'header_format', 'minimum_indent', 290 'serif_family', 'render_tables_as_images', 'sans_family', 291 'mono_family', 'text_size_multiplier_for_rendered_tables', 292 'autorotation', 'header_separation', 'minimum_indent'), 293 294 'mobi': ( 295 'prefer_author_sort', 'toc_title', 'mobi_keep_original_images', 296 'mobi_ignore_margins', 'mobi_toc_at_start', 'dont_compress', 297 'no_inline_toc', 'share_not_sync', 'personal_doc', 298 'mobi_file_type'), 299 300 'pdb': ('format', 'inline_toc', 'pdb_output_encoding'), 301 302 'pdf': ( 303 'use_profile_size', 'paper_size', 'custom_size', 'pdf_hyphenate', 304 'preserve_cover_aspect_ratio', 'pdf_serif_family', 'unit', 305 'pdf_sans_family', 'pdf_mono_family', 'pdf_standard_font', 306 'pdf_default_font_size', 'pdf_mono_font_size', 'pdf_page_numbers', 307 'pdf_footer_template', 'pdf_header_template', 'pdf_add_toc', 308 'toc_title', 'pdf_page_margin_left', 'pdf_page_margin_top', 309 'pdf_page_margin_right', 'pdf_page_margin_bottom', 310 'pdf_use_document_margins', 'pdf_page_number_map', 'pdf_odd_even_offset'), 311 312 'pml': ('inline_toc', 'full_image_depth', 'pml_output_encoding'), 313 314 'rb': ('inline_toc',), 315 316 'snb': ( 317 'snb_insert_empty_line', 'snb_dont_indent_first_line', 318 'snb_hide_chapter_name','snb_full_screen'), 319 320 'txt': ( 321 'newline', 'max_line_length', 'force_max_line_length', 322 'inline_toc', 'txt_output_formatting', 'keep_links', 'keep_image_references', 323 'keep_color', 'txt_output_encoding'), 324 }, 325} 326OPTIONS['output']['txtz'] = OPTIONS['output']['txt'] 327 328 329def options_for_input_fmt(fmt): 330 from calibre.customize.ui import plugin_for_input_format 331 fmt = fmt.lower() 332 plugin = plugin_for_input_format(fmt) 333 if plugin is None: 334 return None, () 335 full_name = plugin.name.lower().replace(' ', '_') 336 name = full_name.rpartition('_')[0] 337 return full_name, OPTIONS['input'].get(name, ()) 338 339 340def options_for_output_fmt(fmt): 341 from calibre.customize.ui import plugin_for_output_format 342 fmt = fmt.lower() 343 plugin = plugin_for_output_format(fmt) 344 if plugin is None: 345 return None, () 346 full_name = plugin.name.lower().replace(' ', '_') 347 name = full_name.rpartition('_')[0] 348 return full_name, OPTIONS['output'].get(name, ()) 349