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

« back to all changes in this revision

Viewing changes to src/calibre/ebooks/lrf/comic/convert_from.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
 
from __future__ import with_statement
2
 
__license__   = 'GPL v3'
3
 
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
4
 
__docformat__ = 'restructuredtext en'
5
 
 
6
 
'''
7
 
Based on ideas from comiclrf created by FangornUK.
8
 
'''
9
 
 
10
 
import os, sys, shutil, traceback, textwrap, fnmatch
11
 
from uuid import uuid4
12
 
 
13
 
 
14
 
 
15
 
 
16
 
from calibre import extract, terminal_controller, __appname__, __version__
17
 
from calibre.utils.config import Config, StringConfig
18
 
from calibre.ptempfile import PersistentTemporaryDirectory
19
 
from calibre.parallel import Server, ParallelJob
20
 
from calibre.utils.terminfo import ProgressBar
21
 
from calibre.ebooks.lrf.pylrs.pylrs import Book, BookSetting, ImageStream, ImageBlock
22
 
from calibre.ebooks.metadata import MetaInformation
23
 
from calibre.ebooks.metadata.opf import OPFCreator
24
 
from calibre.ebooks.epub.from_html import config as html2epub_config, convert as html2epub
25
 
from calibre.customize.ui import run_plugins_on_preprocess
26
 
try:
27
 
    from calibre.utils.PythonMagickWand import \
28
 
            NewMagickWand, NewPixelWand, \
29
 
            MagickSetImageBorderColor, \
30
 
            MagickReadImage, MagickRotateImage, \
31
 
            MagickTrimImage, PixelSetColor,\
32
 
            MagickNormalizeImage, MagickGetImageWidth, \
33
 
            MagickGetImageHeight, \
34
 
            MagickResizeImage, MagickSetImageType, \
35
 
            GrayscaleType, CatromFilter,  MagickSetImagePage, \
36
 
            MagickBorderImage, MagickSharpenImage, MagickDespeckleImage, \
37
 
            MagickQuantizeImage, RGBColorspace, \
38
 
            MagickWriteImage, DestroyPixelWand, \
39
 
            DestroyMagickWand, CloneMagickWand, \
40
 
            MagickThumbnailImage, MagickCropImage, ImageMagick
41
 
    _imagemagick_loaded = True
42
 
except:
43
 
    _imagemagick_loaded = False
44
 
 
45
 
PROFILES = {
46
 
            # Name : (width, height) in pixels
47
 
            'prs500':(584, 754),
48
 
            # The SONY's LRF renderer (on the PRS500) only uses the first 800x600 block of the image
49
 
            'prs500-landscape': (784, 1012)
50
 
            }
51
 
 
52
 
def extract_comic(path_to_comic_file):
53
 
    '''
54
 
    Un-archive the comic file.
55
 
    '''
56
 
    tdir = PersistentTemporaryDirectory(suffix='_comic_extract')
57
 
    extract(path_to_comic_file, tdir)
58
 
    return tdir
59
 
 
60
 
def find_pages(dir, sort_on_mtime=False, verbose=False):
61
 
    '''
62
 
    Find valid comic pages in a previously un-archived comic.
63
 
 
64
 
    :param dir: Directory in which extracted comic lives
65
 
    :param sort_on_mtime: If True sort pages based on their last modified time.
66
 
                          Otherwise, sort alphabetically.
67
 
    '''
68
 
    extensions = ['jpeg', 'jpg', 'gif', 'png']
69
 
    pages = []
70
 
    for datum in os.walk(dir):
71
 
        for name in datum[-1]:
72
 
            path = os.path.join(datum[0], name)
73
 
            if '__MACOSX' in path: continue
74
 
            for ext in extensions:
75
 
                if path.lower().endswith('.'+ext):
76
 
                    pages.append(path)
77
 
                    break
78
 
    if sort_on_mtime:
79
 
        comparator = lambda x, y : cmp(os.stat(x).st_mtime, os.stat(y).st_mtime)
80
 
    else:
81
 
        comparator = lambda x, y : cmp(os.path.basename(x), os.path.basename(y))
82
 
 
83
 
    pages.sort(cmp=comparator)
84
 
    if verbose:
85
 
        print 'Found comic pages...'
86
 
        print '\t'+'\n\t'.join([os.path.basename(p) for p in pages])
87
 
    return pages
88
 
 
89
 
class PageProcessor(list):
90
 
    '''
91
 
    Contains the actual image rendering logic. See :method:`render` and
92
 
    :method:`process_pages`.
93
 
    '''
94
 
 
95
 
    def __init__(self, path_to_page, dest, opts, num):
96
 
        list.__init__(self)
97
 
        self.path_to_page = path_to_page
98
 
        self.opts         = opts
99
 
        self.num          = num
100
 
        self.dest         = dest
101
 
        self.rotate       = False
102
 
        self.render()
103
 
 
104
 
 
105
 
    def render(self):
106
 
        img = NewMagickWand()
107
 
        if img < 0:
108
 
            raise RuntimeError('Cannot create wand.')
109
 
        if not MagickReadImage(img, self.path_to_page):
110
 
            raise IOError('Failed to read image from: %'%self.path_to_page)
111
 
        width  = MagickGetImageWidth(img)
112
 
        height = MagickGetImageHeight(img)
113
 
        if self.num == 0: # First image so create a thumbnail from it
114
 
            thumb = CloneMagickWand(img)
115
 
            if thumb < 0:
116
 
                raise RuntimeError('Cannot create wand.')
117
 
            MagickThumbnailImage(thumb, 60, 80)
118
 
            MagickWriteImage(thumb, os.path.join(self.dest, 'thumbnail.png'))
119
 
            DestroyMagickWand(thumb)
120
 
        self.pages = [img]
121
 
        if width > height:
122
 
            if self.opts.landscape:
123
 
                self.rotate = True
124
 
            else:
125
 
                split1, split2 = map(CloneMagickWand, (img, img))
126
 
                DestroyMagickWand(img)
127
 
                if split1 < 0 or split2 < 0:
128
 
                    raise RuntimeError('Cannot create wand.')
129
 
                MagickCropImage(split1, (width/2)-1, height, 0, 0)
130
 
                MagickCropImage(split2, (width/2)-1, height, width/2, 0 )
131
 
                self.pages = [split2, split1] if self.opts.right2left else [split1, split2]
132
 
        self.process_pages()
133
 
 
134
 
    def process_pages(self):
135
 
        for i, wand in enumerate(self.pages):
136
 
            pw = NewPixelWand()
137
 
            try:
138
 
                if pw < 0:
139
 
                    raise RuntimeError('Cannot create wand.')
140
 
                PixelSetColor(pw, 'white')
141
 
 
142
 
                MagickSetImageBorderColor(wand, pw)
143
 
                if self.rotate:
144
 
                    MagickRotateImage(wand, pw, -90)
145
 
 
146
 
                # 25 percent fuzzy trim?
147
 
                if not self.opts.disable_trim:
148
 
                    MagickTrimImage(wand, 25*65535/100)
149
 
                MagickSetImagePage(wand, 0,0,0,0)   #Clear page after trim, like a "+repage"
150
 
                # Do the Photoshop "Auto Levels" equivalent
151
 
                if not self.opts.dont_normalize:
152
 
                    MagickNormalizeImage(wand)
153
 
                sizex = MagickGetImageWidth(wand)
154
 
                sizey = MagickGetImageHeight(wand)
155
 
 
156
 
                SCRWIDTH, SCRHEIGHT = PROFILES[self.opts.profile]
157
 
 
158
 
                if self.opts.keep_aspect_ratio:
159
 
                    # Preserve the aspect ratio by adding border
160
 
                    aspect = float(sizex) / float(sizey)
161
 
                    if aspect <= (float(SCRWIDTH) / float(SCRHEIGHT)):
