%PDF- %PDF-
| Direktori : /usr/lib/calibre/calibre/ebooks/mobi/writer8/ |
| Current File : //usr/lib/calibre/calibre/ebooks/mobi/writer8/mobi.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import time, random
from struct import pack
from calibre.ebooks.mobi.utils import RECORD_SIZE, utf8_text
from calibre.ebooks.mobi.writer8.header import Header
from calibre.ebooks.mobi.writer2 import (PALMDOC, UNCOMPRESSED)
from calibre.ebooks.mobi.langcodes import iana2mobi
from calibre.ebooks.mobi.writer8.exth import build_exth
from calibre.utils.filenames import ascii_filename
NULL_INDEX = 0xffffffff
FLIS = 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'\xff'*4
def fcis(text_length):
fcis = b'FCIS\x00\x00\x00\x14\x00\x00\x00\x10\x00\x00\x00\x02\x00\x00\x00\x00'
fcis += pack(b'>L', text_length)
fcis += b'\x00\x00\x00\x00\x00\x00\x00\x28\x00\x00\x00\x00\x00\x00\x00'
fcis += b'\x28\x00\x00\x00\x08\x00\x01\x00\x01\x00\x00\x00\x00'
return fcis
class MOBIHeader(Header): # {{{
'''
Represents the first record in a MOBI file, contains all the metadata about
the file.
'''
DEFINITION = '''
# 0: Compression
compression = DYN
# 2: Unused
unused1 = zeroes(2)
# 4: Text length
text_length = DYN
# 8: Last text record
last_text_record = DYN
# 10: Text record size
record_size = {record_size}
# 12: Encryption Type
encryption_type
# 14: Unused
unused2
# 16: Ident
ident = b'MOBI'
# 20: Header length
header_length = 264
# 24: Book Type (0x2 - Book, 0x101 - News hierarchical, 0x102 - News
# (flat), 0x103 - News magazine same as 0x101)
book_type = DYN
# 28: Text encoding (utf-8 = 65001)
encoding = 65001
# 32: UID
uid = DYN
# 36: File version
file_version = {file_version}
# 40: Meta orth record (used in dictionaries)
meta_orth_record = NULL
# 44: Meta infl index
meta_infl_index = NULL
# 48: Extra indices
extra_index0 = NULL
extra_index1 = NULL
extra_index2 = NULL
extra_index3 = NULL
extra_index4 = NULL
extra_index5 = NULL
extra_index6 = NULL
extra_index7 = NULL
# 80: First non text record
first_non_text_record = DYN
# 84: Title offset
title_offset
# 88: Title Length
title_length = DYN
# 92: Language code
language_code = DYN
# 96: Dictionary in and out languages
in_lang
out_lang
# 104: Min version
min_version = {file_version}
# 108: First resource record
first_resource_record = DYN
# 112: Huff/CDIC compression
huff_first_record
huff_count
huff_table_offset = zeroes(4)
huff_table_length = zeroes(4)
# 128: EXTH flags
exth_flags = DYN
# 132: Unknown
unknown = zeroes(32)
# 164: Unknown
unknown_index = NULL
# 168: DRM
drm_offset = NULL
drm_count
drm_size
drm_flags
# 184: Unknown
unknown2 = zeroes(8)
# 192: FDST
# In MOBI 6 the fdst record is instead two two byte fields storing the
# index of the first and last content records
fdst_record = DYN
fdst_count = DYN
# 200: FCIS
fcis_record = DYN
fcis_count = 1
# 208: FLIS
flis_record = DYN
flis_count = 1
# 216: Unknown
unknown3 = zeroes(8)
# 224: SRCS
srcs_record = NULL
srcs_count
# 232: Unknown
unknown4 = nulls(8)
# 240: Extra data flags
# 0b1 - extra multibyte bytes after text records
# 0b10 - TBS indexing data (only used in MOBI 6)
# 0b100 - uncrossable breaks only used in MOBI 6
extra_data_flags = DYN
# 244: KF8 Indices
ncx_index = DYN
chunk_index = DYN
skel_index = DYN
datp_index = NULL
guide_index = DYN
# 264: Unknown
unknown5 = nulls(4)
unknown6 = zeroes(4)
unknown7 = nulls(4)
unknown8 = zeroes(4)
# 280: EXTH
exth = DYN
# Full title
full_title = DYN
# Padding to allow amazon's DTP service to add data
padding = zeroes(8192)
'''
SHORT_FIELDS = {'compression', 'last_text_record', 'record_size',
'encryption_type', 'unused2'}
ALIGN = True
POSITIONS = {'title_offset':'full_title'}
def __init__(self, file_version=8):
self.DEFINITION = self.DEFINITION.format(file_version=file_version,
record_size=RECORD_SIZE)
super().__init__()
def format_value(self, name, val):
if name == 'compression':
val = PALMDOC if val else UNCOMPRESSED
return super().format_value(name, val)
# }}}
HEADER_FIELDS = {'compression', 'text_length', 'last_text_record', 'book_type',
'first_non_text_record', 'title_length', 'language_code',
'first_resource_record', 'exth_flags', 'fdst_record',
'fdst_count', 'ncx_index', 'chunk_index', 'skel_index',
'guide_index', 'exth', 'full_title', 'extra_data_flags',
'flis_record', 'fcis_record', 'uid'}
class KF8Book:
def __init__(self, writer, for_joint=False):
self.build_records(writer, for_joint)
self.used_images = writer.used_images
self.page_progression_direction = writer.oeb.spine.page_progression_direction
self.primary_writing_mode = writer.oeb.metadata.primary_writing_mode
if self.page_progression_direction == 'rtl' and not self.primary_writing_mode:
# Without this the Kindle renderer does not respect
# page_progression_direction
self.primary_writing_mode = 'horizontal-rl'
def build_records(self, writer, for_joint):
metadata = writer.oeb.metadata
# The text records
for x in ('last_text_record_idx', 'first_non_text_record_idx'):
setattr(self, x.rpartition('_')[0], getattr(writer, x))
self.records = writer.records
self.text_length = writer.text_length
# KF8 Indices
self.chunk_index = len(self.records)
self.records.extend(writer.chunk_records)
self.skel_index = len(self.records)
self.records.extend(writer.skel_records)
self.guide_index = NULL_INDEX
if writer.guide_records:
self.guide_index = len(self.records)
self.records.extend(writer.guide_records)
self.ncx_index = NULL_INDEX
if writer.ncx_records:
self.ncx_index = len(self.records)
self.records.extend(writer.ncx_records)
# Resources
resources = writer.resources
for x in ('cover_offset', 'thumbnail_offset', 'masthead_offset'):
setattr(self, x, getattr(resources, x))
self.first_resource_record = NULL_INDEX
before = len(self.records)
if resources.records:
self.first_resource_record = len(self.records)
if not for_joint:
resources.serialize(self.records, writer.used_images)
self.num_of_resources = len(self.records) - before
# FDST
self.fdst_count = writer.fdst_count
self.fdst_record = len(self.records)
self.records.extend(writer.fdst_records)
# FLIS/FCIS
self.flis_record = len(self.records)
self.records.append(FLIS)
self.fcis_record = len(self.records)
self.records.append(fcis(self.text_length))
# EOF
self.records.append(b'\xe9\x8e\r\n') # EOF record
# Miscellaneous header fields
self.compression = writer.compress
self.book_type = 0x101 if writer.opts.mobi_periodical else 2
self.full_title = utf8_text(str(metadata.title[0]))
self.title_length = len(self.full_title)
self.extra_data_flags = 0b1
if writer.has_tbs:
self.extra_data_flags |= 0b10
self.uid = random.randint(0, 0xffffffff)
self.language_code = iana2mobi(str(metadata.language[0]))
self.exth_flags = 0b1010000
if writer.opts.mobi_periodical:
self.exth_flags |= 0b1000
if resources.has_fonts:
self.exth_flags |= 0b1000000000000
self.opts = writer.opts
self.start_offset = writer.start_offset
self.metadata = metadata
self.kuc = 0 if len(resources.records) > 0 else None
@property
def record0(self):
''' We generate the EXTH header and record0 dynamically, to allow other
code to customize various values after build_records() has been
called'''
opts = self.opts
self.exth = build_exth(
self.metadata,
prefer_author_sort=opts.prefer_author_sort,
is_periodical=opts.mobi_periodical,
share_not_sync=opts.share_not_sync,
cover_offset=self.cover_offset,
thumbnail_offset=self.thumbnail_offset,
num_of_resources=self.num_of_resources,
kf8_unknown_count=self.kuc, be_kindlegen2=True,
start_offset=self.start_offset, mobi_doctype=self.book_type,
page_progression_direction=self.page_progression_direction,
primary_writing_mode=self.primary_writing_mode
)
kwargs = {field:getattr(self, field) for field in HEADER_FIELDS}
return MOBIHeader()(**kwargs)
def write(self, outpath):
records = [self.record0] + self.records[1:]
with open(outpath, 'wb') as f:
# Write PalmDB Header
title = ascii_filename(self.full_title.decode('utf-8')).replace(' ', '_')
if not isinstance(title, bytes):
title = title.encode('ascii')
title = title[:31]
title += (b'\0' * (32 - len(title)))
now = int(time.time())
nrecords = len(records)
f.write(title)
f.write(pack(b'>HHIIIIII', 0, 0, now, now, 0, 0, 0, 0))
f.write(b'BOOKMOBI')
f.write(pack(b'>IIH', (2*nrecords)-1, 0, nrecords))
offset = f.tell() + (8 * nrecords) + 2
for i, record in enumerate(records):
f.write(pack(b'>I', offset))
f.write(b'\0' + pack(b'>I', 2*i)[1:])
offset += len(record)
f.write(b'\0\0')
for rec in records:
f.write(rec)