From 6658ccfcf19eb423b64276c0895cbae0a75a46f1 Mon Sep 17 00:00:00 2001 From: David Date: Sat, 21 Jan 2017 23:18:54 +1100 Subject: [PATCH] Rework cover upload Reworked the cover image generation for uploading. There was an error in calculating the size, the Glo HD sizes needed to be diffrent, the cover file was being read multiple times and the full size was not being resized when it should have. Also, some refactoring was done at the request of someone subclassing the driver. Lastly, the firmware build version has been bumped to match the latest release from Kobo. --- src/calibre/devices/kobo/driver.py | 239 ++++++++++++++++++----------- 1 file changed, 149 insertions(+), 90 deletions(-) diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index 63216b4a39..734c0e9730 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -65,7 +65,7 @@ class KOBO(USBMS): gui_name = 'Kobo Reader' description = _('Communicate with the Kobo Reader') author = 'Timothy Legge and David Forrester' - version = (2, 3, 2) + version = (2, 3, 3) dbversion = 0 fwversion = (0,0,0) @@ -1325,7 +1325,7 @@ class KOBOTOUCH(KOBO): # Starting with firmware version 3.19.x, the last number appears to be is a # build number. A number will be recorded here but it can be safely ignored # when testing the firmware version. - max_supported_fwversion = (4, 2, 8094) + max_supported_fwversion = (4, 2, 8283) # The following document firwmare versions where new function or devices were added. # Not all are used, but this feels a good place to record it. min_fwversion_shelves = (2, 0, 0) @@ -1397,19 +1397,29 @@ class KOBOTOUCH(KOBO): # Used for Details screen from FW2.8.1 ' - AndroidBookLoadTablet_Aspect.parsed':[(355,479), 88, 100,False,], } + GLO_HD_COVER_FILE_ENDINGS = { + # Used for screensaver, home screen + ' - N3_FULL.parsed': [(1072,1448), 0, 200,True,], + # Used for Details screen before FW2.8.1, then for current book tile on home screen + ' - N3_LIBRARY_FULL.parsed':[(355, 479), 0, 200,False,], + # Used for library lists + ' - N3_LIBRARY_GRID.parsed':[(149, 201), 0, 200,False,], + # Used for Details screen from FW2.8.1 + ' - AndroidBookLoadTablet_Aspect.parsed':[(355, 471), 88, 100,False,], + } AURA_HD_COVER_FILE_ENDINGS = { # Used for screensaver, home screen - ' - N3_FULL.parsed': [(1080,1440), 0, 200,True,], # Used for screensaver, home screen + ' - N3_FULL.parsed': [(1080,1440), 0, 200,True,], # Used for Details screen before FW2.8.1, then for current book tile on home screen ' - N3_LIBRARY_FULL.parsed':[(355, 471), 0, 200,False,], # Used for library lists - ' - N3_LIBRARY_GRID.parsed':[(149, 198), 0, 200,False,], # Used for library lists + ' - N3_LIBRARY_GRID.parsed':[(149, 198), 0, 200,False,], # Used for Details screen from FW2.8.1 ' - AndroidBookLoadTablet_Aspect.parsed':[(355, 471), 88, 100,False,], } AURA_ONE_COVER_FILE_ENDINGS = { # Used for screensaver, home screen - ' - N3_FULL.parsed': [(1404,1872), 0, 200,True,], # Used for screensaver, home screen + ' - N3_FULL.parsed': [(1404,1872), 0, 200,True,], # Used for Details screen before FW2.8.1, then for current book tile on home screen ' - N3_LIBRARY_FULL.parsed':[(355, 473), 0, 200,False,], # Used for library lists @@ -2390,7 +2400,7 @@ class KOBOTOUCH(KOBO): if not self.upload_covers: # Building thumbnails disabled - # debug_print('KoboTouch: not uploading cover') + # debug_print('KoboTouch: not uploading cover') return # Only upload covers to SD card if that is supported @@ -2428,87 +2438,116 @@ class KOBOTOUCH(KOBO): path = os.path.join(path, imageId) return path - def _upload_cover(self, path, filename, metadata, filepath, uploadgrayscale, keep_cover_aspect=False): - from calibre.utils.img import save_cover_data_to - from calibre.utils.imghdr import identify - debug_print("KoboTouch:_upload_cover - filename='%s' uploadgrayscale='%s' "%(filename, uploadgrayscale)) - - if metadata.cover: - show_debug = self.is_debugging_title(filename) - if show_debug: - debug_print("KoboTouch:_upload_cover - path='%s'"%path, "filename='%s'"%filename) - debug_print(" filepath='%s'"%filepath) - cover = self.normalize_path(metadata.cover.replace('/', os.sep)) - - if os.path.exists(cover): - # Get ContentID for Selected Book - extension = os.path.splitext(filepath)[1] - ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(filepath) - ContentID = self.contentid_from_path(filepath, ContentType) - - try: - with closing(self.device_database_connection()) as connection: - - cursor = connection.cursor() - t = (ContentID,) - cursor.execute('select ImageId from Content where BookID is Null and ContentID = ?', t) - try: - result = cursor.next() - ImageID = result[0] - except StopIteration: - ImageID = self.imageid_from_contentid(ContentID) - debug_print("KoboTouch:_upload_cover - No rows exist in the database - generated ImageID='%s'" % ImageID) - - cursor.close() - - if ImageID is not None: - path = self.images_path(path, ImageID) - - if show_debug: - debug_print("KoboTouch:_upload_cover - About to loop over cover endings") - - image_dir = os.path.dirname(os.path.abspath(path)) - if not os.path.exists(image_dir): - debug_print("KoboTouch:_upload_cover - Image directory does not exust. Creating path='%s'" % (image_dir)) - os.makedirs(image_dir) - - for ending, cover_options in self.cover_file_endings().items(): - resize, min_dbversion, max_dbversion, isFullsize = cover_options - if show_debug: - debug_print("KoboTouch:_upload_cover - resize=%s min_dbversion=%d max_dbversion=%d" % (resize, min_dbversion, max_dbversion)) - if self.dbversion >= min_dbversion and self.dbversion <= max_dbversion: - if show_debug: - debug_print("KoboTouch:_upload_cover - creating cover for ending='%s'"%ending) # , "resize'%s'"%resize) - fpath = path + ending - fpath = self.normalize_path(fpath.replace('/', os.sep)) - - with lopen(cover, 'rb') as f: - data = f.read() - - if keep_cover_aspect: - if isFullsize: - resize = None - else: - fmt, width, height = identify(data) - cover_aspect = width / height - if cover_aspect > 1: - resize = (resize[0], int(resize[0] / cover_aspect)) - elif cover_aspect < 1: - resize = (int(cover_aspect * resize[1]), resize[1]) - - # Return the data resized and in Grayscale if - # required - data = save_cover_data_to(data, grayscale=uploadgrayscale, resize_to=resize) - - with lopen(fpath, 'wb') as f: - f.write(data) - fsync(f) - except Exception as e: - err = str(e) - debug_print("KoboTouch:_upload_cover - Exception string: %s"%err) - raise + def _calculate_kobo_cover_size(self, library_size, kobo_size, keep_cover_aspect, is_full_size): + if keep_cover_aspect: + library_aspect = library_size[0] / library_size[1] + kobo_aspect = kobo_size[0] / kobo_size[1] + if library_aspect > kobo_aspect: + kobo_size = (kobo_size[0], int(kobo_size[0] / library_aspect)) else: - debug_print("KoboTouch:_upload_cover - ImageID could not be retrieved from the database") + kobo_size = (int(library_aspect * kobo_size[1]), kobo_size[1]) + return kobo_size + + + def _create_cover_data(self, cover_data, resize_to, kobo_size, upload_grayscale=False, keep_cover_aspect=False, is_full_size=False): + ''' + This will generate the new cover image from the cover in the library. It is a wrapper + for save_cover_data_to to allow it to be overriden in a subclass. For this reason, + options are passed in that are not used by this implementation. + + :param cover_data: original cover data + :param resize_to: Size to resize the cover to (width, height). None means do not resize. + :param kobo_size: Size of the cover image on the device. + :param upload_grayscale: boolean True if driver configured to send grayscale thumbnails + Passed to allow ability to decide to quantize to 16-col grayscale + at calibre end + :param keep_cover_aspect: bookean - True if the aspect ratio of the cover in the library is to be kept. + :param is_full_size: True if this is the kobo_size is for the full size cover image + Passed to allow ability to process screensaver differently + to smaller thumbnails + ''' + + from calibre.utils.img import save_cover_data_to + data = save_cover_data_to(cover_data, grayscale=upload_grayscale, resize_to=resize_to) + return data + + def _upload_cover(self, path, filename, metadata, filepath, upload_grayscale, keep_cover_aspect=False): + from calibre.utils.imghdr import identify + debug_print("KoboTouch:_upload_cover - filename='%s' upload_grayscale='%s' "%(filename, upload_grayscale)) + + if not metadata.cover: + return + + show_debug = self.is_debugging_title(filename) + if show_debug: + debug_print("KoboTouch:_upload_cover - path='%s'"%path, "filename='%s'"%filename) + debug_print(" filepath='%s'"%filepath) + cover = self.normalize_path(metadata.cover.replace('/', os.sep)) + + if not os.path.exists(cover): + debug_print("KoboTouch:_upload_cover - Cover file does not exist in library") + return + + # Get ContentID for Selected Book + extension = os.path.splitext(filepath)[1] + ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(filepath) + ContentID = self.contentid_from_path(filepath, ContentType) + + try: + with closing(self.device_database_connection()) as connection: + + cursor = connection.cursor() + t = (ContentID,) + cursor.execute('select ImageId from Content where BookID is Null and ContentID = ?', t) + try: + result = cursor.next() + ImageID = result[0] + except StopIteration: + ImageID = self.imageid_from_contentid(ContentID) + debug_print("KoboTouch:_upload_cover - No rows exist in the database - generated ImageID='%s'" % ImageID) + + cursor.close() + + if ImageID is not None: + path = self.images_path(path, ImageID) + + if show_debug: + debug_print("KoboTouch:_upload_cover - About to loop over cover endings") + + image_dir = os.path.dirname(os.path.abspath(path)) + if not os.path.exists(image_dir): + debug_print("KoboTouch:_upload_cover - Image directory does not exust. Creating path='%s'" % (image_dir)) + os.makedirs(image_dir) + + with lopen(cover, 'rb') as f: + cover_data = f.read() + + fmt, width, height = identify(cover_data) + library_cover_size = (width, height) + + for ending, cover_options in self.cover_file_endings().items(): + kobo_size, min_dbversion, max_dbversion, is_full_size = cover_options + if show_debug: + debug_print("KoboTouch:_upload_cover - library_cover_size=%s min_dbversion=%d max_dbversion=%d" % (library_cover_size, min_dbversion, max_dbversion)) + if self.dbversion >= min_dbversion and self.dbversion <= max_dbversion: + if show_debug: + debug_print("KoboTouch:_upload_cover - creating cover for ending='%s'"%ending) # , "library_cover_size'%s'"%library_cover_size) + fpath = path + ending + fpath = self.normalize_path(fpath.replace('/', os.sep)) + + resize_to = self._calculate_kobo_cover_size(library_cover_size, kobo_size, keep_cover_aspect, is_full_size) + + # Return the data resized and in Grayscale if required + data = self._create_cover_data(cover_data, resize_to, kobo_size, upload_grayscale, keep_cover_aspect, is_full_size) + + with lopen(fpath, 'wb') as f: + f.write(data) + fsync(f) + except Exception as e: + err = str(e) + debug_print("KoboTouch:_upload_cover - Exception string: %s"%err) + raise + def remove_book_from_device_bookshelves(self, connection, book): show_debug = self.is_debugging_title(book.title) # or True @@ -2914,10 +2953,30 @@ class KOBOTOUCH(KOBO): return self.detected_device.idProduct in self.TOUCH2_PRODUCT_ID def cover_file_endings(self): - return self.GLO_COVER_FILE_ENDINGS if self.isGlo() or self.isAura() or self.isAuraEdition2() \ - else self.AURA_HD_COVER_FILE_ENDINGS if self.isAuraHD() or self.isAuraH2O() or self.isGloHD() \ - else self.AURA_ONE_COVER_FILE_ENDINGS if self.isAuraOne() \ - else self.COVER_FILE_ENDINGS + if self.isAura(): + _cover_file_endings = self.GLO_COVER_FILE_ENDINGS + elif self.isAuraEdition2(): + _cover_file_endings = self.GLO_COVER_FILE_ENDINGS + elif self.isAuraHD(): + _cover_file_endings = self.AURA_HD_COVER_FILE_ENDINGS + elif self.isAuraH2O(): + _cover_file_endings = self.AURA_HD_COVER_FILE_ENDINGS + elif self.isAuraOne(): + _cover_file_endings = self.AURA_ONE_COVER_FILE_ENDINGS + elif self.isGlo(): + _cover_file_endings = self.GLO_COVER_FILE_ENDINGS + elif self.isGloHD(): + _cover_file_endings = self.GLO_HD_COVER_FILE_ENDINGS + elif self.isMini(): + _cover_file_endings = self.COVER_FILE_ENDINGS + elif self.isTouch(): + _cover_file_endings = self.COVER_FILE_ENDINGS + elif self.isTouch2(): + _cover_file_endings = self.COVER_FILE_ENDINGS + else: + _cover_file_endings = self.COVER_FILE_ENDINGS + + return _cover_file_endings def set_device_name(self): device_name = self.gui_name