diff --git a/src/calibre/debug.py b/src/calibre/debug.py index 51669b9143..962681a267 100644 --- a/src/calibre/debug.py +++ b/src/calibre/debug.py @@ -83,6 +83,23 @@ def debug_device_driver(): s = DeviceScanner() s.scan() print 'USB devices on system:', repr(s.devices) + if iswindows: + wmi = __import__('wmi', globals(), locals(), [], -1) + drives = [] + print 'Drives detected:' + print '\t', '(ID, Partitions, Drive letter)' + for drive in wmi.WMI().Win32_DiskDrive(): + if drive.Partitions == 0: + continue + try: + partition = drive.associators("Win32_DiskDriveToDiskPartition")[0] + logical_disk = partition.associators('Win32_LogicalDiskToPartition')[0] + prefix = logical_disk.DeviceID+os.sep + drives.append((str(drive.PNPDeviceID), drive.Index, prefix)) + except IndexError: + drives.append(str(drive.PNPDeviceID)) + for drive in drives: + print '\t', drive from calibre.devices import devices for dev in devices(): print 'Looking for', dev.__name__ diff --git a/src/calibre/ebooks/epub/__init__.py b/src/calibre/ebooks/epub/__init__.py index 4e305b000b..1bbb80cf13 100644 --- a/src/calibre/ebooks/epub/__init__.py +++ b/src/calibre/ebooks/epub/__init__.py @@ -142,6 +142,8 @@ to auto-generate a Table of Contents. help=_('XPath expression that specifies all tags that should be added to the Table of Contents at level one. If this is specified, it takes precedence over other forms of auto-detection.')) toc('level2_toc', ['--level2-toc'], default=None, help=_('XPath expression that specifies all tags that should be added to the Table of Contents at level two. Each entry is added under the previous level one entry.')) + toc('level3_toc', ['--level3-toc'], default=None, + help=_('XPath expression that specifies all tags that should be added to the Table of Contents at level three. Each entry is added under the previous level two entry.')) toc('from_ncx', ['--from-ncx'], default=None, help=_('Path to a .ncx file that contains the table of contents to use for this ebook. The NCX file should contain links relative to the directory it is placed in. See http://www.niso.org/workrooms/daisy/Z39-86-2005.html#NCX for an overview of the NCX format.')) toc('use_auto_toc', ['--use-auto-toc'], default=False, diff --git a/src/calibre/ebooks/epub/from_html.py b/src/calibre/ebooks/epub/from_html.py index 458fca152c..30191617a5 100644 --- a/src/calibre/ebooks/epub/from_html.py +++ b/src/calibre/ebooks/epub/from_html.py @@ -377,16 +377,13 @@ def convert(htmlfile, opts, notification=None, create_epub=True, mi = merge_metadata(htmlfile, opf, opts) opts.chapter = XPath(opts.chapter, namespaces={'re':'http://exslt.org/regular-expressions'}) - if opts.level1_toc: - opts.level1_toc = XPath(opts.level1_toc, - namespaces={'re':'http://exslt.org/regular-expressions'}) - else: - opts.level1_toc = None - if opts.level2_toc: - opts.level2_toc = XPath(opts.level2_toc, - namespaces={'re':'http://exslt.org/regular-expressions'}) - else: - opts.level2_toc = None + for x in (1, 2, 3): + attr = 'level%d_toc'%x + if getattr(opts, attr): + setattr(opts, attr, XPath(getattr(opts, attr), + namespaces={'re':'http://exslt.org/regular-expressions'})) + else: + setattr(opts, attr, None) with TemporaryDirectory(suffix='_html2epub', keep=opts.keep_intermediate) as tdir: if opts.keep_intermediate: diff --git a/src/calibre/ebooks/epub/split.py b/src/calibre/ebooks/epub/split.py index 6128af588e..9814c40df5 100644 --- a/src/calibre/ebooks/epub/split.py +++ b/src/calibre/ebooks/epub/split.py @@ -307,7 +307,11 @@ class Splitter(LoggingInterface): Search order is: * Heading tags *
tags + *
 tags
+            * 
tags *

tags + *
tags + *

  • tags We try to split in the "middle" of the file (as defined by tag counts. ''' @@ -327,6 +331,7 @@ class Splitter(LoggingInterface): '//hr', '//p', '//br', + '//li', ): elems = root.xpath(path, namespaces={'re':'http://exslt.org/regular-expressions'}) elem = pick_elem(elems) diff --git a/src/calibre/ebooks/html.py b/src/calibre/ebooks/html.py index 32601320d4..bb7081658e 100644 --- a/src/calibre/ebooks/html.py +++ b/src/calibre/ebooks/html.py @@ -558,31 +558,22 @@ class Processor(Parser): def detect_chapters(self): self.detected_chapters = self.opts.chapter(self.root) + chapter_mark = self.opts.chapter_mark + page_break_before = 'display: block; page-break-before: always' + page_break_after = 'display: block; page-break-after: always' for elem in self.detected_chapters: text = u' '.join([t.strip() for t in elem.xpath('descendant::text()')]) self.log_info('\tDetected chapter: %s', text[:50]) - if self.opts.chapter_mark != 'none': - hr = etree.Element('hr') - if elem.getprevious() is None: - elem.getparent()[:0] = [hr] - elif elem.getparent() is not None: - insert = None - for i, c in enumerate(elem.getparent()): - if c is elem: - insert = i - break - elem.getparent()[insert:insert] = [hr] - if self.opts.chapter_mark != 'rule': - hr.set('style', 'width:0pt;page-break-before:always') - if self.opts.chapter_mark == 'both': - hr2 = etree.Element('hr') - hr2.tail = u'\u00a0' - p = hr.getparent() - i = p.index(hr) - p[i:i] = [hr2] - - - + if chapter_mark == 'none': + continue + elif chapter_mark == 'rule': + mark = etree.Element('hr') + elif chapter_mark == 'pagebreak': + mark = etree.Element('div', style=page_break_after) + else: # chapter_mark == 'both': + mark = etree.Element('hr', style=page_break_before) + elem.addprevious(mark) + def save(self): style_path = os.path.splitext(os.path.basename(self.save_path()))[0] for i, sheet in enumerate([self.stylesheet, self.font_css, self.override_css]): @@ -647,6 +638,7 @@ class Processor(Parser): added[elem] = add_item(_href, frag, text, toc, type='chapter') add_item(_href, frag, 'Top', added[elem], type='chapter') if self.opts.level2_toc is not None: + added2 = {} level2 = list(self.opts.level2_toc(self.root)) for elem in level2: level1 = None @@ -657,7 +649,21 @@ class Processor(Parser): text, _href, frag = elem_to_link(elem, href, counter) counter += 1 if text: + added2[elem] = \ add_item(_href, frag, text, level1, type='chapter') + if self.opts.level3_toc is not None: + level3 = list(self.opts.level3_toc(self.root)) + for elem in level3: + level2 = None + for item in self.root.iterdescendants(): + if item in added2.keys(): + level2 = added2[item] + elif item == elem and level2 is not None: + text, _href, frag = elem_to_link(elem, href, counter) + counter += 1 + if text: + add_item(_href, frag, text, level2, type='chapter') + if len(toc) > 0: return diff --git a/src/calibre/ebooks/metadata/epub.py b/src/calibre/ebooks/metadata/epub.py index a8c2105c02..360869cc9c 100644 --- a/src/calibre/ebooks/metadata/epub.py +++ b/src/calibre/ebooks/metadata/epub.py @@ -106,9 +106,11 @@ class CoverRenderer(QObject): WIDTH = 600 HEIGHT = 800 - def __init__(self, url, size, loop): + def __init__(self, path): + if QApplication.instance() is None: + QApplication([]) QObject.__init__(self) - self.loop = loop + self.loop = QEventLoop() self.page = QWebPage() pal = self.page.palette() pal.setBrush(QPalette.Background, Qt.white) @@ -117,33 +119,43 @@ class CoverRenderer(QObject): self.page.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) self.page.mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) QObject.connect(self.page, SIGNAL('loadFinished(bool)'), self.render_html) - self.image_data = None + self._image_data = None self.rendered = False + url = QUrl.fromLocalFile(os.path.normpath(path)) self.page.mainFrame().load(url) def render_html(self, ok): - self.rendered = True try: if not ok: + self.rendered = True return - #size = self.page.mainFrame().contentsSize() - #width, height = fit_image(size.width(), size.height(), self.WIDTH, self.HEIGHT)[1:] - #self.page.setViewportSize(QSize(width, height)) image = QImage(self.page.viewportSize(), QImage.Format_ARGB32) image.setDotsPerMeterX(96*(100/2.54)) image.setDotsPerMeterY(96*(100/2.54)) painter = QPainter(image) self.page.mainFrame().render(painter) painter.end() - ba = QByteArray() buf = QBuffer(ba) buf.open(QBuffer.WriteOnly) image.save(buf, 'JPEG') - self.image_data = str(ba.data()) + self._image_data = str(ba.data()) finally: self.loop.exit(0) - + self.rendered = True + + def image_data(): + def fget(self): + if not self.rendered: + self.loop.exec_() + count = 0 + while count < 50 and not self.rendered: + time.sleep(0.1) + count += 1 + return self._image_data + return property(fget=fget) + image_data = image_data() + def get_cover(opf, opf_path, stream): spine = list(opf.spine_items()) @@ -155,20 +167,11 @@ def get_cover(opf, opf_path, stream): stream.seek(0) ZipFile(stream).extractall() opf_path = opf_path.replace('/', os.sep) - cpage = os.path.join(tdir, os.path.dirname(opf_path), *cpage.split('/')) + cpage = os.path.join(tdir, os.path.dirname(opf_path), cpage) if not os.path.exists(cpage): return - if QApplication.instance() is None: - QApplication([]) - url = QUrl.fromLocalFile(cpage) - loop = QEventLoop() - cr = CoverRenderer(url, os.stat(cpage).st_size, loop) - loop.exec_() - count = 0 - while count < 50 and not cr.rendered: - time.sleep(0.1) - count += 1 - return cr.image_data + cr = CoverRenderer(cpage) + return cr.image_data def get_metadata(stream, extract_cover=True): """ Return metadata as a :class:`MetaInformation` object """ diff --git a/src/calibre/ebooks/mobi/mobiml.py b/src/calibre/ebooks/mobi/mobiml.py index ed4465c8be..50d7b298b9 100644 --- a/src/calibre/ebooks/mobi/mobiml.py +++ b/src/calibre/ebooks/mobi/mobiml.py @@ -148,10 +148,6 @@ class MobiMLizer(object): if bstate.pbreak: etree.SubElement(body, MBP('pagebreak')) bstate.pbreak = False - if istate.ids: - for id in istate.ids: - etree.SubElement(body, XHTML('a'), attrib={'id': id}) - istate.ids.clear() bstate.istate = None bstate.anchor = None parent = bstate.nested[-1] if bstate.nested else bstate.body @@ -186,14 +182,17 @@ class MobiMLizer(object): wrapper.attrib['height'] = self.mobimlize_measure(vspace) para.attrib['width'] = self.mobimlize_measure(indent) elif tag == 'table' and vspace > 0: - body = bstate.body vspace = int(round(vspace / self.profile.fbase)) - index = max((0, len(body) - 1)) while vspace > 0: - body.insert(index, etree.Element(XHTML('br'))) + wrapper.addprevious(etree.Element(XHTML('br'))) vspace -= 1 if istate.halign != 'auto': para.attrib['align'] = istate.halign + if istate.ids: + last = bstate.body[-1] + for id in istate.ids: + last.addprevious(etree.Element(XHTML('a'), attrib={'id': id})) + istate.ids.clear() pstate = bstate.istate if tag in CONTENT_TAGS: bstate.inline = para diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index 51e184a420..e2bb6f6d5f 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal ' Read data from .mobi files ''' -import sys, struct, os, cStringIO, re, atexit, shutil, tempfile +import sys, struct, os, cStringIO, re try: from PIL import Image as PILImage @@ -14,7 +14,7 @@ except ImportError: from lxml import html, etree -from calibre import __appname__, entity_to_unicode +from calibre import entity_to_unicode from calibre.ebooks import DRMError from calibre.ebooks.chardet import ENCODING_PATS from calibre.ebooks.mobi import MobiError @@ -28,7 +28,7 @@ from calibre import sanitize_file_name class EXTHHeader(object): - def __init__(self, raw, codec): + def __init__(self, raw, codec, title): self.doctype = raw[:4] self.length, self.num_items = struct.unpack('>LL', raw[4:12]) raw = raw[12:] @@ -45,22 +45,16 @@ class EXTHHeader(object): elif id == 203: self.has_fake_cover = bool(struct.unpack('>L', content)[0]) elif id == 201: - self.cover_offset, = struct.unpack('>L', content) + co, = struct.unpack('>L', content) + if co < 1e7: + self.cover_offset = co elif id == 202: self.thumbnail_offset, = struct.unpack('>L', content) #else: # print 'unknown record', id, repr(content) - title = re.search(r'\0+([^\0]+)\0+', raw[pos:]) if title: - title = title.group(1).decode(codec, 'replace') - if len(title) > 2: - self.mi.title = title - else: - title = re.search(r'\0+([^\0]+)\0+', ''.join(reversed(raw[pos:]))) - if title: - self.mi.title = ''.join(reversed(title.group(1).decode(codec, 'replace'))) - - + self.mi.title = title + def process_metadata(self, id, content, codec): if id == 100: if self.mi.authors == [_('Unknown')]: @@ -119,6 +113,9 @@ class BookHeader(object): if self.compression_type == 'DH': self.huff_offset, self.huff_number = struct.unpack('>LL', raw[0x70:0x78]) + toff, tlen = struct.unpack('>II', raw[0x54:0x5c]) + tend = toff + tlen + self.title = raw[toff:tend] if tend < len(raw) else _('Unknown') langcode = struct.unpack('!L', raw[0x5C:0x60])[0] langid = langcode & 0xFF sublangid = (langcode >> 10) & 0xFF @@ -129,7 +126,7 @@ class BookHeader(object): 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 = EXTHHeader(raw[16+self.length:], self.codec, self.title) self.exth.mi.uid = self.unique_id self.exth.mi.language = self.language @@ -480,7 +477,7 @@ def get_metadata(stream): try: if hasattr(mr.book_header.exth, 'cover_offset'): cover_index = mr.book_header.first_image_index + mr.book_header.exth.cover_offset - data = mr.sections[cover_index][0] + data = mr.sections[int(cover_index)][0] else: data = mr.sections[mr.book_header.first_image_index][0] buf = cStringIO.StringIO(data) diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py index 3c5a39ebd2..49f4e076a4 100644 --- a/src/calibre/ebooks/mobi/writer.py +++ b/src/calibre/ebooks/mobi/writer.py @@ -23,6 +23,7 @@ from PIL import Image from calibre.ebooks.oeb.base import XML_NS, XHTML, XHTML_NS, OEB_DOCS, \ OEB_RASTER_IMAGES from calibre.ebooks.oeb.base import xpath, barename, namespace, prefixname +from calibre.ebooks.oeb.base import urlnormalize from calibre.ebooks.oeb.base import Logger, OEBBook from calibre.ebooks.oeb.profile import Context from calibre.ebooks.oeb.transforms.flatcss import CSSFlattener @@ -178,7 +179,7 @@ class Serializer(object): def serialize_href(self, href, base=None): hrefs = self.oeb.manifest.hrefs - path, frag = urldefrag(href) + path, frag = urldefrag(urlnormalize(href)) if path and base: path = base.abshref(path) if path and path not in hrefs: @@ -196,6 +197,7 @@ class Serializer(object): def serialize_body(self): buffer = self.buffer + self.anchor_offset = buffer.tell() buffer.write('') # CybookG3 'Start Reading' link if 'text' in self.oeb.guide: @@ -224,14 +226,17 @@ class Serializer(object): or namespace(elem.tag) not in nsrmap: return tag = prefixname(elem.tag, nsrmap) - for attr in ('name', 'id'): - if attr in elem.attrib: - href = '#'.join((item.href, elem.attrib[attr])) - self.id_offsets[href] = buffer.tell() - del elem.attrib[attr] - if tag == 'a' and not elem.attrib \ - and not len(elem) and not elem.text: + # Previous layers take care of @name + id = elem.attrib.pop('id', None) + if id is not None: + href = '#'.join((item.href, id)) + offset = self.anchor_offset or buffer.tell() + self.id_offsets[href] = offset + if self.anchor_offset is not None and \ + tag == 'a' and not elem.attrib and \ + not len(elem) and not elem.text: return + self.anchor_offset = buffer.tell() buffer.write('<') buffer.write(tag) if elem.attrib: @@ -256,10 +261,12 @@ class Serializer(object): if elem.text or len(elem) > 0: buffer.write('>') if elem.text: + self.anchor_offset = None self.serialize_text(elem.text) for child in elem: self.serialize_elem(child, item) if child.tail: + self.anchor_offset = None self.serialize_text(child.tail) buffer.write('' % tag) else: diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 61a41443bc..a1c7703122 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -23,6 +23,8 @@ from calibre import LoggingInterface from calibre.translations.dynamic import translate from calibre.startup import get_lang from calibre.ebooks.oeb.entitydefs import ENTITYDEFS +from calibre.ebooks.metadata.epub import CoverRenderer +from calibre.ptempfile import TemporaryDirectory XML_NS = 'http://www.w3.org/XML/1998/namespace' XHTML_NS = 'http://www.w3.org/1999/xhtml' @@ -351,9 +353,13 @@ class Manifest(object): try: data = etree.fromstring(data) except etree.XMLSyntaxError: + # TODO: Factor out HTML->XML coercion self.oeb.logger.warn('Parsing file %r as HTML' % self.href) data = html.fromstring(data) data.attrib.pop('xmlns', None) + for elem in data.iter(tag=etree.Comment): + if elem.text: + elem.text = elem.text.strip('-') data = etree.tostring(data, encoding=unicode) data = etree.fromstring(data) # Force into the XHTML namespace @@ -447,7 +453,7 @@ class Manifest(object): return cmp(skey, okey) def relhref(self, href): - if '/' not in self.href: + if '/' not in self.href or ':' in href: return href base = os.path.dirname(self.href).split('/') target, frag = urldefrag(href) @@ -463,7 +469,7 @@ class Manifest(object): return relhref def abshref(self, href): - if '/' not in self.href: + if '/' not in self.href or ':' in href: return href dirname = os.path.dirname(self.href) href = os.path.join(dirname, href) @@ -546,7 +552,7 @@ class Manifest(object): elif media_type in OEB_STYLES: media_type = CSS_MIME attrib = {'id': item.id, 'href': item.href, - 'media-type': item.media_type} + 'media-type': media_type} if item.fallback: attrib['fallback'] = item.fallback element(elem, OPF('item'), attrib=attrib) @@ -796,6 +802,9 @@ class TOC(object): class OEBBook(object): + COVER_SVG_XP = XPath('h:body//svg:svg[position() = 1]') + COVER_OBJECT_XP = XPath('h:body//h:object[@data][position() = 1]') + def __init__(self, opfpath=None, container=None, encoding=None, logger=FauxLogger()): if opfpath and not container: @@ -928,7 +937,7 @@ class OEBBook(object): spine.add(item, elem.get('linear')) extras = [] for item in self.manifest.values(): - if item.media_type == XHTML_MIME \ + if item.media_type in OEB_DOCS \ and item not in spine: extras.append(item) extras.sort() @@ -971,7 +980,7 @@ class OEBBook(object): ncx = item.data self.manifest.remove(item) title = xpath(ncx, 'ncx:docTitle/ncx:text/text()') - title = title[0].strip() if title else unicode(self.metadata.title) + title = title[0].strip() if title else unicode(self.metadata.title[0]) self.toc = toc = TOC(title) navmaps = xpath(ncx, 'ncx:navMap') for navmap in navmaps: @@ -1051,42 +1060,59 @@ class OEBBook(object): if self._toc_from_html(opf): return self._toc_from_spine(opf) - def _ensure_cover_image(self): - cover = None + def _cover_from_html(self, hcover): + with TemporaryDirectory('_html_cover') as tdir: + writer = DirWriter() + writer.dump(self, tdir) + path = os.path.join(tdir, hcover.href) + renderer = CoverRenderer(path) + data = renderer.image_data + id, href = self.manifest.generate('cover', 'cover.jpeg') + item = self.manifest.add(id, href, JPEG_MIME, data=data) + return item + + def _locate_cover_image(self): + if self.metadata.cover: + id = str(self.metadata.cover[0]) + item = self.manifest.ids.get(id, None) + if item is not None: + return item hcover = self.spine[0] if 'cover' in self.guide: href = self.guide['cover'].href item = self.manifest.hrefs[href] media_type = item.media_type - if media_type in OEB_RASTER_IMAGES: - cover = item + if media_type in OEB_IMAGES: + return item elif media_type in OEB_DOCS: hcover = item html = hcover.data - if cover is not None: - pass - elif self.metadata.cover: - id = str(self.metadata.cover[0]) - cover = self.manifest.ids[id] - elif MS_COVER_TYPE in self.guide: + if MS_COVER_TYPE in self.guide: href = self.guide[MS_COVER_TYPE].href - cover = self.manifest.hrefs[href] - elif xpath(html, '//h:img[position()=1]'): - img = xpath(html, '//h:img[position()=1]')[0] - href = hcover.abshref(img.get('src')) - cover = self.manifest.hrefs[href] - elif xpath(html, '//h:object[position()=1]'): - object = xpath(html, '//h:object[position()=1]')[0] - href = hcover.abshref(object.get('data')) - cover = self.manifest.hrefs[href] - elif xpath(html, '//svg:svg[position()=1]'): - svg = copy.deepcopy(xpath(html, '//svg:svg[position()=1]')[0]) + item = self.manifest.hrefs.get(href, None) + if item is not None and item.media_type in OEB_IMAGES: + return item + if self.COVER_SVG_XP(html): + svg = copy.deepcopy(self.COVER_SVG_XP(html)[0]) href = os.path.splitext(hcover.href)[0] + '.svg' id, href = self.manifest.generate(hcover.id, href) - cover = self.manifest.add(id, href, SVG_MIME, data=svg) - if cover and not self.metadata.cover: - self.metadata.add('cover', cover.id) - + item = self.manifest.add(id, href, SVG_MIME, data=svg) + return item + if self.COVER_OBJECT_XP(html): + object = self.COVER_OBJECT_XP(html)[0] + href = hcover.abshref(object.get('data')) + item = self.manifest.hrefs.get(href, None) + if item is not None and item.media_type in OEB_IMAGES: + return item + return self._cover_from_html(hcover) + + def _ensure_cover_image(self): + cover = self._locate_cover_image() + if self.metadata.cover: + self.metadata.cover[0].value = cover.id + return + self.metadata.add('cover', cover.id) + def _all_from_opf(self, opf): self._metadata_from_opf(opf) self._manifest_from_opf(opf) diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 29c6c5b2b4..03a1fade10 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -265,6 +265,8 @@ class Stylizer(object): class Style(object): + UNIT_RE = re.compile(r'^(-*[0-9]*[.]?[0-9]*)\s*(%|em|px|mm|cm|in|pt|pc)$') + def __init__(self, element, stylizer): self._element = element self._profile = stylizer.profile @@ -319,13 +321,11 @@ class Style(object): if isinstance(value, (int, long, float)): return value try: - if float(value) == 0: - return 0.0 + return float(value) * 72.0 / self._profile.dpi except: pass result = value - m = re.search( - r"^(-*[0-9]*\.?[0-9]*)\s*(%|em|px|mm|cm|in|pt|pc)$", value) + m = self.UNIT_RE.match(value) if m is not None and m.group(1): value = float(m.group(1)) unit = m.group(2) diff --git a/src/calibre/ebooks/oeb/transforms/flatcss.py b/src/calibre/ebooks/oeb/transforms/flatcss.py index 28b72b04f3..01afcb08e2 100644 --- a/src/calibre/ebooks/oeb/transforms/flatcss.py +++ b/src/calibre/ebooks/oeb/transforms/flatcss.py @@ -23,6 +23,12 @@ from calibre.ebooks.oeb.stylizer import Stylizer COLLAPSE = re.compile(r'[ \t\r\n\v]+') STRIPNUM = re.compile(r'[-0-9]+$') +def asfloat(value, default): + if not isinstance(value, (int, long, float)): + value = default + return float(value) + + class KeyMapper(object): def __init__(self, sbase, dbase, dkey): self.sbase = float(sbase) @@ -179,12 +185,13 @@ class CSSFlattener(object): if cssdict: if self.lineh and self.fbase and tag != 'body': self.clean_edges(cssdict, style, psize) - margin = style['margin-left'] - left += margin if isinstance(margin, float) else 0 - if (left + style['text-indent']) < 0: - percent = (margin - style['text-indent']) / style['width'] + margin = asfloat(style['margin-left'], 0) + indent = asfloat(style['text-indent'], 0) + left += margin + if (left + indent) < 0: + percent = (margin - indent) / style['width'] cssdict['margin-left'] = "%d%%" % (percent * 100) - left -= style['text-indent'] + left -= indent if 'display' in cssdict and cssdict['display'] == 'in-line': cssdict['display'] = 'inline' if self.unfloat and 'float' in cssdict \ diff --git a/src/calibre/ebooks/oeb/transforms/rasterize.py b/src/calibre/ebooks/oeb/transforms/rasterize.py index 97d73d3dcb..12a2812898 100644 --- a/src/calibre/ebooks/oeb/transforms/rasterize.py +++ b/src/calibre/ebooks/oeb/transforms/rasterize.py @@ -23,6 +23,7 @@ from PyQt4.QtGui import QApplication from calibre.ebooks.oeb.base import XHTML_NS, XHTML, SVG_NS, SVG, XLINK from calibre.ebooks.oeb.base import SVG_MIME, PNG_MIME, JPEG_MIME from calibre.ebooks.oeb.base import xml2str, xpath, namespace, barename +from calibre.ebooks.oeb.base import urlnormalize from calibre.ebooks.oeb.stylizer import Stylizer IMAGE_TAGS = set([XHTML('img'), XHTML('object')]) @@ -78,7 +79,7 @@ class SVGRasterizer(object): svg = item.data hrefs = self.oeb.manifest.hrefs for elem in xpath(svg, '//svg:*[@xl:href]'): - href = elem.attrib[XLINK('href')] + href = urlnormalize(elem.attrib[XLINK('href')]) path, frag = urldefrag(href) if not path: continue @@ -100,15 +101,15 @@ class SVGRasterizer(object): def rasterize_item(self, item, stylizer): html = item.data hrefs = self.oeb.manifest.hrefs - for elem in xpath(html, '//h:img'): - src = elem.get('src', None) - image = hrefs.get(item.abshref(src), None) if src else None + for elem in xpath(html, '//h:img[@src]'): + src = urlnormalize(elem.attrib['src']) + image = hrefs.get(item.abshref(src), None) if image and image.media_type == SVG_MIME: style = stylizer.style(elem) self.rasterize_external(elem, style, item, image) - for elem in xpath(html, '//h:object[@type="%s"]' % SVG_MIME): - data = elem.get('data', None) - image = hrefs.get(item.abshref(data), None) if data else None + for elem in xpath(html, '//h:object[@type="%s" and @data]' % SVG_MIME): + data = urlnormalize(elem.attrib['data']) + image = hrefs.get(item.abshref(data), None) if image and image.media_type == SVG_MIME: style = stylizer.style(elem) self.rasterize_external(elem, style, item, image) diff --git a/src/calibre/ebooks/oeb/transforms/trimmanifest.py b/src/calibre/ebooks/oeb/transforms/trimmanifest.py index bc95b43343..643952c03d 100644 --- a/src/calibre/ebooks/oeb/transforms/trimmanifest.py +++ b/src/calibre/ebooks/oeb/transforms/trimmanifest.py @@ -54,7 +54,7 @@ class ManifestTrimmer(object): new.add(found) elif item.media_type == CSS_MIME: def replacer(uri): - absuri = item.abshref(uri) + absuri = item.abshref(urlnormalize(uri)) if absuri in oeb.manifest.hrefs: found = oeb.manifest.hrefs[href] if found not in used: diff --git a/src/calibre/gui2/dialogs/epub.py b/src/calibre/gui2/dialogs/epub.py index 161534a103..614f18e5b4 100644 --- a/src/calibre/gui2/dialogs/epub.py +++ b/src/calibre/gui2/dialogs/epub.py @@ -252,7 +252,7 @@ class Config(ResizableDialog, Ui_Dialog): self.source_format = d.format() def accept(self): - for opt in ('chapter', 'level1_toc', 'level2_toc'): + for opt in ('chapter', 'level1_toc', 'level2_toc', 'level3_toc'): text = unicode(getattr(self, 'opt_'+opt).text()) if text: try: diff --git a/src/calibre/gui2/dialogs/epub.ui b/src/calibre/gui2/dialogs/epub.ui index cfa136e85f..a346bed4e8 100644 --- a/src/calibre/gui2/dialogs/epub.ui +++ b/src/calibre/gui2/dialogs/epub.ui @@ -93,7 +93,7 @@ - 1 + 0 @@ -105,36 +105,6 @@ Book Cover - - - - - - - - - :/images/book.svg - - - true - - - Qt::AlignCenter - - - - - - - - - Use cover from &source file - - - true - - - @@ -186,6 +156,36 @@ + + + + Use cover from &source file + + + true + + + + + + + + + + + + :/images/book.svg + + + true + + + Qt::AlignCenter + + + + + opt_prefer_metadata_cover @@ -777,10 +777,10 @@ p, li { white-space: pre-wrap; } - + - + &Title for generated TOC @@ -790,6 +790,19 @@ p, li { white-space: pre-wrap; } + + + + + + + Level &3 TOC + + + opt_level3_toc + + + diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui index b7538dabf3..c693cf3eea 100644 --- a/src/calibre/gui2/dialogs/metadata_single.ui +++ b/src/calibre/gui2/dialogs/metadata_single.ui @@ -638,6 +638,31 @@ + title + swap_button + authors + author_sort + auto_author_sort + rating + publisher + tags + series + tag_editor_button + remove_series_button + series_index + isbn + comments + fetch_metadata_button + fetch_cover_button + password_button + formats + add_format_button + remove_format_button + button_set_cover + cover_path + cover_button + reset_cover + scrollArea button_box diff --git a/src/calibre/gui2/dialogs/scheduler.py b/src/calibre/gui2/dialogs/scheduler.py index 86b3190c04..d15adebb8b 100644 --- a/src/calibre/gui2/dialogs/scheduler.py +++ b/src/calibre/gui2/dialogs/scheduler.py @@ -8,7 +8,7 @@ Scheduler for automated recipe downloads ''' import sys, copy, time -from datetime import datetime, timedelta +from datetime import datetime, timedelta, date from PyQt4.Qt import QDialog, QApplication, QLineEdit, QPalette, SIGNAL, QBrush, \ QColor, QAbstractListModel, Qt, QVariant, QFont, QIcon, \ QFile, QObject, QTimer, QMutex, QMenu, QAction, QTime @@ -289,7 +289,8 @@ class SchedulerDialog(QDialog, Ui_Dialog): recipe.last_downloaded = datetime.fromordinal(1) recipes.append(recipe) if recipe.needs_subscription and not config['recipe_account_info_%s'%recipe.id]: - error_dialog(self, _('Must set account information'), _('This recipe requires a username and password')).exec_() + error_dialog(self, _('Must set account information'), + _('This recipe requires a username and password')).exec_() self.schedule.setCheckState(Qt.Unchecked) return if self.interval_button.isChecked(): @@ -350,9 +351,11 @@ class SchedulerDialog(QDialog, Ui_Dialog): self.username.blockSignals(False) self.password.blockSignals(False) d = datetime.utcnow() - recipe.last_downloaded - ld = '%.2f'%(d.days + d.seconds/(24.*3600)) + def hm(x): return (x-x%3600)//3600, (x%3600 - (x%3600)%60)//60 + hours, minutes = hm(d.seconds) + tm = _('%d days, %d hours and %d minutes ago')%(d.days, hours, minutes) if d < timedelta(days=366): - self.last_downloaded.setText(_('Last downloaded: %s days ago')%ld) + self.last_downloaded.setText(_('Last downloaded')+': '+tm) else: self.last_downloaded.setText(_('Last downloaded: never')) @@ -431,7 +434,7 @@ class Scheduler(QObject): day_matches = day > 6 or day == now.tm_wday tnow = now.tm_hour*60 + now.tm_min matches = day_matches and (hour*60+minute) < tnow - if matches and delta >= timedelta(days=1): + if matches and nowt.toordinal() < date.today().toordinal(): needs_downloading.add(recipe) self.debug('Needs downloading:', needs_downloading) diff --git a/src/calibre/gui2/dialogs/scheduler.ui b/src/calibre/gui2/dialogs/scheduler.ui index 2cf22f7191..b10e777d7d 100644 --- a/src/calibre/gui2/dialogs/scheduler.ui +++ b/src/calibre/gui2/dialogs/scheduler.ui @@ -5,7 +5,7 @@ 0 0 - 726 + 738 575 @@ -194,6 +194,9 @@ + + true + diff --git a/src/calibre/gui2/lrf_renderer/bookview.py b/src/calibre/gui2/lrf_renderer/bookview.py index 8d81892200..a14361c7ef 100644 --- a/src/calibre/gui2/lrf_renderer/bookview.py +++ b/src/calibre/gui2/lrf_renderer/bookview.py @@ -20,4 +20,5 @@ class BookView(QGraphicsView): def resize_for(self, width, height): self.preferred_size = QSize(width, height) + \ No newline at end of file diff --git a/src/calibre/gui2/lrf_renderer/main.py b/src/calibre/gui2/lrf_renderer/main.py index f080022415..c223f51d2e 100644 --- a/src/calibre/gui2/lrf_renderer/main.py +++ b/src/calibre/gui2/lrf_renderer/main.py @@ -80,8 +80,8 @@ class Main(MainWindow, Ui_MainWindow): QObject.connect(self.search, SIGNAL('search(PyQt_PyObject, PyQt_PyObject)'), self.find) - self.action_next_page.setShortcuts(QKeySequence.MoveToNextPage) - self.action_previous_page.setShortcuts(QKeySequence.MoveToPreviousPage) + self.action_next_page.setShortcuts([QKeySequence.MoveToNextPage, QKeySequence(Qt.Key_Space)]) + self.action_previous_page.setShortcuts([QKeySequence.MoveToPreviousPage, QKeySequence(Qt.Key_Backspace)]) self.action_next_match.setShortcuts(QKeySequence.FindNext) self.addAction(self.action_next_match) QObject.connect(self.action_next_page, SIGNAL('triggered(bool)'), self.next) @@ -191,6 +191,7 @@ class Main(MainWindow, Ui_MainWindow): self.spin_box.setSuffix(' of %d'%(self.document.num_of_pages,)) self.spin_box.updateGeometry() self.stack.setCurrentIndex(0) + self.graphics_view.setFocus(Qt.OtherFocusReason) elif self.renderer.exception is not None: exception = self.renderer.exception print >>sys.stderr, 'Error rendering document' diff --git a/src/calibre/library/server.py b/src/calibre/library/server.py index 383612805e..ba81151517 100644 --- a/src/calibre/library/server.py +++ b/src/calibre/library/server.py @@ -312,7 +312,8 @@ class LibraryServer(object): book, books = MarkupTemplate(self.BOOK), [] for record in items[start:start+num]: - authors = '|'.join([i.replace('|', ',') for i in record[2].split(',')]) + aus = record[2] if record[2] else _('Unknown') + authors = '|'.join([i.replace('|', ',') for i in aus.split(',')]) books.append(book.generate(r=record, authors=authors).render('xml').decode('utf-8')) updated = self.db.last_modified() diff --git a/src/calibre/manual/news.rst b/src/calibre/manual/news.rst index 025f38ec22..3c682ad648 100644 --- a/src/calibre/manual/news.rst +++ b/src/calibre/manual/news.rst @@ -299,10 +299,10 @@ To learn more about writing advanced recipes using some of the facilities, avail :ref:`API Documentation ` Documentation of the ``BasicNewsRecipe`` class and all its important methods and fields. - `BasicNewsRecipe `_ + `BasicNewsRecipe `_ The source code of ``BasicNewsRecipe`` - `Built-in recipes `_ + `Built-in recipes `_ The source code for the built-in recipes that come with |app| Migrating old style profiles to recipes diff --git a/src/calibre/web/feeds/recipes/recipe_lrb.py b/src/calibre/web/feeds/recipes/recipe_lrb.py index a31e4a32e7..459e3580ce 100644 --- a/src/calibre/web/feeds/recipes/recipe_lrb.py +++ b/src/calibre/web/feeds/recipes/recipe_lrb.py @@ -32,3 +32,8 @@ class LondonReviewOfBooks(BasicNewsRecipe): def print_version(self, url): main, split, rest = url.rpartition('/') return main + '/print/' + rest + + def postprocess_html(self, soup, first_fetch): + for t in soup.findAll(['table', 'tr', 'td']): + t.name = 'div' + return soup