Mobipocket support improvements:

- Initial SVG rasterization support.
  - Various minor improvements.
This commit is contained in:
Marshall T. Vandegrift 2009-01-05 07:44:46 -05:00
parent b36ac2f96c
commit d01d57f727
7 changed files with 136 additions and 14 deletions

View File

@ -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'

View File

@ -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('</%s>' % tag)
else:
buffer.write('/>')
def serialize_text(self, text, quot=False):
text = text.replace('&', '&amp;')
text = text.replace('<', '&lt;')
text = text.replace('>', '&gt;')
if quot:
text = text.replace('"', '&quot;')
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

View File

@ -399,3 +399,8 @@ br {
display: block;
}
/* Images and embedded object size defaults */
img, object {
width: auto;
height: auto;
}

View File

@ -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

View File

@ -93,10 +93,12 @@ def xpath(elem, expr):
class CSSSelector(etree.XPath):
MIN_SPACE_RE = re.compile(r' *([>~+]) *')
LOCAL_NAME_RE = re.compile(r"(?<!local-)name[(][)] *= *'[^:]+:")
def __init__(self, css, namespaces=XPNSMAP):
css = self.MIN_SPACE_RE.sub(r'\1', css)
path = css_to_xpath(css)
path = self.LOCAL_NAME_RE.sub(r"local-name() = '", path)
etree.XPath.__init__(self, path, namespaces=namespaces)
self.css = css
@ -163,8 +165,7 @@ class Stylizer(object):
self.style(elem)._update_cssdict(cssdict)
for elem in xpath(tree, '//h:*[@style]'):
self.style(elem)._apply_style_attr()
def flatten_rule(self, rule, href, index):
results = []
if isinstance(rule, CSSStyleRule):
@ -178,7 +179,7 @@ class Stylizer(object):
style = self.flatten_style(rule.style)
self.page_rule.update(style)
return results
def flatten_style(self, cssstyle):
style = {}
for prop in cssstyle:

View File

@ -179,7 +179,8 @@ class CSSFlattener(object):
percent = (margin - style['text-indent']) / style['width']
cssdict['margin-left'] = "%d%%" % (percent * 100)
left -= style['text-indent']
if self.unfloat and 'float' in cssdict and tag != 'img':
if self.unfloat and 'float' in cssdict \
and tag not in ('img', 'object'):
del cssdict['float']
if cssdict.get('display', 'none') != 'none':
del cssdict['display']

View File

@ -0,0 +1,96 @@
'''
SVG rasterization transform.
'''
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
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)