Skip to content

Commit f2aace7

Browse files
committed
[font] expose COLR API
1 parent 6235224 commit f2aace7

File tree

11 files changed

+243
-92
lines changed

11 files changed

+243
-92
lines changed

font/color.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package font
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/go-text/typesetting/font/opentype/tables"
8+
)
9+
10+
// Support for COLR and CPAL tables
11+
12+
// CPAL is the 'CPAL' table,
13+
// with [numPalettes]x[numPaletteEntries] colors.
14+
// CPAL[0] is the default palette
15+
type CPAL [][]tables.ColorRecord
16+
17+
func newCPAL(table tables.CPAL) (CPAL, error) {
18+
numPalettes := len(table.ColorRecordIndices)
19+
numColors := len(table.ColorRecordsArray)
20+
21+
// "The first palette, palette index 0, is the default palette.
22+
// A minimum of one palette must be provided in the CPAL table if the table is present.
23+
// Palettes must have a minimum of one color record. An empty CPAL table,
24+
// with no palettes and no color records is not permitted."
25+
if numPalettes == 0 {
26+
return nil, errors.New("empty CPAL table")
27+
}
28+
out := make(CPAL, numPalettes)
29+
for i, startIndex := range table.ColorRecordIndices {
30+
endIndex := int(startIndex) + int(table.NumPaletteEntries)
31+
if endIndex > numColors {
32+
return nil, fmt.Errorf("invalid CPAL table (expected at least %d colors, got %d)", endIndex, numColors)
33+
}
34+
out[i] = table.ColorRecordsArray[startIndex:endIndex]
35+
}
36+
return out, nil
37+
}