162
 
                        newsizey = SCRHEIGHT
163
 
                        newsizex = int(newsizey * aspect)
164
 
                        deltax = (SCRWIDTH - newsizex) / 2
165
 
                        deltay = 0
166
 
                    else:
167
 
                        newsizex = SCRWIDTH
168
 
                        newsizey = int(newsizex / aspect)
169
 
                        deltax = 0
170
 
                        deltay = (SCRHEIGHT - newsizey) / 2
171
 
                    MagickResizeImage(wand, newsizex, newsizey, CatromFilter, 1.0)
172
 
                    MagickSetImageBorderColor(wand, pw)
173
 
                    MagickBorderImage(wand, pw, deltax, deltay)
174
 
                elif self.opts.wide:
175
 
                    # Keep aspect and Use device height as scaled image width so landscape mode is clean
176
 
                    aspect = float(sizex) / float(sizey)
177
 
                    screen_aspect = float(SCRWIDTH) / float(SCRHEIGHT)
178
 
                    # Get dimensions of the landscape mode screen
179
 
                    # Add 25px back to height for the battery bar.
180
 
                    wscreenx = SCRHEIGHT + 25
181
 
                    wscreeny = int(wscreenx / screen_aspect)
182
 
                    if aspect <= screen_aspect:
183
 
                        newsizey = wscreeny
184
 
                        newsizex = int(newsizey * aspect)
185
 
                        deltax = (wscreenx - newsizex) / 2
186
 
                        deltay = 0
187
 
                    else:
188
 
                        newsizex = wscreenx
189
 
                        newsizey = int(newsizex / aspect)
190
 
                        deltax = 0
191
 
                        deltay = (wscreeny - newsizey) / 2
192
 
                    MagickResizeImage(wand, newsizex, newsizey, CatromFilter, 1.0)
193
 
                    MagickSetImageBorderColor(wand, pw)
194
 
                    MagickBorderImage(wand, pw, deltax, deltay)
195
 
                else:
196
 
                    MagickResizeImage(wand, SCRWIDTH, SCRHEIGHT, CatromFilter, 1.0)
197
 
 
198
 
                if not self.opts.dont_sharpen:
199
 
                    MagickSharpenImage(wand, 0.0, 1.0)
200
 
 
201
 
                MagickSetImageType(wand, GrayscaleType)
202
 
 
203
 
                if self.opts.despeckle:
204
 
                    MagickDespeckleImage(wand)
205
 
 
206
 
                MagickQuantizeImage(wand, self.opts.colors, RGBColorspace, 0, 1, 0)
207
 
                dest = '%d_%d.png'%(self.num, i)
208
 
                dest = os.path.join(self.dest, dest)
209
 
                MagickWriteImage(wand, dest+'8')
210
 
                os.rename(dest+'8', dest)
211
 
                self.append(dest)
212
 
            finally:
213
 
                if pw > 0:
214
 
                    DestroyPixelWand(pw)
215
 
                DestroyMagickWand(wand)
216
 
 
217
 
def render_pages(tasks, dest, opts, notification=None):
218
 
    '''
219
 
    Entry point for the job server.
220
 
    '''
221
 
    failures, pages = [], []
222
 
    with ImageMagick():
223
 
        for num, path in tasks:
224
 
            try:
225
 
                pages.extend(PageProcessor(path, dest, opts, num))
226
 
                msg = _('Rendered %s')%path
227
 
            except:
228
 
                failures.append(path)
229
 
                msg = _('Failed %s')%path
230
 
                if opts.verbose:
231
 
                    msg += '\n' + traceback.format_exc()
232
 
            if notification is not None:
233
 
                notification(0.5, msg)
234
 
 
235
 
    return pages, failures
236
 
 
237
 
 
238
 
class JobManager(object):
239
 
    '''
240
 
    Simple job manager responsible for keeping track of overall progress.
241
 
    '''
