1__license__   = 'GPL v3'
2__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
3
4"""
5This module presents an easy to use interface for getting and setting
6meta information in LRF files.
7Just create an L{LRFMetaFile} object and use its properties
8to get and set meta information. For example:
9
10>>> lrf = LRFMetaFile("mybook.lrf")
11>>> print(lrf.title, lrf.author)
12>>> lrf.category = "History"
13"""
14
15import io, struct, zlib, sys, os
16from shutil import copyfileobj
17import xml.dom.minidom as dom
18from functools import wraps
19
20from calibre.ebooks.chardet import xml_to_unicode
21from calibre.utils.cleantext import clean_xml_chars
22from calibre.ebooks.metadata import MetaInformation, string_to_authors
23from polyglot.builtins import string_or_bytes
24
25BYTE      = "<B"  #: Unsigned char little endian encoded in 1 byte
26WORD      = "<H"  #: Unsigned short little endian encoded in 2 bytes
27DWORD     = "<I"  #: Unsigned integer little endian encoded in 4 bytes
28QWORD     = "<Q"  #: Unsigned long long little endian encoded in 8 bytes
29
30
31class field:
32    """ A U{Descriptor<http://www.cafepy.com/article/python_attributes_and_methods/python_attributes_and_methods.html>}, that implements access
33    to protocol packets in a human readable way.
34    """
35
36    def __init__(self, start=16, fmt=DWORD):
37        """
38        @param start: The byte at which this field is stored in the buffer
39        @param fmt:   The packing format for this field.
40        See U{struct<http://docs.python.org/lib/module-struct.html>}.
41        """
42        self._fmt, self._start = fmt, start
43
44    def __get__(self, obj, typ=None):
45        return obj.unpack(start=self._start, fmt=self._fmt)[0]
46
47    def __set__(self, obj, val):
48        obj.pack(val, start=self._start, fmt=self._fmt)
49
50    def __repr__(self):
51        typ = {DWORD: 'unsigned int', 'QWORD': 'unsigned long long', BYTE: 'unsigned char', WORD: 'unsigned short'}.get(self._fmt, '')
52        return "An " + typ + " stored in " + \
53        str(struct.calcsize(self._fmt)) + \
54        " bytes starting at byte " + str(self._start)
55
56
57class versioned_field(field):
58
59    def __init__(self, vfield, version, start=0, fmt=WORD):
60        field.__init__(self, start=start, fmt=fmt)
61        self.vfield, self.version = vfield, version
62
63    def enabled(self, obj):
64        return self.vfield.__get__(obj) > self.version
65
66    def __get__(self, obj, typ=None):
67        if self.enabled(obj):
68            return field.__get__(self, obj, typ=typ)
69        else:
70            return None
71
72    def __set__(self, obj, val):
73        if not self.enabled(obj):
74            raise LRFException("Trying to set disabled field")
75        else:
76            field.__set__(self, obj, val)
77
78
79class LRFException(Exception):
80    pass
81
82
83class fixed_stringfield:
84    """ A field storing a variable length string. """
85
86    def __init__(self, length=8, start=0):
87        """
88        @param length: Size of this string
89        @param start: The byte at which this field is stored in the buffer
90        """
91        self._length = length
92        self._start = start
93
94    def __get__(self, obj, typ=None):
95        length = str(self._length)
96        return obj.unpack(start=self._start, fmt="<"+length+"s")[0]
97
98    def __set__(self, obj, val):
99        if not isinstance(val, string_or_bytes):
100            val = str(val)
101        if isinstance(val, str):
102            val = val.encode('utf-8')
103        if len(val) != self._length:
104            raise LRFException("Trying to set fixed_stringfield with a " +
105                               "string of  incorrect length")
106        obj.pack(val, start=self._start, fmt="<"+str(len(val))+"s")
107
108    def __repr__(self):
109        return "A string of length " + str(self._length) + \
110                " starting at byte " + str(self._start)
111
112
113class xml_attr_field:
114
115    def __init__(self, tag_name, attr, parent='BookInfo'):
116        self.tag_name = tag_name
117        self.parent = parent
118        self.attr= attr
119
120    def __get__(self, obj, typ=None):
121        """ Return the data in this field or '' if the field is empty """
122        document = obj.info
123        elems = document.getElementsByTagName(self.tag_name)
124        if len(elems):
125            elem = None
126            for candidate in elems:
127                if candidate.parentNode.nodeName == self.parent:
128                    elem = candidate
129            if elem and elem.hasAttribute(self.attr):
130                return elem.getAttribute(self.attr)
131        return ''
132
133    def __set__(self, obj, val):
134        if val is None:
135            val = ""
136        document = obj.info
137        elems = document.getElementsByTagName(self.tag_name)
138        if len(elems):
139            elem = None
140            for candidate in elems:
141                if candidate.parentNode.nodeName == self.parent:
142                    elem = candidate
143        if elem:
144            elem.setAttribute(self.attr, val)
145        obj.info = document
146
147    def __repr__(self):
148        return "XML Attr Field: " + self.tag_name + " in " + self.parent
149
150    def __str__(self):
151        return self.tag_name+'.'+self.attr
152
153
154class xml_field:
155    """
156    Descriptor that gets and sets XML based meta information from an LRF file.
157    Works for simple XML fields of the form <tagname>data</tagname>
158    """
159
160    def __init__(self, tag_name, parent="BookInfo"):
161        """
162        @param tag_name: The XML tag whose data we operate on
163        @param parent: The tagname of the parent element of C{tag_name}
164        """
165        self.tag_name = tag_name
166        self.parent = parent
167
168    def __get__(self, obj, typ=None):
169        """ Return the data in this field or '' if the field is empty """
170        document = obj.info
171
172        elems = document.getElementsByTagName(self.tag_name)
173        if len(elems):
174            elem = None
175            for candidate in elems:
176                if candidate.parentNode.nodeName == self.parent:
177                    elem = candidate
178            if elem:
179                elem.normalize()
180                if elem.hasChildNodes():
181                    return elem.firstChild.data.strip()
182        return ''
183
184    def __set__(self, obj, val):
185        if not val:
186            val = ''
187        document = obj.info
188
189        def create_elem():
190            elem = document.createElement(self.tag_name)
191            parent = document.getElementsByTagName(self.parent)[0]
192            parent.appendChild(elem)
193            return elem
194
195        if not val:
196            val = ''
197        if not isinstance(val, str):
198            val = val.decode('utf-8')
199
200        elems = document.getElementsByTagName(self.tag_name)
201        elem = None
202        if len(elems):
203            for candidate in elems:
204                if candidate.parentNode.nodeName == self.parent:
205                    elem = candidate
206            if not elem:
207                elem = create_elem()
208            else:
209                elem.normalize()
210                while elem.hasChildNodes():
211                    elem.removeChild(elem.lastChild)
212        else:
213            elem = create_elem()
214        elem.appendChild(document.createTextNode(val))
215
216        obj.info = document
217
218    def __str__(self):
219        return self.tag_name
220
221    def __repr__(self):
222        return "XML Field: " + self.tag_name + " in " + self.parent
223
224
225def insert_into_file(fileobj, data, start, end):
226    """
227    Insert data into fileobj at position C{start}.
228
229    This function inserts data into a file, overwriting all data between start
230    and end. If end == start no data is overwritten. Do not use this function to
231    append data to a file.
232
233    @param fileobj: file like object
234    @param data:    data to be inserted into fileobj
235    @param start:   The position at which to start inserting data
236    @param end:     The position in fileobj of data that must not be overwritten
237    @return:        C{start + len(data) - end}
238    """
239    buffer = io.BytesIO()
240    fileobj.seek(end)
241    copyfileobj(fileobj, buffer, -1)
242    buffer.flush()
243    buffer.seek(0)
244    fileobj.seek(start)
245    fileobj.write(data)
246    fileobj.flush()
247    fileobj.truncate()
248    delta = fileobj.tell() - end  # < 0 if len(data) < end-start
249    copyfileobj(buffer, fileobj, -1)
250    fileobj.flush()
251    buffer.close()
252    return delta
253
254
255def get_metadata(stream):
256    """
257    Return basic meta-data about the LRF file in C{stream} as a
258    L{MetaInformation} object.
259    @param stream: A file like object or an instance of L{LRFMetaFile}
260    """
261    lrf = stream if isinstance(stream, LRFMetaFile) else LRFMetaFile(stream)
262    authors = string_to_authors(lrf.author)
263    mi = MetaInformation(lrf.title.strip(), authors)
264    mi.author = lrf.author.strip()
265    mi.comments = lrf.free_text.strip()
266    mi.category = lrf.category.strip()+', '+lrf.classification.strip()
267    tags = [x.strip() for x in mi.category.split(',') if x.strip()]
268    if tags:
269        mi.tags = tags
270    if mi.category.strip() == ',':
271        mi.category = None
272    mi.publisher = lrf.publisher.strip()
273    mi.cover_data = lrf.get_cover()
274    try:
275        mi.title_sort = lrf.title_reading.strip()
276        if not mi.title_sort:
277            mi.title_sort = None
278    except:
279        pass
280    try:
281        mi.author_sort = lrf.author_reading.strip()
282        if not mi.author_sort:
283            mi.author_sort = None
284    except:
285        pass
286    if not mi.title or 'unknown' in mi.title.lower():
287        mi.title = None
288    if not mi.authors:
289        mi.authors = None
290    if not mi.author or 'unknown' in mi.author.lower():
291        mi.author = None
292    if not mi.category or 'unknown' in mi.category.lower():
293        mi.category = None
294    if not mi.publisher or 'unknown' in mi.publisher.lower() or \
295            'some publisher' in mi.publisher.lower():
296        mi.publisher = None
297
298    return mi
299
300
301class LRFMetaFile:
302    """ Has properties to read and write all Meta information in a LRF file. """
303    #: The first 6 bytes of all valid LRF files
304    LRF_HEADER = 'LRF'.encode('utf-16le')
305
306    lrf_header               = fixed_stringfield(length=6, start=0x0)
307    version                  = field(fmt=WORD, start=0x8)
308    xor_key                  = field(fmt=WORD, start=0xa)
309    root_object_id           = field(fmt=DWORD, start=0xc)
310    number_of_objects        = field(fmt=QWORD, start=0x10)
311    object_index_offset      = field(fmt=QWORD, start=0x18)
312    binding                  = field(fmt=BYTE, start=0x24)
313    dpi                      = field(fmt=WORD, start=0x26)
314    width                    = field(fmt=WORD, start=0x2a)
315    height                   = field(fmt=WORD, start=0x2c)
316    color_depth              = field(fmt=BYTE, start=0x2e)
317    toc_object_id            = field(fmt=DWORD, start=0x44)
318    toc_object_offset        = field(fmt=DWORD, start=0x48)
319    compressed_info_size     = field(fmt=WORD, start=0x4c)
320    thumbnail_type           = versioned_field(version, 800, fmt=WORD, start=0x4e)
321    thumbnail_size           = versioned_field(version, 800, fmt=DWORD, start=0x50)
322    uncompressed_info_size   = versioned_field(compressed_info_size, 0,
323                                             fmt=DWORD, start=0x54)
324
325    title                 = xml_field("Title", parent="BookInfo")
326    title_reading         = xml_attr_field("Title", 'reading', parent="BookInfo")
327    author                = xml_field("Author", parent="BookInfo")
328    author_reading        = xml_attr_field("Author", 'reading', parent="BookInfo")
329    # 16 characters. First two chars should be FB for personal use ebooks.
330    book_id               = xml_field("BookID", parent="BookInfo")
331    publisher             = xml_field("Publisher", parent="BookInfo")
332    label                 = xml_field("Label", parent="BookInfo")
333    category              = xml_field("Category", parent="BookInfo")
334    classification        = xml_field("Classification", parent="BookInfo")
335    free_text             = xml_field("FreeText", parent="BookInfo")
336    # Should use ISO 639 language codes
337    language              = xml_field("Language", parent="DocInfo")
338    creator               = xml_field("Creator", parent="DocInfo")
339    # Format is %Y-%m-%d
340    creation_date         = xml_field("CreationDate", parent="DocInfo")
341    producer              = xml_field("Producer", parent="DocInfo")
342    page                  = xml_field("SumPage", parent="DocInfo")
343
344    def safe(func):
345        """
346        Decorator that ensures that function calls leave the pos
347        in the underlying file unchanged
348        """
349        @wraps(func)
350        def restore_pos(*args, **kwargs):
351            obj = args[0]
352            pos = obj._file.tell()
353            res = func(*args, **kwargs)
354            obj._file.seek(0, 2)
355            if obj._file.tell() >= pos:
356                obj._file.seek(pos)
357            return res
358        return restore_pos
359
360    def safe_property(func):
361        """
362        Decorator that ensures that read or writing a property leaves
363        the position in the underlying file unchanged
364        """
365        def decorator(f):
366            def restore_pos(*args, **kwargs):
367                obj = args[0]
368                pos = obj._file.tell()
369                res = f(*args, **kwargs)
370                obj._file.seek(0, 2)
371                if obj._file.tell() >= pos:
372                    obj._file.seek(pos)
373                return res
374            return restore_pos
375        locals_ = func()
376        if 'fget' in locals_:
377            locals_["fget"] = decorator(locals_["fget"])
378        if 'fset' in locals_:
379            locals_["fset"] = decorator(locals_["fset"])
380        return property(**locals_)
381
382    @safe_property
383    def info():
384        doc = \
385        """
386        Document meta information as a minidom Document object.
387        To set use a minidom document object.
388        """
389
390        def fget(self):
391            if self.compressed_info_size == 0:
392                raise LRFException("This document has no meta info")
393            size = self.compressed_info_size - 4
394            self._file.seek(self.info_start)
395            try:
396                src =  zlib.decompress(self._file.read(size))
397                if len(src) != self.uncompressed_info_size:
398                    raise LRFException("Decompression of document meta info\
399                                        yielded unexpected results")
400
401                src = xml_to_unicode(src, strip_encoding_pats=True, resolve_entities=True, assume_utf8=True)[0]
402                return dom.parseString(clean_xml_chars(src))
403            except zlib.error:
404                raise LRFException("Unable to decompress document meta information")
405
406        def fset(self, document):
407            info = document.toxml('utf-8')
408            self.uncompressed_info_size = len(info)
409            stream = zlib.compress(info)
410            orig_size = self.compressed_info_size
411            self.compressed_info_size = len(stream) + 4
412            delta = insert_into_file(self._file, stream, self.info_start,
413                                     self.info_start + orig_size - 4)
414
415            if self.toc_object_offset > 0:
416                self.toc_object_offset   += delta
417            self.object_index_offset += delta
418            self.update_object_offsets(delta)
419
420        return {"fget":fget, "fset":fset, "doc":doc}
421
422    @safe_property
423    def thumbnail_pos():
424        doc = """ The position of the thumbnail in the LRF file """
425
426        def fget(self):
427            return self.info_start + self.compressed_info_size-4
428        return {"fget":fget, "doc":doc}
429
430    @classmethod
431    def _detect_thumbnail_type(cls, slice):
432        """ @param slice: The first 16 bytes of the thumbnail """
433        ttype = 0x14  # GIF
434        if "PNG" in slice:
435            ttype = 0x12
436        if "BM" in slice:
437            ttype = 0x13
438        if "JFIF" in slice:
439            ttype = 0x11
440        return ttype
441
442    @safe_property
443    def thumbnail():
444        doc = \
445        """
446        The thumbnail.
447        Represented as a string.
448        The string you would get from the file read function.
449        """
450
451        def fget(self):
452            size = self.thumbnail_size
453            if size:
454                self._file.seek(self.thumbnail_pos)
455                return self._file.read(size)
456
457        def fset(self, data):
458            if self.version <= 800:
459                raise LRFException("Cannot store thumbnails in LRF files \
460                                    of version <= 800")
461            slice = data[0:16]
462            orig_size = self.thumbnail_size
463            self.thumbnail_size = len(data)
464            delta = insert_into_file(self._file, data, self.thumbnail_pos,
465                                     self.thumbnail_pos + orig_size)
466            self.toc_object_offset += delta
467            self.object_index_offset += delta
468            self.thumbnail_type = self._detect_thumbnail_type(slice)
469            self.update_object_offsets(delta)
470
471        return {"fget":fget, "fset":fset, "doc":doc}
472
473    def __init__(self, file):
474        """ @param file: A file object opened in the r+b mode """
475        file.seek(0, 2)
476        self.size = file.tell()
477        self._file = file
478        if self.lrf_header != LRFMetaFile.LRF_HEADER:
479            raise LRFException(file.name +
480                " has an invalid LRF header. Are you sure it is an LRF file?")
481        # Byte at which the compressed meta information starts
482        self.info_start = 0x58 if self.version > 800 else 0x53
483
484    @safe
485    def update_object_offsets(self, delta):
486        """ Run through the LRF Object index changing the offset by C{delta}. """
487        self._file.seek(self.object_index_offset)
488        count = self.number_of_objects
489        while count > 0:
490            raw = self._file.read(8)
491            new_offset = struct.unpack(DWORD, raw[4:8])[0] + delta
492            if new_offset >= (2**8)**4 or new_offset < 0x4C:
493                raise LRFException(_('Invalid LRF file. Could not set metadata.'))
494            self._file.seek(-4, os.SEEK_CUR)
495            self._file.write(struct.pack(DWORD, new_offset))
496            self._file.seek(8, os.SEEK_CUR)
497            count -= 1
498        self._file.flush()
499
500    @safe
501    def unpack(self, fmt=DWORD, start=0):
502        """
503        Return decoded data from file.
504
505        @param fmt: See U{struct<http://docs.python.org/lib/module-struct.html>}
506        @param start: Position in file from which to decode
507        """
508        end = start + struct.calcsize(fmt)
509        self._file.seek(start)
510        ret =  struct.unpack(fmt, self._file.read(end-start))
511        return ret
512
513    @safe
514    def pack(self, *args, **kwargs):
515        """
516        Encode C{args} and write them to file.
517        C{kwargs} must contain the keywords C{fmt} and C{start}
518
519        @param args: The values to pack
520        @param fmt: See U{struct<http://docs.python.org/lib/module-struct.html>}
521        @param start: Position in file at which to write encoded data
522        """
523        encoded = struct.pack(kwargs["fmt"], *args)
524        self._file.seek(kwargs["start"])
525        self._file.write(encoded)
526        self._file.flush()
527
528    def thumbail_extension(self):
529        """
530        Return the extension for the thumbnail image type as specified
531        by L{self.thumbnail_type}. If the LRF file was created by buggy
532        software, the extension maye be incorrect. See L{self.fix_thumbnail_type}.
533        """
534        ext = "gif"
535        ttype = self.thumbnail_type
536        if ttype == 0x11:
537            ext = "jpeg"
538        elif ttype == 0x12:
539            ext = "png"
540        elif ttype == 0x13:
541            ext = "bmp"
542        return ext
543
544    def fix_thumbnail_type(self):
545        """
546        Attempt to guess the thumbnail image format and set
547        L{self.thumbnail_type} accordingly.
548        """
549        slice = self.thumbnail[0:16]
550        self.thumbnail_type = self._detect_thumbnail_type(slice)
551
552    def seek(self, *args):
553        """ See L{file.seek} """
554        return self._file.seek(*args)
555
556    def tell(self):
557        """ See L{file.tell} """
558        return self._file.tell()
559
560    def read(self):
561        """ See L{file.read} """
562        return self._file.read()
563
564    def write(self, val):
565        """ See L{file.write} """
566        self._file.write(val)
567
568    def _objects(self):
569        self._file.seek(self.object_index_offset)
570        c = self.number_of_objects
571        while c > 0:
572            c -= 1
573            raw = self._file.read(16)
574            pos = self._file.tell()
575            yield struct.unpack('<IIII', raw)[:3]
576            self._file.seek(pos)
577
578    def get_objects_by_type(self, type):
579        from calibre.ebooks.lrf.tags import Tag
580        objects = []
581        for id, offset, size in self._objects():
582            self._file.seek(offset)
583            tag = Tag(self._file)
584            if tag.id == 0xF500:
585                obj_id, obj_type = struct.unpack("<IH", tag.contents)
586                if obj_type == type:
587                    objects.append((obj_id, offset, size))
588        return objects
589
590    def get_object_by_id(self, tid):
591        from calibre.ebooks.lrf.tags import Tag
592        for id, offset, size in self._objects():
593            self._file.seek(offset)
594            tag = Tag(self._file)
595            if tag.id == 0xF500:
596                obj_id, obj_type = struct.unpack("<IH", tag.contents)
597                if obj_id == tid:
598                    return obj_id, offset, size, obj_type
599        return (False, False, False, False)
600
601    @safe
602    def get_cover(self):
603        from calibre.ebooks.lrf.objects import get_object
604
605        for id, offset, size in self.get_objects_by_type(0x0C):
606            image = get_object(None, self._file, id, offset, size, self.xor_key)
607            id, offset, size = self.get_object_by_id(image.refstream)[:3]
608            image_stream = get_object(None, self._file, id, offset, size, self.xor_key)
609            return image_stream.file.rpartition('.')[-1], image_stream.stream
610        return None
611
612
613def option_parser():
614    from calibre.utils.config import OptionParser
615    from calibre.constants import __appname__, __version__
616    parser = OptionParser(usage=_('''%prog [options] mybook.lrf
617
618
619Show/edit the metadata in an LRF file.\n\n'''),
620      version=__appname__+' '+__version__,
621      epilog='Created by Kovid Goyal')
622    parser.add_option("-t", "--title", action="store", type="string",
623                    dest="title", help=_("Set the book title"))
624    parser.add_option('--title-sort', action='store', type='string', default=None,
625                      dest='title_reading', help=_('Set sort key for the title'))
626    parser.add_option("-a", "--author", action="store", type="string",
627                    dest="author", help=_("Set the author"))
628    parser.add_option('--author-sort', action='store', type='string', default=None,
629                      dest='author_reading', help=_('Set sort key for the author'))
630    parser.add_option("-c", "--category", action="store", type="string",
631                    dest="category", help=_("The category this book belongs"
632                    " to. E.g.: History"))
633    parser.add_option("--thumbnail", action="store", type="string",
634                    dest="thumbnail", help=_("Path to a graphic that will be"
635                    " set as this files' thumbnail"))
636    parser.add_option("--comment", action="store", type="string",
637                    dest="comment", help=_("Path to a TXT file containing the "
638                    "comment to be stored in the LRF file."))
639    parser.add_option("--get-thumbnail", action="store_true",
640                    dest="get_thumbnail", default=False,
641                    help=_("Extract thumbnail from LRF file"))
642    parser.add_option('--publisher', default=None, help=_('Set the publisher'))
643    parser.add_option('--classification', default=None, help=_('Set the book classification'))
644    parser.add_option('--creator', default=None, help=_('Set the book creator'))
645    parser.add_option('--producer', default=None, help=_('Set the book producer'))
646    parser.add_option('--get-cover', action='store_true', default=False,
647                      help=_('Extract cover from LRF file. Note that the LRF format has no defined cover, so we use some heuristics to guess the cover.'))
648    parser.add_option('--bookid', action='store', type='string', default=None,
649                      dest='book_id', help=_('Set book ID'))
650    # The SumPage element specifies the number of "View"s (visible pages for the BookSetting element conditions) of the content.
651    # Basically, the total pages per the page size, font size, etc. when the
652    # LRF is first created. Since this will change as the book is reflowed, it
653    # is probably not worth using.
654    # parser.add_option("-p", "--page", action="store", type="string", \
655    #                dest="page", help=_("Don't know what this is for"))
656
657    return parser
658
659
660def set_metadata(stream, mi):
661    lrf = LRFMetaFile(stream)
662    if mi.title:
663        lrf.title = mi.title
664    if mi.authors:
665        lrf.author = ', '.join(mi.authors)
666    if mi.tags:
667        lrf.category = mi.tags[0]
668    if getattr(mi, 'category', False):
669        lrf.category = mi.category
670    if mi.comments:
671        lrf.free_text = mi.comments
672    if mi.author_sort:
673        lrf.author_reading = mi.author_sort
674    if mi.publisher:
675        lrf.publisher = mi.publisher
676
677
678def main(args=sys.argv):
679    parser = option_parser()
680    options, args = parser.parse_args(args)
681    if len(args) != 2:
682        parser.print_help()
683        print()
684        print('No lrf file specified')
685        return 1
686    lrf = LRFMetaFile(open(args[1], "r+b"))
687
688    if options.title:
689        lrf.title        = options.title
690    if options.title_reading is not None:
691        lrf.title_reading = options.title_reading
692    if options.author_reading is not None:
693        lrf.author_reading = options.author_reading
694    if options.author:
695        lrf.author    = options.author
696    if options.publisher:
697        lrf.publisher = options.publisher
698    if options.classification:
699        lrf.classification = options.classification
700    if options.category:
701        lrf.category = options.category
702    if options.creator:
703        lrf.creator = options.creator
704    if options.producer:
705        lrf.producer = options.producer
706    if options.thumbnail:
707        path = os.path.expanduser(os.path.expandvars(options.thumbnail))
708        with open(path, "rb") as f:
709            lrf.thumbnail = f.read()
710    if options.book_id is not None:
711        lrf.book_id = options.book_id
712    if options.comment:
713        path = os.path.expanduser(os.path.expandvars(options.comment))
714        with open(path, 'rb') as f:
715            lrf.free_text = f.read().decode('utf-8', 'replace')
716    if options.get_thumbnail:
717        t = lrf.thumbnail
718        td = "None"
719        if t and len(t) > 0:
720            td = os.path.basename(args[1])+"_thumbnail."+lrf.thumbail_extension()
721            with open(td, "wb") as f:
722                f.write(t)
723
724    fields = LRFMetaFile.__dict__.items()
725    fields.sort()
726    for f in fields:
727        if "XML" in str(f):
728            print(str(f[1]) + ":", lrf.__getattribute__(f[0]).encode('utf-8'))
729    if options.get_thumbnail:
730        print("Thumbnail:", td)
731    if options.get_cover:
732        try:
733            ext, data = lrf.get_cover()
734        except:  # Fails on books created by LRFCreator 1.0
735            ext, data = None, None
736        if data:
737            cover = os.path.splitext(os.path.basename(args[1]))[0]+"_cover."+ext
738            with open(cover, 'wb') as f:
739                f.write(data)
740            print('Cover:', cover)
741        else:
742            print('Could not find cover in the LRF file')
743
744
745if __name__ == '__main__':
746    sys.exit(main())
747