mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-04 08:55:35 +00:00
aee87baefe
Commit https://gioui.org/commit/b331407e81456 added text layout and shaping based on io.Reader and changed Editor to use it. Unfortunately, as ~inkeliz discovered, caching of shapes were also lost. ~inkeliz suggested fix, https://lists.sr.ht/~eliasnaur/gio-patches/patches/15059 adds caching of shapes to Editor to regain lost performance. This change repairs the cache to work on io.Reader API, in hope that the already complicated Editor won't need additional caching. Before this change, text layouts were represented as a slice of (rune, advance) pairs. Unfortunately, this representation doesn't lend itself to caching of shaping results, so change the representation of a line of text to be a pair of text and advances: package text type Layout { Text string Advances []fixed.Int26_6 } The Text field can then be used in a cache key, assuming Advances is consistent with it. The end result is that the two shaper variants of text.Shaper is reduced to just one, and the Len field field of text.Line is no longer needed. The changed representation adds a bit of extra work to package opentype. Cleaning that up is left as a future TODO. Signed-off-by: Elias Naur <mail@eliasnaur.com>
410 lines
9.7 KiB
Go
410 lines
9.7 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
// Package opentype implements text layout and shaping for OpenType
|
|
// files.
|
|
package opentype
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"gioui.org/f32"
|
|
"gioui.org/op"
|
|
"gioui.org/op/clip"
|
|
"gioui.org/text"
|
|
"golang.org/x/image/font"
|
|
"golang.org/x/image/font/sfnt"
|
|
"golang.org/x/image/math/fixed"
|
|
)
|
|
|
|
// Font implements text.Face. It's 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, txt io.Reader) ([]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) op.CallOp {
|
|
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, txt io.Reader) ([]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) op.CallOp {
|
|
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) op.CallOp {
|
|
var lastPos f32.Point
|
|
var builder clip.Path
|
|
ops := new(op.Ops)
|
|
m := op.Record(ops)
|
|
var x fixed.Int26_6
|
|
builder.Begin(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++
|
|
}
|
|
builder.Outline().Add(ops)
|
|
return m.Stop()
|
|
}
|
|
|
|
func readGlyphs(r io.Reader) ([]glyph, error) {
|
|
var glyphs []glyph
|
|
buf := make([]byte, 0, 1024)
|
|
for {
|
|
n, err := r.Read(buf[len(buf):cap(buf)])
|
|
buf = buf[:len(buf)+n]
|
|
lim := len(buf)
|
|
// Read full runes if possible.
|
|
if err != io.EOF {
|
|
lim -= utf8.UTFMax - 1
|
|
}
|
|
i := 0
|
|
for i < lim {
|
|
c, s := utf8.DecodeRune(buf[i:])
|
|
i += s
|
|
glyphs = append(glyphs, glyph{Rune: c})
|
|
}
|
|
n = copy(buf, buf[i:])
|
|
buf = buf[:n]
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return nil, err
|
|
}
|
|
}
|
|
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
|
|
}
|