Merge from trunk

This commit is contained in:
Charles Haley 2011-08-01 09:32:14 +01:00
commit d5e3693eb5
9 changed files with 164 additions and 128 deletions

View File

@ -8,6 +8,7 @@ from various formats.
''' '''
import traceback, os, re import traceback, os, re
from cStringIO import StringIO
from calibre import CurrentDir from calibre import CurrentDir
class ConversionError(Exception): class ConversionError(Exception):
@ -209,4 +210,45 @@ def unit_convert(value, base, font, dpi):
result = value * 0.40 result = value * 0.40
return result 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

@ -1089,6 +1089,11 @@ class TextRecord(object): # {{{
self.trailing_data['uncrossable_breaks'] = self.trailing_data.pop(2) self.trailing_data['uncrossable_breaks'] = self.trailing_data.pop(2)
self.trailing_data['raw_bytes'] = raw_trailing_bytes self.trailing_data['raw_bytes'] = raw_trailing_bytes
for typ, val in self.trailing_data.iteritems():
if isinstance(typ, int):
print ('Record %d has unknown trailing data of type: %d : %r'%
(idx, typ, val))
self.idx = idx self.idx = idx
def dump(self, folder): def dump(self, folder):
@ -1192,8 +1197,7 @@ class TBSIndexing(object): # {{{
'(%d ends, %d complete, %d starts)')%tuple(map(len, (s+e+c, e, '(%d ends, %d complete, %d starts)')%tuple(map(len, (s+e+c, e,
c, s)))) c, s))))
byts = bytearray(r.trailing_data.get('indexing', b'')) byts = bytearray(r.trailing_data.get('indexing', b''))
sbyts = tuple(hex(b)[2:] for b in byts) ans.append('TBS bytes: %s'%format_bytes(byts))
ans.append('TBS bytes: %s'%(' '.join(sbyts)))
for typ, entries in (('Ends', e), ('Complete', c), ('Starts', s)): for typ, entries in (('Ends', e), ('Complete', c), ('Starts', s)):
if entries: if entries:
ans.append('\t%s:'%typ) ans.append('\t%s:'%typ)
@ -1220,8 +1224,14 @@ class TBSIndexing(object): # {{{
ans.append('Outermost index: %d'%outermost_index) ans.append('Outermost index: %d'%outermost_index)
ans.append('Unknown extra start bytes: %s'%repr_extra(extra)) ans.append('Unknown extra start bytes: %s'%repr_extra(extra))
if is_periodical: # Hierarchical periodical if is_periodical: # Hierarchical periodical
byts, a = self.interpret_periodical(tbs_type, byts, try:
byts, a = self.interpret_periodical(tbs_type, byts,
dat['geom'][0]) dat['geom'][0])
except:
import traceback
traceback.print_exc()
a = []
print ('Failed to decode TBS bytes for record: %d'%r.idx)
ans += a ans += a
if byts: if byts:
sbyts = tuple(hex(b)[2:] for b in byts) sbyts = tuple(hex(b)[2:] for b in byts)
@ -1372,7 +1382,7 @@ class MOBIFile(object): # {{{
self.index_header, self.cncx) self.index_header, self.cncx)
self.indexing_record_nums = set(xrange(pir, self.indexing_record_nums = set(xrange(pir,
pir+2+self.index_header.num_of_cncx_blocks)) pir+2+self.index_header.num_of_cncx_blocks))
self.secondary_index_record = self.secondary_index_record = None self.secondary_index_record = self.secondary_index_header = None
sir = self.mobi_header.secondary_index_record sir = self.mobi_header.secondary_index_record
if sir != 0xffffffff: if sir != 0xffffffff:
self.secondary_index_header = SecondaryIndexHeader(self.records[sir]) self.secondary_index_header = SecondaryIndexHeader(self.records[sir])

View File

