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)