From d01d57f727a7a73e26e882dced42998792508c1e Mon Sep 17 00:00:00 2001 From: "Marshall T. Vandegrift" Date: Mon, 5 Jan 2009 07:44:46 -0500 Subject: [PATCH] Mobipocket support improvements: - Initial SVG rasterization support. - Various minor improvements. --- src/calibre/ebooks/mobi/mobiml.py | 6 +- src/calibre/ebooks/mobi/writer.py | 31 ++++-- src/calibre/ebooks/oeb/html.css | 5 + src/calibre/ebooks/oeb/profile.py | 2 +- src/calibre/ebooks/oeb/stylizer.py | 7 +- src/calibre/ebooks/oeb/transforms/flatcss.py | 3 +- .../ebooks/oeb/transforms/rasterize.py | 96 +++++++++++++++++++ 7 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 src/calibre/ebooks/oeb/transforms/rasterize.py diff --git a/src/calibre/ebooks/mobi/mobiml.py b/src/calibre/ebooks/mobi/mobiml.py index 1ad70c0865..65252306a6 100644 --- a/src/calibre/ebooks/mobi/mobiml.py +++ b/src/calibre/ebooks/mobi/mobiml.py @@ -55,7 +55,7 @@ class FormatState(object): self.valign = 'baseline' self.italic = False self.bold = False - self.preserve = True + self.preserve = False self.family = 'serif' self.href = None self.list_num = 0 @@ -278,6 +278,10 @@ class MobiMLizer(object): istate.preserve = (style['white-space'] in ('pre', 'pre-wrap')) if 'monospace' in style['font-family']: istate.family = 'monospace' + elif 'sans-serif' in style['font-family']: + istate.family = 'sans-serif' + else: + istate.family = 'serif' valign = style['vertical-align'] if valign in ('super', 'sup') and asfloat(valign) > 0: istate.valign = 'super' diff --git a/src/calibre/ebooks/mobi/writer.py b/src/calibre/ebooks/mobi/writer.py index 7cfcd7a415..561e6e22f8 100644 --- a/src/calibre/ebooks/mobi/writer.py +++ b/src/calibre/ebooks/mobi/writer.py @@ -19,11 +19,13 @@ from collections import defaultdict from urlparse import urldefrag from lxml import etree from PIL import Image -from calibre.ebooks.oeb.base import XML_NS, XHTML, XHTML_NS, OEB_DOCS +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 FauxLogger, OEBBook from calibre.ebooks.oeb.profile import Context from calibre.ebooks.oeb.transforms.flatcss import CSSFlattener +from calibre.ebooks.oeb.transforms.rasterize import SVGRasterizer from calibre.ebooks.mobi.palmdoc import compress_doc from calibre.ebooks.mobi.langcodes import iana2mobi from calibre.ebooks.mobi.mobiml import MBP_NS, MBP, MobiMLizer @@ -168,19 +170,30 @@ class Serializer(object): index = self.images[val] buffer.write('recindex="%05d"' % index) continue - buffer.write('%s="%s"' % (attr, val)) + buffer.write(attr) + buffer.write('="') + self.serialize_text(val, quot=True) + buffer.write('"') if elem.text or len(elem) > 0: buffer.write('>') if elem.text: - buffer.write(encode(elem.text)) + self.serialize_text(elem.text) for child in elem: self.serialize_elem(child, item) if child.tail: - buffer.write(encode(child.tail)) + self.serialize_text(child.tail) buffer.write('' % tag) else: buffer.write('/>') + def serialize_text(self, text, quot=False): + text = text.replace('&', '&') + text = text.replace('<', '<') + text = text.replace('>', '>') + if quot: + text = text.replace('"', '"') + self.buffer.write(encode(text)) + def fixup_links(self): buffer = self.buffer for id, hoffs in self.href_offsets.items(): @@ -226,7 +239,7 @@ class MobiWriter(object): index = 1 self._images = images = {} for item in self._oeb.manifest.values(): - if item.media_type.startswith('image/'): + if item.media_type in OEB_RASTER_IMAGES: images[item.href] = index index += 1 @@ -298,8 +311,8 @@ class MobiWriter(object): image = Image.open(StringIO(data)) format = image.format changed = False - if image.format not in ('JPEG', 'GIF'): - format = 'GIF' + if image.format not in ('JPEG', 'GIF', 'PNG'): + format = 'PNG' changed = True if dimen is not None: image.thumbnail(dimen, Image.ANTIALIAS) @@ -434,9 +447,11 @@ def main(argv=sys.argv): fbase = context.dest.fbase fkey = context.dest.fnums.values() flattener = CSSFlattener(unfloat=True, fbase=fbase, fkey=fkey) + rasterizer = SVGRasterizer() mobimlizer = MobiMLizer() flattener.transform(oeb, context) - mobimlizer.transform(oeb, context) + rasterizer.transform(oeb, context) + mobimlizer.transform(oeb, context) writer.dump(oeb, outpath) return 0 diff --git a/src/calibre/ebooks/oeb/html.css b/src/calibre/ebooks/oeb/html.css index a454b9b716..4cf8cc5fce 100644 --- a/src/calibre/ebooks/oeb/html.css +++ b/src/calibre/ebooks/oeb/html.css @@ -399,3 +399,8 @@ br { display: block; } +/* Images and embedded object size defaults */ +img, object { + width: auto; + height: auto; +} diff --git a/src/calibre/ebooks/oeb/profile.py b/src/calibre/ebooks/oeb/profile.py index 901555fdd7..d5e3f2ff10 100644 --- a/src/calibre/ebooks/oeb/profile.py +++ b/src/calibre/ebooks/oeb/profile.py @@ -41,7 +41,7 @@ PROFILES = { # Not really, but let's pretend 'MobiDesktop': - Profile(width=280, height=300, dpi=100, fbase=12, + Profile(width=280, height=300, dpi=96, fbase=12, fsizes=[9, 10, 11, 12, 14, 17, 20, 24]), # No clue on usable screen size and DPI diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 45e248febe..4dad03d47e 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -93,10 +93,12 @@ def xpath(elem, expr): class CSSSelector(etree.XPath): MIN_SPACE_RE = re.compile(r' *([>~+]) *') + LOCAL_NAME_RE = re.compile(r"(?' + +import sys +import os +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 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.stylizer import Stylizer + +IMAGE_TAGS = set([XHTML('img'), XHTML('object')]) + +class SVGRasterizer(object): + def __init__(self): + if QApplication.instance() is None: + QApplication([]) + + def transform(self, oeb, context): + self.oeb = oeb + self.profile = context.dest + self.images = {} + self.rasterize_spine() + + def rasterize_spine(self): + for item in self.oeb.spine: + html = item.data + stylizer = Stylizer(html, item.href, self.oeb, self.profile) + self.rasterize_elem(html.find(XHTML('body')), item, stylizer) + + def rasterize_elem(self, elem, item, stylizer): + if not isinstance(elem.tag, basestring): return + style = stylizer.style(elem) + if namespace(elem.tag) == SVG_NS: + return self.rasterize_inline(elem, style) + if elem.tag in IMAGE_TAGS: + manifest = self.oeb.manifest + src = elem.get('src', None) or elem.get('data', None) + image = manifest.hrefs[item.abshref(src)] if src else None + if image and image.media_type == SVG_MIME: + return self.rasterize_external(elem, style, item, image) + 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 + width = style['width'] + if width == 'auto': + width = self.profile.width + width = (width / 72) * self.profile.dpi + height = (height / 72) * self.profile.dpi + 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) + painter = QPainter(image) + svg.render(painter) + painter.end() + array = QByteArray() + buffer = QBuffer(array) + buffer.open(QIODevice.WriteOnly) + image.save(buffer, 'PNG') + data = str(array) + 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) + self.images[key] = href + elem.tag = XHTML('img') + elem.attrib['src'] = item.relhref(href) + elem.text = None + for child in elem: + elem.remove(child) +