Skip to content

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

Closed

Description

@wolfgang-ch

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 mytourbook/mytourbook#1376 (comment)

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
  1. Additional OS info (e.g. OS version, Linux Desktop, etc)
    Windows 10

  2. 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

Activity

  1. laeubi commented on Aug 19, 2024

    @laeubi
    Contributor

    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.

  2. wolfgang-ch commented on Aug 19, 2024

    @wolfgang-ch
    Author

    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 mytourbook/mytourbook#1376 (comment) 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

  3. merks commented on Aug 19, 2024

    @merks
    Contributor

    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

  4. wolfgang-ch commented on Aug 19, 2024

    @wolfgang-ch
    Author

    @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

  5. merks commented on Aug 19, 2024

    @merks
    Contributor

    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.

  6. wolfgang-ch commented on Aug 19, 2024

    @wolfgang-ch
    Author

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

  7. HeikoKlare commented on Aug 19, 2024

    @HeikoKlare
    Contributor

    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 :-)

  8. wolfgang-ch commented on Aug 19, 2024

    @wolfgang-ch
    Author

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

    • Monitor 1: 3840 x 2160 - 100% Windows scaling
    • Monitor 2: 3840 x 2160 - 150%

    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

  9. HeikoKlare commented on Aug 19, 2024

    @HeikoKlare
    Contributor

    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:

    • Completely disable scaling for your application: -Dswt.autoScale=false
    • Make your AWT images adapt size (image + font) to the shell's scaling, i.e., when creating your images and fonts, you need to adapt the size according to the application's scaling
  10. wolfgang-ch commented on Aug 20, 2024

    @wolfgang-ch
    Author

    @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 ?

  11. HeikoKlare commented on Aug 20, 2024

    @HeikoKlare
    Contributor

    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.

  12. wolfgang-ch commented on Aug 20, 2024

    @wolfgang-ch
    Author

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

  13. laeubi commented on Aug 20, 2024

    @laeubi
    Contributor

    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?!

  14. added a commit that references this issue on Aug 20, 2024
  15. wolfgang-ch commented on Aug 20, 2024

    @wolfgang-ch
    Author

    @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

  16. HeikoKlare commented on Aug 20, 2024

    @HeikoKlare
    Contributor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions