text/v2: bug fix: correct rendering vertical texts in Mongolian

Closes #2849
Updates go-text/typesetting#111
This commit is contained in:
Hajime Hoshi 2023-12-09 17:30:08 +09:00
parent ef1fea890f
commit 6878bd79fc
8 changed files with 143 additions and 66 deletions

View File

@ -16,6 +16,12 @@ Open Font License 1.1
https://fonts.google.com/noto/specimen/Noto+Sans+Myanmar/about
# `NotoSansMongolian-Regular.ttf`
Open Font License 1.1
https://fonts.google.com/noto/specimen/Noto+Sans+Mongolian/about
# `NotoSansThai-Regular.ttf`
Open Font License 1.1

Binary file not shown.

View File

@ -83,6 +83,19 @@ func init() {
myanmarFaceSource = s
}
//go:embed NotoSansMongolian-Regular.ttf
var mongolianTTF []byte
var mongolianFaceSource *text.GoTextFaceSource
func init() {
s, err := text.NewGoTextFaceSource(bytes.NewReader(mongolianTTF))
if err != nil {
log.Fatal(err)
}
mongolianFaceSource = s
}
var japaneseFaceSource *text.GoTextFaceSource
func init() {
@ -167,7 +180,26 @@ func (g *Game) Draw(screen *ebiten.Image) {
text.Draw(screen, thaiText, f, op)
}
{
const japaneseText = "あのイーハトーヴォの\nすきとおった風、\n夏でも底に冷たさを\nもつ青いそら…"
const mongolianText = "ᠬᠦᠮᠦᠨ ᠪᠦᠷ ᠲᠥᠷᠥᠵᠦ ᠮᠡᠨᠳᠡᠯᠡᠬᠦ\nᠡᠷᠬᠡ ᠴᠢᠯᠥᠭᠡ ᠲᠡᠢ᠂ ᠠᠳᠠᠯᠢᠬᠠᠨ"
f := &text.GoTextFace{
Source: mongolianFaceSource,
Direction: text.DirectionTopToBottomAndLeftToRight,
Size: 24,
Language: language.Mongolian,
// language.Mongolian.Script() returns "Cyrl" (Cyrillic), but we want Mongolian script here.
Script: language.MustParseScript("Mong"),
}
const lineSpacing = 48
x, y := 20, 280
w, h := text.Measure(mongolianText, f, lineSpacing)
vector.DrawFilledRect(screen, float32(x), float32(y), float32(w), float32(h), gray, false)
op := &text.DrawOptions{}
op.GeoM.Translate(float64(x), float64(y))
op.LineSpacingInPixels = lineSpacing
text.Draw(screen, mongolianText, f, op)
}
{
const japaneseText = "あのイーハトーヴォの\nすきとおった風、\n夏でも底に冷たさを\nもつ青いそら…\nあHello World.あ"
f := &text.GoTextFace{
Source: japaneseFaceSource,
Direction: text.DirectionTopToBottomAndRightToLeft,

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.18
require (
github.com/ebitengine/oto/v3 v3.2.0-alpha.2.0.20231021101548-b794c0292b2b
github.com/ebitengine/purego v0.6.0-alpha.2
github.com/go-text/typesetting v0.0.0-20231211160022-6295f3c76f4d
github.com/go-text/typesetting v0.0.0-20231221124458-48cc05a56658
github.com/hajimehoshi/bitmapfont/v3 v3.0.0
github.com/hajimehoshi/go-mp3 v0.3.4
github.com/jakecoffman/cp v1.2.1

6
go.sum
View File

@ -2,9 +2,9 @@ github.com/ebitengine/oto/v3 v3.2.0-alpha.2.0.20231021101548-b794c0292b2b h1:gi7
github.com/ebitengine/oto/v3 v3.2.0-alpha.2.0.20231021101548-b794c0292b2b/go.mod h1:JtMbxJHZBDXfS8BmVYwzWk9Z6r7jsjwsHzOuZrEkfs4=
github.com/ebitengine/purego v0.6.0-alpha.2 h1:lYSvMtNBEjNGAzqPC5WP7bHUOxkFU3L+JZMdxK7krkw=
github.com/ebitengine/purego v0.6.0-alpha.2/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/go-text/typesetting v0.0.0-20231211160022-6295f3c76f4d h1:AFVBrIZZMhmnB8NbkKayNVz714G048XqZGmNhXjvSag=
github.com/go-text/typesetting v0.0.0-20231211160022-6295f3c76f4d/go.mod h1:MrLApvxyzSW0MhQqLc484jkUWYX4wsEvEqDosB5Io80=
github.com/go-text/typesetting-utils v0.0.0-20231204162240-fa4dc564ba79 h1:3yBOzx29wog0i7TnUBMcp90EwIb+A5kqmr5vny1UOm8=
github.com/go-text/typesetting v0.0.0-20231221124458-48cc05a56658 h1:KeDKnC99J3l5qJK4zV13Au2UwPn4N20TnIlM0YvILj8=
github.com/go-text/typesetting v0.0.0-20231221124458-48cc05a56658/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
github.com/hajimehoshi/bitmapfont/v3 v3.0.0 h1:r2+6gYK38nfztS/et50gHAswb9hXgxXECYgE8Nczmi4=
github.com/hajimehoshi/bitmapfont/v3 v3.0.0/go.mod h1:+CxxG+uMmgU4mI2poq944i3uZ6UYFfAkj9V6WqmuvZA=
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=

View File

@ -278,11 +278,17 @@ func (g *GoTextFace) gScript() glanguage.Script {
// advance implements Face.
func (g *GoTextFace) advance(text string) float64 {
output, _ := g.Source.shape(text, g)
if g.direction().isHorizontal() {
return fixed26_6ToFloat64(output.Advance)
outputs, _ := g.Source.shape(text, g)
var a fixed.Int26_6
for _, output := range outputs {
a += output.Advance
}
return -fixed26_6ToFloat64(output.Advance)
if g.direction().isHorizontal() {
return fixed26_6ToFloat64(a)
}
return -fixed26_6ToFloat64(a)
}
// hasGlyph implements Face.

View File

@ -49,9 +49,9 @@ type glyph struct {
}
type goTextOutputCacheValue struct {
output shaping.Output
glyphs []glyph
atime int64
outputs []shaping.Output
glyphs []glyph
atime int64
}
type goTextGlyphImageCacheKey struct {
@ -164,7 +164,7 @@ func (g *GoTextFaceSource) UnsafeInternal() font.Face {
return g.f
}
func (g *GoTextFaceSource) shape(text string, face *GoTextFace) (shaping.Output, []glyph) {
func (g *GoTextFaceSource) shape(text string, face *GoTextFace) ([]shaping.Output, []glyph) {
g.copyCheck()
g.m.Lock()
@ -173,7 +173,7 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) (shaping.Output,
key := face.outputCacheKey(text)
if out, ok := g.outputCache[key]; ok {
out.atime = now()
return out.output, out.glyphs
return out.outputs, out.glyphs
}
g.f.SetVariations(face.variations)
@ -189,54 +189,81 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) (shaping.Output,
Script: face.gScript(),
Language: language.Language(face.Language.String()),
}
out := (&shaping.HarfbuzzShaper{}).Shape(input)
var inputs []shaping.Input
if face.Direction.isHorizontal() {
// shaping.Segmenter is not used for horizontal texts so far due to a bug (go-text/typesetting#127).
inputs = []shaping.Input{input}
} else {
var seg shaping.Segmenter
inputs = seg.Split(input, &singleFontmap{face: face.Source.f})
}
if face.Direction == DirectionRightToLeft {
// Reverse the input for RTL texts.
for i, j := 0, len(inputs)-1; i < j; i, j = i+1, j-1 {
inputs[i], inputs[j] = inputs[j], inputs[i]
}
}
outputs := make([]shaping.Output, len(inputs))
var gs []glyph
for i, input := range inputs {
out := (&shaping.HarfbuzzShaper{}).Shape(input)
outputs[i] = out
(shaping.Line{out}).AdjustBaselines()
var indices []int
for i := range text {
indices = append(indices, i)
}
indices = append(indices, len(text))
for _, gl := range out.Glyphs {
gl := gl
var segs []api.Segment
switch data := g.f.GlyphData(gl.GlyphID).(type) {
case api.GlyphOutline:
if out.Direction.IsSideways() {
data.Sideways(fixed26_6ToFloat32(-gl.YOffset) / fixed26_6ToFloat32(out.Size) * float32(face.Source.f.Upem()))
}
segs = data.Segments
case api.GlyphSVG:
segs = data.Outline.Segments
case api.GlyphBitmap:
if data.Outline != nil {
segs = data.Outline.Segments
}
}
scaledSegs := make([]api.Segment, len(segs))
scale := float32(g.scale(fixed26_6ToFloat64(out.Size)))
for i, seg := range segs {
scaledSegs[i] = seg
for j := range seg.Args {
scaledSegs[i].Args[j].X *= scale
scaledSegs[i].Args[j].Y *= -scale
}
}
gs = append(gs, glyph{
shapingGlyph: &gl,
startIndex: indices[gl.ClusterIndex],
endIndex: indices[gl.ClusterIndex+gl.RuneCount],
scaledSegments: scaledSegs,
bounds: segmentsToBounds(scaledSegs),
})
}
}
if g.outputCache == nil {
g.outputCache = map[goTextOutputCacheKey]*goTextOutputCacheValue{}
}
var indices []int
for i := range text {
indices = append(indices, i)
}
indices = append(indices, len(text))
gs := make([]glyph, len(out.Glyphs))
for i, gl := range out.Glyphs {
gl := gl
var segs []api.Segment
switch data := g.f.GlyphData(gl.GlyphID).(type) {
case api.GlyphOutline:
segs = data.Segments
case api.GlyphSVG:
segs = data.Outline.Segments
case api.GlyphBitmap:
if data.Outline != nil {
segs = data.Outline.Segments
}
}
scaledSegs := make([]api.Segment, len(segs))
scale := float32(g.scale(fixed26_6ToFloat64(out.Size)))
for i, seg := range segs {
scaledSegs[i] = seg
for j := range seg.Args {
scaledSegs[i].Args[j].X *= scale
scaledSegs[i].Args[j].Y *= -scale
}
}
gs[i] = glyph{
shapingGlyph: &gl,
startIndex: indices[gl.ClusterIndex],
endIndex: indices[gl.ClusterIndex+gl.RuneCount],
scaledSegments: scaledSegs,
bounds: segmentsToBounds(scaledSegs),
}
}
g.outputCache[key] = &goTextOutputCacheValue{
output: out,
glyphs: gs,
atime: now(),
outputs: outputs,
glyphs: gs,
atime: now(),
}
const cacheSoftLimit = 512
@ -250,7 +277,7 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) (shaping.Output,
}
}
return out, gs
return outputs, gs
}
func (g *GoTextFaceSource) scale(size float64) float64 {
@ -266,3 +293,11 @@ func (g *GoTextFaceSource) getOrCreateGlyphImage(goTextFace *GoTextFace, key goT
}
return g.glyphImageCache[goTextFace.Size].getOrCreate(goTextFace, key, create)
}
type singleFontmap struct {
face font.Face
}
func (s *singleFontmap) ResolveFace(r rune) font.Face {
return s.face
}

View File

@ -186,22 +186,18 @@ func forEachLine(text string, face Face, options *LayoutOptions, f func(text str
boundaryWidth = longestAdvance
boundaryHeight = float64(lineCount-1)*options.LineSpacingInPixels + m.HAscent + m.HDescent
} else {
// TODO: Perhaps HAscent and HDescent should be used for sideways glyphs.
boundaryWidth = float64(lineCount-1)*options.LineSpacingInPixels + m.VAscent + m.VDescent
boundaryHeight = longestAdvance
}
var offsetX, offsetY float64
// The Y position has an offset by an ascent for horizontal texts.
if d.isHorizontal() {
offsetY += m.HAscent
}
// TODO: Adjust offsets for vertical texts.
// Adjust the offset based on the secondary alignments.
h, v := calcAligns(d, options.PrimaryAlign, options.SecondaryAlign)
switch d {
case DirectionLeftToRight, DirectionRightToLeft:
offsetY += m.HAscent
switch v {
case verticalAlignTop:
case verticalAlignCenter:
@ -210,7 +206,8 @@ func forEachLine(text string, face Face, options *LayoutOptions, f func(text str
offsetY -= boundaryHeight
}
case DirectionTopToBottomAndLeftToRight:
offsetX -= m.VAscent
// TODO: Perhaps HDescent should be used for sideways glyphs.
offsetX += m.VDescent
switch h {
case horizontalAlignLeft:
case horizontalAlignCenter:
@ -219,6 +216,7 @@ func forEachLine(text string, face Face, options *LayoutOptions, f func(text str
offsetX -= boundaryWidth
}
case DirectionTopToBottomAndRightToLeft:
// TODO: Perhaps HAscent should be used for sideways glyphs.
offsetX -= m.VAscent
switch h {
case horizontalAlignLeft: