forked from joejulian/gio
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:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user