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 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` # `NotoSansThai-Regular.ttf`
Open Font License 1.1 Open Font License 1.1

Binary file not shown.

View File

@ -83,6 +83,19 @@ func init() {
myanmarFaceSource = s 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 var japaneseFaceSource *text.GoTextFaceSource
func init() { func init() {
@ -167,7 +180,26 @@ func (g *Game) Draw(screen *ebiten.Image) {
text.Draw(screen, thaiText, f, op) 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{ f := &text.GoTextFace{
Source: japaneseFaceSource, Source: japaneseFaceSource,
Direction: text.DirectionTopToBottomAndRightToLeft, Direction: text.DirectionTopToBottomAndRightToLeft,

2
go.mod
View File

@ -5,7 +5,7 @@ go 1.18
require ( require (
github.com/ebitengine/oto/v3 v3.2.0-alpha.2.0.20231021101548-b794c0292b2b github.com/ebitengine/oto/v3 v3.2.0-alpha.2.0.20231021101548-b794c0292b2b
github.com/ebitengine/purego v0.6.0-alpha.2 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/bitmapfont/v3 v3.0.0
github.com/hajimehoshi/go-mp3 v0.3.4 github.com/hajimehoshi/go-mp3 v0.3.4
github.com/jakecoffman/cp v1.2.1 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/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 h1:lYSvMtNBEjNGAzqPC5WP7bHUOxkFU3L+JZMdxK7krkw=
github.com/ebitengine/purego v0.6.0-alpha.2/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= 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-20231221124458-48cc05a56658 h1:KeDKnC99J3l5qJK4zV13Au2UwPn4N20TnIlM0YvILj8=
github.com/go-text/typesetting v0.0.0-20231211160022-6295f3c76f4d/go.mod h1:MrLApvxyzSW0MhQqLc484jkUWYX4wsEvEqDosB5Io80= github.com/go-text/typesetting v0.0.0-20231221124458-48cc05a56658/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
github.com/go-text/typesetting-utils v0.0.0-20231204162240-fa4dc564ba79 h1:3yBOzx29wog0i7TnUBMcp90EwIb+A5kqmr5vny1UOm8= 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 h1:r2+6gYK38nfztS/et50gHAswb9hXgxXECYgE8Nczmi4=
github.com/hajimehoshi/bitmapfont/v3 v3.0.0/go.mod h1:+CxxG+uMmgU4mI2poq944i3uZ6UYFfAkj9V6WqmuvZA= github.com/hajimehoshi/bitmapfont/v3 v3.0.0/go.mod h1:+CxxG+uMmgU4mI2poq944i3uZ6UYFfAkj9V6WqmuvZA=
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68= 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. // advance implements Face.
func (g *GoTextFace) advance(text string) float64 { func (g *GoTextFace) advance(text string) float64 {
output, _ := g.Source.shape(text, g) outputs, _ := g.Source.shape(text, g)
if g.direction().isHorizontal() {
return fixed26_6ToFloat64(output.Advance) 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. // hasGlyph implements Face.

View File

@ -49,7 +49,7 @@ type glyph struct {
} }
type goTextOutputCacheValue struct { type goTextOutputCacheValue struct {
output shaping.Output outputs []shaping.Output
glyphs []glyph glyphs []glyph
atime int64 atime int64
} }
@ -164,7 +164,7 @@ func (g *GoTextFaceSource) UnsafeInternal() font.Face {
return g.f 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.copyCheck()
g.m.Lock() g.m.Lock()
@ -173,7 +173,7 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) (shaping.Output,
key := face.outputCacheKey(text) key := face.outputCacheKey(text)
if out, ok := g.outputCache[key]; ok { if out, ok := g.outputCache[key]; ok {
out.atime = now() out.atime = now()
return out.output, out.glyphs return out.outputs, out.glyphs
} }
g.f.SetVariations(face.variations) g.f.SetVariations(face.variations)
@ -189,23 +189,45 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) (shaping.Output,
Script: face.gScript(), Script: face.gScript(),
Language: language.Language(face.Language.String()), Language: language.Language(face.Language.String()),
} }
out := (&shaping.HarfbuzzShaper{}).Shape(input)
if g.outputCache == nil { var inputs []shaping.Input
g.outputCache = map[goTextOutputCacheKey]*goTextOutputCacheValue{} 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 var indices []int
for i := range text { for i := range text {
indices = append(indices, i) indices = append(indices, i)
} }
indices = append(indices, len(text)) indices = append(indices, len(text))
gs := make([]glyph, len(out.Glyphs)) for _, gl := range out.Glyphs {
for i, gl := range out.Glyphs {
gl := gl gl := gl
var segs []api.Segment var segs []api.Segment
switch data := g.f.GlyphData(gl.GlyphID).(type) { switch data := g.f.GlyphData(gl.GlyphID).(type) {
case api.GlyphOutline: 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 segs = data.Segments
case api.GlyphSVG: case api.GlyphSVG:
segs = data.Outline.Segments segs = data.Outline.Segments
@ -225,16 +247,21 @@ func (g *GoTextFaceSource) shape(text string, face *GoTextFace) (shaping.Output,
} }
} }
gs[i] = glyph{ gs = append(gs, glyph{
shapingGlyph: &gl, shapingGlyph: &gl,
startIndex: indices[gl.ClusterIndex], startIndex: indices[gl.ClusterIndex],
endIndex: indices[gl.ClusterIndex+gl.RuneCount], endIndex: indices[gl.ClusterIndex+gl.RuneCount],
scaledSegments: scaledSegs, scaledSegments: scaledSegs,
bounds: segmentsToBounds(scaledSegs), bounds: segmentsToBounds(scaledSegs),
})
} }
} }
if g.outputCache == nil {
g.outputCache = map[goTextOutputCacheKey]*goTextOutputCacheValue{}
}
g.outputCache[key] = &goTextOutputCacheValue{ g.outputCache[key] = &goTextOutputCacheValue{
output: out, outputs: outputs,
glyphs: gs, glyphs: gs,
atime: now(), atime: now(),
} }
@ -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 { 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) 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 boundaryWidth = longestAdvance
boundaryHeight = float64(lineCount-1)*options.LineSpacingInPixels + m.HAscent + m.HDescent boundaryHeight = float64(lineCount-1)*options.LineSpacingInPixels + m.HAscent + m.HDescent
} else { } else {
// TODO: Perhaps HAscent and HDescent should be used for sideways glyphs.
boundaryWidth = float64(lineCount-1)*options.LineSpacingInPixels + m.VAscent + m.VDescent boundaryWidth = float64(lineCount-1)*options.LineSpacingInPixels + m.VAscent + m.VDescent
boundaryHeight = longestAdvance boundaryHeight = longestAdvance
} }
var offsetX, offsetY float64 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. // Adjust the offset based on the secondary alignments.
h, v := calcAligns(d, options.PrimaryAlign, options.SecondaryAlign) h, v := calcAligns(d, options.PrimaryAlign, options.SecondaryAlign)
switch d { switch d {
case DirectionLeftToRight, DirectionRightToLeft: case DirectionLeftToRight, DirectionRightToLeft:
offsetY += m.HAscent
switch v { switch v {
case verticalAlignTop: case verticalAlignTop:
case verticalAlignCenter: case verticalAlignCenter:
@ -210,7 +206,8 @@ func forEachLine(text string, face Face, options *LayoutOptions, f func(text str
offsetY -= boundaryHeight offsetY -= boundaryHeight
} }
case DirectionTopToBottomAndLeftToRight: case DirectionTopToBottomAndLeftToRight:
offsetX -= m.VAscent // TODO: Perhaps HDescent should be used for sideways glyphs.
offsetX += m.VDescent
switch h { switch h {
case horizontalAlignLeft: case horizontalAlignLeft:
case horizontalAlignCenter: case horizontalAlignCenter:
@ -219,6 +216,7 @@ func forEachLine(text string, face Face, options *LayoutOptions, f func(text str
offsetX -= boundaryWidth offsetX -= boundaryWidth
} }
case DirectionTopToBottomAndRightToLeft: case DirectionTopToBottomAndRightToLeft:
// TODO: Perhaps HAscent should be used for sideways glyphs.
offsetX -= m.VAscent offsetX -= m.VAscent
switch h { switch h {
case horizontalAlignLeft: case horizontalAlignLeft: