mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-12-08 14:15:04 -05:00
1115 lines
38 KiB
Python
1115 lines
38 KiB
Python
## Copyright (C) 2007 Kovid Goyal kovid@kovidgoyal.net
|
|
## This program is free software; you can redistribute it and/or modify
|
|
## it under the terms of the GNU General Public License as published by
|
|
## the Free Software Foundation; either version 2 of the License, or
|
|
## (at your option) any later version.
|
|
##
|
|
## This program is distributed in the hope that it will be useful,
|
|
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
## GNU General Public License for more details.
|
|
##
|
|
## You should have received a copy of the GNU General Public License along
|
|
## with this program; if not, write to the Free Software Foundation, Inc.,
|
|
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
import struct, array, zlib, cStringIO, collections, re
|
|
from htmlentitydefs import name2codepoint
|
|
|
|
from libprs500.ebooks.lrf import LRFParseError
|
|
from libprs500.ebooks.lrf.tags import Tag
|
|
|
|
ruby_tags = {
|
|
0xF575: ['rubyAlignAndAdjust', 'W'],
|
|
0xF576: ['rubyoverhang', 'W', {0: 'none', 1:'auto'}],
|
|
0xF577: ['empdotsposition', 'W', {1: 'before', 2:'after'}],
|
|
0xF578: ['','parse_empdots'],
|
|
0xF579: ['emplineposition', 'W', {1: 'before', 2:'after'}],
|
|
0xF57A: ['emplinetype', 'W', {0: 'none', 0x10: 'solid', 0x20: 'dashed', 0x30: 'double', 0x40: 'dotted'}]
|
|
}
|
|
|
|
class LRFObject(object):
|
|
|
|
tag_map = {
|
|
0xF500: ['', ''],
|
|
0xF502: ['infoLink', 'D'],
|
|
0xF501: ['', ''],
|
|
}
|
|
|
|
@classmethod
|
|
def descramble_buffer(cls, buf, l, xorKey):
|
|
i = 0
|
|
a = array.array('B',buf)
|
|
while l>0:
|
|
a[i] ^= xorKey
|
|
i+=1
|
|
l-=1
|
|
return a.tostring()
|
|
|
|
@classmethod
|
|
def parse_empdots(self, tag, f):
|
|
self.refEmpDotsFont, self.empDotsFontName, self.empDotsCode = tag.contents
|
|
|
|
@staticmethod
|
|
def tag_to_val(h, obj, tag, stream):
|
|
if h[1] == 'D':
|
|
val = tag.dword
|
|
elif h[1] == 'W':
|
|
val = tag.word
|
|
elif h[1] == 'w':
|
|
val = tag.word
|
|
if val > 0x8000:
|
|
val -= 0x10000
|
|
elif h[1] == 'B':
|
|
val = tag.byte
|
|
elif h[1] == 'P':
|
|
val = tag.contents
|
|
elif h[1] != '':
|
|
val = getattr(obj, h[1])(tag, stream)
|
|
if len(h) > 2:
|
|
val = h[2](val) if callable(h[2]) else h[2][val]
|
|
return val
|
|
|
|
def __init__(self, document, stream, id, scramble_key, boundary):
|
|
self._scramble_key = scramble_key
|
|
self._document = document
|
|
self.id = id
|
|
|
|
while stream.tell() < boundary:
|
|
tag = Tag(stream)
|
|
self.handle_tag(tag, stream)
|
|
|
|
def parse_bg_image(self, tag, f):
|
|
self.bg_image_mode, self.bg_image_id = struct.unpack("<HI", tag.contents)
|
|
|
|
def handle_tag(self, tag, stream, tag_map=None):
|
|
if tag_map is None:
|
|
tag_map = self.__class__.tag_map
|
|
if tag.id in tag_map:
|
|
h = tag_map[tag.id]
|
|
val = LRFObject.tag_to_val(h, self, tag, stream)
|
|
if h[1] != '' and h[0] != '':
|
|
setattr(self, h[0], val)
|
|
else:
|
|
raise LRFParseError("Unknown tag in %s: %s" % (self.__class__.__name__, str(tag)))
|
|
|
|
def __iter__(self):
|
|
for i in range(0):
|
|
yield i
|
|
|
|
def __unicode__(self):
|
|
return unicode(self.__class__.__name__)
|
|
|
|
def __str__(self):
|
|
return unicode(self).encode('utf-8')
|
|
|
|
class LRFContentObject(LRFObject):
|
|
|
|
tag_map = {}
|
|
|
|
def __init__(self, bytes, objects):
|
|
self.stream = bytes if hasattr(bytes, 'read') else cStringIO.StringIO(bytes)
|
|
length = self.stream_size()
|
|
self.objects = objects
|
|
self._contents = []
|
|
self.current = 0
|
|
self.in_container = True
|
|
self.parse_stream(length)
|
|
|
|
def parse_stream(self, length):
|
|
while self.in_container and self.stream.tell() < length:
|
|
tag = Tag(self.stream)
|
|
self.handle_tag(tag)
|
|
|
|
def stream_size(self):
|
|
pos = self.stream.tell()
|
|
self.stream.seek(0, 2)
|
|
size = self.stream.tell()
|
|
self.stream.seek(pos)
|
|
return size
|
|
|
|
def handle_tag(self, tag):
|
|
if tag.id in self.tag_map:
|
|
action = self.tag_map[tag.id]
|
|
if isinstance(action, basestring):
|
|
func, args = action, tuple([])
|
|
else:
|
|
func, args = action[0], (action[1],)
|
|
getattr(self, func)(tag, *args)
|
|
else:
|
|
raise LRFParseError("Unknown tag in %s: %s" % (self.__class__.__name__, str(tag)))
|
|
|
|
def __iter__(self):
|
|
for i in self._contents:
|
|
yield i
|
|
|
|
|
|
|
|
|
|
class LRFStream(LRFObject):
|
|
tag_map = {
|
|
0xF504: ['', 'read_stream_size'],
|
|
0xF554: ['stream_flags', 'W'],
|
|
0xF505: ['', 'read_stream'],
|
|
0xF506: ['', 'end_stream'],
|
|
}
|
|
tag_map.update(LRFObject.tag_map)
|
|
|
|
def __init__(self, document, stream, id, scramble_key, boundary):
|
|
self.stream = ''
|
|
self.stream_size = 0
|
|
self.stream_read = False
|
|
LRFObject.__init__(self, document, stream, id, scramble_key, boundary)
|
|
|
|
def read_stream_size(self, tag, stream):
|
|
self.stream_size = tag.dword
|
|
|
|
def end_stream(self, tag, stream):
|
|
self.stream_read = True
|
|
|
|
def read_stream(self, tag, stream):
|
|
if self.stream_read:
|
|
raise LRFParseError('There can be only one stream per object')
|
|
if not hasattr(self, 'stream_flags'):
|
|
raise LRFParseError('Stream flags not initialized')
|
|
self.stream = stream.read(self.stream_size)
|
|
if self.stream_flags & 0x200 !=0:
|
|
l = len(self.stream);
|
|
key = l % self._scramble_key + 0xF;
|
|
if l > 0x400 and (isinstance(self, ImageStream) or isinstance(self, Font) or isinstance(self, SoundStream)):
|
|
l = 0x400;
|
|
self.stream = self.descramble_buffer(self.stream, l, key)
|
|
if self.stream_flags & 0x100 !=0:
|
|
decomp_size = struct.unpack("<I", self.stream[:4])[0]
|
|
self.stream = zlib.decompress(self.stream[4:])
|
|
if len(self.stream) != decomp_size:
|
|
raise LRFParseError("Stream decompressed size is wrong!")
|
|
if stream.read(2) != '\x06\xF5':
|
|
print "Warning: corrupted end-of-stream tag at %08X; skipping it"%stream.tell()-2
|
|
self.end_stream(None, None)
|
|
|
|
|
|
class PageTree(LRFObject):
|
|
tag_map = {
|
|
0xF55C: ['_contents', 'P'],
|
|
}
|
|
tag_map.update(LRFObject.tag_map)
|
|
|
|
def __iter__(self):
|
|
for id in self._contents:
|
|
yield self._document.objects[id]
|
|
|
|
class StyleObject(object):
|
|
|
|
def _tags_to_xml(self):
|
|
s = u''
|
|
for h in self.tag_map.values():
|
|
attr = h[0]
|
|
if hasattr(self, attr):
|
|
s += u'%s="%s" '%(attr, getattr(self, attr))
|
|
return s
|
|
|
|
def __unicode__(self):
|
|
s = u'<%s objid="%s" stylelabel="%s" '%(self.__class__.__name__.replace('Attr', 'Style'), self.id, self.id)
|
|
s += self._tags_to_xml()
|
|
s += u'/>\n'
|
|
return s
|
|
|
|
def as_dict(self):
|
|
d = {}
|
|
for h in self.tag_map.values():
|
|
attr = h[0]
|
|
if hasattr(self, attr):
|
|
d[attr] = getattr(self, attr)
|
|
return d
|
|
|
|
class PageAttr(StyleObject, LRFObject):
|
|
tag_map = {
|
|
0xF507: ['oddheaderid', 'D'],
|
|
0xF508: ['evenheaderid', 'D'],
|
|
0xF509: ['oddfooterid', 'D'],
|
|
0xF50A: ['evenfooterid', 'D'],
|
|
0xF521: ['topmargin', 'W'],
|
|
0xF522: ['headheight', 'W'],
|
|
0xF523: ['headsep', 'W'],
|
|
0xF524: ['oddsidemargin', 'W'],
|
|
0xF52C: ['evensidemargin', 'W'],
|
|
0xF525: ['textheight', 'W'],
|
|
0xF526: ['textwidth', 'W'],
|
|
0xF527: ['footspace', 'W'],
|
|
0xF528: ['footheight', 'W'],
|
|
0xF535: ['layout', 'W', {0x41: 'TbRl', 0x34: 'LrTb'}],
|
|
0xF52B: ['pageposition', 'W', {0: 'any', 1:'upper', 2: 'lower'}],
|
|
0xF52A: ['setemptyview', 'W', {1: 'show', 0: 'empty'}],
|
|
0xF5DA: ['setwaitprop', 'W', {1: 'replay', 2: 'noreplay'}],
|
|
0xF529: ['', "parse_bg_image"],
|
|
}
|
|
tag_map.update(LRFObject.tag_map)
|
|
|
|
|
|
class Color(object):
|
|
def __init__(self, val):
|
|
self.a, self.r, self.g, self.b = val & 0xFF, (val>>8)&0xFF, (val>>16)&0xFF, (val>>24)&0xFF
|
|
|
|
def __unicode__(self):
|
|
return u'0x%02x%02x%02x%02x'%(self.a, self.r, self.g, self.b)
|
|
|
|
def __str__(self):
|
|
return unicode(self)
|
|
|
|
def __len__(self):
|
|
return 4
|
|
|
|
def __getitem__(self, i): # Qt compatible ordering and values
|
|
return (self.r, self.g, self.b, 0xff-self.a)[i] # In Qt 0xff is opaque while in LRS 0x00 is opaque
|
|
|
|
|
|
class EmptyPageElement(object):
|
|
def __iter__(self):
|
|
for i in range(0):
|
|
yield i
|
|
|
|
def __str__(self):
|
|
return unicode(self)
|
|
|
|
class PageDiv(EmptyPageElement):
|
|
|
|
def __init__(self, pain, spacesize, linewidth, linecolor):
|
|
self.pain, self.spacesize, self.linewidth = pain, spacesize, linewidth
|
|
self.linecolor = Color(linecolor)
|
|
|
|
def __unicode__(self):
|
|
return u'\n<PageDiv pain="%s" spacesize="%s" linewidth="%s" linecolor="%s" />\n'%\
|
|
(self.pain, self.spacesize, self.linewidth, self.color)
|
|
|
|
|
|
class RuledLine(EmptyPageElement):
|
|
|
|
linetype_map = {0x00: 'none', 0x10: 'solid', 0x20: 'dashed', 0x30: 'double', 0x40: 'dotted', 0x13: 'unknown13'}
|
|
|
|
def __init__(self, linelength, linetype, linewidth, linecolor):
|
|
self.linelength, self.linewidth = linelength, linewidth
|
|
self.linetype = self.linetype_map[linetype]
|
|
self.linecolor = Color(linecolor)
|
|
self.id = -1
|
|
|
|
def __unicode__(self):
|
|
return u'\n<RuledLine linelength="%s" linetype="%s" linewidth="%s" linecolor="%s" />\n'%\
|
|
(self.linelength, self.linetype, self.linewidth, self.linecolor)
|
|
|
|
class Wait(EmptyPageElement):
|
|
|
|
def __init__(self, time):
|
|
self.time = time
|
|
|
|
def __unicode__(self):
|
|
return u'\n<Wait time="%d" />\n'%(self.time)
|
|
|
|
class Locate(EmptyPageElement):
|
|
|
|
pos_map = {1:'bottomleft', 2:'bottomright',3:'topright',4:'topleft', 5:'base'}
|
|
|
|
def __init__(self, pos):
|
|
print pos
|
|
self.pos = self.pos_map[pos]
|
|
|
|
def __unicode__(self):
|
|
return u'\n<Locate pos="%s" />\n'%(self.pos)
|
|
|
|
class BlockSpace(EmptyPageElement):
|
|
|
|
def __init__(self, xspace, yspace):
|
|
self.xspace, self.yspace = xspace, yspace
|
|
|
|
def __unicode__(self):
|
|
return u'\n<BlockSpace xspace="%d" yspace="%d" />\n'%\
|
|
(self.xspace, self.yspace)
|
|
|
|
class Page(LRFStream):
|
|
tag_map = {
|
|
0xF503: ['style_id', 'D'],
|
|
0xF50B: ['obj_list', 'P'],
|
|
0xF571: ['', ''],
|
|
0xF57C: ['parent_page_tree','D'],
|
|
}
|
|
tag_map.update(PageAttr.tag_map)
|
|
tag_map.update(LRFStream.tag_map)
|
|
style = property(fget=lambda self : self._document.objects[self.style_id])
|
|
evenheader = property(fget=lambda self : self._document.objects[self.style.evenheaderid])
|
|
evenfooter = property(fget=lambda self : self._document.objects[self.style.evenfooterid])
|
|
oddheader = property(fget=lambda self : self._document.objects[self.style.oddheaderid])
|
|
oddfooter = property(fget=lambda self : self._document.objects[self.style.oddfooterid])
|
|
|
|
class Content(LRFContentObject):
|
|
tag_map = {
|
|
0xF503: 'link',
|
|
0xF54E: 'page_div',
|
|
0xF547: 'x_space',
|
|
0xF546: 'y_space',
|
|
0xF548: 'do_pos',
|
|
0xF573: 'ruled_line',
|
|
0xF5D4: 'wait',
|
|
0xF5D6: 'sound_stop',
|
|
}
|
|
|
|
def __init__(self, bytes, objects):
|
|
self.in_blockspace = False
|
|
LRFContentObject.__init__(self, bytes, objects)
|
|
|
|
def link(self, tag):
|
|
self.close_blockspace()
|
|
self._contents.append(self.objects[tag.dword])
|
|
|
|
def page_div(self, tag):
|
|
self.close_blockspace()
|
|
pars = struct.unpack("<HIHI", tag.contents)
|
|
self._contents.append(PageDiv(*pars))
|
|
|
|
def x_space(self, tag):
|
|
self.xspace = tag.word
|
|
self.in_blockspace = True
|
|
|
|
def y_space(self, tag):
|
|
self.yspace = tag.word
|
|
self.in_blockspace = True
|
|
|
|
def do_pos(self, tag):
|
|
self.pos = tag.wordself.pos_map[tag.word]
|
|
self.in_blockspace = True
|
|
|
|
def ruled_line(self, tag):
|
|
self.close_blockspace()
|
|
pars = struct.unpack("<HHHI", tag.contents)
|
|
self._contents.append(RuledLine(*pars))
|
|
|
|
def wait(self, tag):
|
|
self.close_blockspace()
|
|
self._contents.append(Wait(tag.word))
|
|
|
|
def sound_stop(self, tag):
|
|
self.close_blockspace()
|
|
|
|
def close_blockspace(self):
|
|
if self.in_blockspace:
|
|
if hasattr(self, 'pos'):
|
|
self._contents.append(Locate(self.pos))
|
|
delattr(self, 'pos')
|
|
else:
|
|
xspace = self.xspace if hasattr(self, 'xspace') else 0
|
|
yspace = self.yspace if hasattr(self, 'yspace') else 0
|
|
self._contents.append(BlockSpace(xspace, yspace))
|
|
if hasattr(self, 'xspace'): delattr(self, 'xspace')
|
|
if hasattr(self, 'yspace'): delattr(self, 'yspace')
|
|
|
|
def header(self, odd):
|
|
id = self._document.objects[self.style_id].oddheaderid if odd else self._document.objects[self.style_id].evenheaderid
|
|
return self._document.objects[id]
|
|
|
|
def footer(self, odd):
|
|
id = self._document.objects[self.style_id].oddfooterid if odd else self._document.objects[self.style_id].evenfooterid
|
|
return self._document.objects[id]
|
|
|
|
def initialize(self):
|
|
self.content = Page.Content(self.stream, self._document.objects)
|
|
|
|
def __iter__(self):
|
|
for i in self.content:
|
|
yield i
|
|
|
|
def __unicode__(self):
|
|
s = u'\n<Page pagestyle="%d" objid="%d">\n'%(self.style_id, self.id)
|
|
for i in self:
|
|
s += unicode(i)
|
|
s += '\n</Page>\n'
|
|
return s
|
|
|
|
def __str__(self):
|
|
return unicode(self)
|
|
|
|
|
|
|
|
class BlockAttr(StyleObject, LRFObject):
|
|
tag_map = {
|
|
0xF531: ['blockwidth', 'W'],
|
|
0xF532: ['blockheight', 'W'],
|
|
0xF533: ['blockrule', 'W', {0x14: "horz-fixed", 0x12: "horz-adjustable", 0x41: "vert-fixed", 0x21: "vert-adjustable", 0x44: "block-fixed", 0x22: "block-adjustable"}],
|
|
0xF534: ['bgcolor', 'D', Color],
|
|
0xF535: ['layout', 'W', {0x41: 'TbRl', 0x34: 'LrTb'}],
|
|
0xF536: ['framewidth', 'W'],
|
|
0xF537: ['framecolor', 'D', Color],
|
|
0xF52E: ['framemode', 'W', {0: 'none', 2: 'curve', 1:'square'}],
|
|
0xF538: ['topskip', 'W'],
|
|
0xF539: ['sidemargin', 'W'],
|
|
0xF53A: ['footskip', 'W'],
|
|
0xF529: ['', 'parse_bg_image'],
|
|
}
|
|
tag_map.update(LRFObject.tag_map)
|
|
|
|
class TextAttr(StyleObject, LRFObject):
|
|
tag_map = {
|
|
0xF511: ['fontsize', 'w'],
|
|
0xF512: ['fontwidth', 'w'],
|
|
0xF513: ['fontescapement', 'w'],
|
|
0xF514: ['fontorientation', 'w'],
|
|
0xF515: ['fontweight', 'W'],
|
|
0xF516: ['fontfacename', 'P'],
|
|
0xF517: ['textcolor', 'D', Color],
|
|
0xF518: ['textbgcolor', 'D', Color],
|
|
0xF519: ['wordspace', 'w'],
|
|
0xF51A: ['letterspace', 'w'],
|
|
0xF51B: ['baselineskip', 'w'],
|
|
0xF51C: ['linespace', 'w'],
|
|
0xF51D: ['parindent', 'w'],
|
|
0xF51E: ['parskip', 'w'],
|
|
0xF53C: ['align', 'W', {1: 'head', 4: 'center', 8: 'foot'}],
|
|
0xF53D: ['column', 'W'],
|
|
0xF53E: ['columnsep', 'W'],
|
|
0xF5DD: ['charspace', 'w'],
|
|
0xF5F1: ['textlinewidth', 'W'],
|
|
0xF5F2: ['linecolor', 'D', Color],
|
|
}
|
|
tag_map.update(ruby_tags)
|
|
tag_map.update(LRFObject.tag_map)
|
|
|
|
|
|
class Block(LRFStream):
|
|
tag_map = {
|
|
0xF503: ['style_id', 'D'],
|
|
}
|
|
tag_map.update(BlockAttr.tag_map)
|
|
tag_map.update(TextAttr.tag_map)
|
|
tag_map.update(LRFStream.tag_map)
|
|
extra_attrs = [i[0] for i in BlockAttr.tag_map.values()]
|
|
extra_attrs.extend([i[0] for i in TextAttr.tag_map.values()])
|
|
|
|
style = property(fget=lambda self : self._document.objects[self.style_id])
|
|
textstyle = property(fget=lambda self : self._document.objects[self.textstyle_id])
|
|
|
|
def initialize(self):
|
|
self.attrs = {}
|
|
stream = cStringIO.StringIO(self.stream)
|
|
tag = Tag(stream)
|
|
if tag.id != 0xF503:
|
|
raise LRFParseError("Bad block content")
|
|
obj = self._document.objects[tag.dword]
|
|
if isinstance(obj, SimpleText):
|
|
self.name = 'SimpleTextBlock'
|
|
self.textstyle_id = obj.style_id
|
|
elif isinstance(obj, Text):
|
|
self.name = 'TextBlock'
|
|
self.textstyle_id = obj.style_id
|
|
elif isinstance(obj, Image):
|
|
self.name = 'ImageBlock'
|
|
for attr in ('x0', 'x1', 'y0', 'y1', 'xsize', 'ysize', 'refstream'):
|
|
self.attrs[attr] = getattr(obj, attr)
|
|
self.refstream = self._document.objects[self.attrs['refstream']]
|
|
elif isinstance(obj, Button):
|
|
self.name = 'ButtonBlock'
|
|
else:
|
|
raise LRFParseError("Unexpected block type: "+obj.__class__.__name__)
|
|
|
|
self.content = obj
|
|
|
|
|
|
for attr in self.extra_attrs:
|
|
if hasattr(self, attr):
|
|
self.attrs[attr] = getattr(self, attr)
|
|
|
|
def __unicode__(self):
|
|
s = u'\n<%s objid="%d" blockstyle="%d" '%(self.name, self.id, self.style_id)
|
|
if hasattr(self, 'textstyle_id'):
|
|
s += 'textstyle="%d" '%(self.textstyle_id,)
|
|
for attr in self.attrs:
|
|
s += '%s="%s" '%(attr, self.attrs[attr])
|
|
s = s.rstrip()+'>\n'
|
|
s += unicode(self.content)
|
|
s += '</%s>\n'%(self.name,)
|
|
return s
|
|
|
|
|
|
class MiniPage(LRFStream):
|
|
tag_map = {
|
|
0xF541: ['minipagewidth', 'W'],
|
|
0xF542: ['minipageheight', 'W'],
|
|
}
|
|
tag_map.update(LRFStream.tag_map)
|
|
tag_map.update(BlockAttr.tag_map)
|
|
|
|
|
|
|
|
class Text(LRFStream):
|
|
tag_map = {
|
|
0xF503: ['style_id', 'D'],
|
|
}
|
|
tag_map.update(TextAttr.tag_map)
|
|
tag_map.update(LRFStream.tag_map)
|
|
|
|
style = property(fget=lambda self : self._document.objects[self.style_id])
|
|
|
|
text_map = { 0x22: u'"', 0x26: u'&', 0x27: u'&squot;', 0x3c: u'<', 0x3e: u'>' }
|
|
entity_pattern = re.compile(r'&(\S+?);')
|
|
|
|
text_tags = {
|
|
0xF581: ['simple_container', 'Italic'],
|
|
0xF582: 'end_container',
|
|
0xF5B1: ['simple_container', 'Yoko'],
|
|
0xF5B2: 'end_container',
|
|
0xF5B3: ['simple_container', 'Tate'],
|
|
0xF5B4: 'end_container',
|
|
0xF5B5: ['simple_container', 'Nekase'],
|
|
0xF5B6: 'end_container',
|
|
0xF5A1: 'start_para',
|
|
0xF5A2: 'end_para',
|
|
0xF5A7: 'char_button',
|
|
0xF5A8: 'end_container',
|
|
0xF5A9: ['simple_container', 'Rubi'],
|
|
0xF5AA: 'end_container',
|
|
0xF5AB: ['simple_container', 'Oyamoji'],
|
|
0xF5AC: 'end_container',
|
|
0xF5AD: ['simple_container', 'Rubimoji'],
|
|
0xF5AE: 'end_container',
|
|
0xF5B7: ['simple_container', 'Sup'],
|
|
0xF5B8: 'end_container',
|
|
0xF5B9: ['simple_container', 'Sub'],
|
|
0xF5BA: 'end_container',
|
|
0xF5BB: ['simple_container', 'NoBR'],
|
|
0xF5BC: 'end_container',
|
|
0xF5BD: ['simple_container', 'EmpDots'],
|
|
0xF5BE: 'end_container',
|
|
0xF5C1: 'empline',
|
|
0xF5C2: 'end_container',
|
|
0xF5C3: 'draw_char',
|
|
0xF5C4: 'end_container',
|
|
0xF5C6: 'box',
|
|
0xF5C7: 'end_container',
|
|
0xF5CA: 'space',
|
|
0xF5D1: 'plot',
|
|
0xF5D2: 'cr',
|
|
}
|
|
|
|
class TextTag(object):
|
|
|
|
def __init__(self, name, attrs={}, self_closing=False):
|
|
self.name = name
|
|
self.attrs = attrs
|
|
self.self_closing = self_closing
|
|
|
|
def __unicode__(self):
|
|
s = u'<%s '%(self.name,)
|
|
for name, val in self.attrs.items():
|
|
s += '%s="%s" '%(name, val)
|
|
return s.rstrip() + (u' />' if self.self_closing else u'>') + (u'\n' if self.name in ('P', 'CR') else u'')
|
|
|
|
class Span(TextTag):
|
|
pass
|
|
|
|
linetype_map = {0: 'none', 0x10: 'solid', 0x20: 'dashed', 0x30: 'double', 0x40: 'dotted'}
|
|
adjustment_map = {1: 'top', 2: 'center', 3: 'baseline', 4: 'bottom'}
|
|
lineposition_map = {1:'before', 2:'after'}
|
|
|
|
|
|
def handle_entity(self, match):
|
|
ent = match.group(1)
|
|
if ent.startswith(u'#x'):
|
|
return unichr(int(ent[2:], 16))
|
|
if ent.startswith(u'#'):
|
|
return unichr(int(ent[1:]))
|
|
return unichr(name2codepoint[ent])
|
|
|
|
def add_text(self, text):
|
|
s = unicode(text, "utf-16-le")
|
|
if s:
|
|
s = s.translate(self.text_map)
|
|
self.content.append(self.entity_pattern.sub(self.handle_entity, s))
|
|
|
|
def end_container(self, tag, stream):
|
|
self.content.append(None)
|
|
|
|
def start_para(self, tag, stream):
|
|
self.content.append(self.__class__.TextTag('P'))
|
|
|
|
def close_containers(self, start=0):
|
|
open_containers = 0
|
|
if len(self.content) > 0 and isinstance(self.content[-1], self.__class__.Span):
|
|
self.content.pop()
|
|
while start < len(self.content):
|
|
c = self.content[start]
|
|
if c is None:
|
|
open_containers -= 1
|
|
elif isinstance(c, self.__class__.TextTag) and not c.self_closing:
|
|
open_containers += 1
|
|
start += 1
|
|
self.content.extend(None for i in range(open_containers))
|
|
|
|
|
|
def end_para(self, tag, stream):
|
|
i = len(self.content)-1
|
|
while i > -1:
|
|
if isinstance(self.content[i], Text.TextTag) and self.content[i].name == 'P':
|
|
break
|
|
i -= 1
|
|
self.close_containers(start=i)
|
|
|
|
def cr(self, tag, stream):
|
|
self.content.append(self.__class__.TextTag('CR', self_closing=True))
|
|
|
|
def char_button(self, tag, stream):
|
|
self.content.append(self.__class__.TextTag(
|
|
'CharButton', attrs={'refobj':tag.dword}))
|
|
|
|
def simple_container(self, tag, name):
|
|
self.content.append(self.__class__.TextTag(name))
|
|
|
|
def empline(self, tag, stream):
|
|
|
|
def invalid(op):
|
|
stream.seek(op)
|
|
self.simple_container(None, 'EmpLine')
|
|
|
|
oldpos = stream.tell()
|
|
try:
|
|
t = Tag(stream)
|
|
if t.id not in [0xF579, 0xF57A]:
|
|
raise LRFParseError
|
|
except LRFParseError:
|
|
invalid(oldpos)
|
|
return
|
|
h = TextAttr.tag_map[t.id]
|
|
attrs = {}
|
|
attrs[h[0]] = TextAttr.tag_to_val(h, None, t, None)
|
|
oldpos = stream.tell()
|
|
try:
|
|
t = Tag(stream)
|
|
if t.id not in [0xF579, 0xF57A]:
|
|
raise LRFParseError
|
|
h = TextAttr.tag_map[t.id]
|
|
attrs[h[0]] = TextAttr.tag_to_val(h, None, t, None)
|
|
except LRFParseError:
|
|
stream.seek(oldpos)
|
|
|
|
self.content.append(self.__class__.TextTag(
|
|
'EmpLine', attrs=attrs))
|
|
|
|
def space(self, tag, stream):
|
|
self.content.append(self.__class__.TextTag('Space',
|
|
attrs={'xsize':tag.sword},
|
|
self_closing=True))
|
|
|
|
def plot(self, tag, stream):
|
|
xsize, ysize, refobj, adjustment = struct.unpack("<HHII", tag.contents)
|
|
plot = self.__class__.TextTag('Plot',
|
|
{'xsize': xsize, 'ysize': ysize, 'refobj':refobj,
|
|
'adjustment':self.adjustment_map[adjustment]}, self_closing=True)
|
|
plot.refobj = self._document.objects[refobj]
|
|
self.content.append(plot)
|
|
|
|
def draw_char(self, tag, stream):
|
|
self.content.append(self.__class__.TextTag('DrawChar', {'line':tag.word}))
|
|
|
|
def box(self, tag, stream):
|
|
self.content.append(self.__class__.TextTag('Box',
|
|
{'linetype':self.linetype_map[tag.word]}))
|
|
|
|
def initialize(self):
|
|
self.content = collections.deque()
|
|
stream = cStringIO.StringIO(self.stream)
|
|
length = len(self.stream)
|
|
style = self.style.as_dict()
|
|
current_style = style.copy()
|
|
|
|
while stream.tell() < length:
|
|
|
|
# Is there some text beofre a tag?
|
|
pos = self.stream.find('\xf5', stream.tell()) - 1
|
|
if pos > 0:
|
|
self.add_text(self.stream[stream.tell():pos])
|
|
stream.seek(pos)
|
|
|
|
tag = Tag(stream)
|
|
|
|
if tag.id == 0xF5CC:
|
|
self.add_text(stream.read(tag.word))
|
|
elif tag.id in self.__class__.text_tags: # A Text tag
|
|
action = self.__class__.text_tags[tag.id]
|
|
if isinstance(action, basestring):
|
|
getattr(self, action)(tag, stream)
|
|
else:
|
|
getattr(self, action[0])(tag, action[1])
|
|
elif tag.id in TextAttr.tag_map: # A Span attribute
|
|
action = TextAttr.tag_map[tag.id]
|
|
if len(self.content) == 0:
|
|
current_style = style.copy()
|
|
name, val = action[0], LRFObject.tag_to_val(action, None, tag, None)
|
|
if current_style[name] != val:
|
|
# No existing Span
|
|
if len(self.content) > 0 and isinstance(self.content[-1], self.__class__.Span):
|
|
self.content[-1].attrs[name] = val
|
|
else:
|
|
self.content.append(self.__class__.Span('Span', {name:val}))
|
|
current_style[name] = val
|
|
if len(self.content) > 0:
|
|
self.close_containers()
|
|
self.stream = None
|
|
|
|
def __unicode__(self):
|
|
s = u''
|
|
open_containers = collections.deque()
|
|
for c in self.content:
|
|
if isinstance(c, basestring):
|
|
s += c
|
|
elif c is None:
|
|
p = open_containers.pop()
|
|
nl = u'\n' if p.name == 'P' else u''
|
|
s += nl + u'</%s>'%(p.name,) + nl
|
|
else:
|
|
s += unicode(c)
|
|
if not c.self_closing:
|
|
open_containers.append(c)
|
|
|
|
if len(open_containers) > 0:
|
|
raise LRFParseError('Malformed text stream %s'%([i.name for i in open_containers if isinstance(i, Text.TextTag)],))
|
|
return s
|
|
|
|
|
|
class Image(LRFObject):
|
|
tag_map = {
|
|
0xF54A: ['', 'parse_image_rect'],
|
|
0xF54B: ['', 'parse_image_size'],
|
|
0xF54C: ['refstream', 'D'],
|
|
0xF555: ['comment', 'P'],
|
|
}
|
|
|
|
def parse_image_rect(self, tag, f):
|
|
self.x0, self.y0, self.x1, self.y1 = struct.unpack("<HHHH", tag.contents)
|
|
|
|
def parse_image_size(self, tag, f):
|
|
self.xsize, self.ysize = struct.unpack("<HH", tag.contents)
|
|
|
|
encoding = property(fget=lambda self : self._document.objects[self.refstream].encoding)
|
|
data = property(fget=lambda self : self._document.objects[self.refstream].stream)
|
|
|
|
|
|
def __unicode__(self):
|
|
return u'<Image objid="%s" x0="%d" y0="%d" x1="%d" y1="%d" xsize="%d" ysize="%d" refstream="%d" />\n'%\
|
|
(self.id, self.x0, self.y0, self.x1, self.y1, self.xsize, self.ysize, self.refstream)
|
|
|
|
class PutObj(EmptyPageElement):
|
|
|
|
def __init__(self, objects, x, y, refobj):
|
|
self.x, self.y, self.refobj = x, y, refobj
|
|
self.object = objects[refobj]
|
|
|
|
def __unicode__(self):
|
|
return u'<PutObj x="%d" y="%d" refobj="%d" />'%(self.x, self.y, self.refobj)
|
|
|
|
class Canvas(LRFStream):
|
|
tag_map = {
|
|
0xF551: ['canvaswidth', 'W'],
|
|
0xF552: ['canvasheight', 'W'],
|
|
0xF5DA: ['', 'parse_waits'],
|
|
0xF533: ['blockrule', 'W', {0x44: "block-fixed", 0x22: "block-adjustable"}],
|
|
0xF534: ['bgcolor', 'D', Color],
|
|
0xF535: ['layout', 'W', {0x41: 'TbRl', 0x34: 'LrTb'}],
|
|
0xF536: ['framewidth', 'W'],
|
|
0xF537: ['framecolor', 'D', Color],
|
|
0xF52E: ['framemode', 'W', {0: 'none', 2: 'curve', 1:'square'}],
|
|
}
|
|
tag_map.update(LRFStream.tag_map)
|
|
extra_attrs = ['canvaswidth', 'canvasheight', 'blockrule', 'layout',
|
|
'framewidth', 'framecolor', 'framemode']
|
|
|
|
def parse_waits(self, tag, f):
|
|
val = tag.word
|
|
self.setwaitprop = val&0xF
|
|
self.setwaitsync = val&0xF0
|
|
|
|
def initialize(self):
|
|
self.attrs = {}
|
|
for attr in self.extra_attrs:
|
|
if hasattr(self, attr):
|
|
self.attrs[attr] = getattr(self, attr)
|
|
self._contents = []
|
|
stream = cStringIO.StringIO(self.stream)
|
|
while stream.tell() < len(self.stream):
|
|
tag = Tag(stream)
|
|
self._contents.append(PutObj(self._document.objects, *struct.unpack("<HHI", tag.contents)))
|
|
|
|
def __unicode__(self):
|
|
s = '\n<%s objid="%s" '%(self.__class__.__name__, self.id,)
|
|
for attr in self.attrs:
|
|
s += '%s="%s" '%(attr, self.attrs[attr])
|
|
s = s.rstrip() + '>\n'
|
|
for po in self:
|
|
s += unicode(po) + '\n'
|
|
s += '</%s>\n'%(self.__class__.__name__,)
|
|
return s
|
|
|
|
def __iter__(self):
|
|
for i in self._contents:
|
|
yield i
|
|
|
|
class Header(Canvas):
|
|
pass
|
|
|
|
class Footer(Canvas):
|
|
pass
|
|
|
|
|
|
class ESound(LRFObject):
|
|
pass
|
|
|
|
class ImageStream(LRFStream):
|
|
tag_map = {
|
|
0xF555: ['comment', 'P'],
|
|
}
|
|
imgext = {0x11: 'jpeg', 0x12: 'png', 0x13: 'bmp', 0x14: 'gif'}
|
|
|
|
tag_map.update(LRFStream.tag_map)
|
|
|
|
encoding = property(fget=lambda self : self.imgext[self.stream_flags & 0xFF].upper())
|
|
|
|
def end_stream(self, *args):
|
|
LRFStream.end_stream(self, *args)
|
|
self.file = str(self.id) + '.' + self.encoding.lower()
|
|
self._document.image_map[self.id] = self
|
|
|
|
def __unicode__(self):
|
|
return u'<ImageStream objid="%s" encoding="%s" file="%s" />\n'%\
|
|
(self.id, self.encoding, self.file)
|
|
|
|
class Import(LRFStream):
|
|
pass
|
|
|
|
class Button(LRFObject):
|
|
tag_map = {
|
|
0xF503: ['', 'do_ref_image'],
|
|
0xF561: ['button_flags','W'], #<Button/>
|
|
0xF562: ['','do_base_button'], #<BaseButton>
|
|
0xF563: ['',''], #</BaseButton>
|
|
0xF564: ['','do_focusin_button'], #<FocusinButton>
|
|
0xF565: ['',''], #</FocusinButton>
|
|
0xF566: ['','do_push_button'], #<PushButton>
|
|
0xF567: ['',''], #</PushButton>
|
|
0xF568: ['','do_up_button'], #<UpButton>
|
|
0xF569: ['',''], #</UpButton>
|
|
0xF56A: ['','do_start_actions'], #start actions
|
|
0xF56B: ['',''], #end actions
|
|
0xF56C: ['','parse_jump_to'], #JumpTo
|
|
0xF56D: ['','parse_send_message'], #<SendMessage
|
|
0xF56E: ['','parse_close_window'], #<CloseWindow/>
|
|
0xF5D6: ['','parse_sound_stop'], #<SoundStop/>
|
|
0xF5F9: ['','parse_run'], #Run
|
|
}
|
|
tag_map.update(LRFObject.tag_map)
|
|
|
|
def __init__(self, document, stream, id, scramble_key, boundary):
|
|
self.xml = u''
|
|
self.refimage = {}
|
|
self.actions = {}
|
|
self.to_dump = True
|
|
LRFObject.__init__(self, document, stream, id, scramble_key, boundary)
|
|
|
|
def do_ref_image(self, tag, f):
|
|
self.refimage[self.button_yype] = tag.dword
|
|
|
|
def do_base_button(self, tag, f):
|
|
self.button_type = 0
|
|
self.actions[self.button_type] = []
|
|
|
|
def do_focus_in_button(self, tag, f):
|
|
self.button_type = 1
|
|
|
|
def do_push_button(self, tag, f):
|
|
self.button_type = 2
|
|
|
|
def do_up_button(self, tag, f):
|
|
self.button_type = 3
|
|
|
|
def do_start_actions(self, tag, f):
|
|
self.actions[self.button_type] = []
|
|
|
|
def parse_jump_to(self, tag, f):
|
|
self.actions[self.button_type].append((1, struct.unpack("<II", tag.contents)))
|
|
|
|
def parse_send_message(self, tag, f):
|
|
params = (tag.word, Tag.string_parser(f), Tag.string_parser(f))
|
|
self.actions[self.button_type].append((2, params))
|
|
|
|
def parse_close_window(self, tag, f):
|
|
self.actions[self.button_type].append((3,))
|
|
|
|
def parse_sound_stop(self, tag, f):
|
|
self.actions[self.button_type].append((4,))
|
|
|
|
def parse_run(self, tag, f):
|
|
self.actions[self.button_type].append((5, struct.unpack("<HI", tag.contents)))
|
|
|
|
def jump_action(self, button_type):
|
|
for i in self.actions[button_type]:
|
|
if i[0] == 1:
|
|
return i[1:][0]
|
|
|
|
def __unicode__(self):
|
|
s = u'<Button objid="%s">\n'%(self.id,)
|
|
if self.button_flags & 0x10 != 0:
|
|
s += '<PushButton '
|
|
if 2 in self.refimage:
|
|
s += 'refimage="%s" '%(self.refimage[2],)
|
|
s = s.rstrip() + '>\n'
|
|
s += '<JumpTo refpage="%s" refobj="%s" />\n'% self.jump_action(2)
|
|
s += '</PushButton>\n'
|
|
else:
|
|
raise LRFParseError('Unsupported button type')
|
|
s += '</Button>\n'
|
|
return s
|
|
|
|
refpage = property(fget=lambda self : self.jump_action(2)[0])
|
|
refobj = property(fget=lambda self : self.jump_action(2)[1])
|
|
|
|
|
|
class Window(LRFObject):
|
|
pass
|
|
|
|
class PopUpWin(LRFObject):
|
|
pass
|
|
|
|
class Sound(LRFObject):
|
|
pass
|
|
|
|
class SoundStream(LRFObject):
|
|
pass
|
|
|
|
class Font(LRFStream):
|
|
tag_map = {
|
|
0xF559: ['fontfilename', 'P'],
|
|
0xF55D: ['fontfacename', 'P'],
|
|
}
|
|
tag_map.update(LRFStream.tag_map)
|
|
data = property(fget=lambda self: self.stream)
|
|
|
|
def end_stream(self, *args):
|
|
LRFStream.end_stream(self, *args)
|
|
self._document.font_map[self.fontfacename] = self
|
|
self.file = self.fontfacename + '.ttf'
|
|
|
|
def __unicode__(self):
|
|
s = '<RegistFont objid="%s" fontfilename="%s" fontfacename="%s" encoding="TTF" file="%s" />\n'%\
|
|
(self.id, self.fontfilename, self.fontfacename, self.file)
|
|
return s
|
|
|
|
class ObjectInfo(LRFStream):
|
|
pass
|
|
|
|
|
|
class BookAttr(StyleObject, LRFObject):
|
|
tag_map = {
|
|
0xF57B: ['page_tree_id', 'D'],
|
|
0xF5D8: ['', 'add_font'],
|
|
0xF5DA: ['setwaitprop', 'W', {1: 'replay', 2: 'noreplay'}],
|
|
}
|
|
tag_map.update(ruby_tags)
|
|
tag_map.update(LRFObject.tag_map)
|
|
binding_map = {1: 'Lr', 16 : 'Rl'}
|
|
|
|
def __init__(self, document, stream, id, scramble_key, boundary):
|
|
self.font_link_list = []
|
|
LRFObject.__init__(self, document, stream, id, scramble_key, boundary)
|
|
|
|
def add_font(self, tag, f):
|
|
self.font_link_list.append(tag.dword)
|
|
|
|
def __unicode__(self):
|
|
s = u'<BookStyle objid="%s" stylelabel="%s">\n'%(self.id, self.id)
|
|
s += u'<SetDefault %s />\n'%(self._tags_to_xml(),)
|
|
doc = self._document
|
|
s += u'<BookSetting bindingdirection="%s" dpi="%s" screenwidth="%s" screenheight="%s" colordepth="%s" />\n'%\
|
|
(self.binding_map[doc.binding], doc.dpi, doc.width, doc.height, doc.color_depth)
|
|
for font in self._document.font_map.values():
|
|
s += unicode(font)
|
|
s += '</BookStyle>\n'
|
|
return s
|
|
|
|
class SimpleText(Text):
|
|
pass
|
|
|
|
class TocLabel(object):
|
|
|
|
def __init__(self, refpage, refobject, label):
|
|
self.refpage, self.refobject, self.label = refpage, refobject, label
|
|
|
|
def __unicode__(self):
|
|
return u'<TocLabel refpage="%s" refobj="%s">%s</TocLabel>\n'%(self.refpage, self.refobject, self.label)
|
|
|
|
class TOCObject(LRFStream):
|
|
|
|
def initialize(self):
|
|
stream = cStringIO.StringIO(self.stream)
|
|
c = struct.unpack("<H", stream.read(2))[0]
|
|
stream.seek(4*(c+1))
|
|
self._contents = []
|
|
while c > 0:
|
|
refpage = struct.unpack("<I", stream.read(4))[0]
|
|
refobj = struct.unpack("<I", stream.read(4))[0]
|
|
cnt = struct.unpack("<H", stream.read(2))[0]
|
|
label = unicode(stream.read(cnt), "utf_16")
|
|
self._contents.append(TocLabel(refpage, refobj, label))
|
|
c -= 1
|
|
|
|
def __iter__(self):
|
|
for i in self._contents:
|
|
yield i
|
|
|
|
def __unicode__(self):
|
|
s = u'<TOC>\n'
|
|
for i in self:
|
|
s += unicode(i)
|
|
return s + '</TOC>\n'
|
|
|
|
|
|
object_map = [
|
|
None, #00
|
|
PageTree, #01
|
|
Page, #02
|
|
Header, #03
|
|
Footer, #04
|
|
PageAttr, #05
|
|
Block, #06
|
|
BlockAttr, #07
|
|
MiniPage, #08
|
|
None, #09
|
|
Text, #0A
|
|
TextAttr, #0B
|
|
Image, #0C
|
|
Canvas, #0D
|
|
ESound, #0E
|
|
None, #0F
|
|
None, #10
|
|
ImageStream,#11
|
|
Import, #12
|
|
Button, #13
|
|
Window, #14
|
|
PopUpWin, #15
|
|
Sound, #16
|
|
SoundStream,#17
|
|
None, #18
|
|
Font, #19
|
|
ObjectInfo, #1A
|
|
None, #1B
|
|
BookAttr, #1C
|
|
SimpleText, #1D
|
|
TOCObject, #1E
|
|
]
|
|
|
|
|
|
def get_object(document, stream, id, offset, size, scramble_key):
|
|
stream.seek(offset)
|
|
start_tag = Tag(stream)
|
|
if start_tag.id != 0xF500:
|
|
raise LRFParseError('Bad object start')
|
|
obj_id, obj_type = struct.unpack("<IH", start_tag.contents)
|
|
if obj_type < len(object_map) and object_map[obj_type] is not None:
|
|
return object_map[obj_type](document, stream, obj_id, scramble_key, offset+size-Tag.tags[0][0])
|
|
|
|
raise LRFParseError("Unknown object type: %02X!" % obj_type)
|
|
|
|
|