Implemented adding Mobipocket covers and cover thumbnails. Degrade image quality to fit Mobi size constraints.

This commit is contained in:
Marshall T. Vandegrift 2008-12-29 09:22:51 -05:00
parent e61c667505
commit b8373de13b
2 changed files with 79 additions and 27 deletions

View File

@ -115,7 +115,7 @@ class DirContainer(AbstractContainer):
def exists(self, path): def exists(self, path):
path = os.path.join(self.rootdir, path) path = os.path.join(self.rootdir, path)
return os.path.isfile(path) return os.path.isfile(urlunquote(path))
class Metadata(object): class Metadata(object):
@ -227,7 +227,8 @@ class Metadata(object):
class Manifest(object): class Manifest(object):
class Item(object): class Item(object):
def __init__(self, id, href, media_type, fallback=None, loader=str): def __init__(self, id, href, media_type,
fallback=None, loader=str, data=None):
self.id = id self.id = id
self.href = self.path = urlnormalize(href) self.href = self.path = urlnormalize(href)
self.media_type = media_type self.media_type = media_type
@ -235,7 +236,7 @@ class Manifest(object):
self.spine_position = None self.spine_position = None
self.linear = True self.linear = True
self._loader = loader self._loader = loader
self._data = None self._data = data
def __repr__(self): def __repr__(self):
return 'Item(id=%r, href=%r, media_type=%r)' \ return 'Item(id=%r, href=%r, media_type=%r)' \
@ -243,10 +244,10 @@ class Manifest(object):
def data(): def data():
def fget(self): def fget(self):
if self._data: if self._data is not None:
return self._data return self._data
data = self._loader(self.href) data = self._loader(self.href)
if self.media_type == XHTML_MIME: if self.media_type in OEB_DOCS:
data = etree.fromstring(data, parser=XML_PARSER) data = etree.fromstring(data, parser=XML_PARSER)
if namespace(data.tag) != XHTML_NS: if namespace(data.tag) != XHTML_NS:
data.attrib['xmlns'] = XHTML_NS data.attrib['xmlns'] = XHTML_NS
@ -255,6 +256,7 @@ class Manifest(object):
elif self.media_type.startswith('application/') \ elif self.media_type.startswith('application/') \
and self.media_type.endswith('+xml'): and self.media_type.endswith('+xml'):
data = etree.fromstring(data, parser=XML_PARSER) data = etree.fromstring(data, parser=XML_PARSER)
self._data = data
return data return data
def fset(self, value): def fset(self, value):
self._data = value self._data = value
@ -271,38 +273,56 @@ class Manifest(object):
def __init__(self, oeb): def __init__(self, oeb):
self.oeb = oeb self.oeb = oeb
self.items = {} self.ids = {}
self.hrefs = {} self.hrefs = {}
def add(self, id, href, media_type, fallback=None): def add(self, id, href, media_type, fallback=None, loader=None, data=None):
loader = loader or self.oeb.container.read
item = self.Item( item = self.Item(
id, href, media_type, fallback, self.oeb.container.read) id, href, media_type, fallback, loader, data)
self.items[item.id] = item self.ids[item.id] = item
self.hrefs[item.href] = item self.hrefs[item.href] = item
return item return item
def remove(self, id): def remove(self, item):
href = self.items[id].href if item in self.ids:
del self.items[id] item = self.ids[item]
del self.hrefs[href] del self.ids[item.id]
del self.hrefs[item.href]
if item in self.oeb.spine:
self.oeb.spine.remove(item)
def generate(self, id, href):
href = urlnormalize(href)
base = id
index = 1
while id in self.ids:
id = base + str(index)
index += 1
base, ext = os.path.splitext(href)
index = 1
while href in self.hrefs:
href = base + str(index) + ext
index += 1
return id, href
def __iter__(self): def __iter__(self):
for id in self.items: for id in self.ids:
yield id yield id
def __getitem__(self, id): def __getitem__(self, id):
return self.items[id] return self.ids[id]
def values(self): def values(self):
for item in self.items.values(): for item in self.ids.values():
yield item yield item
def items(self): def items(self):
for id, item in self.refs.items(): for id, item in self.ids.items():
yield id, items yield id, item
def __contains__(self, key): def __contains__(self, key):
return key in self.items return key in self.ids
def to_opf1(self, parent=None): def to_opf1(self, parent=None):
elem = element(parent, 'manifest') elem = element(parent, 'manifest')
@ -706,9 +726,8 @@ class OEBBook(object):
imgs = xpath(html, '//h:img[position()=1]') imgs = xpath(html, '//h:img[position()=1]')
href = imgs[0].get('src') if imgs else None href = imgs[0].get('src') if imgs else None
cover = self.manifest.hrefs[href] if href else None cover = self.manifest.hrefs[href] if href else None
if cover: if cover and not self.metadata.cover:
if not self.metadata.cover: self.metadata.add('cover', cover.id)
self.metadata.add('cover', cover.id)
def _all_from_opf(self, opf): def _all_from_opf(self, opf):
self._metadata_from_opf(opf) self._metadata_from_opf(opf)

