~ubuntu-branches/ubuntu/karmic/calibre/karmic

« back to all changes in this revision

Viewing changes to src/calibre/ebooks/conversion/plumber.py

  • Committer: Bazaar Package Importer
  • Author(s): Martin Pitt
  • Date: 2009-07-30 12:49:41 UTC
  • mfrom: (1.3.2 upstream)
  • Revision ID: james.westby@ubuntu.com-20090730124941-qjdsmri25zt8zocn
Tags: 0.6.3+dfsg-0ubuntu1
* New upstream release. Please see http://calibre.kovidgoyal.net/new_in_6/
  for the list of new features and changes.
* remove_postinstall.patch: Update for new version.
* build_debug.patch: Does not apply any more, disable for now. Might not be
  necessary any more.
* debian/copyright: Fix reference to versionless GPL.
* debian/rules: Drop obsolete dh_desktop call.
* debian/rules: Add workaround for weird Python 2.6 setuptools behaviour of
  putting compiled .so files into src/calibre/plugins/calibre/plugins
  instead of src/calibre/plugins.
* debian/rules: Drop hal fdi moving, new upstream version does not use hal
  any more. Drop hal dependency, too.
* debian/rules: Install udev rules into /lib/udev/rules.d.
* Add debian/calibre.preinst: Remove unmodified
  /etc/udev/rules.d/95-calibre.rules on upgrade.
* debian/control: Bump Python dependencies to 2.6, since upstream needs
  it now.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: utf-8 -*-
 
2
__license__ = 'GPL 3'
 
3
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
 
4
__docformat__ = 'restructuredtext en'
 
5
 
 
6
import os, re, sys
 
7
 
 
8
from calibre.customize.conversion import OptionRecommendation, DummyReporter
 
9
from calibre.customize.ui import input_profiles, output_profiles, \
 
10
        plugin_for_input_format, plugin_for_output_format, \
 
11
        available_input_formats, available_output_formats
 
12
from calibre.ebooks.conversion.preprocess import HTMLPreProcessor
 
13
from calibre.ptempfile import PersistentTemporaryDirectory
 
14
from calibre import extract, walk
 
15
 
 
16
def supported_input_formats():
 
17
    fmts = available_input_formats()
 
18
    for x in ('zip', 'rar', 'oebzip'):
 
19
        fmts.add(x)
 
20
    return fmts
 
21
 
 
22
class OptionValues(object):
 
23
    pass
 
24
 
 
25
class CompositeProgressReporter(object):
 
26
 
 
27
    def __init__(self, global_min, global_max, global_reporter):
 
28
        self.global_min, self.global_max = global_min, global_max
 
29
        self.global_reporter = global_reporter
 
30
 
 
31
    def __call__(self, fraction, msg=''):
 
32
        global_frac = self.global_min + fraction * \
 
33
                (self.global_max - self.global_min)
 
34
        self.global_reporter(global_frac, msg)
 
35
 
 
36
class Plumber(object):
 
37
    '''
 
38
    The `Plumber` manages the conversion pipeline. An UI should call the methods
 
39
    :method:`merge_ui_recommendations` and then :method:`run`. The plumber will
 
40
    take care of the rest.
 
41
    '''
 
42
 
 
43
    metadata_option_names = [
 
44
        'title', 'authors', 'title_sort', 'author_sort', 'cover', 'comments',
 
45
        'publisher', 'series', 'series_index', 'rating', 'isbn',
 
46
        'tags', 'book_producer', 'language'
 
47
        ]
 
48
 
 
49
    def __init__(self, input, output, log, report_progress=DummyReporter(), dummy=False):
 
50
        '''
 
51
        :param input: Path to input file.
 
52
        :param output: Path to output file/directory
 
53
        '''
 
54
        self.input = os.path.abspath(input)
 
55
        self.output = os.path.abspath(output)
 
56
        self.log = log
 
57
        self.ui_reporter = report_progress
 
58
 
 
59
        # Initialize the conversion options that are independent of input and
 
60
        # output formats. The input and output plugins can still disable these
 
61
        # options via recommendations.
 
