Fixed various bugs; added various improvements.

This commit is contained in:
Marshall T. Vandegrift 2009-01-06 09:44:24 -05:00
parent 28512fac5a
commit 4346c7f66c
7 changed files with 183 additions and 43 deletions

View File

@ -104,7 +104,7 @@ class MobiMLizer(object):
return ptsize return ptsize
fbase = self.profile.fbase fbase = self.profile.fbase
if ptsize < fbase: if ptsize < fbase:
return "%dpt" % int(round(ptsize * 2)) return "%dpt" % int(round(ptsize))
return "%dem" % int(round(ptsize / fbase)) return "%dem" % int(round(ptsize / fbase))
def preize_text(self, text): def preize_text(self, text):
@ -284,9 +284,9 @@ class MobiMLizer(object):
else: else:
istate.family = 'serif' 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') or asfloat(valign) > 0:
istate.valign = 'super' istate.valign = 'super'
elif valign == 'sub' and asfloat(valign) < 0: elif valign == 'sub' or asfloat(valign) < 0:
istate.valign = 'sub' istate.valign = 'sub'
else: else:
istate.valign = 'baseline' istate.valign = 'baseline'
@ -300,6 +300,15 @@ class MobiMLizer(object):
if tag == 'img' and 'src' in elem.attrib: if tag == 'img' and 'src' in elem.attrib:
istate.attrib['src'] = elem.attrib['src'] istate.attrib['src'] = elem.attrib['src']
istate.attrib['align'] = 'baseline' 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: elif tag == 'hr' and asfloat(style['width']) > 0:
prop = style['width'] / self.profile.width prop = style['width'] / self.profile.width
istate.attrib['width'] = "%d%%" % int(round(prop * 100)) istate.attrib['width'] = "%d%%" % int(round(prop * 100))

View File

@ -225,11 +225,22 @@ class MobiWriter(object):
self._oeb = oeb self._oeb = oeb
self._stream = stream self._stream = stream
self._records = [None] self._records = [None]
self._remove_html_cover()
self._generate_content() self._generate_content()
self._generate_record0() self._generate_record0()
self._write_header() self._write_header()
self._write_content() 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): def _generate_content(self):
self._map_image_names() self._map_image_names()
self._generate_text() self._generate_text()
@ -391,7 +402,7 @@ class MobiWriter(object):
nrecs += 1 nrecs += 1
if oeb.metadata.cover: if oeb.metadata.cover:
id = str(oeb.metadata.cover[0]) id = str(oeb.metadata.cover[0])
item = oeb.manifest[id] item = oeb.manifest.ids[id]
href = item.href href = item.href
index = self._images[href] - 1 index = self._images[href] - 1
exth.write(pack('>III', 0xc9, 0x0c, index)) exth.write(pack('>III', 0xc9, 0x0c, index))

View File