View File

@ -18,6 +18,7 @@ from itertools import izip, count
from collections import defaultdict 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 calibre.ebooks.mobi.palmdoc import compress_doc from calibre.ebooks.mobi.palmdoc import compress_doc
from calibre.ebooks.lit.oeb import XHTML, XHTML_NS, OEB_DOCS from calibre.ebooks.lit.oeb import XHTML, XHTML_NS, OEB_DOCS
from calibre.ebooks.lit.oeb import xpath, barename, namespace from calibre.ebooks.lit.oeb import xpath, barename, namespace
@ -213,10 +214,21 @@ class MobiWriter(object):
def _generate_images(self): def _generate_images(self):
images = [(index, href) for href, index in self._images.items()] images = [(index, href) for href, index in self._images.items()]
images.sort() images.sort()
metadata = self._oeb.metadata
coverid = metadata.cover[0] if metadata.cover else None
for _, href in images: for _, href in images:
item = self._oeb.manifest.hrefs[href] item = self._oeb.manifest.hrefs[href]
data = item.data data = item.data
# TODO: Re-size etc images # TODO: Re-size etc images
image = Image.open(StringIO(item.data))
maxsizek = 89 if coverid == item.id else 63
maxsizeb = maxsizek * 1024
for quality in xrange(95, -1, -1):
data = StringIO()
image.save(data, 'JPEG', quality=quality)
data = data.getvalue()
if len(data) <= maxsizeb:
break
self._records.append(data) self._records.append(data)
def _generate_record0(self): def _generate_record0(self):
@ -262,16 +274,37 @@ 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])
href = oeb.manifest[id].href item = oeb.manifest[id]
index = self._images[href] + self._text_nrecords - 1 href = item.href
index = self._images[href] - 1
exth.write(pack('>III', 0xc9, 0x0c, index)) exth.write(pack('>III', 0xc9, 0x0c, index))
nrecs += 1 exth.write(pack('>III', 0xcb, 0x0c, 0))
trail = exth.tell() % 4 index = self._add_thumbnail(item) - 1
pad = '' if not trail else '\0' * (4 - trail) exth.write(pack('>III', 0xca, 0x0c, index))
nrecs += 3
exth = exth.getvalue() exth = exth.getvalue()
trail = len(exth) % 4
pad = '' if not trail else '\0' * (4 - trail)
exth = ['EXTH', pack('>II', len(exth) + 12, nrecs), exth, pad] exth = ['EXTH', pack('>II', len(exth) + 12, nrecs), exth, pad]
return ''.join(exth) return ''.join(exth)
def _add_thumbnail(self, item):
thumbnail = Image.open(StringIO(item.data))
thumbnail.thumbnail((180, 240), Image.ANTIALIAS)
for quality in xrange(95, -1, -1):
data = StringIO()
thumbnail.save(data, 'JPEG', quality=quality)
data = data.getvalue()
if len(data) <= (1024 * 16):
break
manifest = self._oeb.manifest
id, href = manifest.generate('thumbnail', 'thumbnail.jpeg')
manifest.add(id, href, 'image/jpeg', data=data)
index = len(self._images) + 1
self._images[href] = index
self._records.append(data)
return index
def _write_header(self): def _write_header(self):
title = str(self._oeb.metadata.title[0]) title = str(self._oeb.metadata.title[0])
title = re.sub('[^-A-Za-z0-9]+', '_', title)[:32] title = re.sub('[^-A-Za-z0-9]+', '_', title)[:32]