[office/crow-translate] src: Fix Windows multi-monitor capture area with per-monitor DPI

Mauritius Clemens <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit da9ff89d35c1f2c26c2e067029b3f4e0888790c4 by Mauritius Clemens.
Committed on 16/08/2026 at 22:20.
Pushed by pillowtrucker into branch 'master'.

Fix Windows multi-monitor capture area with per-monitor DPI

Request PER_MONITOR_AWARE_V2 so Qt reports consistent geometry across a
scaled main monitor and a second monitor. Map the capture selection from the
overlay's logical coordinates into device pixels per-screen in
SnippingArea::selectedPixmap, so the grabbed image is correctly offset and
scaled for OCR on 200%-scaled or mixed-DPI setups.

M  +12   -0    src/main.cpp
M  +52   -4    src/ocr/snippingarea.cpp
M  +4    -0    src/ocr/snippingarea.h

https://invent.kde.org/office/crow-translate/-/commit/da9ff89d35c1f2c26c2e067029b3f4e0888790c4

diff --git a/src/main.cpp b/src/main.cpp
index ab9c57a3..f892e1d8 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -18,6 +18,12 @@
 #include <KIconTheme>
 #endif
 
+#ifdef Q_OS_WIN
+#define WIN32_LEAN_AND_MEAN
+#define NOMINMAX
+#include <windows.h>
+#endif
+
 #ifdef Q_OS_UNIX
 #include "ocr/ocr.h"
 
@@ -69,6 +75,12 @@ int launchGui(int argc, char *argv[])
 
 #if defined(Q_OS_WIN)
     QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
+    // Request per-monitor DPI awareness (V2) explicitly so that the screen
+    // geometry and grabbed images reported by Qt use a single, consistent scale
+    // regardless of any application manifest. Without this, on a scaled main
+    // monitor alongside a second monitor the capture area maps its coordinates
+    // incorrectly and the grabbed pixmap is offset or clipped.
+    SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
 #endif
 #if defined(Q_OS_LINUX)
     QGuiApplication::setDesktopFileName(QStringLiteral(DESKTOP_FILE_BASENAME));
