mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-05 17:35:36 +00:00
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:
+101
-35
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user