diff --git a/src/calibre/ebooks/mobi/mobiml.py b/src/calibre/ebooks/mobi/mobiml.py index 0e7dcdee1b..bd0d3a8a57 100644 --- a/src/calibre/ebooks/mobi/mobiml.py +++ b/src/calibre/ebooks/mobi/mobiml.py @@ -104,7 +104,7 @@ class MobiMLizer(object): return ptsize fbase = self.profile.fbase if ptsize < fbase: - return "%dpt" % int(round(ptsize * 2)) + return "%dpt" % int(round(ptsize)) return "%dem" % int(round(ptsize / fbase)) def preize_text(self, text): @@ -284,9 +284,9 @@ class MobiMLizer(object): else: istate.family = 'serif' valign = style['vertical-align'] - if valign in ('super', 'sup') and asfloat(valign) > 0: + if valign in ('super', 'sup') or asfloat(valign) > 0: istate.valign = 'super' - elif valign == 'sub' and asfloat(valign) < 0: + elif valign == 'sub' or asfloat(valign) < 0: istate.valign = 'sub' else: istate.valign = 'baseline' @@ -300,6 +300,15 @@ class MobiMLizer(object): if tag == 'img' and 'src' in elem.attrib: istate.attrib['src'] = elem.attrib['src'] istate.attrib['align'] = 'baseline' + for prop in ('width', 'height'): + if style[prop] != 'auto': + value = style[prop] + if value == getattr(self.profile, prop): + result = '100%' + else: + ems = int(round(value / self.profile.fbase)) + result = "%dem" % ems + istate.attrib[prop] = result elif tag == 'hr' and asfloat(style['width']) > 0: prop = style['width'] / self.profile.width istate.attrib['width'] = "%d%%" % int(round(prop * 100)) diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py index 0bb1bfc3f4..a5b34f41dc 100644 --- a/src/calibre/ebooks/mobi/writer.py +++ b/src/calibre/ebooks/mobi/writer.py @@ -225,11 +225,22 @@ class MobiWriter(object): self._oeb = oeb self._stream = stream self._records = [None] + self._remove_html_cover() self._generate_content() self._generate_record0() self._write_header() self._write_content() + def _remove_html_cover(self): + oeb = self._oeb + if not oeb.metadata.cover \ + or 'cover' not in oeb.guide: + return + href = oeb.guide['cover'].href + del oeb.guide['cover'] + item = oeb.manifest.hrefs[href] + oeb.manifest.remove(item) + def _generate_content(self): self._map_image_names() self._generate_text() @@ -391,7 +402,7 @@ class MobiWriter(object): nrecs += 1 if oeb.metadata.cover: id = str(oeb.metadata.cover[0]) - item = oeb.manifest[id] + item = oeb.manifest.ids[id] href = item.href index = self._images[href] - 1 exth.write(pack('>III', 0xc9, 0x0c, index)) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 847aa412ec..0ce0aef871 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -17,6 +17,7 @@ import logging import re import htmlentitydefs import uuid +import copy from lxml import etree from calibre import LoggingInterface @@ -32,10 +33,11 @@ XSI_NS = 'http://www.w3.org/2001/XMLSchema-instance' DCTERMS_NS = 'http://purl.org/dc/terms/' NCX_NS = 'http://www.daisy.org/z3986/2005/ncx/' SVG_NS = 'http://www.w3.org/2000/svg' +XLINK_NS = 'http://www.w3.org/1999/xlink' XPNSMAP = {'h': XHTML_NS, 'o1': OPF1_NS, 'o2': OPF2_NS, 'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS, 'xsi': XSI_NS, 'dt': DCTERMS_NS, 'ncx': NCX_NS, - 'svg': SVG_NS} + 'svg': SVG_NS, 'xl': XLINK_NS} def XML(name): return '{%s}%s' % (XML_NS, name) def XHTML(name): return '{%s}%s' % (XHTML_NS, name) @@ -43,6 +45,7 @@ def OPF(name): return '{%s}%s' % (OPF2_NS, name) def DC(name): return '{%s}%s' % (DC11_NS, name) def NCX(name): return '{%s}%s' % (NCX_NS, name) def SVG(name): return '{%s}%s' % (SVG_NS, name) +def XLINK(name): return '{%s}%s' % (XLINK_NS, name) EPUB_MIME = 'application/epub+zip' XHTML_MIME = 'application/xhtml+xml' @@ -246,10 +249,10 @@ class Metadata(object): self.oeb = oeb self.items = defaultdict(list) - def add(self, term, value, attrib={}, **kwargs): + def add(self, term, value, attrib={}, index=-1, **kwargs): item = self.Item(term, value, attrib, **kwargs) items = self.items[barename(item.term)] - items.append(item) + items.insert(index, item) return item def iterkeys(self): @@ -323,8 +326,7 @@ class Manifest(object): data = self._loader(self.href) if self.media_type in OEB_DOCS: data = self._force_xhtml(data) - elif self.media_type[-4:] in ('+xml', '/xml') \ - and self.media_type != SVG_MIME: + elif self.media_type[-4:] in ('+xml', '/xml'): data = etree.fromstring(data, parser=XML_PARSER) self._data = data return data @@ -341,6 +343,9 @@ class Manifest(object): return xml2str(data) return str(data) + def __eq__(self, other): + return id(self) == id(other) + def __cmp__(self, other): result = cmp(self.spine_position, other.spine_position) if result != 0: @@ -558,9 +563,12 @@ class Guide(object): for type, ref in self.refs.items(): yield type, ref - def __getitem__(self, index): - return self.refs[index] + def __getitem__(self, key): + return self.refs[key] + def __delitem__(self, key): + del self.refs[key] + def __contains__(self, key): return key in self.refs @@ -891,20 +899,27 @@ class OEBBook(object): def _ensure_cover_image(self): cover = None + spine0 = self.spine[0] + html = spine0.data if self.metadata.cover: id = str(self.metadata.cover[0]) - cover = self.manifest[id] + cover = self.manifest.ids[id] elif MS_COVER_TYPE in self.guide: href = self.guide[MS_COVER_TYPE].href cover = self.manifest.hrefs[href] - elif 'cover' in self.guide: - href = self.guide['cover'].href + elif xpath(html, '//h:img[position()=1]'): + img = xpath(html, '//h:img[position()=1]')[0] + href = img.get('src') cover = self.manifest.hrefs[href] - else: - html = self.spine[0].data - imgs = xpath(html, '//h:img[position()=1]') - href = imgs[0].get('src') if imgs else None - cover = self.manifest.hrefs[href] if href else None + elif xpath(html, '//h:object[position()=1]'): + object = xpath(html, '//h:object[position()=1]')[0] + href = 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]) + href = os.path.splitext(spine0.href)[0] + '.svg' + id, href = self.manifest.generate(spine0.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) diff --git a/src/calibre/ebooks/oeb/html.css b/src/calibre/ebooks/oeb/html.css index 4cf8cc5fce..63d57a3e29 100644 --- a/src/calibre/ebooks/oeb/html.css +++ b/src/calibre/ebooks/oeb/html.css @@ -35,7 +35,8 @@ * * ***** END LICENSE BLOCK ***** */ -@namespace url(http://www.w3.org/1999/xhtml); /* set default namespace to HTML */ +@namespace url(http://www.w3.org/1999/xhtml); +@namespace svg url(http://www.w3.org/2000/svg); /* blocks */ @@ -399,8 +400,8 @@ br { display: block; } -/* Images and embedded object size defaults */ -img, object { +/* Images, embedded object, and SVG size defaults */ +img, object, svg|svg { width: auto; height: auto; } diff --git a/src/calibre/ebooks/oeb/profile.py b/src/calibre/ebooks/oeb/profile.py index d5e3f2ff10..3fdec6c8a5 100644 --- a/src/calibre/ebooks/oeb/profile.py +++ b/src/calibre/ebooks/oeb/profile.py @@ -41,8 +41,8 @@ PROFILES = { # Not really, but let's pretend 'MobiDesktop': - Profile(width=280, height=300, dpi=96, fbase=12, - fsizes=[9, 10, 11, 12, 14, 17, 20, 24]), + Profile(width=280, height=300, dpi=96, fbase=18, + fsizes=[14, 14, 16, 18, 20, 22, 22, 24]), # No clue on usable screen size and DPI 'CybookG3': diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 4dad03d47e..43db3afc42 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -268,6 +268,7 @@ class Style(object): self._style = {} self._fontSize = None self._width = None + self._height = None stylizer._styles[element] = self def _update_cssdict(self, cssdict): @@ -390,17 +391,38 @@ class Style(object): base = styles[self._element.getparent()].width else: base = self._profile.width - if 'width' in self._style: + if 'width' is self._element.attrib: + width = self._element.attrib['width'] + elif 'width' in self._style: width = self._style['width'] - if width == 'auto': - result = base - else: - result = self._unit_convert(width, base=base) else: result = base + if not result: + result = self._unit_convert(width, base=base) self._width = result return self._width + @property + def height(self): + if self._height is None: + result = None + base = None + if self._has_parent(): + styles = self._stylizer._styles + base = styles[self._element.getparent()].height + else: + base = self._profile.height + if 'height' is self._element.attrib: + height = self._element.attrib['height'] + elif 'height' in self._style: + height = self._style['height'] + else: + result = base + if not result: + result = self._unit_convert(height, base=base) + self._height = result + return self._height + def __str__(self): items = self._style.items() items.sort() diff --git a/src/calibre/ebooks/oeb/transforms/rasterize.py b/src/calibre/ebooks/oeb/transforms/rasterize.py index 80373f8ffa..90d7a07579 100644 --- a/src/calibre/ebooks/oeb/transforms/rasterize.py +++ b/src/calibre/ebooks/oeb/transforms/rasterize.py @@ -8,17 +8,21 @@ __copyright__ = '2008, Marshall T. Vandegrift ' import sys import os +from urlparse import urldefrag +import base64 from lxml import etree from PyQt4.QtCore import Qt from PyQt4.QtCore import QByteArray from PyQt4.QtCore import QBuffer from PyQt4.QtCore import QIODevice +from PyQt4.QtGui import QColor from PyQt4.QtGui import QImage from PyQt4.QtGui import QPainter from PyQt4.QtSvg import QSvgRenderer from PyQt4.QtGui import QApplication -from calibre.ebooks.oeb.base import XHTML, SVG, SVG_NS, SVG_MIME -from calibre.ebooks.oeb.base import namespace, barename +from calibre.ebooks.oeb.base import XHTML_NS, XHTML, SVG_NS, SVG, XLINK +from calibre.ebooks.oeb.base import SVG_MIME, PNG_MIME +from calibre.ebooks.oeb.base import xml2str, xpath, namespace, barename from calibre.ebooks.oeb.stylizer import Stylizer IMAGE_TAGS = set([XHTML('img'), XHTML('object')]) @@ -32,8 +36,55 @@ class SVGRasterizer(object): self.oeb = oeb self.profile = context.dest self.images = {} + self.dataize_manifest() self.rasterize_spine() + self.rasterize_cover() + def rasterize_svg(self, elem, width=0, height=0): + data = QByteArray(xml2str(elem)) + svg = QSvgRenderer(data) + size = svg.defaultSize() + if size.width() == 100 and size.height() == 100 \ + and 'viewBox' in elem.attrib: + box = [float(x) for x in elem.attrib['viewBox'].split()] + size.setWidth(box[2] - box[0]) + size.setHeight(box[3] - box[1]) + if width or height: + size.scale(width, height, Qt.KeepAspectRatio) + image = QImage(size, QImage.Format_ARGB32_Premultiplied) + image.fill(QColor("white").rgb()) + painter = QPainter(image) + svg.render(painter) + painter.end() + array = QByteArray() + buffer = QBuffer(array) + buffer.open(QIODevice.WriteOnly) + image.save(buffer, 'PNG') + return str(array) + + def dataize_manifest(self): + for item in self.oeb.manifest.values(): + if item.media_type == SVG_MIME: + self.dataize_svg(item) + + def dataize_svg(self, item, svg=None): + if svg is None: + svg = item.data + hrefs = self.oeb.manifest.hrefs + for elem in xpath(svg, '//svg:*[@xl:href]'): + href = elem.attrib[XLINK('href')] + path, frag = urldefrag(href) + if not path: + continue + abshref = item.abshref(path) + if abshref not in hrefs: + continue + linkee = hrefs[abshref] + data = base64.encodestring(str(linkee)) + data = "data:%s;base64,%s" % (linkee.media_type, data) + elem.attrib[XLINK('href')] = data + return svg + def rasterize_spine(self): for item in self.oeb.spine: html = item.data @@ -44,7 +95,7 @@ class SVGRasterizer(object): if not isinstance(elem.tag, basestring): return style = stylizer.style(elem) if namespace(elem.tag) == SVG_NS: - return self.rasterize_inline(elem, style) + return self.rasterize_inline(elem, style, item) if elem.tag in IMAGE_TAGS: manifest = self.oeb.manifest src = elem.get('src', None) or elem.get('data', None) @@ -54,27 +105,46 @@ class SVGRasterizer(object): for child in elem: self.rasterize_elem(child, item, stylizer) - def rasterize_inline(self, elem, style): - pass - - def rasterize_external(self, elem, style, item, svgitem): - data = QByteArray(svgitem.data) - svg = QSvgRenderer(data) - size = svg.defaultSize() - height = style['height'] - if height == 'auto': - height = self.profile.height + def rasterize_inline(self, elem, style, item): width = style['width'] if width == 'auto': width = self.profile.width + height = style['height'] + if height == 'auto': + height = self.profile.height width = (width / 72) * self.profile.dpi height = (height / 72) * self.profile.dpi + elem = self.dataize_svg(item, elem) + data = self.rasterize_svg(elem, width, height) + manifest = self.oeb.manifest + href = os.path.splitext(item.href)[0] + '.png' + id, href = manifest.generate(item.id, href) + manifest.add(id, href, PNG_MIME, data=data) + img = etree.Element(XHTML('img'), src=item.relhref(href)) + elem.getparent().replace(elem, img) + for prop in ('width', 'height'): + if prop in elem.attrib: + img.attrib[prop] = elem.attrib[prop] + + def rasterize_external(self, elem, style, item, svgitem): + width = style['width'] + if width == 'auto': + width = self.profile.width + height = style['height'] + if height == 'auto': + height = self.profile.height + width = (width / 72) * self.profile.dpi + height = (height / 72) * self.profile.dpi + data = QByteArray(str(svgitem)) + svg = QSvgRenderer(data) + size = svg.defaultSize() size.scale(width, height, Qt.KeepAspectRatio) key = (svgitem.href, size.width(), size.height()) if key in self.images: href = self.images[key] else: image = QImage(size, QImage.Format_ARGB32_Premultiplied) + image.fill(QColor("white").rgb()) painter = QPainter(image) svg.render(painter) painter.end() @@ -86,7 +156,7 @@ class SVGRasterizer(object): manifest = self.oeb.manifest href = os.path.splitext(svgitem.href)[0] + '.png' id, href = manifest.generate(svgitem.id, href) - manifest.add(id, href, 'image/png', data=data) + manifest.add(id, href, PNG_MIME, data=data) self.images[key] = href elem.tag = XHTML('img') elem.attrib['src'] = item.relhref(href) @@ -94,3 +164,15 @@ class SVGRasterizer(object): for child in elem: elem.remove(child) + def rasterize_cover(self): + covers = self.oeb.metadata.cover + if not covers: + return + cover = self.oeb.manifest.ids[str(covers[0])] + if not cover.media_type == SVG_MIME: + return + data = self.rasterize_svg(cover.data, 600, 800) + href = os.path.splitext(cover.href)[0] + '.png' + id, href = self.oeb.manifest.generate(cover.id, href) + self.oeb.manifest.add(id, href, PNG_MIME, data=data) + covers[0].value = id