Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to render vertical texts #111

Closed
hajimehoshi opened this issue Nov 24, 2023 · 13 comments · Fixed by #124
Closed

How to render vertical texts #111

hajimehoshi opened this issue Nov 24, 2023 · 13 comments · Fixed by #124

Comments

@hajimehoshi
Copy link
Contributor

hajimehoshi commented Nov 24, 2023

Hi, I'm using this library for my game engine. This is awesome!

I have several questions about vertical texts:

  • How can I render a vertical Mongolican text? Usually Mongolian font files include glyphs for horizontal texts, so I think I have to rotate glyphs by myself based on a Unicode table for Vertical_Orientation. Is that correct?

(The left-bottom texts are in Mongolian. This rendering result is without Vertical_Orientation)
image

  • Even if I rotate glyphs, the Y advances seems too big and glyphs were rendered in incorrect positions. How can I get correct Y advances?

(This rendering result is with Vertical_Orientation)
image

  • When I use the Unicode table, "… (U+2026)"'s Vertical_Orientation value is also R, so this glyph seems to have to be rotated. However, Japanese fonts usually have a vertical glyph for this code point, so I don't want to rotate this glyph. You can see the glyph "…" was incorrectly rendered in the above Japanese text (the right-bottom text). Is there a way to know whether a glyph ID is for vertical texts or not?

Thanks!

@hajimehoshi
Copy link
Contributor Author

Rendering a Mongolian text in an horizontal direction seems fine by the way

image

@benoitkugler
Copy link
Contributor

Hi !
Thank you for your interest in this package.

I think rendering vertical text has not been tested thoroughly yet, but it is definitively an area we would like to improve !
I'll dig deeper in your issue; thanks for the detailed problem statement.

PS : As an aside, I've skimmed your project issue tracker, and found hajimehoshi/ebiten#788. Perhaps you will be interested by the fontscan (and its FontMap type) that you could use to access system fonts.

@hajimehoshi
Copy link
Contributor Author

Thank you for your quick response!

Perhaps you will be interested by the fontscan (and its FontMap type) that you could use to access system fonts.

Thanks, I'll take a look!

@benoitkugler
Copy link
Contributor

From a first research, I've understood that :

  • you should not have to rotate the glyphs yourself, the shaping step should do it for you. We will add a list of scripts requiring rotation (including Mongolian) and performs rotation conditionally on the script.
  • fonts use a feature ('vert') to replace the glyphs by their vertical versions. The good news is that Harfbuzz already activates this feature when using a vertical direction, so that you should not have to take care of it yourself.

So basically, go-text/harfbuzz should do all the work.

Could you paste the Mongolian and Japanese strings you have used so that I can reproduce and hopefully fix the issue ?

@hajimehoshi
Copy link
Contributor Author

Thank you for taking a look!

Could you paste the Mongolian and Japanese strings you have used so that I can reproduce and hopefully fix the issue ?

Here you are:

"ᠬᠦᠮᠦᠨ ᠪᠦᠷ ᠲᠥᠷᠥᠵᠦ ᠮᠡᠨᠳᠡᠯᠡᠬᠦ\nᠡᠷᠬᠡ ᠴᠢᠯᠥᠭᠡ ᠲᠡᠢ᠂ ᠠᠳᠠᠯᠢᠬᠠᠨ"
"あのイーハトーヴォの\nすきとおった風、\n夏でも底に冷たさを\nもつ青いそら…"

@andydotxyz
Copy link
Contributor

I will have to learn about this to update the go-text/render output as well, as I think it is just horizontal...

@benoitkugler
Copy link
Contributor

benoitkugler commented Nov 26, 2023

Here is what I found after more digging.

There is an ambiguity when we say "vertical text" : it may be that we want to render glyphs upright, on top of each other; or that we want to rotate glyphs (that is, basically, to render them as in horizontal mode, and then rotate the whole line).
Following the CSS spec, we can use the wording upright for the former, sideways for the later.

Depending on the context and script, both ways may be "acceptable", or sometimes one way is more natural than the other. Here are whats Firefox renders :

Screenshot 2023-11-26 at 16-56-40 Screenshot

HTML source
<style>
  .vert {
    writing-mode: vertical-lr;
    text-orientation: mixed;
  }

  .sideways {
    writing-mode: vertical-lr;
    text-orientation: sideways;
  }

  .upright {
    writing-mode: vertical-lr;
    text-orientation: upright;
  }
