diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index 5656079ead..280bf02cae 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -3,7 +3,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' __docformat__ = 'restructuredtext en' import sys, os, re, logging, time, subprocess, mimetypes, \ - __builtin__, warnings + __builtin__, warnings, multiprocessing __builtin__.__dict__['dynamic_property'] = lambda(func): func(None) from htmlentitydefs import name2codepoint from math import floor @@ -265,40 +265,7 @@ class StreamReadWrapper(object): def detect_ncpus(): """Detects the number of effective CPUs in the system""" - try: - from PyQt4.QtCore import QThread - ans = QThread.idealThreadCount() - if ans > 0: - return ans - except: - pass - #for Linux, Unix and MacOS - if hasattr(os, "sysconf"): - if os.sysconf_names.has_key("SC_NPROCESSORS_ONLN"): - #Linux and Unix - ncpus = os.sysconf("SC_NPROCESSORS_ONLN") - if isinstance(ncpus, int) and ncpus > 0: - return ncpus - else: - #MacOS X - try: - return int(subprocess.Popen(('sysctl', '-n', 'hw.cpu'), stdout=subprocess.PIPE).stdout.read()) - except IOError: # Occassionally the system call gets interrupted - try: - return int(subprocess.Popen(('sysctl', '-n', 'hw.cpu'), stdout=subprocess.PIPE).stdout.read()) - except IOError: - return 1 - except ValueError: # On some systems the sysctl call fails - return 1 - - #for Windows - if os.environ.has_key("NUMBER_OF_PROCESSORS"): - ncpus = int(os.environ["NUMBER_OF_PROCESSORS"]); - if ncpus > 0: - return ncpus - #return the default value - return 1 - + return multiprocessing.cpu_count() def launch(path_or_url): if os.path.exists(path_or_url): diff --git a/src/calibre/customize/conversion.py b/src/calibre/customize/conversion.py index 3a89a9b156..4d19ba4fad 100644 --- a/src/calibre/customize/conversion.py +++ b/src/calibre/customize/conversion.py @@ -64,6 +64,10 @@ class OptionRecommendation(object): self.validate_parameters() + @property + def help(self): + return self.option.help + def clone(self): return OptionRecommendation(recommended_value=self.recommended_value, level=self.level, option=self.option.clone()) diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py index f60f7b5e7b..4c184ca36d 100644 --- a/src/calibre/customize/profiles.py +++ b/src/calibre/customize/profiles.py @@ -3,7 +3,6 @@ __license__ = 'GPL 3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import re from itertools import izip from calibre.customize import Plugin as _Plugin @@ -141,16 +140,13 @@ class OutputProfile(Plugin): 'if you want to produce a document intended to be read at a ' 'computer or on a range of devices.') - # ADE dies an agonizing, long drawn out death if HTML files have more - # bytes than this. - flow_size = -1 - # ADE runs screaming when it sees these characters - remove_special_chars = re.compile(u'[\u200b\u00ad]') - # ADE falls to the ground in a dead faint when it sees an - remove_object_tags = True # The image size for comics comic_screen_size = (584, 754) + @classmethod + def tags_to_string(cls, tags): + return ', '.join(tags) + class SonyReaderOutput(OutputProfile): name = 'Sony Reader' @@ -158,7 +154,6 @@ class SonyReaderOutput(OutputProfile): description = _('This profile is intended for the SONY PRS line. ' 'The 500/505/700 etc.') - flow_size = 270000 screen_size = (600, 775) dpi = 168.451 fbase = 12 @@ -236,6 +231,11 @@ class KindleOutput(OutputProfile): fbase = 16 fsizes = [12, 12, 14, 16, 18, 20, 22, 24] + @classmethod + def tags_to_string(cls, tags): + return 'ttt '.join(tags)+'ttt ' + + output_profiles = [OutputProfile, SonyReaderOutput, MSReaderOutput, MobipocketOutput, HanlinV3Output, CybookG3Output, KindleOutput, SonyReaderLandscapeOutput] diff --git a/src/calibre/debug.py b/src/calibre/debug.py index ac164c14ab..6714db0331 100644 --- a/src/calibre/debug.py +++ b/src/calibre/debug.py @@ -108,6 +108,11 @@ def debug_device_driver(): drives.append((str(drive.PNPDeviceID), 'No mount points found')) for drive in drives: print '\t', drive + if isosx: + from calibre.devices.usbms.device import Device + raw = Device.run_ioreg() + open('/tmp/ioreg.txt', 'wb').write(raw) + print 'ioreg output saved to /tmp/ioreg.txt' from calibre.devices import devices for dev in devices(): print 'Looking for', dev.__name__ diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index 709ead05ef..a4b0c3fa4a 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -231,13 +231,19 @@ class Device(DeviceConfig, DevicePlugin): self._card_a_prefix = drives.get('carda', None) self._card_b_prefix = drives.get('cardb', None) + @classmethod + def run_ioreg(cls, raw=None): + if raw is not None: + return raw + ioreg = '/usr/sbin/ioreg' + if not os.access(ioreg, os.X_OK): + ioreg = 'ioreg' + return subprocess.Popen((ioreg+' -w 0 -S -c IOMedia').split(), + stdout=subprocess.PIPE).communicate()[0] + + def get_osx_mountpoints(self, raw=None): - if raw is None: - ioreg = '/usr/sbin/ioreg' - if not os.access(ioreg, os.X_OK): - ioreg = 'ioreg' - raw = subprocess.Popen((ioreg+' -w 0 -S -c IOMedia').split(), - stdout=subprocess.PIPE).communicate()[0] + raw = self.run_ioreg(raw) lines = raw.splitlines() names = {} diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 502102a59a..b409198aac 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -19,6 +19,10 @@ def supported_input_formats(): fmts.add(x) return fmts +INPUT_FORMAT_PREFERENCES = ['cbr', 'cbz', 'cbc', 'lit', 'mobi', 'prc', 'azw', 'fb2', 'html', + 'rtf', 'pdf', 'txt', 'pdb'] +OUTPUT_FORMAT_PREFERENCES = ['epub', 'mobi', 'lit', 'pdf', 'pdb', 'txt'] + class OptionValues(object): pass @@ -114,7 +118,7 @@ OptionRecommendation(name='font_size_mapping', ), OptionRecommendation(name='line_height', - recommended_value=None, level=OptionRecommendation.LOW, + recommended_value=0, level=OptionRecommendation.LOW, help=_('The line height in pts. Controls spacing between consecutive ' 'lines of text. By default no line height manipulation is ' 'performed.' @@ -463,6 +467,12 @@ OptionRecommendation(name='list_recipes', if rec.option == name: return rec + def get_option_help(self, name): + rec = self.get_option_by_name(name) + help = getattr(rec, 'help', None) + if help is not None: + return help.replace('%default', str(rec.recommended_value)) + def merge_plugin_recommendations(self): for source in (self.input_plugin, self.output_plugin): for name, val, level in source.recommendations: @@ -598,7 +608,7 @@ OptionRecommendation(name='list_recipes', from calibre.ebooks.oeb.transforms.flatcss import CSSFlattener fbase = self.opts.base_font_size - if fbase == 0: + if fbase < 1e-4: fbase = float(self.opts.dest.fbase) fkey = self.opts.font_size_mapping if fkey is None: @@ -618,8 +628,11 @@ OptionRecommendation(name='list_recipes', if self.output_plugin.file_type == 'lrf': self.opts.insert_blank_line = False self.opts.remove_paragraph_spacing = False + line_height = self.opts.line_height + if line_height < 1e-4: + line_height = None flattener = CSSFlattener(fbase=fbase, fkey=fkey, - lineh=self.opts.line_height, + lineh=line_height, untable=self.output_plugin.file_type in ('mobi','lit'), unfloat=self.output_plugin.file_type in ('mobi', 'lit')) flattener(self.oeb, self.opts) diff --git a/src/calibre/ebooks/epub/from_comic.py b/src/calibre/ebooks/epub/from_comic.py deleted file mode 100644 index c6dff349da..0000000000 --- a/src/calibre/ebooks/epub/from_comic.py +++ /dev/null @@ -1,21 +0,0 @@ -from __future__ import with_statement -__license__ = 'GPL v3' -__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' -__docformat__ = 'restructuredtext en' - -'Convert a comic in CBR/CBZ format to epub' - -import sys -from functools import partial -from calibre.ebooks.lrf.comic.convert_from import do_convert, option_parser, config, main as _main - -convert = partial(do_convert, output_format='epub') -main = partial(_main, output_format='epub') - -if __name__ == '__main__': - sys.exit(main()) - -if False: - option_parser - config - \ No newline at end of file diff --git a/src/calibre/ebooks/epub/output.py b/src/calibre/ebooks/epub/output.py index aba9bff0d8..efd27ef8f3 100644 --- a/src/calibre/ebooks/epub/output.py +++ b/src/calibre/ebooks/epub/output.py @@ -6,7 +6,7 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os, shutil +import os, shutil, re from urllib import unquote from calibre.customize.conversion import OutputFormatPlugin @@ -42,6 +42,13 @@ class EPUBOutput(OutputFormatPlugin): ) ), + OptionRecommendation(name='flow_size', recommended_value=260, + help=_('Split all HTML files larger than this size (in KB). ' + 'This is necessary as most EPUB readers cannot handle large ' + 'file sizes. The default of %defaultKB is the size required ' + 'for Adobe Digital Editions.') + ), + ]) @@ -104,7 +111,7 @@ class EPUBOutput(OutputFormatPlugin): from calibre.ebooks.oeb.transforms.split import Split split = Split(not self.opts.dont_split_on_page_breaks, - max_flow_size=self.opts.output_profile.flow_size + max_flow_size=self.opts.flow_size*1024 ) split(self.oeb, self.opts) @@ -243,14 +250,12 @@ class EPUBOutput(OutputFormatPlugin): br.tail = '' br.tail += sibling.tail - - if self.opts.output_profile.remove_object_tags: - for tag in XPath('//h:embed')(root): - tag.getparent().remove(tag) - for tag in XPath('//h:object')(root): - if tag.get('type', '').lower().strip() in ('image/svg+xml',): - continue - tag.getparent().remove(tag) + for tag in XPath('//h:embed')(root): + tag.getparent().remove(tag) + for tag in XPath('//h:object')(root): + if tag.get('type', '').lower().strip() in ('image/svg+xml',): + continue + tag.getparent().remove(tag) for tag in XPath('//h:title|//h:style')(root): if not tag.text: @@ -276,5 +281,12 @@ class EPUBOutput(OutputFormatPlugin): stylesheet.data.add('a[href] { color: blue; ' 'text-decoration: underline; cursor:pointer; }') + special_chars = re.compile(u'[\u200b\u00ad]') + for elem in root.iterdescendants(): + if getattr(elem, 'text', False): + elem.text = special_chars.sub('', elem.text) + if getattr(elem, 'tail', False): + elem.tail = special_chars.sub('', elem.tail) + diff --git a/src/calibre/ebooks/lrf/output.py b/src/calibre/ebooks/lrf/output.py index 40e99256e9..52c2a22ccd 100644 --- a/src/calibre/ebooks/lrf/output.py +++ b/src/calibre/ebooks/lrf/output.py @@ -53,6 +53,7 @@ class LRFOptions(object): self.lrs = False self.minimize_memory_usage = False self.autorotation = opts.enable_autorotation + self.header_separation = (self.profile.dpi/72.) * opts.header_separation for x in ('top', 'bottom', 'left', 'right'): @@ -60,7 +61,7 @@ class LRFOptions(object): 'margin_'+x)) for x in ('wordspace', 'header', 'header_format', - 'header_separation', 'minimum_indent', 'serif_family', + 'minimum_indent', 'serif_family', 'render_tables_as_images', 'sans_family', 'mono_family', 'text_size_multiplier_for_rendered_tables'): setattr(self, x, getattr(opts, x)) @@ -87,7 +88,7 @@ class LRFOutput(OutputFormatPlugin): 'and %t by the title. Default is %default') ), OptionRecommendation(name='header_separation', recommended_value=0, - help=_('Add extra spacing below the header. Default is %default px.') + help=_('Add extra spacing below the header. Default is %default pt.') ), OptionRecommendation(name='minimum_indent', recommended_value=0, help=_('Minimum paragraph indent (the indent of the first line ' @@ -99,7 +100,7 @@ class LRFOutput(OutputFormatPlugin): 'document has large or complex tables)') ), OptionRecommendation(name='text_size_multiplier_for_rendered_tables', - recommended_value=1, + recommended_value=1.0, help=_('Multiply the size of text in rendered tables by this ' 'factor. Default is %default') ), diff --git a/src/calibre/ebooks/mobi/from_any.py b/src/calibre/ebooks/mobi/from_any.py deleted file mode 100644 index fc9e94dafb..0000000000 --- a/src/calibre/ebooks/mobi/from_any.py +++ /dev/null @@ -1,70 +0,0 @@ -''' -Convert any ebook format to Mobipocket. -''' - -from __future__ import with_statement - -__license__ = 'GPL v3' -__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net ' \ - 'and Marshall T. Vandegrift ' -__docformat__ = 'restructuredtext en' - -import sys, os, glob, logging - -from calibre.ebooks.epub.from_any import any2epub, formats, USAGE -from calibre.ebooks.epub import config as common_config -from calibre.ptempfile import TemporaryDirectory -from calibre.ebooks.mobi.writer import oeb2mobi, config as mobi_config - -def config(defaults=None): - c = common_config(defaults=defaults, name='mobi') - c.remove_opt('profile') - mobic = mobi_config(defaults=defaults) - c.update(mobic) - return c - -def option_parser(usage=USAGE): - usage = usage % ('Mobipocket', formats()) - parser = config().option_parser(usage=usage) - return parser - -def any2mobi(opts, path, notification=None): - ext = os.path.splitext(path)[1] - if not ext: - raise ValueError('Unknown file type: '+path) - ext = ext.lower()[1:] - - if opts.output is None: - opts.output = os.path.splitext(os.path.basename(path))[0]+'.mobi' - - opts.output = os.path.abspath(opts.output) - orig_output = opts.output - - with TemporaryDirectory('_any2mobi') as tdir: - oebdir = os.path.join(tdir, 'oeb') - os.mkdir(oebdir) - opts.output = os.path.join(tdir, 'dummy.epub') - opts.profile = 'None' - opts.dont_split_on_page_breaks = True - orig_bfs = opts.base_font_size2 - opts.base_font_size2 = 0 - any2epub(opts, path, create_epub=False, oeb_cover=True, extract_to=oebdir) - opts.base_font_size2 = orig_bfs - opf = glob.glob(os.path.join(oebdir, '*.opf'))[0] - opts.output = orig_output - logging.getLogger('html2epub').info(_('Creating Mobipocket file from EPUB...')) - oeb2mobi(opts, opf) - - -def main(args=sys.argv): - parser = option_parser() - opts, args = parser.parse_args(args) - if len(args) < 2: - parser.print_help() - print 'No input file specified.' - return 1 - any2mobi(opts, args[1]) - return 0 - -if __name__ == '__main__': - sys.exit(main()) diff --git a/src/calibre/ebooks/mobi/from_comic.py b/src/calibre/ebooks/mobi/from_comic.py deleted file mode 100644 index 87d63ea15f..0000000000 --- a/src/calibre/ebooks/mobi/from_comic.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python -__license__ = 'GPL v3' -__copyright__ = '2009, Kovid Goyal kovid@kovidgoyal.net' -__docformat__ = 'restructuredtext en' - -''' -''' -import sys, os -from calibre.ebooks.lrf.comic.convert_from import do_convert, option_parser, \ - ProgressBar, terminal_controller -from calibre.ebooks.mobi.from_any import config, any2mobi -from calibre.ptempfile import PersistentTemporaryFile - - -def convert(path_to_file, opts, notification=lambda m, p: p): - pt = PersistentTemporaryFile('_comic2mobi.epub') - pt.close() - orig_output = opts.output - opts.output = pt.name - do_convert(path_to_file, opts, notification=notification, output_format='epub') - opts = config('').parse() - if orig_output is None: - orig_output = os.path.splitext(path_to_file)[0]+'.mobi' - opts.output = orig_output - any2mobi(opts, pt.name) - -def main(args=sys.argv): - 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 - - pb = ProgressBar(terminal_controller, _('Rendering comic pages...'), - no_progress_bar=opts.no_progress_bar or getattr(opts, 'no_process', False)) - notification = pb.update - - source = os.path.abspath(args[1]) - convert(source, opts, notification=notification) - return 0 - -if __name__ == '__main__': - sys.exit(main()) \ No newline at end of file diff --git a/src/calibre/ebooks/mobi/from_feeds.py b/src/calibre/ebooks/mobi/from_feeds.py deleted file mode 100644 index 205550f730..0000000000 --- a/src/calibre/ebooks/mobi/from_feeds.py +++ /dev/null @@ -1,74 +0,0 @@ -from __future__ import with_statement -__license__ = 'GPL v3' -__copyright__ = '2009, Kovid Goyal kovid@kovidgoyal.net' -__docformat__ = 'restructuredtext en' - -''' -Convert feeds to MOBI ebook -''' - -import sys, glob, os -from calibre.web.feeds.main import config as feeds2disk_config, USAGE, run_recipe -from calibre.ebooks.mobi.writer import config as oeb2mobi_config, oeb2mobi -from calibre.ptempfile import TemporaryDirectory -from calibre import strftime, sanitize_file_name - -def config(defaults=None): - c = feeds2disk_config(defaults=defaults) - c.remove('lrf') - c.remove('epub') - c.remove('mobi') - c.remove('output_dir') - c.update(oeb2mobi_config(defaults=defaults)) - c.remove('encoding') - c.remove('source_profile') - c.add_opt('output', ['-o', '--output'], default=None, - help=_('Output file. Default is derived from input filename.')) - return c - -def option_parser(): - c = config() - return c.option_parser(usage=USAGE) - -def convert(opts, recipe_arg, notification=None): - opts.lrf = False - opts.epub = False - opts.mobi = True - if opts.debug: - opts.verbose = 2 - parser = option_parser() - with TemporaryDirectory('_feeds2mobi') as tdir: - opts.output_dir = tdir - recipe = run_recipe(opts, recipe_arg, parser, notification=notification) - c = config() - recipe_opts = c.parse_string(recipe.oeb2mobi_options) - c.smart_update(recipe_opts, opts) - opts = recipe_opts - opf = glob.glob(os.path.join(tdir, '*.opf')) - if not opf: - raise Exception('Downloading of recipe: %s failed'%recipe_arg) - opf = opf[0] - - if opts.output is None: - fname = recipe.title + strftime(recipe.timefmt) + '.mobi' - opts.output = os.path.join(os.getcwd(), sanitize_file_name(fname)) - - print 'Generating MOBI...' - opts.encoding = 'utf-8' - opts.source_profile = 'Browser' - oeb2mobi(opts, opf) - - -def main(args=sys.argv, notification=None, handler=None): - parser = option_parser() - opts, args = parser.parse_args(args) - if len(args) != 2 and opts.feeds is None: - parser.print_help() - return 1 - recipe_arg = args[1] if len(args) > 1 else None - convert(opts, recipe_arg, notification=notification) - - return 0 - -if __name__ == '__main__': - sys.exit(main()) \ No newline at end of file diff --git a/src/calibre/ebooks/oeb/transforms/jacket.py b/src/calibre/ebooks/oeb/transforms/jacket.py index 78f4ab871e..8c995dadec 100644 --- a/src/calibre/ebooks/oeb/transforms/jacket.py +++ b/src/calibre/ebooks/oeb/transforms/jacket.py @@ -70,9 +70,8 @@ class Jacket(object): tags = map(unicode, self.oeb.metadata.subject) except: tags = [] - tags = u'/'.join(tags) if tags: - tags = 'Tags: ' + u'/%s/'%tags + tags = 'Tags: ' + self.opts.dest.tags_to_string(tags) else: tags = '' try: diff --git a/src/calibre/ebooks/pdb/ereader/reader.py b/src/calibre/ebooks/pdb/ereader/reader.py index a1ab0a7a65..13e204fd5e 100644 --- a/src/calibre/ebooks/pdb/ereader/reader.py +++ b/src/calibre/ebooks/pdb/ereader/reader.py @@ -8,7 +8,7 @@ __license__ = 'GPL v3' __copyright__ = '2009, John Schember ' __docformat__ = 'restructuredtext en' -import os, re, sys, struct, zlib +import os, re, struct, zlib from calibre import CurrentDir from calibre.ebooks import DRMError @@ -30,7 +30,7 @@ class HeaderRecord(object): def __init__(self, raw): self.version, = struct.unpack('>H', raw[0:2]) - self.non_text_offset, = struct.unpack('>H', raw[12:14]) + self.non_text_offset, = struct.unpack('>H', raw[12:14]) self.footnote_rec, = struct.unpack('>H', raw[28:30]) self.sidebar_rec, = struct.unpack('>H', raw[30:32]) self.bookmark_offset, = struct.unpack('>H', raw[32:34]) @@ -39,17 +39,17 @@ class HeaderRecord(object): self.footnote_offset, = struct.unpack('>H', raw[48:50]) self.sidebar_offset, = struct.unpack('>H', raw[50:52]) self.last_data_offset, = struct.unpack('>H', raw[52:54]) - + self.num_text_pages = self.non_text_offset - 1 self.num_image_pages = self.metadata_offset - self.image_data_offset - + class Reader(FormatReader): def __init__(self, header, stream, log, encoding=None): self.log = log self.encoding = encoding - + self.sections = [] for i in range(header.num_sections): self.sections.append(header.section_data(i)) @@ -61,17 +61,17 @@ class Reader(FormatReader): raise DRMError('eReader DRM is not supported.') else: raise EreaderError('Unknown book version %i.' % self.header_record.version) - + def section_data(self, number): return self.sections[number] - + def decompress_text(self, number): if self.header_record.version == 2: return decompress_doc(self.section_data(number)).decode('cp1252' if self.encoding is None else self.encoding) if self.header_record.version == 10: return zlib.decompress(self.section_data(number)).decode('cp1252' if self.encoding is None else self.encoding) - + def get_image(self, number): if number < self.header_record.image_data_offset or number > self.header_record.image_data_offset + self.header_record.num_image_pages - 1: return 'empty', '' @@ -79,7 +79,7 @@ class Reader(FormatReader): name = data[4:4+32].strip('\x00') img = data[62:] return name, img - + def get_text_page(self, number): ''' Only palmdoc and zlib compressed are supported. The text is @@ -88,21 +88,21 @@ class Reader(FormatReader): ''' if number not in range(1, self.header_record.num_text_pages + 1): return '' - + return self.decompress_text(number) def extract_content(self, output_dir): output_dir = os.path.abspath(output_dir) - + if not os.path.exists(output_dir): os.makedirs(output_dir) - + html = u'' - + for i in range(1, self.header_record.num_text_pages + 1): self.log.debug('Extracting text page %i' % i) html += pml_to_html(self.get_text_page(i)) - + if self.header_record.footnote_rec > 0: html += '

