This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new fc5ec84dd7 Fix blurry Hop GUI icons on Windows at 200% display scaling
(#7955)
fc5ec84dd7 is described below
commit fc5ec84dd71af89822b0d39ee001b224c9e1b15c
Author: Lance <[email protected]>
AuthorDate: Sat Aug 15 16:20:25 2026 +0800
Fix blurry Hop GUI icons on Windows at 200% display scaling (#7955)
* Fix blurry Hop GUI icons on Windows at 200% display scaling
Signed-off-by: lance <[email protected]>
* Fix blurry Hop GUI icons on Windows at 200% display scaling
Signed-off-by: lance <[email protected]>
* Restore LF line endings for MetadataPerspective and HopGuiSearchHelper
These two files were accidentally committed with CRLF in c4e16b75.
Signed-off-by: lance <[email protected]>
Co-authored-by: Cursor <[email protected]>
* Fix blurry Hop GUI icons on Windows at 200% display scaling
Signed-off-by: lance <[email protected]>
* update lf
---------
Signed-off-by: lance <[email protected]>
Co-authored-by: Cursor <[email protected]>
---
.../org/apache/hop/core/SwtUniversalImage.java | 68 ++++++++++++-
.../apache/hop/core/SwtUniversalImageBitmap.java | 12 +++
.../org/apache/hop/core/SwtUniversalImageSvg.java | 46 +++++----
.../org/apache/hop/ui/core/gui/GuiResource.java | 111 ++++++++++-----------
.../org/apache/hop/ui/hopgui/shared/SwtGc.java | 12 ++-
.../org/apache/hop/core/SwtUniversalImageTest.java | 47 +++++++++
6 files changed, 213 insertions(+), 83 deletions(-)
diff --git a/ui/src/main/java/org/apache/hop/core/SwtUniversalImage.java
b/ui/src/main/java/org/apache/hop/core/SwtUniversalImage.java
index f2126bfd9d..b57f419f30 100644
--- a/ui/src/main/java/org/apache/hop/core/SwtUniversalImage.java
+++ b/ui/src/main/java/org/apache/hop/core/SwtUniversalImage.java
@@ -21,9 +21,11 @@ import java.awt.image.BufferedImage;
import java.util.Map;
import java.util.TreeMap;
import org.apache.hop.core.exception.HopRuntimeException;
+import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.ImageDataProvider;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.RGB;
@@ -117,8 +119,49 @@ public abstract class SwtUniversalImage {
return result;
}
- /** Converts BufferedImage to SWT/Image with alpha channel. */
- protected Image swing2swt(Device device, BufferedImage img) {
+ /**
+ * SWT 3.134+ on Windows treats {@code new Image(device, ImageData)} as 100%
zoom and
+ * raster-scales it to the monitor zoom (SMOOTH), which makes icons blurry
at 200% DPI. {@link
+ * ImageDataProvider} re-rasterize at the requested zoom instead. RAP has no
per-monitor zoom, so
+ * keep the ImageData constructor there.
+ */
+ static boolean isDpiAwareImageProviderSupported() {
+ return !"rap".equals(SWT.getPlatform());
+ }
+
+ /**
+ * Pixel size of a logical extent at an SWT zoom percentage. Must be linear
({@code 200} → {@code
+ * 2 * 100}) to satisfy the {@link ImageDataProvider} contract.
+ */
+ static int pixelSize(int logical, int zoom) {
+ return Math.max(1, logical * zoom / 100);
+ }
+
+ /**
+ * Creates an {@link Image} that can supply native pixels for every SWT
zoom. On RAP the 100%
+ * variant is used as-is.
+ */
+ public static Image createDpiAwareImage(Device device, ImageDataProvider
provider) {
+ if (!isDpiAwareImageProviderSupported()) {
+ return new Image(device, provider.getImageData(100));
+ }
+ return new Image(device, provider);
+ }
+
+ /** ImageData at the given zoom, with a RAP-safe fallback that scales the
100% variant. */
+ public static ImageData getImageDataAtZoom(Image image, int zoom) {
+ if (isDpiAwareImageProviderSupported()) {
+ return image.getImageData(zoom);
+ }
+ ImageData data = image.getImageData();
+ if (zoom == 100) {
+ return data;
+ }
+ return data.scaledTo(pixelSize(data.width, zoom), pixelSize(data.height,
zoom));
+ }
+
+ /** Converts BufferedImage to SWT ImageData with alpha channel. */
+ static ImageData toImageData(BufferedImage img) {
PaletteData palette = new PaletteData(0xFF0000, 0xFF00, 0xFF);
ImageData data = new ImageData(img.getWidth(), img.getHeight(), 32,
palette);
for (int y = 0; y < data.height; y++) {
@@ -130,6 +173,25 @@ public abstract class SwtUniversalImage {
data.setAlpha(x, y, a);
}
}
- return new Image(device, data);
+ return data;
+ }
+
+ /**
+ * Creates a zoom-aware SWT image from a renderer that produces pixels at a
concrete width/height.
+ */
+ protected Image createDpiAwareImage(
+ Device device, int width, int height, ImageDataAtSize renderer) {
+ return createDpiAwareImage(
+ device, zoom -> renderer.render(pixelSize(width, zoom),
pixelSize(height, zoom)));
+ }
+
+ /** Converts BufferedImage to SWT/Image with alpha channel. */
+ protected Image swing2swt(Device device, BufferedImage img) {
+ return new Image(device, toImageData(img));
+ }
+
+ @FunctionalInterface
+ protected interface ImageDataAtSize {
+ ImageData render(int width, int height);
}
}
diff --git a/ui/src/main/java/org/apache/hop/core/SwtUniversalImageBitmap.java
b/ui/src/main/java/org/apache/hop/core/SwtUniversalImageBitmap.java
index bb7342dec3..0f78358504 100644
--- a/ui/src/main/java/org/apache/hop/core/SwtUniversalImageBitmap.java
+++ b/ui/src/main/java/org/apache/hop/core/SwtUniversalImageBitmap.java
@@ -20,6 +20,7 @@ package org.apache.hop.core;
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.Transform;
public class SwtUniversalImageBitmap extends SwtUniversalImage {
@@ -51,6 +52,17 @@ public class SwtUniversalImageBitmap extends
SwtUniversalImage {
@Override
protected Image renderRotated(Device device, int width, int height, double
angleRadians) {
+ if (angleRadians == 0d && zoomFactor == 1d) {
+ return createDpiAwareImage(
+ device,
+ width * 2,
+ height * 2,
+ (w, h) -> {
+ ImageData src = getImageDataAtZoom(bitmap, 100);
+ return src.scaledTo(w, h);
+ });
+ }
+
Image result = new Image(device, width * 2, height * 2);
GC gc = new GC(result);
diff --git a/ui/src/main/java/org/apache/hop/core/SwtUniversalImageSvg.java
b/ui/src/main/java/org/apache/hop/core/SwtUniversalImageSvg.java
index b6437a22aa..6c485970ca 100644
--- a/ui/src/main/java/org/apache/hop/core/SwtUniversalImageSvg.java
+++ b/ui/src/main/java/org/apache/hop/core/SwtUniversalImageSvg.java
@@ -81,32 +81,38 @@ public class SwtUniversalImageSvg extends SwtUniversalImage
{
@Override
protected Image renderSimple(Device device, int width, int height) {
- BufferedImage area = SwingUniversalImage.createBitmap(width, height);
- Graphics2D gc = SwingUniversalImage.createGraphics(area);
- SwingUniversalImageSvg.render(
- gc, svgGraphicsNode, svgGraphicsSize, width / 2, height / 2, width,
height, 0);
- gc.dispose();
-
- return swing2swt(device, area);
+ return createDpiAwareImage(device, width, height, (w, h) ->
toImageData(renderSvg(w, h, 0d)));
}
@Override
protected Image renderRotated(Device device, int width, int height, double
angleRadians) {
- BufferedImage doubleArea = SwingUniversalImage.createDoubleBitmap(width,
height);
+ return createDpiAwareImage(
+ device,
+ zoom -> {
+ int svgW = pixelSize(width, zoom);
+ int svgH = pixelSize(height, zoom);
+ BufferedImage doubleArea =
SwingUniversalImage.createDoubleBitmap(svgW, svgH);
+ Graphics2D gc = SwingUniversalImage.createGraphics(doubleArea);
+ SwingUniversalImageSvg.render(
+ gc,
+ svgGraphicsNode,
+ svgGraphicsSize,
+ doubleArea.getWidth() / 2,
+ doubleArea.getHeight() / 2,
+ svgW,
+ svgH,
+ angleRadians);
+ gc.dispose();
+ return toImageData(doubleArea);
+ });
+ }
- Graphics2D gc = SwingUniversalImage.createGraphics(doubleArea);
+ private BufferedImage renderSvg(int width, int height, double angleRadians) {
+ BufferedImage area = SwingUniversalImage.createBitmap(width, height);
+ Graphics2D gc = SwingUniversalImage.createGraphics(area);
SwingUniversalImageSvg.render(
- gc,
- svgGraphicsNode,
- svgGraphicsSize,
- doubleArea.getWidth() / 2,
- doubleArea.getHeight() / 2,
- width,
- height,
- angleRadians);
-
+ gc, svgGraphicsNode, svgGraphicsSize, width / 2, height / 2, width,
height, angleRadians);
gc.dispose();
-
- return swing2swt(device, doubleArea);
+ return area;
}
}
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
index 9a9960c5a6..88e25806d6 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
@@ -1553,18 +1553,18 @@ public class GuiResource {
builder.append(height);
String key = builder.toString();
- Image image = imageMap.get(key);
- if (image == null) {
- SwtUniversalImage svg = SwtSvgImageUtil.getImage(display, location);
- int realWidth = (int) Math.round(zoomFactor * width);
- int realHeight = (int) Math.round(zoomFactor * height);
- image =
- new Image(
- display, svg.getAsBitmapForSize(display, realWidth, realHeight),
SWT.IMAGE_COPY);
- svg.dispose();
- imageMap.put(key, image);
- }
- return image;
+ return imageMap.computeIfAbsent(
+ key,
+ k -> {
+ SwtUniversalImage svg = SwtSvgImageUtil.getImage(display, location);
+ int realWidth = (int) Math.round(zoomFactor * width);
+ int realHeight = (int) Math.round(zoomFactor * height);
+ Image loaded =
+ new Image(
+ display, svg.getAsBitmapForSize(display, realWidth,
realHeight), SWT.IMAGE_COPY);
+ svg.dispose();
+ return loaded;
+ });
}
/**
@@ -1601,60 +1601,53 @@ public class GuiResource {
builder.append('|').append(width).append('|').append(height).append('|').append(disabled);
String key = builder.toString();
- Image image = imageMap.get(key);
- if (image == null) {
- SwtUniversalImage svg = SwtSvgImageUtil.getUniversalImage(display,
classLoader, location);
-
- Image zoomedImaged = getZoomedImaged(svg, display, width, height);
- if (disabled) {
- // First disabled the image...
- //
- image = new Image(display, zoomedImaged, SWT.IMAGE_GRAY);
-
- // Now darken or lighten the image...
- //
- float factor;
- if (PropsUi.getInstance().isDarkMode()) {
- factor = 0.4f;
- } else {
- factor = 2.5f;
- }
-
- ImageData data = image.getImageData();
- for (int x = 0; x < data.width; x++) {
- for (int y = 0; y < data.height; y++) {
- int pixel = data.getPixel(x, y);
- int a = (pixel >> 24) & 0xFF;
- int b = (pixel >> 16) & 0xFF;
- int g = (pixel >> 8) & 0xFF;
- int r = pixel & 0xFF;
- a = (int) (a * factor);
- b = (int) (b * factor);
- g = (int) (g * factor);
- r = (int) (r * factor);
- data.setPixel(x, y, r + (g << 8) + (b << 16) + (a << 25));
+ return imageMap.computeIfAbsent(
+ key,
+ k -> {
+ SwtUniversalImage svg = SwtSvgImageUtil.getUniversalImage(display,
classLoader, location);
+
+ Image zoomedImaged = getZoomedImaged(svg, display, width, height);
+ Image loaded;
+ if (disabled) {
+ Image gray = new Image(display, zoomedImaged, SWT.IMAGE_GRAY);
+ float factor = PropsUi.getInstance().isDarkMode() ? 0.4f : 2.5f;
+ loaded =
+ SwtUniversalImage.createDpiAwareImage(
+ display,
+ zoom ->
+ applyDisabledContrast(
+ SwtUniversalImage.getImageDataAtZoom(gray, zoom),
factor));
+ } else {
+ loaded = new Image(display, zoomedImaged, SWT.IMAGE_COPY);
}
- image.dispose();
- image = new Image(display, data);
- }
- } else {
- image = new Image(display, zoomedImaged, SWT.IMAGE_COPY);
- }
- svg.dispose();
- imageMap.put(key, image);
+ svg.dispose();
+ return loaded;
+ });
+ }
+
+ private static ImageData applyDisabledContrast(ImageData source, float
factor) {
+ ImageData data = (ImageData) source.clone();
+ for (int x = 0; x < data.width; x++) {
+ for (int y = 0; y < data.height; y++) {
+ int pixel = data.getPixel(x, y);
+ int a = (pixel >> 24) & 0xFF;
+ int b = (pixel >> 16) & 0xFF;
+ int g = (pixel >> 8) & 0xFF;
+ int r = pixel & 0xFF;
+ a = (int) (a * factor);
+ b = (int) (b * factor);
+ g = (int) (g * factor);
+ r = (int) (r * factor);
+ data.setPixel(x, y, r + (g << 8) + (b << 16) + (a << 25));
+ }
}
- return image;
+ return data;
}
public Color getColor(int red, int green, int blue) {
RGB rgb = new RGB(red, green, blue);
- Color color = colorMap.get(rgb);
- if (color == null) {
- color = new Color(display, rgb);
- colorMap.put(rgb, color);
- }
- return color;
+ return colorMap.computeIfAbsent(rgb, key -> new Color(display, key));
}
/**
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SwtGc.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SwtGc.java
index 9edaca5fe6..253fb3e598 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SwtGc.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/shared/SwtGc.java
@@ -607,7 +607,17 @@ public class SwtGc implements IGc {
}
try (java.io.InputStream in =
org.apache.hop.core.vfs.HopVfs.getInputStream(path)) {
org.eclipse.swt.graphics.ImageData data = new
org.eclipse.swt.graphics.ImageData(in);
- Image img = new Image(gc.getDevice(), data);
+ Image img =
+ SwtUniversalImage.createDpiAwareImage(
+ gc.getDevice(),
+ zoom -> {
+ if (zoom == 100) {
+ return data;
+ }
+ int w = Math.max(1, data.width * zoom / 100);
+ int h = Math.max(1, data.height * zoom / 100);
+ return data.scaledTo(w, h);
+ });
try {
gc.drawImage(img, 0, 0, data.width, data.height, x, y, width,
height);
} finally {
diff --git a/ui/src/test/java/org/apache/hop/core/SwtUniversalImageTest.java
b/ui/src/test/java/org/apache/hop/core/SwtUniversalImageTest.java
new file mode 100644
index 0000000000..95f4f1ce8e
--- /dev/null
+++ b/ui/src/test/java/org/apache/hop/core/SwtUniversalImageTest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.core;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.awt.image.BufferedImage;
+import org.eclipse.swt.graphics.ImageData;
+import org.junit.jupiter.api.Test;
+
+/** Unit test for {@link SwtUniversalImage} */
+class SwtUniversalImageTest {
+
+ @Test
+ void pixelSizeScalesLinearlyWithZoom() {
+ assertEquals(16, SwtUniversalImage.pixelSize(16, 100));
+ assertEquals(32, SwtUniversalImage.pixelSize(16, 200));
+ assertEquals(24, SwtUniversalImage.pixelSize(16, 150));
+ assertEquals(1, SwtUniversalImage.pixelSize(0, 200));
+ }
+
+ @Test
+ void toImageDataKeepsSizeAndAlpha() {
+ BufferedImage src = new BufferedImage(16, 8, BufferedImage.TYPE_INT_ARGB);
+ src.setRGB(0, 0, 0x80FF0000);
+ ImageData data = SwtUniversalImage.toImageData(src);
+
+ assertEquals(16, data.width);
+ assertEquals(8, data.height);
+ assertEquals(0x80, data.getAlpha(0, 0));
+ }
+}