def extract_images(self, processed_records, output_dir): self.log.debug('Extracting images...') output_dir = os.path.abspath(os.path.join(output_dir, 'images')) if not os.path.exists(output_dir): os.makedirs(output_dir) image_index = 0 self.image_names = [] start = getattr(self.book_header, 'first_image_index', -1) if start > self.num_sections or start < 0: # BAEN PRC files have bad headers start = 0 for i in range(start, self.num_sections): if i in processed_records: continue processed_records.append(i) data = self.sections[i][0] image_index += 1 if data[:4] in { b'FLIS', b'FCIS', b'SRCS', b'\xe9\x8e\r\n', b'RESC', b'BOUN', b'FDST', b'DATP', b'AUDI', b'VIDE' }: # This record is a known non image type, not need to try to # load the image continue path = os.path.join(output_dir, '%05d.jpg' % image_index) try: if what(None, data) not in { 'jpg', 'jpeg', 'gif', 'png', 'bmp', 'webp' }: continue save_cover_data_to(data, path, minify_to=(10000, 10000)) except Exception: continue self.image_names.append(os.path.basename(path))
def _write_new_cover(new_cdata, cpath): from calibre.utils.magick.draw import save_cover_data_to new_cover = PersistentTemporaryFile(suffix=os.path.splitext(cpath)[1]) new_cover.close() save_cover_data_to(new_cdata, new_cover.name) return new_cover
def extract_images(self, processed_records, output_dir): self.log.debug('Extracting images...') output_dir = os.path.abspath(os.path.join(output_dir, 'images')) if not os.path.exists(output_dir): os.makedirs(output_dir) image_index = 0 self.image_names = [] start = getattr(self.book_header, 'first_image_index', -1) if start > self.num_sections or start < 0: # BAEN PRC files have bad headers start = 0 for i in range(start, self.num_sections): if i in processed_records: continue processed_records.append(i) data = self.sections[i][0] image_index += 1 if data[:4] in {b'FLIS', b'FCIS', b'SRCS', b'\xe9\x8e\r\n', b'RESC', b'BOUN', b'FDST', b'DATP', b'AUDI', b'VIDE'}: # This record is a known non image type, not need to try to # load the image continue path = os.path.join(output_dir, '%05d.jpg' % image_index) try: if what(None, data) not in {'jpg', 'jpeg', 'gif', 'png', 'bmp', 'webp'}: continue save_cover_data_to(data, path, minify_to=(10000, 10000)) except Exception: continue self.image_names.append(os.path.basename(path))
def main(args=sys.argv): parser = option_parser() opts, args = parser.parse_args(args) buf = BytesIO() log = create_log(buf) abort = Event() authors = [] if opts.authors: authors = string_to_authors(opts.authors) identifiers = {} if opts.isbn: identifiers['isbn'] = opts.isbn results = identify(log, abort, title=opts.title, authors=authors, identifiers=identifiers, timeout=int(opts.timeout)) if not results: print(log, file=sys.stderr) prints('No results found', file=sys.stderr) raise SystemExit(1) result = results[0] cf = None if opts.cover and results: cover = download_cover(log, title=opts.title, authors=authors, identifiers=result.identifiers, timeout=int(opts.timeout)) if cover is None: prints('No cover found', file=sys.stderr) else: save_cover_data_to(cover[-1], opts.cover) result.cover = cf = opts.cover log = buf.getvalue() result = (metadata_to_opf(result) if opts.opf else unicode(result).encode('utf-8')) if opts.verbose: print(log, file=sys.stderr) print(result) if not opts.opf and opts.cover: prints('Cover :', cf) return 0
def main(args=sys.argv): parser = option_parser() opts, args = parser.parse_args(args) buf = BytesIO() log = create_log(buf) abort = Event() authors = [] if opts.authors: authors = string_to_authors(opts.authors) identifiers = {} if opts.isbn: identifiers['isbn'] = opts.isbn results = identify(log, abort, title=opts.title, authors=authors, identifiers=identifiers, timeout=int(opts.timeout)) if not results: print (log, file=sys.stderr) prints('No results found', file=sys.stderr) raise SystemExit(1) result = results[0] cf = None if opts.cover and results: cover = download_cover(log, title=opts.title, authors=authors, identifiers=result.identifiers, timeout=int(opts.timeout)) if cover is None: prints('No cover found', file=sys.stderr) else: save_cover_data_to(cover[-1], opts.cover) result.cover = cf = opts.cover log = buf.getvalue() result = (metadata_to_opf(result) if opts.opf else unicode(result).encode('utf-8')) if opts.verbose: print (log, file=sys.stderr) print (result) if not opts.opf and opts.cover: prints('Cover :', cf) return 0
def get_cover(self, id, thumbnail=False, thumb_width=60, thumb_height=80): try: self.set_header('Content-Type', 'image/jpeg') cover = self.db.cover(id, index_is_id=True) if cover is None: cover = self.default_cover updated = self.build_time else: updated = self.db.cover_last_modified(id, index_is_id=True) self.set_header('Last-Modified', self.last_modified(updated)) if thumbnail: return generate_thumbnail(cover, width=thumb_width, height=thumb_height)[-1] img = Image() img.load(cover) width, height = img.size scaled, width, height = fit_image( width, height, thumb_width if thumbnail else self.max_cover_width, thumb_height if thumbnail else self.max_cover_height) if not scaled: return cover return save_cover_data_to(img, 'img.jpg', return_data=True, resize_to=(width, height)) except Exception as err: import traceback logging.error('Failed to generate cover:') logging.error(traceback.print_exc()) raise web.HTTPError(404, 'Failed to generate cover: %r' % err)
def get_cover(self, id, thumbnail=False, thumb_width=60, thumb_height=80): try: cherrypy.response.headers['Content-Type'] = 'image/jpeg' cherrypy.response.timeout = 3600 cover = self.db.cover(id, index_is_id=True) if cover is None: cover = self.default_cover updated = self.build_time else: updated = self.db.cover_last_modified(id, index_is_id=True) cherrypy.response.headers['Last-Modified'] = self.last_modified(updated) if thumbnail: return generate_thumbnail(cover, width=thumb_width, height=thumb_height)[-1] img = Image() img.load(cover) width, height = img.size scaled, width, height = fit_image(width, height, thumb_width if thumbnail else self.max_cover_width, thumb_height if thumbnail else self.max_cover_height) if not scaled: return cover return save_cover_data_to(img, 'img.jpg', return_data=True, resize_to=(width, height)) except Exception as err: import traceback cherrypy.log.error('Failed to generate cover:') cherrypy.log.error(traceback.print_exc()) raise cherrypy.HTTPError(404, 'Failed to generate cover: %r'%err)
def set_cover(self, book_id, path, data): path = os.path.abspath(os.path.join(self.library_path, path)) if not os.path.exists(path): os.makedirs(path) path = os.path.join(path, 'cover.jpg') if callable(getattr(data, 'save', None)): from calibre.gui2 import pixmap_to_data data = pixmap_to_data(data) else: if callable(getattr(data, 'read', None)): data = data.read() try: save_cover_data_to(data, path) except (IOError, OSError): time.sleep(0.2) save_cover_data_to(data, path)
def set_cover(self, book_id, path, data): path = os.path.abspath(os.path.join(self.library_path, path)) if not os.path.exists(path): os.makedirs(path) path = os.path.join(path, 'cover.jpg') if callable(getattr(data, 'save', None)): from calibre.gui2 import pixmap_to_data data = pixmap_to_data(data) else: if callable(getattr(data, 'read', None)): data = data.read() try: save_cover_data_to(data, path) except (IOError, OSError): time.sleep(0.2) save_cover_data_to(data, path)
def get_metadata(stream): from calibre.ebooks.metadata import MetaInformation from calibre.ptempfile import TemporaryDirectory from calibre.ebooks.mobi.reader.headers import MetadataHeader from calibre.ebooks.mobi.reader.mobi6 import MobiReader from calibre.utils.magick.draw import save_cover_data_to from calibre import CurrentDir stream.seek(0) try: raw = stream.read(3) except: raw = '' stream.seek(0) if raw == b'TPZ': from calibre.ebooks.metadata.topaz import get_metadata return get_metadata(stream) from calibre.utils.logging import Log log = Log() try: mi = MetaInformation(os.path.basename(stream.name), [_('Unknown')]) except: mi = MetaInformation(_('Unknown'), [_('Unknown')]) mh = MetadataHeader(stream, log) if mh.title and mh.title != _('Unknown'): mi.title = mh.title if mh.exth is not None: if mh.exth.mi is not None: mi = mh.exth.mi else: size = 1024**3 if hasattr(stream, 'seek') and hasattr(stream, 'tell'): pos = stream.tell() stream.seek(0, 2) size = stream.tell() stream.seek(pos) if size < 4*1024*1024: with TemporaryDirectory('_mobi_meta_reader') as tdir: with CurrentDir(tdir): mr = MobiReader(stream, log) parse_cache = {} mr.extract_content(tdir, parse_cache) if mr.embedded_mi is not None: mi = mr.embedded_mi if hasattr(mh.exth, 'cover_offset'): cover_index = mh.first_image_index + mh.exth.cover_offset data = mh.section_data(int(cover_index)) else: try: data = mh.section_data(mh.first_image_index) except: data = '' if data and what(None, data) in {'jpg', 'jpeg', 'gif', 'png', 'bmp', 'webp'}: try: mi.cover_data = ('jpg', save_cover_data_to(data, 'cover.jpg', return_data=True)) except Exception: log.exception('Failed to read MOBI cover') return mi
def rescale_image(data, maxsizeb=IMAGE_MAX_SIZE, dimen=None): ''' Convert image setting all transparent pixels to white and changing format to JPEG. Ensure the resultant image has a byte size less than maxsizeb. If dimen is not None, generate a thumbnail of width=dimen, height=dimen or width, height = dimen (depending on the type of dimen) Returns the image as a bytestring ''' if dimen is not None: if hasattr(dimen, '__len__'): width, height = dimen else: width = height = dimen data = thumbnail(data, width=width, height=height, compression_quality=90)[-1] else: # Replace transparent pixels with white pixels and convert to JPEG data = save_cover_data_to(data, 'img.jpg', return_data=True) if len(data) <= maxsizeb: return data orig_data = data img = Image() quality = 95 img.load(data) while len(data) >= maxsizeb and quality >= 10: quality -= 5 img.set_compression_quality(quality) data = img.export('jpg') if len(data) <= maxsizeb: return data orig_data = data scale = 0.9 while len(data) >= maxsizeb and scale >= 0.05: img = Image() img.load(orig_data) w, h = img.size img.size = (int(scale * w), int(scale * h)) img.set_compression_quality(quality) data = img.export('jpg') scale -= 0.05 return data
def process_result(log, result): plugin, data = result try: im = Image() im.load(data) im.trim(10) width, height = im.size fmt = im.format if width < 50 or height < 50: raise ValueError('Image too small') data = save_cover_data_to(im, '/cover.jpg', return_data=True) except: log.exception('Invalid cover from', plugin.name) return None return (plugin, width, height, fmt, data)
def process_result(log, result): plugin, data = result try: im = Image() im.load(data) im.trim(10) width, height = im.size fmt = im.format if width < 50 or height < 50: raise ValueError('Image too small') data = save_cover_data_to(im, '/cover.jpg', return_data=True) except: log.exception('Invalid cover from', plugin.name) return None return (plugin, width, height, fmt, data)
def rescale_image(data, maxsizeb=IMAGE_MAX_SIZE, dimen=None): ''' Convert image setting all transparent pixels to white and changing format to JPEG. Ensure the resultant image has a byte size less than maxsizeb. If dimen is not None, generate a thumbnail of width=dimen, height=dimen or width, height = dimen (depending on the type of dimen) Returns the image as a bytestring ''' if dimen is not None: if hasattr(dimen, '__len__'): width, height = dimen else: width = height = dimen data = thumbnail(data, width=width, height=height, compression_quality=90)[-1] else: # Replace transparent pixels with white pixels and convert to JPEG data = save_cover_data_to(data, 'img.jpg', return_data=True) if len(data) <= maxsizeb: return data orig_data = data img = Image() quality = 95 img.load(data) while len(data) >= maxsizeb and quality >= 10: quality -= 5 img.set_compression_quality(quality) data = img.export('jpg') if len(data) <= maxsizeb: return data orig_data = data scale = 0.9 while len(data) >= maxsizeb and scale >= 0.05: img = Image() img.load(orig_data) w, h = img.size img.size = (int(scale*w), int(scale*h)) img.set_compression_quality(quality) data = img.export('jpg') scale -= 0.05 return data
def process_result(log, result): plugin, data = result try: im = Image() im.load(data) if getattr(plugin, "auto_trim_covers", False): im.trim(10) width, height = im.size fmt = im.format if width < 50 or height < 50: raise ValueError("Image too small") data = save_cover_data_to(im, "/cover.jpg", return_data=True) except: log.exception("Invalid cover from", plugin.name) return None return (plugin, width, height, fmt, data)
def get_cover(self, id, thumbnail=False, thumb_width=60, thumb_height=80): try: cherrypy.response.headers['Content-Type'] = 'image/jpeg' cherrypy.response.timeout = 3600 cover = self.db.cover(id, index_is_id=True) if cover is None: cover = self.default_cover updated = self.build_time else: updated = self.db.cover_last_modified(id, index_is_id=True) cherrypy.response.headers['Last-Modified'] = self.last_modified( updated) if thumbnail: quality = tweaks[ 'content_server_thumbnail_compression_quality'] if quality < 50: quality = 50 elif quality > 99: quality = 99 return generate_thumbnail(cover, width=thumb_width, height=thumb_height, compression_quality=quality)[-1] img = Image() img.load(cover) width, height = img.size scaled, width, height = fit_image( width, height, thumb_width if thumbnail else self.max_cover_width, thumb_height if thumbnail else self.max_cover_height) if not scaled: return cover return save_cover_data_to(img, 'img.jpg', return_data=True, resize_to=(width, height)) except Exception as err: import traceback cherrypy.log.error('Failed to generate cover:') cherrypy.log.error(traceback.print_exc()) raise cherrypy.HTTPError(404, 'Failed to generate cover: %r' % err)
def image_to_hexstring(self, data): data = save_cover_data_to(data, 'cover.jpg', return_data=True) width, height = identify_data(data)[:2] raw_hex = '' for char in data: raw_hex += hex(ord(char)).replace('0x', '').rjust(2, '0') # Images must be broken up so that they are no longer than 129 chars # per line hex_string = '' col = 1 for char in raw_hex: if col == 129: hex_string += '\n' col = 1 col += 1 hex_string += char return (hex_string, width, height)
def image_to_hexstring(self, data): data = save_cover_data_to(data, 'cover.jpg', return_data=True) width, height = identify_data(data)[:2] raw_hex = '' for char in data: raw_hex += hex(ord(char)).replace('0x', '').rjust(2, '0') # Images must be broken up so that they are no longer than 129 chars # per line hex_string = '' col = 1 for char in raw_hex: if col == 129: hex_string += '\n' col = 1 col += 1 hex_string += char return (hex_string, width, height)
def _encode_into_jpeg(data): data = save_cover_data_to(data, 'cover.jpg', return_data=True) return b64encode(data)
def convert(self, oeb_book, output_path, input_plugin, opts, log): from lxml import etree from calibre.ebooks.oeb.base import OEB_IMAGES, SVG_MIME from calibre.ebooks.metadata.opf2 import OPF, metadata_to_opf from calibre.utils.zipfile import ZipFile from calibre.utils.filenames import ascii_filename # HTML if opts.htmlz_css_type == 'inline': from calibre.ebooks.htmlz.oeb2html import OEB2HTMLInlineCSSizer OEB2HTMLizer = OEB2HTMLInlineCSSizer elif opts.htmlz_css_type == 'tag': from calibre.ebooks.htmlz.oeb2html import OEB2HTMLNoCSSizer OEB2HTMLizer = OEB2HTMLNoCSSizer else: from calibre.ebooks.htmlz.oeb2html import OEB2HTMLClassCSSizer as OEB2HTMLizer with TemporaryDirectory(u'_htmlz_output') as tdir: htmlizer = OEB2HTMLizer(log) html = htmlizer.oeb2html(oeb_book, opts) fname = u'index' if opts.htmlz_title_filename: from calibre.utils.filenames import shorten_components_to fname = shorten_components_to( 100, (ascii_filename(unicode(oeb_book.metadata.title[0])), ))[0] with open(os.path.join(tdir, fname + u'.html'), 'wb') as tf: tf.write(html) # CSS if opts.htmlz_css_type == 'class' and opts.htmlz_class_style == 'external': with open(os.path.join(tdir, u'style.css'), 'wb') as tf: tf.write(htmlizer.get_css(oeb_book)) # Images images = htmlizer.images if images: if not os.path.exists(os.path.join(tdir, u'images')): os.makedirs(os.path.join(tdir, u'images')) for item in oeb_book.manifest: if item.media_type in OEB_IMAGES and item.href in images: if item.media_type == SVG_MIME: data = unicode( etree.tostring(item.data, encoding=unicode)) else: data = item.data fname = os.path.join(tdir, u'images', images[item.href]) with open(fname, 'wb') as img: img.write(data) # Cover cover_path = None try: cover_data = None if oeb_book.metadata.cover: term = oeb_book.metadata.cover[0].term cover_data = oeb_book.guide[term].item.data if cover_data: from calibre.utils.magick.draw import save_cover_data_to cover_path = os.path.join(tdir, u'cover.jpg') with open(cover_path, 'w') as cf: cf.write('') save_cover_data_to(cover_data, cover_path) except: import traceback traceback.print_exc() # Metadata with open(os.path.join(tdir, u'metadata.opf'), 'wb') as mdataf: opf = OPF(StringIO(etree.tostring( oeb_book.metadata.to_opf1()))) mi = opf.to_book_metadata() if cover_path: mi.cover = u'cover.jpg' mdataf.write(metadata_to_opf(mi)) htmlz = ZipFile(output_path, 'w') htmlz.add_dir(tdir)
def _write_new_cover(new_cdata, cpath): from calibre.utils.magick.draw import save_cover_data_to new_cover = PersistentTemporaryFile(suffix=os.path.splitext(cpath)[1]) new_cover.close() save_cover_data_to(new_cdata, new_cover.name) return new_cover
def _encode_into_jpeg(data): data = save_cover_data_to(data, 'cover.jpg', return_data=True) return b64encode(data)
def convert(self, oeb_book, output_path, input_plugin, opts, log): from lxml import etree from calibre.ebooks.oeb.base import OEB_IMAGES, SVG_MIME from calibre.ebooks.metadata.opf2 import OPF, metadata_to_opf from calibre.utils.zipfile import ZipFile from calibre.utils.filenames import ascii_filename # HTML if opts.htmlz_css_type == 'inline': from calibre.ebooks.htmlz.oeb2html import OEB2HTMLInlineCSSizer OEB2HTMLizer = OEB2HTMLInlineCSSizer elif opts.htmlz_css_type == 'tag': from calibre.ebooks.htmlz.oeb2html import OEB2HTMLNoCSSizer OEB2HTMLizer = OEB2HTMLNoCSSizer else: from calibre.ebooks.htmlz.oeb2html import OEB2HTMLClassCSSizer as OEB2HTMLizer with TemporaryDirectory(u'_htmlz_output') as tdir: htmlizer = OEB2HTMLizer(log) html = htmlizer.oeb2html(oeb_book, opts) fname = u'index' if opts.htmlz_title_filename: from calibre.utils.filenames import shorten_components_to fname = shorten_components_to(100, (ascii_filename(unicode(oeb_book.metadata.title[0])),))[0] with open(os.path.join(tdir, fname+u'.html'), 'wb') as tf: tf.write(html) # CSS if opts.htmlz_css_type == 'class' and opts.htmlz_class_style == 'external': with open(os.path.join(tdir, u'style.css'), 'wb') as tf: tf.write(htmlizer.get_css(oeb_book)) # Images images = htmlizer.images if images: if not os.path.exists(os.path.join(tdir, u'images')): os.makedirs(os.path.join(tdir, u'images')) for item in oeb_book.manifest: if item.media_type in OEB_IMAGES and item.href in images: if item.media_type == SVG_MIME: data = unicode(etree.tostring(item.data, encoding=unicode)) else: data = item.data fname = os.path.join(tdir, u'images', images[item.href]) with open(fname, 'wb') as img: img.write(data) # Cover cover_path = None try: cover_data = None if oeb_book.metadata.cover: term = oeb_book.metadata.cover[0].term cover_data = oeb_book.guide[term].item.data if cover_data: from calibre.utils.magick.draw import save_cover_data_to cover_path = os.path.join(tdir, u'cover.jpg') with open(cover_path, 'w') as cf: cf.write('') save_cover_data_to(cover_data, cover_path) except: import traceback traceback.print_exc() # Metadata with open(os.path.join(tdir, u'metadata.opf'), 'wb') as mdataf: opf = OPF(StringIO(etree.tostring(oeb_book.metadata.to_opf1()))) mi = opf.to_book_metadata() if cover_path: mi.cover = u'cover.jpg' mdataf.write(metadata_to_opf(mi)) htmlz = ZipFile(output_path, 'w') htmlz.add_dir(tdir)
def generate_cover_for_book(self, mi, saved_setting_name=None, path_to_cover=None): ''' This method is designed to be called from other plugins It is a convenience wrapper to generate a cover for the specified book to a path. The caller must take responsibility for updating the UI to ensure the book details panel and cover flow if visible are updated if overwriting a book's cover (see _generate_cover above). mi - the metadata object for the book to convert. e.g. db.get_metadata(idx) saved_setting_name - the saved cover profile name within this plugin to use if not specified uses the last cover settings of the plugin path_to_cover - the absolute filepath for where to write the cover image if not specified overwrites the image in the calibre library for this book {Return value} - the Magick image object, None if operation aborted ''' if not mi: if DEBUG: print( 'Generate Cover Error: Missing mi parameter in call to generate_cover_for_book' ) if getattr(self, 'gui', None): return error_dialog( self.gui, 'Cannot generate cover', 'Missing metadata passed for book to generate cover', show=True) return None options = cfg.plugin_prefs[cfg.STORE_CURRENT] if saved_setting_name: if saved_setting_name not in cfg.plugin_prefs[ cfg.STORE_SAVED_SETTINGS]: if DEBUG: print( 'Generate Cover Error: Saved setting %s not found in generate_cover_for_book' % saved_setting_name) if getattr(self, 'gui', None): return error_dialog(self.gui, 'Cannot generate cover', 'No cover settings exist named: "%s"' % saved_setting_name, show=True) return None options = cfg.plugin_prefs[ cfg.STORE_SAVED_SETTINGS][saved_setting_name] db = self.gui.current_db if path_to_cover: mi._path_to_cover = path_to_cover cover_data = generate_cover_for_book(mi, options=options, db=db.new_api) if not path_to_cover and db: # Overwrite the cover for this particular book db.set_cover(mi.id, cover_data) else: # The caller wants the cover written to a specific location if callable(getattr(cover_data, 'read', None)): cover_data = cover_data.read() from calibre.utils.magick.draw import save_cover_data_to save_cover_data_to(cover_data, path_to_cover) return cover_data
def get_metadata(stream): from calibre.ebooks.metadata import MetaInformation from calibre.ptempfile import TemporaryDirectory from calibre.ebooks.mobi.reader.headers import MetadataHeader from calibre.ebooks.mobi.reader.mobi6 import MobiReader from calibre.utils.magick.draw import save_cover_data_to from calibre import CurrentDir stream.seek(0) try: raw = stream.read(3) except: raw = '' stream.seek(0) if raw == b'TPZ': from calibre.ebooks.metadata.topaz import get_metadata return get_metadata(stream) from calibre.utils.logging import Log log = Log() try: mi = MetaInformation(os.path.basename(stream.name), [_('Unknown')]) except: mi = MetaInformation(_('Unknown'), [_('Unknown')]) mh = MetadataHeader(stream, log) if mh.title and mh.title != _('Unknown'): mi.title = mh.title if mh.exth is not None: if mh.exth.mi is not None: mi = mh.exth.mi else: size = 1024**3 if hasattr(stream, 'seek') and hasattr(stream, 'tell'): pos = stream.tell() stream.seek(0, 2) size = stream.tell() stream.seek(pos) if size < 4 * 1024 * 1024: with TemporaryDirectory('_mobi_meta_reader') as tdir: with CurrentDir(tdir): mr = MobiReader(stream, log) parse_cache = {} mr.extract_content(tdir, parse_cache) if mr.embedded_mi is not None: mi = mr.embedded_mi if hasattr(mh.exth, 'cover_offset'): cover_index = mh.first_image_index + mh.exth.cover_offset data = mh.section_data(int(cover_index)) else: try: data = mh.section_data(mh.first_image_index) except: data = '' if data and what(None, data) in {'jpg', 'jpeg', 'gif', 'png', 'bmp', 'webp'}: try: mi.cover_data = ('jpg', save_cover_data_to(data, 'cover.jpg', return_data=True)) except Exception: log.exception('Failed to read MOBI cover') return mi