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 <sourcehut@taintedbit.com>
This commit is contained in:
tainted-bit
2020-07-07 03:17:44 -04:00
committed by Elias Naur
parent 7bbe0da0c7
commit a6afa86d85
+101 -35
View File
@@ -6,7 +6,6 @@ package opentype
import ( import (
"io" "io"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
@@ -25,9 +24,11 @@ type Font struct {
font *sfnt.Font 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 { type Collection struct {
coll *sfnt.Collection fonts []*opentype
} }
type opentype struct { type opentype struct {
@@ -55,7 +56,7 @@ func ParseCollection(src []byte) (*Collection, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Collection{c}, nil return newCollectionFrom(c)
} }
// ParseCollectionReaderAt parses an SFNT collection, such as TTC or OTC data, // 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 { if err != nil {
return nil, err 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. // NumFonts returns the number of fonts in the collection.
func (c *Collection) NumFonts() int { func (c *Collection) NumFonts() int {
return c.coll.NumFonts() return len(c.fonts)
} }
// Font returns the i'th font in the collection. // Font returns the i'th font in the collection.
func (c *Collection) Font(i int) (*Font, error) { func (c *Collection) Font(i int) (*Font, error) {
fnt, err := c.coll.Font(i) if i < 0 || len(c.fonts) <= i {
if err != nil { return nil, sfnt.ErrNotFound
return nil, err
} }
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) { 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 { if err != nil {
return nil, err return nil, err
} }
fonts := []*opentype{{Font: f.font, Hinting: font.HintingFull}}
var buf sfnt.Buffer 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 { func (f *Font) Shape(ppem fixed.Int26_6, str []text.Glyph) op.CallOp {
var buf sfnt.Buffer 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 { 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) return o.Metrics(&buf, ppem)
} }
func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, f *opentype, glyphs []text.Glyph) ([]text.Line, error) { func (c *Collection) Layout(ppem fixed.Int26_6, maxWidth int, txt io.Reader) ([]text.Line, error) {
m := f.Metrics(sbuf, ppem) glyphs, err := readGlyphs(txt)
lineTmpl := text.Line{ if err != nil {
Ascent: m.Ascent, 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. // m.Height is equal to m.Ascent + m.Descent + linegap.
// Compute the descent including the linegap. // Compute the descent including the linegap.
Descent: m.Height - m.Ascent, descent := m.Height - m.Ascent
Bounds: f.Bounds(sbuf, ppem), 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) maxDotX := fixed.I(maxWidth)
type state struct { type state struct {
r rune r rune
f *opentype
adv fixed.Int26_6 adv fixed.Int26_6
x fixed.Int26_6 x fixed.Int26_6
idx int idx int
@@ -126,26 +176,33 @@ func layoutText(sbuf *sfnt.Buffer, ppem fixed.Int26_6, maxWidth int, f *opentype
} }
var prev, word state var prev, word state
endLine := func() { endLine := func() {
line := lineTmpl if prev.f != nil {
line.Layout = glyphs[:prev.idx:prev.idx] updateBounds(prev.f)
line.Len = prev.len }
line.Width = prev.x + prev.adv nextLine.Layout = glyphs[:prev.idx:prev.idx]
line.Bounds.Max.X += prev.x nextLine.Len = prev.len
lines = append(lines, line) nextLine.Width = prev.x + prev.adv
nextLine.Bounds.Max.X += prev.x
lines = append(lines, nextLine)
glyphs = glyphs[prev.idx:] glyphs = glyphs[prev.idx:]
nextLine = text.Line{}
prev = state{} prev = state{}
word = state{} word = state{}
} }
for prev.idx < len(glyphs) { for prev.idx < len(glyphs) {
g := &glyphs[prev.idx] g := &glyphs[prev.idx]
a, valid := f.GlyphAdvance(sbuf, ppem, g.Rune)
next := state{ next := state{
r: g.Rune, r: g.Rune,
idx: prev.idx + 1, f: fontForGlyph(sbuf, fonts, g.Rune),
len: prev.len + utf8.RuneLen(g.Rune), idx: prev.idx + 1,
x: prev.x + prev.adv, len: prev.len + utf8.RuneLen(g.Rune),
adv: a, x: prev.x + prev.adv,
valid: valid, }
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' { if g.Rune == '\n' {
// The newline is zero width; use the previous // 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 continue
} }
var k fixed.Int26_6 var k fixed.Int26_6
if prev.valid { if prev.valid && next.f != nil {
k = f.Kern(sbuf, ppem, prev.r, next.r) k = next.f.Kern(sbuf, ppem, prev.r, next.r)
} }
// Break the line if we're out of space. // Break the line if we're out of space.
if prev.idx > 0 && next.x+next.adv+k > maxDotX { 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 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 lastPos f32.Point
var builder clip.Path var builder clip.Path
ops := new(op.Ops) 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) builder.Begin(ops)
for _, g := range str { for _, g := range str {
if !unicode.IsSpace(g.Rune) { if !unicode.IsSpace(g.Rune) {
f := fontForGlyph(buf, fonts, g.Rune)
if f == nil {
continue
}
segs, ok := f.LoadGlyph(buf, ppem, g.Rune) segs, ok := f.LoadGlyph(buf, ppem, g.Rune)
if !ok { if !ok {
continue continue
@@ -274,6 +335,11 @@ func readGlyphs(r io.Reader) ([]text.Glyph, error) {
return glyphs, nil 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) { 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) g, err := f.Font.GlyphIndex(buf, r)
if err != nil { if err != nil {