eclipse-platform / eclipse.platform.swt

Eclipse SWT
https://www.eclipse.org/swt/
Eclipse Public License 2.0
117 stars 135 forks source link

[4k Display] AWT fonts within SWT are badly scaled on 4k displays #1411

Closed wolfgang-ch closed 2 months ago

wolfgang-ch commented 2 months ago

Describe the bug

Mainly small fonts are looking really bad compared with SWT fonts.

Why do I use AWT fonts? SWT has a limit with antialiasing when drawing into an image, see here https://github.com/mytourbook/mytourbook/issues/1376#issuecomment-2200085788

With autoScale=100 (for none 4k displays) the AWT font rendering is as good as SWT but autoScale > 100 has this issue.

Is there any AWT/SWT parameter to disable AWT font/image scaling when swt.autoScale > 100 ?

To Reproduce

package test4k;

import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;

import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

public class SWTvsAWT_FontsAndImages {

   private static int                  AUTO_SCALE;

   private static int                  IMAGE_WIDTH;
   private static int                  IMAGE_HEIGHT;

   private static final String         FONT_NAME            = "Segoe UI";
   private static int[]                FONT_SIZE;

   private static final int            MARGIN               = 2;

   private static final int            SHELL_X              = 1500;
   private static final int            SHELL_Y              = 100;

   private static final int            FOREGROUND_COLOR_SWT = SWT.COLOR_BLACK;
   private static final int            BACKGROUND_COLOR_SWT = SWT.COLOR_WHITE;

   private static final java.awt.Color FOREGROUND_COLOR_AWT = java.awt.Color.black;
   private static final java.awt.Color BACKGROUND_COLOR_AWT = java.awt.Color.white;

   private static Display              _swtDisplay;
   private static Font[]               _swtFont;
   private static int[]                _swtFontHeight;

   static {

      AUTO_SCALE = 200;
      AUTO_SCALE = 100;
      AUTO_SCALE = 150;

      FONT_SIZE = new int[] {
            10,
            12,
            16,
            20,
            24,
//            28,
            36
      };

      System.setProperty("swt.autoScale", Integer.toString(AUTO_SCALE));

      _swtDisplay = new Display();

      int numFonts = FONT_SIZE.length;
      int allFontHeights = 0;

      _swtFont = new Font[numFonts];
      _swtFontHeight = new int[numFonts];

      for (int fontIndex = 0; fontIndex < numFonts; fontIndex++) {

         Font swtFont = new Font(_swtDisplay, FONT_NAME, FONT_SIZE[fontIndex], SWT.NORMAL);
         FontData swtFontData = swtFont.getFontData()[0];

         int fontHeight = swtFontData.getHeight();

         _swtFont[fontIndex] = swtFont;
         _swtFontHeight[fontIndex] = fontHeight;

         allFontHeights += fontHeight;
      }

      IMAGE_WIDTH = 1500;
      IMAGE_HEIGHT = (int) (allFontHeights * 1.8);
   }

   private static String getTestText(int fontIndex) {

      return "  autoScale=%d  FONT SIZE=%d".formatted(AUTO_SCALE, FONT_SIZE[fontIndex]);
   }

   private static Image convertAWTtoSWT(final BufferedImage awtImage) {

      final int imageWidth = awtImage.getWidth();
      final int imageHeight = awtImage.getHeight();

      final ImageData swtImageData = new ImageData(imageWidth, imageHeight, 24, new PaletteData(0xFF0000, 0xFF00, 0xFF));

      final int scansize = (((imageWidth * 3) + 3) * 4) / 4;

      final WritableRaster alphaRaster = awtImage.getAlphaRaster();
      final byte[] alphaBytes = new byte[imageWidth];

      for (int y = 0; y < imageHeight; y++) {

         final int[] buff = awtImage.getRGB(0, y, imageWidth, 1, null, 0, scansize);

         swtImageData.setPixels(0, y, imageWidth, buff, 0);

         if (alphaRaster != null) {

            final int[] alpha = alphaRaster.getPixels(0, y, imageWidth, 1, (int[]) null);

            for (int i = 0; i < imageWidth; i++) {
               alphaBytes[i] = (byte) alpha[i];
            }

            swtImageData.setAlphas(0, y, imageWidth, alphaBytes, 0);
         }
      }

      return new Image(_swtDisplay, swtImageData);
   }

   private static BufferedImage createAWTImage() {

      final BufferedImage awtImage = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_4BYTE_ABGR);

      final Graphics2D g2d = awtImage.createGraphics();
      try {

         g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

         g2d.setBackground(BACKGROUND_COLOR_AWT);
         g2d.clearRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);

         int devY = 0;

