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

Better support for vertical text #124

Merged
merged 23 commits into from
Dec 21, 2023
Merged

Better support for vertical text #124

merged 23 commits into from
Dec 21, 2023

Conversation

benoitkugler
Copy link
Contributor

@benoitkugler benoitkugler commented Dec 8, 2023

This PR aims at fixing #111, by supporting "sideways" glyph orientation for vertical text.

It is roughly divided in two parts :

  1. Adding a "sideways" flag in Direction, and implementing "sideways" mode in Shape. A convenience method is provided : GlyphOutline.Sideways to be used by renderers to rotate glyph.
  2. Resolving the preferred orientation base on Unicode rules : this is done in Segmenter.Split

The only change required for users/renderers is thus to check against the Ouput.Direction.IsSideways() boolean, to properly rotate glyphs when rendering.

Happy to read your thoughts !

(@hajimehoshi Maybe you would like to test this change ? The code in ExampleShaper_Shape and drawVGlyphs may be useful to do so)

@benoitkugler benoitkugler linked an issue Dec 8, 2023 that may be closed by this pull request
@hajimehoshi
Copy link
Contributor

Thanks! I tested Monglian and Japanese texts. Fonts are Noto Sans Monglian and M+ 1 Regular.

SetSideways(false):
image

SetSideways(true):
image

  • Mongolian texts are not rendered correctly in both cases.
  • With SetSideways(true), horizontal glyphs in Japanese like "ー" and "…" are now used. This is not correct as a vertical Japanese text. I am not sure this is intended or not. Also, alphabets are not rotated in both cases.

This is just a quick report. I'll try to make a minimal test case if needed.

@benoitkugler
Copy link
Contributor Author

@hajimehoshi Thank you for the quick tests !

Could you make sure you use Segmenter.Split to properly assign orientation (based on the script) ?
If you manually specify SetSideways, you don't benefit from auto-selection.

Also, in sideways mode, it seems you do not use GlyphOutlines.Sideways when drawing ?

@hajimehoshi
Copy link
Contributor

I'll test those tomorrow. Could you tell me what APIs were newly added?

@benoitkugler
Copy link
Contributor Author

I'll test those tomorrow. Could you tell me what APIs were newly added?

So I've updated the PR description with links towards the new API : Segmenter.Split, Direction.IsSideways, GlyphOutline.Sideways. Hope it is clearer now :)

@hajimehoshi
Copy link
Contributor

hajimehoshi commented Dec 9, 2023

// If [yOffset] is zero, the resulting glyph will typically be displayed
// under the baseline. To position the glyph over the baseline, pass
// the YAdvance as [yOffset] (a positive value to lift the glyph up).

yOffset is float32 while YAdvance is fixed.Int26_6. How should I convert this appropriately?

@hajimehoshi
Copy link
Contributor

hajimehoshi commented Dec 9, 2023

I utilized Segment.Split and Glyph.Sideways without Direction.SetSideways, and the result seems much better.

image

The Monglians seems fine! I found that mixing Japanese characters and English characters doesn't work well. In the above figure "あHello, World.あ"'s Y offsets does match. The last あ overlaps the adjacent English sentence. Should I adjust this with HAscent for each outputs?

Thanks!

EDIT: You can test this with

go run github.com/hajimehoshi/ebiten/v2/examples/texti18n@874d48f945b0d0480d2975e2a7ad69120fd949b4

EDIT2: Arabic results was also changed unexpectedly. I'll take a look...

EDIT3: Fixed the Arabic issue at 363a4f853717300361c6c17ac51dc4e513b87aef by reversing the split inputs for RTL texts.

@benoitkugler
Copy link
Contributor Author

benoitkugler commented Dec 9, 2023

// If [yOffset] is zero, the resulting glyph will typically be displayed
// under the baseline. To position the glyph over the baseline, pass
// the YAdvance as [yOffset] (a positive value to lift the glyph up).

yOffset is float32 while YAdvance is fixed.Int26_6. How should I convert this appropriately?

