deps,text,widget,font/opentype: [API] add harfbuzz-powered text shaper

This commit introduces a new text shaping infrastructure
powered by Benoit Kugler's Go source-port of harfbuzz.
This shaper can properly display complex scripts and RTL
text. This commit changes the signature of the text.Shaper
function, which is a breaking API change.

The new functionality is available via opentype.ParseHarfbuzz,
which configures a text.Shaper leveraging the new backend.

References: https://todo.sr.ht/~eliasnaur/gio/146
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
Chris Waldon
2022-03-16 16:01:33 -04:00
committed by Elias Naur
parent db82d12372
commit 1e5a3696f5
12 changed files with 3009 additions and 143 deletions
+10 -6
View File
@@ -3,6 +3,7 @@
package text
import (
"gioui.org/io/system"
"gioui.org/op/clip"
"golang.org/x/image/math/fixed"
)
@@ -27,17 +28,20 @@ type path struct {
next, prev *path
key pathKey
val clip.PathSpec
layout Layout
}
type layoutKey struct {
ppem fixed.Int26_6
maxWidth int
str string
locale system.Locale
}
type pathKey struct {
ppem fixed.Int26_6
str string
ppem fixed.Int26_6
str string
gidHash uint64
}
const maxSize = 1000
@@ -81,8 +85,8 @@ func (l *layoutCache) insert(lt *layoutElem) {
lt.next.prev = lt
}
func (c *pathCache) Get(k pathKey) (clip.PathSpec, bool) {
if v, ok := c.m[k]; ok {
func (c *pathCache) Get(k pathKey, l Layout) (clip.PathSpec, bool) {
if v, ok := c.m[k]; ok && l.equals(v.layout) {
c.remove(v)
c.insert(v)
return v.val, true
@@ -90,7 +94,7 @@ func (c *pathCache) Get(k pathKey) (clip.PathSpec, bool) {
return clip.PathSpec{}, false
}
func (c *pathCache) Put(k pathKey, v clip.PathSpec) {
func (c *pathCache) Put(k pathKey, l Layout, v clip.PathSpec) {
if c.m == nil {
c.m = make(map[pathKey]*path)
c.head = new(path)
@@ -98,7 +102,7 @@ func (c *pathCache) Put(k pathKey, v clip.PathSpec) {
c.head.prev = c.tail
c.tail.next = c.head
}
val := &path{key: k, val: v}
val := &path{key: k, val: v, layout: l}
c.m[k] = val
c.insert(val)
if len(c.m) > maxSize {
+2 -2
View File
@@ -24,10 +24,10 @@ func TestLayoutLRU(t *testing.T) {
func TestPathLRU(t *testing.T) {
c := new(pathCache)
put := func(i int) {
c.Put(pathKey{str: strconv.Itoa(i)}, clip.PathSpec{})
c.Put(pathKey{str: strconv.Itoa(i), gidHash: uint64(i)}, Layout{Runes: Range{Count: i}}, clip.PathSpec{})
}
get := func(i int) bool {
_, ok := c.Get(pathKey{str: strconv.Itoa(i)})
_, ok := c.Get(pathKey{str: strconv.Itoa(i), gidHash: uint64(i)}, Layout{Runes: Range{Count: i}})
return ok
}
testLRU(t, put, get)
+32 -12
View File
@@ -3,20 +3,23 @@
package text
import (
"encoding/binary"
"hash/maphash"
"io"
"strings"
"golang.org/x/image/math/fixed"
"gioui.org/io/system"
"gioui.org/op/clip"
)
// Shaper implements layout and shaping of text.
type Shaper interface {
// Layout a text according to a set of options.
Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error)
Layout(font Font, size fixed.Int26_6, maxWidth int, lc system.Locale, txt io.RuneReader) ([]Line, error)
// LayoutString is Layout for strings.
LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line
LayoutString(font Font, size fixed.Int26_6, maxWidth int, lc system.Locale, str string) []Line
// Shape a line of text and return a clipping operation for its outline.
Shape(font Font, size fixed.Int26_6, layout Layout) clip.PathSpec
}
@@ -44,6 +47,7 @@ type faceCache struct {
face Face
layoutCache layoutCache
pathCache pathCache
seed maphash.Seed
}
func (c *Cache) lookup(font Font) *faceCache {
@@ -108,15 +112,15 @@ func NewCache(collection []FontFace) *Cache {
}
// Layout implements the Shaper interface.
func (c *Cache) Layout(font Font, size fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error) {
func (c *Cache) Layout(font Font, size fixed.Int26_6, maxWidth int, lc system.Locale, txt io.RuneReader) ([]Line, error) {
cache := c.lookup(font)
return cache.face.Layout(size, maxWidth, txt)
return cache.face.Layout(size, maxWidth, lc, txt)
}
// LayoutString is a caching implementation of the Shaper interface.
func (c *Cache) LayoutString(font Font, size fixed.Int26_6, maxWidth int, str string) []Line {
func (c *Cache) LayoutString(font Font, size fixed.Int26_6, maxWidth int, lc system.Locale, str string) []Line {
cache := c.lookup(font)
return cache.layout(size, maxWidth, str)
return cache.layout(size, maxWidth, lc, str)
}
// Shape is a caching implementation of the Shaper interface. Shape assumes that the layout
@@ -126,7 +130,7 @@ func (c *Cache) Shape(font Font, size fixed.Int26_6, layout Layout) clip.PathSpe
return cache.shape(size, layout)
}
func (f *faceCache) layout(ppem fixed.Int26_6, maxWidth int, str string) []Line {
func (f *faceCache) layout(ppem fixed.Int26_6, maxWidth int, lc system.Locale, str string) []Line {
if f == nil {
return nil
}
@@ -134,27 +138,43 @@ func (f *faceCache) layout(ppem fixed.Int26_6, maxWidth int, str string) []Line
ppem: ppem,
maxWidth: maxWidth,
str: str,
locale: lc,
}
if l, ok := f.layoutCache.Get(lk); ok {
return l
}
l, _ := f.face.Layout(ppem, maxWidth, strings.NewReader(str))
l, _ := f.face.Layout(ppem, maxWidth, lc, strings.NewReader(str))
f.layoutCache.Put(lk, l)
return l
}
// hashGIDs returns a 64-bit hash value of the font GIDs contained
// within the provided layout.
func (f *faceCache) hashGIDs(layout Layout) uint64 {
if f.seed == (maphash.Seed{}) {
f.seed = maphash.MakeSeed()
}
var h maphash.Hash
h.SetSeed(f.seed)
for _, g := range layout.Glyphs {
binary.Write(&h, binary.LittleEndian, g.ID)
}
return h.Sum64()
}
func (f *faceCache) shape(ppem fixed.Int26_6, layout Layout) clip.PathSpec {
if f == nil {
return clip.PathSpec{}
}
pk := pathKey{
ppem: ppem,
str: layout.Text,
ppem: ppem,
str: layout.Text,
gidHash: f.hashGIDs(layout),
}
if clip, ok := f.pathCache.Get(pk); ok {
if clip, ok := f.pathCache.Get(pk, layout); ok {
return clip
}
clip := f.face.Shape(ppem, layout)
f.pathCache.Put(pk, clip)
f.pathCache.Put(pk, layout, clip)
return clip
}
+124 -1
View File
@@ -5,7 +5,9 @@ package text
import (
"io"
"gioui.org/io/system"
"gioui.org/op/clip"
"github.com/go-text/typesetting/font"
"golang.org/x/image/math/fixed"
)
@@ -23,9 +25,130 @@ type Line struct {
Bounds fixed.Rectangle26_6
}
// Range describes the position and quantity of a range of text elements
// within a larger slice. The unit is usually runes of unicode data or
// glyphs of shaped font data.
type Range struct {
// Count describes the number of items represented by the Range.
Count int
// Offset describes the start position of the represented
// items within a larger list.
Offset int
}
// GlyphID uniquely identifies a glyph within a specific font.
type GlyphID = font.GID
// Glyph contains the metadata needed to render a glyph.
type Glyph struct {
// ID is this glyph's identifier within the font it was shaped with.
ID GlyphID
// ClusterIndex is the identifier for the text shaping cluster that
// this glyph is part of.
ClusterIndex int
// GlyphCount is the number of glyphs in the same cluster as this glyph.
GlyphCount int
// RuneCount is the quantity of runes in the source text that this glyph
// corresponds to.
RuneCount int
// XAdvance and YAdvance describe the distance the dot moves when
// laying out the glyph on the X or Y axis.
XAdvance, YAdvance fixed.Int26_6
// XOffset and YOffset describe offsets from the dot that should be
// applied when rendering the glyph.
XOffset, YOffset fixed.Int26_6
}
// GlyphCluster provides metadata about a sequence of indivisible shaped
// glyphs.
type GlyphCluster struct {
// Advance is the cumulative advance of all glyphs in the cluster.
Advance fixed.Int26_6
// Runes indicates the position and quantity of the runes represented by
// this cluster within the text.
Runes Range
// Glyphs indicates the position and quantity of the glyphs within this
// cluster in a Layout's Glyphs slice.
Glyphs Range
}
// RuneWidth returns the effective width of one rune for this cluster.
// If the cluster contains multiple runes, the width of the glyphs of
// the cluster is divided evenly among the runes.
func (c GlyphCluster) RuneWidth() fixed.Int26_6 {
if c.Runes.Count == 0 {
return 0
}
return c.Advance / fixed.Int26_6(c.Runes.Count)
}
type Layout struct {
// Glyphs are the actual font characters for the text. The are ordered
// from left to right regardless of the text direction of the underlying
// text.
Glyphs []Glyph
// Clusters is metadata about the shaped glyphs. It is mostly useful for
// interactive text widgets like editors. The order of clusters is logical,
// so the first cluster will describe the beginning of the text and may
// refer to the final glyphs in the Glyphs field if the text is RTL.
Clusters []GlyphCluster
Text string
Advances []fixed.Int26_6
// Runes describes the position of the text data this layout represents
// within the overall body of text being shaped.
Runes Range
// Direction is the layout direction of the text.
Direction system.TextDirection
}
// Slice returns a layout starting at the glyph cluster index start
// and running through the glyph cluster index end. The Offsets field
// of the returned layout is adjusted to reflect the new rune range
// covered by the layout. The returned layout will have no Clusters.
func (l Layout) Slice(start, end int) Layout {
if start == end || end == 0 || start == len(l.Clusters) {
return Layout{}
}
newRuneStart := l.Clusters[start].Runes.Offset
runesBefore := newRuneStart - l.Runes.Offset
endCluster := l.Clusters[end-1]
startCluster := l.Clusters[start]
runesAfter := l.Runes.Offset + l.Runes.Count - (endCluster.Runes.Offset + endCluster.Runes.Count)
if l.Direction.Progression() == system.TowardOrigin {
startCluster, endCluster = endCluster, startCluster
}
glyphStart := startCluster.Glyphs.Offset
glyphEnd := endCluster.Glyphs.Offset + endCluster.Glyphs.Count
out := l
out.Clusters = nil
out.Glyphs = out.Glyphs[glyphStart:glyphEnd]
out.Runes.Offset = newRuneStart
out.Runes.Count -= runesBefore + runesAfter
return out
}
// equals returns true when l2 is logically equivalent to l.
func (l Layout) equals(l2 Layout) bool {
if len(l.Glyphs) != len(l2.Glyphs) || len(l.Clusters) != len(l2.Clusters) {
return false
}
if l.Runes != l2.Runes || l.Direction != l2.Direction {
return false
}
for i := range l.Clusters {
if l.Clusters[i] != l2.Clusters[i] {
return false
}
}
for i := range l.Glyphs {
if l.Glyphs[i] != l2.Glyphs[i] {
return false
}
}
return true
}
// Style is the font style.
@@ -47,7 +170,7 @@ type Font struct {
// Face implements text layout and shaping for a particular font. All
// methods must be safe for concurrent use.
type Face interface {
Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]Line, error)
Layout(ppem fixed.Int26_6, maxWidth int, lc system.Locale, txt io.RuneReader) ([]Line, error)
Shape(ppem fixed.Int26_6, str Layout) clip.PathSpec
}