~eliasnaur/gio-patches

font/opentype: support using Collection as a Face v2 PROPOSED

tainted-bit: 1
 font/opentype: support using Collection as a Face

 1 files changed, 115 insertions(+), 37 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~eliasnaur/gio-patches/patches/11445/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH v2] font/opentype: support using Collection as a Face Export this patch

This change allows font collection files (extensions .ttc or .otc)
to be used as a text.Face. These files contain an ordered list of
SFNT fonts, each supporting a maximum of 2^16 glyphs. When used as
a text.Face, each rune in the string to layout or render will be
assigned to the first font with a glyph for that rune, or to the
replacement character from the first font in the file otherwise.

With this change, it is possible to support multiple unicode planes
in a single text.Face by using a Collection with more than one
internal SFNT file. For example, it is now possible to display
characters from the basic multilingual plane and emoji in a single
widget.Label by loading an appropriate OTC file.

Fixes gio#104

Signed-off-by: tainted-bit <sourcehut@taintedbit.com>
---
This patch revision removes the unused aggregate metrics. It also
adds locking around the sfnt.Buffer for both Fonts and Collections,
which fixes a race condition with multiple windows.

The patch can be tested with the new font repositories at
github.com/gonoto

 font/opentype/opentype.go | 152 ++++++++++++++++++++++++++++----------
 1 file changed, 115 insertions(+), 37 deletions(-)

diff --git a/font/opentype/opentype.go b/font/opentype/opentype.go
index 27afea4..7c29a28 100644
--- a/font/opentype/opentype.go
+++ b/font/opentype/opentype.go
@@ -6,7 +6,7 @@ package opentype

import (
	"io"

	"sync"
	"unicode"
	"unicode/utf8"

@@ -21,13 +21,18 @@ import (

// Font implements text.Face.
type Font struct {
	font *sfnt.Font
	buf  sfnt.Buffer
	font    *sfnt.Font
	buf     sfnt.Buffer
	bufLock sync.Mutex
}

// Collection is a collection of one or more fonts.
// 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 {
	coll *sfnt.Collection
	fonts   []*opentype
	buf     sfnt.Buffer // Buffer for rendering; we don't want per-font buffers
	bufLock sync.Mutex
}

type opentype struct {
@@ -55,7 +60,7 @@ func ParseCollection(src []byte) (*Collection, error) {
	if err != nil {
		return nil, err
	}
	return &Collection{c}, nil
	return newCollectionFrom(c)
}

// ParseCollectionReaderAt parses an SFNT collection, such as TTC or OTC data,
@@ -68,21 +73,35 @@ func ParseCollectionReaderAt(src io.ReaderAt) (*Collection, error) {
	if err != nil {
		return nil, err
	}
	return &Collection{c}, nil
	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 c.coll.NumFonts()
	return len(c.fonts)
}

// Font returns the i'th font in the collection.
func (c *Collection) Font(i int) (*Font, error) {
	fnt, err := c.coll.Font(i)
	if err != nil {
		return nil, err
	if i < 0 || len(c.fonts) <= i {
		return nil, sfnt.ErrNotFound
	}
	return &Font{font: fnt}, nil
	return &Font{font: c.fonts[i].Font}, nil
}

func (f *Font) Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]text.Line, error) {
@@ -90,31 +109,74 @@ func (f *Font) Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]text.L
	if err != nil {
		return nil, err
	}
	return layoutText(&f.buf, ppem, maxWidth, &opentype{Font: f.font, Hinting: font.HintingFull}, glyphs)
	fonts := []*opentype{{Font: f.font, Hinting: font.HintingFull}}
	f.bufLock.Lock()
	defer f.bufLock.Unlock()
	return layoutText(&f.buf, ppem, maxWidth, fonts, glyphs)
}

func (f *Font) Shape(ppem fixed.Int26_6, str []text.Glyph) op.CallOp {
	return textPath(&f.buf, ppem, &opentype{Font: f.font, Hinting: font.HintingFull}, str)
	f.bufLock.Lock()
	defer f.bufLock.Unlock()
	return textPath(&f.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}
	f.bufLock.Lock()
	defer f.bufLock.Unlock()
	return o.Metrics(&f.buf, ppem)
}

func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, f *opentype, glyphs []text.Glyph) ([]text.Line, error) {
	m := f.Metrics(sbuf, ppem)
	lineTmpl := text.Line{
		Ascent: m.Ascent,
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
	}
	c.bufLock.Lock()
	defer c.bufLock.Unlock()
	return layoutText(&c.buf, ppem, maxWidth, c.fonts, glyphs)
}

func (c *Collection) Shape(ppem fixed.Int26_6, str []text.Glyph) op.CallOp {
	c.bufLock.Lock()
	defer c.bufLock.Unlock()
	return textPath(&c.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 []text.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,
		Bounds:  f.Bounds(sbuf, ppem),
		descent := m.Height - m.Ascent
		if descent > nextLine.Descent {
			nextLine.Descent = descent
		}
		b := f.Bounds(sbuf, ppem)
		nextLine.Bounds = nextLine.Bounds.Union(b)
	}
	var lines []text.Line
	maxDotX := fixed.I(maxWidth)
	type state struct {
		r     rune
		f     *opentype
		adv   fixed.Int26_6
		x     fixed.Int26_6
		idx   int
@@ -123,26 +185,33 @@ func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, f *opentype
	}
	var prev, word state
	endLine := func() {
		line := lineTmpl
		line.Layout = glyphs[:prev.idx:prev.idx]
		line.Len = prev.len
		line.Width = prev.x + prev.adv
		line.Bounds.Max.X += prev.x
		lines = append(lines, line)
		if prev.f != nil {
			updateBounds(prev.f)
		}
		nextLine.Layout = glyphs[:prev.idx:prev.idx]
		nextLine.Len = prev.len
		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]
		a, valid := f.GlyphAdvance(sbuf, ppem, g.Rune)
		next := state{
			r:     g.Rune,
			idx:   prev.idx + 1,
			len:   prev.len + utf8.RuneLen(g.Rune),
			x:     prev.x + prev.adv,
			adv:   a,
			valid: valid,
			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
@@ -153,8 +222,8 @@ func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, f *opentype
			continue
		}
		var k fixed.Int26_6
		if prev.valid {
			k = f.Kern(sbuf, ppem, prev.r, next.r)
		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 {
@@ -181,7 +250,7 @@ func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, f *opentype
	return lines, nil
}

func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, str []text.Glyph) op.CallOp {
func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, fonts []*opentype, str []text.Glyph) op.CallOp {
	var lastPos f32.Point
	var builder clip.Path
	ops := new(op.Ops)
@@ -190,6 +259,10 @@ func textPath(buf *sfnt.Buffer, ppem fixed.Int26_6, f *opentype, str []text.Glyp
	builder.Begin(ops)
	for _, g := range str {
		if !unicode.IsSpace(g.Rune) {
			f := fontForGlyph(buf, fonts, g.Rune)
			if f == nil {
				continue
			}
			segs, ok := f.LoadGlyph(buf, ppem, g.Rune)
			if !ok {
				continue
@@ -271,6 +344,11 @@ func readGlyphs(r io.Reader) ([]text.Glyph, error) {
	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 {
-- 
2.27.0