From f97147afec4093687ce8a9c24b8b6acade9779b1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 27 Jan 2024 09:01:52 +0530 Subject: [PATCH] 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 --- src/calibre/db/backend.py | 10 ++++++++-- src/calibre/db/cache.py | 4 ++-- src/calibre/gui2/library/alternate_views.py | 19 ++++++++----------- src/calibre/utils/img.py | 18 ++++++++++-------- 4 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/calibre/db/backend.py b/src/calibre/db/backend.py index 87a3346375..4e79368bd6 100644 --- a/src/calibre/db/backend.py +++ b/src/calibre/db/backend.py @@ -1708,7 +1708,7 @@ class DB: return True 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)) try: stat = os.stat(path) @@ -1723,7 +1723,13 @@ class DB: time.sleep(0.2) f = open(path, 'rb') 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): cpath_map = {} diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index fcb6ec52ab..83a039c5f2 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -1161,12 +1161,12 @@ class Cache: return ret @read_api - def cover_or_cache(self, book_id, timestamp): + def cover_or_cache(self, book_id, timestamp, as_what='bytes'): try: path = self._field_for('path', book_id).replace('/', os.sep) except AttributeError: return False, None, None - return self.backend.cover_or_cache(path, timestamp) + return self.backend.cover_or_cache(path, timestamp, as_what) @read_api def cover_last_modified(self, book_id): diff --git a/src/calibre/gui2/library/alternate_views.py b/src/calibre/gui2/library/alternate_views.py index 74f4326e95..ddbad2eedc 100644 --- a/src/calibre/gui2/library/alternate_views.py +++ b/src/calibre/gui2/library/alternate_views.py @@ -777,10 +777,7 @@ class GridView(QListView): @property def device_pixel_ratio(self): - try: - return self.devicePixelRatioF() - except AttributeError: - return self.devicePixelRatio() + return self.devicePixelRatioF() @property def first_visible_row(self): @@ -964,17 +961,16 @@ class GridView(QListView): cdata, timestamp = tc[book_id] # None, None if not cached. if timestamp is None: # 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: # There is a cover.jpg. Convert the byte string to an image. cache_valid = False - cdata = Image.open(BytesIO(cdata)) else: # No cover.jpg cache_valid = None else: # 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 tcdata is None: # The cached cover is up-to-date. @@ -983,9 +979,6 @@ class GridView(QListView): else: # The cached cover is stale cache_valid = False - # Convert the bytes from the cover.jpg. The image will be - # resized later. - cdata = Image.open(BytesIO(tcdata)) else: # We found a cached cover for a book without a cover. This can # 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 # the image will be converted to a QPixmap on the GUI thread. Putting # 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 def re_render(self, book_id, thumb): @@ -1076,7 +1073,7 @@ class GridView(QListView): self.delegate.cover_cache.clear_staging() if thumb is not None: # 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) m = self.model() try: diff --git a/src/calibre/utils/img.py b/src/calibre/utils/img.py index bf26916a4b..bcb7d333d6 100644 --- a/src/calibre/utils/img.py +++ b/src/calibre/utils/img.py @@ -688,10 +688,16 @@ def align8to32(bytes, width, mode): 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 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 elif im.mode == "L": fmt = QImage.Format.Format_Indexed8 @@ -700,12 +706,6 @@ def convert_PIL_image_to_pixmap(im): fmt = QImage.Format.Format_Indexed8 palette = im.getpalette() 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": im = im.point(lambda i: i * 256) fmt = QImage.Format.Format_Grayscale16 @@ -715,6 +715,8 @@ def convert_PIL_image_to_pixmap(im): size = im.size data = data or align8to32(im.tobytes(), size[0], im.mode) qimg = QImage(data, size[0], size[1], fmt) + if device_pixel_ratio != 1.0: + qimg.setDevicePixelRatio(device_pixel_ratio) if colortable: qimg.setColorTable(colortable) return QPixmap.fromImage(qimg)