[graphics/krita] libs/ui: Handle cases where zoom preview center stuck on corners and egdes

Emmet O'Neill <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit b8577f41dfd5a04b6d23f80aa962c03442d4b664 by Emmet O'Neill, on behalf of Dat Le.
Committed on 13/08/2026 at 23:16.
Pushed by emmetoneill into branch 'master'.

Handle cases where zoom preview center stuck on corners and egdes

M  +2    -2    libs/ui/KisReferenceImage.cpp
M  +53   -22   libs/ui/tool/KisAsyncColorSamplerHelper.cpp

https://invent.kde.org/graphics/krita/-/commit/b8577f41dfd5a04b6d23f80aa962c03442d4b664

diff --git a/libs/ui/KisReferenceImage.cpp b/libs/ui/KisReferenceImage.cpp
index 347a744b0f9..ae56cec6ec6 100644
--- a/libs/ui/KisReferenceImage.cpp
+++ b/libs/ui/KisReferenceImage.cpp
@@ -377,7 +377,7 @@ KoColor KisReferenceImage::getPixel(QPointF position)
     transparent.setOpacity(0.0);
     if (transparency() == 1.0) return transparent;
 
-    const QPointF localPosition = documentToPixel(position);
+    const QPoint localPosition = documentToPixelFloored(position);
 
     if (d->cachedImage.isNull()) {
         d->updateCache();
@@ -391,7 +391,7 @@ KoColor KisReferenceImage::getPixel(QPointF position)
 #endif
 
     KoColor c(cs);
-    QColor pixel = d->cachedImage.pixelColor(localPosition.toPoint());
+    QColor pixel = d->cachedImage.pixelColor(localPosition);
     QVector<float> channels = {
         static_cast<float>(pixel.blueF()),
         static_cast<float>(pixel.greenF()),
diff --git a/libs/ui/tool/KisAsyncColorSamplerHelper.cpp b/libs/ui/tool/KisAsyncColorSamplerHelper.cpp
index 4ab355f3613..63386560ea5 100644
--- a/libs/ui/tool/KisAsyncColorSamplerHelper.cpp
+++ b/libs/ui/tool/KisAsyncColorSamplerHelper.cpp
@@ -79,8 +79,8 @@ struct KisAsyncColorSamplerHelper::Private
 
     QPainterPath cacheCircleInnerClip;
     QVector<KisReferenceImage*> cacheReferenceImageList; // Sorted reference images by zIndex
-    QRect cacheCanvasRect;
-    QImage cacheCanvasImage;
+    QRect cacheCanvasPreviewRect;
+    QImage cacheCanvasPreviewImage;
 
     QColor currentColor;
     QColor baseColor;
@@ -253,9 +253,7 @@ void KisAsyncColorSamplerHelper::activate(bool sampleCurrentLayer, bool pickFgCo
     m_d->circlePreviewExtraCircles = cfg.colorSamplerPreviewCircleExtraCirclesEnabled();
     m_d->circleZoomPreviewScale = cfg.colorSamplerZoomPreviewScale()/100.0; // saved in percentages
 
-    // Reset the cache
-    m_d->cacheCanvasImage = QImage();
-    m_d->cacheCanvasRect = QRect();
+    // Update and sort the list of current reference images currently on canvas
     m_d->updateCachedReferenceImages();
 
     m_d->activationDelayTimer.start();
@@ -342,6 +340,10 @@ void KisAsyncColorSamplerHelper::deactivate()
     m_d->baseColor = QColor();
     m_d->cache = QPixmap();
 
+    // Reset the cached zoom preview image and rect
+    m_d->cacheCanvasPreviewImage = QImage();
+    m_d->cacheCanvasPreviewRect = QRect();
+
     m_d->isActive = false;
 
     Q_EMIT sigRequestCursorReset();
@@ -600,6 +602,8 @@ void KisAsyncColorSamplerHelper::paintCircle(QPainter &gc,
         // Sample a rect with size that is (zoomPreviewScale) times smaller than (previewDocRect size)
         sampleDocRectF.setSize(sampleDocRectF.size() / m_d->circleZoomPreviewScale);
         sampleDocRectF.moveCenter(m_d->previewDocRect.center());
+        dbgUI << "Preview rect:" << m_d->previewDocRect;
+        dbgUI << "Sample rect:" << sampleDocRectF;
 
         // Invert the preview offset to move the sample rect to the actual position sampled
         QPointF invertOffsetDocPoint = m_d->canvas->coordinatesConverter()->viewToDocument(QPointF(-m_d->circlePreviewHorizontalOffset, -m_d->circlePreviewVerticalOffset));
@@ -642,12 +646,12 @@ QImage KisAsyncColorSamplerHelper::cacheCanvasImage(QRect &canvasPixelRect) {
     }
 
     // If already cached the whole canvas, just use it from now on
-    if (m_d->cacheCanvasRect == canvasImage->bounds()) {
+    if (m_d->cacheCanvasPreviewRect == canvasImage->bounds()) {
         dbgUI << "Using cached canvas image!";
-        return m_d->cacheCanvasImage;
+        return m_d->cacheCanvasPreviewImage;
     }
 
-    if (m_d->cacheCanvasRect.isEmpty() || !m_d->cacheCanvasRect.contains(canvasPixelRect)) {
+    if (m_d->cacheCanvasPreviewRect.isEmpty() || !m_d->cacheCanvasPreviewRect.contains(canvasPixelRect)) {
         // Cache an area larger than the needed preview area
         // to avoid rapid small dynamic allocations
         qreal cacheScale = 4;
@@ -664,24 +668,39 @@ QImage KisAsyncColorSamplerHelper::cacheCanvasImage(QRect &canvasPixelRect) {
         dbgUI << "Canvas preview: " << canvasPixelRect;
         dbgUI << "Canvas cache: " << cacheCanvasRect;
 
-        m_d->cacheCanvasImage = canvasImage->convertToQImage(cacheCanvasRect, canvasImage->profile());
+        m_d->cacheCanvasPreviewImage = canvasImage->convertToQImage(cacheCanvasRect, canvasImage->profile());
 
-        m_d->cacheCanvasRect = cacheCanvasRect;
+        m_d->cacheCanvasPreviewRect = cacheCanvasRect;
     }
 
-    canvasPixelRect.translate(-m_d->cacheCanvasRect.topLeft());
+    canvasPixelRect.translate(-m_d->cacheCanvasPreviewRect.topLeft());
 
-    return m_d->cacheCanvasImage;
+    return m_d->cacheCanvasPreviewImage;
 }
 
 void KisAsyncColorSamplerHelper::paintCircleCanvasPreview(QPainter &gc, const QRectF &viewRectF, const QRectF &sampleDocRectF, const QPainterPath &clip) {
     gc.save();
 
-    QPoint canvasPixelTopLeftFloored = m_d->canvas->image()->documentToImagePixelFloored(sampleDocRectF.topLeft());
-    QPoint canvasPixelBottomRightFloored = m_d->canvas->image()->documentToImagePixelFloored(sampleDocRectF.bottomRight());
-    QRect canvasPixelRect = QRect(canvasPixelTopLeftFloored, canvasPixelBottomRightFloored);
+    KisImageWSP image = m_d->canvas->image();
+    QRect canvasPixelRect = image->documentToPixel(sampleDocRectF).toRect();
+
+    // If width and height get rounded to different number, the center will be stuck between pixel border
+    if (canvasPixelRect.width() != canvasPixelRect.height()) {
+        if (canvasPixelRect.width() % 2 == 0) canvasPixelRect.setWidth(canvasPixelRect.height());
+        else canvasPixelRect.setHeight(canvasPixelRect.width());
+    }
+    // If width and height is even, the center will be stuck in pixel corner
+    else if (canvasPixelRect.width() % 2 == 0) {
+        canvasPixelRect.setWidth(canvasPixelRect.width() - 1);
+        canvasPixelRect.setHeight(canvasPixelRect.height() - 1);
+    }
+
+    dbgUI << "Pixel rect:" << canvasPixelRect;
     // In case zoom and scale is both so large that sample size becomes 0
-    if (canvasPixelRect.isEmpty()) canvasPixelRect.setSize(QSize(1, 1));
+    if (canvasPixelRect.width() == 0 || canvasPixelRect.height() == 0) canvasPixelRect.setSize(QSize(1, 1));
+
+    // Make sure the center is the pixel currently sampled (in case of rounding errors)
+    canvasPixelRect.moveCenter(image->documentToImagePixelFloored(sampleDocRectF.center()));
 
     QImage cachedImage = cacheCanvasImage(canvasPixelRect);
     gc.setCompositionMode(QPainter::CompositionMode_SourceOver);
@@ -689,7 +708,6 @@ void KisAsyncColorSamplerHelper::paintCircleCanvasPreview(QPainter &gc, const QR
     gc.setClipPath(clip);
 
     // QtDoc: The image is scaled to fit the rectangle, if both the image and rectangle size disagree.
-
     // Since the piece of canvas is (zoomPreviewScale) times smaller than cacheRect
     // drawImage will scale it up that many times, thus achieving the zoom effect
     gc.drawImage(viewRectF, cachedImage, canvasPixelRect);
@@ -698,6 +716,7 @@ void KisAsyncColorSamplerHelper::paintCircleCanvasPreview(QPainter &gc, const QR
 }
 
 // Won't show opacity because the color sampled doesn't respect opacity either
+// TODO: Test if reference image still show when not visible, opacity = 0. Maybe we need to cover that too
 void KisAsyncColorSamplerHelper::paintCircleReferenceImagePreview(QPainter &gc, const QRectF &viewRectF, const QRectF &sampleDocRectF, const QPainterPath &clip) {
     gc.save();
 
@@ -714,16 +733,27 @@ void KisAsyncColorSamplerHelper::paintCircleReferenceImagePreview(QPainter &gc,
         QImage image = refImage->getCachedImage();
         // image.convertTo(QImage::Format_ARGB32); Do this in KisReferenceImage to avoid having to copy?
 
-        QPoint sampleRefPointF = refImage->documentToPixelFloored(sampleDocRectF.center());
+        QPoint sampleRefPoint = refImage->documentToPixelFloored(sampleDocRectF.center());
 
         qreal xScale = refImage->boundingRect().width() / image.width();
         qreal yScale = refImage->boundingRect().height() / image.height();
 
-        QRectF sampleRefRectF = QRectF(sampleRefPointF, QSizeF(sampleDocRectF.width() / xScale, sampleDocRectF.height() / yScale));
-        sampleRefRectF.moveCenter(sampleRefPointF);
+        QRect sampleRefRect = QRect(sampleRefPoint, QSize(sampleDocRectF.width() / xScale, sampleDocRectF.height() / yScale));
+
+        // If width and height get rounded to different number, the center will be stuck between pixel border
+        if (sampleRefRect.width() != sampleRefRect.height()) {
+            if (sampleRefRect.width() % 2 == 0) sampleRefRect.setWidth(sampleRefRect.height());
+            else sampleRefRect.setHeight(sampleRefRect.width());
+        }
+        // If width and height is even, the center will be stuck in pixel corner
+        else if (sampleRefRect.width() % 2 == 0) {
+            sampleRefRect.setWidth(sampleRefRect.width() - 1);
+            sampleRefRect.setHeight(sampleRefRect.height() - 1);
+        }
+
+        if (sampleRefRect.width() == 0 || sampleRefRect.height() == 0) sampleRefRect.setSize(QSize(1, 1));
 
-        QRect sampleRefRect = sampleRefRectF.toRect();
-        if (sampleRefRect.isEmpty()) sampleRefRect.setSize(QSize(1, 1));
+        sampleRefRect.moveCenter(sampleRefPoint);
 
         // Rotate the painter, draw, rotate back is seemingly easier than
         // trying to rotate the image and the shenanighens that follow
@@ -749,6 +779,7 @@ void KisAsyncColorSamplerHelper::slotAddSamplingJob(const QPointF &docPoint)
 
     KisImageSP image = m_d->canvas->image();
 
+    dbgUI << "Original doc point in slotAddSamplingJob:" << docPoint;
     const QPoint imagePoint = image->documentToImagePixelFloored(docPoint);
 
     if (!m_d->sampleCurrentLayer) {
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.