From 7c7f6a259e2ce8fa138aea76098238290dc9df59 Mon Sep 17 00:00:00 2001 From: al-noori Date: Tue, 9 Dec 2025 16:28:58 +0100 Subject: [PATCH] Fix GC#drawImage + ImageGcDrawer for Cropping and Scaling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GC#drawImage method takes (image, srcX, srcY, srcWidth, srcHeight, destX, destY, destWidth, destHeight) as arguments and crops and scales from the source region to the destination region. Passing an image drawn via ImageGCDrawer led to the following issue: The image handle from the subcall is resolved using the monitor zoom (data.nativeZoom) and the calculated scaledImageZoom (gcZoom * scaleFactor). This handle corresponds to an ImageData initialized at scaledImageZoom, whereas the drawings of the second GC are performed using the monitor zoom, subject to the auto-scale property. This mismatch results in unaligned sizing of drawings. For example, a 200% monitor zoom combined with a scale factor of 0.5 produces a scaledImageZoom of 100%. As a result, the ImageData is initialized at 100%, while drawing occurs at 200%. Furthermore, the calculation of scaledImageZoom uses fallback logic that only allows 100% and 200% as possible outcomes, which is clearly unintended in this context. The fix delegates resolving the correct handle to the Image class by passing the width/height of the full image scaled by the scaledImageZoom. This is a space on where scaled src coordinates/width/height lie. A callback then creates a new handle for the height/width and respects the auto-scale property. If the returned handle matches the full image scaled to the requested scaledImageZoom in width and height, the source region coordinates/width/height are passed directly in pixels at that zoom. Otherwise, the internal zoom factor is derived from the returned handle’s width relative to the full image, and the source region coordinates are converted to pixel values using this internal zoom. --- .../win32/org/eclipse/swt/graphics/GC.java | 87 ++++++++++--------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java index 1c85beb57d..4894a5de70 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java @@ -15,16 +15,13 @@ import java.util.*; -import java.util.List; import java.util.function.*; -import java.util.stream.*; import org.eclipse.swt.*; import org.eclipse.swt.graphics.Image.*; import org.eclipse.swt.internal.*; import org.eclipse.swt.internal.gdip.*; import org.eclipse.swt.internal.win32.*; -import org.eclipse.swt.widgets.*; /** * Class GC is where all of the drawing capabilities that are @@ -1178,18 +1175,52 @@ private class DrawScalingImageToImageOperation extends ImageOperation { @Override void apply() { + draw(getImage(), source.x, source.y, source.width, source.height, destination.x, destination.y, destination.width, destination.height); + } + + private void draw(Image image, int srcX, int srcY, int srcWidth, int srcHeight, int destX, int destY, int destWidth, int destHeight) { int gcZoom = getZoom(); - int srcImageZoom = calculateZoomForImage(gcZoom, source.width, source.height, destination.width, destination.height); - drawImage(getImage(), source.x, source.y, source.width, source.height, destination.x, destination.y, destination.width, destination.height, gcZoom, srcImageZoom); + int requestedImageZoom = calculateZoomForImage(gcZoom, source.width, source.height, destination.width, destination.height); + + Rectangle src = new Rectangle(srcX, srcY, srcWidth, srcHeight); + Rectangle destPixels = Win32DPIUtils.pointToPixel(drawable, new Rectangle(destX, destY, destWidth, destHeight), gcZoom); + Rectangle fullImageBounds = image.getBounds(); + Rectangle requestedFullImageBoundsPixels = Win32DPIUtils.pointToPixel(drawable, fullImageBounds, requestedImageZoom); + + image.executeOnImageHandleAtBestFittingSize((tempHandle) -> { + Rectangle srcPixels = computeSourceRectangle(tempHandle, fullImageBounds, src); + drawImage(image, srcPixels.x, srcPixels.y, srcPixels.width, srcPixels.height, destPixels.x, destPixels.y, destPixels.width, + destPixels.height, false, tempHandle); + }, requestedFullImageBoundsPixels.width, requestedFullImageBoundsPixels.height); } - private Collection getAllCurrentMonitorZooms() { - if (device instanceof Display display) { - return Arrays.stream(display.getMonitors()) - .map(Monitor::getZoom) - .collect(Collectors.toSet()); + private Rectangle computeSourceRectangle(ImageHandle imageHandle, Rectangle fullImageBounds, Rectangle src) { + /* + * The point values (x, y, width, height) of the source "part" in points will be + * computed to pixels depending on the factor of the full image bounds to the + * actual OS handle size that will be used. + */ + float scaleFactor = Math.min(1f * imageHandle.width() / fullImageBounds.width, 1f * imageHandle.height() / fullImageBounds.height); + int closestZoomOfHandle = Math.round(scaleFactor * 100); + Rectangle srcPixels = Win32DPIUtils.pointToPixel(drawable, src, closestZoomOfHandle); + + if (closestZoomOfHandle != 100) { + /* + * This is a HACK! Due to rounding errors at fractional scale factors, + * the coordinates may be slightly off. The workaround is to restrict + * coordinates to the allowed bounds. + */ + int errX = srcPixels.x + srcPixels.width - imageHandle.width(); + int errY = srcPixels.y + srcPixels.height - imageHandle.height(); + if (errX != 0 || errY != 0) { + if (errX <= closestZoomOfHandle / 100 && errY <= closestZoomOfHandle / 100) { + srcPixels.intersect(new Rectangle(0, 0, imageHandle.width(), imageHandle.height())); + } else { + SWT.error (SWT.ERROR_INVALID_ARGUMENT); + } + } } - return Collections.emptySet(); + return srcPixels; } private int calculateZoomForImage(int gcZoom, int srcWidth, int srcHeight, int destWidth, int destHeight) { @@ -1205,15 +1236,9 @@ private int calculateZoomForImage(int gcZoom, int srcWidth, int srcHeight, int d return gcZoom; } - float imageScaleFactor = 1f * destWidth / srcWidth; + float imageScaleFactor = Math.max(1f * destWidth / srcWidth, 1f * destHeight / srcHeight); int imageZoom = Math.round(gcZoom * imageScaleFactor); - if (getAllCurrentMonitorZooms().contains(imageZoom)) { - return imageZoom; - } - if (imageZoom > 150) { - return 200; - } - return 100; + return imageZoom; } } @@ -1241,30 +1266,6 @@ private void drawImage(Image image, int destX, int destY, int destWidth, int des }, destPixels.width, destPixels.height); } -private void drawImage(Image image, int srcX, int srcY, int srcWidth, int srcHeight, int destX, int destY, - int destWidth, int destHeight, int imageZoom, int scaledImageZoom) { - Rectangle src = Win32DPIUtils.pointToPixel(drawable, new Rectangle(srcX, srcY, srcWidth, srcHeight), scaledImageZoom); - Rectangle dest = Win32DPIUtils.pointToPixel(drawable, new Rectangle(destX, destY, destWidth, destHeight), imageZoom); - if (scaledImageZoom != 100) { - /* - * This is a HACK! Due to rounding errors at fractional scale factors, - * the coordinates may be slightly off. The workaround is to restrict - * coordinates to the allowed bounds. - */ - Rectangle b = image.getBounds(scaledImageZoom); - int errX = src.x + src.width - b.width; - int errY = src.y + src.height - b.height; - if (errX != 0 || errY != 0) { - if (errX <= scaledImageZoom / 100 && errY <= scaledImageZoom / 100) { - src.intersect(b); - } else { - SWT.error (SWT.ERROR_INVALID_ARGUMENT); - } - } - } - drawImage(image, src.x, src.y, src.width, src.height, dest.x, dest.y, dest.width, dest.height, false, image.getHandle(scaledImageZoom, data.nativeZoom)); -} - private class DrawImageToImageOperation extends ImageOperation { private final Rectangle source; private final Rectangle destination;