62
        self.pipeline_options = [
 
63
 
 
64
OptionRecommendation(name='verbose',
 
65
            recommended_value=0, level=OptionRecommendation.LOW,
 
66
            short_switch='v',
 
67
            help=_('Level of verbosity. Specify multiple times for greater '
 
68
                   'verbosity.')
 
69
        ),
 
70
 
 
71
OptionRecommendation(name='input_profile',
 
72
            recommended_value='default', level=OptionRecommendation.LOW,
 
73
            choices=[x.short_name for x in input_profiles()],
 
74
            help=_('Specify the input profile. The input profile gives the '
 
75
                   'conversion system information on how to interpret '
 
76
                   'various information in the input document. For '
 
77
                   'example resolution dependent lengths (i.e. lengths in '
 
78
                   'pixels). Choices are:')+\
 
79
                        ', '.join([x.short_name for x in input_profiles()])
 
80
        ),
 
81
 
 
82
OptionRecommendation(name='output_profile',
 
83
            recommended_value='default', level=OptionRecommendation.LOW,
 
84
            choices=[x.short_name for x in output_profiles()],
 
85
            help=_('Specify the output profile. The output profile '
 
86
                   'tells the conversion system how to optimize the '
 
87
                   'created document for the specified device. In some cases, '
 
88
                   'an output profile is required to produce documents that '
 
89
                   'will work on a device. For example EPUB on the SONY reader. '
 
90
                   'Choices are:') + \
 
91
                           ', '.join([x.short_name for x in output_profiles()])
 
92
        ),
 
93
 
 
94
OptionRecommendation(name='base_font_size',
 
95
            recommended_value=0, level=OptionRecommendation.LOW,
 
96
            help=_('The base font size in pts. All font sizes in the produced book '
 
97
                   'will be rescaled based on this size. By choosing a larger '
 
98
                   'size you can make the fonts in the output bigger and vice '
 
99
                   'versa. By default, the base font size is chosen based on '
 
100
                   'the output profile you chose.'
 
101
                   )
 
102
        ),
 
103
 
 
104
OptionRecommendation(name='font_size_mapping',
 
105
            recommended_value=None, level=OptionRecommendation.LOW,
 
106
            help=_('Mapping from CSS font names to font sizes in pts. '
 
107
                   'An example setting is 12,12,14,16,18,20,22,24. '
 
108
                   'These are the mappings for the sizes xx-small to xx-large, '
 
109
                   'with the final size being for huge fonts. The font '
 
110
                   'rescaling algorithm uses these sizes to intelligently '
 
111
                   'rescale fonts. The default is to use a mapping based on '
 
112
                   'the output profile you chose.'
 
113
                   )
 
114
        ),
 
115
 
 
116
OptionRecommendation(name='disable_font_rescaling',
 
117
            recommended_value=False, level=OptionRecommendation.LOW,
 
118
            help=_('Disable all rescaling of font sizes.'
 
119
                   )
 
120
        ),
 
121
 
 
122
 
 
123
OptionRecommendation(name='line_height',
 
124
            recommended_value=0, level=OptionRecommendation.LOW,
 
125
            help=_('The line height in pts. Controls spacing between consecutive '
 
126
                   'lines of text. By default no line height manipulation is '
 
127
                   'performed.'
 
128
                   )
 
129
        ),
 
130
 
 
131
OptionRecommendation(name='linearize_tables',
 
132
            recommended_value=False, level=OptionRecommendation.LOW,
 
133
            help=_('Some badly designed documents use tables to control the '
 
134
                'layout of text on the page. When converted these documents '
 
135
                'often have text that runs off the page and other artifacts. '
 
136
                'This option will extract the content from the tables and '
 
137
                'present it in a linear fashion.'
 
138
                )
 
139
        ),
 
140
 
 
141
OptionRecommendation(name='level1_toc',
 
142
            recommended_value=None, level=OptionRecommendation.LOW,
 
143
            help=_('XPath expression that specifies all tags that '
 
144
            'should be added to the Table of Contents at level one. If '
 
145
            'this is specified, it takes precedence over other forms '
 
146
            'of auto-detection.'
 
147
                )
 
148
        ),
 
149
 
 
150
OptionRecommendation(name='level2_toc',
 
151
            recommended_value=None, level=OptionRecommendation.LOW,
 
152
            help=_('XPath expression that specifies all tags that should be '
 
153
            'added to the Table of Contents at level two. Each entry is added '
 
154
            'under the previous level one entry.'
 
155
                )
 
156
        ),
 
157
 
 
158
OptionRecommendation(name='level3_toc',
 
159
            recommended_value=None, level=OptionRecommendation.LOW,
 
160
            help=_('XPath expression that specifies all tags that should be '
 
161
                'added to the Table of Contents at level three. Each entry '
 
162
                'is added under the previous level two entry.'
 
163
                )
 
164
        ),
 
165
 
 
166
OptionRecommendation(name='use_auto_toc',
 
167
            recommended_value=False, level=OptionRecommendation.LOW,
 
168
            help=_('Normally, if the source file already has a Table of '
 
169
            'Contents, it is used in preference to the auto-generated one. '
 
170
            'With this option, the auto-generated one is always used.'
 
171
                )
 
172
        ),
 
173
 
 
174
OptionRecommendation(name='no_chapters_in_toc',
 
175
            recommended_value=False, level=OptionRecommendation.LOW,
 
176
            help=_("Don't add auto-detected chapters to the Table of "
 
177
            'Contents.'
 
178
                )
 
179
        ),
 
180
 
 
181
OptionRecommendation(name='toc_threshold',
 
182
            recommended_value=6, level=OptionRecommendation.LOW,
 
183
            help=_(
 
184
        'If fewer than this number of chapters is detected, then links '
 
185
        'are added to the Table of Contents. Default: %default')
 
186
        ),
 
187
 
 
188
OptionRecommendation(name='max_toc_links',
 
189
            recommended_value=50, level=OptionRecommendation.LOW,
 
190
            help=_('Maximum number of links to insert into the TOC. Set to 0 '
 
191
               'to disable. Default is: %default. Links are only added to the '
 
192
            'TOC if less than the threshold number of chapters were detected.'
 
193
                )
 
194
        ),
 
195
 
 
196
OptionRecommendation(name='toc_filter',
 
197
            recommended_value=None, level=OptionRecommendation.LOW,
 
198
            help=_('Remove entries from the Table of Contents whose titles '
 
199
            'match the specified regular expression. Matching entries and all '
 
200
            'their children are removed.'
 
201
                )
 
202
        ),
 
203
 
 
204
 
 
205
OptionRecommendation(name='chapter',
 
206
        recommended_value="//*[((name()='h1' or name()='h2') and "
 
207
              r"re:test(., 'chapter|book|section|part\s+', 'i')) or @class "
 
208
              "= 'chapter']", level=OptionRecommendation.LOW,
 
209
            help=_('An XPath expression to detect chapter titles. The default '
 
210
                'is to consider <h1> or <h2> tags that contain the words '
 
211
                '"chapter","book","section" or "part" as chapter titles as '
 
212
                'well as any tags that have class="chapter". The expression '
 
213
                'used must evaluate to a list of elements. To disable chapter '
 
214
                'detection, use the expression "/". See the XPath Tutorial '
 
215
                'in the calibre User Manual for further help on using this '
 
216
                'feature.'
 
217
                )
 
218
        ),
 
219
 
 
220
OptionRecommendation(name='chapter_mark',
 
221
            recommended_value='pagebreak', level=OptionRecommendation.LOW,
 
222
            choices=['pagebreak', 'rule', 'both', 'none'],
 
223
            help=_('Specify how to mark detected chapters. A value of '
 
224
                    '"pagebreak" will insert page breaks before chapters. '
 
225
                    'A value of "rule" will insert a line before chapters. '
 
226
                    'A value of "none" will disable chapter marking and a '
 
227
                    'value of "both" will use both page breaks and lines '
 
228
                    'to mark chapters.')
 
229
        ),
 
230
 
 
231
OptionRecommendation(name='extra_css',
 
232
            recommended_value=None, level=OptionRecommendation.LOW,
 
233
            help=_('Either the path to a CSS stylesheet or raw CSS. '
 
234
                'This CSS will be appended to the style rules from '
 
235
                'the source file, so it can be used to override those '
 
236
                'rules.')
 
237
        ),
 
238
 
 
239
OptionRecommendation(name='page_breaks_before',
 
240
            recommended_value="//*[name()='h1' or name()='h2']",
 
241
            level=OptionRecommendation.LOW,
 
242
            help=_('An XPath expression. Page breaks are inserted '
 
243
            'before the specified elements.')
 
244
        ),
 
245
 
 
246
OptionRecommendation(name='margin_top',
 
247
        recommended_value=5.0, level=OptionRecommendation.LOW,
 
248
        help=_('Set the top margin in pts. Default is %default. '
 
249
            'Note: 72 pts equals 1 inch')),
 
250
 
 
251
OptionRecommendation(name='margin_bottom',
 
252
        recommended_value=5.0, level=OptionRecommendation.LOW,
 
253
        help=_('Set the bottom margin in pts. Default is %default. '
 
254
            'Note: 72 pts equals 1 inch')),
 
255
 
 
256
OptionRecommendation(name='margin_left',
 
257
        recommended_value=5.0, level=OptionRecommendation.LOW,
 
258
        help=_('Set the left margin in pts. Default is %default. '
 
259
            'Note: 72 pts equals 1 inch')),
 
260
 
 
261
OptionRecommendation(name='margin_right',
 
262
        recommended_value=5.0, level=OptionRecommendation.LOW,
 
263
        help=_('Set the right margin in pts. Default is %default. '
 
264
            'Note: 72 pts equals 1 inch')),
 
265
 
 
266
OptionRecommendation(name='dont_justify',
 
267
        recommended_value=False, level=OptionRecommendation.LOW,
 
268
        help=_('Do not force text to be justified in output. Whether text '
 
269
            'is actually displayed justified or not depends on whether '
 
270
            'the ebook format and reading device support justification.')
 
271
        ),
 
272
 
 
273
OptionRecommendation(name='remove_paragraph_spacing',
 
274
        recommended_value=False, level=OptionRecommendation.LOW,
 
275
        help=_('Remove spacing between paragraphs. Also sets an indent on '
 
276
        'paragraphs of 1.5em. Spacing removal will not work '
 
277
        'if the source file does not use paragraphs (<p> or <div> tags).')
 
278
        ),
 
279
 
 
280
OptionRecommendation(name='prefer_metadata_cover',
 
281
        recommended_value=False, level=OptionRecommendation.LOW,
 
282
        help=_('Use the cover detected from the source file in preference '
 
283
        'to the specified cover.')
 
284
        ),
 
285
 
 
286
OptionRecommendation(name='insert_blank_line',
 
287
        recommended_value=False, level=OptionRecommendation.LOW,
 
288
        help=_('Insert a blank line between paragraphs. Will not work '
 
289
            'if the source file does not use paragraphs (<p> or <div> tags).'
 
290
            )
 
291
        ),
 
292
 
 
293
OptionRecommendation(name='remove_first_image',
 
294
        recommended_value=False, level=OptionRecommendation.LOW,
 
295
        help=_('Remove the first image from the input ebook. Useful if the '
 
296
        'first image in the source file is a cover and you are specifying '
 
297
        'an external cover.'
 
298
            )
 
299
        ),
 
300
 
 
301
OptionRecommendation(name='insert_metadata',
 
302
        recommended_value=False, level=OptionRecommendation.LOW,
 
303
        help=_('Insert the book metadata at the start of '
 
304
            'the book. This is useful if your ebook reader does not support '
 
305
            'displaying/searching metadata directly.'
 
306
            )
 
307
        ),
 
308
 
 
309
OptionRecommendation(name='preprocess_html',
 
310
        recommended_value=False, level=OptionRecommendation.LOW,
 
311
        help=_('Attempt to detect and correct hard line breaks and other '
 
312
            'problems in the source file. This may make things worse, so use '
 
313
            'with care.'
 
314
            )
 
315
        ),
 
316
 
 
317
OptionRecommendation(name='remove_header',
 
318
        recommended_value=False, level=OptionRecommendation.LOW,
 
319
        help=_('Use a regular expression to try and remove the header.'
 
320
            )
 
321
        ),
 
322
 
 
323
OptionRecommendation(name='header_regex',
 
324
        recommended_value='(?i)(?<=<hr>)((\s*<a name=\d+></a>((<img.+?>)*<br>\s*)?\d+<br>\s*.*?\s*)|(\s*<a name=\d+></a>((<img.+?>)*<br>\s*)?.*?<br>\s*\d+))(?=<br>)',
 
325
        level=OptionRecommendation.LOW,
 
326
        help=_('The regular expression to use to remove the header.'
 
327
            )
 
328
        ),
 
329
 
 
330
OptionRecommendation(name='remove_footer',
 
331
        recommended_value=False, level=OptionRecommendation.LOW,
 
332
        help=_('Use a regular expression to try and remove the footer.'
 
333
            )
 
334
        ),
 
335
 
 
336
OptionRecommendation(name='footer_regex',
 
337
        recommended_value='(?i)(?<=<hr>)((\s*<a name=\d+></a>((<img.+?>)*<br>\s*)?\d+<br>\s*.*?\s*)|(\s*<a name=\d+></a>((<img.+?>)*<br>\s*)?.*?<br>\s*\d+))(?=<br>)',
 
338
        level=OptionRecommendation.LOW,
 
339
        help=_('The regular expression to use to remove the footer.'
 
340
            )
 
341
        ),
 
342
 
 
343
OptionRecommendation(name='read_metadata_from_opf',
 
344
            recommended_value=None, level=OptionRecommendation.LOW,
 
345
            short_switch='m',
 
346
            help=_('Read metadata from the specified OPF file. Metadata read '
 
347
                   'from this file will override any metadata in the source '
 
348
                   'file.')
 
349
        ),
 
350
 
 
351
OptionRecommendation(name='asciiize',
 
352
        recommended_value=False, level=OptionRecommendation.LOW,
 
353
        help=(_('Transliterate unicode characters to an ASCII '
 
354
            'representation. Use with care because this will replace '
 
355
            'unicode characters with ASCII. For instance it will replace "%s" '
 
356
            'with "Mikhail Gorbachiov". Also, note that in '
 
357
            'cases where there are multiple representations of a character '
 
358
            '(characters shared by Chinese and Japanese for instance) the '
 
359
            'representation used by the largest number of people will be '
 
360
            'used (Chinese in the previous example).')%\
 
361
            u'\u041c\u0438\u0445\u0430\u0438\u043b '
 
362
            u'\u0413\u043e\u0440\u0431\u0430\u0447\u0451\u0432'
 
363
)
 
364
        ),
 
365
 
 
366
OptionRecommendation(name='title',
 
367
    recommended_value=None, level=OptionRecommendation.LOW,
 
368
    help=_('Set the title.')),
 
369
 
 
370
OptionRecommendation(name='authors',
 
371
    recommended_value=None, level=OptionRecommendation.LOW,
 
372
    help=_('Set the authors. Multiple authors should be separated by '
 
373
    'ampersands.')),
 
374
 
 
375
OptionRecommendation(name='title_sort',
 
376
    recommended_value=None, level=OptionRecommendation.LOW,
 
377
    help=_('The version of the title to be used for sorting. ')),
 
378
 
 
379
OptionRecommendation(name='author_sort',
 
380
    recommended_value=None, level=OptionRecommendation.LOW,
 
381
    help=_('String to be used when sorting by author. ')),
 
382
 
 
383
OptionRecommendation(name='cover',
 
384
    recommended_value=None, level=OptionRecommendation.LOW,
 
385
    help=_('Set the cover to the specified file.')),
 
386
 
 
387
OptionRecommendation(name='comments',
 
388
    recommended_value=None, level=OptionRecommendation.LOW,
 
389
    help=_('Set the ebook description.')),
 
390
 
 
391
OptionRecommendation(name='publisher',
 
392
    recommended_value=None, level=OptionRecommendation.LOW,
 
393
    help=_('Set the ebook publisher.')),
 
394
 
 
395
OptionRecommendation(name='series',
 
396
    recommended_value=None, level=OptionRecommendation.LOW,
 
397
    help=_('Set the series this ebook belongs to.')),
 
398
 
 
399
OptionRecommendation(name='series_index',
 
400
    recommended_value=None, level=OptionRecommendation.LOW,
 
401
    help=_('Set the index of the book in this series.')),
 
402
 
 
403
OptionRecommendation(name='rating',
 
404
    recommended_value=None, level=OptionRecommendation.LOW,
 
405
    help=_('Set the rating. Should be a number between 1 and 5.')),
 
406
 
 
407
OptionRecommendation(name='isbn',
 
408
    recommended_value=None, level=OptionRecommendation.LOW,
 
409
    help=_('Set the ISBN of the book.')),
 
410
 
 
411
OptionRecommendation(name='tags',
 
412
    recommended_value=None, level=OptionRecommendation.LOW,
 
413
    help=_('Set the tags for the book. Should be a comma separated list.')),
 
414
 
 
415
OptionRecommendation(name='book_producer',
 
416
    recommended_value=None, level=OptionRecommendation.LOW,
 
417
    help=_('Set the book producer.')),
 
418
 
 
419
OptionRecommendation(name='language',
 
420
    recommended_value=None, level=OptionRecommendation.LOW,
 
421
    help=_('Set the language.')),
 
422
 
 
423
]
 
