font/opentype: [API] replace old font type with harfbuzz

This commit replaces the previous opentype.Font with
an implementation that uses the new text shaper. In
order to keep the implementation simple, support for
opentype font collections was dropped. It should be
possible to re-add this support after some changes
to the text shaper's line wrapping algorithm.

To expand on the above, doing proper font fallback with
harfbuzz will require splitting the input text on font
glyph support boundaries, changing the input from a
simple shaping.Input to []shaping.Input with each input
matched against the font that supports its language.
The line wrapping then needs to be able to properly
consume that slice. Since the line wrapping algorithm is
really complex, I'm hoping to defer that modification
until this simple version is accepted.

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:38 -04:00
committed by Elias Naur
parent 42c99a5cb2
commit 01276238df
4 changed files with 20 additions and 393 deletions
+1 -1
View File
@@ -78,7 +78,7 @@ func CollectionHB() []text.FontFace {
}
func registerHB(fnt text.Font, ttf []byte) {
face, err := opentype.ParseHarfbuzz(ttf)
face, err := opentype.Parse(ttf)
if err != nil {
panic(fmt.Errorf("failed to parse font: %v", err))
}
+10 -385
View File
@@ -9,15 +9,12 @@ import (
"fmt"
"image"
"io"
"unicode"
"unicode/utf8"
"github.com/benoitkugler/textlayout/fonts"
"github.com/benoitkugler/textlayout/fonts/truetype"
"github.com/benoitkugler/textlayout/harfbuzz"
"github.com/go-text/typesetting/shaping"
"golang.org/x/image/font"
"golang.org/x/image/font/sfnt"
"golang.org/x/image/math/fixed"
"gioui.org/f32"
@@ -28,32 +25,32 @@ import (
"gioui.org/text"
)
// HarfbuzzFont implements the text.Shaper interface using a rich text
// Font implements the text.Shaper interface using a rich text
// shaping engine.
type HarfbuzzFont struct {
type Font struct {
font *truetype.Font
}
// ParseHarfbuzz constructs a HarfbuzzFont from source bytes.
func ParseHarfbuzz(src []byte) (*HarfbuzzFont, error) {
// Parse constructs a Font from source bytes.
func Parse(src []byte) (*Font, error) {
face, err := truetype.Parse(bytes.NewReader(src))
if err != nil {
return nil, fmt.Errorf("failed parsing truetype font: %w", err)
}
return &HarfbuzzFont{
return &Font{
font: face,
}, nil
}
func (f *HarfbuzzFont) Layout(ppem fixed.Int26_6, maxWidth int, lc system.Locale, txt io.RuneReader) ([]text.Line, error) {
func (f *Font) Layout(ppem fixed.Int26_6, maxWidth int, lc system.Locale, txt io.RuneReader) ([]text.Line, error) {
return internal.Document(shaping.Shape, f.font, ppem, maxWidth, lc, txt), nil
}
func (f *HarfbuzzFont) Shape(ppem fixed.Int26_6, str text.Layout) clip.PathSpec {
return harfbuzzTextPath(ppem, f, str)
func (f *Font) Shape(ppem fixed.Int26_6, str text.Layout) clip.PathSpec {
return textPath(ppem, f, str)
}
func (f *HarfbuzzFont) Metrics(ppem fixed.Int26_6) font.Metrics {
func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics {
metrics := font.Metrics{}
font := harfbuzz.NewFont(f.font)
font.XScale = int32(ppem.Ceil()) << 6
@@ -75,312 +72,7 @@ func (f *HarfbuzzFont) Metrics(ppem fixed.Int26_6) font.Metrics {
return metrics
}
// Font implements text.Face. Its methods are safe to use
// concurrently.
type Font struct {
font *sfnt.Font
}
// Collection is a collection of one or more fonts. When used as a text.Face,
// each rune will be assigned a glyph from the first font in the collection
// that supports it.
type Collection struct {
fonts []*opentype
}
type opentype struct {
Font *sfnt.Font
Hinting font.Hinting
}
// a glyph represents a rune and its advance according to a Font.
// TODO: remove this type and work on io.Readers directly.
type glyph struct {
Rune rune
Advance fixed.Int26_6
}
// NewFont parses an SFNT font, such as TTF or OTF data, from a []byte
// data source.
func Parse(src []byte) (*Font, error) {
fnt, err := sfnt.Parse(src)
if err != nil {
return nil, err
}
return &Font{font: fnt}, nil
}
// ParseCollection parses an SFNT font collection, such as TTC or OTC data,
// from a []byte data source.
//
// If passed data for a single font, a TTF or OTF instead of a TTC or OTC,
// it will return a collection containing 1 font.
func ParseCollection(src []byte) (*Collection, error) {
c, err := sfnt.ParseCollection(src)
if err != nil {
return nil, err
}
return newCollectionFrom(c)
}
// ParseCollectionReaderAt parses an SFNT collection, such as TTC or OTC data,
// from an io.ReaderAt data source.
//
// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, it
// will return a collection containing 1 font.
func ParseCollectionReaderAt(src io.ReaderAt) (*Collection, error) {
c, err := sfnt.ParseCollectionReaderAt(src)
if err != nil {
return nil, err
}
return newCollectionFrom(c)
}
func newCollectionFrom(coll *sfnt.Collection) (*Collection, error) {
fonts := make([]*opentype, coll.NumFonts())
for i := range fonts {
fnt, err := coll.Font(i)
if err != nil {
return nil, err
}
fonts[i] = &opentype{
Font: fnt,
Hinting: font.HintingFull,
}
}
return &Collection{fonts: fonts}, nil
}
// NumFonts returns the number of fonts in the collection.
func (c *Collection) NumFonts() int {
return len(c.fonts)
}
// Font returns the i'th font in the collection.
func (c *Collection) Font(i int) (*Font, error) {
if i < 0 || len(c.fonts) <= i {
return nil, sfnt.ErrNotFound
}
return &Font{font: c.fonts[i].Font}, nil
}
func (f *Font) Layout(ppem fixed.Int26_6, maxWidth int, lc system.Locale, txt io.RuneReader) ([]text.Line, error) {
glyphs, err := readGlyphs(txt)
if err != nil {
return nil, err
}
fonts := []*opentype{{Font: f.font, Hinting: font.HintingFull}}
var buf sfnt.Buffer
return layoutText(&buf, ppem, maxWidth, fonts, glyphs)
}
func (f *Font) Shape(ppem fixed.Int26_6, str text.Layout) clip.PathSpec {
var buf sfnt.Buffer
return textPath(&buf, ppem, []*opentype{{Font: f.font, Hinting: font.HintingFull}}, str)
}
func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics {
o := &opentype{Font: f.font, Hinting: font.HintingFull}
var buf sfnt.Buffer
return o.Metrics(&buf, ppem)
}
func (c *Collection) Layout(ppem fixed.Int26_6, maxWidth int, lc system.Locale, txt io.RuneReader) ([]text.Line, error) {
glyphs, err := readGlyphs(txt)
if err != nil {
return nil, err
}
var buf sfnt.Buffer
return layoutText(&buf, ppem, maxWidth, c.fonts, glyphs)
}
func (c *Collection) Shape(ppem fixed.Int26_6, str text.Layout) clip.PathSpec {
var buf sfnt.Buffer
return textPath(&buf, ppem, c.fonts, str)
}
func fontForGlyph(buf *sfnt.Buffer, fonts []*opentype, r rune) *opentype {
if len(fonts) < 1 {
return nil
}
for _, f := range fonts {
if f.HasGlyph(buf, r) {
return f
}
}
return fonts[0] // Use replacement character from the first font if necessary
}
func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, fonts []*opentype, glyphs []glyph) ([]text.Line, error) {
var lines []text.Line
var nextLine text.Line
updateBounds := func(f *opentype) {
m := f.Metrics(sbuf, ppem)
if m.Ascent > nextLine.Ascent {
nextLine.Ascent = m.Ascent
}
// m.Height is equal to m.Ascent + m.Descent + linegap.
// Compute the descent including the linegap.
descent := m.Height - m.Ascent
if descent > nextLine.Descent {
nextLine.Descent = descent
}
b := f.Bounds(sbuf, ppem)
nextLine.Bounds = nextLine.Bounds.Union(b)
}
maxDotX := fixed.I(maxWidth)
type state struct {
r rune
f *opentype
adv fixed.Int26_6
x fixed.Int26_6
idx int
len int
valid bool
}
var prev, word state
endLine := func() {
if prev.f == nil && len(fonts) > 0 {
prev.f = fonts[0]
}
updateBounds(prev.f)
nextLine.Layout = toLayout(glyphs[:prev.idx:prev.idx])
nextLine.Width = prev.x + prev.adv
nextLine.Bounds.Max.X += prev.x
lines = append(lines, nextLine)
glyphs = glyphs[prev.idx:]
nextLine = text.Line{}
prev = state{}
word = state{}
}
for prev.idx < len(glyphs) {
g := &glyphs[prev.idx]
next := state{
r: g.Rune,
f: fontForGlyph(sbuf, fonts, g.Rune),
idx: prev.idx + 1,
len: prev.len + utf8.RuneLen(g.Rune),
x: prev.x + prev.adv,
}
if next.f != nil {
if next.f != prev.f {
updateBounds(next.f)
}
next.adv, next.valid = next.f.GlyphAdvance(sbuf, ppem, g.Rune)
}
if g.Rune == '\n' {
// The newline is zero width; use the previous
// character for line measurements.
prev.idx = next.idx
prev.len = next.len
endLine()
continue
}
var k fixed.Int26_6
if prev.valid && next.f != nil {
k = next.f.Kern(sbuf, ppem, prev.r, next.r)
}
// Break the line if we're out of space.
if prev.idx > 0 && next.x+next.adv+k > maxDotX {
// If the line contains no word breaks, break off the last rune.
if word.idx == 0 {
word = prev
}
next.x -= word.x + word.adv
next.idx -= word.idx
next.len -= word.len
prev = word
endLine()
} else if k != 0 {
glyphs[prev.idx-1].Advance += k
next.x += k
}
g.Advance = next.adv
if unicode.IsSpace(g.Rune) {
word = next
}
prev = next
}
endLine()
return lines, nil
}
// toLayout converts a slice of glyphs to a text.Layout.
func toLayout(glyphs []glyph) text.Layout {
var buf bytes.Buffer
advs := make([]fixed.Int26_6, len(glyphs))
for i, g := range glyphs {
buf.WriteRune(g.Rune)
advs[i] = glyphs[i].Advance
}
return text.Layout{Text: buf.String(), Advances: advs}
}
func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, fonts []*opentype, str text.Layout) clip.PathSpec {
var lastPos f32.Point
var builder clip.Path
var x fixed.Int26_6
builder.Begin(new(op.Ops))
rune := 0
for _, r := range str.Text {
if !unicode.IsSpace(r) {
f := fontForGlyph(buf, fonts, r)
if f == nil {
continue
}
segs, ok := f.LoadGlyph(buf, ppem, r)
if !ok {
continue
}
// Move to glyph position.
pos := f32.Point{
X: float32(x) / 64,
}
builder.Move(pos.Sub(lastPos))
lastPos = pos
var lastArg f32.Point
// Convert sfnt.Segments to relative segments.
for _, fseg := range segs {
nargs := 1
switch fseg.Op {
case sfnt.SegmentOpQuadTo:
nargs = 2
case sfnt.SegmentOpCubeTo:
nargs = 3
}
var args [3]f32.Point
for i := 0; i < nargs; i++ {
a := f32.Point{
X: float32(fseg.Args[i].X) / 64,
Y: float32(fseg.Args[i].Y) / 64,
}
args[i] = a.Sub(lastArg)
if i == nargs-1 {
lastArg = a
}
}
switch fseg.Op {
case sfnt.SegmentOpMoveTo:
builder.Move(args[0])
case sfnt.SegmentOpLineTo:
builder.Line(args[0])
case sfnt.SegmentOpQuadTo:
builder.Quad(args[0], args[1])
case sfnt.SegmentOpCubeTo:
builder.Cube(args[0], args[1], args[2])
default:
panic("unsupported segment op")
}
}
lastPos = lastPos.Add(lastArg)
}
x += str.Advances[rune]
rune++
}
return builder.End()
}
func harfbuzzTextPath(ppem fixed.Int26_6, font *HarfbuzzFont, str text.Layout) clip.PathSpec {
func textPath(ppem fixed.Int26_6, font *Font, str text.Layout) clip.PathSpec {
var lastPos f32.Point
var builder clip.Path
ops := new(op.Ops)
@@ -444,70 +136,3 @@ func harfbuzzTextPath(ppem fixed.Int26_6, font *HarfbuzzFont, str text.Layout) c
}
return builder.End()
}
func readGlyphs(r io.RuneReader) ([]glyph, error) {
var glyphs []glyph
for {
c, _, err := r.ReadRune()
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
glyphs = append(glyphs, glyph{Rune: c})
}
return glyphs, nil
}
func (f *opentype) HasGlyph(buf *sfnt.Buffer, r rune) bool {
g, err := f.Font.GlyphIndex(buf, r)
return g != 0 && err == nil
}
func (f *opentype) GlyphAdvance(buf *sfnt.Buffer, ppem fixed.Int26_6, r rune) (advance fixed.Int26_6, ok bool) {
g, err := f.Font.GlyphIndex(buf, r)
if err != nil {
return 0, false
}
adv, err := f.Font.GlyphAdvance(buf, g, ppem, f.Hinting)
return adv, err == nil
}
func (f *opentype) Kern(buf *sfnt.Buffer, ppem fixed.Int26_6, r0, r1 rune) fixed.Int26_6 {
g0, err := f.Font.GlyphIndex(buf, r0)
if err != nil {
return 0
}
g1, err := f.Font.GlyphIndex(buf, r1)
if err != nil {
return 0
}
adv, err := f.Font.Kern(buf, g0, g1, ppem, f.Hinting)
if err != nil {
return 0
}
return adv
}
func (f *opentype) Metrics(buf *sfnt.Buffer, ppem fixed.Int26_6) font.Metrics {
m, _ := f.Font.Metrics(buf, ppem, f.Hinting)
return m
}
func (f *opentype) Bounds(buf *sfnt.Buffer, ppem fixed.Int26_6) fixed.Rectangle26_6 {
r, _ := f.Font.Bounds(buf, ppem, f.Hinting)
return r
}
func (f *opentype) LoadGlyph(buf *sfnt.Buffer, ppem fixed.Int26_6, r rune) ([]sfnt.Segment, bool) {
g, err := f.Font.GlyphIndex(buf, r)
if err != nil {
return nil, false
}
segs, err := f.Font.LoadGlyph(buf, g, ppem, nil)
if err != nil {
return nil, false
}
return segs, true
}
+7 -5
View File
@@ -10,9 +10,7 @@ import (
"strings"
"testing"
"golang.org/x/image/font"
"golang.org/x/image/font/gofont/goregular"
"golang.org/x/image/font/sfnt"
"golang.org/x/image/math/fixed"
"gioui.org/internal/ops"
@@ -43,9 +41,13 @@ func TestEmptyString(t *testing.T) {
t.Fatalf("Layout returned no lines for empty string; expected 1")
}
l := lines[0]
exp, err := face.font.Bounds(new(sfnt.Buffer), ppem, font.HintingFull)
if err != nil {
t.Fatal(err)
exp := fixed.Rectangle26_6{
Min: fixed.Point26_6{
Y: fixed.Int26_6(-12094),
},
Max: fixed.Point26_6{
Y: fixed.Int26_6(2700),
},
}
if got := l.Bounds; got != exp {
t.Errorf("got bounds %+v for empty string; expected %+v", got, exp)
+2 -2
View File
@@ -174,7 +174,7 @@ var arabic = system.Locale{
}
var arabicCollection = func() []text.FontFace {
parsed, _ := opentype.ParseHarfbuzz(nsareg.TTF)
parsed, _ := opentype.Parse(nsareg.TTF)
return []text.FontFace{{Font: text.Font{}, Face: parsed}}
}()
@@ -250,7 +250,7 @@ func TestEditorLigature(t *testing.T) {
Constraints: layout.Exact(image.Pt(100, 100)),
Locale: english,
}
face, err := opentype.ParseHarfbuzz(robotoregular.TTF)
face, err := opentype.Parse(robotoregular.TTF)
if err != nil {
t.Skipf("failed parsing test font: %v", err)
}