%s

' % _('Footnotes') footnoteids = re.findall('\w+(?=\x00)', self.section_data(self.header_record.footnote_offset).decode('cp1252' if self.encoding is None else self.encoding)) @@ -110,8 +110,8 @@ class Reader(FormatReader): self.log.debug('Extracting footnote page %i' % i) html += '
' html += footnote_sidebar_to_html(footnoteids[fid], self.decompress_text(i)) - html += '
' - + html += '' + if self.header_record.sidebar_rec > 0: html += '

%s

' % _('Sidebar') sidebarids = re.findall('\w+(?=\x00)', self.section_data(self.header_record.sidebar_offset).decode('cp1252' if self.encoding is None else self.encoding)) @@ -120,9 +120,9 @@ class Reader(FormatReader): html += '
' html += footnote_sidebar_to_html(sidebarids[sid], self.decompress_text(i)) html += '
' - + html += '' - + with CurrentDir(output_dir): with open('index.html', 'wb') as index: self.log.debug('Writing text to index.html') @@ -138,19 +138,19 @@ class Reader(FormatReader): with open(name, 'wb') as imgf: self.log.debug('Writing image %s to images/' % name) imgf.write(img) - + opf_path = self.create_opf(output_dir, images) - + return opf_path - + def create_opf(self, output_dir, images): mi = MetaInformation(None, None) - + with CurrentDir(output_dir): opf = OPFCreator(output_dir, mi) - + manifest = [('index.html', None)] - + for i in images: manifest.append((os.path.join('images/', i), None)) @@ -158,21 +158,21 @@ class Reader(FormatReader): opf.create_spine(['index.html']) with open('metadata.opf', 'wb') as opffile: opf.render(opffile) - + return os.path.join(output_dir, 'metadata.opf') - + def dump_pml(self): ''' This is primarily used for debugging and 3rd party tools to get the plm markup that comprises the text in the file. ''' pml = '' - + for i in range(1, self.header_record.num_text_pages + 1): pml += self.get_text_page(i) - + return pml - + def dump_images(self, output_dir): ''' This is primarily used for debugging and 3rd party tools to @@ -181,7 +181,7 @@ class Reader(FormatReader): if not os.path.exists(output_dir): os.makedirs(output_dir) - with CurrentDir(output_dir): + with CurrentDir(output_dir): for i in range(0, self.header_record.num_image_pages): name, img = self.get_image(self.header_record.image_data_offset + i) with open(name, 'wb') as imgf: diff --git a/src/calibre/ebooks/pdb/header.py b/src/calibre/ebooks/pdb/header.py index 8a9b7b105c..48c39fc0ad 100644 --- a/src/calibre/ebooks/pdb/header.py +++ b/src/calibre/ebooks/pdb/header.py @@ -8,7 +8,7 @@ __license__ = 'GPL v3' __copyright__ = '2009, John Schember ' __docformat__ = 'restructuredtext en' -import os, re, struct, time +import re, struct, time class PdbHeaderReader(object): @@ -53,7 +53,8 @@ class PdbHeaderReader(object): start = self.section_offset(number) if number == self.num_sections -1: - end = os.stat(self.stream.name).st_size + self.stream.seek(0, 2) + end = self.stream.tell() else: end = self.section_offset(number + 1) self.stream.seek(start) @@ -65,18 +66,18 @@ class PdbHeaderBuilder(object): def __init__(self, identity, title): self.identity = identity.ljust(3, '\x00')[:8] self.title = re.sub('[^-A-Za-z0-9]+', '_', title).ljust(32, '\x00')[:32] - + def build_header(self, section_lengths, out_stream): ''' section_lengths = Lenght of each section in file. ''' - + now = int(time.time()) nrecords = len(section_lengths) - + out_stream.write(self.title + struct.pack('>HHIIIIII', 0, 0, now, now, 0, 0, 0, 0)) out_stream.write(self.identity + struct.pack('>IIH', nrecords, 0, nrecords)) - + offset = 78 + (8 * nrecords) + 2 for id, record in enumerate(section_lengths): out_stream.write(struct.pack('>LBBBB', long(offset), 0, 0, 0, 0)) diff --git a/src/calibre/ebooks/pdf/output.py b/src/calibre/ebooks/pdf/output.py index ae44d270f7..0cc73931f4 100644 --- a/src/calibre/ebooks/pdf/output.py +++ b/src/calibre/ebooks/pdf/output.py @@ -15,7 +15,6 @@ import os, glob from calibre.customize.conversion import OutputFormatPlugin, \ OptionRecommendation -from calibre.ebooks.oeb.output import OEBOutput from calibre.ebooks.metadata.opf2 import OPF from calibre.ptempfile import TemporaryDirectory from calibre.ebooks.pdf.writer import PDFWriter, ImagePDFWriter, PDFMetadata @@ -52,24 +51,24 @@ class PDFOutput(OutputFormatPlugin): self.input_plugin, self.opts, self.log = input_plugin, opts, log self.output_path = output_path self.metadata = oeb_book.metadata - + if input_plugin.is_image_collection: self.convert_images(input_plugin.get_images()) else: self.convert_text(oeb_book) - + def convert_images(self, images): self.write(ImagePDFWriter, images) - + def convert_text(self, oeb_book): with TemporaryDirectory('_pdf_out') as oeb_dir: from calibre.customize.ui import plugin_for_output_format oeb_output = plugin_for_output_format('oeb') oeb_output.convert(oeb_book, oeb_dir, self.input_plugin, self.opts, self.log) - + opfpath = glob.glob(os.path.join(oeb_dir, '*.opf'))[0] opf = OPF(opfpath, os.path.dirname(opfpath)) - + self.write(PDFWriter, [s.path for s in opf.spine]) def write(self, Writer, items): diff --git a/src/calibre/gui2/convert/__init__.py b/src/calibre/gui2/convert/__init__.py new file mode 100644 index 0000000000..dd24d05120 --- /dev/null +++ b/src/calibre/gui2/convert/__init__.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os + +from PyQt4.Qt import QWidget, QSpinBox, QDoubleSpinBox, QLineEdit, QTextEdit, \ + QCheckBox, QComboBox, Qt, QIcon, SIGNAL + +from calibre.customize.conversion import OptionRecommendation +from calibre.utils.config import config_dir +from calibre.utils.lock import ExclusiveFile +from calibre import sanitize_file_name + +config_dir = os.path.join(config_dir, 'conversion') +if not os.path.exists(config_dir): + os.makedirs(config_dir) + +def name_to_path(name): + return os.path.join(config_dir, sanitize_file_name(name)+'.py') + +def save_defaults(name, recs): + path = name_to_path(name) + raw = str(recs) + with open(path, 'wb'): + pass + with ExclusiveFile(path) as f: + f.write(raw) +save_defaults_ = save_defaults + +def load_defaults(name): + path = name_to_path(name) + if not os.path.exists(path): + open(path, 'wb').close() + with ExclusiveFile(path) as f: + raw = f.read() + r = GuiRecommendations() + if raw: + r.from_string(raw) + return r + +def save_specifics(db, book_id, recs): + raw = str(recs) + db.set_conversion_options(book_id, 'PIPE', raw) + +def load_specifics(db, book_id): + raw = db.conversion_options(book_id, 'PIPE') + r = GuiRecommendations() + if raw: + r.from_string(raw) + return r + +class GuiRecommendations(dict): + + def __new__(cls, *args): + dict.__new__(cls) + obj = super(GuiRecommendations, cls).__new__(cls, *args) + obj.disabled_options = set([]) + return obj + + def to_recommendations(self, level=OptionRecommendation.LOW): + ans = [] + for key, val in self.items(): + ans.append((key, val, level)) + return ans + + def __str__(self): + ans = ['{'] + for key, val in self.items(): + ans.append('\t'+repr(key)+' : '+repr(val)+',') + ans.append('}') + return '\n'.join(ans) + + def from_string(self, raw): + try: + d = eval(raw) + except SyntaxError: + d = None + if d: + self.update(d) + + def merge_recommendations(self, get_option, level, options): + for name in options: + opt = get_option(name) + if opt is None: continue + if opt.level == OptionRecommendation.HIGH: + self[name] = opt.recommended_value + self.disabled_options.add(name) + elif opt.level > level or name not in self: + self[name] = opt.recommended_value + + +class Widget(QWidget): + + TITLE = _('Unknown') + ICON = ':/images/config.svg' + HELP = '' + + def __init__(self, parent, name, options): + QWidget.__init__(self, parent) + self.setupUi(self) + self._options = options + self._name = name + self._icon = QIcon(self.ICON) + for name in self._options: + if not hasattr(self, 'opt_'+name): + raise Exception('Option %s missing in %s'%(name, + self.__class__.__name__)) + + def initialize_options(self, get_option, get_help, db=None, book_id=None): + ''' + :param get_option: A callable that takes one argument: the option name + and returns the correspoing OptionRecommendation. + :param get_help: A callable that takes the option name and return a help + string. + ''' + defaults = load_defaults(self._name) + defaults.merge_recommendations(get_option, OptionRecommendation.LOW, + self._options) + + if db is not None: + specifics = load_specifics(db, book_id) + specifics.merge_recommendations(get_option, OptionRecommendation.HIGH, + self._options) + defaults.update(specifics) + self.apply_recommendations(defaults) + self.setup_help(get_help) + + def commit_options(self, save_defaults=False): + recs = self.create_recommendations() + if save_defaults: + save_defaults_(self._name, recs) + return recs + + def create_recommendations(self): + recs = GuiRecommendations() + for name in self._options: + gui_opt = getattr(self, 'opt_'+name, None) + if gui_opt is None: continue + recs[name] = self.get_value(gui_opt) + return recs + + def apply_recommendations(self, recs): + for name, val in recs.items(): + gui_opt = getattr(self, 'opt_'+name, None) + if gui_opt is None: continue + self.set_value(gui_opt, val) + if name in getattr(recs, 'disabled_options', []): + gui_opt.setDisabled(True) + + + def get_value(self, g): + ret = self.get_value_handler(g) + if ret != 'this is a dummy return value, xcswx1avcx4x': + return ret + if isinstance(g, (QSpinBox, QDoubleSpinBox)): + return g.value() + elif isinstance(g, (QLineEdit, QTextEdit)): + func = getattr(g, 'toPlainText', getattr(g, 'text', None))() + ans = unicode(func).strip() + if not ans: + ans = None + return ans + elif isinstance(g, QComboBox): + return unicode(g.currentText()) + elif isinstance(g, QCheckBox): + return bool(g.isChecked()) + else: + raise Exception('Can\'t get value from %s'%type(g)) + + + def set_value(self, g, val): + if self.set_value_handler(g, val): + return + if isinstance(g, (QSpinBox, QDoubleSpinBox)): + g.setValue(val) + elif isinstance(g, (QLineEdit, QTextEdit)): + if not val: val = '' + getattr(g, 'setPlainText', g.setText)(val) + getattr(g, 'setCursorPosition', lambda x: x)(0) + elif isinstance(g, QComboBox) and val: + idx = g.findText(val, Qt.MatchFixedString) + if idx < 0: + g.addItem(val) + idx = g.findText(val, Qt.MatchFixedString) + g.setCurrentIndex(idx) + elif isinstance(g, QCheckBox): + g.setCheckState(Qt.Checked if bool(val) else Qt.Unchecked) + else: + raise Exception('Can\'t set value %s in %s'%(repr(val), type(g))) + self.post_set_value(g, val) + + def set_help(self, msg): + if msg and getattr(msg, 'strip', lambda:True)(): + self.emit(SIGNAL('set_help(PyQt_PyObject)'), msg) + + def setup_help(self, help_provider): + for name in self._options: + g = getattr(self, 'opt_'+name, None) + if g is None: + continue + help = help_provider(name) + if not help: continue + g._help = help + g.setToolTip(help.replace('<', '<').replace('>', '>')) + g.setWhatsThis(help.replace('<', '<').replace('>', '>')) + g.__class__.enterEvent = lambda obj, event: self.set_help(getattr(obj, '_help', obj.toolTip())) + + + def set_value_handler(self, g, val): + return False + + def post_set_value(self, g, val): + pass + + def get_value_handler(self, g): + return 'this is a dummy return value, xcswx1avcx4x' + + def post_get_value(self, g): + pass + + def commit(self, save_defaults=False): + return self.commit_options(save_defaults) + + def config_title(self): + return self.TITLE + + def config_icon(self): + return self._icon + diff --git a/src/calibre/gui2/convert/comic_input.py b/src/calibre/gui2/convert/comic_input.py new file mode 100644 index 0000000000..00e095fb2a --- /dev/null +++ b/src/calibre/gui2/convert/comic_input.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +from calibre.gui2.convert.comic_input_ui import Ui_Form +from calibre.gui2.convert import Widget + +class PluginWidget(Widget, Ui_Form): + + TITLE = _('Comic Input') + + def __init__(self, parent, get_option, get_help, db=None, book_id=None): + Widget.__init__(self, parent, 'comic_input', + ['colors', 'dont_normalize', 'keep_aspect_ratio', 'right2left', + 'despeckle', 'no_sort', 'no_process', 'landscape', + 'dont_sharpen', 'disable_trim', 'wide'] + ) + self.db, self.book_id = db, book_id + self.initialize_options(get_option, get_help, db, book_id) + self.opt_no_process.toggle() + self.opt_no_process.toggle() + + diff --git a/src/calibre/gui2/convert/comic_input.ui b/src/calibre/gui2/convert/comic_input.ui new file mode 100644 index 0000000000..2ca98fa7b4 --- /dev/null +++ b/src/calibre/gui2/convert/comic_input.ui @@ -0,0 +1,304 @@ + + + Form + + + + 0 + 0 + 599 + 305 + + + + Form + + + + + + &Number of Colors: + + + opt_colors + + + + + + + 8 + + + 3200000 + + + 8 + + + + + + + Disable &normalize + + + + + + + Keep &aspect ratio + + + + + + + Disable &Sharpening + + + + + + + Disable &Trimming + + + + + + + &Wide + + + + + + + &Landscape + + + + + + + &Right to left + + + + + + + Don't so&rt + + + + + + + De&speckle + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + &Disable comic processing + + + + + + + + + opt_no_process + toggled(bool) + label_3 + setDisabled(bool) + + + 184 + 11 + + + 210 + 44 + + + + + opt_no_process + toggled(bool) + opt_colors + setDisabled(bool) + + + 284 + 17 + + + 371 + 39 + + + + + opt_no_process + toggled(bool) + opt_dont_normalize + setDisabled(bool) + + + 107 + 15 + + + 20 + 67 + + + + + opt_no_process + toggled(bool) + opt_keep_aspect_ratio + setDisabled(bool) + + + 171 + 11 + + + 38 + 98 + + + + + opt_no_process + toggled(bool) + opt_dont_sharpen + setDisabled(bool) + + + 126 + 12 + + + 110 + 124 + + + + + opt_no_process + toggled(bool) + opt_disable_trim + setDisabled(bool) + + + 153 + 5 + + + 67 + 155 + + + + + opt_no_process + toggled(bool) + opt_wide + setDisabled(bool) + + + 164 + 13 + + + 84 + 180 + + + + + opt_no_process + toggled(bool) + opt_landscape + setDisabled(bool) + + + 129 + 11 + + + 145 + 209 + + + + + opt_no_process + toggled(bool) + opt_right2left + setDisabled(bool) + + + 183 + 15 + + + 52 + 225 + + + + + opt_no_process + toggled(bool) + opt_no_sort + setDisabled(bool) + + + 162 + 11 + + + 105 + 256 + + + + + opt_no_process + toggled(bool) + opt_despeckle + setDisabled(bool) + + + 254 + 19 + + + 180 + 296 + + + + + diff --git a/src/calibre/gui2/convert/epub_output.py b/src/calibre/gui2/convert/epub_output.py new file mode 100644 index 0000000000..74f0913398 --- /dev/null +++ b/src/calibre/gui2/convert/epub_output.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +from calibre.gui2.convert.epub_output_ui import Ui_Form +from calibre.gui2.convert import Widget + +class PluginWidget(Widget, Ui_Form): + + TITLE = _('EPUB Output') + + def __init__(self, parent, get_option, get_help, db=None, book_id=None): + Widget.__init__(self, parent, 'epub_output', + ['dont_split_on_page_breaks', 'flow_size'] + ) + self.db, self.book_id = db, book_id + self.initialize_options(get_option, get_help, db, book_id) + diff --git a/src/calibre/gui2/convert/epub_output.ui b/src/calibre/gui2/convert/epub_output.ui new file mode 100644 index 0000000000..acd95beb7d --- /dev/null +++ b/src/calibre/gui2/convert/epub_output.ui @@ -0,0 +1,67 @@ + + + Form + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + Do not &split on page breaks + + + + + + + Split files &larger than: + + + opt_flow_size + + + + + + + KB + + + 100 + + + 1000000 + + + 20 + + + + + + + Qt::Vertical + + + + 20 + 262 + + + + + + + + + diff --git a/src/calibre/gui2/convert/look_and_feel.py b/src/calibre/gui2/convert/look_and_feel.py new file mode 100644 index 0000000000..e2015dc380 --- /dev/null +++ b/src/calibre/gui2/convert/look_and_feel.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +from calibre.gui2.convert.look_and_feel_ui import Ui_Form +from calibre.gui2.convert import Widget + +class LookAndFeelWidget(Widget, Ui_Form): + + TITLE = _('Look & Feel') + ICON = ':/images/lookfeel.svg' + HELP = _('Control the look and feel of the output') + + def __init__(self, parent, get_option, get_help, db=None, book_id=None): + Widget.__init__(self, parent, 'look_and_feel', + ['dont_justify', 'extra_css', 'base_font_size', + 'font_size_mapping', 'insert_metadata', 'line_height', + 'linearize_tables', 'remove_first_image', + 'remove_paragraph_spacing', 'input_encoding'] + ) + self.db, self.book_id = db, book_id + self.initialize_options(get_option, get_help, db, book_id) + diff --git a/src/calibre/gui2/convert/look_and_feel.ui b/src/calibre/gui2/convert/look_and_feel.ui new file mode 100644 index 0000000000..c6a1a715a6 --- /dev/null +++ b/src/calibre/gui2/convert/look_and_feel.ui @@ -0,0 +1,153 @@ + + + Form + + + + 0 + 0 + 600 + 500 + + + + Form + + + + + + + + Base &font size: + + + opt_base_font_size + + + + + + + pt + + + 1 + + + 0.000000000000000 + + + 30.000000000000000 + + + 1.000000000000000 + + + 15.000000000000000 + + + + + + + Line &height: + + + opt_line_height + + + + + + + pt + + + 1 + + + + + + + Remove &spacing between paragraphs + + + + + + + No text &justification + + + + + + + &Linearize tables + + + + + + + Remove &first image from source file + + + + + + + Font size &key: + + + opt_font_size_mapping + + + + + + + + + + Insert &metadata at start of book + + + + + + + Input character &encoding + + + opt_input_encoding + + + + + + + + + + + + Extra &CSS + + + + + + + + + + + + + + + + diff --git a/src/calibre/gui2/convert/lrf_output.py b/src/calibre/gui2/convert/lrf_output.py new file mode 100644 index 0000000000..78a7bfa3fa --- /dev/null +++ b/src/calibre/gui2/convert/lrf_output.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +from calibre.gui2.convert.lrf_output_ui import Ui_Form +from calibre.gui2.convert import Widget +from calibre.gui2.widgets import FontFamilyModel + +font_family_model = None + +class PluginWidget(Widget, Ui_Form): + + TITLE = _('LRF Output') + + def __init__(self, parent, get_option, get_help, db=None, book_id=None): + Widget.__init__(self, parent, 'lrf_output', + ['wordspace', 'header', 'header_format', + 'minimum_indent', 'serif_family', + 'render_tables_as_images', 'sans_family', 'mono_family', + 'text_size_multiplier_for_rendered_tables', 'autorotation', + 'header_separation', 'minimum_indent'] + ) + self.db, self.book_id = db, book_id + global font_family_model + if font_family_model is None: + font_family_model = FontFamilyModel() + self.font_family_model = font_family_model + self.opt_serif_family.setModel(self.font_family_model) + self.opt_sans_family.setModel(self.font_family_model) + self.opt_mono_family.setModel(self.font_family_model) + + self.initialize_options(get_option, get_help, db, book_id) + self.opt_header.toggle(), self.opt_header.toggle() + self.opt_render_tables_as_images.toggle() + self.opt_render_tables_as_images.toggle() + + + def set_value_handler(self, g, val): + if val is None and unicode(g.objectName()) in ('opt_serif_family', + 'opt_sans_family', 'opt_mono_family'): + g.setCurrentIndex(0) + return True + return False diff --git a/src/calibre/gui2/convert/lrf_output.ui b/src/calibre/gui2/convert/lrf_output.ui new file mode 100644 index 0000000000..ecbe673c61 --- /dev/null +++ b/src/calibre/gui2/convert/lrf_output.ui @@ -0,0 +1,304 @@ + + + Form + + + + 0 + 0 + 588 + 481 + + + + Form + + + + + + Enable &autorotation of wide images + + + + + + + + + &Wordspace: + + + opt_wordspace + + + + + + + pt + + + 1 + + + 1.000000000000000 + + + 20.000000000000000 + + + 2.500000000000000 + + + + + + + Minimum para. &indent: + + + opt_minimum_indent + + + + + + + pt + + + 1 + + + + + + + + + Render &tables as images + + + + + + + + + Text size multiplier for text in rendered tables: + + + + + + + + + + + + + + Add &header + + + + + + + Header &separation: + + + opt_header_separation + + + + + + + pt + + + 1 + + + + + + + Header &format: + + + opt_header_format + + + + + + + + + + + + &Embed fonts + + + + + + &Serif font family: + + + opt_serif_family + + + + + + + S&ans-serif font family: + + + opt_sans_family + + + + + + + &Monospaced font family: + + + opt_mono_family + + + + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + opt_render_tables_as_images + toggled(bool) + opt_text_size_multiplier_for_rendered_tables + setEnabled(bool) + + + 76 + 80 + + + 418 + 105 + + + + + opt_header + toggled(bool) + opt_header_separation + setEnabled(bool) + + + 346 + 144 + + + 408 + 167 + + + + + opt_header + toggled(bool) + opt_header_format + setEnabled(bool) + + + 124 + 138 + + + 230 + 208 + + + + + opt_render_tables_as_images + toggled(bool) + label_3 + setEnabled(bool) + + + 96 + 76 + + + 31 + 113 + + + + + opt_header + toggled(bool) + label_4 + setEnabled(bool) + + + 67 + 144 + + + 72 + 165 + + + + + opt_header + toggled(bool) + label_5 + setEnabled(bool) + + + 107 + 141 + + + 102 + 211 + + + + + diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py new file mode 100644 index 0000000000..31d8db0867 --- /dev/null +++ b/src/calibre/gui2/convert/metadata.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os, uuid + +from PyQt4.Qt import QPixmap, SIGNAL + +from calibre.gui2 import choose_images, error_dialog, pixmap_to_data +from calibre.gui2.convert.metadata_ui import Ui_Form +from calibre.ebooks.metadata import authors_to_string, string_to_authors, \ + MetaInformation +from calibre.ebooks.metadata.opf2 import OPFCreator +from calibre.ptempfile import PersistentTemporaryFile +from calibre.gui2.convert import Widget + +class MetadataWidget(Widget, Ui_Form): + + TITLE = _('Metadata') + ICON = ':/images/dialog_information.svg' + HELP = _('Set the metadata. The output file will contain as much of this ' + 'metadata as possible.') + + def __init__(self, parent, get_option, get_help, db=None, book_id=None): + Widget.__init__(self, parent, 'metadata', ['prefer_metadata_cover']) + self.db, self.book_id = db, book_id + self.cover_changed = False + if self.db is not None: + self.initialize_metadata_options() + self.initialize_options(get_option, get_help, db, book_id) + self.connect(self.cover_button, SIGNAL("clicked()"), self.select_cover) + + def initialize_metadata_options(self): + all_series = self.db.all_series() + all_series.sort(cmp=lambda x, y : cmp(x[1], y[1])) + for series in all_series: + self.series.addItem(series[1]) + self.series.setCurrentIndex(-1) + + mi = self.db.get_metadata(self.book_id, index_is_id=True) + self.title.setText(mi.title) + if mi.authors: + self.author.setText(authors_to_string(mi.authors)) + else: + self.author.setText('') + self.publisher.setText(mi.publisher if mi.publisher else '') + self.author_sort.setText(mi.author_sort if mi.author_sort else '') + self.tags.setText(', '.join(mi.tags if mi.tags else [])) + self.comment.setText(mi.comments if mi.comments else '') + if mi.series: + self.series.setCurrentIndex(self.series.findText(mi.series)) + if mi.series_index is not None: + self.series_index.setValue(mi.series_index) + + cover = self.db.cover(self.book_id, index_is_id=True) + if cover: + pm = QPixmap() + pm.loadFromData(cover) + if not pm.isNull(): + self.cover.setPixmap(pm) + + def get_title_and_authors(self): + title = unicode(self.title.text()).strip() + if not title: + title = _('Unknown') + authors = unicode(self.author.text()).strip() + authors = string_to_authors(authors) if authors else [_('Unknown')] + return title, authors + + def get_metadata(self): + title, authors = self.get_title_and_authors() + mi = MetaInformation(title, authors) + publisher = unicode(self.publisher.text()).strip() + if publisher: + mi.publisher = publisher + author_sort = unicode(self.author_sort.text()).strip() + if author_sort: + mi.author_sort = author_sort + comments = unicode(self.comment.toPlainText()).strip() + if comments: + mi.comments = comments + mi.series_index = int(self.series_index.value()) + if self.series.currentIndex() > -1: + mi.series = unicode(self.series.currentText()).strip() + tags = [t.strip() for t in unicode(self.tags.text()).strip().split(',')] + if tags: + mi.tags = tags + + return mi + + def select_cover(self): + files = choose_images(self, 'change cover dialog', + _('Choose cover for ') + unicode(self.title.text())) + if not files: + return + _file = files[0] + if _file: + _file = os.path.abspath(_file) + if not os.access(_file, os.R_OK): + d = error_dialog(self.window, _('Cannot read'), + _('You do not have permission to read the file: ') + _file) + d.exec_() + return + cf, cover = None, None + try: + cf = open(_file, "rb") + cover = cf.read() + except IOError, e: + d = error_dialog(self.window, _('Error reading file'), + _("