424
 
 
425
        input_fmt = os.path.splitext(self.input)[1]
 
426
        if not input_fmt:
 
427
            raise ValueError('Input file must have an extension')
 
428
        input_fmt = input_fmt[1:].lower()
 
429
        if input_fmt in ('zip', 'rar', 'oebzip'):
 
430
            self.log('Processing archive...')
 
431
            tdir = PersistentTemporaryDirectory('_plumber')
 
432
            self.input, input_fmt = self.unarchive(self.input, tdir)
 
433
 
 
434
        if os.path.exists(self.output) and os.path.isdir(self.output):
 
435
            output_fmt = 'oeb'
 
436
        else:
 
437
            output_fmt = os.path.splitext(self.output)[1]
 
438
            if not output_fmt:
 
439
                output_fmt = '.oeb'
 
440
            output_fmt = output_fmt[1:].lower()
 
441
 
 
442
        self.input_plugin  = plugin_for_input_format(input_fmt)
 
443
        self.output_plugin = plugin_for_output_format(output_fmt)
 
444
 
 
445
        if self.input_plugin is None:
 
446
            raise ValueError('No plugin to handle input format: '+input_fmt)
 
447
 
 
448
        if self.output_plugin is None:
 
449
            raise ValueError('No plugin to handle output format: '+output_fmt)
 