font/font.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ type Font struct {
154154
bitmap bitmap
155155
sbix sbix
156156

157+
COLR *tables.COLR1 // color glyphs, optional
158+
CPAL CPAL // color glyphs, optional
159+
157160
os2 os2
158161
names tables.Name
159162
head tables.Head
@@ -267,6 +270,18 @@ func NewFont(ld *ot.Loader) (*Font, error) {
267270
svg, _, _ := tables.ParseSVG(raw)
268271
out.svg, _ = newSvg(svg)
269272

273+
raw, _ = ld.RawTable(ot.MustNewTag("COLR"))
274+
if colr, err := tables.ParseCOLR(raw); err == nil {
275+
out.COLR = &colr
276+
// color table without CPAL is broken
277+
raw, _ = ld.RawTable(ot.MustNewTag("CPAL"))
278+
cpal, _, _ := tables.ParseCPAL(raw)
279+
out.CPAL, err = newCPAL(cpal)
280+
if err != nil {
281+
return nil, err
282+
}
283+
}
284+
270285
out.hhea, out.hmtx, _ = loadHmtx(ld, out.nGlyphs)
271286
out.vhea, out.vmtx, _ = loadVmtx(ld, out.nGlyphs)
272287

font/font_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,15 @@ func TestCapHeight(t *testing.T) {
134134
tu.Assert(t, face.LineMetric(CapHeight) == 730)
135135
tu.Assert(t, face.LineMetric(XHeight) == 520)
136136
}
137+
138+
func TestLoadColor(t *testing.T) {
139+
ld := readFontFile(t, "color/NotoColorEmoji-Regular.ttf")
140+
ft, err := NewFont(ld)
141+
tu.AssertNoErr(t, err)
142+
tu.Assert(t, ft.COLR != nil && ft.CPAL != nil)
143+
144+
ld = readFontFile(t, "color/CoralPixels-Regular.ttf")
145+
ft, err = NewFont(ld)
146+
tu.AssertNoErr(t, err)
147+
tu.Assert(t, ft.COLR != nil && ft.CPAL != nil)
148+
}

font/opentype/tables/glyphs.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,9 @@ type EBLC = CBLC
1515
// Bloc is the bitmap location table
1616
// See - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6bloc.html
1717
type Bloc = CBLC
18+
19+
// PaintColrLayersResolved is a simili PaintTable, build
20+
// from COLR version 0 table.
21+
type PaintColrLayersResolved []Layer
22+
23+
func (PaintColrLayersResolved) isPaintTable() {}

font/opentype/tables/glyphs_color_test.go

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,44 +10,67 @@ func TestCOLR(t *testing.T) {
1010
ft := readFontFile(t, "color/NotoColorEmoji-Regular.ttf")
1111
colr, err := ParseCOLR(readTable(t, ft, "COLR"))
1212
tu.AssertNoErr(t, err)
13-
tu.Assert(t, len(colr.BaseGlyphRecords) == 0)
14-
tu.Assert(t, len(colr.LayerRecords) == 0)
15-
tu.Assert(t, len(colr.BaseGlyphList.PaintRecords) == 3845)
16-
tu.Assert(t, colr.BaseGlyphList.PaintRecords[0].Paint == PaintColrLayers{1, 3, 47625})
17-
tu.Assert(t, colr.ClipList.Clips[0].ClipBox == ClipBoxFormat1{1, 480, 192, 800, 512})
13+
tu.Assert(t, len(colr.baseGlyphRecords) == 0)
14+
tu.Assert(t, len(colr.layerRecords) == 0)
15+
tu.Assert(t, len(colr.baseGlyphList.paintRecords) == 3845)
16+
tu.Assert(t, colr.baseGlyphList.paintRecords[0].Paint == PaintColrLayers{1, 3, 47625})
17+
tu.Assert(t, colr.ClipList.clips[0].ClipBox == ClipBoxFormat1{1, 480, 192, 800, 512})
1818
tu.Assert(t, colr.VarIndexMap == nil && colr.ItemVariationStore == nil)
19-
tu.Assert(t, len(colr.LayerList.PaintTables) == 69264)
19+
tu.Assert(t, len(colr.LayerList.paintTables) == 69264)
20+
21+
clipBox, ok := colr.ClipList.Search(87)
22+
tu.Assert(t, ok && clipBox == ClipBoxFormat1{1, 64, -224, 1216, 928})
2023

2124
// reference from fonttools
22-
paint := colr.LayerList.PaintTables[6]
25+
paint := colr.LayerList.paintTables[6]
2326
transform, ok := paint.(PaintTransform)
2427
tu.Assert(t, ok)
2528
_, innerOK := transform.Paint.(PaintGlyph)
2629
tu.Assert(t, transform.Transform == Affine2x3{1, 0, 0, 1, 4.3119965, 0.375})
2730
tu.Assert(t, innerOK)
2831

32+
pt, ok := colr.Search(12)
33+
asColrLayers, ok2 := pt.(PaintColrLayers)
34+
tu.Assert(t, ok && ok2)
35+
tu.Assert(t, asColrLayers == PaintColrLayers{1, 9, 2427})
36+
37+
for _, paint := range colr.baseGlyphList.paintRecords {
38+
if layers, ok := paint.Paint.(PaintColrLayers); ok {
39+
l, err := colr.LayerList.Resolve(layers)
40+
tu.AssertNoErr(t, err)
41+
tu.Assert(t, len(l) == int(layers.NumLayers))
42+
}
43+
}
44+
2945
ft = readFontFile(t, "color/CoralPixels-Regular.ttf")
3046
colr, err = ParseCOLR(readTable(t, ft, "COLR"))
3147
tu.AssertNoErr(t, err)
32-
tu.Assert(t, len(colr.BaseGlyphRecords) == 335)
33-
tu.Assert(t, len(colr.LayerRecords) == 5603)
34-
g1, g2 := colr.BaseGlyphRecords[0], colr.BaseGlyphRecords[1]
35-
tu.Assert(t, g1 == BaseGlyph{0, 0, 11} && g2 == BaseGlyph{2, 11, 18})
36-
tu.Assert(t, colr.LayerRecords[0].PaletteIndex == 4)
48+
tu.Assert(t, len(colr.baseGlyphRecords) == 335)
49+
tu.Assert(t, len(colr.layerRecords) == 5603)
50+
g1, g2 := colr.baseGlyphRecords[0], colr.baseGlyphRecords[1]
51+
tu.Assert(t, g1 == baseGlyph{0, 0, 11} && g2 == baseGlyph{2, 11, 18})
52+
tu.Assert(t, colr.layerRecords[0].PaletteIndex == 4)
53+
54+
pt, ok = colr.Search(0)
55+
asLayers, ok2 := pt.(PaintColrLayersResolved)
56+
tu.Assert(t, ok && ok2)
57+
tu.Assert(t, len(asLayers) == 11)
58+
tu.Assert(t, asLayers[0].PaletteIndex == 4)
59+
tu.Assert(t, asLayers[10].PaletteIndex == 11)
3760
}
3861

3962
func TestCPAL(t *testing.T) {
4063
ft := readFontFile(t, "color/NotoColorEmoji-Regular.ttf")
4164
cpal, _, err := ParseCPAL(readTable(t, ft, "CPAL"))
4265
tu.AssertNoErr(t, err)
4366
tu.Assert(t, cpal.Version == 0)
44-
tu.Assert(t, cpal.numPaletteEntries == 5921)
67+
tu.Assert(t, cpal.NumPaletteEntries == 5921)
4568
tu.Assert(t, cpal.numPalettes == 1 && len(cpal.ColorRecordIndices) == 1)
4669

4770
ft = readFontFile(t, "color/CoralPixels-Regular.ttf")
4871
cpal, _, err = ParseCPAL(readTable(t, ft, "CPAL"))
4972
tu.AssertNoErr(t, err)
5073
tu.Assert(t, cpal.Version == 0)
51-
tu.Assert(t, cpal.numPaletteEntries == 32)
74+
tu.Assert(t, cpal.NumPaletteEntries == 32)
5275
tu.Assert(t, cpal.numPalettes == 2 && len(cpal.ColorRecordIndices) == 2)
5376
}

0 commit comments

Comments
 (0)