</style>
<body>
  <div
    style="
      display: grid;
      grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
      row-gap: 20px;
    "
  >
    <div><b>Script</b></div>
    <div><b>Horizontal</b></div>
    <div><b>Upright</b></div>
    <div><b>Sideways</b></div>
    <div><b>Browser default</b></div>

    <div>Latin</div>
    <div>ABCD</div>
    <div class="upright">ABCD</div>
    <div class="sideways">ABCD</div>
    <div class="vert">ABCD</div>

    <div>Mongolian</div>
    <div>ᠬᠦᠮᠦᠨ ᠪᠦᠷ</div>
    <div class="upright">ᠬᠦᠮᠦᠨ ᠪᠦᠷ</div>
    <div class="sideways">ᠬᠦᠮᠦᠨ ᠪᠦᠷ</div>
    <div class="vert">ᠬᠦᠮᠦᠨ ᠪᠦᠷ</div>

    <div>Japanese</div>
    <div>もつ青いそら…</div>
    <div class="upright">もつ青いそら…</div>
    <div class="sideways">もつ青いそら…</div>
    <div class="vert">もつ青いそら…</div>
  </div>
</body>

Now, notice that Harfbuzz has no precise notion of this difference : using a direction of TopToBottom will generate the upright form, never the sideways one.

So my first conclusion is that we should support the two ways. The upright mode is already covered by Harfbuzz. A sketch of implementation for the sideways one would be to 1) shape horizontally 2) rotate the whole output. This means that we will have to add a flag to glyphs indicating that their content should be rotated when rendering (so that go-text/render and other consumer may properly rotate the glyph)

A second step would be to select the "best way" to render vertical text, given an input text, that is, matching what web browsers do by default. This is where the famous Unicode Vertical_Orientation table is useful: if I'm not mistaken, it gives us a way to segment the text according to the U or Tu (=upright) / R or Tr (=sideways) alternative.

Side note : the upright rendering of the Mongolian text using Harfbuzz seems indeed "broken". I've checked the NotoSansMongolian font and found that the vertical advance values are constant. It means that the font is not designed for this rendering. By the way, you can see that Firefox doesn't even support it !

@hajimehoshi
Copy link
Contributor Author

hajimehoshi commented Nov 26, 2023

Thank you for taking a look!

"…" (U+2026) in the all above figure seems not rendered expectedly at least to me. Probably the browser behaves correctly as they prefer English fonts for this glyph for "…". If a Japanese font is used, it should have its own special vertical glyph for "…", as I shown in the first comment. "…" is grouped as "R" in the Unicode spec, but this actually should NOT be rotated when a Japanese glyph is used. If CSS font-family prefers a Japanese font over others, the result should be changed. This is complicated!

Yeah, I feel like the second step seems the best. As I said above, some glyphs should not be rotated against the Unicode spec, so

func shouldRotateGlyph() bool {
    // This is for "…" for example
    if theGlyphIsAlreadySpecialVerticalGlyph {
        return false
    }
    if theUnicodePropertyIsROrTr {
        return true
    }
    return false
}

would be what we need. What do you think?

Side note : the upright rendering of the Mongolian text using Harfbuzz seems indeed "broken". I've checked the NotoSansMongolian font and found that the vertical advance values are constant. It means that the font is not designed for this rendering. By the way, you can see that Firefox doesn't even support it !

When rotating glyphs, shouldn't horizontal advances be used for them instead of vertical ones? It makes sense if the Monglian font doesn't have vertical advance info as the font has only horizontal glyphs.

@benoitkugler
Copy link
Contributor

When rotating glyphs, shouldn't horizontal advances be used for them instead of vertical ones? It makes sense if the Monglian font doesn't have vertical advance info as the font has only horizontal glyphs.

That will be the case using the "sideways" mode (like browsers do), but the "upright" mode will stay broken, and I think the real solution would be for the font author to include proper vertical metrics ('vtmx' table).

@benoitkugler
Copy link
Contributor

"…" (U+2026) in the all above figure seems not rendered expectedly at least to me. Probably the browser behaves correctly as they prefer English fonts for this glyph for "…". If a Japanese font is used, it should have its own special vertical glyph for "…", as I shown in the first comment. "…" is grouped as "R" in the Unicode spec, but this actually should NOT be rotated when a Japanese glyph is used. If CSS font-family prefers a Japanese font over others, the result should be changed. This is complicated!

Yeah, I feel like the second step seems the best. As I said above, some glyphs should not be rotated against the Unicode spec, so

func shouldRotateGlyph() bool {
    // This is for "…" for example
    if theGlyphIsAlreadySpecialVerticalGlyph {
        return false
    }
    if theUnicodePropertyIsROrTr {
        return true
    }
    return false
}

would be what we need. What do you think?