450
 
 
451
        self.input_fmt = input_fmt
 
452
        self.output_fmt = output_fmt
 
453
 
 
454
 
 
455
        self.all_format_options = set()
 
456
        self.input_options = set()
 
457
        self.output_options = set()
 
458
        # Build set of all possible options. Two options are equal if their
 
459
        # names are the same.
 
460
        if not dummy:
 
461
            self.input_options  = self.input_plugin.options.union(
 
462
                                        self.input_plugin.common_options)
 
463
            self.output_options = self.output_plugin.options.union(
 
464
                                    self.output_plugin.common_options)
 
465
        else:
 
466
            for fmt in available_input_formats():
 
467
                input_plugin = plugin_for_input_format(fmt)
 
468
                if input_plugin:
 
469
                    self.all_format_options = self.all_format_options.union(
 
470
                        input_plugin.options.union(input_plugin.common_options))
 
471
            for fmt in available_output_formats():
 
472
                output_plugin = plugin_for_output_format(fmt)
 
473
                if output_plugin:
 
474
                    self.all_format_options = self.all_format_options.union(
 
475
                        output_plugin.options.union(output_plugin.common_options))
 
476
 
 
477
        # Remove the options that have been disabled by recommendations from the
 
478
        # plugins.
 
479
        for w in ('input_options', 'output_options',
 
480
                'all_format_options'):
 
481
            temp = set([])
 
482
            for x in getattr(self, w):
 
483
                temp.add(x.clone())
 
484
            setattr(self, w, temp)
 
485
        self.merge_plugin_recommendations()
 
486
 
 
487
    @classmethod
 
488
    def unarchive(self, path, tdir):
 
489
        extract(path, tdir)
 
490
        files = list(walk(tdir))
 
491
        from calibre.customize.ui import available_input_formats
 
492
        fmts = available_input_formats()
 
493
        for x in ('htm', 'html', 'xhtm', 'xhtml'): fmts.remove(x)
 
494
 
 
495
        for ext in fmts:
 
496
            for f in files:
 
497
                if f.lower().endswith('.'+ext):
 
498
                    if ext in ['txt', 'rtf'] and os.stat(f).st_size < 2048:
 
499
                        continue
 
500
                    return f, ext
 
501
        return self.find_html_index(files)
 
502
 
 
503
    @classmethod
 
504
    def find_html_index(self, files):
 
505
        '''
 
506
        Given a list of files, find the most likely root HTML file in the
 
507
        list.
 
508
        '''
 
509
        html_pat = re.compile(r'\.(x){0,1}htm(l){0,1}$', re.IGNORECASE)
 
510
        html_files = [f for f in files if html_pat.search(f) is not None]
 
511
        if not html_files:
 
512
            raise ValueError(_('Could not find an ebook inside the archive'))
 
513
        html_files = [(f, os.stat(f).st_size) for f in html_files]
 
514
        html_files.sort(cmp = lambda x, y: cmp(x[1], y[1]))
 
515
        html_files = [f[0] for f in html_files]
 
516
        for q in ('toc', 'index'):
 
517
            for f in html_files:
 
518
                if os.path.splitext(os.path.basename(f))[0].lower() == q:
 
519
                    return f, os.path.splitext(f)[1].lower()[1:]
 
520
        return html_files[-1], os.path.splitext(html_files[-1])[1].lower()[1:]
 
521
 
 
522
 
 
523
 
 
524
    def get_option_by_name(self, name):
 
525
        for group in (self.input_options, self.pipeline_options,
 
526
                      self.output_options, self.all_format_options):
 
527
            for rec in group:
 
528
                if rec.option == name:
 
529
                    return rec
 
530
 
 
531
    def get_option_help(self, name):
 
532
        rec = self.get_option_by_name(name)
 
533
        help = getattr(rec, 'help', None)
 
534
        if help is not None:
 
535
            return help.replace('%default', str(rec.recommended_value))
 
536
 
 
537
    def merge_plugin_recommendations(self):
 
538
        for source in (self.input_plugin, self.output_plugin):
 
539
            for name, val, level in source.recommendations:
 
540
                rec = self.get_option_by_name(name)
 
541
                if rec is not None and rec.level <= level:
 
542
                    rec.recommended_value = val
 
543
                    rec.level = level
 
544
 
 
545
    def merge_ui_recommendations(self, recommendations):
 
546
        '''
 
547
        Merge recommendations from the UI. As long as the UI recommendation
 
548
        level is >= the baseline recommended level, the UI value is used,
 
549
        *except* if the baseline has a recommendation level of `HIGH`.
 
550
        '''
 
551
        for name, val, level in recommendations:
 
552
            rec = self.get_option_by_name(name)
 
553
            if rec is not None and rec.level <= level and rec.level < rec.HIGH:
 
554
                rec.recommended_value = val
 
555
                rec.level = level
 
556
 
 
557
    def read_user_metadata(self):
 
558
        '''
 
559
        Read all metadata specified by the user. Command line options override
 
560
        metadata from a specified OPF file.
 
561
        '''
 
562
        from calibre.ebooks.metadata import MetaInformation, string_to_authors
 
563
        from calibre.ebooks.metadata.opf2 import OPF
 
564
        mi = MetaInformation(None, [])
 
565
        if self.opts.read_metadata_from_opf is not None:
 
566
            self.opts.read_metadata_from_opf = os.path.abspath(
 
567
                                            self.opts.read_metadata_from_opf)
 
568
            opf = OPF(open(self.opts.read_metadata_from_opf, 'rb'),
 
569
                      os.path.dirname(self.opts.read_metadata_from_opf))
 
570
            mi = MetaInformation(opf)
 
571
        for x in self.metadata_option_names:
 
572
            val = getattr(self.opts, x, None)
 
573
            if val is not None:
 
574
                if x == 'authors':
 
575
                    val = string_to_authors(val)
 
576
                elif x == 'tags':
 
577
                    val = [i.strip() for i in val.split(',')]
 
578
                elif x in ('rating', 'series_index'):
 
579
                    val = float(val)
 
580
                setattr(mi, x, val)
 
581
        if mi.cover:
 
582
            mi.cover_data = ('', open(mi.cover, 'rb').read())
 
583
            mi.cover = None
 
584
        self.user_metadata = mi
 
585
 
 
586
    def setup_options(self):
 
587
        '''
 
588
        Setup the `self.opts` object.
 
589
        '''
 
590
        self.opts = OptionValues()
 
591
        for group in (self.input_options, self.pipeline_options,
 
592
                  self.output_options, self.all_format_options):
 
593
            for rec in group:
 
594
                setattr(self.opts, rec.option.name, rec.recommended_value)
 
595
 
 
596
        for x in input_profiles():
 
597
            if x.short_name == self.opts.input_profile:
 
598
                self.opts.input_profile = x
 
599
                break
 
600
 
 
601
        for x in output_profiles():
 
602
            if x.short_name == self.opts.output_profile:
 
603
                self.opts.output_profile = x
 
604
                break
 
605
 
 
606
        self.read_user_metadata()
 
607
        self.opts.no_inline_navbars = self.opts.output_profile.supports_mobi_indexing \
 
608
                and self.output_fmt == 'mobi'
 
609
 
 
610
    def flush(self):
 
611
        try:
 
612
            sys.stdout.flush()
 
613
            sys.stderr.flush()
 
614
        except:
 
615
            pass
 
616
 
 
617
    def run(self):
 
618
        '''
 
619
        Run the conversion pipeline
 
620
        '''
 
621
        # Setup baseline option values
 
622
        self.setup_options()
 
623
        if self.opts.verbose:
 
624
            self.log.filter_level = self.log.DEBUG
 
625
        self.flush()
 
626
 
 
627
        # Run any preprocess plugins
 
628
        from calibre.customize.ui import run_plugins_on_preprocess
 
629
        self.input = run_plugins_on_preprocess(self.input)
 
630
 
 
631
        self.flush()
 
632
        # Create an OEBBook from the input file. The input plugin does all the
 
633
        # heavy lifting.
 
634
        accelerators = {}
 
635
 
 
636
        tdir = PersistentTemporaryDirectory('_plumber')
 
637
        stream = self.input if self.input_fmt == 'recipe' else \
 
638
                open(self.input, 'rb')
 
639
 
 
640
        if hasattr(self.opts, 'lrf') and self.output_plugin.file_type == 'lrf':
 
641
            self.opts.lrf = True
 
642
 
 
643
        self.ui_reporter(0.01, _('Converting input to HTML...'))
 
644
        ir = CompositeProgressReporter(0.01, 0.34, self.ui_reporter)
 
645
        self.input_plugin.report_progress = ir
 
646
        self.oeb = self.input_plugin(stream, self.opts,
 
647
                                    self.input_fmt, self.log,
 
648
                                    accelerators, tdir)
 
649
        if self.opts.debug_input is not None:
 
650
            self.log('Debug input called, aborting the rest of the pipeline.')
 
651
            return
 
652
        if not hasattr(self.oeb, 'manifest'):
 
653
            self.oeb = create_oebbook(self.log, self.oeb, self.opts,
 
654
                    self.input_plugin)
 
655
        self.input_plugin.postprocess_book(self.oeb, self.opts, self.log)
 
656
        pr = CompositeProgressReporter(0.34, 0.67, self.ui_reporter)
 
657
        self.flush()
 
658
        pr(0., _('Running transforms on ebook...'))
 
659
 
 
660
        from calibre.ebooks.oeb.transforms.guide import Clean
 
661
        Clean()(self.oeb, self.opts)
 
662
        pr(0.1)
 
663
        self.flush()
 
