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