Yes, you have nicely sum up the issue, and I overall agree with your pseudo-code solution. However, I don't think there is a proper way to detect theGlyphIsAlreadySpecialVerticalGlyph (without shaping early on), so we will instead use the context. In your example, we would associate the ... with the Japanese script, and thus use "upright" mode. This means we can't rely only on the Vertical_Orientation table, and we will need some other heuristics. Maybe, since there is not too many scripts using vertical layout, can we hard code some behaviors per script.

@hajimehoshi
Copy link
Contributor Author

hajimehoshi commented Nov 27, 2023

I tested the HTML with a Japanese font (Noto Sans CJK JP), and the default rendering result worked well on my Chrome. On Firefox, the results seems a little suspicious. In Chrome, the browser default seems expected to me, since alphabets are rotated, and a special vertical glyph is used for "…". I don't know how browser works, but as you explained, emulating this behavior is pretty difficult, right?

Chrome:
image

Firefox:
image

HTML
<html lang="ja">
<style>
  body {
    font-family: "Noto Sans CJK JP";
  }

  .vert {
    writing-mode: vertical-lr;
    text-orientation: mixed;
  }

  .sideways {
    writing-mode: vertical-lr;
    text-orientation: sideways;
  }

  .upright {
    writing-mode: vertical-lr;
    text-orientation: upright;
  }
</style>
<body>
  <div
    style="
      display: grid;
      grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
      row-gap: 20px;
    "
  >
    <div><b>Script</b></div>
    <div><b>Horizontal</b></div>
    <div><b>Upright</b></div>
    <div><b>Sideways</b></div>
    <div><b>Browser default</b></div>

    <div>Latin</div>
    <div>ABCD</div>
    <div class="upright">ABCD</div>
    <div class="sideways">ABCD</div>
    <div class="vert">ABCD</div>

    <div>Mongolian</div>
    <div>ᠬᠦᠮᠦᠨ ᠪᠦᠷ</div>
    <div class="upright">ᠬᠦᠮᠦᠨ ᠪᠦᠷ</div>
    <div class="sideways">ᠬᠦᠮᠦᠨ ᠪᠦᠷ</div>
    <div class="vert">ᠬᠦᠮᠦᠨ ᠪᠦᠷ</div>

    <div>Japanese</div>
    <div>ABCDもつ青いそら…</div>
    <div class="upright">ABCDもつ青いそら…</div>
    <div class="sideways">ABCD青いそら…</div>
    <div class="vert">ABCD青いそら…</div>
  </div>
</body>
</html>

That will be the case using the "sideways" mode (like browsers do), but the "upright" mode will stay broken, and I think the real solution would be for the font author to include proper vertical metrics ('vtmx' table).

I see. I don't think I need 'upright' mode for Mongolian scripts in any environments, so I am OK.

Yes, you have nicely sum up the issue, and I overall agree with your pseudo-code solution. However, I don't think there is a proper way to detect theGlyphIsAlreadySpecialVerticalGlyph (without shaping early on), so we will instead use the context. In your example, we would associate the ... with the Japanese script, and thus use "upright" mode. This means we can't rely only on the Vertical_Orientation table, and we will need some other heuristics. Maybe, since there is not too many scripts using vertical layout, can we hard code some behaviors per script.

Interesting. From the above result, I thought Chrome has a special magic that does theGlyphIsAlreadySpecialVerticalGlyph, but I am not sure. If we don't follow the Chrome way, maybe is this the compromise?

func shouldRotateGlyph() bool {
    if theFontUsesVertFeature {
        return false
    }
    if theUnicodePropertyIsROrTr {
        return true
    }
    return false
}

In this case, "ABCD" in the Japanese text in the above example's "browser default" would not be rotated. Well, I think that's fine, though that's not the best...

@khaledhosny
Copy link

khaledhosny commented Nov 30, 2023

You can check of the vert feature applies to the glyph of a Tr character and not rotate it, here is how it is done in LibreOffice https://git.libreoffice.org/core/+/refs/heads/master/vcl/source/gdi/CommonSalLayout.cxx#178.

@benoitkugler
Copy link
Contributor

You can check of the vert feature applies to the glyph of a Tr character and not rotate it, here is how it is done in LibreOffice https://git.libreoffice.org/core/+/refs/heads/master/vcl/source/gdi/CommonSalLayout.cxx#178.

Thank you for the nice pointer !
At a first glance, I'm a bit reluctant to go down this road, because I'm afraid about the possible substitutions, and also because Harfbuzz may synthesize its own replacement if the font has no 'vert' table. But perhaps it does not matter in practice ?

(Also, generally speaking, I would like to avoid interfering with Harfbuzz and avoid guessing what it will do or not... )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants