mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Mobipocket support improvements:
- Initial SVG rasterization support. - Various minor improvements.
This commit is contained in:
parent
b36ac2f96c
commit
d01d57f727
@ -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'
|
||||
|
@ -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('&', '&')
|
||||
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
|
||||
|
||||
|
@ -399,3 +399,8 @@ br {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Images and embedded object size defaults */
|
||||
img, object {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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']
|
||||
|
96
src/calibre/ebooks/oeb/transforms/rasterize.py
Normal file
96
src/calibre/ebooks/oeb/transforms/rasterize.py
Normal 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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user