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));
+  }
+}

Reply via email to