Some optimizations for previous PR

1) Set device pixel ratio on generated QPixmap
2) Avoid copying image data when loading cover from library
3) Convert thumbnail to RGBA in cover thread so as to do less work in
   GUI thread
This commit is contained in:
Kovid Goyal 2024-01-27 09:01:52 +05:30
parent dbc4860ac5
commit f97147afec
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
4 changed files with 28 additions and 23 deletions

View File

@ -1708,7 +1708,7 @@ class DB:
return True return True
return False return False
def cover_or_cache(self, path, timestamp): def cover_or_cache(self, path, timestamp, as_what='bytes'):
path = os.path.abspath(os.path.join(self.library_path, path, COVER_FILE_NAME)) path = os.path.abspath(os.path.join(self.library_path, path, COVER_FILE_NAME))
try: try:
stat = os.stat(path) stat = os.stat(path)
@ -1723,7 +1723,13 @@ class DB:
time.sleep(0.2) time.sleep(0.2)
f = open(path, 'rb') f = open(path, 'rb')
with f: with f:
return True, f.read(), stat.st_mtime if as_what == 'pil_image':
from PIL import Image
data = Image.open(f)
data.load()
else:
data = f.read()
return True, data, stat.st_mtime
def compress_covers(self, path_map, jpeg_quality, progress_callback): def compress_covers(self, path_map, jpeg_quality, progress_callback):
cpath_map = {} cpath_map = {}

View File

@ -1161,12 +1161,12 @@ class Cache:
return ret return ret
@read_api @read_api
def cover_or_cache(self, book_id, timestamp): def cover_or_cache(self, book_id, timestamp, as_what='bytes'):
try: try:
path = self._field_for('path', book_id).replace('/', os.sep) path = self._field_for('path', book_id).replace('/', os.sep)
except AttributeError: except AttributeError:
return False, None, None return False, None, None
return self.backend.cover_or_cache(path, timestamp) return self.backend.cover_or_cache(path, timestamp, as_what)
@read_api @read_api
def cover_last_modified(self, book_id): def cover_last_modified(self, book_id):

View File

@ -777,10 +777,7 @@ class GridView(QListView):
@property @property
def device_pixel_ratio(self): def device_pixel_ratio(self):
try: return self.devicePixelRatioF()
return self.devicePixelRatioF()
except AttributeError:
return self.devicePixelRatio()
@property @property
def first_visible_row(self): def first_visible_row(self):
@ -964,17 +961,16 @@ class GridView(QListView):
cdata, timestamp = tc[book_id] # None, None if not cached. cdata, timestamp = tc[book_id] # None, None if not cached.
if timestamp is None: if timestamp is None:
# Cover not in cache. Try to read the cover from the library. # Cover not in cache. Try to read the cover from the library.
has_cover, cdata, timestamp = db.new_api.cover_or_cache(book_id, 0) has_cover, cdata, timestamp = db.new_api.cover_or_cache(book_id, 0, as_what='pil_image')
if has_cover: if has_cover:
# There is a cover.jpg. Convert the byte string to an image. # There is a cover.jpg. Convert the byte string to an image.
cache_valid = False cache_valid = False
cdata = Image.open(BytesIO(cdata))
else: else:
# No cover.jpg # No cover.jpg
cache_valid = None cache_valid = None
else: else:
# A cover is in the cache. Check whether it is up to date. # A cover is in the cache. Check whether it is up to date.
has_cover, tcdata, timestamp = db.new_api.cover_or_cache(book_id, timestamp) has_cover, tcdata, timestamp = db.new_api.cover_or_cache(book_id, timestamp, as_what='pil_image')
if has_cover: if has_cover:
if tcdata is None: if tcdata is None:
# The cached cover is up-to-date. # The cached cover is up-to-date.
@ -983,9 +979,6 @@ class GridView(QListView):
else: else:
# The cached cover is stale # The cached cover is stale
cache_valid = False cache_valid = False
# Convert the bytes from the cover.jpg. The image will be
# resized later.
cdata = Image.open(BytesIO(tcdata))
else: else:
# We found a cached cover for a book without a cover. This can # We found a cached cover for a book without a cover. This can
# happen in older version of calibre that can reuse book_ids # happen in older version of calibre that can reuse book_ids
@ -1067,6 +1060,10 @@ class GridView(QListView):
# Return the thumbnail, which is either None or a PIL Image. If not None # Return the thumbnail, which is either None or a PIL Image. If not None
# the image will be converted to a QPixmap on the GUI thread. Putting # the image will be converted to a QPixmap on the GUI thread. Putting
# None into the CoverCache ensures re-rendering won't try again. # None into the CoverCache ensures re-rendering won't try again.
if getattr(thumb, 'mode', None) == 'RGB':
# Conversion to QPixmap needs RGBA data so do it here rather than
# in the GUI thread
thumb = thumb.convert('RGBA')
return thumb return thumb
def re_render(self, book_id, thumb): def re_render(self, book_id, thumb):
@ -1076,7 +1073,7 @@ class GridView(QListView):
self.delegate.cover_cache.clear_staging() self.delegate.cover_cache.clear_staging()
if thumb is not None: if thumb is not None:
# Convert the image to a QPixmap # Convert the image to a QPixmap
thumb = convert_PIL_image_to_pixmap(thumb) thumb = convert_PIL_image_to_pixmap(thumb, self.device_pixel_ratio)
self.delegate.cover_cache.set(book_id, thumb) self.delegate.cover_cache.set(book_id, thumb)
m = self.model() m = self.model()
try: try:

View File

@ -688,10 +688,16 @@ def align8to32(bytes, width, mode):
return b"".join(new_data) return b"".join(new_data)
def convert_PIL_image_to_pixmap(im): def convert_PIL_image_to_pixmap(im, device_pixel_ratio=1.0):
data = None data = None
colortable = None colortable = None
if im.mode == "1": if im.mode == "RGBA":
fmt = QImage.Format.Format_RGBA8888
data = im.tobytes("raw", "RGBA")
elif im.mode == "RGB":
fmt = QImage.Format.Format_RGBX8888
data = im.convert("RGBA").tobytes("raw", "RGBA")
elif im.mode == "1":
fmt = QImage.Format.Format_Mono fmt = QImage.Format.Format_Mono
elif im.mode == "L": elif im.mode == "L":
fmt = QImage.Format.Format_Indexed8 fmt = QImage.Format.Format_Indexed8
@ -700,12 +706,6 @@ def convert_PIL_image_to_pixmap(im):
fmt = QImage.Format.Format_Indexed8 fmt = QImage.Format.Format_Indexed8
palette = im.getpalette() palette = im.getpalette()
colortable = [qRgba(*palette[i : i + 3], 255) & 0xFFFFFFFF for i in range(0, len(palette), 3)] colortable = [qRgba(*palette[i : i + 3], 255) & 0xFFFFFFFF for i in range(0, len(palette), 3)]
elif im.mode == "RGB":
fmt = QImage.Format.Format_RGBX8888
data = im.convert("RGBA").tobytes("raw", "RGBA")
elif im.mode == "RGBA":
fmt = QImage.Format.Format_RGBA8888
data = im.tobytes("raw", "RGBA")
elif im.mode == "I;16": elif im.mode == "I;16":
im = im.point(lambda i: i * 256) im = im.point(lambda i: i * 256)
fmt = QImage.Format.Format_Grayscale16 fmt = QImage.Format.Format_Grayscale16
@ -715,6 +715,8 @@ def convert_PIL_image_to_pixmap(im):
size = im.size size = im.size
data = data or align8to32(im.tobytes(), size[0], im.mode) data = data or align8to32(im.tobytes(), size[0], im.mode)
qimg = QImage(data, size[0], size[1], fmt) qimg = QImage(data, size[0], size[1], fmt)
if device_pixel_ratio != 1.0:
qimg.setDevicePixelRatio(device_pixel_ratio)
if colortable: if colortable:
qimg.setColorTable(colortable) qimg.setColorTable(colortable)
return QPixmap.fromImage(qimg) return QPixmap.fromImage(qimg)