         for (int fontIndex = 0; fontIndex < FONT_SIZE.length; fontIndex++) {

            java.awt.Font font = new java.awt.Font(FONT_NAME, java.awt.Font.PLAIN, FONT_SIZE[fontIndex]);
            g2d.setFont(font);

            final FontMetrics fontMetrics = g2d.getFontMetrics();
            final int textHeight = fontMetrics.getHeight();
            devY += textHeight;

            g2d.setColor(FOREGROUND_COLOR_AWT);
            g2d.drawString(" AWT" + getTestText(fontIndex), MARGIN, devY);
         }

      } finally {

         g2d.dispose();
      }

      return awtImage;
   }

   private static Image createSWTImage() {

      final Image swtImage = new Image(_swtDisplay, IMAGE_WIDTH, IMAGE_HEIGHT);

      final GC gc = new GC(swtImage);
      {
         gc.setAntialias(SWT.ON);

         gc.setBackground(_swtDisplay.getSystemColor(BACKGROUND_COLOR_SWT));
         gc.fillRectangle(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);

         int devY = 0;

         for (int fontIndex = 0; fontIndex < FONT_SIZE.length; fontIndex++) {

            gc.setFont(_swtFont[fontIndex]);
            gc.setForeground(_swtDisplay.getSystemColor(FOREGROUND_COLOR_SWT));
            gc.drawString(" SWT" + getTestText(fontIndex), MARGIN, devY, true);

            devY += _swtFontHeight[fontIndex] + 10;
         }
      }
      gc.dispose();

      return swtImage;
   }

   public static void main(final String[] args) {

      final Image swtImage = createSWTImage();

      final BufferedImage awtImage = createAWTImage();
      final Image swtImageFromAwt = convertAWTtoSWT(awtImage);

      final Shell shell = new Shell(_swtDisplay);
      shell.setText("SWT vs AWT scaling");
      shell.setLocation(SHELL_X, SHELL_Y);
      shell.setSize(IMAGE_WIDTH + 50, IMAGE_HEIGHT * 2 + 30);

      shell.addListener(SWT.Paint, event -> {

         int devY = MARGIN;

         GC gc = event.gc;

         gc.drawImage(swtImage, MARGIN, devY);
         devY += IMAGE_HEIGHT + MARGIN;

         gc.drawImage(swtImageFromAwt, MARGIN, devY);
      });

      shell.open();

      while (!shell.isDisposed()) {
         if (!_swtDisplay.readAndDispatch()) {
            _swtDisplay.sleep();
         }
      }

      swtImage.dispose();
      swtImageFromAwt.dispose();

      for (Font font : _swtFont) {
         font.dispose();
      }

      _swtDisplay.dispose();
   }
}

Expected behavior AWT within SWT should scale fonts with the same quality as SWT

Screenshots This screenshot should be viewed without github scaling !

SwtVsAwtScaling

Environment:

  1. Select the platform(s) on which the behavior is seen:

      • [ ] All OS
      • [x ] Windows
      • [?] Linux, not tested
      • [?] macOS, not tested
  2. Additional OS info (e.g. OS version, Linux Desktop, etc) Windows 10

  3. JRE/JDK version java.vendor.version=Temurin-21.0.4+7

Version since 4.23 and older

Workaround (or) Additional context Have not yet found

laeubi commented 2 months ago

Just wondering as you already have a BufferedImage, why not use that? I always found AWTs Graphics2D to provide superior features and quality over SWT GC.

wolfgang-ch commented 2 months ago

Just wondering as you already have a BufferedImage, why not use that? I always found AWTs Graphics2D to provide superior features and quality over SWT GC.

In https://github.com/mytourbook/mytourbook/issues/1376#issuecomment-2200085788 a BufferedImage is used but the AWT/SWT scaling issue is visible to me since I have a 4k display, 2 weeks ago.

I need to use autoScale > 100 otherwise the UI is too small on a 4k display

merks commented 2 months ago

FYI, I have 3 4k monitors, each with a difference scale because each is a different physical size. I personally find that letting the system do the scaling looks better than what the application tries to do, so I change this settings on the java.exe and the javaw.exe:

image

wolfgang-ch commented 2 months ago

@merks With you suggestion, the scaling is different, SWT looks worse compared with AWT, e.g. large S, but it is blury

This screenshot is zoomed 300% without antialiasing

image

merks commented 2 months ago

In my case, my primary monitor is at 100% and then the others are 125% and 225%, so I think in that case the two smaller monitors are down scaling the full resolution of the primary monitor, and that looks quite nice visually. The captures here though look blurry but that's not how it actually look on the smaller monitors because these images all physically the same size across the monitors:

image

image

image

Anyway, it's just a thought because the behavior without this setting was horribly tiny unreadable content for me.

In any case,, there are folks working hard to improve the hdpi support, but I doubt AWT will be a priority for them. Have you been testing with the 4.33 I-Builds? You might consider helping in the hdpi effort. If so, I expect some folks could probably offer some guidance.

wolfgang-ch commented 2 months ago

I just tested it with I20240818-1800 but it looks not better.

HeikoKlare commented 2 months ago