Since YAdvance has been scaled by Input.Size (output = font_units * Size / face.Upem), and Sideways expects a value in font units, you should pass YAdvance / (Size * face.Upem) to convert back to fonts units. More precisely, you could use
float32((-YAdvance / Size).Round()) / float32(face.Upem())
which is a bit inaccurate due to Rounding. A better way would be to use a fixed.Int26_6 to float32 conversion :

func toFloat32(a fixed.Int26_6) float32 {
    return float32(a) / 64
}

@hajimehoshi
Copy link
Contributor

hajimehoshi commented Dec 9, 2023

A better way would be to use a fixed.Int26_6 to float32 conversion :

OK, I actually did this:

func fixed26_6ToFloat32(x fixed.Int26_6) float32 { 
    return float32(x>>6) + float32(x&((1<<6)-1))/float32(1<<6)
}

EDIT: Your /64 is much better. This worked correctly even with negative numbers.

@hajimehoshi
Copy link
Contributor

Since YAdvance has been scaled by Input.Size (output = font_units * Size / face.Upem), and Sideways expect a value in font units,

So, the simple conversion like YAdvance / 64 doesn't sound correct in terms of unit conversion, but is this actually better...?

@hajimehoshi
Copy link
Contributor

I think I was misunderstanding. Your intention is like this?

fixed26_6ToFloat32(-YAdvance) / Size / float32(face.Upem())

@hajimehoshi
Copy link
Contributor

hajimehoshi commented Dec 9, 2023

I'm sorry for spamming, but

Since YAdvance has been scaled by Input.Size (output = font_units * Size / face.Upem), and Sideways expect a value in font units, you should pass YAdvance / (Size * face.Upem) to convert back to fonts units.

YAdvance is a value in the scaled units, while Sideways expects a value in the font units. So

-YAdvance / Size * face.Upem

seems correct, but am I missing something? With multiplying Upem, the rendering result is broken, but I still don't understand why -YAdvance / Size / face.Upem works (though there is still the overlapping issue). The below result is with multiplying Upem.

image

@benoitkugler
Copy link
Contributor Author

benoitkugler commented Dec 9, 2023

YAdvance is a value in the scaled units, while Sideways expects a value in the font units. So

-YAdvance / Size * face.Upem

seems correct, but am I missing something?

I agree with this formula. Hum... I'll try to render the glyphs from your example and investigate tomorrow.

@benoitkugler
Copy link
Contributor Author

benoitkugler commented Dec 11, 2023

@hajimehoshi I've rendered the Mongolian and (a sample of) Japanese texts, and have noticed none of the visual issues you have found.

See the outputs I get :

shape_verts

Could you try and find where your pipeline differs from this one ?

@hajimehoshi
Copy link
Contributor

hajimehoshi commented Dec 11, 2023

I'll check this later, but

glyphData.Sideways(float32(-g.YAdvance.Round()) / sizeFactor)

Isn't the value almost 0?

EDIT: NVM, I missed that sizeFactor := float32(out.Size.Round()) / float32(out.Face.Upem())

shaping/render_test.go Outdated Show resolved Hide resolved
@benoitkugler
Copy link
Contributor Author

There is one issue remaining when laying out mixed "sideways" and "upright" : the baseline are not nicely aligned.

So, I've explored a little bit the Opentype BASE table and thought about baseline. I think there are two different concepts :

  • the general support of mixed baselines in a single line (for both horizontal and vertical directions), which is tricky and requires font knowledge
  • the adjustment of 'sideways' and 'upright' text

Implementing the first one is probably desirable, but postponed to another PR.

Implementing the second one is visually more important, and may be done quite simply, as in 582c627. Now, calling Line.AdjustBaseline just before rendering will make 'sideways' text follow the same convention than 'upright', that is roughly placing the baseline in the middle of each glyph.

We may want to fine tune the implementation of Line.AdjustBaseline latter, but it seems to be a good enough start.