@ -169,19 +169,26 @@ def get_trailing_data(record, extra_data_flags):
:return: Trailing data, record - trailing data :return: Trailing data, record - trailing data
''' '''
data = OrderedDict() data = OrderedDict()
for i in xrange(16, -1, -1): flags = extra_data_flags >> 1
flag = 1 << i # 2**i
if flag & extra_data_flags: num = 0
if i == 0: while flags:
# Only the first two bits are used for the size since there can num += 1
# never be more than 3 trailing multibyte chars if flags & 0b1:
sz = (ord(record[-1]) & 0b11) + 1 sz, consumed = decint(record, forward=False)
consumed = 1
else:
sz, consumed = decint(record, forward=False)
if sz > consumed: if sz > consumed:
data[i] = record[-sz:-consumed] data[num] = record[-sz:-consumed]
record = record[:-sz] record = record[:-sz]
flags >>= 1
# Read multibyte chars if any
if extra_data_flags & 0b1:
# Only the first two bits are used for the size since there can
# never be more than 3 trailing multibyte chars
sz = (ord(record[-1]) & 0b11) + 1
consumed = 1
if sz > consumed:
data[0] = record[-sz:-consumed]
record = record[:-sz]
return data, record return data, record
def encode_trailing_data(raw): def encode_trailing_data(raw):

View File

@ -430,6 +430,7 @@ class MobiWriter(object):
text.seek(npos) text.seek(npos)
return data, overlap return data, overlap
# TBS {{{
def _generate_flat_indexed_navpoints(self): def _generate_flat_indexed_navpoints(self):
# Assemble a HTMLRecordData instance for each HTML record # Assemble a HTMLRecordData instance for each HTML record
# Return True if valid, False if invalid # Return True if valid, False if invalid
@ -1174,6 +1175,8 @@ class MobiWriter(object):
self._tbSequence = tbSequence self._tbSequence = tbSequence
# }}}
def _evaluate_periodical_toc(self): def _evaluate_periodical_toc(self):
''' '''
Periodical: Periodical:

View File

@ -14,7 +14,7 @@ from collections import OrderedDict, defaultdict
from calibre.ebooks.mobi.writer2 import RECORD_SIZE from calibre.ebooks.mobi.writer2 import RECORD_SIZE
from calibre.ebooks.mobi.utils import (encint, encode_number_as_hex, 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): # {{{ class CNCX(object): # {{{
@ -323,16 +323,22 @@ class TBS(object): # {{{
class Indexer(object): # {{{ class Indexer(object): # {{{
def __init__(self, serializer, number_of_text_records, 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.serializer = serializer
self.number_of_text_records = number_of_text_records self.number_of_text_records = number_of_text_records
self.text_size = (RECORD_SIZE * (self.number_of_text_records-1) + self.text_size = (RECORD_SIZE * (self.number_of_text_records-1) +
size_of_last_text_record) size_of_last_text_record)
self.masthead_offset = masthead_offset
self.oeb = oeb self.oeb = oeb
self.log = oeb.log self.log = oeb.log
self.opts = opts 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.log('Generating MOBI index for a %s'%('periodical' if
self.is_periodical else 'book')) self.is_periodical else 'book'))
self.is_flat_periodical = False self.is_flat_periodical = False

View File

