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.
This commit is contained in:
David 2017-01-21 23:18:54 +11:00
parent 0073fbcc19
commit 6658ccfcf1

View File

@ -65,7 +65,7 @@ class KOBO(USBMS):
gui_name = 'Kobo Reader' gui_name = 'Kobo Reader'
description = _('Communicate with the Kobo Reader') description = _('Communicate with the Kobo Reader')
author = 'Timothy Legge and David Forrester' author = 'Timothy Legge and David Forrester'
version = (2, 3, 2) version = (2, 3, 3)
dbversion = 0 dbversion = 0
fwversion = (0,0,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 # 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 # build number. A number will be recorded here but it can be safely ignored
# when testing the firmware version. # 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. # 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. # Not all are used, but this feels a good place to record it.
min_fwversion_shelves = (2, 0, 0) min_fwversion_shelves = (2, 0, 0)
@ -1397,19 +1397,29 @@ class KOBOTOUCH(KOBO):
# Used for Details screen from FW2.8.1 # Used for Details screen from FW2.8.1
' - AndroidBookLoadTablet_Aspect.parsed':[(355,479), 88, 100,False,], ' - 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 = { AURA_HD_COVER_FILE_ENDINGS = {
# Used for screensaver, home screen # 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 # 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,], ' - N3_LIBRARY_FULL.parsed':[(355, 471), 0, 200,False,],
# Used for library lists # 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 # Used for Details screen from FW2.8.1
' - AndroidBookLoadTablet_Aspect.parsed':[(355, 471), 88, 100,False,], ' - AndroidBookLoadTablet_Aspect.parsed':[(355, 471), 88, 100,False,],
} }
AURA_ONE_COVER_FILE_ENDINGS = { AURA_ONE_COVER_FILE_ENDINGS = {
# Used for screensaver, home screen # 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 # 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,], ' - N3_LIBRARY_FULL.parsed':[(355, 473), 0, 200,False,],
# Used for library lists # Used for library lists
@ -2390,7 +2400,7 @@ class KOBOTOUCH(KOBO):
if not self.upload_covers: if not self.upload_covers:
# Building thumbnails disabled # Building thumbnails disabled
# debug_print('KoboTouch: not uploading cover') # debug_print('KoboTouch: not uploading cover')
return return
# Only upload covers to SD card if that is supported # Only upload covers to SD card if that is supported
@ -2428,87 +2438,116 @@ class KOBOTOUCH(KOBO):
path = os.path.join(path, imageId) path = os.path.join(path, imageId)
return path return path
def _upload_cover(self, path, filename, metadata, filepath, uploadgrayscale, keep_cover_aspect=False): def _calculate_kobo_cover_size(self, library_size, kobo_size, keep_cover_aspect, is_full_size):
from calibre.utils.img import save_cover_data_to if keep_cover_aspect:
from calibre.utils.imghdr import identify library_aspect = library_size[0] / library_size[1]
debug_print("KoboTouch:_upload_cover - filename='%s' uploadgrayscale='%s' "%(filename, uploadgrayscale)) kobo_aspect = kobo_size[0] / kobo_size[1]
if library_aspect > kobo_aspect:
if metadata.cover: kobo_size = (kobo_size[0], int(kobo_size[0] / library_aspect))
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
else: 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): def remove_book_from_device_bookshelves(self, connection, book):
show_debug = self.is_debugging_title(book.title) # or True 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 return self.detected_device.idProduct in self.TOUCH2_PRODUCT_ID
def cover_file_endings(self): def cover_file_endings(self):
return self.GLO_COVER_FILE_ENDINGS if self.isGlo() or self.isAura() or self.isAuraEdition2() \ if self.isAura():
else self.AURA_HD_COVER_FILE_ENDINGS if self.isAuraHD() or self.isAuraH2O() or self.isGloHD() \ _cover_file_endings = self.GLO_COVER_FILE_ENDINGS
else self.AURA_ONE_COVER_FILE_ENDINGS if self.isAuraOne() \ elif self.isAuraEdition2():
else self.COVER_FILE_ENDINGS _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): def set_device_name(self):
device_name = self.gui_name device_name = self.gui_name