package fontface
import (
"bytes"
"crypto/sha256"
"errors"
"fmt"
"image/color"
"math"
gotextfont "github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/font/opentype/tables"
"golang.org/x/image/font/opentype"
"golang.org/x/image/font/sfnt"
"golang.org/x/image/math/fixed"
)
const (
maxParsedFontFaces = 256
maxCOLR0GlyphLayers = 1_024
)
var errShapingParserPanic = errors .New ("shaping font parser panicked" )
var errColorGlyphLookupPanic = errors .New ("color glyph table lookup panicked" )
type shapingCollectionParser func (gotextfont .Resource ) ([]*gotextfont .Face , error )
type FaceCountLimitError struct {
Count int
Limit int
}
func (e *FaceCountLimitError ) Error () string {
return fmt .Sprintf ("font collection face count %d exceeds limit %d" , e .Count , e .Limit )
}
type Collection struct {
outline *opentype .Collection
shaping []*gotextfont .Face
digest [sha256 .Size ]byte
}
type parsedFaceSource struct {
digest [sha256 .Size ]byte
outline *sfnt .Font
shaping *gotextfont .Face
shapingFont *gotextfont .Font
bundledOutline bool
}
type ParsedFace struct {
Outline *sfnt .Font
Shaping *gotextfont .Face
source parsedFaceSource
}
type BundledFaceSource struct {
face *ParsedFace
}
type BundledNotoColorEmojiSource = BundledFaceSource
func (s *BundledFaceSource ) parsed () (*ParsedFace , error ) {
if s == nil || s .face == nil || !s .face .hasParsedSourceComponents () {
return nil , fmt .Errorf ("bundled font clone source is unavailable" )
}
return s .face , nil
}
func (s *BundledFaceSource ) Outline () (*sfnt .Font , error ) {
face , err := s .parsed ()
if err != nil {
return nil , err
}
return face .Outline , nil
}
func (s *BundledFaceSource ) IsBundledNotoColorEmoji () bool {
face , err := s .parsed ()
return err == nil && face .IsBundledNotoColorEmoji ()
}
func (s *BundledFaceSource ) COLR0GlyphLayers (glyphID uint32 ) ([]ColorGlyphLayer , bool , error ) {
face , err := s .parsed ()
if err != nil {
return nil , false , err
}
return face .COLR0GlyphLayers (glyphID )
}
func (s *BundledFaceSource ) CompileBundledNotoColorEmojiCOLRv1Plan (glyphID uint32 ) (*COLRv1Plan , bool , error ) {
face , err := s .parsed ()
if err != nil {
return nil , false , err
}
return face .CompileBundledNotoColorEmojiCOLRv1Plan (glyphID )
}
func (s *BundledFaceSource ) GlyphRenderBounds (glyphID uint32 , size fixed .Int26_6 ) (fixed .Rectangle26_6 , bool , error ) {
face , err := s .parsed ()
if err != nil {
return fixed .Rectangle26_6 {}, false , err
}
return face .GlyphRenderBounds (glyphID , size )
}
func (s *BundledFaceSource ) GlyphDataKind (glyphID uint32 ) (string , error ) {
face , err := s .parsed ()
if err != nil {
return "" , err
}
return glyphDataKind (face .Shaping .GlyphData (gotextfont .GID (glyphID ))), nil
}
func glyphDataKind(data gotextfont .GlyphData ) string {
switch data .(type ) {
case gotextfont .GlyphOutline :
return "outline"
case gotextfont .GlyphColor :
return "color"
case gotextfont .GlyphBitmap :
return "bitmap"
case gotextfont .GlyphSVG :
return "SVG"
case nil :
return "missing"
default :
return "non-outline"
}
}
func (s *BundledFaceSource ) CloneReadOnly () (*ParsedFace , error ) {
face , err := s .parsed ()
if err != nil {
return nil , err
}
clone := new (ParsedFace )
if err := face .CloneInto (clone ); err != nil {
return nil , err
}
return clone , nil
}
func (s *BundledFaceSource ) CloneReadOnlyInto (clone *ParsedFace ) error {
face , err := s .parsed ()
if err != nil {
return err
}
return face .CloneInto (clone )
}
func (s *BundledFaceSource ) SupportsRenderableRune (value rune ) (bool , error ) {
face , err := s .parsed ()
if err != nil {
return false , err
}
shapingGlyph , ok := face .Shaping .Font .NominalGlyph (value )
if !ok || shapingGlyph == 0 {
return false , nil
}
var buffer sfnt .Buffer
outlineGlyph , err := face .Outline .GlyphIndex (&buffer , value )
if err != nil || outlineGlyph == 0 || uint32 (outlineGlyph ) != uint32 (shapingGlyph ) {
return false , nil
}
if !face .hasParsedSource (bundledNotoColorEmojiCOLRv1SHA256 ) {
return true , nil
}
plan , found , err := face .CompileBundledNotoColorEmojiCOLRv1Plan (uint32 (outlineGlyph ))
if err != nil {
return false , err
}
if found {
return plan .Clip != nil && plan .Clip .XMin < plan .Clip .XMax && plan .Clip .YMin < plan .Clip .YMax , nil
}
layers , colorGlyph , err := face .COLR0GlyphLayers (uint32 (outlineGlyph ))
if err != nil {
return false , err
}
if colorGlyph {
for _ , layer := range layers {
if _ , err := face .Outline .LoadGlyph (&buffer , sfnt .GlyphIndex (layer .GlyphID ), fixed .I (1_000 ), nil ); err != nil {
return false , fmt .Errorf ("load COLRv0 layer glyph %d: %w" , layer .GlyphID , err )
}
}
return true , nil
}
if _ , err := face .Outline .LoadGlyph (&buffer , outlineGlyph , fixed .I (1_000 ), nil ); err != nil {
return false , nil
}
return true , nil
}
type ColorGlyphLayer struct {
GlyphID uint32
Color color .NRGBA
Foreground bool
}
func parseFaceCollection(data []byte ) (*Collection , error ) {
return ParseFaceCollectionWithLimit (data , maxParsedFontFaces )
}
func ParseFaceCollectionWithLimit (data []byte , maxFaces int ) (*Collection , error ) {
maxFaces = min (maxFaces , maxParsedFontFaces )
return parseFaceCollectionWithLimitUsingParser (data , maxFaces , gotextfont .ParseTTC )
}
func parseFaceCollectionWithLimitUsingParser(data []byte , maxFaces int , parseShaping shapingCollectionParser ) (*Collection , error ) {
collection , err := parseFaceCollectionWithLimitUsingParserAndDigest (data , maxFaces , parseShaping , [sha256 .Size ]byte {})
if err != nil {
return nil , err
}
if len (data ) == bundledNotoColorEmojiCOLRv1Size {
collection .digest = sha256 .Sum256 (data )
}
return collection , nil
}
func parseFaceCollectionWithLimitUsingParserAndDigest(data []byte , maxFaces int , parseShaping shapingCollectionParser , digest [sha256 .Size ]byte ) (*Collection , error ) {
if len (data ) == 0 {
return nil , fmt .Errorf ("empty font data" )
}
if maxFaces <= 0 {
return nil , fmt .Errorf ("font face limit must be positive" )
}
outline , err := opentype .ParseCollection (data )
if err != nil {
return nil , fmt .Errorf ("parse TrueType/OpenType font: %w" , err )
}
if outline .NumFonts () > maxFaces {
return nil , &FaceCountLimitError {Count : outline .NumFonts (), Limit : maxFaces }
}
shaping , err := parseShapingCollection (bytes .NewReader (data ), parseShaping )
if err != nil {
return nil , fmt .Errorf ("parse shaping font: %w" , err )
}
if outline .NumFonts () != len (shaping ) {
return nil , fmt .Errorf ("font parser face-count mismatch: outline=%d shaping=%d" , outline .NumFonts (), len (shaping ))
}
return &Collection {outline : outline , shaping : shaping , digest : digest }, nil
}
func parseShapingCollection(resource gotextfont .Resource , parse shapingCollectionParser ) (faces []*gotextfont .Face , err error ) {
if parse == nil {
return nil , fmt .Errorf ("nil shaping font parser" )
}
defer func () {
if recover () != nil {
faces = nil
err = errShapingParserPanic
}
}()
return parse (resource )
}
func (c *Collection ) NumFaces () int {
if c == nil {
return 0
}
return len (c .shaping )
}
func (c *Collection ) Face (index int ) (*ParsedFace , error ) {
if c == nil || index < 0 || index >= c .NumFaces () {
return nil , fmt .Errorf ("load font face %d: collection has %d faces" , index , c .NumFaces ())
}
outline , err := c .outline .Font (index )
if err != nil {
return nil , fmt .Errorf ("load outline font face %d: %w" , index , err )
}
return newParsedFace (outline , c .shaping [index ], c .digest ), nil
}
func newParsedFace(outline *sfnt .Font , shaping *gotextfont .Face , digest [sha256 .Size ]byte ) *ParsedFace {
face := &ParsedFace {Outline : outline , Shaping : shaping }
if outline != nil && shaping != nil && shaping .Font != nil {
face .source = parsedFaceSource {digest : digest , outline : outline , shaping : shaping , shapingFont : shaping .Font }
}
return face
}
func (f *ParsedFace ) Clone () (*ParsedFace , error ) {
if f == nil || f .Outline == nil || f .Shaping == nil || f .Shaping .Font == nil {
return nil , fmt .Errorf ("nil parsed font face" )
}
clone := new (ParsedFace )
if err := f .CloneInto (clone ); err != nil {
return nil , err
}
return clone , nil
}
func (f *ParsedFace ) CloneInto (clone *ParsedFace ) error {
if f == nil || f .Outline == nil || f .Shaping == nil || f .Shaping .Font == nil {
return fmt .Errorf ("nil parsed font face" )
}
if clone == nil {
return fmt .Errorf ("nil parsed font face clone destination" )
}
outline , shapingFont := f .Outline , f .Shaping .Font
digest , bundledOutline , sourced := f .source .digest , f .source .bundledOutline , f .hasParsedSourceComponents ()
shaping := gotextfont .NewFace (shapingFont )
result := ParsedFace {Outline : outline , Shaping : shaping }
if sourced {
result .source = parsedFaceSource {
digest : digest , outline : outline , shaping : shaping , shapingFont : shapingFont , bundledOutline : bundledOutline ,
}
}
*clone = result
return nil
}
func (f *ParsedFace ) hasParsedSourceComponents () bool {
return f != nil && f .source .outline != nil && f .Outline == f .source .outline && f .Shaping == f .source .shaping && f .Shaping != nil && f .Shaping .Font == f .source .shapingFont
}
func (f *ParsedFace ) hasParsedSource (digest [sha256 .Size ]byte ) bool {
return digest != ([sha256 .Size ]byte {}) && f .hasParsedSourceComponents () && f .source .digest == digest
}
func (f *ParsedFace ) IsBundledNotoColorEmoji () bool {
return f .hasParsedSource (bundledNotoColorEmojiCOLRv1SHA256 )
}
func ParseFace (data []byte , faceIndex uint16 ) (*ParsedFace , error ) {
collection , err := parseFaceCollection (data )
if err != nil {
return nil , err
}
face , err := collection .Face (int (faceIndex ))
if err != nil {
return nil , err
}
if face .IsBundledNotoColorEmoji () {
if _ , err := registerAuthenticatedBundledNotoColorEmoji (data , collection .digest ); err != nil {
return nil , err
}
}
return face , nil
}
func (f *ParsedFace ) SupportsRenderableRune (value rune ) (bool , error ) {
if f == nil || f .Outline == nil || f .Shaping == nil {
return false , fmt .Errorf ("nil parsed font face" )
}
if f .hasParsedSourceComponents () && f .source .bundledOutline {
shapingGlyph , ok := f .Shaping .Font .NominalGlyph (value )
if !ok || shapingGlyph == 0 {
return false , nil
}
var buffer sfnt .Buffer
outlineGlyph , err := f .Outline .GlyphIndex (&buffer , value )
return err == nil && outlineGlyph != 0 && uint32 (outlineGlyph ) == uint32 (shapingGlyph ), nil
}
shapingGlyph , ok := f .Shaping .NominalGlyph (value )
if !ok || shapingGlyph == 0 {
return false , nil
}
var buffer sfnt .Buffer
outlineGlyph , err := f .Outline .GlyphIndex (&buffer , value )
if err != nil || outlineGlyph == 0 || uint32 (outlineGlyph ) != uint32 (shapingGlyph ) {
return false , nil
}
plan , trusted , err := f .trustedCOLRv1Plan (uint32 (outlineGlyph ))
if err != nil {
return false , err
}
if trusted {
return plan .Clip != nil && plan .Clip .XMin < plan .Clip .XMax && plan .Clip .YMin < plan .Clip .YMax , nil
}
return supportsRenderableRune (f .Outline , f .Shaping , value )
}
func (f *ParsedFace ) trustedCOLRv1Plan (glyphID uint32 ) (*COLRv1Plan , bool , error ) {
if !f .hasParsedSource (bundledNotoColorEmojiCOLRv1SHA256 ) {
return nil , false , nil
}
return f .CompileBundledNotoColorEmojiCOLRv1Plan (glyphID )
}
func supportsRenderableRune(outline *sfnt .Font , shaping *gotextfont .Face , value rune ) (bool , error ) {
if outline == nil || shaping == nil {
return false , fmt .Errorf ("nil outline or shaping face" )
}
shapingGlyph , ok := shaping .NominalGlyph (value )
if !ok || shapingGlyph == 0 {
return false , nil
}
var buffer sfnt .Buffer
outlineGlyph , err := outline .GlyphIndex (&buffer , value )
if err != nil || outlineGlyph == 0 || uint32 (outlineGlyph ) != uint32 (shapingGlyph ) {
return false , nil
}
face := &ParsedFace {Outline : outline , Shaping : shaping }
layers , colorGlyph , err := face .COLR0GlyphLayers (uint32 (outlineGlyph ))
if err != nil {
return false , err
}
if colorGlyph {
for _ , layer := range layers {
if _ , err := outline .LoadGlyph (&buffer , sfnt .GlyphIndex (layer .GlyphID ), fixed .I (1_000 ), nil ); err != nil {
return false , fmt .Errorf ("load COLRv0 layer glyph %d: %w" , layer .GlyphID , err )
}
}
return true , nil
}
if _ , err := outline .LoadGlyph (&buffer , outlineGlyph , fixed .I (1_000 ), nil ); err != nil {
return false , nil
}
return true , nil
}
func (f *ParsedFace ) COLR0GlyphLayers (glyphID uint32 ) ([]ColorGlyphLayer , bool , error ) {
if f == nil || f .Outline == nil || f .Shaping == nil {
return nil , false , fmt .Errorf ("nil parsed font face" )
}
if glyphID == 0 || glyphID > math .MaxUint16 || int (glyphID ) >= f .Outline .NumGlyphs () {
return nil , false , fmt .Errorf ("glyph ID %d is out of range" , glyphID )
}
if f .Shaping .COLR == nil {
return nil , false , nil
}
paint , ok , err := searchCOLRPaint (f .Shaping .COLR , tables .GlyphID (glyphID ))
if err != nil {
return nil , false , err
}
if !ok {
return nil , false , nil
}
return f .resolveColorPaint (glyphID , paint )
}
func (f *ParsedFace ) resolveColorPaint (glyphID uint32 , paint tables .PaintTable ) ([]ColorGlyphLayer , bool , error ) {
layers , ok := paint .(tables .PaintColrLayersResolved )
if !ok {
var buffer sfnt .Buffer
segments , err := f .Outline .LoadGlyph (&buffer , sfnt .GlyphIndex (glyphID ), fixed .I (1_000 ), nil )
if err != nil {
return nil , false , fmt .Errorf ("load fallback outline for color glyph %d: %w" , glyphID , err )
}
if len (segments ) == 0 {
return nil , false , fmt .Errorf ("color glyph %d uses unsupported COLRv1 paint without an outline fallback" , glyphID )
}
return nil , false , nil
}
if len (layers ) == 0 {
return nil , true , fmt .Errorf ("COLRv0 glyph %d has no layers" , glyphID )
}
if len (f .Shaping .CPAL ) == 0 {
return nil , true , fmt .Errorf ("COLRv0 glyph %d has no CPAL palette" , glyphID )
}
validated , err := validateCOLR0Layers (layers , f .Shaping .CPAL [0 ], f .Outline .NumGlyphs ())
if err != nil {
return nil , true , fmt .Errorf ("COLRv0 glyph %d: %w" , glyphID , err )
}
return validated , true , nil
}
func searchCOLRPaint(colr *tables .COLR1 , glyphID tables .GlyphID ) (paint tables .PaintTable , found bool , err error ) {
if colr == nil {
return nil , false , nil
}
defer func () {
if recover () != nil {
paint = nil
found = false
err = errColorGlyphLookupPanic
}
}()
paint , found = colr .Search (glyphID )
return paint , found , nil
}
func (f *ParsedFace ) GlyphRenderBounds (glyphID uint32 , size fixed .Int26_6 ) (fixed .Rectangle26_6 , bool , error ) {
if f == nil || f .Outline == nil || f .Shaping == nil {
return fixed .Rectangle26_6 {}, false , fmt .Errorf ("nil parsed font face" )
}
if size <= 0 {
return fixed .Rectangle26_6 {}, false , fmt .Errorf ("glyph size must be positive" )
}
plan , trusted , err := f .trustedCOLRv1Plan (glyphID )
if err != nil {
return fixed .Rectangle26_6 {}, false , err
}
if trusted {
if plan .Clip == nil {
return fixed .Rectangle26_6 {}, false , fmt .Errorf ("trusted COLRv1 glyph %d has no static clip box" , glyphID )
}
return colrv1ClipBounds (plan .Clip , f .Outline .UnitsPerEm (), size )
}
layers , colorGlyph , err := f .COLR0GlyphLayers (glyphID )
if err != nil {
return fixed .Rectangle26_6 {}, false , err
}
if !colorGlyph {
var buffer sfnt .Buffer
segments , err := f .Outline .LoadGlyph (&buffer , sfnt .GlyphIndex (glyphID ), size , nil )
if err != nil {
return fixed .Rectangle26_6 {}, false , fmt .Errorf ("load outline glyph %d: %w" , glyphID , err )
}
return segments .Bounds (), len (segments ) != 0 , nil
}
var buffer sfnt .Buffer
var bounds fixed .Rectangle26_6
hasInk := false
for _ , layer := range layers {
segments , err := f .Outline .LoadGlyph (&buffer , sfnt .GlyphIndex (layer .GlyphID ), size , nil )
if err != nil {
return fixed .Rectangle26_6 {}, false , fmt .Errorf ("load COLRv0 layer glyph %d: %w" , layer .GlyphID , err )
}
if len (segments ) == 0 {
continue
}
if hasInk {
bounds = bounds .Union (segments .Bounds ())
} else {
bounds = segments .Bounds ()
hasInk = true
}
}
return bounds , hasInk , nil
}
func colrv1ClipBounds(clip *COLRv1ClipBox , unitsPerEm sfnt .Units , size fixed .Int26_6 ) (fixed .Rectangle26_6 , bool , error ) {
if clip == nil || unitsPerEm == 0 {
return fixed .Rectangle26_6 {}, false , fmt .Errorf ("invalid COLRv1 clip bounds" )
}
if !finiteCOLRv1 (clip .XMin ) || !finiteCOLRv1 (clip .YMin ) || !finiteCOLRv1 (clip .XMax ) || !finiteCOLRv1 (clip .YMax ) || clip .XMin >= clip .XMax || clip .YMin >= clip .YMax {
return fixed .Rectangle26_6 {}, false , fmt .Errorf ("invalid COLRv1 clip bounds" )
}
scale := float64 (size ) / float64 (unitsPerEm )
convert := func (value float64 ) (fixed .Int26_6 , error ) {
value *= scale
if !finiteCOLRv1 (value ) || value > float64 (math .MaxInt32 ) || value < float64 (math .MinInt32 ) {
return 0 , fmt .Errorf ("COLRv1 clip bounds exceed fixed-point range" )
}
return fixed .Int26_6 (math .Round (value )), nil
}
xMin , err := convert (clip .XMin )
if err != nil {
return fixed .Rectangle26_6 {}, false , err
}
yMin , err := convert (-clip .YMax )
if err != nil {
return fixed .Rectangle26_6 {}, false , err
}
xMax , err := convert (clip .XMax )
if err != nil {
return fixed .Rectangle26_6 {}, false , err
}
yMax , err := convert (-clip .YMin )
if err != nil {
return fixed .Rectangle26_6 {}, false , err
}
if xMin >= xMax || yMin >= yMax {
return fixed .Rectangle26_6 {}, false , fmt .Errorf ("COLRv1 clip bounds collapse at requested size" )
}
return fixed .Rectangle26_6 {Min : fixed .Point26_6 {X : xMin , Y : yMin }, Max : fixed .Point26_6 {X : xMax , Y : yMax }}, true , nil
}
func finiteCOLRv1(value float64 ) bool { return !math .IsNaN (value ) && !math .IsInf (value , 0 ) }
func validateCOLR0Layers(layers []tables .Layer , palette []tables .ColorRecord , numGlyphs int ) ([]ColorGlyphLayer , error ) {
if numGlyphs <= 0 {
return nil , fmt .Errorf ("font has no glyphs" )
}
if len (layers ) == 0 {
return nil , fmt .Errorf ("glyph has no layers" )
}
if len (layers ) > maxCOLR0GlyphLayers {
return nil , fmt .Errorf ("layer count %d exceeds limit %d" , len (layers ), maxCOLR0GlyphLayers )
}
validated := make ([]ColorGlyphLayer , 0 , len (layers ))
for index , layer := range layers {
if int (layer .GlyphID ) >= numGlyphs {
return nil , fmt .Errorf ("layer %d glyph ID %d is out of range" , index , layer .GlyphID )
}
resolved := ColorGlyphLayer {GlyphID : uint32 (layer .GlyphID )}
if layer .PaletteIndex == math .MaxUint16 {
resolved .Foreground = true
} else {
if int (layer .PaletteIndex ) >= len (palette ) {
return nil , fmt .Errorf ("layer %d palette index %d is out of range" , index , layer .PaletteIndex )
}
entry := palette [layer .PaletteIndex ]
resolved .Color = color .NRGBA {R : entry .Red , G : entry .Green , B : entry .Blue , A : entry .Alpha }
}
validated = append (validated , resolved )
}
return validated , nil
}
The pages are generated with Golds v0.8.7 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .