mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 10:14:46 -04:00
Greater control over Kobo covers processing.
* Optional dithering down to the exact eInk color palette. Note that, on some devices, FW >= 4.11 will do a much better job of it than us. That's true on the H2O, for instance, but not on the Forma, where it only does so on thumbnails, not on the sleep cover... When it does, grayscaling is done in sRGB space, and not linear light, like we do, but that's a much more minor issue, and one where no-one is actually wrong ;). Depends on B&W covers to avoid nonsensical results. * Optional letterboxing of full-screen covers to avoid extra Nickel processing. Depends on Keep AR to avoid nonsensical results. * Optional storage as PNG to avoid JPG wrecking the dithering Depends on B&W covers to avoid storing stupidly large color PNGs. * Fix rounding errors when calculating thumbnail sizes: AR should be a float, and we want to honor the *height* expected by Nickel when there's potential for rounding mistakes (which is pretty much always for thumbnails, given that the perfect AR should be 0.75). Meaning we'll want to round properly before truncating ;). * Fix thumbnail sizes on the Forma. Apparently, quite a few bits of the FW behave as if the screen was Aura One sized... Try to do it right on our end instead of following suit ;). Unfortunately, full-screen cover processing is slightly broken on nickel's side right now: it appears to be treating them as Aura One sized, which incurs an ugly and unavoidable scaling pass, one way or the other... c.f., http://www.mobileread.com/forums/showpost.php?p=3025725&postcount=225 and the few pages around it.
This commit is contained in:
parent
c415bb9f08
commit
832ab64c63
@ -3,11 +3,11 @@
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2010-2012, Timothy Legge <timlegge@gmail.com>, Kovid Goyal <kovid@kovidgoyal.net> and David Forrester <davidfor@internode.on.net>'
|
||||
__copyright__ = '2010-2018, Timothy Legge <timlegge@gmail.com>, Kovid Goyal <kovid@kovidgoyal.net> and David Forrester <davidfor@internode.on.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
'''
|
||||
Driver for Kobo ereaders. Supports all e-ink devices.
|
||||
Driver for Kobo eReaders. Supports all e-ink devices.
|
||||
|
||||
Originally developed by Timothy Legge <timlegge@gmail.com>.
|
||||
Extended to support Touch firmware 2.0.0 and later and newer devices by David Forrester <davidfor@internode.on.net>
|
||||
@ -154,10 +154,13 @@ class KOBO(USBMS):
|
||||
OPT_COLLECTIONS = 0
|
||||
OPT_UPLOAD_COVERS = 1
|
||||
OPT_UPLOAD_GRAYSCALE_COVERS = 2
|
||||
OPT_SHOW_EXPIRED_BOOK_RECORDS = 3
|
||||
OPT_SHOW_PREVIEWS = 4
|
||||
OPT_SHOW_RECOMMENDATIONS = 5
|
||||
OPT_SUPPORT_NEWER_FIRMWARE = 6
|
||||
OPT_DITHERED_COVERS = 3
|
||||
OPT_LETTERBOX_FULLSCREEN_COVERS = 4
|
||||
OPT_PNG_COVERS = 5
|
||||
OPT_SHOW_EXPIRED_BOOK_RECORDS = 6
|
||||
OPT_SHOW_PREVIEWS = 7
|
||||
OPT_SHOW_RECOMMENDATIONS = 8
|
||||
OPT_SUPPORT_NEWER_FIRMWARE = 9
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
USBMS.__init__(self, *args, **kwargs)
|
||||
@ -998,13 +1001,28 @@ class KOBO(USBMS):
|
||||
else:
|
||||
uploadgrayscale = True
|
||||
|
||||
if not opts.extra_customization[self.OPT_DITHERED_COVERS]:
|
||||
ditheredcovers = False
|
||||
else:
|
||||
ditheredcovers = True
|
||||
|
||||
if not opts.extra_customization[self.OPT_LETTERBOX_FULLSCREEN_COVERS]:
|
||||
letterboxcovers = False
|
||||
else:
|
||||
letterboxcovers = True
|
||||
|
||||
if not opts.extra_customization[self.OPT_PNG_COVERS]:
|
||||
pngcovers = False
|
||||
else:
|
||||
pngcovers = True
|
||||
|
||||
debug_print('KOBO: uploading cover')
|
||||
try:
|
||||
self._upload_cover(path, filename, metadata, filepath, uploadgrayscale)
|
||||
self._upload_cover(path, filename, metadata, filepath, uploadgrayscale, ditheredcovers, letterboxcovers, pngcovers)
|
||||
except:
|
||||
debug_print('FAILED to upload cover', filepath)
|
||||
|
||||
def _upload_cover(self, path, filename, metadata, filepath, uploadgrayscale):
|
||||
def _upload_cover(self, path, filename, metadata, filepath, uploadgrayscale, ditheredcovers, letterboxcovers, pngcovers):
|
||||
from calibre.utils.img import save_cover_data_to
|
||||
if metadata.cover:
|
||||
cover = self.normalize_path(metadata.cover.replace('/', os.sep))
|
||||
@ -1051,9 +1069,9 @@ class KOBO(USBMS):
|
||||
with lopen(cover, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
# Return the data resized and in Grayscale if
|
||||
# Return the data resized and grayscaled/dithered/letterboxed if
|
||||
# required
|
||||
data = save_cover_data_to(data, grayscale=uploadgrayscale, resize_to=resize)
|
||||
data = save_cover_data_to(data, grayscale=uploadgrayscale, eink=ditheredcovers, resize_to=resize, minify_to=resize, letterbox=letterboxcovers, data_fmt="png" if pngcovers else "jpeg")
|
||||
|
||||
with lopen(fpath, 'wb') as f:
|
||||
f.write(data)
|
||||
@ -1098,10 +1116,13 @@ class KOBO(USBMS):
|
||||
OPT_COLLECTIONS = 0
|
||||
OPT_UPLOAD_COVERS = 1
|
||||
OPT_UPLOAD_GRAYSCALE_COVERS = 2
|
||||
OPT_SHOW_EXPIRED_BOOK_RECORDS = 3
|
||||
OPT_SHOW_PREVIEWS = 4
|
||||
OPT_SHOW_RECOMMENDATIONS = 5
|
||||
OPT_SUPPORT_NEWER_FIRMWARE = 6
|
||||
OPT_DITHERED_COVERS = 3
|
||||
OPT_LETTERBOX_FULLSCREEN_COVERS = 4
|
||||
OPT_PNG_COVERS = 5
|
||||
OPT_SHOW_EXPIRED_BOOK_RECORDS = 6
|
||||
OPT_SHOW_PREVIEWS = 7
|
||||
OPT_SHOW_RECOMMENDATIONS = 8
|
||||
OPT_SUPPORT_NEWER_FIRMWARE = 9
|
||||
|
||||
p = {}
|
||||
p['format_map'] = old_settings.format_map
|
||||
@ -1115,6 +1136,9 @@ class KOBO(USBMS):
|
||||
|
||||
p['upload_covers'] = old_settings.extra_customization[OPT_UPLOAD_COVERS]
|
||||
p['upload_grayscale'] = old_settings.extra_customization[OPT_UPLOAD_GRAYSCALE_COVERS]
|
||||
p['dithered_covers'] = old_settings.extra_customization[OPT_DITHERED_COVERS]
|
||||
p['letterbox_fs_covers'] = old_settings.extra_customization[OPT_LETTERBOX_FULLSCREEN_COVERS]
|
||||
p['png_covers'] = old_settings.extra_customization[OPT_PNG_COVERS]
|
||||
|
||||
p['show_expired_books'] = old_settings.extra_customization[OPT_SHOW_EXPIRED_BOOK_RECORDS]
|
||||
p['show_previews'] = old_settings.extra_customization[OPT_SHOW_PREVIEWS]
|
||||
@ -1339,12 +1363,12 @@ class KOBO(USBMS):
|
||||
|
||||
class KOBOTOUCH(KOBO):
|
||||
name = 'KoboTouch'
|
||||
gui_name = 'Kobo Touch/Glo/Mini/Aura HD/Aura H2O/Glo HD/Touch 2'
|
||||
gui_name = 'Kobo eReader'
|
||||
author = 'David Forrester'
|
||||
description = _(
|
||||
'Communicate with the Kobo Touch, Glo, Mini, Aura HD,'
|
||||
' Aura H2O, Glo HD, Touch 2, Aura ONE, Aura Edition 2,'
|
||||
' Aura H2O Edition 2, Clara HD and Forma ereaders.'
|
||||
' Aura H2O Edition 2, Clara HD and Forma eReaders.'
|
||||
' Based on the existing Kobo driver by %s.') % KOBO.author
|
||||
# icon = I('devices/kobotouch.jpg')
|
||||
|
||||
@ -1459,6 +1483,20 @@ class KOBOTOUCH(KOBO):
|
||||
# Used for Details screen from FW2.8.1
|
||||
' - AndroidBookLoadTablet_Aspect.parsed':[(355, 471), 88, 100,False,],
|
||||
}
|
||||
AURA_H2O_COVER_FILE_ENDINGS = {
|
||||
# Used for screensaver, home screen
|
||||
# NOTE: Top 11px are dead. Confirmed w/ fbgrab.
|
||||
' - N3_FULL.parsed': [(1080,1429), 0, 200,True,],
|
||||
# Used for Details screen before FW2.8.1, then for current book tile on home screen
|
||||
# NOTE: Should probably be 354x472 or 357x476 to keep honoring the 0.75 AR,
|
||||
# but that's not what Nickel does...
|
||||
' - N3_LIBRARY_FULL.parsed':[(355, 473), 0, 200,False,],
|
||||
# Used for library lists
|
||||
# NOTE: Again, 147x196 or 150x200 would match the 0.75 AR perfectly...
|
||||
' - 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,],
|
||||
@ -1469,11 +1507,24 @@ class KOBOTOUCH(KOBO):
|
||||
}
|
||||
FORMA_COVER_FILE_ENDINGS = {
|
||||
# Used for screensaver, home screen
|
||||
# NOTE: Nickel keeps generating smaller images (1404x1872) for sideloaded content,
|
||||
# and will *also* download Aura One sized images for kePubs, which is stupid.
|
||||
# What's worse is that it expects that size during the full pipeline,
|
||||
# which means sleep covers get mangled by a terrible upscaling pass...
|
||||
# Hopefully that's just a teething quirk that'll be fixed in a later FW.
|
||||
' - N3_FULL.parsed': [(1440,1920), 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,],
|
||||
# NOTE: Same thing, Nickel generates tiny thumbnails (355x473),
|
||||
# but *will* download slightly larger ones for kePubs.
|
||||
# That's still probably A1 sized, I'd expect roughly x636 instead...
|
||||
# The actual widget itself has space for a 316x421 image...
|
||||
' - N3_LIBRARY_FULL.parsed':[(398, 530), 0, 200,False,],
|
||||
# Used for library lists
|
||||
' - N3_LIBRARY_GRID.parsed':[(149, 198), 0, 200,False,], # Used for library lists
|
||||
# NOTE: Same thing, Nickel generates tiny thumbnails (149x198),
|
||||
# but downloads larger ones for kePubs.
|
||||
# Again, probably still A1 sized, I'd expect roughly x266 instead...
|
||||
# The actual widget itself has space for a 155x207 image...
|
||||
' - N3_LIBRARY_GRID.parsed':[(167, 223), 0, 200,False,], # Used for library lists
|
||||
}
|
||||
# Following are the sizes used with pre2.1.4 firmware
|
||||
# COVER_FILE_ENDINGS = {
|
||||
@ -1506,7 +1557,7 @@ class KOBOTOUCH(KOBO):
|
||||
# Just dump some info to the logs.
|
||||
super(KOBOTOUCH, self).open_osx()
|
||||
|
||||
# Wrap some debugging output in a try/except so that it unlikely to break things completely.
|
||||
# Wrap some debugging output in a try/except so that it is unlikely to break things completely.
|
||||
try:
|
||||
if DEBUG:
|
||||
from calibre.constants import plugins
|
||||
@ -2558,7 +2609,7 @@ class KOBOTOUCH(KOBO):
|
||||
|
||||
# debug_print('KoboTouch: uploading cover')
|
||||
try:
|
||||
self._upload_cover(path, filename, metadata, filepath, self.upload_grayscale, self.keep_cover_aspect)
|
||||
self._upload_cover(path, filename, metadata, filepath, self.upload_grayscale, self.dithered_covers, self.keep_cover_aspect, self.letterbox_fs_covers, self.png_covers)
|
||||
except Exception as e:
|
||||
debug_print('KoboTouch: FAILED to upload cover=%s Exception=%s'%(filepath, unicode_type(e)))
|
||||
|
||||
@ -2587,17 +2638,26 @@ class KOBOTOUCH(KOBO):
|
||||
path = os.path.join(path, imageId)
|
||||
return path
|
||||
|
||||
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:
|
||||
kobo_size = (int(library_aspect * kobo_size[1]), kobo_size[1])
|
||||
return kobo_size
|
||||
def _calculate_kobo_cover_size(self, library_size, kobo_size, keep_cover_aspect, letterbox):
|
||||
# Remember the canvas size
|
||||
canvas_size = kobo_size
|
||||
|
||||
def _create_cover_data(self, cover_data, resize_to, kobo_size, upload_grayscale=False, keep_cover_aspect=False, is_full_size=False):
|
||||
if keep_cover_aspect:
|
||||
# NOTE: Py3k wouldn't need explicit casts to return a float
|
||||
# NOTE: Ideally, the target AR should be 0.75, but that's rarely exactly the case for thumbnails,
|
||||
# which is why we try to limit accumulating even more rounding errors on top of Nickel's.
|
||||
library_aspect = library_size[0] / float(library_size[1])
|
||||
kobo_aspect = kobo_size[0] / float(kobo_size[1])
|
||||
if library_aspect > kobo_aspect:
|
||||
kobo_size = (kobo_size[0], int(round(kobo_size[0] / library_aspect)))
|
||||
else:
|
||||
kobo_size = (int(round(library_aspect * kobo_size[1])), kobo_size[1])
|
||||
# Did we actually want to letterbox?
|
||||
if not letterbox:
|
||||
canvas_size = kobo_size
|
||||
return (kobo_size, canvas_size)
|
||||
|
||||
def _create_cover_data(self, cover_data, resize_to, minify_to, kobo_size, upload_grayscale=False, dithered_covers=False, keep_cover_aspect=False, is_full_size=False, letterbox=False, png_covers=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,
|
||||
@ -2605,23 +2665,26 @@ class KOBOTOUCH(KOBO):
|
||||
|
||||
:param cover_data: original cover data
|
||||
:param resize_to: Size to resize the cover to (width, height). None means do not resize.
|
||||
:param minify_to: Maximum canvas size for the resized cover (width, height).
|
||||
: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
|
||||
:param dithered_covers: boolean True if driver configured 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 keep_cover_aspect: boolean - 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
|
||||
:param letterbox: True if we were asked to handle the letterboxing at calibre end
|
||||
:param png_covers: True if we were asked to encode those images in PNG instead of JPG
|
||||
'''
|
||||
|
||||
from calibre.utils.img import save_cover_data_to
|
||||
data = save_cover_data_to(cover_data, grayscale=upload_grayscale, resize_to=resize_to)
|
||||
data = save_cover_data_to(cover_data, resize_to=resize_to, minify_to=minify_to, grayscale=upload_grayscale, eink=dithered_covers, letterbox=letterbox, data_fmt="png" if png_covers else "jpeg")
|
||||
return data
|
||||
|
||||
def _upload_cover(self, path, filename, metadata, filepath, upload_grayscale, keep_cover_aspect=False):
|
||||
def _upload_cover(self, path, filename, metadata, filepath, upload_grayscale, dithered_covers=False, keep_cover_aspect=False, letterbox_fs_covers=False, png_covers=False):
|
||||
from calibre.utils.imghdr import identify
|
||||
debug_print("KoboTouch:_upload_cover - filename='%s' upload_grayscale='%s' "%(filename, upload_grayscale))
|
||||
debug_print("KoboTouch:_upload_cover - filename='%s' upload_grayscale='%s' dithered_covers='%s' "%(filename, upload_grayscale, dithered_covers))
|
||||
|
||||
if not metadata.cover:
|
||||
return
|
||||
@ -2664,7 +2727,7 @@ class KOBOTOUCH(KOBO):
|
||||
|
||||
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))
|
||||
debug_print("KoboTouch:_upload_cover - Image directory does not exist. Creating path='%s'" % (image_dir))
|
||||
os.makedirs(image_dir)
|
||||
|
||||
with lopen(cover, 'rb') as f:
|
||||
@ -2676,8 +2739,8 @@ class KOBOTOUCH(KOBO):
|
||||
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, is_full_size=%s" % (
|
||||
library_cover_size, min_dbversion, max_dbversion, is_full_size))
|
||||
debug_print("KoboTouch:_upload_cover - library_cover_size=%s -> kobo_size=%s, min_dbversion=%d max_dbversion=%d, is_full_size=%s" % (
|
||||
library_cover_size, kobo_size, min_dbversion, max_dbversion, is_full_size))
|
||||
|
||||
if self.dbversion >= min_dbversion and self.dbversion <= max_dbversion:
|
||||
if show_debug:
|
||||
@ -2685,10 +2748,16 @@ class KOBOTOUCH(KOBO):
|
||||
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)
|
||||
# Never letterbox thumbnails, that's ugly. But for fullscreen covers, honor the setting.
|
||||
letterbox = letterbox_fs_covers if is_full_size else False
|
||||
|
||||
# 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)
|
||||
resize_to, minify_to = self._calculate_kobo_cover_size(library_cover_size, kobo_size, keep_cover_aspect, letterbox)
|
||||
if show_debug:
|
||||
debug_print("KoboTouch:_calculate_kobo_cover_size - minify_to=%s (vs. kobo_size=%s) & resize_to=%s, keep_cover_aspect=%s & letterbox_fs_covers=%s, png_covers=%s" % (
|
||||
minify_to, kobo_size, resize_to, keep_cover_aspect, letterbox_fs_covers, png_covers))
|
||||
|
||||
# Return the data resized and properly grayscaled/dithered/letterboxed if requested
|
||||
data = self._create_cover_data(cover_data, resize_to, minify_to, kobo_size, upload_grayscale, dithered_covers, keep_cover_aspect, is_full_size, letterbox, png_covers)
|
||||
|
||||
with lopen(fpath, 'wb') as f:
|
||||
f.write(data)
|
||||
@ -3170,8 +3239,11 @@ class KOBOTOUCH(KOBO):
|
||||
c.add_opt('ignore_collections_names', default='')
|
||||
|
||||
c.add_opt('upload_covers', default=False)
|
||||
c.add_opt('dithered_covers', default=False)
|
||||
c.add_opt('keep_cover_aspect', default=False)
|
||||
c.add_opt('upload_grayscale', default=False)
|
||||
c.add_opt('letterbox_fs_covers', default=False)
|
||||
c.add_opt('png_covers', default=False)
|
||||
|
||||
c.add_opt('show_archived_books', default=False)
|
||||
c.add_opt('show_previews', default=False)
|
||||
@ -3249,7 +3321,7 @@ class KOBOTOUCH(KOBO):
|
||||
elif self.isAuraHD():
|
||||
_cover_file_endings = self.AURA_HD_COVER_FILE_ENDINGS
|
||||
elif self.isAuraH2O():
|
||||
_cover_file_endings = self.AURA_HD_COVER_FILE_ENDINGS
|
||||
_cover_file_endings = self.AURA_H2O_COVER_FILE_ENDINGS
|
||||
elif self.isAuraH2OEdition2():
|
||||
_cover_file_endings = self.AURA_HD_COVER_FILE_ENDINGS
|
||||
elif self.isAuraOne():
|
||||
@ -3355,6 +3427,18 @@ class KOBOTOUCH(KOBO):
|
||||
def upload_grayscale(self):
|
||||
return self.upload_covers and self.get_pref('upload_grayscale')
|
||||
|
||||
@property
|
||||
def dithered_covers(self):
|
||||
return self.upload_grayscale and self.get_pref('dithered_covers')
|
||||
|
||||
@property
|
||||
def letterbox_fs_covers(self):
|
||||
return self.keep_cover_aspect and self.get_pref('letterbox_fs_covers')
|
||||
|
||||
@property
|
||||
def png_covers(self):
|
||||
return self.upload_grayscale and self.get_pref('png_covers')
|
||||
|
||||
def modifying_epub(self):
|
||||
return self.modifying_css()
|
||||
|
||||
@ -3501,8 +3585,14 @@ class KOBOTOUCH(KOBO):
|
||||
count_options += 1
|
||||
OPT_UPLOAD_GRAYSCALE_COVERS = count_options
|
||||
count_options += 1
|
||||
OPT_DITHERED_COVERS = count_options
|
||||
count_options += 1
|
||||
OPT_KEEP_COVER_ASPECT_RATIO = count_options
|
||||
count_options += 1
|
||||
OPT_LETTERBOX_FULLSCREEN_COVERS = count_options
|
||||
count_options += 1
|
||||
OPT_PNG_COVERS = count_options
|
||||
count_options += 1
|
||||
OPT_SHOW_ARCHIVED_BOOK_RECORDS = count_options
|
||||
count_options += 1
|
||||
OPT_SHOW_PREVIEWS = count_options
|
||||
@ -3533,6 +3623,9 @@ class KOBOTOUCH(KOBO):
|
||||
settings.upload_covers = settings.extra_customization[OPT_UPLOAD_COVERS]
|
||||
settings.keep_cover_aspect = settings.extra_customization[OPT_KEEP_COVER_ASPECT_RATIO]
|
||||
settings.upload_grayscale = settings.extra_customization[OPT_UPLOAD_GRAYSCALE_COVERS]
|
||||
settings.dithered_covers = settings.extra_customization[OPT_DITHERED_COVERS]
|
||||
settings.letterbox_fs_covers = settings.extra_customization[OPT_LETTERBOX_FULLSCREEN_COVERS]
|
||||
settings.png_covers = settings.extra_customization[OPT_PNG_COVERS]
|
||||
|
||||
settings.show_archived_books = settings.extra_customization[OPT_SHOW_ARCHIVED_BOOK_RECORDS]
|
||||
settings.show_previews = settings.extra_customization[OPT_SHOW_PREVIEWS]
|
||||
|
@ -3,7 +3,7 @@
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__copyright__ = '2015-2018, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import textwrap
|
||||
@ -106,6 +106,9 @@ class KOBOTOUCHConfig(TabbedDeviceConfig):
|
||||
p['upload_covers'] = self.upload_covers
|
||||
p['keep_cover_aspect'] = self.keep_cover_aspect
|
||||
p['upload_grayscale'] = self.upload_grayscale
|
||||
p['dithered_covers'] = self.dithered_covers
|
||||
p['letterbox_fs_covers'] = self.letterbox_fs_covers
|
||||
p['png_covers'] = self.png_covers
|
||||
|
||||
p['show_recommendations'] = self.show_recommendations
|
||||
p['show_previews'] = self.show_previews
|
||||
@ -305,18 +308,67 @@ class CoversGroupBox(DeviceOptionsGroupBox):
|
||||
|
||||
self.upload_grayscale_checkbox = create_checkbox(
|
||||
_('Upload black and white covers'),
|
||||
_('Convert covers to black and white when uploading'),
|
||||
_('Convert covers to grayscale when uploading.'),
|
||||
device.get_pref('upload_grayscale')
|
||||
)
|
||||
|
||||
self.dithered_covers_checkbox = create_checkbox(
|
||||
_('Upload dithered covers'),
|
||||
_('Dither cover images to the appropriate 16c grayscale palette for an eInk screen.'
|
||||
' This usually ensures greater accuracy and avoids banding, making sleep covers look better.'
|
||||
' Note that in some cases, you might want to leave this disabled,'
|
||||
' as Nickel will do a better job than Calibre, especially on newer FW versions (>= 4.11)!'
|
||||
' Unfortunately, this desirable behavior appears to depend on the exact device and FW version combo...'
|
||||
' Has no effect without "Upload black and white covers"!'),
|
||||
device.get_pref('dithered_covers')
|
||||
)
|
||||
# Make it visually depend on B&W being enabled!
|
||||
# c.f., https://stackoverflow.com/q/36281103
|
||||
self.dithered_covers_checkbox.setEnabled(device.get_pref('upload_grayscale'))
|
||||
self.upload_grayscale_checkbox.toggled.connect(self.dithered_covers_checkbox.setEnabled)
|
||||
self.upload_grayscale_checkbox.toggled.connect(
|
||||
lambda checked: not checked and self.dithered_covers_checkbox.setChecked(False))
|
||||
|
||||
self.keep_cover_aspect_checkbox = create_checkbox(
|
||||
_('Keep cover aspect ratio'),
|
||||
_('When uploading covers, do not change the aspect ratio when resizing for the device.'
|
||||
' This is for firmware versions 2.3.1 and later.'),
|
||||
device.get_pref('keep_cover_aspect'))
|
||||
|
||||
self.letterbox_fs_covers_checkbox = create_checkbox(
|
||||
_('Letterbox full-screen covers'),
|
||||
_('Do it on our end, instead of letting nickel handle it.'
|
||||
' Provides pixel-perfect results on devices where Nickel does not do extra processing.'
|
||||
' Obviously has no effect without "Keep cover aspect ratio".'
|
||||
' This is also probably undesirable if you disable the "Show book covers full screen"'
|
||||
' setting on your device.'),
|
||||
device.get_pref('letterbox_fs_covers'))
|
||||
# Make it visually depend on AR being enabled!
|
||||
self.letterbox_fs_covers_checkbox.setEnabled(device.get_pref('keep_cover_aspect'))
|
||||
self.keep_cover_aspect_checkbox.toggled.connect(self.letterbox_fs_covers_checkbox.setEnabled)
|
||||
self.keep_cover_aspect_checkbox.toggled.connect(
|
||||
lambda checked: not checked and self.letterbox_fs_covers_checkbox.setChecked(False))
|
||||
|
||||
self.png_covers_checkbox = create_checkbox(
|
||||
_('Save covers as PNG'),
|
||||
_('Use the PNG image format instead of JPG.'
|
||||
' Higher quality, especially with "Upload dithered covers" enabled,'
|
||||
' which will also help generate potentially smaller files.'
|
||||
' Behavior completely unknown on "old" Kobo firmwares,'
|
||||
' last tested on FW 4.9 to 4.12.'
|
||||
' Has no effect without "Upload black and white covers"!'),
|
||||
device.get_pref('png_covers'))
|
||||
# Make it visually depend on B&W being enabled, to avoid storing ridiculously large color PNGs.
|
||||
self.png_covers_checkbox.setEnabled(device.get_pref('upload_grayscale'))
|
||||
self.upload_grayscale_checkbox.toggled.connect(self.png_covers_checkbox.setEnabled)
|
||||
self.upload_grayscale_checkbox.toggled.connect(
|
||||
lambda checked: not checked and self.png_covers_checkbox.setChecked(False))
|
||||
|
||||
self.options_layout.addWidget(self.keep_cover_aspect_checkbox, 0, 0, 1, 1)
|
||||
self.options_layout.addWidget(self.upload_grayscale_checkbox, 1, 0, 1, 1)
|
||||
self.options_layout.addWidget(self.dithered_covers_checkbox, 2, 0, 1, 1)
|
||||
self.options_layout.addWidget(self.letterbox_fs_covers_checkbox, 3, 0, 1, 1)
|
||||
self.options_layout.addWidget(self.png_covers_checkbox, 4, 0, 1, 1)
|
||||
|
||||
@property
|
||||
def upload_covers(self):
|
||||
@ -326,10 +378,22 @@ class CoversGroupBox(DeviceOptionsGroupBox):
|
||||
def upload_grayscale(self):
|
||||
return self.upload_grayscale_checkbox.isChecked()
|
||||
|
||||
@property
|
||||
def dithered_covers(self):
|
||||
return self.dithered_covers_checkbox.isChecked()
|
||||
|
||||
@property
|
||||
def keep_cover_aspect(self):
|
||||
return self.keep_cover_aspect_checkbox.isChecked()
|
||||
|
||||
@property
|
||||
def letterbox_fs_covers(self):
|
||||
return self.letterbox_fs_covers_checkbox.isChecked()
|
||||
|
||||
@property
|
||||
def png_covers(self):
|
||||
return self.png_covers_checkbox.isChecked()
|
||||
|
||||
|
||||
class DeviceListGroupBox(DeviceOptionsGroupBox):
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2015, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
# License: GPLv3 Copyright: 2015-2018, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
@ -175,7 +175,7 @@ def save_image(img, path, **kw):
|
||||
f.write(image_to_data(image_from_data(img), **kw))
|
||||
|
||||
|
||||
def save_cover_data_to(data, path=None, bgcolor='#ffffff', resize_to=None, compression_quality=90, minify_to=None, grayscale=False, data_fmt='jpeg'):
|
||||
def save_cover_data_to(data, path=None, bgcolor='#ffffff', resize_to=None, compression_quality=90, minify_to=None, grayscale=False, eink=False, letterbox=False, data_fmt='jpeg'):
|
||||
'''
|
||||
Saves image in data to path, in the format specified by the path
|
||||
extension. Removes any transparency. If there is no transparency and no
|
||||
@ -193,6 +193,13 @@ def save_cover_data_to(data, path=None, bgcolor='#ffffff', resize_to=None, compr
|
||||
:param minify_to: A tuple (width, height) to specify maximum target size.
|
||||
The image will be resized to fit into this target size. If None the
|
||||
value from the tweak is used.
|
||||
:param grayscale: If True, the image is converted to grayscale,
|
||||
if that's not already the case.
|
||||
:param eink: If True, the image is dithered down to the 16 specific shades
|
||||
of gray of the eInk palette.
|
||||
Works best with formats that actually support color indexing (i.e., PNG)
|
||||
:param letterbox: If True, in addition to fit resize_to inside minify_to,
|
||||
the image will be letterboxed (i.e., centered on a black background).
|
||||
'''
|
||||
fmt = normalize_format_name(data_fmt if path is None else os.path.splitext(path)[1][1:])
|
||||
if isinstance(data, QImage):
|
||||
@ -207,6 +214,12 @@ def save_cover_data_to(data, path=None, bgcolor='#ffffff', resize_to=None, compr
|
||||
img = img.scaled(resize_to[0], resize_to[1], Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
||||
owidth, oheight = img.width(), img.height()
|
||||
nwidth, nheight = tweaks['maximum_cover_size'] if minify_to is None else minify_to
|
||||
if letterbox:
|
||||
img = blend_on_canvas(img, nwidth, nheight, bgcolor='#000000')
|
||||
# Check if we were minified
|
||||
if oheight != nheight or owidth != nwidth:
|
||||
changed = True
|
||||
else:
|
||||
scaled, nwidth, nheight = fit_image(owidth, oheight, nwidth, nheight)
|
||||
if scaled:
|
||||
changed = True
|
||||
@ -218,6 +231,17 @@ def save_cover_data_to(data, path=None, bgcolor='#ffffff', resize_to=None, compr
|
||||
if not img.allGray():
|
||||
changed = True
|
||||
img = grayscale_image(img)
|
||||
if eink:
|
||||
eink_cmap = ['#000000', '#111111', '#222222', '#333333', '#444444', '#555555', '#666666', '#777777',
|
||||
'#888888', '#999999', '#AAAAAA', '#BBBBBB', '#CCCCCC', '#DDDDDD', '#EEEEEE', '#FFFFFF']
|
||||
# NOTE: Keep in mind that JPG does NOT actually support indexed colors, so the JPG algorithm will then smush everything back into a 256c mess...
|
||||
# Thankfully, Nickel handles PNG just fine, and we generate smaller files to boot, because they're properly color indexed ;).
|
||||
img = quantize_image(img, max_colors=16, dither=True, palette=eink_cmap)
|
||||
'''
|
||||
# NOTE: Neither Grayscale8 nor Indexed8 actually do any kind of dithering?... :/.
|
||||
img = img.convertToFormat(QImage.Format_Grayscale8, [QColor(x).rgb() for x in eink_cmap], Qt.AutoColor | Qt.DiffuseDither | Qt.ThresholdAlphaDither | Qt.PreferDither)
|
||||
'''
|
||||
changed = True
|
||||
if path is None:
|
||||
return image_to_data(img, compression_quality, fmt) if changed else data
|
||||
with lopen(path, 'wb') as f:
|
||||
|
Loading…
x
Reference in New Issue
Block a user