@hajimehoshi Could you confirm that using Line.AdjustBaseline visually improves the rendering ?

Copy link
Member

@whereswaldon whereswaldon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benoitkugler Thanks, as always, for the excellent work here. It's exciting to see us grow better vertical text support. The changes make sense, and seem to be implemented at the right level of abstraction. It's unclear to me whether this needs a second review from @andydotxyz. I don't think the public API is breaking, but it is changing (the new exported fields/methods on Direction).

@hajimehoshi Thank you so much for the excellent bug reports and features that drove this change. Thanks to your efforts, Ebitengine, Gio, Fyne, and (hopefully) many more projects will now have more correct text shaping.

opentype/api/font.go Outdated Show resolved Hide resolved
@hajimehoshi
Copy link
Contributor

@benoitkugler
image

Now everything looks fine. Thank you very much!

What I did is to add these lines to expect the side effect of AdjustBaseline. I hope this is correct.

line := shaping.Line{out}
line.AdjustBaseline()

// After that, `out` is directly used and `line` is no longer used.

@hajimehoshi
Copy link
Contributor

image

I found an interesting thing: the baseline varies for an English sentence. In the above figure, the existence of g matters. Is this intended?

@hajimehoshi
Copy link
Contributor

hajimehoshi commented Dec 14, 2023

I found an interesting thing: the baseline varies for an English sentence. In the above figure, the existence of g matters. Is this intended?

image

Unfortunately this is an issue as the baseline are not alinged with English texts. In the above figure, I rendered "Hello\nEbitengine\nWorld" with the same line spaces, but the baseline are not alinged as expected.

@whereswaldon
Copy link
Member

Perhaps we should align the text not by the ascent/descent of the particular glyphs within it, but by the face's advertised ascent/descent? That should be independent of specific glyph contents, and it should look decent.

@whereswaldon
Copy link
Member

I found an interesting thing: the baseline varies for an English sentence. In the above figure, the existence of g matters. Is this intended?

image

Unfortunately this is an issue as the baseline are not alinged with English texts. In the above figure, I rendered "Hello\nEbitengine\nWorld" with the same line spaces, but the baseline are not alinged as expected.

This particular issue is more of a line-spacing/line-height problem. The typesetting repo doesn't currently implement this part of the text display process internally. It's up to applications to decide where to start the baseline of each line of text. See the discussion here for some examples of the complexities inherent in this space: gioui/gio#123

@hajimehoshi
Copy link
Contributor

Perhaps we should align the text not by the ascent/descent of the particular glyphs within it, but by the face's advertised ascent/descent? That should be independent of specific glyph contents, and it should look decent.

I agree with you. A face's metrics should be used rather than each glyph's metrics.

It's up to applications to decide where to start the baseline of each line of text.

OK, but if an application moves the dot position for each line with a constant interval, each line should be aligned, right?

@benoitkugler
Copy link
Contributor Author

benoitkugler commented Dec 14, 2023

OK, but if an application moves the dot position for each line with a constant interval, each line should be aligned, right?

Well, in theory, the shaper (harfbuzz) could decide to move glyphs up (for instance if the font provides a positioning table), so I'm not sure this promise holds.
Also, consider lines with different text sizes, or even with different fonts.

@benoitkugler
Copy link
Contributor Author

This is what I get using the font line extents (left) vs glyph extents (right).
It seems to me the left version still feels a bit akward..

shape_vert_mixed

@hajimehoshi
Copy link
Contributor

Also, consider lines with different text sizes, or even with different fonts.

Yeah, but in my example, the same size and face were used. I'm OK even if lines are not aligned when multiple faces or sizes are used.

@hajimehoshi
Copy link
Contributor

It seems to me the left version still feels a bit akward..

In Chrome browsers, the baseline seems the same as the right. 'g' is put so that the glyph is extended to the left side. I think this makes sense.

image

@whereswaldon
Copy link
Member

OK, but if an application moves the dot position for each line with a constant interval, each line should be aligned, right?

