From a6afa86d853e3dfe7da320e87dea823d271b790c Mon Sep 17 00:00:00 2001 From: tainted-bit Date: Tue, 7 Jul 2020 03:17:44 -0400 Subject: [PATCH] font/opentype: support using Collection as a Face 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 --- font/opentype/opentype.go | 136 ++++++++++++++++++++++++++++---------- 1 file changed, 101 insertions(+), 35 deletions(-) diff --git a/font/opentype/opentype.go b/font/opentype/opentype.go index 568faee8..c509c8d3 100644 --- a/font/opentype/opentype.go +++ b/font/opentype/opentype.go @@ -6,7 +6,6 @@ package opentype import ( "io" - "unicode" "unicode/utf8" @@ -25,9 +24,11 @@ type Font struct { font *sfnt.Font } -// 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 } type opentype struct { @@ -55,7 +56,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 +69,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,13 +105,14 @@ func (f *Font) Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]text.L if err != nil { return nil, err } + fonts := []*opentype{{Font: f.font, Hinting: font.HintingFull}} var buf sfnt.Buffer - return layoutText(&buf, ppem, maxWidth, &opentype{Font: f.font, Hinting: font.HintingFull}, glyphs) + return layoutText(&buf, ppem, maxWidth, fonts, glyphs) } func (f *Font) Shape(ppem fixed.Int26_6, str []text.Glyph) op.CallOp { var buf sfnt.Buffer - return textPath(&buf, ppem, &opentype{Font: f.font, Hinting: font.HintingFull}, str) + return textPath(&buf, ppem, []*opentype{{Font: f.font, Hinting: font.HintingFull}}, str) } func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics { @@ -105,19 +121,53 @@ func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics { return o.Metrics(&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 + } + var buf sfnt.Buffer + return layoutText(&buf, ppem, maxWidth, c.fonts, glyphs) +} + +func (c *Collection) Shape(ppem fixed.Int26_6, str []text.Glyph) 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 []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 @@ -126,26 +176,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 @@ -156,8 +213,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 { @@ -184,7 +241,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) @@ -193,6 +250,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 @@ -274,6 +335,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 {