mirror of
https://github.com/gryf/ebook-converter.git
synced 2026-04-21 05:31:30 +02:00
Added mobi writer files
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
'''
|
||||
HTML-TOC-adding transform.
|
||||
'''
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
||||
|
||||
from calibre.ebooks.oeb.base import XML, XHTML, XHTML_NS
|
||||
from calibre.ebooks.oeb.base import XHTML_MIME, CSS_MIME
|
||||
from calibre.ebooks.oeb.base import element, XPath
|
||||
from polyglot.builtins import unicode_type
|
||||
|
||||
__all__ = ['HTMLTOCAdder']
|
||||
|
||||
DEFAULT_TITLE = __('Table of Contents')
|
||||
|
||||
STYLE_CSS = {
|
||||
'nested': """
|
||||
.calibre_toc_header {
|
||||
text-align: center;
|
||||
}
|
||||
.calibre_toc_block {
|
||||
margin-left: 1.2em;
|
||||
text-indent: -1.2em;
|
||||
}
|
||||
.calibre_toc_block .calibre_toc_block {
|
||||
margin-left: 2.4em;
|
||||
}
|
||||
.calibre_toc_block .calibre_toc_block .calibre_toc_block {
|
||||
margin-left: 3.6em;
|
||||
}
|
||||
""",
|
||||
|
||||
'centered': """
|
||||
.calibre_toc_header {
|
||||
text-align: center;
|
||||
}
|
||||
.calibre_toc_block {
|
||||
text-align: center;
|
||||
}
|
||||
body > .calibre_toc_block {
|
||||
margin-top: 1.2em;
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
|
||||
class HTMLTOCAdder(object):
|
||||
|
||||
def __init__(self, title=None, style='nested', position='end'):
|
||||
self.title = title
|
||||
self.style = style
|
||||
self.position = position
|
||||
|
||||
@classmethod
|
||||
def config(cls, cfg):
|
||||
group = cfg.add_group('htmltoc', _('HTML TOC generation options.'))
|
||||
group('toc_title', ['--toc-title'], default=None,
|
||||
help=_('Title for any generated in-line table of contents.'))
|
||||
return cfg
|
||||
|
||||
@classmethod
|
||||
def generate(cls, opts):
|
||||
return cls(title=opts.toc_title)
|
||||
|
||||
def __call__(self, oeb, context):
|
||||
has_toc = getattr(getattr(oeb, 'toc', False), 'nodes', False)
|
||||
|
||||
if 'toc' in oeb.guide:
|
||||
# Ensure toc pointed to in <guide> is in spine
|
||||
from calibre.ebooks.oeb.base import urlnormalize
|
||||
href = urlnormalize(oeb.guide['toc'].href)
|
||||
if href in oeb.manifest.hrefs:
|
||||
item = oeb.manifest.hrefs[href]
|
||||
if (hasattr(item.data, 'xpath') and
|
||||
XPath('//h:a[@href]')(item.data)):
|
||||
if oeb.spine.index(item) < 0:
|
||||
if self.position == 'end':
|
||||
oeb.spine.add(item, linear=False)
|
||||
else:
|
||||
oeb.spine.insert(0, item, linear=True)
|
||||
return
|
||||
elif has_toc:
|
||||
oeb.guide.remove('toc')
|
||||
else:
|
||||
oeb.guide.remove('toc')
|
||||
if not has_toc:
|
||||
return
|
||||
oeb.logger.info('Generating in-line TOC...')
|
||||
title = self.title or oeb.translate(DEFAULT_TITLE)
|
||||
style = self.style
|
||||
if style not in STYLE_CSS:
|
||||
oeb.logger.error('Unknown TOC style %r' % style)
|
||||
style = 'nested'
|
||||
id, css_href = oeb.manifest.generate('tocstyle', 'tocstyle.css')
|
||||
oeb.manifest.add(id, css_href, CSS_MIME, data=STYLE_CSS[style])
|
||||
language = unicode_type(oeb.metadata.language[0])
|
||||
contents = element(None, XHTML('html'), nsmap={None: XHTML_NS},
|
||||
attrib={XML('lang'): language})
|
||||
head = element(contents, XHTML('head'))
|
||||
htitle = element(head, XHTML('title'))
|
||||
htitle.text = title
|
||||
element(head, XHTML('link'), rel='stylesheet', type=CSS_MIME,
|
||||
href=css_href)
|
||||
body = element(contents, XHTML('body'),
|
||||
attrib={'class': 'calibre_toc'})
|
||||
h1 = element(body, XHTML('h2'),
|
||||
attrib={'class': 'calibre_toc_header'})
|
||||
h1.text = title
|
||||
self.add_toc_level(body, oeb.toc)
|
||||
id, href = oeb.manifest.generate('contents', 'contents.xhtml')
|
||||
item = oeb.manifest.add(id, href, XHTML_MIME, data=contents)
|
||||
if self.position == 'end':
|
||||
oeb.spine.add(item, linear=False)
|
||||
else:
|
||||
oeb.spine.insert(0, item, linear=True)
|
||||
oeb.guide.add('toc', 'Table of Contents', href)
|
||||
|
||||
def add_toc_level(self, elem, toc):
|
||||
for node in toc:
|
||||
block = element(elem, XHTML('div'),
|
||||
attrib={'class': 'calibre_toc_block'})
|
||||
line = element(block, XHTML('a'),
|
||||
attrib={'href': node.href,
|
||||
'class': 'calibre_toc_line'})
|
||||
line.text = node.title
|
||||
self.add_toc_level(block, node)
|
||||
@@ -0,0 +1,117 @@
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
'''
|
||||
CSS case-mangling transform.
|
||||
'''
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
||||
|
||||
from lxml import etree
|
||||
from calibre.ebooks.oeb.base import XHTML, XHTML_NS
|
||||
from calibre.ebooks.oeb.base import CSS_MIME
|
||||
from calibre.ebooks.oeb.base import namespace
|
||||
from calibre.ebooks.oeb.stylizer import Stylizer
|
||||
from polyglot.builtins import string_or_bytes
|
||||
|
||||
CASE_MANGLER_CSS = """
|
||||
.calibre_lowercase {
|
||||
font-variant: normal;
|
||||
font-size: 0.65em;
|
||||
}
|
||||
"""
|
||||
|
||||
TEXT_TRANSFORMS = {'capitalize', 'uppercase', 'lowercase'}
|
||||
|
||||
|
||||
class CaseMangler(object):
|
||||
|
||||
@classmethod
|
||||
def config(cls, cfg):
|
||||
return cfg
|
||||
|
||||
@classmethod
|
||||
def generate(cls, opts):
|
||||
return cls()
|
||||
|
||||
def __call__(self, oeb, context):
|
||||
oeb.logger.info('Applying case-transforming CSS...')
|
||||
self.oeb = oeb
|
||||
self.opts = context
|
||||
self.profile = context.source
|
||||
self.mangle_spine()
|
||||
|
||||
def mangle_spine(self):
|
||||
id, href = self.oeb.manifest.generate('manglecase', 'manglecase.css')
|
||||
self.oeb.manifest.add(id, href, CSS_MIME, data=CASE_MANGLER_CSS)
|
||||
for item in self.oeb.spine:
|
||||
html = item.data
|
||||
relhref = item.relhref(href)
|
||||
etree.SubElement(html.find(XHTML('head')), XHTML('link'),
|
||||
rel='stylesheet', href=relhref, type=CSS_MIME)
|
||||
stylizer = Stylizer(html, item.href, self.oeb, self.opts, self.profile)
|
||||
self.mangle_elem(html.find(XHTML('body')), stylizer)
|
||||
|
||||
def text_transform(self, transform, text):
|
||||
if transform == 'capitalize':
|
||||
return icu_title(text)
|
||||
elif transform == 'uppercase':
|
||||
return icu_upper(text)
|
||||
elif transform == 'lowercase':
|
||||
return icu_lower(text)
|
||||
return text
|
||||
|
||||
def split_text(self, text):
|
||||
results = ['']
|
||||
isupper = text[0].isupper()
|
||||
for char in text:
|
||||
if char.isupper() == isupper:
|
||||
results[-1] += char
|
||||
else:
|
||||
isupper = not isupper
|
||||
results.append(char)
|
||||
return results
|
||||
|
||||
def smallcaps_elem(self, elem, attr):
|
||||
texts = self.split_text(getattr(elem, attr))
|
||||
setattr(elem, attr, None)
|
||||
last = elem if attr == 'tail' else None
|
||||
attrib = {'class': 'calibre_lowercase'}
|
||||
for text in texts:
|
||||
if text.isupper():
|
||||
if last is None:
|
||||
elem.text = text
|
||||
else:
|
||||
last.tail = text
|
||||
else:
|
||||
child = elem.makeelement(XHTML('span'), attrib=attrib)
|
||||
child.text = text.upper()
|
||||
if last is None:
|
||||
elem.insert(0, child)
|
||||
else:
|
||||
# addnext() moves the tail for some reason
|
||||
tail = last.tail
|
||||
last.addnext(child)
|
||||
last.tail = tail
|
||||
child.tail = None
|
||||
last = child
|
||||
|
||||
def mangle_elem(self, elem, stylizer):
|
||||
if not isinstance(elem.tag, string_or_bytes) or \
|
||||
namespace(elem.tag) != XHTML_NS:
|
||||
return
|
||||
children = list(elem)
|
||||
style = stylizer.style(elem)
|
||||
transform = style['text-transform']
|
||||
variant = style['font-variant']
|
||||
if elem.text:
|
||||
if transform in TEXT_TRANSFORMS:
|
||||
elem.text = self.text_transform(transform, elem.text)
|
||||
if variant == 'small-caps':
|
||||
self.smallcaps_elem(elem, 'text')
|
||||
for child in children:
|
||||
self.mangle_elem(child, stylizer)
|
||||
if child.tail:
|
||||
if transform in TEXT_TRANSFORMS:
|
||||
child.tail = self.text_transform(transform, child.tail)
|
||||
if variant == 'small-caps':
|
||||
self.smallcaps_elem(child, 'tail')
|
||||
@@ -0,0 +1,239 @@
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
'''
|
||||
SVG rasterization transform.
|
||||
'''
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
||||
|
||||
import os, re
|
||||
|
||||
from PyQt5.Qt import (
|
||||
Qt, QByteArray, QBuffer, QIODevice, QColor, QImage, QPainter, QSvgRenderer)
|
||||
from calibre.ebooks.oeb.base import XHTML, XLINK
|
||||
from calibre.ebooks.oeb.base import SVG_MIME, PNG_MIME
|
||||
from calibre.ebooks.oeb.base import xml2str, xpath
|
||||
from calibre.ebooks.oeb.base import urlnormalize
|
||||
from calibre.ebooks.oeb.stylizer import Stylizer
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
from calibre.utils.imghdr import what
|
||||
from polyglot.builtins import unicode_type
|
||||
from polyglot.urllib import urldefrag
|
||||
|
||||
IMAGE_TAGS = {XHTML('img'), XHTML('object')}
|
||||
KEEP_ATTRS = {'class', 'style', 'width', 'height', 'align'}
|
||||
|
||||
|
||||
class Unavailable(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SVGRasterizer(object):
|
||||
|
||||
def __init__(self, base_css=''):
|
||||
self.base_css = base_css
|
||||
from calibre.gui2 import must_use_qt
|
||||
must_use_qt()
|
||||
|
||||
@classmethod
|
||||
def config(cls, cfg):
|
||||
return cfg
|
||||
|
||||
@classmethod
|
||||
def generate(cls, opts):
|
||||
return cls()
|
||||
|
||||
def __call__(self, oeb, context):
|
||||
oeb.logger.info('Rasterizing SVG images...')
|
||||
self.temp_files = []
|
||||
self.stylizer_cache = {}
|
||||
self.oeb = oeb
|
||||
self.opts = context
|
||||
self.profile = context.dest
|
||||
self.images = {}
|
||||
self.dataize_manifest()
|
||||
self.rasterize_spine()
|
||||
self.rasterize_cover()
|
||||
for pt in self.temp_files:
|
||||
try:
|
||||
os.remove(pt)
|
||||
except:
|
||||
pass
|
||||
|
||||
def rasterize_svg(self, elem, width=0, height=0, format='PNG'):
|
||||
view_box = elem.get('viewBox', elem.get('viewbox', None))
|
||||
sizes = None
|
||||
logger = self.oeb.logger
|
||||
|
||||
if view_box is not None:
|
||||
try:
|
||||
box = [float(x) for x in filter(None, re.split('[, ]', view_box))]
|
||||
sizes = [box[2]-box[0], box[3] - box[1]]
|
||||
except (TypeError, ValueError, IndexError):
|
||||
logger.warn('SVG image has invalid viewBox="%s", ignoring the viewBox' % view_box)
|
||||
else:
|
||||
for image in elem.xpath('descendant::*[local-name()="image" and '
|
||||
'@height and contains(@height, "%")]'):
|
||||
logger.info('Found SVG image height in %, trying to convert...')
|
||||
try:
|
||||
h = float(image.get('height').replace('%', ''))/100.
|
||||
image.set('height', unicode_type(h*sizes[1]))
|
||||
except:
|
||||
logger.exception('Failed to convert percentage height:',
|
||||
image.get('height'))
|
||||
|
||||
data = QByteArray(xml2str(elem, with_tail=False))
|
||||
svg = QSvgRenderer(data)
|
||||
size = svg.defaultSize()
|
||||
if size.width() == 100 and size.height() == 100 and sizes:
|
||||
size.setWidth(sizes[0])
|
||||
size.setHeight(sizes[1])
|
||||
if width or height:
|
||||
size.scale(width, height, Qt.KeepAspectRatio)
|
||||
logger.info('Rasterizing %r to %dx%d'
|
||||
% (elem, size.width(), size.height()))
|
||||
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, format)
|
||||
return array.data()
|
||||
|
||||
def dataize_manifest(self):
|
||||
for item in self.oeb.manifest.values():
|
||||
if item.media_type == SVG_MIME and item.data is not None:
|
||||
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 = urlnormalize(elem.attrib[XLINK('href')])
|
||||
path = urldefrag(href)[0]
|
||||
if not path:
|
||||
continue
|
||||
abshref = item.abshref(path)
|
||||
if abshref not in hrefs:
|
||||
continue
|
||||
linkee = hrefs[abshref]
|
||||
data = linkee.bytes_representation
|
||||
ext = what(None, data) or 'jpg'
|
||||
with PersistentTemporaryFile(suffix='.'+ext) as pt:
|
||||
pt.write(data)
|
||||
self.temp_files.append(pt.name)
|
||||
elem.attrib[XLINK('href')] = pt.name
|
||||
return svg
|
||||
|
||||
def stylizer(self, item):
|
||||
ans = self.stylizer_cache.get(item, None)
|
||||
if ans is None:
|
||||
ans = Stylizer(item.data, item.href, self.oeb, self.opts,
|
||||
self.profile, base_css=self.base_css)
|
||||
self.stylizer_cache[item] = ans
|
||||
return ans
|
||||
|
||||
def rasterize_spine(self):
|
||||
for item in self.oeb.spine:
|
||||
self.rasterize_item(item)
|
||||
|
||||
def rasterize_item(self, item):
|
||||
html = item.data
|
||||
hrefs = self.oeb.manifest.hrefs
|
||||
for elem in xpath(html, '//h:img[@src]'):
|
||||
src = urlnormalize(elem.attrib['src'])
|
||||
image = hrefs.get(item.abshref(src), None)
|
||||
if image and image.media_type == SVG_MIME:
|
||||
style = self.stylizer(item).style(elem)
|
||||
self.rasterize_external(elem, style, item, image)
|
||||
for elem in xpath(html, '//h:object[@type="%s" and @data]' % SVG_MIME):
|
||||
data = urlnormalize(elem.attrib['data'])
|
||||
image = hrefs.get(item.abshref(data), None)
|
||||
if image and image.media_type == SVG_MIME:
|
||||
style = self.stylizer(item).style(elem)
|
||||
self.rasterize_external(elem, style, item, image)
|
||||
for elem in xpath(html, '//svg:svg'):
|
||||
style = self.stylizer(item).style(elem)
|
||||
self.rasterize_inline(elem, style, item)
|
||||
|
||||
def rasterize_inline(self, elem, style, item):
|
||||
width = style['width']
|
||||
height = style['height']
|
||||
width = (width / 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 = elem.makeelement(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']
|
||||
height = style['height']
|
||||
width = (width / 72) * self.profile.dpi
|
||||
height = (height / 72) * self.profile.dpi
|
||||
data = QByteArray(svgitem.bytes_representation)
|
||||
svg = QSvgRenderer(data)
|
||||
size = svg.defaultSize()
|
||||
size.scale(width, height, Qt.KeepAspectRatio)
|
||||
key = (svgitem.href, size.width(), size.height())
|
||||
if key in self.images:
|
||||
href = self.images[key]
|
||||
else:
|
||||
logger = self.oeb.logger
|
||||
logger.info('Rasterizing %r to %dx%d'
|
||||
% (svgitem.href, size.width(), size.height()))
|
||||
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')
|
||||
data = array.data()
|
||||
manifest = self.oeb.manifest
|
||||
href = os.path.splitext(svgitem.href)[0] + '.png'
|
||||
id, href = manifest.generate(svgitem.id, href)
|
||||
manifest.add(id, href, PNG_MIME, data=data)
|
||||
self.images[key] = href
|
||||
elem.tag = XHTML('img')
|
||||
for attr in elem.attrib:
|
||||
if attr not in KEEP_ATTRS:
|
||||
del elem.attrib[attr]
|
||||
elem.attrib['src'] = item.relhref(href)
|
||||
if elem.text:
|
||||
elem.attrib['alt'] = elem.text
|
||||
elem.text = None
|
||||
for child in elem:
|
||||
elem.remove(child)
|
||||
|
||||
def rasterize_cover(self):
|
||||
covers = self.oeb.metadata.cover
|
||||
if not covers:
|
||||
return
|
||||
if unicode_type(covers[0]) not in self.oeb.manifest.ids:
|
||||
self.oeb.logger.warn('Cover not in manifest, skipping.')
|
||||
self.oeb.metadata.clear('cover')
|
||||
return
|
||||
cover = self.oeb.manifest.ids[unicode_type(covers[0])]
|
||||
if not cover.media_type == SVG_MIME:
|
||||
return
|
||||
width = (self.profile.width / 72) * self.profile.dpi
|
||||
height = (self.profile.height / 72) * self.profile.dpi
|
||||
data = self.rasterize_svg(cover.data, width, height)
|
||||
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
|
||||
Reference in New Issue
Block a user