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