@ -11,7 +11,7 @@ import re, random, time
from cStringIO import StringIO from cStringIO import StringIO
from struct import pack 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.oeb.base import OEB_RASTER_IMAGES
from calibre.ebooks.mobi.writer2.serializer import Serializer from calibre.ebooks.mobi.writer2.serializer import Serializer
from calibre.ebooks.compression.palmdoc import compress_doc 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.utils.filenames import ascii_filename
from calibre.ebooks.mobi.writer2 import (PALMDOC, UNCOMPRESSED, RECORD_SIZE) from calibre.ebooks.mobi.writer2 import (PALMDOC, UNCOMPRESSED, RECORD_SIZE)
from calibre.ebooks.mobi.utils import (rescale_image, encint, 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 from calibre.ebooks.mobi.writer2.indexer import Indexer
EXTH_CODES = { EXTH_CODES = {
@ -35,6 +35,9 @@ EXTH_CODES = {
'type': 111, 'type': 111,
'source': 112, 'source': 112,
'versionnumber': 114, 'versionnumber': 114,
'coveroffset': 201,
'thumboffset': 202,
'hasfakecover': 203,
'lastupdatetime': 502, 'lastupdatetime': 502,
'title': 503, 'title': 503,
} }
@ -79,13 +82,14 @@ class MobiWriter(object):
self.write_content() self.write_content()
def generate_content(self): 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() self.generate_text()
# The uncrossable breaks trailing entries come before the indexing
# trailing entries
self.write_uncrossable_breaks()
# Index records come after text records # Index records come after text records
self.generate_index() self.generate_index()
self.write_uncrossable_breaks()
# Image records come after index records
self.generate_images()
# Indexing {{{ # Indexing {{{
def generate_index(self): def generate_index(self):
@ -93,6 +97,7 @@ class MobiWriter(object):
try: try:
self.indexer = Indexer(self.serializer, self.last_text_record_idx, self.indexer = Indexer(self.serializer, self.last_text_record_idx,
len(self.records[self.last_text_record_idx]), len(self.records[self.last_text_record_idx]),
self.masthead_offset, self.is_periodical,
self.opts, self.oeb) self.opts, self.oeb)
except: except:
self.log.exception('Failed to generate MOBI index:') self.log.exception('Failed to generate MOBI index:')
@ -104,11 +109,6 @@ class MobiWriter(object):
self.records[i] += encode_trailing_data(tbs) self.records[i] += encode_trailing_data(tbs)
self.records.extend(self.indexer.records) 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): # {{{ def write_uncrossable_breaks(self): # {{{
@ -138,58 +138,51 @@ class MobiWriter(object):
# }}} # }}}
# Images {{{ # 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): def generate_images(self):
self.oeb.logger.info('Serializing images...') oeb = self.oeb
images = [(index, href) for href, index in self.images.iteritems()] oeb.logger.info('Serializing images...')
images.sort() self.image_records = []
self.first_image_record = None
for _, href in images: mh_href = self.masthead_offset = None
item = self.oeb.manifest.hrefs[href] 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: try:
data = rescale_image(item.data) data = rescale_image(item.data)
except: except:
self.oeb.logger.warn('Bad image file %r' % item.href) oeb.logger.warn('Bad image file %r' % item.href)
continue 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: finally:
item.unload_data_from_memory() 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 +275,13 @@ class MobiWriter(object):
def generate_record0(self): # MOBI header {{{ def generate_record0(self): # MOBI header {{{
metadata = self.oeb.metadata metadata = self.oeb.metadata
exth = self.build_exth() 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 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) flis_number = len(self.records)
self.records.append( 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'+ 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 +360,7 @@ class MobiWriter(object):
# 0x58 - 0x5b : Format version # 0x58 - 0x5b : Format version
# 0x5c - 0x5f : First image record number # 0x5c - 0x5f : First image record number
record0.write(pack(b'>II', record0.write(pack(b'>II',
6, self.first_image_record if self.first_image_record else 6, first_image_record if first_image_record else len(self.records)))
len(self.records)-1))
# 0x60 - 0x63 : First HUFF/CDIC record number # 0x60 - 0x63 : First HUFF/CDIC record number
# 0x64 - 0x67 : Number of HUFF/CDIC records # 0x64 - 0x67 : Number of HUFF/CDIC records
@ -539,20 +535,15 @@ class MobiWriter(object):
exth.write(pack(b'>III', code, 12, val)) exth.write(pack(b'>III', code, 12, val))
nrecs += 1 nrecs += 1
if (oeb.metadata.cover and if self.cover_offset is not None:
unicode(oeb.metadata.cover[0]) in oeb.manifest.ids): exth.write(pack(b'>III', EXTH_CODES['coveroffset'], 12,
id = unicode(oeb.metadata.cover[0]) self.cover_offset))
item = oeb.manifest.ids[id] exth.write(pack(b'>III', EXTH_CODES['hasfakecover'], 12, 0))
href = item.href nrecs += 2
if href in self.images: if self.thumbnail_offset is not None:
index = self.images[href] - 1 exth.write(pack(b'>III', EXTH_CODES['thumboffset'], 12,
exth.write(pack(b'>III', 0xc9, 0x0c, index)) self.thumbnail_offset))
exth.write(pack(b'>III', 0xcb, 0x0c, 0)) nrecs += 1
nrecs += 2
index = self.add_thumbnail(item)
if index is not None:
exth.write(pack(b'>III', 0xca, 0x0c, index - 1))
nrecs += 1
exth = exth.getvalue() exth = exth.getvalue()
trail = len(exth) % 4 trail = len(exth) % 4

View File

@ -1,7 +1,8 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.Qt import (Qt, QDialog, QTableWidgetItem, QIcon, QByteArray, QString) from PyQt4.Qt import (Qt, QDialog, QTableWidgetItem, QIcon, QByteArray,
QString, QSize)
from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor
from calibre.gui2 import question_dialog, error_dialog, gprefs from calibre.gui2 import question_dialog, error_dialog, gprefs
@ -84,8 +85,6 @@ class TagListEditor(QDialog, Ui_TagListEditor):
try: try:
self.table_column_widths = \ self.table_column_widths = \
gprefs.get('tag_list_editor_table_widths', None) gprefs.get('tag_list_editor_table_widths', None)
geom = gprefs.get('tag_list_editor_dialog_geometry', bytearray(''))
self.restoreGeometry(QByteArray(geom))
except: except:
pass pass
@ -147,6 +146,15 @@ class TagListEditor(QDialog, Ui_TagListEditor):
self.table.itemChanged.connect(self.finish_editing) self.table.itemChanged.connect(self.finish_editing)
self.buttonBox.accepted.connect(self.accepted) self.buttonBox.accepted.connect(self.accepted)
try:
geom = gprefs.get('tag_list_editor_dialog_geometry', None)
if geom is not None:
self.restoreGeometry(QByteArray(geom))
else:
self.resize(self.sizeHint()+QSize(150, 100))
except:
pass
def table_column_resized(self, col, old, new): def table_column_resized(self, col, old, new):
self.table_column_widths = [] self.table_column_widths = []
for c in range(0, self.table.columnCount()): for c in range(0, self.table.columnCount()):

View File

@ -951,7 +951,7 @@ msgstr "Araztu saioa"
#: /home/kovid/work/calibre/src/calibre/devices/android/driver.py:13 #: /home/kovid/work/calibre/src/calibre/devices/android/driver.py:13
msgid "Communicate with Android phones." msgid "Communicate with Android phones."
msgstr "Adroid telefonoekin komunikatu." msgstr "Android telefonoekin komunikatu."
#: /home/kovid/work/calibre/src/calibre/devices/android/driver.py:113 #: /home/kovid/work/calibre/src/calibre/devices/android/driver.py:113
msgid "" msgid ""

View File

@ -1083,40 +1083,9 @@ class BasicNewsRecipe(Recipe):
MI_HEIGHT = 60 MI_HEIGHT = 60
def default_masthead_image(self, out_path): def default_masthead_image(self, out_path):
from calibre.ebooks.conversion.config import load_defaults from calibre.ebooks import generate_masthead
from calibre.utils.fonts import fontconfig generate_masthead(self.get_masthead_title(), output_path=out_path,
font_path = default_font = P('fonts/liberation/LiberationSerif-Bold.ttf') width=self.MI_WIDTH, height=self.MI_HEIGHT)
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')
def prepare_masthead_image(self, path_to_image, out_path): def prepare_masthead_image(self, path_to_image, out_path):
from calibre import fit_image from calibre import fit_image