From 3bd1be3aabeca3bcef8b1c8b52b67b0faeb2b4d3 Mon Sep 17 00:00:00 2001 From: Heiko Klare Date: Thu, 9 Jul 2026 17:41:43 +0200 Subject: [PATCH] [Win32] Improve size calculation for cropped and scaled image drawing When drawing cropped and scaled images, the calculation for the source rectangle to draw is quite error prone: - It does not distinguish between different scale factors in X and Y direction, leading to large rounding errors if the extents in one direction are highly different from the extents in the other direction - It does not apply rounding that is consistent to the scaling done by the Image class, thus leading to differently rounded sizes when scaling an image in the Image class and drawing that same image in the GC - The "error correction" to deal with rounding at fractional scale factors is too restrictive, in particular when the scale factor is less than 1, and is not applied when only one axis has a non-unity scale factor This change reimplements the source rectangle calculation as follows: - It treats the scale factors for both axes independently - It applies the same rounding method to the rectangle extents as done by the Image scaling implementation - It rounds up the scale factor when checking for the allowed size error on fractional scaling, such that a scale factor less than 1 still allows for an error of 1 in size - It applies the bounds correction whenever either axis has a non-unity scale factor, not only when both axes do A regression test is added to GCWin32Tests that verifies all three drawImage overloads (3-arg, 5-arg, and 9-arg) produce identical pixel output for a 500-wide image across a matrix of small prime heights and fractional zoom levels (100%, 125%, 150%, 175%, 200%). Fixes https://github.com/eclipse-platform/eclipse.platform.swt/issues/3454 Co-Authored-By: Claude Sonnet 4.6 --- .../eclipse/swt/graphics/GCWin32Tests.java | 72 ++++++++++++++++++- .../win32/org/eclipse/swt/graphics/GC.java | 16 +++-- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java index 6b4cb998c86..47b20458042 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Tests/win32/org/eclipse/swt/graphics/GCWin32Tests.java @@ -14,16 +14,19 @@ package org.eclipse.swt.graphics; import static org.junit.Assert.assertEquals; -import static org.junit.jupiter.api.Assertions.assertAll; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; +import java.util.*; import java.util.concurrent.*; +import java.util.stream.*; import org.eclipse.swt.*; import org.eclipse.swt.internal.*; import org.eclipse.swt.widgets.*; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.*; +import org.junit.jupiter.params.*; +import org.junit.jupiter.params.provider.*; @ExtendWith(PlatformSpecificExecutionExtension.class) @ExtendWith(WithMonitorSpecificScalingExtension.class) @@ -142,4 +145,69 @@ private static int renderTextAndCountNonWhitePixels(Image target, Font font, Str } return count; } + + /** + * Regression test for the size calculation in scaling/cropping GC.drawImage() + * operations with asymmetric source dimensions (smaller height than width) at + * fractional zoom levels. + *

+ * At fractional zoom levels the effective X and Y scale factors diverge because + * each axis is rounded independently (e.g. at 125%: + * scaleFactorX = 625/500 = 1.25 but + * scaleFactorY = 24/19 ≈ 1.263). + */ + @ParameterizedTest + @MethodSource("zoomAndHeightArguments") + public void drawImage_asymmetricDimensionsAtFractionalZoom(int zoom, int height) { + Display display = Display.getDefault(); + + int logicalWidth = 500; + int logicalHeight = height; + + PaletteData palette = new PaletteData(0xFF0000, 0xFF00, 0xFF); + ImageData srcData = new ImageData(logicalWidth, logicalHeight, 32, palette); + for (int y = 0; y < logicalHeight; y++) { + for (int x = 0; x < logicalWidth; x++) { + // left half red, right half blue – makes wrong-rectangle errors visible + srcData.setPixel(x, y, x < logicalWidth / 2 ? 0xFF0000 : 0x0000FF); + } + } + Image srcImage = new Image(display, srcData); + + int previousZoom = DPIUtil.getDeviceZoom(); + try { + DPIUtil.setDeviceZoom(zoom); + + Image referenceImage = new Image(display, logicalWidth + 5, logicalHeight + 5); + GC referenceGC = new GC(referenceImage); + referenceGC.drawImage(srcImage, 0, 0); + referenceGC.dispose(); + + Image testImageScaled = new Image(display, logicalWidth + 5, logicalHeight + 5); + GC testGC = new GC(testImageScaled); + testGC.drawImage(srcImage, 0, 0, logicalWidth, logicalHeight); + testGC.dispose(); + assertArrayEquals(referenceImage.getImageData(zoom).data, testImageScaled.getImageData(zoom).data); + testImageScaled.dispose(); + + Image testImageScaledCropped = new Image(display, logicalWidth + 5, logicalHeight + 5); + testGC = new GC(testImageScaledCropped); + testGC.drawImage(srcImage, 0, 0, logicalWidth, logicalHeight, 0, 0, logicalWidth, logicalHeight); + testGC.dispose(); + assertArrayEquals(referenceImage.getImageData(zoom).data, testImageScaledCropped.getImageData(zoom).data); + testImageScaledCropped.dispose(); + + referenceImage.dispose(); + } finally { + DPIUtil.setDeviceZoom(previousZoom); + srcImage.dispose(); + } + } + + private static Stream zoomAndHeightArguments() { + int[] zooms = { 25, 50, 75, 100, 125, 150, 175, 200 }; + int[] heights = IntStream.rangeClosed(4, 20).toArray(); + return Arrays.stream(zooms).boxed() + .flatMap(zoom -> Arrays.stream(heights).mapToObj(height -> Arguments.of(zoom, height))); + } } 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 e6b99131a7b..525cf88296d 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 @@ -1250,11 +1250,15 @@ private Rectangle computeSourceRectangle(ImageHandle imageHandle, Rectangle full * 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) { + float scaleFactorX = (1f * imageHandle.width()) / fullImageBounds.width; + float scaleFactorY = (1f * imageHandle.height()) / fullImageBounds.height; + int srcXPixels = Math.round(scaleFactorX * src.x); + int srcWidthPixels = Math.round(scaleFactorX * (src.x + src.width)) - srcXPixels; + int srcYPixels = Math.round(scaleFactorY * src.y); + int srcHeightPixels = Math.round(scaleFactorY * (src.y + src.height)) - srcYPixels; + Rectangle srcPixels = new Rectangle(srcXPixels, srcYPixels, srcWidthPixels, srcHeightPixels); + + if (Math.abs(scaleFactorX - 1f) >= 0.01f || Math.abs(scaleFactorY - 1f) >= 0.01f) { /* * This is a HACK! Due to rounding errors at fractional scale factors, * the coordinates may be slightly off. The workaround is to restrict @@ -1263,7 +1267,7 @@ private Rectangle computeSourceRectangle(ImageHandle imageHandle, Rectangle full 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) { + if (errX <= Math.max(1, scaleFactorX) && errY <= Math.max(1, scaleFactorY)) { srcPixels.intersect(new Rectangle(0, 0, imageHandle.width(), imageHandle.height())); } else { SWT.error (SWT.ERROR_INVALID_ARGUMENT);