The improved HiDPI support (for Windows) is still work in progress and will not be available in the upcoming 2024-09 release in an "easy" way. If you want to test, you can add the following VM arguments to your Eclipse application: -Dswt.autoScale.updateOnRuntime=true -Dswt.autoScale=quarter. As a result, each shell will rescale depending on the monitor is currently placed on.

We plan to provide an Eclipse preference to enable the feature as soon as possible in 2024-12 development (early M1 phase), so that it becomes easier to activate and test that feature.

In any case,, there are folks working hard to improve the hdpi support, but I doubt AWT will be a priority for them.

We have not taken a detailed look into AWT by now. But since we are using AWT in our RCP product as well, we will consider that at some point in time in more detail. But of course, any help on this is appreciated, so if you/someone wants to work on improvements in the meantime, feel free :-)

wolfgang-ch commented 2 months ago

I've tested the above code with -Dswt.autoScale.updateOnRuntime=true -Dswt.autoScale=quarter and eclipse I20240818-1800 on my 2 Monitors

The shell is opened in the first monitor (my Notebook) and it looks OK...

image

...when moving the window to the second monitor, then SWT has the same scaling issue as AWT (you have to zoomin into the image to skip github scaling)

image

This is the slightly adjusted code SWTvsAWT_FontsAndImages.zip

HeikoKlare commented 2 months ago

I took a deeper look into your example and hope I understand it correctly: if am not mistaken, the reason for your issue is that the images you create are raster-scaled, i.e., they are drawn according to a 100% monitor scaling and then the result is simply scaled up pixel-by-pixel. Thus, it's not a rendering problem but a rescaling problem. Precisely, you always seem to reder with fixed font sizes into an image of fixed size while your widgets become larger with autoScale > 100 (e.g., the values passed to shell.setSize() are automatically scaled up). The gc.drawImage() operation will also do an automatic scale-up, and since the image is provided as a bitmap and not via some image data provider that could provide the image in a proper scaling, it will just use a raster scaling, which leads to blurry results. The SWT one works for the scaling of the primary monitor, because an SWT image is always properly initialized for the primary monitor's zoom. But that's SWT functionality and not done by AWT, which is why AWT results look worse.

I see two options for you:

wolfgang-ch commented 2 months ago

@HeikoKlare

Thank you for your analysis

-Dswt.autoScale=false/100 is not an option for me otherwise the app UI fonts/icons on a 4k display are too tiny

In my app, the texts are painted into an image because this is done in a background thread which may explain why this is not rendered directly (without an image) in the UI thread. This "background" image is then overlayed and rendered in the UI thread.

... and not via some image data provider that could provide the image in a proper scaling, ...

Which image data provider do you mean ?

HeikoKlare commented 2 months ago

Doing the rendering in a background thread is, of course, fine. You then just have to consider the proper size for the image to be rendered. Currently, you render the image for a 100% scaling in the background and then it is used to draw it onto a potentially higher-scaled UI, which means it is upscaled when drawing the image onto the GC.

What you need to do is to consider the current scaling when creating the image you paint into (in your background thred). An easy (non-API) way to do so would be to retrieve the zoom value via DPIUtil.getNativeZoom() and use that to scale the image and font you are drawing into (the scale factor should be DPIUtil.getNativeZoom() / 100. That will only work as long as the application only has a single zoom value and the operating system keeps tracks of (blurry) scaling it when moving to another monitor with a different scaling (as explained by Ed).

A probably better option (and one that will also work when having the updateOnRuntime feature enabled for better HiDPI support) would be to implement an ImageDataProvider (org.eclipse.swt.graphics.ImageDataProvider), which only has a single method getImageData (int zoom). In that implementation you can create your image with the zoom passed to the method of the provider on demand. You can, of course, still cache the buffered image for a specific zoom value inside, but this way you will be able to receive the zoom value and create a properly sized image for that. You can pass this image data provider to the constructor of the image to be draw via the GC in the UI. When painting that image in the UI, the image data provider will be requested to deliver the image with the proper zoom. You will probably need to run your application with -Dswt.autoScale=quarter then, as otherwise this zoom value will only be 100 or 200.

wolfgang-ch commented 2 months ago

This sounds interresting, I'll try it by using the ImageDataProvider solution

laeubi commented 2 months ago

This sounds interresting, I'll try it by using the ImageDataProvider solution

Of course that makes rendering in the background somewhat uselsess as it will ask that ad-hoc if required, I must confess that I still don't understand why not rendering it directly on the image?!

wolfgang-ch commented 2 months ago

@HeikoKlare 🏆 🥇 🥈 🥉

It works 😎

In this test, SWT is not yet using an ImageDataProvider but AWT

image

image

image

This is the modified code with an ImageDataProvider SWTvsAWT_FontsAndImages.zip

HeikoKlare commented 2 months ago

Thank you for the feedback, @wolfgang-ch! Great to see that it works and also an interesting insight for us to see the interaction with AWT.