242
 
 
243
 
    def __init__(self, total, update):
244
 
        self.total  = total
245
 
        self.update = update
246
 
        self.done   = 0
247
 
        self.add_job        = lambda j: j
248
 
        self.output         = lambda j: j
249
 
        self.start_work     = lambda j: j
250
 
        self.job_done       = lambda j: j
251
 
 
252
 
    def status_update(self, job):
253
 
        self.done += 1
254
 
        #msg = msg%os.path.basename(job.args[0])
255
 
        self.update(float(self.done)/self.total, job.msg)
256
 
 
257
 
def process_pages(pages, opts, update):
258
 
    '''
259
 
    Render all identified comic pages.
260
 
    '''
261
 
    if not _imagemagick_loaded:
262
 
        raise RuntimeError('Failed to load ImageMagick')
263
 
 
264
 
    tdir = PersistentTemporaryDirectory('_comic2lrf_pp')
265
 
    job_manager = JobManager(len(pages), update)
266
 
    server = Server()
267
 
    jobs = []
268
 
    tasks = server.split(pages)
269
 
    for task in tasks:
270
 
        jobs.append(ParallelJob('render_pages', lambda s:s, job_manager=job_manager,
271
 
                                args=[task, tdir, opts]))
272
 
        server.add_job(jobs[-1])
273
 
    server.wait()
274
 
    server.killall()
275
 
    server.close()
276
 
    ans, failures = [], []
277
 
 
278
 
    for job in jobs:
279
 
        if job.result is None:
280
 
            raise Exception(_('Failed to process comic: %s\n\n%s')%(job.exception, job.traceback))
281
 
        pages, failures_ = job.result
282
 
        ans += pages
283
 
        failures += failures_
284
 
    return ans, failures, tdir
285
 
 
286
 
def config(defaults=None,output_format='lrf'):
287
 
    desc = _('Options to control the conversion of comics (CBR, CBZ) files into ebooks')
288
 
    if defaults is None:
289
 
        c = Config('comic', desc)
290
 
    else:
291
 
        c = StringConfig(defaults, desc)
292
 
    c.add_opt('title', ['-t', '--title'],
293
 
              help=_('Title for generated ebook. Default is to use the filename.'))
294
 
    c.add_opt('author', ['-a', '--author'],
295
 
              help=_('Set the author in the metadata of the generated ebook. Default is %default'),
296
 
              default=_('Unknown'))
297
 
    c.add_opt('output', ['-o', '--output'],
298
 
              help=_('Path to output file. By default a file is created in the current directory.'))
299
 
    c.add_opt('colors', ['-c', '--colors'], type='int', default=64,
300
 
              help=_('Number of colors for grayscale image conversion. Default: %default'))
301
 
    c.add_opt('dont_normalize', ['-n', '--disable-normalize'], default=False,
302
 
              help=_('Disable normalize (improve contrast) color range for pictures. Default: False'))
303
 
    c.add_opt('keep_aspect_ratio', ['-r', '--keep-aspect-ratio'], default=False,
304
 
              help=_('Maintain picture aspect ratio. Default is to fill the screen.'))
305
 
    c.add_opt('dont_sharpen', ['-s', '--disable-sharpen'], default=False,
306
 
              help=_('Disable sharpening.'))
307
 
    c.add_opt('disable_trim', ['--disable-trim'], default=False,
308
 
              help=_('Disable trimming of comic pages. For some comics, '
309
 
                     'trimming might remove content as well as borders.'))
310
 
    c.add_opt('landscape', ['-l', '--landscape'], default=False,
311
 
              help=_("Don't split landscape images into two portrait images"))
312
 
    c.add_opt('wide', ['-w', '--wide-aspect'], default=False,
313
 
              help=_("Keep aspect ratio and scale image using screen height as image width for viewing in landscape mode."))
314
 
    c.add_opt('right2left', ['--right2left'], default=False, action='store_true',
315
 
              help=_('Used for right-to-left publications like manga. Causes landscape pages to be split into portrait pages from right to left.'))
316
 
    c.add_opt('despeckle', ['-d', '--despeckle'], default=False,
317
 
              help=_('Enable Despeckle. Reduces speckle noise. May greatly increase processing time.'))
318
 
    c.add_opt('no_sort', ['--no-sort'], default=False,
319
 
              help=_("Don't sort the files found in the comic alphabetically by name. Instead use the order they were added to the comic."))
320
 
    c.add_opt('profile', ['-p', '--profile'], default='prs500', choices=PROFILES.keys(),
321
 
              help=_('Choose a profile for the device you are generating this file for. The default is the SONY PRS-500 with a screen size of 584x754 pixels. This is suitable for any reader with the same screen size. Choices are %s')%PROFILES.keys())
322
 
    c.add_opt('verbose', ['-v', '--verbose'], default=0, action='count',
323
 
              help=_('Be verbose, useful for debugging. Can be specified multiple times for greater verbosity.'))
324
 
    c.add_opt('no_progress_bar', ['--no-progress-bar'], default=False,
325
 
                      help=_("Don't show progress bar."))
326
 
    if output_format == 'pdf':
327
 
        c.add_opt('no_process',['--no_process'], default=False,
328
 
                      help=_("Apply no processing to the image"))
329
 
    return c
330
 
 
331
 
def option_parser(output_format='lrf'):
332
 
    c = config(output_format=output_format)
333
 
    return c.option_parser(usage=_('''\
334
 
%prog [options] comic.cb[z|r]
335
 
 
336
 
Convert a comic in a CBZ or CBR file to an ebook.
337
 
'''))
338
 
 
339
 
def create_epub(pages, profile, opts, thumbnail=None):
340
 
    wrappers = []
341
 
    WRAPPER = textwrap.dedent('''\
342
 
    <html>
343
 
        <head>
344
 
            <title>Page #%d</title>
345
 
            <style type="text/css">@page {margin:0pt; padding: 0pt;}</style>
346
 
        </head>
347
 
        <body style="margin: 0pt; padding: 0pt">
348
 
            <div style="text-align:center">
349
 
                <img src="%s" alt="comic page #%d" />
350
 
            </div>
351
 
        </body>
352
 
    </html>
353
 
    ''')
354
 
    dir = os.path.dirname(pages[0])
355
 
    for i, page in enumerate(pages):
356
 
        wrapper = WRAPPER%(i+1, os.path.basename(page), i+1)
357
 
        page = os.path.join(dir, 'page_%d.html'%(i+1))
358
 
        open(page, 'wb').write(wrapper)
359
 
        wrappers.append(page)
360
 
 
361
 
    mi  = MetaInformation(opts.title, [opts.author])
362
 
    opf = OPFCreator(dir, mi)
363
 
    opf.create_manifest([(w, None) for w in wrappers])
364
 
    opf.create_spine(wrappers)
365
 
    metadata = os.path.join(dir, 'metadata.opf')
366
 
    opf.render(open(metadata, 'wb'))
367
 
    opts2 = html2epub_config('margin_left=0\nmargin_right=0\nmargin_top=0\nmargin_bottom=0').parse()
368
 
    opts2.output = opts.output
369
 
    html2epub(metadata, opts2)
370
 
 
371
 
def create_lrf(pages, profile, opts, thumbnail=None):
372
 
    width, height = PROFILES[profile]
373
 
    ps = {}
374
 
    ps['topmargin']      = 0
375
 
    ps['evensidemargin'] = 0
376
 
    ps['oddsidemargin']  = 0
377
 
    ps['textwidth']      = width
378
 
    ps['textheight']     = height
379
 
    book = Book(title=opts.title, author=opts.author,
380
 
            bookid=uuid4().hex,
381
 
            publisher='%s %s'%(__appname__, __version__), thumbnail=thumbnail,
382
 
            category='Comic', pagestyledefault=ps,
383
 
            booksetting=BookSetting(screenwidth=width, screenheight=height))
384
 
    for page in pages:
385
 
        imageStream = ImageStream(page)
386
 
        _page = book.create_page()
387
 
        _page.append(ImageBlock(refstream=imageStream,
388
 
                    blockwidth=width, blockheight=height, xsize=width,
389
 
                    ysize=height, x1=width, y1=height))
390
 
        book.append(_page)
391
 
 
392
 
    book.renderLrf(open(opts.output, 'wb'))
393
 
    print _('Output written to'), opts.output
394
 
 
395
 
 
396
 
def create_pdf(pages, profile, opts, thumbnail=None,toc=None):
397
 
    width, height = PROFILES[profile]
398
 
 
399
 
    from reportlab.pdfgen import canvas
400
 
 
401
 
    cur_page=0
402
 
    heading = []
403
 
    if toc != None:
404
 
        if len(toc) == 1:
405
 
            toc = None
406
 
        else:
407
 
            toc_index = 0
408
 
            base_cur = 0
409
 
            rem = 0
410
 
            breaker = False
411
 
            while True:
412
 
                letter=toc[0][0][base_cur]
413
 
                for i in range(len(toc)):
414
 
                    if letter != toc[i][0][base_cur]:
415
 
                        breaker = True
416
 
                if breaker:
417
 
                    break
418
 
                if letter == os.sep:
419
 
                    rem=base_cur
420
 
                base_cur += 1
421
 
            toc.append(("Not seen",-1))
422
 
 
423
 
 
424
 
    pdf = canvas.Canvas(filename=opts.output, pagesize=(width,height+15))
425
 
    pdf.setAuthor(opts.author)
426
 
    pdf.setTitle(opts.title)
427
 
 
428
 
 
429
 
    for page in pages:
430
 
        if opts.keep_aspect_ratio:
431
 
            img = NewMagickWand()
432
 
            if img < 0:
433
 
                raise RuntimeError('Cannot create wand.')
434
 
            if not MagickReadImage(img, page):
435
 
                raise IOError('Failed to read image from: %'%page)
436
 
            sizex  = MagickGetImageWidth(img)
437
 
            sizey = MagickGetImageHeight(img)
438
 
            if opts.keep_aspect_ratio:
439
 
                # Preserve the aspect ratio by adding border
440
 
                aspect = float(sizex) / float(sizey)
441
 
                if aspect <= (float(width) / float(height)):
442
 
                    newsizey = height
443
 
                    newsizex = int(newsizey * aspect)
444
 
                    deltax = (width - newsizex) / 2
445
 
                    deltay = 0
446
 
                else:
447
 
                    newsizex = width
448
 
                    newsizey = int(newsizex / aspect)
449
 
                    deltax = 0
450
 
                    deltay = (height - newsizey) / 2
451
 
            pdf.drawImage(page, x=deltax,y=deltay,width=newsizex, height=newsizey)
452
 
        else:
453
 
            pdf.drawImage(page, x=0,y=0,width=width, height=height)
454
 
        if toc != None:
455
 
            if toc[toc_index][1] == cur_page:
456
 
                tmp=toc[toc_index][0]
457
 
                toc_current=tmp[rem:len(tmp)-4]
458
 
                index=0
459
 
                while True:
460
 
                    key = 'page%d-%d' % (cur_page, index)
461
 
                    pdf.bookmarkPage(key)
462
 
                    (head,dummy,list)=toc_current.partition(os.sep)
463
 
                    try:
464
 
                        if heading[index] != head:
465
 
                            heading[index] = head
466
 
                            pdf.addOutlineEntry(title=head,key=key,level=index)
467
 
                    except:
468
 
                        heading.append(head)
469
 
                        pdf.addOutlineEntry(title=head,key=key,level=index)
470
 
                    index += 1
471
 
                    toc_current=list
472
 
                    if dummy == "":
473
 
                        break
474
 
                toc_index += 1
475
 
            cur_page += 1
