mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
New MOBI Output: Clean up image handling
This commit is contained in:
parent
6b637d1e5f
commit
0930375551
@ -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')
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user