Repository navigation
[4k Display] AWT fonts within SWT are badly scaled on 4k displays #1411
Description
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 !
Environment:
- Select the platform(s) on which the behavior is seen:
-
- All OS
-
- [x ] Windows
-
- [?] Linux, not tested
-
- [?] macOS, not tested
-
Additional OS info (e.g. OS version, Linux Desktop, etc)
Windows 10 -
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
laeubi commentedon Aug 19, 2024
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 commentedon Aug 19, 2024
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
merks commentedon Aug 19, 2024
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:
wolfgang-ch commentedon Aug 19, 2024
@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
merks commentedon Aug 19, 2024
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:
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 commentedon Aug 19, 2024
I just tested it with I20240818-1800 but it looks not better.
HeikoKlare commentedon Aug 19, 2024
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.
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 commentedon Aug 19, 2024
I've tested the above code with
-Dswt.autoScale.updateOnRuntime=true -Dswt.autoScale=quarterand eclipse I20240818-1800 on my 2 MonitorsThe shell is opened in the first monitor (my Notebook) and it looks OK...
...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)
This is the slightly adjusted code SWTvsAWT_FontsAndImages.zip
HeikoKlare commentedon Aug 19, 2024
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). Thegc.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:
-Dswt.autoScale=falsewolfgang-ch commentedon Aug 20, 2024
@HeikoKlare
Thank you for your analysis
-Dswt.autoScale=false/100is not an option for me otherwise the app UI fonts/icons on a 4k display are too tinyIn 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.
Which image data provider do you mean ?
HeikoKlare commentedon Aug 20, 2024
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 beDPIUtil.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
updateOnRuntimefeature enabled for better HiDPI support) would be to implement anImageDataProvider(org.eclipse.swt.graphics.ImageDataProvider), which only has a single methodgetImageData (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=quarterthen, as otherwise this zoom value will only be 100 or 200.wolfgang-ch commentedon Aug 20, 2024
This sounds interresting, I'll try it by using the
ImageDataProvidersolutionlaeubi commentedon Aug 20, 2024
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?!
[SWTvsAWT] Testing font scaling
wolfgang-ch commentedon Aug 20, 2024
@HeikoKlare 🏆 🥇 🥈 🥉
It works 😎
In this test, SWT is not yet using an
ImageDataProviderbut AWTThis is the modified code with an
ImageDataProviderSWTvsAWT_FontsAndImages.zipHeikoKlare commentedon Aug 20, 2024
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.