476
 
        pdf.showPage()
477
 
    # Write the document to disk
478
 
    pdf.save()
479
 
 
480
 
 
481
 
def do_convert(path_to_file, opts, notification=lambda m, p: p, output_format='lrf'):
482
 
    path_to_file = run_plugins_on_preprocess(path_to_file)
483
 
    source = path_to_file
484
 
    to_delete = []
485
 
    toc = []
486
 
    list = []
487
 
    pages = []
488
 
 
489
 
 
490
 
    if not opts.title:
491
 
        opts.title = os.path.splitext(os.path.basename(source))[0]
492
 
    if not opts.output:
493
 
        opts.output = os.path.abspath(os.path.splitext(os.path.basename(source))[0]+'.'+output_format)
494
 
    if os.path.isdir(source):
495
 
        for path in all_files( source , '*.cbr|*.cbz' ):
496
 
            list.append( path )
497
 
    else:
498
 
            list= [ os.path.abspath(source) ]
499
 
 
500
 
    for source in list:
501
 
        tdir  = extract_comic(source)
502
 
        new_pages = find_pages(tdir, sort_on_mtime=opts.no_sort, verbose=opts.verbose)
503
 
        thumbnail = None
504
 
        if not new_pages:
505
 
            raise ValueError('Could not find any pages in the comic: %s'%source)
506
 
        if not getattr(opts, 'no_process', False):
507
 
            new_pages, failures, tdir2 = process_pages(new_pages, opts, notification)
508
 
            if not new_pages:
509
 
                raise ValueError('Could not find any valid pages in the comic: %s'%source)
510
 
            if failures:
511
 
                print 'Could not process the following pages (run with --verbose to see why):'
512
 
                for f in failures:
513
 
                    print '\t', f
514
 
            thumbnail = os.path.join(tdir2, 'thumbnail.png')
515
 
            if not os.access(thumbnail, os.R_OK):
516
 
                thumbnail = None
517
 
        toc.append((source,len(pages)))
518
 
        pages.extend(new_pages)
519
 
        to_delete.append(tdir)
520
 
 
521
 
 
522
 
    if output_format == 'lrf':
523
 
        create_lrf(pages, opts.profile, opts, thumbnail=thumbnail)
524
 
    if output_format == 'epub':
525
 
        create_epub(pages, opts.profile, opts, thumbnail=thumbnail)
526
 
    if output_format == 'pdf':
527
 
        create_pdf(pages, opts.profile, opts, thumbnail=thumbnail,toc=toc)
528
 
    for tdir in to_delete:
529
 
        shutil.rmtree(tdir)
530
 
 
531
 
 
532
 
def all_files(root, patterns='*'):
533
 
    # Expand patterns from semicolon-separated string to list
534
 
    patterns = patterns.split('|')
535
 
    for path, subdirs, files in os.walk(root):
536
 
        files.sort( )
537
 
        for name in files:
538
 
            for pattern in patterns:
539
 
                if fnmatch.fnmatch(name, pattern):
540
 
                    yield os.path.join(path, name)
541
 
                    break
542
 
 
543
 
 
544
 
def main(args=sys.argv, notification=None, output_format='lrf'):
545
 
    parser = option_parser(output_format=output_format)
546
 
    opts, args = parser.parse_args(args)
547
 
    if len(args) < 2:
548
 
        parser.print_help()
549
 
        print '\nYou must specify a file to convert'
550
 
        return 1
551
 
 
552
 
    if not callable(notification):
553
 
        pb = ProgressBar(terminal_controller, _('Rendering comic pages...'),
554
 
                         no_progress_bar=opts.no_progress_bar or getattr(opts, 'no_process', False))
555
 
        notification = pb.update
556
 
 
557
 
    source = os.path.abspath(args[1])
558
 
    do_convert(source, opts, notification, output_format=output_format)
559
 
    return 0
560
 
 
561
 
if __name__ == '__main__':
562
 
    sys.exit(main())