From 4eb9d9eca23c00a5186932c63e9b129e353cd0b1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 17 Jan 2026 11:16:44 +0530 Subject: [PATCH] Explicitly release GIL while loading QImage in thumbnail renderer thread --- src/calibre/gui2/library/caches.py | 3 ++- src/calibre/utils/imageops/imageops.cpp | 5 ++++- src/calibre/utils/imageops/imageops.h | 1 + src/calibre/utils/imageops/imageops.sip | 9 +++------ 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/library/caches.py b/src/calibre/gui2/library/caches.py index c01f33798a..e92837ffc5 100644 --- a/src/calibre/gui2/library/caches.py +++ b/src/calibre/gui2/library/caches.py @@ -21,6 +21,7 @@ from qt.core import QBuffer, QByteArray, QColor, QImage, QImageWriter, QIODevice from calibre.db.utils import ThumbnailCache as TC from calibre.utils import join_with_timeout from calibre.utils.img import resize_to_fit +from calibre_extensions.imageops import load_from_data_without_gil class ThumbnailCache(TC): @@ -129,7 +130,7 @@ class Thumbnailer: if not cover_as_bytes: return self.thumbnail_class(), b'' cover: QImage = self.thumbnail_class() - if not cover.loadFromData(cover_as_bytes): + if not load_from_data_without_gil(cover, cover_as_bytes): return cover, b'' cover = self.resize_to_fit(cover, width, height) serialized = self.serialize(cover) diff --git a/src/calibre/utils/imageops/imageops.cpp b/src/calibre/utils/imageops/imageops.cpp index 605180b3e6..622db1b559 100644 --- a/src/calibre/utils/imageops/imageops.cpp +++ b/src/calibre/utils/imageops/imageops.cpp @@ -639,7 +639,6 @@ void overlay(const QImage &image, QImage &canvas, unsigned int left, unsigned in } // }}} QColor dominant_color(const QImage &image) { // {{{ - ScopedGILRelease PyGILRelease; if (image.isNull()) return QColor(); QImage img; // Resize the image to a thumbnail for improved performance @@ -1005,3 +1004,7 @@ QImage texture_image(const QImage &image, const QImage &texturei) { // {{{ } return canvas; } // }}} + +bool load_from_data_without_gil(QImage &image, const char *data, size_t len) { // {{{ + return image.loadFromData((const uchar*)data, len, NULL); +} // }}} diff --git a/src/calibre/utils/imageops/imageops.h b/src/calibre/utils/imageops/imageops.h index 150be5b4ea..7a178d3fb2 100644 --- a/src/calibre/utils/imageops/imageops.h +++ b/src/calibre/utils/imageops/imageops.h @@ -25,6 +25,7 @@ QImage set_opacity(const QImage &image, double alpha); QImage texture_image(const QImage &image, const QImage &texturei); QImage ordered_dither(const QImage &image); QColor dominant_color(const QImage &image); +bool load_from_data_without_gil(QImage &image, const char *data, size_t len); class ScopedGILRelease { public: diff --git a/src/calibre/utils/imageops/imageops.sip b/src/calibre/utils/imageops/imageops.sip index befdd5e6eb..b3833831fb 100644 --- a/src/calibre/utils/imageops/imageops.sip +++ b/src/calibre/utils/imageops/imageops.sip @@ -18,6 +18,8 @@ } catch (...) { PyErr_SetString(PyExc_RuntimeError, "unknown error"); return NULL;} %End +bool load_from_data_without_gil(QImage &image, const char *data /Array/, size_t len /ArraySize/) /ReleaseGIL/; + QImage* remove_borders(const QImage &image, double fuzz); %MethodCode IMAGEOPS_PREFIX @@ -74,12 +76,7 @@ QImage oil_paint(const QImage &image, const float radius=-1, const bool high_qua IMAGEOPS_SUFFIX %End -QColor dominant_color(const QImage &image); -%MethodCode - IMAGEOPS_PREFIX - sipRes = new QColor(dominant_color(*a0)); - IMAGEOPS_SUFFIX -%End +QColor dominant_color(const QImage &image) /ReleaseGIL/; QImage quantize(const QImage &image, unsigned int maximum_colors, bool dither, const QList &palette); %MethodCode