toptensoftware / RichTextKit

Rich text rendering for SkiaSharp
Other
366 stars 73 forks source link

Support for overriding vertical font metrics #67

Open handerss-spotfire opened 1 year ago

handerss-spotfire commented 1 year ago

This PR adds the ability to provide custom vertical font metrics when layouting text.

The background here is that Skia report different font metrics depending on platform. Windows uses usWinAscent, usWinDescent, and this lovely formula for calculating leading: MAX(0, (hhea.ascender - hhea.descender + hhea.lineGap) - (usWinAscent + usWinDescent)). If I remember correctly Linux uses the sTypo* metrics.

For cross-platform .NET applications that want to have similar rendering on both Windows and Linux (in particular vertical positioning) there needs to be a way to override the metrics reported by Skia.

I realize this may be out-of-scope for this library, feel free to close the PR in that case.

dbriard commented 8 months ago

Hi @handerss-spotfire, your PR looks very interresting as after reading your explanation, I noticed that the vertical positioning of my text is not the same on Windows and WebAssembly version of my app. Just one question: you recommand to use "Typo" metrics for cross-plarform, can I get that from Skia? or do I have to parse the font manually to extract those values? Thank you!

handerss-spotfire commented 8 months ago

You can get the relevant data tables using SKTypeface.TryGetTableData but then you have to parse them yourself. This is roughly we do it to get "Windows" style rendering on all platforms:

private static uint GetIntTag(string v)
{
    return
        (uint)v[0] << 24 |
        (uint)v[1] << 16 |
        (uint)v[2] << 08 |
        (uint)v[3] << 00;
}

public static bool TryReadFontMetrics(SKTypeface typeface, out (int Ascent, int Descent, int LineGap) fontMetrics)
{
    fontMetrics = (0, 0, 0);
    (int Ascent, int Descent, int? LineGap) os2Metrics = (0, 0, null);

    if (typeface.TryGetTableData(GetIntTag("OS/2"), out var os2Table))
    {
        os2Metrics = ReadOS2Table(os2Table);
    }

    if (os2Metrics.LineGap == null && typeface.TryGetTableData(GetIntTag("hhea"), out var hheaTable))
    {
        var hhea = ReadHheaTable(hheaTable);

        // See: https://learn.microsoft.com/en-us/typography/opentype/spec/recom#baseline-to-baseline-distances
        var lineGap = Math.Max(
            0,
            (hhea.Ascent - hhea.Descent + hhea.LineGap) - (os2Metrics.Ascent + os2Metrics.Descent));
        fontMetrics = (os2Metrics.Ascent, os2Metrics.Descent, lineGap);
    }

    return fontMetrics.Ascent > 0 && fontMetrics.Descent > 0;
}

Where ReadOS2Table and ReadHheaTable parse using a BinaryReader according the specs OS/2 hhea.

Note that when parsing the OS/2 table it's important to read metrics as specified by the font selection flags: https://learn.microsoft.com/en-us/typography/opentype/spec/os2#fsselection.

dbriard commented 8 months ago

Thanks a lot for the details, I found tables and the properties, just need to find how to convert them to float correctly but I will look at the spec links.