New MOBI Output: Clean up image handling

This commit is contained in:
Kovid Goyal 2011-07-31 12:38:01 -06:00
parent 6b637d1e5f
commit 0930375551
4 changed files with 114 additions and 108 deletions

View File

@ -8,6 +8,7 @@ from various formats.
'''
import traceback, os, re
from cStringIO import StringIO
from calibre import CurrentDir
class ConversionError(Exception):
@ -209,4 +210,45 @@ def unit_convert(value, base, font, dpi):
result = value * 0.40
return result
def generate_masthead(title, output_path=None, width=600, height=60):
from calibre.ebooks.conversion.config import load_defaults
from calibre.utils.fonts import fontconfig
font_path = default_font = P('fonts/liberation/LiberationSerif-Bold.ttf')
recs = load_defaults('mobi_output')
masthead_font_family = recs.get('masthead_font', 'Default')
if masthead_font_family != 'Default':
masthead_font = fontconfig.files_for_family(masthead_font_family)
# Assume 'normal' always in dict, else use default
# {'normal': (path_to_font, friendly name)}
if 'normal' in masthead_font:
font_path = masthead_font['normal'][0]
if not font_path or not os.access(font_path, os.R_OK):
font_path = default_font
try:
from PIL import Image, ImageDraw, ImageFont
Image, ImageDraw, ImageFont
except ImportError:
import Image, ImageDraw, ImageFont
img = Image.new('RGB', (width, height), 'white')
draw = ImageDraw.Draw(img)
try:
font = ImageFont.truetype(font_path, 48)
except:
font = ImageFont.truetype(default_font, 48)
text = title.encode('utf-8')
width, height = draw.textsize(text, font=font)
left = max(int((width - width)/2.), 0)
top = max(int((height - height)/2.), 0)
draw.text((left, top), text, fill=(0,0,0), font=font)
if output_path is None:
f = StringIO()
img.save(f, 'JPEG')
return f.getvalue()
else:
with open(output_path, 'wb') as f:
img.save(f, 'JPEG')

View File

@ -14,7 +14,7 @@ from collections import OrderedDict, defaultdict
from calibre.ebooks.mobi.writer2 import RECORD_SIZE
from calibre.ebooks.mobi.utils import (encint, encode_number_as_hex,
encode_tbs, align_block, utf8_text, detect_periodical)
encode_tbs, align_block, utf8_text)
class CNCX(object): # {{{
@ -323,16 +323,22 @@ class TBS(object): # {{{
class Indexer(object): # {{{
def __init__(self, serializer, number_of_text_records,
size_of_last_text_record, opts, oeb):
size_of_last_text_record, masthead_offset, is_periodical,
opts, oeb):
self.serializer = serializer
self.number_of_text_records = number_of_text_records
self.text_size = (RECORD_SIZE * (self.number_of_text_records-1) +
size_of_last_text_record)
self.masthead_offset = masthead_offset
self.oeb = oeb
self.log = oeb.log
self.opts = opts
self.is_periodical = detect_periodical(self.oeb.toc, self.log)
self.is_periodical = is_periodical
if self.is_periodical and self.masthead_offset is None:
raise ValueError('Periodicals must have a masthead')
self.log('Generating MOBI index for a %s'%('periodical' if
self.is_periodical else 'book'))
self.is_flat_periodical = False

View File

@ -11,7 +11,7 @@ import re, random, time
from cStringIO import StringIO
from struct import pack
from calibre.ebooks import normalize
from calibre.ebooks import normalize, generate_masthead
from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES
from calibre.ebooks.mobi.writer2.serializer import Serializer
from calibre.ebooks.compression.palmdoc import compress_doc
@ -19,7 +19,7 @@ from calibre.ebooks.mobi.langcodes import iana2mobi
from calibre.utils.filenames import ascii_filename
from calibre.ebooks.mobi.writer2 import (PALMDOC, UNCOMPRESSED, RECORD_SIZE)
from calibre.ebooks.mobi.utils import (rescale_image, encint,
encode_trailing_data, align_block)
encode_trailing_data, align_block, detect_periodical)
from calibre.ebooks.mobi.writer2.indexer import Indexer
EXTH_CODES = {
@ -35,6 +35,9 @@ EXTH_CODES = {
'type': 111,
'source': 112,
'versionnumber': 114,
'coveroffset': 201,
'thumboffset': 202,
'hasfakecover': 203,
'lastupdatetime': 502,
'title': 503,
}
@ -79,13 +82,12 @@ class MobiWriter(object):
self.write_content()
def generate_content(self):
self.map_image_names()
self.is_periodical = detect_periodical(self.oeb.toc, self.oeb.log)
self.generate_images()
self.generate_text()
# Index records come after text records
self.generate_index()
self.write_uncrossable_breaks()
# Image records come after index records
self.generate_images()
# Indexing {{{
def generate_index(self):
@ -93,6 +95,7 @@ class MobiWriter(object):
try:
self.indexer = Indexer(self.serializer, self.last_text_record_idx,
len(self.records[self.last_text_record_idx]),
self.masthead_offset, self.is_periodical,
self.opts, self.oeb)
except:
self.log.exception('Failed to generate MOBI index:')
@ -104,11 +107,6 @@ class MobiWriter(object):
self.records[i] += encode_trailing_data(tbs)
self.records.extend(self.indexer.records)
@property
def is_periodical(self):
return (self.primary_index_record_idx is None or not
self.indexer.is_periodical)
# }}}
def write_uncrossable_breaks(self): # {{{
@ -138,58 +136,51 @@ class MobiWriter(object):
# }}}
# Images {{{
def map_image_names(self):
'''
Map image names to record indices, ensuring that the masthead image if
present has index number 1.
'''
index = 1
self.images = images = {}
mh_href = None
if 'masthead' in self.oeb.guide:
mh_href = self.oeb.guide['masthead'].href
images[mh_href] = 1
index += 1
for item in self.oeb.manifest.values():
if item.media_type in OEB_RASTER_IMAGES:
if item.href == mh_href: continue
images[item.href] = index
index += 1
def generate_images(self):
self.oeb.logger.info('Serializing images...')
images = [(index, href) for href, index in self.images.iteritems()]
images.sort()
self.first_image_record = None
for _, href in images:
item = self.oeb.manifest.hrefs[href]
oeb = self.oeb
oeb.logger.info('Serializing images...')
self.image_records = []
mh_href = self.masthead_offset = None
if 'masthead' in oeb.guide:
mh_href = oeb.guide['masthead'].href
elif self.is_periodical:
# Generate a default masthead
data = generate_masthead(unicode(self.oeb.metadata('title')[0]))
self.image_records.append(data)
self.masthead_offset = 0
cover_href = self.cover_offset = self.thumbnail_offset = None
if (oeb.metadata.cover and
unicode(oeb.metadata.cover[0]) in oeb.manifest.ids):
cover_id = unicode(oeb.metadata.cover[0])
item = oeb.manifest.ids[cover_id]
cover_href = item.href
for item in self.oeb.manifest.values():
if item.media_type not in OEB_RASTER_IMAGES: continue
try:
data = rescale_image(item.data)
except:
self.oeb.logger.warn('Bad image file %r' % item.href)
oeb.logger.warn('Bad image file %r' % item.href)
continue
else:
if item.href == mh_href:
self.masthead_offset = len(self.image_records) - 1
elif item.href == cover_href:
self.image_records.append(data)
self.cover_offset = len(self.image_records) - 1
try:
data = rescale_image(item.data, dimen=MAX_THUMB_DIMEN,
maxsizeb=MAX_THUMB_SIZE)
except:
oeb.logger.warn('Failed to generate thumbnail')
else:
self.image_records.append(data)
self.thumbnail_offset = len(self.image_records) - 1
finally:
item.unload_data_from_memory()
self.records.append(data)
if self.first_image_record is None:
self.first_image_record = len(self.records) - 1
def add_thumbnail(self, item):
try:
data = rescale_image(item.data, dimen=MAX_THUMB_DIMEN,
maxsizeb=MAX_THUMB_SIZE)
except IOError:
self.oeb.logger.warn('Bad image file %r' % item.href)
return None
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
# }}}
@ -282,9 +273,13 @@ class MobiWriter(object):
def generate_record0(self): # MOBI header {{{
metadata = self.oeb.metadata
exth = self.build_exth()
first_image_record = None
if self.image_records:
first_image_record = len(self.records)
self.records.extend(self.image_records)
last_content_record = len(self.records) - 1
# FCIS/FLIS (Seem to server no purpose)
# FCIS/FLIS (Seems to serve no purpose)
flis_number = len(self.records)
self.records.append(
b'FLIS\0\0\0\x08\0\x41\0\0\0\0\0\0\xff\xff\xff\xff\0\x01\0\x03\0\0\0\x03\0\0\0\x01'+
@ -363,8 +358,7 @@ class MobiWriter(object):
# 0x58 - 0x5b : Format version
# 0x5c - 0x5f : First image record number
record0.write(pack(b'>II',
6, self.first_image_record if self.first_image_record else
len(self.records)-1))
6, first_image_record if first_image_record else len(self.records)))
# 0x60 - 0x63 : First HUFF/CDIC record number
# 0x64 - 0x67 : Number of HUFF/CDIC records
@ -539,20 +533,15 @@ class MobiWriter(object):
exth.write(pack(b'>III', code, 12, val))
nrecs += 1
if (oeb.metadata.cover and
unicode(oeb.metadata.cover[0]) in oeb.manifest.ids):
id = unicode(oeb.metadata.cover[0])
item = oeb.manifest.ids[id]
href = item.href
if href in self.images:
index = self.images[href] - 1
exth.write(pack(b'>III', 0xc9, 0x0c, index))
exth.write(pack(b'>III', 0xcb, 0x0c, 0))
nrecs += 2
index = self.add_thumbnail(item)
if index is not None:
exth.write(pack(b'>III', 0xca, 0x0c, index - 1))
nrecs += 1
if self.cover_offset is not None:
exth.write(pack(b'>III', EXTH_CODES['coveroffset'], 12,
self.cover_offset))
exth.write(pack(b'>III', EXTH_CODES['hasfakecover'], 12, 0))
nrecs += 2
if self.thumbnail_offset is not None:
exth.write(pack(b'>III', EXTH_CODES['thumboffset'], 12,
self.thumbnail_offset))
nrecs += 1
exth = exth.getvalue()
trail = len(exth) % 4

View File

@ -1083,40 +1083,9 @@ class BasicNewsRecipe(Recipe):
MI_HEIGHT = 60
def default_masthead_image(self, out_path):
from calibre.ebooks.conversion.config import load_defaults
from calibre.utils.fonts import fontconfig
font_path = default_font = P('fonts/liberation/LiberationSerif-Bold.ttf')
recs = load_defaults('mobi_output')
masthead_font_family = recs.get('masthead_font', 'Default')
if masthead_font_family != 'Default':
masthead_font = fontconfig.files_for_family(masthead_font_family)
# Assume 'normal' always in dict, else use default
# {'normal': (path_to_font, friendly name)}
if 'normal' in masthead_font:
font_path = masthead_font['normal'][0]
if not font_path or not os.access(font_path, os.R_OK):
font_path = default_font
try:
from PIL import Image, ImageDraw, ImageFont
Image, ImageDraw, ImageFont
except ImportError:
import Image, ImageDraw, ImageFont
img = Image.new('RGB', (self.MI_WIDTH, self.MI_HEIGHT), 'white')
draw = ImageDraw.Draw(img)
try:
font = ImageFont.truetype(font_path, 48)
except:
font = ImageFont.truetype(default_font, 48)
text = self.get_masthead_title().encode('utf-8')
width, height = draw.textsize(text, font=font)
left = max(int((self.MI_WIDTH - width)/2.), 0)
top = max(int((self.MI_HEIGHT - height)/2.), 0)
draw.text((left, top), text, fill=(0,0,0), font=font)
img.save(open(out_path, 'wb'), 'JPEG')
from calibre.ebooks import generate_masthead
generate_masthead(self.get_masthead_title(), output_path=out_path,
width=self.MI_WIDTH, height=self.MI_HEIGHT)
def prepare_masthead_image(self, path_to_image, out_path):
from calibre import fit_image