664
 
 
665
        self.opts.source = self.opts.input_profile
 
666
        self.opts.dest = self.opts.output_profile
 
667
 
 
668
        from calibre.ebooks.oeb.transforms.metadata import MergeMetadata
 
669
        MergeMetadata()(self.oeb, self.user_metadata, self.opts)
 
670
        pr(0.2)
 
671
        self.flush()
 
672
 
 
673
        from calibre.ebooks.oeb.transforms.structure import DetectStructure
 
674
        DetectStructure()(self.oeb, self.opts)
 
675
        pr(0.35)
 
676
        self.flush()
 
677
 
 
678
        from calibre.ebooks.oeb.transforms.flatcss import CSSFlattener
 
679
        fbase = self.opts.base_font_size
 
680
        if fbase < 1e-4:
 
681
            fbase = float(self.opts.dest.fbase)
 
682
        fkey = self.opts.font_size_mapping
 
683
        if fkey is None:
 
684
            fkey = self.opts.dest.fkey
 
685
        else:
 
686
            fkey = map(float, fkey.split(','))
 
687
 
 
688
        from calibre.ebooks.oeb.transforms.jacket import Jacket
 
689
        Jacket()(self.oeb, self.opts, self.user_metadata)
 
690
        pr(0.4)
 
691
        self.flush()
 
692
 
 
693
        if self.opts.extra_css and os.path.exists(self.opts.extra_css):
 
694
            self.opts.extra_css = open(self.opts.extra_css, 'rb').read()
 
695
 
 
696
        oibl = self.opts.insert_blank_line
 
697
        orps  = self.opts.remove_paragraph_spacing
 
698
        if self.output_plugin.file_type == 'lrf':
 
699
            self.opts.insert_blank_line = False
 
700
            self.opts.remove_paragraph_spacing = False
 
701
        line_height = self.opts.line_height
 
702
        if line_height < 1e-4:
 
703
            line_height = None
 
704
 
 
705
        flattener = CSSFlattener(fbase=fbase, fkey=fkey,
 
706
                lineh=line_height,
 
707
                untable=self.output_plugin.file_type in ('mobi','lit'),
 
708
                unfloat=self.output_plugin.file_type in ('mobi', 'lit'))
 
709
        flattener(self.oeb, self.opts)
 
710
        self.opts.insert_blank_line = oibl
 
711
        self.opts.remove_paragraph_spacing = orps
 
712
 
 
713
        if self.opts.linearize_tables and \
 
714
                self.output_plugin.file_type not in ('mobi', 'lrf'):
 
715
            from calibre.ebooks.oeb.transforms.linearize_tables import LinearizeTables
 
716
            LinearizeTables()(self.oeb, self.opts)
 
717
        pr(0.9)
 
718
        self.flush()
 
719
 
 
720
        from calibre.ebooks.oeb.transforms.trimmanifest import ManifestTrimmer
 
721
 
 
722
        self.log.info('Cleaning up manifest...')
 
723
        trimmer = ManifestTrimmer()
 
724
        trimmer(self.oeb, self.opts)
 
725
 
 
726
        self.oeb.toc.rationalize_play_orders()
 
727
        pr(1.)
 
728
        self.flush()
 
729
 
 
730
        self.log.info('Creating %s...'%self.output_plugin.name)
 
731
        our = CompositeProgressReporter(0.67, 1., self.ui_reporter)
 
732
        self.output_plugin.report_progress = our
 
733
        our(0., _('Creating')+' %s'%self.output_plugin.name)
 
734
        self.output_plugin.convert(self.oeb, self.output, self.input_plugin,
 
735
                self.opts, self.log)
 
736
        self.ui_reporter(1.)
 
737
        self.log(self.output_fmt.upper(), 'output written to', self.output)
 
738
        self.flush()
 
739
 
 
740
def create_oebbook(log, path_or_stream, opts, input_plugin, reader=None,
 
741
        encoding='utf-8', populate=True):
 
742
    '''
 
743
    Create an OEBBook.
 
744
    '''
 
745
    from calibre.ebooks.oeb.base import OEBBook
 
746
    html_preprocessor = HTMLPreProcessor(input_plugin.preprocess_html,
 
747
            opts.preprocess_html, opts)
 
748
    oeb = OEBBook(log, html_preprocessor,
 
749
            pretty_print=opts.pretty_print, input_encoding=encoding)
 
750
    if not populate:
 
751
        return oeb
 
752
    # Read OEB Book into OEBBook
 
753
    log('Parsing all content...')
 
754
    if reader is None:
 
755
        from calibre.ebooks.oeb.reader import OEBReader
 
756
        reader = OEBReader
 
757
 
 
758
    reader()(oeb, path_or_stream)
 
759
    return oeb