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('%s>' % 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