From df883310c2a582b504fab6ac2f55f1c3da739142 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 30 Jul 2008 17:22:09 -0700 Subject: [PATCH 01/13] Fixed encoding bug on chinese windows vista --- src/calibre/gui2/status.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/status.py b/src/calibre/gui2/status.py index 8b059f5711..20df329364 100644 --- a/src/calibre/gui2/status.py +++ b/src/calibre/gui2/status.py @@ -5,7 +5,7 @@ import re from PyQt4.QtGui import QStatusBar, QMovie, QLabel, QFrame, QHBoxLayout, QPixmap, \ QVBoxLayout, QSizePolicy, QToolButton, QIcon from PyQt4.QtCore import Qt, QSize, SIGNAL -from calibre import fit_image +from calibre import fit_image, preferred_encoding from calibre.gui2 import qstring_to_unicode class BookInfoDisplay(QFrame): @@ -77,8 +77,12 @@ class BookInfoDisplay(QFrame): for key in data.keys(): txt = data[key] #txt = '
\n'.join(textwrap.wrap(txt, 120)) - rows += '%s:%s'%(key, txt) - self.book_data.setText(''+rows+'
') + if isinstance(key, str): + key = key.decode(preferred_encoding, 'replace') + if isinstance(txt, str): + txt = txt.decode(preferred_encoding, 'replace') + rows += u'%s:%s'%(key, txt) + self.book_data.setText(u''+rows+u'
') self.clear_message() self.setVisible(True) From 5c8c683c2f0bef52f44e5b0fce0130505974f761 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 31 Jul 2008 05:26:31 -0700 Subject: [PATCH 02/13] Fix #874 (UnicodeDecodeError: in feeds2disk) --- src/calibre/utils/genshi/core.py | 4 +++- src/calibre/web/feeds/news.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/calibre/utils/genshi/core.py b/src/calibre/utils/genshi/core.py index aaa10edc82..d64860f086 100644 --- a/src/calibre/utils/genshi/core.py +++ b/src/calibre/utils/genshi/core.py @@ -491,7 +491,9 @@ class Markup(unicode): if hasattr(text, '__html__'): return Markup(text.__html__()) - text = unicode(text).replace('&', '&') \ + if isinstance(text, str): + text = text.decode('utf-8', 'replace') + text = text.replace('&', '&') \ .replace('<', '<') \ .replace('>', '>') if quotes: diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index 9cc7139b28..33f24b5a05 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -7,7 +7,7 @@ Defines various abstract base classes that can be subclassed to create powerful __docformat__ = "restructuredtext en" -import logging, os, cStringIO, time, traceback, re, urlparse +import logging, os, cStringIO, time, traceback, re, urlparse, sys from collections import defaultdict from functools import partial @@ -533,6 +533,9 @@ class BasicNewsRecipe(object, LoggingInterface): self.image_map[feed.image_url] = img except: pass + if isinstance(feed.image_url, str): + feed.image_url = feed.image_url.decode(sys.getfilesystemencoding(), 'strict') + templ = templates.FeedTemplate() return templ.generate(feed, self.description_limiter).render(doctype='xhtml') From 92b65434331d2a60021ec6a9a2c66982398ded25 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 31 Jul 2008 14:59:31 -0700 Subject: [PATCH 03/13] Fix #920 --- src/calibre/linux_installer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/linux_installer.py b/src/calibre/linux_installer.py index b433c36ffc..9415cb054a 100644 --- a/src/calibre/linux_installer.py +++ b/src/calibre/linux_installer.py @@ -216,7 +216,10 @@ cd $frozen_path def extract_tarball(tar, destdir): if hasattr(tar, 'read'): - tarfile.open(fileobj=tar, mode='r').extractall(destdir) + try: + tarfile.open(fileobj=tar, mode='r').extractall(destdir) + except: # tarfile.py on Fedora 9 is buggy + subprocess.check_call(['tar', 'xvjf', tar.name, destdir]) else: tarfile.open(tar, 'r').extractall(destdir) From 917cbdf3480d014f62750b4727f100c0b8a8358a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 31 Jul 2008 16:46:44 -0700 Subject: [PATCH 04/13] IGN:... --- src/calibre/linux_installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/linux_installer.py b/src/calibre/linux_installer.py index 9415cb054a..ad24508900 100644 --- a/src/calibre/linux_installer.py +++ b/src/calibre/linux_installer.py @@ -219,7 +219,7 @@ def extract_tarball(tar, destdir): try: tarfile.open(fileobj=tar, mode='r').extractall(destdir) except: # tarfile.py on Fedora 9 is buggy - subprocess.check_call(['tar', 'xvjf', tar.name, destdir]) + subprocess.check_call(['tar', 'xvjf', tar.name, '-C', destdir]) else: tarfile.open(tar, 'r').extractall(destdir) From 8e964de8c4541ef3efa1ae78441fe59d8205f1c1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 31 Jul 2008 21:51:17 -0700 Subject: [PATCH 05/13] IGN:Look for Qt translations in the right place on Mandriva --- resources.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/resources.py b/resources.py index a969329a16..f1bea6fd08 100644 --- a/resources.py +++ b/resources.py @@ -22,17 +22,19 @@ def main(args=sys.argv): bytes = repr(open(path, 'rb').read()) data += key + ' = ' + bytes + '\n\n' - TPATH = '/usr/share/qt4/translations' - if os.path.exists(TPATH): - files = glob.glob(TPATH + '/qt_??.qm') - - for f in files: - key = os.path.basename(f).partition('.')[0] - bytes = repr(open(f, 'rb').read()) - data += key + ' = ' + bytes + '\n\n' - - else: - print 'WARNING: Could not find Qt transations in', TPATH + translations_found = False + for TPATH in ('/usr/share/qt4/translations', '/usr/lib/qt4/translations'): + if os.path.exists(TPATH): + files = glob.glob(TPATH + '/qt_??.qm') + + for f in files: + key = os.path.basename(f).partition('.')[0] + bytes = repr(open(f, 'rb').read()) + data += key + ' = ' + bytes + '\n\n' + translations_found = True + break + if not translations_found: + print 'WARNING: Could not find Qt transations' open('src'+os.sep+__appname__+os.sep+'/resources.py', 'wb').write(data) return 0 From 4e6c1be0e412e114b5b973e037031622c6509249 Mon Sep 17 00:00:00 2001 From: "Marshall T. Vandegrift" Date: Sat, 2 Aug 2008 16:10:56 -0400 Subject: [PATCH 06/13] Add correct namespace scoping to generated OPF --- src/calibre/ebooks/metadata/opf.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/calibre/ebooks/metadata/opf.xml b/src/calibre/ebooks/metadata/opf.xml index 10623715ff..3a6cfec58c 100644 --- a/src/calibre/ebooks/metadata/opf.xml +++ b/src/calibre/ebooks/metadata/opf.xml @@ -1,14 +1,14 @@ - - ${mi.title} - ${author} - ${mi.application_id} + + ${mi.title} + ${author} + ${mi.application_id} ${mi.language if mi.language else 'Unknown'} ${mi.category} From 5f7e9f898274f7e3fce54013be162ac66ec2b61a Mon Sep 17 00:00:00 2001 From: "Marshall T. Vandegrift" Date: Sat, 2 Aug 2008 16:13:34 -0400 Subject: [PATCH 07/13] Fix Genshi default namespace bug --- src/calibre/utils/genshi/output.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/calibre/utils/genshi/output.py b/src/calibre/utils/genshi/output.py index d07d3f72b0..3664d92f37 100644 --- a/src/calibre/utils/genshi/output.py +++ b/src/calibre/utils/genshi/output.py @@ -573,6 +573,7 @@ class NamespaceFlattener(object): def __call__(self, stream): prefixes = dict([(v, [k]) for k, v in self.prefixes.items()]) namespaces = {XML_NAMESPACE.uri: ['xml']} + default = prefixes.get('', ['']) def _push_ns(prefix, uri): namespaces.setdefault(uri, []).append(prefix) prefixes.setdefault(prefix, []).append(uri) @@ -596,14 +597,14 @@ class NamespaceFlattener(object): tagname = tag.localname tagns = tag.namespace - if tagns: + if tagns and tagns != default[-1]: if tagns in namespaces: prefix = namespaces[tagns][-1] if prefix: tagname = u'%s:%s' % (prefix, tagname) else: _push_ns_attr((u'xmlns', tagns)) - _push_ns('', tagns) + default.push(tagns) new_attrs = [] for attr, value in attrs: @@ -626,7 +627,7 @@ class NamespaceFlattener(object): elif kind is END: tagname = data.localname tagns = data.namespace - if tagns: + if tagns and tagns != default[-1]: prefix = namespaces[tagns][-1] if prefix: tagname = u'%s:%s' % (prefix, tagname) @@ -634,12 +635,19 @@ class NamespaceFlattener(object): elif kind is START_NS: prefix, uri = data - if uri not in namespaces: - prefix = prefixes.get(uri, [prefix])[-1] + push_attr = False + if prefix is '' and default[-1] != uri: + default.append(uri) _push_ns_attr(_make_ns_attr(prefix, uri)) - _push_ns(prefix, uri) + elif uri not in namespaces: + prefix = namespaces.get(uri, [prefix])[-1] + _push_ns_attr(_make_ns_attr(prefix, uri)) + if prefix is not '': + _push_ns(prefix, uri) elif kind is END_NS: + if data is '': + default.pop() if data in prefixes: uris = prefixes.get(data) uri = uris.pop() From 6d2c277d93193644b2b16489d8b1c4bbc111faa5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Aug 2008 15:10:10 -0700 Subject: [PATCH 08/13] Detect proxies automatically on windows --- src/calibre/__init__.py | 90 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 11 deletions(-) diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 2dc9ad1a6b..68d202f201 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -76,7 +76,11 @@ if iswindows: if not winutil: raise RuntimeError('Failed to load the winutil plugin: %s'%winutilerror) sys.argv[1:] = winutil.argv()[1:] - + win32event = __import__('win32event') + winerror = __import__('winerror') + win32api = __import__('win32api') +else: + import fcntl _abspath = os.path.abspath def my_abspath(path, encoding=sys.getfilesystemencoding()): @@ -313,15 +317,46 @@ def extract(path, dir): raise Exception('Unknown archive type') extractor(path, dir) +def get_proxies(self): + proxies = {} + if iswindows: + try: + winreg = __import__('_winreg') + settings = winreg.OpenKey(winreg.HKEY_CURRENT_USER, + 'Software\\Microsoft\\Windows' + '\\CurrentVersion\\Internet Settings') + proxy = winreg.QueryValueEx(settings, "ProxyEnable")[0] + if proxy: + server = str(winreg.QueryValueEx(settings, 'ProxyServer')[0]) + if ';' in server: + for p in server.split(';'): + protocol, address = p.split('=') + proxies[protocol] = address + else: + proxies['http'] = server + proxies['ftp'] = server + settings.Close() + except Exception, e: + print('Unable to detect proxy settings: %s' % str(e)) + if proxies: + print('Using proxies: %s' % proxies) + else: + for q in ('http', 'ftp'): + proxy = os.environ.get(q+'_proxy', None) + if not proxy: continue + if proxy.startswith(q+'://'): + proxy = proxy[7:] + proxies[q] = proxy + return proxies + + def browser(honor_time=False): - http_proxy = os.environ.get('http_proxy', None) + http_proxy = get_proxies().get('http', None) opener = mechanize.Browser() opener.set_handle_refresh(True, honor_time=honor_time) opener.set_handle_robots(False) opener.addheaders = [('User-agent', 'Mozilla/5.0 (X11; U; i686 Linux; en_US; rv:1.8.0.4) Gecko/20060508 Firefox/1.5.0.4')] if http_proxy: - if http_proxy.startswith('http://'): - http_proxy = http_proxy[7:] opener.set_proxies({'http':http_proxy}) return opener @@ -474,7 +509,44 @@ def _clean_lock_file(file): os.remove(file.name) except: pass + +class LockError(Exception): + pass +class ExclusiveFile(object): + def __init__(self, path, timeout=10): + self.path = path + self.timeout = timeout + + def __enter__(self): + self.file = open(self.path, 'a+b') + self.file.seek(0) + timeout = self.timeout + if iswindows: + name = ('Local\\'+(__appname__+self.file.name).replace('\\', '_'))[:201] + while self.timeout < 0 or timeout >= 0: + self.mutex = win32event.CreateMutex(None, False, name) + if win32api.GetLastError() != winerror.ERROR_ALREADY_EXISTS: break + time.sleep(1) + timeout -= 1 + else: + while self.timeout < 0 or timeout >= 0: + try: + fcntl.lockf(self.file.fileno(), fcntl.LOCK_EX|fcntl.LOCK_NB) + break + except IOError: + time.sleep(1) + timeout -= 1 + if timeout < 0 and self.timeout >= 0: + self.file.close() + raise LockError + return self.file + + def __exit__(self, type, value, traceback): + self.file.close() + if iswindows: + win32api.CloseHandle(self.mutex) + def singleinstance(name): ''' Return True if no other instance of the application identified by name is running, @@ -483,16 +555,12 @@ def singleinstance(name): @type name: string ''' if iswindows: - from win32event import CreateMutex - from win32api import CloseHandle, GetLastError - from winerror import ERROR_ALREADY_EXISTS mutexname = 'mutexforsingleinstanceof'+__appname__+name - mutex = CreateMutex(None, False, mutexname) + mutex = win32event.CreateMutex(None, False, mutexname) if mutex: - atexit.register(CloseHandle, mutex) - return not GetLastError() == ERROR_ALREADY_EXISTS + atexit.register(win32api.CloseHandle, mutex) + return not win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS else: - import fcntl global _lock_file path = os.path.expanduser('~/.'+__appname__+'_'+name+'.lock') try: From 313bc4d51b2fc56db2e4b7d929234083c207e61e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Aug 2008 15:53:22 -0700 Subject: [PATCH 09/13] IGN:Remove wmi initialization for the device scanner as wmi is no longer needed --- src/calibre/gui2/device.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index ddcc7208ba..9d76f6cf67 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -25,12 +25,7 @@ class DeviceDetector(QThread): self.keep_going = True def run(self): - _wmi = None - if iswindows: - import wmi, pythoncom - pythoncom.CoInitialize() - _wmi = wmi.WMI() - scanner = DeviceScanner(_wmi) + scanner = DeviceScanner() while self.keep_going: scanner.scan() for device in self.devices: From acc94d3736dbf052682e47d630fce698e2b31620 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 2 Aug 2008 22:31:59 -0700 Subject: [PATCH 10/13] Integrate conversion of comics into the GUI --- installer/linux/freeze.py | 4 +- src/calibre/__init__.py | 3 +- src/calibre/devices/libusb.py | 2 +- src/calibre/ebooks/__init__.py | 2 +- src/calibre/ebooks/lrf/comic/convert_from.py | 107 ++++---- src/calibre/gui2/dialogs/comicconf.py | 85 ++++++ src/calibre/gui2/dialogs/comicconf.ui | 166 ++++++++++++ src/calibre/gui2/main.py | 127 +++++++-- src/calibre/library/database.py | 2 +- src/calibre/linux.py | 3 +- src/calibre/linux_installer.py | 4 +- src/calibre/parallel.py | 3 + src/calibre/utils/config.py | 269 +++++++++++++++++++ 13 files changed, 706 insertions(+), 71 deletions(-) create mode 100644 src/calibre/gui2/dialogs/comicconf.py create mode 100644 src/calibre/gui2/dialogs/comicconf.ui create mode 100644 src/calibre/utils/config.py diff --git a/installer/linux/freeze.py b/installer/linux/freeze.py index 4627ad90ad..34c2b0d10d 100644 --- a/installer/linux/freeze.py +++ b/installer/linux/freeze.py @@ -19,6 +19,8 @@ EXTRAS = ('/usr/lib/python2.5/site-packages/PIL', os.path.expanduser('~/ SQLITE = '/usr/lib/libsqlite3.so.0' DBUS = '/usr/lib/libdbus-1.so.3' LIBMNG = '/usr/lib/libmng.so.1' +LIBZ = '/lib/libz.so.1' +LIBUSB = '/lib/libusb.so' CALIBRESRC = os.path.join(CALIBREPREFIX, 'src') @@ -117,7 +119,7 @@ binaries += [('clit', CLIT, 'BINARY'), ('pdftohtml', PDFTOHTML, 'BINARY'), ('libunrar.so', LIBUNRAR, 'BINARY')] print 'Adding external libraries...' -binaries += [ (os.path.basename(x), x, 'BINARY') for x in (SQLITE, DBUS, LIBMNG)] +binaries += [ (os.path.basename(x), x, 'BINARY') for x in (SQLITE, DBUS, LIBMNG, LIBZ, LIBUSB)] qt = [] diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 68d202f201..8caebc5be2 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -26,6 +26,7 @@ terminal_controller = TerminalController(sys.stdout) iswindows = 'win32' in sys.platform.lower() or 'win64' in sys.platform.lower() isosx = 'darwin' in sys.platform.lower() islinux = not(iswindows or isosx) +isfrozen = hasattr(sys, 'frozen') try: locale.setlocale(locale.LC_ALL, '') @@ -351,11 +352,11 @@ def get_proxies(self): def browser(honor_time=False): - http_proxy = get_proxies().get('http', None) opener = mechanize.Browser() opener.set_handle_refresh(True, honor_time=honor_time) opener.set_handle_robots(False) opener.addheaders = [('User-agent', 'Mozilla/5.0 (X11; U; i686 Linux; en_US; rv:1.8.0.4) Gecko/20060508 Firefox/1.5.0.4')] + http_proxy = get_proxies().get('http', None) if http_proxy: opener.set_proxies({'http':http_proxy}) return opener diff --git a/src/calibre/devices/libusb.py b/src/calibre/devices/libusb.py index 7d5be468f2..1502772bf4 100644 --- a/src/calibre/devices/libusb.py +++ b/src/calibre/devices/libusb.py @@ -8,7 +8,7 @@ from ctypes import cdll, POINTER, byref, pointer, Structure, \ c_ubyte, c_ushort, c_int, c_char, c_void_p, c_byte, c_uint from errno import EBUSY, ENOMEM -from calibre import iswindows, isosx, load_library +from calibre import iswindows, isosx, load_library, isfrozen _libusb_name = 'libusb' PATH_MAX = 511 if iswindows else 1024 if isosx else 4096 diff --git a/src/calibre/ebooks/__init__.py b/src/calibre/ebooks/__init__.py index dd7e8fc96a..2716963e8d 100644 --- a/src/calibre/ebooks/__init__.py +++ b/src/calibre/ebooks/__init__.py @@ -17,4 +17,4 @@ class UnknownFormatError(Exception): BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'htm', 'xhtm', 'html', 'xhtml', 'epub', 'pdf', 'prc', 'mobi', 'azw', - 'epub', 'fb2', 'djvu', 'lrx'] + 'epub', 'fb2', 'djvu', 'lrx', 'cbr', 'cbz'] diff --git a/src/calibre/ebooks/lrf/comic/convert_from.py b/src/calibre/ebooks/lrf/comic/convert_from.py index 829e331526..847b588aef 100755 --- a/src/calibre/ebooks/lrf/comic/convert_from.py +++ b/src/calibre/ebooks/lrf/comic/convert_from.py @@ -10,8 +10,9 @@ Based on ideas from comiclrf created by FangornUK. import os, sys, traceback, shutil from uuid import uuid4 -from calibre import extract, OptionParser, detect_ncpus, terminal_controller, \ +from calibre import extract, detect_ncpus, terminal_controller, \ __appname__, __version__ +from calibre.utils.config import Config, StringConfig from calibre.ptempfile import PersistentTemporaryDirectory from calibre.utils.threadpool import ThreadPool, WorkRequest from calibre.utils.terminfo import ProgressBar @@ -21,7 +22,7 @@ try: NewMagickWand, NewPixelWand, \ MagickSetImageBorderColor, \ MagickReadImage, MagickRotateImage, \ - MagickTrimImage, \ + MagickTrimImage, PixelSetColor,\ MagickNormalizeImage, MagickGetImageWidth, \ MagickGetImageHeight, \ MagickResizeImage, MagickSetImageType, \ @@ -133,7 +134,7 @@ class PageProcessor(list): pw = NewPixelWand() if pw < 0: raise RuntimeError('Cannot create wand.') - #flag = PixelSetColor(pw, 'white') + PixelSetColor(pw, 'white') MagickSetImageBorderColor(wand, pw) @@ -145,7 +146,7 @@ class PageProcessor(list): MagickSetImagePage(wand, 0,0,0,0) #Clear page after trim, like a "+repage" # Do the Photoshop "Auto Levels" equivalent - if self.opts.normalize: + if not self.opts.dont_normalize: MagickNormalizeImage(wand) sizex = MagickGetImageWidth(wand) @@ -173,7 +174,7 @@ class PageProcessor(list): else: MagickResizeImage(wand, SCRWIDTH, SCRHEIGHT, CatromFilter, 1.0) - if self.opts.sharpen: + if not self.opts.dont_sharpen: MagickSharpenImage(wand, 0.0, 1.0) MagickSetImageType(wand, GrayscaleType) @@ -225,34 +226,46 @@ def process_pages(pages, opts, update): finally: finalize() +def config(defaults=None): + desc = _('Options to control the conversion of comics (CBR, CBZ) files into ebooks') + if defaults is None: + c = Config('comic', desc) + else: + c = StringConfig(defaults, desc) + c.add_opt('title', ['-t', '--title'], + help=_('Title for generated ebook. Default is to use the filename.')) + c.add_opt('author', ['-a', '--author'], + help=_('Set the author in the metadata of the generated ebook. Default is %default'), + default=_('Unknown')) + c.add_opt('output', ['-o', '--output'], + help=_('Path to output LRF file. By default a file is created in the current directory.')) + c.add_opt('colors', ['-c', '--colors'], type='int', default=64, + help=_('Number of colors for Grayscale image conversion. Default: %default')) + c.add_opt('dont_normalize', ['-n', '--disable-normalize'], default=False, + help=_('Disable normalize (improve contrast) color range for pictures. Default: False')) + c.add_opt('keep_aspect_ratio', ['-r', '--keep-aspect-ratio'], default=False, + help=_('Maintain picture aspect ratio. Default is to fill the screen.')) + c.add_opt('dont_sharpen', ['-s', '--disable-sharpen'], default=False, + help=_('Disable sharpening.')) + c.add_opt('landscape', ['-l', '--landscape'], default=False, + help=_("Don't split landscape images into two portrait images")) + c.add_opt('no_sort', ['--no-sort'], default=False, + help=_("Don't sort the files found in the comic alphabetically by name. Instead use the order they were added to the comic.")) + c.add_opt('profile', ['-p', '--profile'], default='prs500', choices=PROFILES.keys(), + help=_('Choose a profile for the device you are generating this LRF for. The default is the SONY PRS-500 with a screen size of 584x754 pixels. Choices are %s')%PROFILES.keys()) + c.add_opt('verbose', ['--verbose'], default=0, action='count', + help=_('Be verbose, useful for debugging. Can be specified multiple times for greater verbosity.')) + c.add_opt('no_progress_bar', ['--no-progress-bar'], default=False, + help=_("Don't show progress bar.")) + return c + def option_parser(): - parser = OptionParser(_('''\ + c = config() + return c.option_parser(usage=_('''\ %prog [options] comic.cb[z|r] Convert a comic in a CBZ or CBR file to an LRF ebook. - ''')) - parser.add_option('-t', '--title', help=_('Title for generated ebook. Default is to use the filename.'), default=None) - parser.add_option('-a', '--author', help=_('Set the author in the metadata of the generated ebook. Default is %default'), default=_('Unknown')) - parser.add_option('-o', '--output', help=_('Path to output LRF file. By default a file is created in the current directory.'), default=None) - parser.add_option('-c', '--colors', type='int', default=64, - help=_('Number of colors for Grayscale image conversion. Default: %default')) - parser.add_option('-n', '--disable-normalize', dest='normalize', default=True, action='store_false', - help=_('Disable normalize (improve contrast) color range for pictures. Default: False')) - parser.add_option('-r', '--keep-aspect-ratio', action='store_true', default=False, - help=_('Maintain picture aspect ratio. Default is to fill the screen.')) - parser.add_option('-s', '--disable-sharpen', default=True, action='store_false', dest='sharpen', - help=_('Disable sharpening.')) - parser.add_option('-l', '--landscape', default=False, action='store_true', - help=_("Don't split landscape images into two portrait images")) - parser.add_option('--no-sort', default=False, action='store_true', - help=_("Don't sort the files found in the comic alphabetically by name. Instead use the order they were added to the comic.")) - parser.add_option('-p', '--profile', default='prs500', dest='profile', type='choice', - choices=PROFILES.keys(), help=_('Choose a profile for the device you are generating this LRF for. The default is the SONY PRS-500 with a screen size of 584x754 pixels. Choices are %s')%PROFILES.keys()) - parser.add_option('--verbose', default=False, action='store_true', - help=_('Be verbose, useful for debugging')) - parser.add_option('--no-progress-bar', default=False, action='store_true', - help=_("Don't show progress bar.")) - return parser +''')) def create_lrf(pages, profile, opts, thumbnail=None): width, height = PROFILES[profile] @@ -277,22 +290,8 @@ def create_lrf(pages, profile, opts, thumbnail=None): book.renderLrf(open(opts.output, 'wb')) - - -def main(args=sys.argv, notification=None): - parser = option_parser() - opts, args = parser.parse_args(args) - if len(args) < 2: - parser.print_help() - print '\nYou must specify a file to convert' - return 1 - - if not callable(notification): - pb = ProgressBar(terminal_controller, _('Rendering comic pages...'), - no_progress_bar=opts.no_progress_bar) - notification = pb.update - - source = os.path.abspath(args[1]) +def do_convert(path_to_file, opts, notification=lambda m, p: p): + source = path_to_file if not opts.title: opts.title = os.path.splitext(os.path.basename(source)) if not opts.output: @@ -315,6 +314,24 @@ def main(args=sys.argv, notification=None): create_lrf(pages, opts.profile, opts, thumbnail=thumbnail) shutil.rmtree(tdir) shutil.rmtree(tdir2) + + +def main(args=sys.argv, notification=None): + parser = option_parser() + opts, args = parser.parse_args(args) + if len(args) < 2: + parser.print_help() + print '\nYou must specify a file to convert' + return 1 + + if not callable(notification): + pb = ProgressBar(terminal_controller, _('Rendering comic pages...'), + no_progress_bar=opts.no_progress_bar) + notification = pb.update + + source = os.path.abspath(args[1]) + do_convert(source, opts, notification) + print _('Output written to'), opts.output return 0 if __name__ == '__main__': diff --git a/src/calibre/gui2/dialogs/comicconf.py b/src/calibre/gui2/dialogs/comicconf.py new file mode 100644 index 0000000000..7f42750fdd --- /dev/null +++ b/src/calibre/gui2/dialogs/comicconf.py @@ -0,0 +1,85 @@ +from __future__ import with_statement +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' + +'''''' +from PyQt4.QtGui import QDialog +from calibre.gui2.dialogs.comicconf_ui import Ui_Dialog +from calibre.gui2 import qstring_to_unicode +from calibre.ebooks.lrf.comic.convert_from import config + +def set_conversion_defaults(window): + d = ComicConf(window) + d.exec_() + +def get_bulk_conversion_options(window): + c = config(None) + with open(c.config_file_path, 'rb') as f: + d = ComicConf(window, config_defaults=f.read()) + if d.exec_() == QDialog.Accepted: + return d.config.parse() + +def get_conversion_options(window, defaults, title, author): + if defaults is None: + c = config(None) + with open(c.config_file_path, 'rb') as f: + defaults = f.read() + defaults += '\ntitle=%s\nauthor=%s'%(repr(title), repr(author)) + d = ComicConf(window, config_defaults=defaults, generic=False) + if d.exec_() == QDialog.Accepted: + return d.config.parse(), d.config.src + return None, None + + +class ComicConf(QDialog, Ui_Dialog): + + def __init__(self, window, config_defaults=None, generic=True, + title=_('Set defaults for conversion of comics (CBR/CBZ files)')): + QDialog.__init__(self, window) + Ui_Dialog.__init__(self) + self.setupUi(self) + self.setWindowTitle(title) + self.config = config(config_defaults) + opts = self.config.parse() + if generic: + for i in ('title', 'author'): + getattr(self, 'opt_'+i).setVisible(False) + getattr(self, i+'_label').setVisible(False) + else: + title = opts.title + if not title: + title = _('Unknown') + self.setWindowTitle(_('Set options for converting %s')%title) + author = opts.author + self.opt_title.setText(title) + self.opt_author.setText(author) + self.opt_colors.setValue(opts.colors) + self.opt_profile.addItem(opts.profile) + self.opt_dont_normalize.setChecked(opts.dont_normalize) + self.opt_keep_aspect_ratio.setChecked(opts.keep_aspect_ratio) + self.opt_dont_sharpen.setChecked(opts.dont_sharpen) + self.opt_landscape.setChecked(opts.landscape) + self.opt_no_sort.setChecked(opts.no_sort) + + for opt in self.config.option_set.preferences: + g = getattr(self, 'opt_'+opt.name, False) + if opt.help and g: + g.setToolTip(opt.help) + + def accept(self): + for opt in self.config.option_set.preferences: + g = getattr(self, 'opt_'+opt.name, False) + if not g or not g.isVisible(): continue + if hasattr(g, 'isChecked'): + val = bool(g.isChecked()) + elif hasattr(g, 'value'): + val = g.value() + elif hasattr(g, 'itemText'): + val = qstring_to_unicode(g.itemText(g.currentIndex())) + elif hasattr(g, 'text'): + val = qstring_to_unicode(g.text()) + else: + raise Exception('Bad coding') + self.config.set(opt.name, val) + return QDialog.accept(self) \ No newline at end of file diff --git a/src/calibre/gui2/dialogs/comicconf.ui b/src/calibre/gui2/dialogs/comicconf.ui new file mode 100644 index 0000000000..9e6198eaf7 --- /dev/null +++ b/src/calibre/gui2/dialogs/comicconf.ui @@ -0,0 +1,166 @@ + + Dialog + + + + 0 + 0 + 646 + 468 + + + + Dialog + + + + :/images/convert.svg:/images/convert.svg + + + + + + &Title: + + + opt_title + + + + + + + + + + &Author(s): + + + opt_author + + + + + + + + + + &Number of Colors: + + + opt_colors + + + + + + + 8 + + + 3200000 + + + 8 + + + + + + + &Profile: + + + opt_profile + + + + + + + + + + Disable &normalize + + + + + + + Keep &aspect ratio + + + + + + + Disable &Sharpening + + + + + + + &Landscape + + + + + + + Dont so&rt + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index c1ca797d41..32e400dafd 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -37,6 +37,7 @@ from calibre.gui2.dialogs.lrf_single import LRFSingleDialog, LRFBulkDialog from calibre.gui2.dialogs.config import ConfigDialog from calibre.gui2.dialogs.search import SearchDialog from calibre.gui2.dialogs.user_profiles import UserProfiles +import calibre.gui2.dialogs.comicconf as ComicConf from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.gui2.dialogs.book_info import BookInfo from calibre.ebooks.metadata.meta import set_metadata @@ -173,11 +174,13 @@ class Main(MainWindow, Ui_MainWindow): cm.addAction(_('Convert individually')) cm.addAction(_('Bulk convert')) cm.addSeparator() - cm.addAction(_('Set conversion defaults')) + cm.addAction(_('Set defaults for conversion to LRF')) + cm.addAction(_('Set defaults for conversion of comics')) self.action_convert.setMenu(cm) QObject.connect(cm.actions()[0], SIGNAL('triggered(bool)'), self.convert_single) QObject.connect(cm.actions()[1], SIGNAL('triggered(bool)'), self.convert_bulk) QObject.connect(cm.actions()[3], SIGNAL('triggered(bool)'), self.set_conversion_defaults) + QObject.connect(cm.actions()[4], SIGNAL('triggered(bool)'), self.set_comic_conversion_defaults) QObject.connect(self.action_convert, SIGNAL('triggered(bool)'), self.convert_single) self.convert_menu = cm self.tool_bar.widgetForAction(self.action_news).setPopupMode(QToolButton.InstantPopup) @@ -763,12 +766,25 @@ class Main(MainWindow, Ui_MainWindow): ############################### Convert #################################### - def convert_bulk(self, checked): - rows = self.library_view.selectionModel().selectedRows() + def get_books_for_conversion(self): + rows = [r.row() for r in self.library_view.selectionModel().selectedRows()] if not rows or len(rows) == 0: d = error_dialog(self, _('Cannot convert'), _('No books selected')) d.exec_() - return + return [], [] + comics, others = [], [] + db = self.library_view.model().db + for r in rows: + formats = db.formats(r) + if not formats: continue + formats = formats.lower().split(',') + if 'cbr' in formats or 'cbz' in formats: + comics.append(r) + else: + others.append(r) + return comics, others + + def convert_bulk_others(self, rows): d = LRFBulkDialog(self) d.exec_() if d.result() != QDialog.Accepted: @@ -813,34 +829,67 @@ class Main(MainWindow, Ui_MainWindow): cmdline.append(pt.name) id = self.job_manager.run_conversion_job(self.book_converted, 'any2lrf', args=[cmdline], - job_description='Convert book %d of %d'%(i+1, len(rows))) + job_description=_('Convert book %d of %d (%s)')%(i+1, len(rows), repr(mi.title))) self.conversion_jobs[id] = (d.cover_file, pt, of, d.output_format, self.library_view.model().db.id(row)) - - res = [] for row in bad_rows: title = self.library_view.model().db.title(row) res.append('
  • %s
  • '%title) if res: - msg = '

    Could not convert %d of %d books, because no suitable source format was found.

      %s
    '%(len(res), len(rows), '\n'.join(res)) - warning_dialog(self, 'Could not convert some books', msg).exec_() - + msg = _('

    Could not convert %d of %d books, because no suitable source format was found.

      %s
    ')%(len(res), len(rows), '\n'.join(res)) + warning_dialog(self, _('Could not convert some books'), msg).exec_() + + + def convert_bulk(self, checked): + comics, others = self.get_books_for_conversion() + if others: + self.convert_bulk_others(others) + if comics: + opts = ComicConf.get_bulk_conversion_options(self) + if opts: + for i, row in enumerate(comics): + options = opts.copy() + mi = self.library_view.model().db.get_metadata(row) + if mi.title: + options.title = mi.title + if mi.authors: + opts.author = ','.join(mi.authors) + data = None + for fmt in ['cbz', 'cbr']: + try: + data = self.library_view.model().db.format(row, fmt.upper()) + break + except: + continue + + pt = PersistentTemporaryFile('.'+fmt.lower()) + pt.write(data) + pt.close() + of = PersistentTemporaryFile('.lrf') + of.close() + setattr(options, 'output', of.name) + options.verbose = 1 + args = [pt.name, options] + id = self.job_manager.run_conversion_job(self.book_converted, + 'comic2lrf', args=args, + job_description=_('Convert comic %d of %d (%s)')%(i+1, len(comics), repr(options.title))) + self.conversion_jobs[id] = (None, pt, of, 'lrf', + self.library_view.model().db.id(row)) + def set_conversion_defaults(self, checked): d = LRFSingleDialog(self, None, None) d.exec_() - - def convert_single(self, checked): - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot convert'), _('No books selected')) - d.exec_() + def set_comic_conversion_defaults(self, checked): + ComicConf.set_conversion_defaults(self) + + def convert_single_others(self, rows): changed = False - for row in [r.row() for r in rows]: + for row in rows: d = LRFSingleDialog(self, self.library_view.model().db, row) if d.selected_format: d.exec_() @@ -856,7 +905,7 @@ class Main(MainWindow, Ui_MainWindow): cmdline.append(pt.name) id = self.job_manager.run_conversion_job(self.book_converted, 'any2lrf', args=[cmdline], - job_description='Convert book:'+d.title()) + job_description=_('Convert book: ')+d.title()) self.conversion_jobs[id] = (d.cover_file, pt, of, d.output_format, d.id) @@ -865,6 +914,48 @@ class Main(MainWindow, Ui_MainWindow): self.library_view.model().resort(reset=False) self.library_view.model().research() + + def convert_single(self, checked): + comics, others = self.get_books_for_conversion() + if others: + self.convert_single_others(others) + changed = False + db = self.library_view.model().db + for row in comics: + mi = db.get_metadata(row) + title = author = _('Unknown') + if mi.title: + title = mi.title + if mi.authors: + author = ','.join(mi.authors) + defaults = db.conversion_options(db.id(row), 'comic') + opts, defaults = ComicConf.get_conversion_options(self, defaults, title, author) + if defaults is not None: + db.set_conversion_options(db.id(row), 'comic', defaults) + if opts is None: continue + for fmt in ['cbz', 'cbr']: + try: + data = db.format(row, fmt.upper()) + break + except: + continue + pt = PersistentTemporaryFile('.'+fmt) + pt.write(data) + pt.close() + of = PersistentTemporaryFile('.lrf') + of.close() + opts.output = of.name + opts.verbose = 1 + args = [pt.name, opts] + changed = True + id = self.job_manager.run_conversion_job(self.book_converted, + 'comic2lrf', args=args, + job_description=_('Convert comic: ')+opts.title) + self.conversion_jobs[id] = (None, pt, of, 'lrf', + self.library_view.model().db.id(row)) + if changed: + self.library_view.model().resort(reset=False) + self.library_view.model().research() def book_converted(self, id, description, result, exception, formatted_traceback, log): of, fmt, book_id = self.conversion_jobs.pop(id)[2:] diff --git a/src/calibre/library/database.py b/src/calibre/library/database.py index 0a05f48dc9..e84e9b66e3 100644 --- a/src/calibre/library/database.py +++ b/src/calibre/library/database.py @@ -1092,7 +1092,7 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; self.set_tags(id, val.split(','), append=False) def set_conversion_options(self, id, format, options): - data = sqlite.Binary(cPickle.dumps(options)) + data = sqlite.Binary(cPickle.dumps(options, -1)) oid = self.conn.execute('SELECT id FROM conversion_options WHERE book=? AND format=?', (id, format.upper())).fetchone() if oid: self.conn.execute('UPDATE conversion_options SET data=? WHERE id=?', (data, oid[0])) diff --git a/src/calibre/linux.py b/src/calibre/linux.py index b55f02c15e..efcb49b54c 100644 --- a/src/calibre/linux.py +++ b/src/calibre/linux.py @@ -1,8 +1,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' -import shutil ''' Post installation script for linux ''' -import sys, os, re +import sys, os, re, shutil from subprocess import check_call, call from calibre import __version__, __appname__ diff --git a/src/calibre/linux_installer.py b/src/calibre/linux_installer.py index ad24508900..d2e19e913a 100644 --- a/src/calibre/linux_installer.py +++ b/src/calibre/linux_installer.py @@ -215,11 +215,12 @@ cd $frozen_path ''' def extract_tarball(tar, destdir): + print 'Extracting application files...' if hasattr(tar, 'read'): try: tarfile.open(fileobj=tar, mode='r').extractall(destdir) except: # tarfile.py on Fedora 9 is buggy - subprocess.check_call(['tar', 'xvjf', tar.name, '-C', destdir]) + subprocess.check_call(['tar', 'xjf', tar.name, '-C', destdir]) else: tarfile.open(tar, 'r').extractall(destdir) @@ -242,6 +243,7 @@ def do_postinstall(destdir): try: os.chdir(destdir) os.environ['LD_LIBRARY_PATH'] = destdir+':'+os.environ.get('LD_LIBRARY_PATH', '') + os.environ['PYTHONPATH'] = destdir subprocess.call((os.path.join(destdir, 'calibre_postinstall'), '--save-manifest-to', t.name)) finally: os.chdir(cwd) diff --git a/src/calibre/parallel.py b/src/calibre/parallel.py index d46f61c8fd..a26e5e0721 100644 --- a/src/calibre/parallel.py +++ b/src/calibre/parallel.py @@ -48,6 +48,9 @@ PARALLEL_FUNCS = { 'render_table' : ('calibre.ebooks.lrf.html.table_as_image', 'do_render', {}, None), + + 'comic2lrf' : + ('calibre.ebooks.lrf.comic.convert_from', 'do_convert', {}, 'notification'), } diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py new file mode 100644 index 0000000000..eea9c9749e --- /dev/null +++ b/src/calibre/utils/config.py @@ -0,0 +1,269 @@ +from __future__ import with_statement +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' + +''' +Manage application-wide preferences. +''' +import os, re, cPickle +from copy import deepcopy +from PyQt4.QtCore import QString +from calibre import iswindows, isosx, OptionParser, ExclusiveFile, LockError +from collections import defaultdict + +if iswindows: + from calibre import plugins + config_dir = plugins['winutil'][0].special_folder_path(plugins['winutil'][0].CSIDL_APPDATA) + if not os.access(config_dir, os.W_OK|os.X_OK): + config_dir = os.path.expanduser('~') + config_dir = os.path.join(config_dir, 'calibre') +elif isosx: + config_dir = os.path.expanduser('~/Library/Preferences/calibre') +else: + config_dir = os.path.expanduser('~/.config/calibre') + +if not os.path.exists(config_dir): + os.makedirs(config_dir) + +class Option(object): + + def __init__(self, name, switches=[], help='', type=None, choices=None, + check=None, group=None, default=None, action=None, metavar=None): + if choices: + type = 'choice' + + self.name = name + self.switches = switches + self.help = help.replace('%default', repr(default)) if help else None + self.type = type + self.choices = choices + self.check = check + self.group = group + self.default = default + self.action = action + self.metavar = metavar + + def __eq__(self, other): + return self.name == getattr(other, 'name', None) + +class OptionValues(object): + + def copy(self): + return deepcopy(self) + +class OptionSet(object): + + OVERRIDE_PAT = re.compile(r'#{3,100} Override Options #{15}(.*?)#{3,100} End Override #{3,100}', + re.DOTALL|re.IGNORECASE) + + def __init__(self, description=''): + self.description = description + self.preferences = [] + self.group_list = [] + self.groups = {} + self.set_buffer = {} + + def has_option(self, name_or_option_object): + if name_or_option_object in self.preferences: + return True + for p in self.preferences: + if p.name == name_or_option_object: + return True + return False + + def add_group(self, name, description=''): + if name in self.group_list: + raise ValueError('A group by the name %s already exists in this set'%name) + self.groups[name] = description + self.group_list.append(name) + + def add_opt(self, name, switches=[], help=None, type=None, choices=None, + group=None, default=None, action=None, metavar=None): + ''' + Add an option to this section. + + :param name: The name of this option. Must be a valid Python identifier. + Must also be unique in this OptionSet and all its subsets. + :param switches: List of command line switches for this option + (as supplied to :module:`optparse`). If empty, this + option will not be added to the command line parser. + :param help: Help text. + :param type: Type checking of option values. Supported types are: + `None, 'choice', 'complex', 'float', 'int', 'long', 'string'`. + :param choices: List of strings or `None`. + :param group: Group this option belongs to. You must previously + have created this group with a call to :method:`add_group`. + :param default: The default value for this option. + :param action: The action to pass to optparse. Supported values are: + `None, 'count'`. For choices and boolean options, + action is automatically set correctly. + ''' + pref = Option(name, switches=switches, help=help, type=type, choices=choices, + group=group, default=default, action=action, metavar=None) + if group is not None and group not in self.groups.keys(): + raise ValueError('Group %s has not been added to this section'%group) + if pref in self.preferences: + raise ValueError('An option with the name %s already exists in this set.'%name) + self.preferences.append(pref) + + def option_parser(self, user_defaults=None, usage='', gui_mode=False): + parser = OptionParser(usage, gui_mode=gui_mode) + groups = defaultdict(lambda : parser) + for group, desc in self.groups.items(): + groups[group] = parser.add_group(group, desc) + + for pref in self.preferences: + if not pref.switches: + continue + g = groups[pref.group] + action = pref.action + if action is None: + action = 'store' + if pref.default is True or pref.default is False: + action = 'store_' + ('false' if pref.default else 'true') + args = dict( + dest=pref.name, + help=pref.help, + metavar=pref.metavar, + type=pref.type, + choices=pref.choices, + default=getattr(user_defaults, pref.name, pref.default), + action=action, + ) + g.add_option(*pref.switches, **args) + + + return parser + + def get_override_section(self, src): + match = self.OVERRIDE_PAT.search(src) + if match: + return match.group() + return '' + + def parse_string(self, src): + options = {'cPickle':cPickle} + if src is not None: + exec src in options + opts = OptionValues() + for pref in self.preferences: + setattr(opts, pref.name, options.get(pref.name, pref.default)) + + return opts + + def render_group(self, name, desc, opts): + prefs = [pref for pref in self.preferences if pref.group == name] + lines = ['### Begin group: %s'%(name if name else 'DEFAULT')] + if desc: + lines += map(lambda x: '# '+x for x in desc.split('\n')) + lines.append(' ') + for pref in prefs: + lines.append('# '+pref.name.replace('_', ' ')) + if pref.help: + lines += map(lambda x: '# ' + x, pref.help.split('\n')) + lines.append('%s = %s'%(pref.name, + self.serialize_opt(getattr(opts, pref.name, pref.default)))) + lines.append(' ') + return '\n'.join(lines) + + def serialize_opt(self, val): + if val is val is True or val is False or val is None or \ + isinstance(val, (int, float, long, basestring)): + return repr(val) + if isinstance(val, QString): + return repr(unicode(val)) + pickle = cPickle.dumps(val, -1) + return 'cPickle.loads(%s)'%repr(pickle) + + def serialize(self, opts): + src = '# %s\n\n'%(self.description.replace('\n', '\n# ')) + groups = [self.render_group(name, self.groups.get(name, ''), opts) \ + for name in [None] + self.group_list] + return src + '\n\n'.join(groups) + +class Config(object): + + def __init__(self, basename, description=''): + self.config_file_path = os.path.join(config_dir, basename+'.py') + self.option_set = OptionSet(description=description) + self.add_opt = self.option_set.add_opt + self.add_group = self.option_set.add_group + + def option_parser(self, usage='', gui_mode=False): + return self.option_set.option_parser(user_defaults=self.parse(), + usage=usage, gui_mode=gui_mode) + + def parse(self): + try: + with ExclusiveFile(self.config_file_path) as f: + src = f.read() + except LockError: + raise IOError('Could not lock config file: %s'%self.config_file_path) + return self.option_set.parse_string(src) + + def set(self, name, val): + if not self.option_set.has_option(name): + raise ValueError('The option %s is not defined.'%name) + try: + with ExclusiveFile(self.config_file_path) as f: + src = f.read() + opts = self.option_set.parse_string(src) + setattr(opts, name, val) + footer = self.option_set.get_override_section(src) + src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n' + f.seek(0) + f.truncate() + f.write(src) + except LockError: + raise IOError('Could not lock config file: %s'%self.config_file_path) + +class StringConfig(object): + + def __init__(self, src, description=''): + self.src = src + self.option_set = OptionSet(description=description) + self.add_opt = self.option_set.add_opt + self.option_parser = self.option_set.option_parser + + def option_parser(self, usage='', gui_mode=False): + return self.option_set.option_parser(user_defaults=self.parse(), + usage=usage, gui_mode=gui_mode) + + def parse(self): + return self.option_set.parse_string(self.src) + + def set(self, name, val): + if not self.option_set.has_option(name): + raise ValueError('The option %s is not defined.'%name) + opts = self.option_set.parse_string(self.src) + setattr(opts, name, val) + footer = self.option_set.get_override_section(self.src) + self.src = self.option_set.serialize(opts)+ '\n\n' + footer + '\n' + + + + +if __name__ == '__main__': + import subprocess + from PyQt4.Qt import QByteArray + c = Config('test', 'test config') + + c.add_opt('one', ['-1', '--one'], help="This is option #1") + c.set('one', u'345') + + c.add_opt('two', help="This is option #2") + c.set('two', 345) + + c.add_opt('three', help="This is option #3") + c.set('three', QString(u'aflatoon')) + + c.add_opt('four', help="This is option #4") + c.set('four', QByteArray('binary aflatoon')) + + subprocess.call(['pygmentize', os.path.expanduser('~/.config/calibre/test.py')]) + + opts = c.parse() + for i in ('one', 'two', 'three', 'four'): + print i, repr(getattr(opts, i)) + \ No newline at end of file From 0c1f1c1c7ba6962f2d542b6585e8c7e7799b6264 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 3 Aug 2008 14:10:09 -0700 Subject: [PATCH 11/13] Fix #922 (ERROR: Unhandled exception when opening Calibre-0.4.80) --- src/calibre/devices/prs505/driver.py | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/calibre/devices/prs505/driver.py b/src/calibre/devices/prs505/driver.py index 4462407e18..45c76d7089 100644 --- a/src/calibre/devices/prs505/driver.py +++ b/src/calibre/devices/prs505/driver.py @@ -126,7 +126,36 @@ class PRS505(Device): self._card_prefix = re.search('/dev/%s(\w*)\s+on\s+([^\(]+)\s+'%(devname,), mount).group(2) + os.sep + def open_windows_nowmi(self): + from calibre import plugins + winutil = plugins['winutil'][0] + volumes = winutil.get_mounted_volumes_for_usb_device(self.VENDOR_ID, self.PRODUCT_ID) + main = None + for device_id in volumes.keys(): + if 'PRS-505/UC&' in device_id: + main = volumes[device_id]+':\\' + if not main: + DeviceError(_('Unable to detect the %s disk drive. Try rebooting.')%self.__class__.__name__) + self._main_prefix = main + card = self._card_prefix = None + win32api = __import__('win32api') + for device_id in volumes.keys(): + if 'PRS-505/UC:' in device_id: + card = volumes[device_id]+':\\' + try: + win32api.GetVolumeInformation(card) + self._card_prefix = card + break + except: + continue + + def open_windows(self): + try: + self.open_windows_nowmi() + return + except: + pass drives = [] wmi = __import__('wmi', globals(), locals(), [], -1) c = wmi.WMI() From 1c5ecad88ff300060c3142e9e02e3a877c278d30 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 3 Aug 2008 14:30:48 -0700 Subject: [PATCH 12/13] IGN:... --- src/calibre/__init__.py | 2 +- src/calibre/ebooks/lrf/comic/convert_from.py | 12 +++++------- src/calibre/utils/PythonMagickWand.py | 8 ++++++++ src/calibre/web/feeds/main.py | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 8caebc5be2..ff03260f2b 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -318,7 +318,7 @@ def extract(path, dir): raise Exception('Unknown archive type') extractor(path, dir) -def get_proxies(self): +def get_proxies(): proxies = {} if iswindows: try: diff --git a/src/calibre/ebooks/lrf/comic/convert_from.py b/src/calibre/ebooks/lrf/comic/convert_from.py index 847b588aef..a8bc6cbb15 100755 --- a/src/calibre/ebooks/lrf/comic/convert_from.py +++ b/src/calibre/ebooks/lrf/comic/convert_from.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +from __future__ import with_statement __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' @@ -31,12 +31,13 @@ try: MagickQuantizeImage, RGBColorspace, \ MagickWriteImage, DestroyPixelWand, \ DestroyMagickWand, CloneMagickWand, \ - MagickThumbnailImage, MagickCropImage, initialize, finalize + MagickThumbnailImage, MagickCropImage, ImageMagick _imagemagick_loaded = True except: _imagemagick_loaded = False PROFILES = { + # Name : (width, height) in pixels 'prs500':(584, 754), } @@ -205,8 +206,7 @@ def process_pages(pages, opts, update): ''' if not _imagemagick_loaded: raise RuntimeError('Failed to load ImageMagick') - initialize() - try: + with ImageMagick(): tdir = PersistentTemporaryDirectory('_comic2lrf_pp') processed_pages = [PageProcessor(path, tdir, opts, i) for i, path in enumerate(pages)] tp = ThreadPool(detect_ncpus()) @@ -223,9 +223,7 @@ def process_pages(pages, opts, update): else: ans += pp return ans, failures, tdir - finally: - finalize() - + def config(defaults=None): desc = _('Options to control the conversion of comics (CBR, CBZ) files into ebooks') if defaults is None: diff --git a/src/calibre/utils/PythonMagickWand.py b/src/calibre/utils/PythonMagickWand.py index e3e421fe55..301557c369 100644 --- a/src/calibre/utils/PythonMagickWand.py +++ b/src/calibre/utils/PythonMagickWand.py @@ -104,6 +104,14 @@ def finalize(): _magick.MagickWandTerminus() _initialized = False +class ImageMagick(object): + + def __enter__(self): + initialize() + + def __exit__(self, *args): + finalize() + class MetricType(ctypes.c_int): pass UndefinedMetric = MetricType(0) diff --git a/src/calibre/web/feeds/main.py b/src/calibre/web/feeds/main.py index 7d2547360d..14299abecd 100644 --- a/src/calibre/web/feeds/main.py +++ b/src/calibre/web/feeds/main.py @@ -67,7 +67,7 @@ def run_recipe(opts, recipe_arg, parser, notification=None, handler=None): if notification is None: from calibre.utils.terminfo import TerminalController, ProgressBar term = TerminalController(sys.stdout) - pb = ProgressBar(term, _('Fetching feeds...'), no_progress_bar=opts.progress_bar) + pb = ProgressBar(term, _('Fetching feeds...'), no_progress_bar=not opts.progress_bar) notification = pb.update recipe, is_profile = None, False From 82d021b460331d4caa52ec4f62a7aa07d3cd9986 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 3 Aug 2008 16:36:48 -0700 Subject: [PATCH 13/13] Handle ancient text-only PRC ebooks. --- src/calibre/ebooks/mobi/reader.py | 80 +++++++++++++++++-------------- 1 file changed, 45 insertions(+), 35 deletions(-) diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index c9045b4a8f..9c5ba7839f 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -72,43 +72,51 @@ class BookHeader(object): self.compression_type = raw[:2] self.records, self.records_size = struct.unpack('>HH', raw[8:12]) self.encryption_type, = struct.unpack('>H', raw[12:14]) - - self.doctype = raw[16:20] - self.length, self.type, self.codepage, self.unique_id, self.version = \ - struct.unpack('>LLLLL', raw[20:40]) - if ident == 'TEXTREAD': self.codepage = 1252 - - try: - self.codec = { - 1252 : 'cp1252', - 65001 : 'utf-8', - }[self.codepage] - except IndexError, KeyError: - print '[WARNING] Unknown codepage %d. Assuming cp-1252'%self.codepage - self.codec = 'cp1252' - - if ident == 'TEXTREAD' or self.length < 0xE4 or 0xE8 < self.length: + if len(raw) <= 16: + self.codec = 'cp1251' self.extra_flags = 0 + self.language = 'ENGLISH' + self.sublanguage = 'NEUTRAL' + self.exth_flag, self.exth = 0, None + self.ancient = True else: - self.extra_flags, = struct.unpack('>L', raw[0xF0:0xF4]) - - if self.compression_type == 'DH': - self.huff_offset, self.huff_number = struct.unpack('>LL', raw[0x70:0x78]) - - langcode = struct.unpack('!L', raw[0x5C:0x60])[0] - langid = langcode & 0xFF - sublangid = (langcode >> 10) & 0xFF - self.language = main_language.get(langid, 'ENGLISH') - self.sublanguage = sub_language.get(sublangid, 'NEUTRAL') - - self.exth_flag, = struct.unpack('>L', raw[0x80:0x84]) - self.exth = None - if self.exth_flag & 0x40: - self.exth = EXTHHeader(raw[16+self.length:], self.codec) - self.exth.mi.uid = self.unique_id - self.exth.mi.language = self.language + self.ancient = False + self.doctype = raw[16:20] + self.length, self.type, self.codepage, self.unique_id, self.version = \ + struct.unpack('>LLLLL', raw[20:40]) + + + try: + self.codec = { + 1252 : 'cp1252', + 65001 : 'utf-8', + }[self.codepage] + except IndexError, KeyError: + print '[WARNING] Unknown codepage %d. Assuming cp-1252'%self.codepage + self.codec = 'cp1252' + + if ident == 'TEXTREAD' or self.length < 0xE4 or 0xE8 < self.length: + self.extra_flags = 0 + else: + self.extra_flags, = struct.unpack('>L', raw[0xF0:0xF4]) + + if self.compression_type == 'DH': + self.huff_offset, self.huff_number = struct.unpack('>LL', raw[0x70:0x78]) + + langcode = struct.unpack('!L', raw[0x5C:0x60])[0] + langid = langcode & 0xFF + sublangid = (langcode >> 10) & 0xFF + self.language = main_language.get(langid, 'ENGLISH') + self.sublanguage = sub_language.get(sublangid, 'NEUTRAL') + + self.exth_flag, = struct.unpack('>L', raw[0x80:0x84]) + self.exth = None + if self.exth_flag & 0x40: + self.exth = EXTHHeader(raw[16+self.length:], self.codec) + self.exth.mi.uid = self.unique_id + self.exth.mi.language = self.language class MobiReader(object): @@ -145,7 +153,6 @@ class MobiReader(object): else: end_off = self.section_headers[section_number + 1][0] off = self.section_headers[section_number][0] - return raw[off:end_off] for i in range(self.num_sections): @@ -201,6 +208,8 @@ class MobiReader(object): def cleanup_html(self): self.processed_html = re.sub(r'
    ', '', self.processed_html) + if self.book_header.ancient and '')+'' def cleanup_soup(self, soup): for tag in soup.recursiveChildGenerator(): @@ -313,7 +322,8 @@ class MobiReader(object): self.mobi_html = ''.join(text_sections) else: raise MobiError('Unknown compression algorithm: %s'%repr(self.book_header.compression_type)) - + if self.book_header.ancient and '