There was an error reading from file:
") + _file + "


"+str(e)) + d.exec_() + if cover: + pix = QPixmap() + pix.loadFromData(cover) + if pix.isNull(): + d = error_dialog(self.window, _('Error reading file'), + _file + _(" is not a valid picture")) + d.exec_() + else: + self.cover_path.setText(_file) + self.cover.setPixmap(pix) + self.cover_changed = True + self.cpixmap = pix + + def get_recommendations(self): + return { + 'prefer_metadata_cover': + bool(self.opt_prefer_metadata_cover.isChecked()), + } + + + def commit(self, save_defaults=False): + ''' + Settings are stored in two attributes: `opf_file` and `cover_file`. + Both may be None. Also returns a recommendation dictionary. + ''' + recs = self.commit_options(save_defaults) + self.user_mi = self.get_metadata() + self.cover_file = self.opf_file = None + if self.db is not None: + self.db.set_metadata(self.book_id, self.user_mi) + self.mi = self.db.get_metadata(self.book_id, index_is_id=True) + self.mi.application_id = uuid.uuid4() + opf = OPFCreator(os.getcwdu(), self.mi) + self.opf_file = PersistentTemporaryFile('.opf') + opf.render(self.opf_file) + self.opf_file.close() + if self.cover_changed: + self.db.set_cover(self.book_id, pixmap_to_data(self.cover.pixmap())) + cover = self.db.cover(self.book_id, index_is_id=True) + if cover: + cf = PersistentTemporaryFile('.jpeg') + cf.write(cover) + cf.close() + self.cover_file = cf + return recs + diff --git a/src/calibre/gui2/convert/metadata.ui b/src/calibre/gui2/convert/metadata.ui new file mode 100644 index 0000000000..5b68d6383d --- /dev/null +++ b/src/calibre/gui2/convert/metadata.ui @@ -0,0 +1,336 @@ + + Form + + + + 0 + 0 + 600 + 500 + + + + Form + + + + + + Book Cover + + + + + + 6 + + + 0 + + + + + Change &cover image: + + + cover_path + + + + + + + 6 + + + 0 + + + + + true + + + + + + + Browse for an image to use as the cover of this book. + + + ... + + + + :/images/document_open.svg:/images/document_open.svg + + + + + + + + + + + Use cover from &source file + + + true + + + + + + + + + + + + :/images/book.svg + + + true + + + Qt::AlignCenter + + + + + + + opt_prefer_metadata_cover + + + + + + + + + + + &Title: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + title + + + + + + + Change the title of this book + + + + + + + &Author(s): + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + author + + + + + + + + 1 + 0 + + + + Change the author(s) of this book. Multiple authors should be separated by an &. If the author name contains an &, use && to represent it. + + + + + + + Author So&rt: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + author_sort + + + + + + + + 0 + 0 + + + + Change the author(s) of this book. Multiple authors should be separated by a comma + + + + + + + &Publisher: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + publisher + + + + + + + Change the publisher of this book + + + + + + + Ta&gs: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + tags + + + + + + + Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas. + + + + + + + &Series: + + + Qt::PlainText + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + series + + + + + + + + 10 + 0 + + + + List of known series. You can add new series. + + + List of known series. You can add new series. + + + true + + + QComboBox::InsertAlphabetically + + + QComboBox::AdjustToContents + + + + + + + true + + + Series index. + + + Series index. + + + Book + + + 1 + + + 10000 + + + + + + + + + + 0 + 0 + + + + + 16777215 + 200 + + + + Comments + + + + + + + 16777215 + 180 + + + + + + + + + + + + + + ImageView + QLabel +
widgets.h
+
+
+ + + + + +
diff --git a/src/calibre/gui2/convert/mobi_output.py b/src/calibre/gui2/convert/mobi_output.py new file mode 100644 index 0000000000..32cb74ba47 --- /dev/null +++ b/src/calibre/gui2/convert/mobi_output.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + + +from calibre.gui2.convert.mobi_output_ui import Ui_Form +from calibre.gui2.convert import Widget + +class PluginWidget(Widget, Ui_Form): + + TITLE = _('MOBI Output') + + def __init__(self, parent, get_option, get_help, db=None, book_id=None): + Widget.__init__(self, parent, 'mobi_output', + ['prefer_author_sort', 'rescale_images', 'toc_title'] + ) + self.db, self.book_id = db, book_id + self.initialize_options(get_option, get_help, db, book_id) + diff --git a/src/calibre/gui2/convert/mobi_output.ui b/src/calibre/gui2/convert/mobi_output.ui new file mode 100644 index 0000000000..a91f48d592 --- /dev/null +++ b/src/calibre/gui2/convert/mobi_output.ui @@ -0,0 +1,61 @@ + + + Form + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + &Title for Table of Contents: + + + opt_toc_title + + + + + + + + + + Rescale images for &Palm devices + + + + + + + Use author &sort for author + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/calibre/gui2/convert/page_setup.py b/src/calibre/gui2/convert/page_setup.py new file mode 100644 index 0000000000..0d2ce91dd1 --- /dev/null +++ b/src/calibre/gui2/convert/page_setup.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import Qt, QAbstractListModel, QVariant, SIGNAL + +from calibre.gui2.convert.page_setup_ui import Ui_Form +from calibre.gui2.convert import Widget +from calibre.gui2 import NONE +from calibre.customize.ui import input_profiles, output_profiles + +class ProfileModel(QAbstractListModel): + + def __init__(self, profiles): + QAbstractListModel.__init__(self) + self.profiles = list(profiles) + + def rowCount(self, *args): + return len(self.profiles) + + def data(self, index, role): + profile = self.profiles[index.row()] + if role == Qt.DisplayRole: + return QVariant(profile.name) + if role in (Qt.ToolTipRole, Qt.StatusTipRole, Qt.WhatsThisRole): + return QVariant(profile.description) + return NONE + +class PageSetupWidget(Widget, Ui_Form): + + TITLE = _('Page Setup') + + def __init__(self, parent, get_option, get_help, db=None, book_id=None): + Widget.__init__(self, parent, 'lrf_output', + ['margin_top', 'margin_left', 'margin_right', 'margin_bottom', + 'input_profile', 'output_profile'] + ) + + self.db, self.book_id = db, book_id + self.input_model = ProfileModel(input_profiles()) + self.output_model = ProfileModel(output_profiles()) + self.opt_input_profile.setModel(self.input_model) + self.opt_output_profile.setModel(self.output_model) + for x in (self.opt_input_profile, self.opt_output_profile): + x.setMouseTracking(True) + self.connect(x, SIGNAL('entered(QModelIndex)'), self.show_desc) + self.initialize_options(get_option, get_help, db, book_id) + + def show_desc(self, index): + desc = index.model().data(index, Qt.StatusTipRole).toString() + self.profile_description.setText(desc) + + def set_value_handler(self, g, val): + if g in (self.opt_input_profile, self.opt_output_profile): + g.clearSelection() + for idx, p in enumerate(g.model().profiles): + if p.short_name == val: + break + idx = g.model().index(idx) + sm = g.selectionModel() + g.setCurrentIndex(idx) + sm.select(idx, sm.SelectCurrent) + return True + return False + + def get_value_handler(self, g): + if g in (self.opt_input_profile, self.opt_output_profile): + idx = g.currentIndex().row() + return g.model().profiles[idx].short_name + return Widget.get_value_handler(self, g) diff --git a/src/calibre/gui2/convert/page_setup.ui b/src/calibre/gui2/convert/page_setup.ui new file mode 100644 index 0000000000..0aa2a97e70 --- /dev/null +++ b/src/calibre/gui2/convert/page_setup.ui @@ -0,0 +1,189 @@ + + + Form + + + + 0 + 0 + 572 + 476 + + + + Form + + + + + + + + + + &Output profile: + + + opt_output_profile + + + + + + + + + + + + + 4 + 0 + + + + Profile description + + + + + + + 4 + 0 + + + + + + + true + + + + + + + + + + + + + + + + &Input profile: + + + opt_input_profile + + + + + + + + + + + + + 4 + 0 + + + + Margins + + + + + + &Left: + + + opt_margin_left + + + + + + + pt + + + 1 + + + + + + + &Top: + + + opt_margin_top + + + + + + + pt + + + 1 + + + + + + + &Right: + + + opt_margin_right + + + + + + + pt + + + 1 + + + + + + + &Bottom: + + + opt_margin_bottom + + + + + + + pt + + + 1 + + + + + + + + + + + + + diff --git a/src/calibre/gui2/convert/single.py b/src/calibre/gui2/convert/single.py new file mode 100644 index 0000000000..295656237b --- /dev/null +++ b/src/calibre/gui2/convert/single.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import sys, cPickle + +from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont + +from calibre.gui2 import ResizableDialog, NONE +from calibre.gui2.convert import GuiRecommendations, save_specifics, \ + load_specifics +from calibre.gui2.convert.single_ui import Ui_Dialog +from calibre.gui2.convert.metadata import MetadataWidget +from calibre.gui2.convert.look_and_feel import LookAndFeelWidget +from calibre.gui2.convert.page_setup import PageSetupWidget + +from calibre.ebooks.conversion.plumber import Plumber, supported_input_formats, \ + INPUT_FORMAT_PREFERENCES, OUTPUT_FORMAT_PREFERENCES +from calibre.customize.ui import available_output_formats +from calibre.customize.conversion import OptionRecommendation +from calibre.utils.logging import Log + +class NoSupportedInputFormats(Exception): + pass + +def sort_formats_by_preference(formats, prefs): + def fcmp(x, y): + try: + x = prefs.index(x) + except ValueError: + x = sys.maxint + try: + y = prefs.index(y) + except ValueError: + y = sys.maxint + return cmp(x, y) + return sorted(formats, cmp=fcmp) + +class GroupModel(QAbstractListModel): + + def __init__(self, widgets): + self.widgets = widgets + QAbstractListModel.__init__(self) + + def rowCount(self, *args): + return len(self.widgets) + + def data(self, index, role): + try: + widget = self.widgets[index.row()] + except: + return NONE + if role == Qt.DisplayRole: + return QVariant(widget.config_title()) + if role == Qt.DecorationRole: + return QVariant(widget.config_icon()) + if role == Qt.FontRole: + f = QFont() + f.setBold(True) + return QVariant(f) + return NONE + +class Config(ResizableDialog, Ui_Dialog): + ''' + Configuration dialog for single book conversion. If accepted, has the + following important attributes + + input_path - Path to input file + output_format - Output format (without a leading .) + input_format - Input format (without a leading .) + opf_path - Path to OPF file with user specified metadata + cover_path - Path to user specified cover (can be None) + recommendations - A pickled list of 3 tuples in the same format as the + recommendations member of the Input/Output plugins. + ''' + + def __init__(self, parent, db, book_id, + preferred_input_format=None, preferred_output_format=None): + ResizableDialog.__init__(self, parent) + + if preferred_input_format is None and db is not None: + recs = load_specifics(db, book_id) + if recs: + preferred_input_format = recs.get('gui_preferred_input_format', + None) + + self.setup_input_output_formats(db, book_id, preferred_input_format, + preferred_input_format) + self.db, self.book_id = db, book_id + self.setup_pipeline() + + self.connect(self.input_formats, SIGNAL('currentIndexChanged(QString)'), + self.setup_pipeline) + self.connect(self.output_formats, SIGNAL('currentIndexChanged(QString)'), + self.setup_pipeline) + self.connect(self.groups, SIGNAL('activated(QModelIndex)'), + self.show_pane) + self.connect(self.groups, SIGNAL('clicked(QModelIndex)'), + self.show_pane) + self.connect(self.groups, SIGNAL('entered(QModelIndex)'), + self.show_group_help) + self.groups.setMouseTracking(True) + + @property + def input_format(self): + return unicode(self.input_formats.currentText()).lower() + + @property + def output_format(self): + return unicode(self.output_formats.currentText()).lower() + + + def setup_pipeline(self, *args): + input_format = self.input_format + output_format = self.output_format + input_path = self.db.format_abspath(self.book_id, input_format, + index_is_id=True) + self.input_path = input_path + output_path = 'dummy.'+output_format + log = Log() + log.outputs = [] + self.plumber = Plumber(input_path, output_path, log) + + def widget_factory(cls): + return cls(self.stack, self.plumber.get_option_by_name, + self.plumber.get_option_help, self.db, self.book_id) + + + self.mw = widget_factory(MetadataWidget) + self.setWindowTitle(_('Convert')+ ' ' + unicode(self.mw.title.text())) + lf = widget_factory(LookAndFeelWidget) + ps = widget_factory(PageSetupWidget) + + output_widget = None + name = self.plumber.output_plugin.name.lower().replace(' ', '_') + try: + output_widget = __import__(name) + pw = output_widget.PluginWidget + pw.ICON = ':/images/back.svg' + pw.HELP = _('Options specific to the output format.') + output_widget = widget_factory(pw) + except ImportError: + pass + input_widget = None + name = self.plumber.input_plugin.name.lower().replace(' ', '_') + try: + input_widget = __import__(name) + pw = input_widget.PluginWidget + pw.ICON = ':/images/forward.svg' + pw.HELP = _('Options specific to the input format.') + input_widget = widget_factory(pw) + except ImportError: + pass + + while True: + c = self.stack.currentWidget() + if not c: break + self.stack.removeWidget(c) + + widgets = [self.mw, lf, ps] + if input_widget is not None: + widgets.append(input_widget) + if output_widget is not None: + widgets.append(output_widget) + for w in widgets: + self.stack.addWidget(w) + self.connect(w, SIGNAL('set_help(PyQt_PyObject)'), + self.help.setPlainText) + + self._groups_model = GroupModel(widgets) + self.groups.setModel(self._groups_model) + + self.groups.setCurrentIndex(self._groups_model.index(0)) + + + def setup_input_output_formats(self, db, book_id, preferred_input_format, + preferred_output_format): + available_formats = db.formats(book_id, index_is_id=True) + if not available_formats: + available_formats = '' + available_formats = set([x.lower() for x in + available_formats.split(',')]) + input_formats = set([x.lower() for x in supported_input_formats()]) + input_formats = \ + sorted(available_formats.intersection(input_formats)) + if not input_formats: + raise NoSupportedInputFormats + output_formats = sorted(available_output_formats()) + output_formats.remove('oeb') + preferred_input_format = preferred_input_format if \ + preferred_input_format in input_formats else \ + sort_formats_by_preference(input_formats, + INPUT_FORMAT_PREFERENCES)[0] + preferred_output_format = preferred_output_format if \ + preferred_output_format in output_formats else \ + sort_formats_by_preference(output_formats, + OUTPUT_FORMAT_PREFERENCES)[0] + self.input_formats.addItems(list(map(QString, [x.upper() for x in + input_formats]))) + self.output_formats.addItems(list(map(QString, [x.upper() for x in + output_formats]))) + self.input_formats.setCurrentIndex(input_formats.index(preferred_input_format)) + self.output_formats.setCurrentIndex(output_formats.index(preferred_output_format)) + + def show_pane(self, index): + self.stack.setCurrentIndex(index.row()) + + def accept(self): + recs = GuiRecommendations() + for w in self._groups_model.widgets: + x = w.commit(save_defaults=False) + recs.update(x) + self.opf_path, self.cover_path = self.mw.opf_file, self.mw.cover_file + self._recommendations = recs + if self.db is not None: + recs['gui_preferred_input_format'] = self.input_format + save_specifics(self.db, self.book_id, recs) + ResizableDialog.accept(self) + + @property + def recommendations(self): + recs = [(k, v, OptionRecommendation.HIGH) for k, v in + self._recommendations.items()] + return cPickle.dumps(recs, -1) + + def show_group_help(self, index): + widget = self._groups_model.widgets[index.row()] + self.help.setPlainText(widget.HELP) + + +if __name__ == '__main__': + from calibre.library.database2 import LibraryDatabase2 + from calibre.gui2 import images_rc, Application + images_rc + a = Application([]) + db = LibraryDatabase2('/home/kovid/documents/library') + d = Config(None, db, 594) + d.show() + a.exec_() + diff --git a/src/calibre/gui2/convert/single.ui b/src/calibre/gui2/convert/single.ui new file mode 100644 index 0000000000..713f7471f3 --- /dev/null +++ b/src/calibre/gui2/convert/single.ui @@ -0,0 +1,200 @@ + + + Dialog + + + + 0 + 0 + 1024 + 700 + + + + Dialog + + + + :/images/convert.svg:/images/convert.svg + + + + + + + + &Input format: + + + input_formats + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &Output format: + + + output_formats + + + + + + + + + + + + + 1 + 0 + + + + true + + + + 48 + 48 + + + + 20 + + + true + + + + + + + + 4 + 10 + + + + QFrame::NoFrame + + + 0 + + + true + + + + + 0 + 0 + 810 + 492 + + + + + 0 + + + + + + 0 + 0 + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + 0 + 0 + + + + + 16777215 + 130 + + + + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 886320aedb..7f3487f035 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -285,7 +285,7 @@ class FontFamilyModel(QAbstractListModel): print 'WARNING: Could not load fonts' traceback.print_exc() self.families.sort() - self.families[:0] = ['None'] + self.families[:0] = [_('None')] def rowCount(self, *args): return len(self.families) diff --git a/src/calibre/utils/complete.py b/src/calibre/utils/complete.py index 8fc4fe85e2..7164e61635 100644 --- a/src/calibre/utils/complete.py +++ b/src/calibre/utils/complete.py @@ -53,7 +53,7 @@ def get_opts_from_parser(parser, prefix): for x in do_opt(o): yield x def send(ans): - pat = re.compile('([^0-9a-zA-Z_.])') + pat = re.compile('([^0-9a-zA-Z_./])') for x in sorted(set(ans)): x = pat.sub(lambda m : '\\'+m.group(1), x) if x.endswith('\\ '): diff --git a/todo b/todo index f9a85ff35e..b516e60e7f 100644 --- a/todo +++ b/todo @@ -3,14 +3,12 @@ * Refactor IPC code to use communication logic from multiprocessing -* Use multiprocessing for cpu_count instead of QThread - * Rationalize books table. Add a pubdate column, remove the uri column (and associated support in add_books) and convert series_index to a float. * Replace single application stuff with Listener from multiprocessing * Refactor add books to use a separate process named calibre-worker-add - Dont use the process for adding a single book - - Use a process pool for speed + - Use a process pool for speed or multiple process for stability (20 per process?) * Change mobi metadata setter to use author_sort setting from MOBI output plugin instead of mobi.py diff --git a/upload.py b/upload.py index a29e5b097c..2a6261e3bc 100644 --- a/upload.py +++ b/upload.py @@ -286,7 +286,7 @@ class gui(OptionlessCommand): with open('images.qrc', 'wb') as f: f.write(manifest) try: - check_call(['pyrcc4', '-o', images, 'images.qrc']) + check_call(['pyrcc4', '-py2', '-o', images, 'images.qrc']) except: import traceback traceback.print_exc() @@ -399,7 +399,7 @@ class update(OptionlessCommand): ' a version update.' def run(self): - for x in ['build', 'dist', 'docs'] + \ + for x in ['build', 'dist'] + \ glob.glob(os.path.join('src', 'calibre', 'plugins', '*')): if os.path.exists(x): if os.path.isdir(x):