@ -17,6 +17,7 @@ import logging
import re import re
import htmlentitydefs import htmlentitydefs
import uuid import uuid
import copy
from lxml import etree from lxml import etree
from calibre import LoggingInterface from calibre import LoggingInterface
@ -32,10 +33,11 @@ XSI_NS = 'http://www.w3.org/2001/XMLSchema-instance'
DCTERMS_NS = 'http://purl.org/dc/terms/' DCTERMS_NS = 'http://purl.org/dc/terms/'
NCX_NS = 'http://www.daisy.org/z3986/2005/ncx/' NCX_NS = 'http://www.daisy.org/z3986/2005/ncx/'
SVG_NS = 'http://www.w3.org/2000/svg' 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, XPNSMAP = {'h': XHTML_NS, 'o1': OPF1_NS, 'o2': OPF2_NS,
'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS, 'd09': DC09_NS, 'd10': DC10_NS, 'd11': DC11_NS,
'xsi': XSI_NS, 'dt': DCTERMS_NS, 'ncx': NCX_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 XML(name): return '{%s}%s' % (XML_NS, name)
def XHTML(name): return '{%s}%s' % (XHTML_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 DC(name): return '{%s}%s' % (DC11_NS, name)
def NCX(name): return '{%s}%s' % (NCX_NS, name) def NCX(name): return '{%s}%s' % (NCX_NS, name)
def SVG(name): return '{%s}%s' % (SVG_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' EPUB_MIME = 'application/epub+zip'
XHTML_MIME = 'application/xhtml+xml' XHTML_MIME = 'application/xhtml+xml'
@ -246,10 +249,10 @@ class Metadata(object):
self.oeb = oeb self.oeb = oeb
self.items = defaultdict(list) 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) item = self.Item(term, value, attrib, **kwargs)
items = self.items[barename(item.term)] items = self.items[barename(item.term)]
items.append(item) items.insert(index, item)
return item return item
def iterkeys(self): def iterkeys(self):
@ -323,8 +326,7 @@ class Manifest(object):
data = self._loader(self.href) data = self._loader(self.href)
if self.media_type in OEB_DOCS: if self.media_type in OEB_DOCS:
data = self._force_xhtml(data) data = self._force_xhtml(data)
elif self.media_type[-4:] in ('+xml', '/xml') \ elif self.media_type[-4:] in ('+xml', '/xml'):
and self.media_type != SVG_MIME:
data = etree.fromstring(data, parser=XML_PARSER) data = etree.fromstring(data, parser=XML_PARSER)
self._data = data self._data = data
return data return data
@ -341,6 +343,9 @@ class Manifest(object):
return xml2str(data) return xml2str(data)
return str(data) return str(data)
def __eq__(self, other):
return id(self) == id(other)
def __cmp__(self, other): def __cmp__(self, other):
result = cmp(self.spine_position, other.spine_position) result = cmp(self.spine_position, other.spine_position)
if result != 0: if result != 0:
@ -558,9 +563,12 @@ class Guide(object):
for type, ref in self.refs.items(): for type, ref in self.refs.items():
yield type, ref yield type, ref
def __getitem__(self, index): def __getitem__(self, key):
return self.refs[index] return self.refs[key]
def __delitem__(self, key):
del self.refs[key]
def __contains__(self, key): def __contains__(self, key):
return key in self.refs return key in self.refs
@ -891,20 +899,27 @@ class OEBBook(object):
def _ensure_cover_image(self): def _ensure_cover_image(self):
cover = None cover = None
spine0 = self.spine[0]
html = spine0.data
if self.metadata.cover: if self.metadata.cover:
id = str(self.metadata.cover[0]) id = str(self.metadata.cover[0])
cover = self.manifest[id] cover = self.manifest.ids[id]
elif MS_COVER_TYPE in self.guide: elif MS_COVER_TYPE in self.guide:
href = self.guide[MS_COVER_TYPE].href href = self.guide[MS_COVER_TYPE].href
cover = self.manifest.hrefs[href] cover = self.manifest.hrefs[href]
elif 'cover' in self.guide: elif xpath(html, '//h:img[position()=1]'):
href = self.guide['cover'].href img = xpath(html, '//h:img[position()=1]')[0]
href = img.get('src')
cover = self.manifest.hrefs[href] cover = self.manifest.hrefs[href]
else: elif xpath(html, '//h:object[position()=1]'):
html = self.spine[0].data object = xpath(html, '//h:object[position()=1]')[0]
imgs = xpath(html, '//h:img[position()=1]') href = object.get('data')
href = imgs[0].get('src') if imgs else None cover = self.manifest.hrefs[href]
cover = self.manifest.hrefs[href] if href else None 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: if cover and not self.metadata.cover:
self.metadata.add('cover', cover.id) self.metadata.add('cover', cover.id)

View File

@ -35,7 +35,8 @@
* *
* ***** END LICENSE BLOCK ***** */ * ***** 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 */ /* blocks */
@ -399,8 +400,8 @@ br {
display: block; display: block;
} }
/* Images and embedded object size defaults */ /* Images, embedded object, and SVG size defaults */
img, object { img, object, svg|svg {
width: auto; width: auto;
height: auto; height: auto;
} }

View File

@ -41,8 +41,8 @@ PROFILES = {
# Not really, but let's pretend # Not really, but let's pretend
'MobiDesktop': 'MobiDesktop':
Profile(width=280, height=300, dpi=96, fbase=12, Profile(width=280, height=300, dpi=96, fbase=18,
fsizes=[9, 10, 11, 12, 14, 17, 20, 24]), fsizes=[14, 14, 16, 18, 20, 22, 22, 24]),
# No clue on usable screen size and DPI # No clue on usable screen size and DPI
'CybookG3': 'CybookG3':

View File

@ -268,6 +268,7 @@ class Style(object):
self._style = {} self._style = {}
self._fontSize = None self._fontSize = None
self._width = None self._width = None
self._height = None
stylizer._styles[element] = self stylizer._styles[element] = self
def _update_cssdict(self, cssdict): def _update_cssdict(self, cssdict):
@ -390,17 +391,38 @@ class Style(object):
base = styles[self._element.getparent()].width base = styles[self._element.getparent()].width
else: else:
base = self._profile.width 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'] width = self._style['width']
if width == 'auto':
result = base
else:
result = self._unit_convert(width, base=base)
else: else:
result = base result = base
if not result:
result = self._unit_convert(width, base=base)
self._width = result self._width = result
return self._width 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): def __str__(self):
items = self._style.items() items = self._style.items()
items.sort() items.sort()

View File

@ -8,17 +8,21 @@ __copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
import sys import sys
import os import os
from urlparse import urldefrag
import base64
from lxml import etree from lxml import etree
from PyQt4.QtCore import Qt from PyQt4.QtCore import Qt
from PyQt4.QtCore import QByteArray from PyQt4.QtCore import QByteArray
from PyQt4.QtCore import QBuffer from PyQt4.QtCore import QBuffer
from PyQt4.QtCore import QIODevice from PyQt4.QtCore import QIODevice
from PyQt4.QtGui import QColor
from PyQt4.QtGui import QImage from PyQt4.QtGui import QImage
from PyQt4.QtGui import QPainter from PyQt4.QtGui import QPainter
from PyQt4.QtSvg import QSvgRenderer from PyQt4.QtSvg import QSvgRenderer
from PyQt4.QtGui import QApplication from PyQt4.QtGui import QApplication
from calibre.ebooks.oeb.base import XHTML, SVG, SVG_NS, SVG_MIME from calibre.ebooks.oeb.base import XHTML_NS, XHTML, SVG_NS, SVG, XLINK
from calibre.ebooks.oeb.base import namespace, barename 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 from calibre.ebooks.oeb.stylizer import Stylizer
IMAGE_TAGS = set([XHTML('img'), XHTML('object')]) IMAGE_TAGS = set([XHTML('img'), XHTML('object')])
@ -32,8 +36,55 @@ class SVGRasterizer(object):
self.oeb = oeb self.oeb = oeb
self.profile = context.dest self.profile = context.dest
self.images = {} self.images = {}
self.dataize_manifest()
self.rasterize_spine() 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): def rasterize_spine(self):
for item in self.oeb.spine: for item in self.oeb.spine:
html = item.data html = item.data
@ -44,7 +95,7 @@ class SVGRasterizer(object):
if not isinstance(elem.tag, basestring): return if not isinstance(elem.tag, basestring): return
style = stylizer.style(elem) style = stylizer.style(elem)
if namespace(elem.tag) == SVG_NS: 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: if elem.tag in IMAGE_TAGS:
manifest = self.oeb.manifest manifest = self.oeb.manifest
src = elem.get('src', None) or elem.get('data', None) src = elem.get('src', None) or elem.get('data', None)
@ -54,27 +105,46 @@ class SVGRasterizer(object):
for child in elem: for child in elem:
self.rasterize_elem(child, item, stylizer) self.rasterize_elem(child, item, stylizer)
def rasterize_inline(self, elem, style): def rasterize_inline(self, elem, style, item):
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'] width = style['width']
if width == 'auto': if width == 'auto':
width = self.profile.width width = self.profile.width
height = style['height']
if height == 'auto':
height = self.profile.height
width = (width / 72) * self.profile.dpi width = (width / 72) * self.profile.dpi
height = (height / 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) size.scale(width, height, Qt.KeepAspectRatio)
key = (svgitem.href, size.width(), size.height()) key = (svgitem.href, size.width(), size.height())
if key in self.images: if key in self.images:
href = self.images[key] href = self.images[key]
else: else:
image = QImage(size, QImage.Format_ARGB32_Premultiplied) image = QImage(size, QImage.Format_ARGB32_Premultiplied)
image.fill(QColor("white").rgb())
painter = QPainter(image) painter = QPainter(image)
svg.render(painter) svg.render(painter)
painter.end() painter.end()
@ -86,7 +156,7 @@ class SVGRasterizer(object):
manifest = self.oeb.manifest manifest = self.oeb.manifest
href = os.path.splitext(svgitem.href)[0] + '.png' href = os.path.splitext(svgitem.href)[0] + '.png'
id, href = manifest.generate(svgitem.id, href) 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 self.images[key] = href
elem.tag = XHTML('img') elem.tag = XHTML('img')
elem.attrib['src'] = item.relhref(href) elem.attrib['src'] = item.relhref(href)
@ -94,3 +164,15 @@ class SVGRasterizer(object):
for child in elem: for child in elem:
elem.remove(child) 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