HumbleUI / Skija

Java bindings for Skia
Apache License 2.0
498 stars 34 forks source link

Doesn't seem to be any way to use LCD font rendering? #68

Closed Barney-x1 closed 2 months ago

Barney-x1 commented 3 months ago

NOTE: originally posted to the old (jetbrains) repository by accident: here. @tonsky replied with example which suggested that FontEdging achieves this. It does not! More below.

I couldn't find anything which looked like it would do this in the source.

I note that in SkiaSharp this is achieved via SKPaint.LcdRenderText property.

Have I missed something? Could this be added?

Is there any workaround/hack to access this?

It seems to me that getting font rendering to look exactly like it does in Chrome and other Skia based apps would be one of the biggest reasons for trying/using skija, so this seems pretty major to me.

Response in the old forum linked to an example which uses FontEdging.SUBPIXEL_ANTI_ALIAS presumably intended to force LCD rendering. (I couldn't actually figure out how to run it, but my example using FontEdging is below:)


import io.github.humbleui.skija.*;

import javax.imageio.*;
import javax.swing.*;
import java.io.*;

public class TextAliasing {
    public static void main(String[] ignored) throws Exception {
        int width = 130;
        int height = 30;
        Surface surface = Surface.makeRasterN32Premul(width, height);
        Canvas canvas = surface.getCanvas();
        canvas.clear(0xFFFFFFFF);

        Typeface typeface = Typeface.makeFromName("Arial", FontStyle.NORMAL);
        Font font = new Font(typeface, 24);
        font.setEdging(FontEdging.SUBPIXEL_ANTI_ALIAS);
        Paint paint = new Paint();
        paint.setColor(0xFF000000);
        canvas.drawString("Hello World", 3, height * 0.8f, font, paint);

        byte[] pngBytes = surface.makeImageSnapshot().encodeToData().getBytes();
        java.awt.Image image = ImageIO.read(new ByteArrayInputStream(pngBytes));
        image = image.getScaledInstance(width * 4, height * 4, java.awt.Image.SCALE_REPLICATE);
        JFrame frame=new JFrame();
        frame.getContentPane().add(new JLabel(new ImageIcon(image)));
        //frame.getContentPane().add(component);
        frame.pack();
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
        //java.nio.file.Files.write(java.nio.file.Path.of("output.png"), pngBytes);

    }
}

The results are as follows, using ANTI_ALIAS top, SUBPIXEL_ANTI_ALIAS bottom:

image

As you can see, so-called SUBPIXEL_ANTI_ALIAS does not perform subpixel rendering. Every pixel in the image is grey. I'm not sure what it is actually doing, but it isn't subpixel rendering which uses distinct values for red, green and blue.

The SkiaSharp LcdRenderText property can be seen in action about one third way down this page and it enables real LCD subpixel rendering (there is some blown-up text demonstrating this).

Many thanks.

tonsky commented 2 months ago

Try using Surface::makeRaster version that accepts SurfaceProps. In these props, set PixelGeometry to RGB_H. Then see if it helps

Barney-x1 commented 2 months ago

Thanks, but it didn't help.

Also tried the other geometries and several tweaks to the other parameters, no difference observed.

Slightly modified code and result below.

package adhoc;

import io.github.humbleui.skija.*;

import javax.imageio.*;
import javax.swing.*;
import java.io.*;

public class TextAliasing {
    public static void main(String[] ignored) throws Exception {
        int width = 350;
        int height = 100;
        SurfaceProps props = new SurfaceProps(PixelGeometry.RGB_H);
        ImageInfo info = new ImageInfo(new ColorInfo(ColorType.ARGB_4444, ColorAlphaType.OPAQUE, null), width, height);
        Surface surface = Surface.makeRaster(info, 0, props);
        Canvas canvas = surface.getCanvas();
        canvas.clear(0xFFFFFFFF);

        Typeface typeface = Typeface.makeFromName("Arial", FontStyle.NORMAL);
        Font font = new Font(typeface, 18);

        Paint paint = new Paint();
        paint.setColor(0xFF000000);

        float y = font.getSize() * 1f;
        font.setEdging(FontEdging.ANTI_ALIAS);
        font.setSubpixel(false);
        canvas.drawString("SubPixel = " + font.isSubpixel() + "; Edging = " + font.getEdging(), 3, y, font, paint);
        y += font.getSize() * 1.2f;
        font.setEdging(FontEdging.ANTI_ALIAS);
        font.setSubpixel(true);
        canvas.drawString("SubPixel = " + font.isSubpixel() + "; Edging = " + font.getEdging(), 3, y, font, paint);
        y += font.getSize() * 1.2f;
        font.setEdging(FontEdging.SUBPIXEL_ANTI_ALIAS);
        font.setSubpixel(false);
        canvas.drawString("SubPixel = " + font.isSubpixel() + "; Edging = " + font.getEdging(), 3, y, font, paint);
        y += font.getSize() * 1.2f;
        font.setEdging(FontEdging.SUBPIXEL_ANTI_ALIAS);
        font.setSubpixel(true);
        canvas.drawString("SubPixel = " + font.isSubpixel() + "; Edging = " + font.getEdging(), 3, y, font, paint);

        byte[] pngBytes = surface.makeImageSnapshot().encodeToData().getBytes();
        java.awt.Image image = ImageIO.read(new ByteArrayInputStream(pngBytes));
        image = image.getScaledInstance(width * 6, height * 6, java.awt.Image.SCALE_REPLICATE);
        JFrame frame=new JFrame();
        frame.getContentPane().add(new JLabel(new ImageIcon(image)));
        //frame.getContentPane().add(component);
        frame.pack();
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
        //java.nio.file.Files.write(java.nio.file.Path.of("output.png"), pngBytes);

    }
}

image

dzaima commented 2 months ago

I can't get your example to work either, but inserting your font creation & text drawing in a project which creates a surface via Surface.wrapBackendRenderTarget from JWM I do see subpixel anti-aliasing, so the problem should be something with the surface creation/type.

tonsky commented 2 months ago

Try new new SurfaceProps(true, PixelGeometry.RGB_H);? Also maybe BGRA_8888? ARGB_4444 is non-standard, and I remember Skia acting weirdly on weird byte alignments

dzaima commented 2 months ago

Those two things combined work!

Barney-x1 commented 2 months ago

Yes, for me too!

Still not sure what the exact settings should be. (PixelGeometry.BGR_H also "works" but presumably I need somehow to query the user's monitor's pixel geometry? I imagine there's something somewhere in AWT to do this).

A quick question, I notice the kerning is not the same as Chrome's in all cases. For example "Wo" are closer together in Chrome. Decimal numbers e.g. 0.0 also closer together in Chrome. I had imagined kerning rules were somehow just a property of the font, but is there some external "kerning engine" in play? What are my chances of matching Chrome better?

(This probably isn't a "bug" as such as I suppose Skija doesn't promise to exactly emulate e.g. Chrome. So I'm not raising it as a an Issue at this point!)

dzaima commented 2 months ago

Did you try the possible font.setHinting/font.setAutoHintingForced values? But there are probably a bunch of things to consider (subpixel coordinates? fractional font sizes? weight? idk); at the end of the day it'll be HarfBuzz shaping the text, so it's all into the configuration (assuming the necessary knobs even exist).

tonsky commented 2 months ago

presumably I need somehow to query the user's monitor's pixel geometry?

Yes, that’s the idea. ClearType only works when displayed 1:1 on LCD monitor with exact pixel order.

I imagine there's something somewhere in AWT to do this

You should try for sure, but from my experience both AWT and JavaFX are consistently getting subpixel aliasing wrong, so I would not have high hopes for that. Better to go to WinAPI directly

What are my chances of matching Chrome better?

I’m pretty sure it should be doable. I would even aim for Windows native font rendering, as it is what you really need to match.

tonsky commented 2 months ago

One bit relevant to the font rendering is: you should always render font at 1:1 scale. If you scale your canvas, it will look wrong

From here https://issues.skia.org/issues/40042307:

Since the default on SkFont is hinted metrics, drawing through a non-identity matrix will lead to less than desirable results. If you want to be able to freely scale and have the spacing not look bad, you'll need to SkFont::setLinearMetrics(true) and/or SkFont::setHinting(SkFontHinting::kNone). Generally if you shape with any hinting (the default) you will not want to draw that with anything other than an identity matrix.