Well, in theory, the shaper (harfbuzz) could decide to move glyphs up (for instance if the font provides a positioning table), so I'm not sure this promise holds. Also, consider lines with different text sizes, or even with different fonts.

I think conceptually it's correct to shift the gap between dots by a fixed value. It's true that situationally the glyphs may look misaligned if the shaper did something fancy, but that would be a feature, not a bug.

@whereswaldon
Copy link
Member

It seems to me the left version still feels a bit akward..

In Chrome browsers, the baseline seems the same as the right. 'g' is put so that the glyph is extended to the left side. I think this makes sense.

image

To achieve this, would we shift the glyphs by half the distance from the face's baseline to its ascent? I'd expect that to visually center the above-the-baseline portion of the text. I don't know for sure if this would be correct for all scripts, but it certainly would be for Latin and CJK.

@benoitkugler
Copy link
Contributor Author

benoitkugler commented Dec 14, 2023

To achieve this, would we shift the glyphs by half the distance from the face's baseline to its ascent? I'd expect that to visually center the above-the-baseline portion of the text. I don't know for sure if this would be correct for all scripts, but it certainly would be for Latin and CJK.

Hum, I've just tried that and it seems to move the glyphs a bit too much down. I think the proper way to go would be to align the baseline : ideographic and roman.
So we would detect what baseline to use based on the script and align them.

However, it seems to me this does not solve the "only english" example. Maybe, in this case we could shift by the font advance as @whereswaldon suggested earlier.

@benoitkugler
Copy link
Contributor Author

Well, what do you think of handling the baseline issue in a separate PR ? This one is already large and it seems we need more discussion on how to do it anyway.

@whereswaldon
Copy link
Member

Well, what do you think of handling the baseline issue in a separate PR ? This one is already large and it seems we need more discussion on how to do it anyway.

Fine by me

@hajimehoshi
Copy link
Contributor

Well, what do you think of handling the baseline issue in a separate PR ? This one is already large and it seems we need more discussion on how to do it anyway.

+1. I'm fine with this issue addressed in another PR. I really appreciate your team's hard work!

@hajimehoshi
Copy link
Contributor

@andydotxyz friendly ping

Copy link
Contributor

@andydotxyz andydotxyz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, and my apologies for the delay.
I had hoped to test this by using it in our render package but I am not managing to get the time for such coding sorry.

A couple of comments inline, the only one that is a change request being the typo in docs.
I do wonder though, and may welcome other comments, if adding the rasterising render code here might start duplicating the render repo and whether it would be worth testing this through usage of the rasteriser we maintain as a public API?

di/direction.go Outdated
}

// SwitchAwis switch from horizontal to vertical (and vice versa), preserving
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SwitchAxis probably ;) The linter should have caught this, perhaps our CI needs a few improvements.
Possibly "switches" not "switch" as well if this line is being updated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated, thank you.

// Note that this method only update cross-axis metrics,
// so that the advance is preserved. As such, it is valid
// to call this method after line wrapping, for instance.
func (l Line) AdjustBaseline() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the doc implies this should be called AlignBaselines instead.
Just a thought, not a change request.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added an "s" to imply each run may be shifted.

@@ -0,0 +1,195 @@
package shaping
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to look at whether this is needed in the typesetting repo as there is more render work in the render repository, it would be a shame to duplicate for test purposes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree this file seems a bit out of place, but, in the meantime, I found this quick and dirty visualisation tool very handy when developing.

As you said, a better solution would be to use render. Perhaps we can keep this file for now, and once render supports vertical text, move the examples there and remove shaping/render_test.go ?

Copy link
Contributor

@andydotxyz andydotxyz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

@benoitkugler benoitkugler merged commit 48cc05a into main Dec 21, 2023
20 checks passed
@benoitkugler benoitkugler deleted the vertical-scripts branch December 21, 2023 12:45
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 this pull request may close these issues.

How to render vertical texts
4 participants