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.valign = 'baseline'
self.italic = False self.italic = False
self.bold = False self.bold = False
self.preserve = True self.preserve = False
self.family = 'serif' self.family = 'serif'
self.href = None self.href = None
self.list_num = 0 self.list_num = 0
@ -278,6 +278,10 @@ class MobiMLizer(object):
istate.preserve = (style['white-space'] in ('pre', 'pre-wrap')) istate.preserve = (style['white-space'] in ('pre', 'pre-wrap'))
if 'monospace' in style['font-family']: if 'monospace' in style['font-family']:
istate.family = 'monospace' istate.family = 'monospace'
elif 'sans-serif' in style['font-family']:
istate.family = 'sans-serif'
else:
istate.family = 'serif'
valign = style['vertical-align'] valign = style['vertical-align']
if valign in ('super', 'sup') and asfloat(valign) > 0: if valign in ('super', 'sup') and asfloat(valign) > 0:
istate.valign = 'super' istate.valign = 'super'

View File

@ -19,11 +19,13 @@ from collections import defaultdict
from urlparse import urldefrag from urlparse import urldefrag
from lxml import etree from lxml import etree
from PIL import Image 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 xpath, barename, namespace, prefixname
from calibre.ebooks.oeb.base import FauxLogger, OEBBook from calibre.ebooks.oeb.base import FauxLogger, OEBBook
from calibre.ebooks.oeb.profile import Context from calibre.ebooks.oeb.profile import Context
from calibre.ebooks.oeb.transforms.flatcss import CSSFlattener 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.palmdoc import compress_doc
from calibre.ebooks.mobi.langcodes import iana2mobi from calibre.ebooks.mobi.langcodes import iana2mobi
from calibre.ebooks.mobi.mobiml import MBP_NS, MBP, MobiMLizer from calibre.ebooks.mobi.mobiml import MBP_NS, MBP, MobiMLizer
@ -168,19 +170,30 @@ class Serializer(object):
index = self.images[val] index = self.images[val]
buffer.write('recindex="%05d"' % index) buffer.write('recindex="%05d"' % index)
continue 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: if elem.text or len(elem) > 0:
buffer.write('>') buffer.write('>')
if elem.text: if elem.text:
buffer.write(encode(elem.text)) self.serialize_text(elem.text)
for child in elem: for child in elem:
self.serialize_elem(child, item) self.serialize_elem(child, item)
if child.tail: if child.tail:
buffer.write(encode(child.tail)) self.serialize_text(child.tail)
buffer.write('</%s>' % tag) buffer.write('</%s>' % tag)
else: else:
buffer.write('/>') 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): def fixup_links(self):
buffer = self.buffer buffer = self.buffer
for id, hoffs in self.href_offsets.items(): for id, hoffs in self.href_offsets.items():
@ -226,7 +239,7 @@ class MobiWriter(object):
index = 1 index = 1
self._images = images = {} self._images = images = {}
for item in self._oeb.manifest.values(): 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 images[item.href] = index
index += 1 index += 1
@ -298,8 +311,8 @@ class MobiWriter(object):
image = Image.open(StringIO(data)) image = Image.open(StringIO(data))
format = image.format format = image.format
changed = False changed = False
if image.format not in ('JPEG', 'GIF'): if image.format not in ('JPEG', 'GIF', 'PNG'):
format = 'GIF' format = 'PNG'
changed = True changed = True
if dimen is not None: if dimen is not None:
image.thumbnail(dimen, Image.ANTIALIAS) image.thumbnail(dimen, Image.ANTIALIAS)
@ -434,8 +447,10 @@ def main(argv=sys.argv):
fbase = context.dest.fbase fbase = context.dest.fbase
fkey = context.dest.fnums.values() fkey = context.dest.fnums.values()
flattener = CSSFlattener(unfloat=True, fbase=fbase, fkey=fkey) flattener = CSSFlattener(unfloat=True, fbase=fbase, fkey=fkey)
rasterizer = SVGRasterizer()
mobimlizer = MobiMLizer() mobimlizer = MobiMLizer()
flattener.transform(oeb, context) flattener.transform(oeb, context)
rasterizer.transform(oeb, context)
mobimlizer.transform(oeb, context) mobimlizer.transform(oeb, context)
writer.dump(oeb, outpath) writer.dump(oeb, outpath)
return 0 return 0

View File

@ -399,3 +399,8 @@ br {
display: block; 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 # Not really, but let's pretend
'MobiDesktop': '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]), fsizes=[9, 10, 11, 12, 14, 17, 20, 24]),
# No clue on usable screen size and DPI # No clue on usable screen size and DPI

View File

@ -93,10 +93,12 @@ def xpath(elem, expr):
class CSSSelector(etree.XPath): class CSSSelector(etree.XPath):
MIN_SPACE_RE = re.compile(r' *([>~+]) *') MIN_SPACE_RE = re.compile(r' *([>~+]) *')
LOCAL_NAME_RE = re.compile(r"(?<!local-)name[(][)] *= *'[^:]+:")
def __init__(self, css, namespaces=XPNSMAP): def __init__(self, css, namespaces=XPNSMAP):
css = self.MIN_SPACE_RE.sub(r'\1', css) css = self.MIN_SPACE_RE.sub(r'\1', css)
path = css_to_xpath(css) path = css_to_xpath(css)
path = self.LOCAL_NAME_RE.sub(r"local-name() = '", path)
etree.XPath.__init__(self, path, namespaces=namespaces) etree.XPath.__init__(self, path, namespaces=namespaces)
self.css = css self.css = css
@ -164,7 +166,6 @@ class Stylizer(object):
for elem in xpath(tree, '//h:*[@style]'): for elem in xpath(tree, '//h:*[@style]'):
self.style(elem)._apply_style_attr() self.style(elem)._apply_style_attr()
def flatten_rule(self, rule, href, index): def flatten_rule(self, rule, href, index):
results = [] results = []
if isinstance(rule, CSSStyleRule): if isinstance(rule, CSSStyleRule):

View File

@ -179,7 +179,8 @@ class CSSFlattener(object):
percent = (margin - style['text-indent']) / style['width'] percent = (margin - style['text-indent']) / style['width']
cssdict['margin-left'] = "%d%%" % (percent * 100) cssdict['margin-left'] = "%d%%" % (percent * 100)
left -= style['text-indent'] 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'] del cssdict['float']
if cssdict.get('display', 'none') != 'none': if cssdict.get('display', 'none') != 'none':
del cssdict['display'] 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)