diff --git a/src/ocr/snippingarea.cpp b/src/ocr/snippingarea.cpp
index 9ae3a789..a2dd324c 100644
--- a/src/ocr/snippingarea.cpp
+++ b/src/ocr/snippingarea.cpp
@@ -449,7 +449,7 @@ int SnippingArea::boundsLeft(int newTopLeftX, bool mouse)
 int SnippingArea::boundsRight(int newTopLeftX, bool mouse)
 {
     // The max X coordinate of the top left point
-    const int realMaxX = qRound((width() - m_selection.width()) * devicePixelRatioF());
+    const int realMaxX = width() - m_selection.width();
     const int xOffset = newTopLeftX - realMaxX;
     if (xOffset > 0) {
         if (mouse)
@@ -763,16 +763,62 @@ SnippingArea::MouseState SnippingArea::mouseLocation(QPointF pos) const
 
 QPixmap SnippingArea::selectedPixmap() const
 {
-    return m_screenPixmap.copy(m_selection);
+    // The selection is stored in the overlay's logical (device-independent)
+    // coordinates, but each screen image is captured at its native
+    // (device-pixel) resolution. Extract the selection at full resolution by
+    // intersecting it with every screen and scaling by that screen's
+    // devicePixelRatio, preserving the pixels for OCR.
+    const QRect deviceSelection = toDeviceRect(m_selection);
+    QPixmap result(deviceSelection.size());
+    result.fill(Qt::transparent);
+
+    QPainter painter(&result);
+    for (auto it = m_images.constBegin(); it != m_images.constEnd(); ++it) {
+        const QRect screenArea = deviceGeometry(it.key()).translated(-m_deviceScreensRect.topLeft());
+        const QRect matched = screenArea.intersected(deviceSelection);
+        if (matched.isEmpty())
+            continue;
+
+        const QRect sourceRect = matched.translated(-screenArea.topLeft());
+        painter.drawImage(matched.translated(-deviceSelection.topLeft()), it.value(), sourceRect);
+    }
+    return result;
+}
+
+QRect SnippingArea::deviceGeometry(const QScreen *screen)
+{
+    const qreal dpr = screen->devicePixelRatio();
+    const QRect geometry = screen->geometry();
+    return QRect(qRound(geometry.x() * dpr),
+                 qRound(geometry.y() * dpr),
+                 qRound(geometry.width() * dpr),
+                 qRound(geometry.height() * dpr));
+}
+
+QRect SnippingArea::toDeviceRect(const QRect &rect) const
+{
+    // Map a rectangle expressed in the overlay's logical coordinates to the
+    // device-pixel space of the composited screenshot. Use the screen under the
+    // rectangle's center so that a single consistent scale factor is applied.
+    const QPointF overlayCenter = rect.center() + m_screensRect.topLeft();
+    const QScreen *screen = QGuiApplication::screenAt(overlayCenter.toPoint());
+    if (screen == nullptr)
+        screen = QGuiApplication::primaryScreen();
+
+    const qreal dpr = screen->devicePixelRatio();
+    const QPointF screenRelativeTopLeft = rect.topLeft() + m_screensRect.topLeft() - screen->geometry().topLeft();
+    const QPointF deviceTopLeft = screenRelativeTopLeft * dpr + deviceGeometry(screen).topLeft() - m_deviceScreensRect.topLeft();
+    return QRect(deviceTopLeft.toPoint(), QSizeF(rect.width() * dpr, rect.height() * dpr).toSize());
 }
 
 void SnippingArea::createPixmapFromScreens()
 {
-    m_screenPixmap = QPixmap(m_screensRect.width(), m_screensRect.height());
+    m_screenPixmap = QPixmap(m_screensRect.size());
     QPainter painter(&m_screenPixmap);
     // Geometry can have negative coordinates, so it is necessary to subtract the upper left point, because coordinates on the widget are counted from 0
+    // Each screen image is grabbed at its native resolution, so it must be scaled down to the screen's logical geometry when composited
     for (auto it = m_images.constBegin(); it != m_images.constEnd(); ++it)
-        painter.drawImage(it.key()->geometry().topLeft() - m_screensRect.topLeft(), it.value());
+        painter.drawImage(it.key()->geometry().translated(-m_screensRect.topLeft()), it.value());
 }
 
 void SnippingArea::setGeometryToScreenPixmap()
@@ -880,8 +926,10 @@ void SnippingArea::setBottomHelpText()
 void SnippingArea::preparePaint()
 {
     m_screensRect = {};
+    m_deviceScreensRect = {};
     for (auto it = m_images.constBegin(); it != m_images.constEnd(); ++it) {
         m_screensRect = m_screensRect.united(it.key()->geometry());
+        m_deviceScreensRect = m_deviceScreensRect.united(deviceGeometry(it.key()));
     }
 }
 
diff --git a/src/ocr/snippingarea.h b/src/ocr/snippingarea.h
index daaa153c..ccc491bf 100644
--- a/src/ocr/snippingarea.h
+++ b/src/ocr/snippingarea.h
@@ -104,6 +104,9 @@ private:
     static bool isPointInsideCircle(QPointF circleCenter, qreal radius, QPointF point);
     static bool isInRange(qreal low, qreal high, qreal value);
     static bool isWithinThreshold(qreal offset, qreal threshold);
+    static QRect deviceGeometry(const QScreen *screen);
+
+    QRect toDeviceRect(const QRect &rect) const;
 
     static constexpr int s_handleRadiusMouse = 9;
     static constexpr int s_handleRadiusTouch = 12;
@@ -146,6 +149,7 @@ private:
     QPointF m_startPos;
     QPointF m_initialTopLeft;
     QRect m_screensRect;
+    QRect m_deviceScreensRect;
 
     QMap<const QScreen *, QImage> m_images;
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.