~eliasnaur/gio-patches

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

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

 1 files changed, 156 insertions(+), 23 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/11340/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH] 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>
---
 font/opentype/opentype.go | 179 +++++++++++++++++++++++++++++++++-----
 1 file changed, 156 insertions(+), 23 deletions(-)

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

import (
	"image"
	"io"

	"unicode"
@@ -25,9 +26,13 @@ type Font struct {
	buf  sfnt.Buffer
}

// 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
	ppemExtremes map[fixed.Int26_6]extremities
}

type opentype struct {
@@ -35,6 +40,11 @@ type opentype struct {
	Hinting font.Hinting
}

type extremities struct {
	metrics font.Metrics
	bounds  fixed.Rectangle26_6
}

// NewFont parses an SFNT font, such as TTF or OTF data, from a []byte
// data source.
func Parse(src []byte) (*Font, error) {
@@ -55,7 +65,7 @@ func ParseCollection(src []byte) (*Collection, error) {
	if err != nil {
		return nil, err
	}
	return &Collection{c}, nil
	return newCollectionFrom(c), nil
}

// ParseCollectionReaderAt parses an SFNT collection, such as TTC or OTC data,
@@ -68,21 +78,35 @@ func ParseCollectionReaderAt(src io.ReaderAt) (*Collection, error) {
	if err != nil {
		return nil, err
	}
	return &Collection{c}, nil
	return newCollectionFrom(c), nil
}

func newCollectionFrom(coll *sfnt.Collection) *Collection {
	fonts := make([]*opentype, coll.NumFonts())
	for i := range fonts {
		fnt, _ := coll.Font(i)
		fonts[i] = &opentype{
			Font:    fnt,
			Hinting: font.HintingFull,
		}
	}
	return &Collection{
		fonts:        fonts,
		ppemExtremes: make(map[fixed.Int26_6]extremities),
	}
}

// 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,11 +114,14 @@ 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}}
	m := fonts[0].Metrics(&f.buf, ppem)
	bounds := fonts[0].Bounds(&f.buf, ppem)
	return layoutText(&f.buf, ppem, maxWidth, fonts, m, bounds, 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)
	return textPath(&f.buf, ppem, []*opentype{{Font: f.font, Hinting: font.HintingFull}}, str)
}

func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics {
@@ -102,14 +129,110 @@ func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics {
	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)
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
	}
	ext := c.extremities(ppem)
	return layoutText(&c.buf, ppem, maxWidth, c.fonts, ext.metrics, ext.bounds, glyphs)
}

func (c *Collection) Shape(ppem fixed.Int26_6, str []text.Glyph) op.CallOp {
	return textPath(&c.buf, ppem, c.fonts, str)
}

func (c *Collection) Metrics(ppem fixed.Int26_6) font.Metrics {
	return c.extremities(ppem).metrics
}

func (c *Collection) extremities(ppem fixed.Int26_6) extremities {
	ext, ok := c.ppemExtremes[ppem]
	if !ok {
		if len(c.fonts) > 0 {
			m := c.fonts[0].Metrics(&c.buf, ppem)
			b := c.fonts[0].Bounds(&c.buf, ppem)
			for i := 1; i < len(c.fonts); i++ {
				m2 := c.fonts[i].Metrics(&c.buf, ppem)
				b2 := c.fonts[i].Bounds(&c.buf, ppem)

				if m.Height < m2.Height {
					m.Height = m2.Height
				}
				if m.Ascent < m2.Ascent {
					m.Ascent = m2.Ascent
				}
				if m.Descent < m2.Descent {
					m.Descent = m2.Descent
				}
				if m.XHeight < m2.XHeight {
					m.XHeight = m2.XHeight
				}
				if m.CapHeight < m2.CapHeight {
					m.CapHeight = m2.CapHeight
				}
				if c.slopeGreater(m2.CaretSlope, m.CaretSlope) {
					m.CaretSlope = m2.CaretSlope
				}

				b.Union(b2)
			}
			ext = extremities{
				metrics: m,
				bounds:  b,
			}
		}
		c.ppemExtremes[ppem] = ext
	}
	return ext
}

func (c *Collection) slopeGreater(p1, p2 image.Point) bool {
	// This algorithm arbitrarily chooses the slope with the greater absolute X or Y value.
	// If caret slopes do not match in the collection, then the caller should expect some weirdness.
	absX1 := p1.X
	if absX1 < 0 {
		absX1 = -absX1
	}
	absX2 := p2.X
	if absX2 < 0 {
		absX2 = -absX2
	}
	if absX1 > absX2 {
		return true
	} else if absX1 < absX2 {
		return false
	}
	absY1 := p1.Y
	if absY1 < 0 {
		absY1 = -absY1
	}
	absY2 := p2.Y
	if absY2 < 0 {
		absY2 = -absY2
	}
	return absY1 > absY2
}

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, m font.Metrics, bounds fixed.Rectangle26_6, glyphs []text.Glyph) ([]text.Line, error) {
	lineTmpl := text.Line{
		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),
		Bounds:  bounds,
	}
	var lines []text.Line
	maxDotX := fixed.I(maxWidth)
@@ -135,14 +258,15 @@ func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, f *opentype
	}
	for prev.idx < len(glyphs) {
		g := &glyphs[prev.idx]
		a, valid := f.GlyphAdvance(sbuf, ppem, g.Rune)
		f := fontForGlyph(sbuf, fonts, 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,
			idx: prev.idx + 1,
			len: prev.len + utf8.RuneLen(g.Rune),
			x:   prev.x + prev.adv,
		}
		if f != nil {
			next.adv, next.valid = f.GlyphAdvance(sbuf, ppem, g.Rune)
		}
		if g.Rune == '\n' {
			// The newline is zero width; use the previous
@@ -153,7 +277,7 @@ func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, f *opentype
			continue
		}
		var k fixed.Int26_6
		if prev.valid {
		if prev.valid && f != nil {
			k = f.Kern(sbuf, ppem, prev.r, next.r)
		}
		// Break the line if we're out of space.
@@ -181,7 +305,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 +314,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 +399,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