package d2scenebuild
import (
"context"
"fmt"
"math"
"sort"
"strings"
"unicode/utf8"
"github.com/go-text/typesetting/segmenter"
"golang.org/x/image/math/fixed"
"github.com/pancsta/d2/d2renderers/d2fonts"
"github.com/pancsta/d2/d2renderers/d2scene"
"github.com/pancsta/d2/d2renderers/internal/fontface"
"github.com/pancsta/d2/d2renderers/internal/fontfallback"
)
type sceneTextReference struct {
node *d2scene .Node
value d2scene .TextRun
pointer *d2scene .TextRun
missing map [rune ]struct {}
}
type previousShapedText struct {
ids [8 ]d2scene .AssetID
text string
glyphs []d2scene .Glyph
size fixed .Int26_6
coverage int64
runs, idCount int
hasShapedValue bool
}
func (r *sceneTextReference ) setFallbacks (ids []d2scene .AssetID ) {
r .value .Fallbacks = ids
if r .pointer != nil {
r .pointer .Fallbacks = ids
}
}
func (r *sceneTextReference ) setGlyphs (glyphs []d2scene .Glyph ) {
r .value .Glyphs = glyphs
if r .pointer != nil {
r .pointer .Glyphs = glyphs
return
}
r .node .Primitive = r .value
}
type sceneFontCoverage struct {
font *fontface .ParsedFace
source *fontface .BundledFaceSource
runes map [rune ]uint8
}
const (
sceneRuneUnsupported uint8 = 1
sceneRuneSupported uint8 = 2
)
type sceneFontFallbackKey struct {
family string
style string
weight int
}
type sceneFontFallbackBucket struct {
request d2fonts .FallbackRequest
missing map [rune ]struct {}
references []*sceneTextReference
}
const (
defaultFontFallbackMaxRunesPerText = 100_000
defaultFontFallbackMaxTotalRunes = 10_000_000
defaultFontFallbackMaxCoverageChecks = int64 (50_000_000 )
defaultFontShapingMaxFacesPerText = 64
defaultFontShapingMaxRuns = 100_000
defaultFontShapingMaxGlyphs = 10_000_000
maxSceneFontCoverageEntries = 4_096
)
var missingGlyphPlaceholders = [...]rune {'\ufffd' , '\u25a1' , '\u2610' , '?' }
type fontFallbackWorkLimits struct {
maxRunesPerText int
maxTotalRunes int
maxCoverageChecks int64
maxFacesPerText int
maxShapingRuns int
maxShapedGlyphs int
}
func (c sceneFontCoverage ) supports (ctx context .Context , value rune ) (bool , error ) {
if err := ctx .Err (); err != nil {
return false , err
}
if supported := c .runes [value ]; supported != 0 {
return supported == sceneRuneSupported , nil
}
var supported bool
var err error
if c .source != nil {
supported , err = c .source .SupportsRenderableRune (value )
} else {
supported , err = c .font .SupportsRenderableRune (value )
}
if err != nil {
return false , err
}
if len (c .runes ) < maxSceneFontCoverageEntries {
if supported {
c .runes [value ] = sceneRuneSupported
} else {
c .runes [value ] = sceneRuneUnsupported
}
}
return supported , nil
}
func (b *builder ) resolveFontFallbacks (references []*sceneTextReference ) error {
workLimits , err := normalizedFontFallbackWorkLimits (b .options .Fonts )
if err != nil {
return err
}
coverage := make (map [d2scene .AssetID ]sceneFontCoverage )
missingAll := make (map [rune ]struct {})
bucketsByKey := make (map [sceneFontFallbackKey ]*sceneFontFallbackBucket )
var buckets []*sceneFontFallbackBucket
var reusableBundledFallbacks []d2scene .AssetID
reuseResolutionScratch := len (references ) > 1
var runeScratch []rune
supports := func (id d2scene .AssetID , value rune ) (bool , error ) {
if b .fontCoverageChecks >= workLimits .maxCoverageChecks {
return false , fmt .Errorf ("scene: font coverage checks exceed limit %d" , workLimits .maxCoverageChecks )
}
b .fontCoverageChecks ++
face , err := b .fontCoverage (id , coverage )
if err != nil {
return false , err
}
return face .supports (b .ctx , value )
}
for _ , reference := range references {
if len (reference .value .Glyphs ) != 0 || reference .value .Text == "" {
continue
}
if !utf8 .ValidString (reference .value .Text ) {
return fmt .Errorf ("scene: text node %q is not valid UTF-8" , reference .node .ID )
}
if len (reference .value .Fallbacks ) >= workLimits .maxFacesPerText {
return fmt .Errorf ("scene: text node %q fallback font reference count %d exceeds per-text face limit %d" , reference .node .ID , len (reference .value .Fallbacks ), workLimits .maxFacesPerText )
}
fontIDs := make ([]d2scene .AssetID , 0 , 1 +len (reference .value .Fallbacks ))
fontIDs = append (fontIDs , reference .value .Font .Asset )
fontIDs = append (fontIDs , reference .value .Fallbacks ...)
key := sceneFontFallbackKey {
family : reference .value .Font .Family , style : reference .value .Font .Style , weight : reference .value .Font .Weight ,
}
var bucket *sceneFontFallbackBucket
runeCapacity := min (len (reference .value .Text ), workLimits .maxRunesPerText )
runes := runeScratch [:0 ]
if !reuseResolutionScratch || cap (runes ) < runeCapacity {
runes = make ([]rune , 0 , runeCapacity )
}
for _ , value := range reference .value .Text {
if len (runes ) >= workLimits .maxRunesPerText {
return fmt .Errorf ("scene: text node %q rune count exceeds per-text limit %d" , reference .node .ID , workLimits .maxRunesPerText )
}
if b .fontRunes >= workLimits .maxTotalRunes {
return fmt .Errorf ("scene: text rune count exceeds total limit %d" , workLimits .maxTotalRunes )
}
b .fontRunes ++
if b .fontRunes &1023 == 0 {
if err := b .ctx .Err (); err != nil {
return err
}
}
runes = append (runes , value )
}
if reuseResolutionScratch && cap (runes ) <= maxSceneFontCoverageEntries {
runeScratch = runes
} else {
runeScratch = nil
}
var graphemes segmenter .Segmenter
graphemes .Init (runes )
for iterator := graphemes .GraphemeIterator (); iterator .Next (); {
if err := b .ctx .Err (); err != nil {
return err
}
cluster := iterator .Grapheme ()
covered := false
for _ , id := range fontIDs {
faceCoversCluster := true
for _ , value := range cluster .Text {
if fontRuneIsDefaultIgnorable (value ) {
continue
}
supportedByFace , err := supports (id , value )
if err != nil {
return fmt .Errorf ("scene: text node %q: %w" , reference .node .ID , err )
}
if !supportedByFace {
faceCoversCluster = false
break
}
}
if faceCoversCluster {
covered = true
break
}
}
if covered {
continue
}
if reference .missing == nil {
reference .missing = make (map [rune ]struct {})
}
for _ , value := range cluster .Text {
if !fontRuneIsDefaultIgnorable (value ) {
reference .missing [value ] = struct {}{}
missingAll [value ] = struct {}{}
if bucket == nil {
bucket = bucketsByKey [key ]
if bucket == nil {
bucket = &sceneFontFallbackBucket {
request : d2fonts .FallbackRequest {Family : key .family , Style : key .style , Weight : key .weight },
missing : make (map [rune ]struct {}),
}
bucketsByKey [key ] = bucket
buckets = append (buckets , bucket )
}
bucket .references = append (bucket .references , reference )
}
bucket .missing [value ] = struct {}{}
}
}
}
}
retainedAssets , retainedBytes , err := b .retainedFontUsage ()
if err != nil {
return err
}
if b .options .Fonts != nil {
if b .options .Fonts .MaxAssets <= 0 || b .options .Fonts .MaxBytes <= 0 {
return fmt .Errorf ("scene: font fallback resolver requires positive asset and byte limits" )
}
if retainedAssets > b .options .Fonts .MaxAssets {
return fmt .Errorf ("scene: retained font asset count %d exceeds limit %d" , retainedAssets , b .options .Fonts .MaxAssets )
}
if retainedBytes > b .options .Fonts .MaxBytes {
return fmt .Errorf ("scene: retained font bytes %d exceed limit %d" , retainedBytes , b .options .Fonts .MaxBytes )
}
}
if len (missingAll ) == 0 {
return nil
}
for _ , bucket := range buckets {
resolverMissing := make (map [rune ]struct {}, len (bucket .missing ))
for value := range bucket .missing {
resolverMissing [value ] = struct {}{}
}
fallbackIDs := make ([]d2scene .AssetID , 0 , len (reusableBundledFallbacks ))
for _ , id := range reusableBundledFallbacks {
used := false
for value := range resolverMissing {
supportedByFace , err := supports (id , value )
if err != nil {
return err
}
if supportedByFace {
delete (resolverMissing , value )
used = true
}
}
if used {
fallbackIDs = appendUniqueAssetID (fallbackIDs , id )
}
}
var resources []fontfallback .SceneFont
if b .options .Fonts != nil && b .options .Fonts .Resolver != nil && len (resolverMissing ) != 0 {
bucket .request .Runes = sortedRunes (resolverMissing )
sceneResources , internal , resolveErr := fontfallback .ResolveForScene (b .ctx , b .options .Fonts .Resolver , fontfallback .Request {
Runes : bucket .request .Runes , Family : bucket .request .Family , Style : bucket .request .Style , Weight : bucket .request .Weight ,
})
if resolveErr != nil {
return fmt .Errorf ("scene: font fallback for family %q style %q weight %d: %w" , bucket .request .Family , bucket .request .Style , bucket .request .Weight , resolveErr )
}
if internal {
resources = sceneResources
} else {
resolved , resolveErr := b .options .Fonts .Resolver .ResolveFallbacks (b .ctx , bucket .request )
if resolveErr != nil {
return fmt .Errorf ("scene: font fallback for family %q style %q weight %d: %w" , bucket .request .Family , bucket .request .Style , bucket .request .Weight , resolveErr )
}
resources = make ([]fontfallback .SceneFont , len (resolved ))
for index := range resolved {
resources [index ].Font = fontfallback .Font {
Name : resolved [index ].Name , MIMEType : resolved [index ].MIMEType ,
Data : resolved [index ].Data , FaceIndex : resolved [index ].FaceIndex ,
}
}
}
}
if b .options .Fonts != nil && len (resources ) > b .options .Fonts .MaxAssets {
return fmt .Errorf ("scene: font fallback resolver returned %d resources, exceeding result limit %d" , len (resources ), b .options .Fonts .MaxAssets )
}
returnedBytes := int64 (0 )
for index , resource := range resources {
if int64 (len (resource .Data )) > b .options .Fonts .MaxBytes -returnedBytes {
return fmt .Errorf ("scene: font fallback resolver bytes exceed limit %d at resource %d (%s)" , b .options .Fonts .MaxBytes , index , safeFallbackName (resource .Name ))
}
returnedBytes += int64 (len (resource .Data ))
}
for index , resource := range resources {
if err := b .ctx .Err (); err != nil {
return err
}
if len (resource .Data ) == 0 {
return fmt .Errorf ("scene: font fallback %d (%s) has no data" , index , safeFallbackName (resource .Name ))
}
if strings .TrimSpace (resource .MIMEType ) == "" {
return fmt .Errorf ("scene: font fallback %d (%s) has no MIME type" , index , safeFallbackName (resource .Name ))
}
id := resource .ID
if resource .Shared {
if id == "" || resource .Face == nil {
return fmt .Errorf ("scene: shared font fallback %d (%s) is incomplete" , index , safeFallbackName (resource .Name ))
}
} else {
id , err = hashFallbackFont (b .ctx , resource .Font )
if err != nil {
return err
}
}
if _ , exists := b .assets [id ]; !exists {
if retainedAssets >= b .options .Fonts .MaxAssets {
return fmt .Errorf ("scene: retained font asset count exceeds limit %d" , b .options .Fonts .MaxAssets )
}
if int64 (len (resource .Data )) > b .options .Fonts .MaxBytes -retainedBytes {
return fmt .Errorf ("scene: retained font bytes exceed limit %d" , b .options .Fonts .MaxBytes )
}
data := resource .Data
if !resource .Shared {
data = append ([]byte (nil ), resource .Data ...)
}
b .assets [id ] = d2scene .FontAsset {MIMEType : resource .MIMEType , Data : data , FaceIndex : resource .FaceIndex }
if resource .Shared {
b .fontFaces [id ] = resource .Face
}
retainedAssets ++
retainedBytes += int64 (len (resource .Data ))
}
resolvedCoverage , err := b .fontCoverage (id , coverage )
if err != nil {
return fmt .Errorf ("scene: font fallback %d (%s): %w" , index , safeFallbackName (resource .Name ), err )
}
if resolvedCoverage .font .IsBundledNotoColorEmoji () {
reusableBundledFallbacks = appendUniqueAssetID (reusableBundledFallbacks , id )
}
fallbackIDs = appendUniqueAssetID (fallbackIDs , id )
}
for _ , reference := range bucket .references {
referenceMissing := sortedRunes (reference .missing )
selected := append ([]d2scene .AssetID (nil ), reference .value .Fallbacks ...)
for _ , id := range fallbackIDs {
for _ , value := range referenceMissing {
supportedByFace , err := supports (id , value )
if err != nil {
return err
}
if supportedByFace {
selected = appendUniqueAssetID (selected , id )
break
}
}
}
placeholder := false
for _ , id := range append ([]d2scene .AssetID {reference .value .Font .Asset }, selected ...) {
for _ , value := range missingGlyphPlaceholders {
supportedByFace , err := supports (id , value )
if err != nil {
return err
}
if supportedByFace {
placeholder = true
break
}
}
if placeholder {
break
}
}
if !placeholder {
id , addedBytes , err := b .ensureMissingGlyphFont (retainedAssets , retainedBytes )
if err != nil {
return fmt .Errorf ("scene: text node %q: %w" , reference .node .ID , err )
}
if addedBytes != 0 {
retainedAssets ++
retainedBytes += addedBytes
}
selected = appendUniqueAssetID (selected , id )
}
reference .setFallbacks (selected )
}
}
return b .ctx .Err ()
}
func (b *builder ) shapeTextRuns (references []*sceneTextReference ) error {
workLimits , err := normalizedFontFallbackWorkLimits (b .options .Fonts )
if err != nil {
return err
}
loadFace := func (id d2scene .AssetID ) (*fontface .ParsedFace , error ) {
return b .fontFace (id )
}
var shapingWorkspace fontface .ShapingWorkspace
var idScratch [8 ]d2scene .AssetID
var faceScratch []fontface .ShapeFace
reuseFaceScratch := len (references ) > 1
if reuseFaceScratch {
faceScratch = make ([]fontface .ShapeFace , 0 , 8 )
}
var previous *previousShapedText
if len (references ) > 1 {
previous = new (previousShapedText )
}
for _ , reference := range references {
text := reference .value
if len (text .Glyphs ) != 0 || text .Text == "" {
continue
}
if err := b .ctx .Err (); err != nil {
return err
}
if len (text .Fallbacks ) >= workLimits .maxFacesPerText {
return fmt .Errorf ("scene: text node %q fallback font reference count %d exceeds per-text face limit %d" , reference .node .ID , len (text .Fallbacks ), workLimits .maxFacesPerText )
}
faceCapacity := 1 + len (text .Fallbacks )
var ids []d2scene .AssetID
if !reuseFaceScratch {
ids = make ([]d2scene .AssetID , 0 , faceCapacity )
seen := make (map [d2scene .AssetID ]bool , faceCapacity )
for _ , id := range append ([]d2scene .AssetID {text .Font .Asset }, text .Fallbacks ...) {
if seen [id ] {
continue
}
seen [id ] = true
ids = append (ids , id )
}
} else if faceCapacity <= len (idScratch ) {
ids = idScratch [:1 ]
} else {
ids = make ([]d2scene .AssetID , 1 , faceCapacity )
}
if reuseFaceScratch {
ids [0 ] = text .Font .Asset
var seen map [d2scene .AssetID ]struct {}
for _ , id := range text .Fallbacks {
duplicate := false
if seen == nil {
for _ , existing := range ids {
if existing == id {
duplicate = true
break
}
}
if !duplicate && len (ids ) == len (idScratch ) {
seen = make (map [d2scene .AssetID ]struct {}, faceCapacity )
for _ , existing := range ids {
seen [existing ] = struct {}{}
}
}
} else {
_, duplicate = seen [id ]
}
if duplicate {
continue
}
if seen != nil {
seen [id ] = struct {}{}
}
ids = append (ids , id )
}
}
if len (ids ) > workLimits .maxFacesPerText {
return fmt .Errorf ("scene: text node %q font face count %d exceeds per-text limit %d" , reference .node .ID , len (ids ), workLimits .maxFacesPerText )
}
var shapeFaces []fontface .ShapeFace
if faceScratch != nil && len (ids ) <= cap (faceScratch ) {
shapeFaces = faceScratch [:0 ]
} else {
shapeFaces = make ([]fontface .ShapeFace , 0 , len (ids ))
}
for _ , id := range ids {
face , err := loadFace (id )
if err != nil {
return fmt .Errorf ("scene: text node %q: %w" , reference .node .ID , err )
}
shapeFaces = append (shapeFaces , fontface .ShapeFace {ID : string (id ), Face : face })
}
remainingCoverage := workLimits .maxCoverageChecks - b .fontCoverageChecks
if remainingCoverage <= 0 {
return fmt .Errorf ("scene: font coverage checks exceed limit %d" , workLimits .maxCoverageChecks )
}
remainingRuns := workLimits .maxShapingRuns - b .fontShapeRuns
if remainingRuns <= 0 {
return fmt .Errorf ("scene: text shaping run count exceeds limit %d" , workLimits .maxShapingRuns )
}
remainingGlyphs := workLimits .maxShapedGlyphs - b .fontGlyphs
if remainingGlyphs <= 0 {
return fmt .Errorf ("scene: shaped glyph count exceeds limit %d" , workLimits .maxShapedGlyphs )
}
if !finite (text .Font .Size ) || text .Font .Size <= 0 || text .Font .Size > float64 (math .MaxInt32 )/64 {
return fmt .Errorf ("scene: text node %q has an invalid font size for shaping" , reference .node .ID )
}
ppem := fixed .Int26_6 (math .Round (text .Font .Size * 64 ))
matchesPrevious := previous != nil && previous .hasShapedValue && text .Text == previous .text && ppem == previous .size && len (ids ) == previous .idCount
if matchesPrevious {
for index , id := range ids {
if previous .ids [index ] != id {
matchesPrevious = false
break
}
}
}
if matchesPrevious && previous .coverage <= remainingCoverage && previous .runs <= remainingRuns && len (previous .glyphs ) <= remainingGlyphs {
glyphs := make ([]d2scene .Glyph , len (previous .glyphs ))
for offset := 0 ; offset < len (glyphs ); offset += 1_024 {
if err := b .ctx .Err (); err != nil {
return err
}
end := min (offset +1_024 , len (glyphs ))
copy (glyphs [offset :end ], previous .glyphs [offset :end ])
}
b .fontCoverageChecks += previous .coverage
b .fontShapeRuns += previous .runs
b .fontGlyphs += len (glyphs )
reference .setGlyphs (glyphs )
continue
}
shaped , err := shapingWorkspace .ShapeTextTransient (b .ctx , text .Text , ppem , shapeFaces , fontface .ShapeLimits {
Runes : workLimits .maxRunesPerText ,
Faces : workLimits .maxFacesPerText ,
CoverageChecks : remainingCoverage ,
Runs : remainingRuns ,
Glyphs : remainingGlyphs ,
})
if err != nil {
return fmt .Errorf ("scene: text node %q: %w" , reference .node .ID , err )
}
glyphs := make ([]d2scene .Glyph , 0 , len (shaped .Glyphs ))
for _ , glyph := range shaped .Glyphs {
if glyph .Face < 0 || glyph .Face >= len (ids ) {
return fmt .Errorf ("scene: text node %q shaper returned invalid face %d" , reference .node .ID , glyph .Face )
}
ink := d2scene .Bounds {}
if glyph .HasInk {
ink = d2scene .NewBounds (
fixedToSceneFloat (glyph .Ink .Min .X ), fixedToSceneFloat (glyph .Ink .Min .Y ),
fixedToSceneFloat (glyph .Ink .Max .X ), fixedToSceneFloat (glyph .Ink .Max .Y ),
)
}
glyphs = append (glyphs , d2scene .Glyph {
ID : glyph .ID , Empty : glyph .Empty , Asset : ids [glyph .Face ],
Position : d2scene .Point {X : glyph .PositionX , Y : glyph .PositionY },
Advance : glyph .Advance , Ink : ink ,
})
}
if len (glyphs ) == 0 {
return fmt .Errorf ("scene: text node %q shaped non-empty text to no glyphs" , reference .node .ID )
}
b .fontCoverageChecks += shaped .CoverageChecks
b .fontShapeRuns += shaped .Runs
b .fontGlyphs += len (glyphs )
reference .setGlyphs (glyphs )
if previous != nil && len (ids ) <= len (previous .ids ) {
clear (previous .ids [:])
copy (previous .ids [:], ids )
previous .text = text .Text
previous .glyphs = glyphs
previous .size = ppem
previous .coverage = shaped .CoverageChecks
previous .runs = shaped .Runs
previous .idCount = len (ids )
previous .hasShapedValue = true
} else if previous != nil {
previous .hasShapedValue = false
}
}
return b .ctx .Err ()
}
func fixedToSceneFloat(value fixed .Int26_6 ) float64 {
return float64 (value ) / 64
}
func normalizedFontFallbackWorkLimits(options *FontFallbackOptions ) (fontFallbackWorkLimits , error ) {
limits := fontFallbackWorkLimits {
maxRunesPerText : defaultFontFallbackMaxRunesPerText , maxTotalRunes : defaultFontFallbackMaxTotalRunes ,
maxCoverageChecks : defaultFontFallbackMaxCoverageChecks ,
maxFacesPerText : defaultFontShapingMaxFacesPerText , maxShapingRuns : defaultFontShapingMaxRuns ,
maxShapedGlyphs : defaultFontShapingMaxGlyphs ,
}
if options == nil {
return limits , nil
}
if options .MaxRunesPerText < 0 || options .MaxTotalRunes < 0 || options .MaxCoverageChecks < 0 ||
options .MaxFontFacesPerText < 0 || options .MaxShapingRuns < 0 || options .MaxShapedGlyphs < 0 {
return fontFallbackWorkLimits {}, fmt .Errorf ("scene: font fallback work limits must not be negative" )
}
if options .MaxRunesPerText != 0 {
limits .maxRunesPerText = options .MaxRunesPerText
}
if options .MaxTotalRunes != 0 {
limits .maxTotalRunes = options .MaxTotalRunes
}
if options .MaxCoverageChecks != 0 {
limits .maxCoverageChecks = options .MaxCoverageChecks
}
if options .MaxFontFacesPerText != 0 {
limits .maxFacesPerText = options .MaxFontFacesPerText
} else if options .MaxAssets > 0 {
limits .maxFacesPerText = min (limits .maxFacesPerText , options .MaxAssets )
}
if options .MaxShapingRuns != 0 {
limits .maxShapingRuns = options .MaxShapingRuns
}
if options .MaxShapedGlyphs != 0 {
limits .maxShapedGlyphs = options .MaxShapedGlyphs
}
return limits , nil
}
func (b *builder ) retainedFontUsage () (int , int64 , error ) {
count := 0
bytes := int64 (0 )
for id , rawAsset := range b .assets {
var asset d2scene .FontAsset
switch value := rawAsset .(type ) {
case d2scene .FontAsset :
asset = value
case *d2scene .FontAsset :
if value == nil {
return 0 , 0 , fmt .Errorf ("scene: font asset %q is nil" , id )
}
asset = *value
default :
continue
}
if int64 (len (asset .Data )) > math .MaxInt64 -bytes {
return 0 , 0 , fmt .Errorf ("scene: retained font bytes exceed the int64 domain" )
}
count ++
bytes += int64 (len (asset .Data ))
}
return count , bytes , nil
}
func (b *builder ) fontCoverage (id d2scene .AssetID , cache map [d2scene .AssetID ]sceneFontCoverage ) (sceneFontCoverage , error ) {
if face , ok := cache [id ]; ok {
return face , nil
}
face , err := b .fontFace (id )
if err != nil {
return sceneFontCoverage {}, err
}
var source *fontface .BundledFaceSource
if asset , ok := b .assets [id ].(d2scene .FontAsset ); ok {
var matched bool
source , matched , err = fontface .RegisteredBundledFace (asset .Data , asset .FaceIndex )
if err == nil && !matched {
source , _, err = fontface .RegisteredBundledNotoColorEmoji (asset .Data , asset .FaceIndex )
}
if err != nil {
return sceneFontCoverage {}, err
}
}
result := sceneFontCoverage {font : face , source : source , runes : make (map [rune ]uint8 )}
cache [id ] = result
return result , nil
}
func (b *builder ) fontFace (id d2scene .AssetID ) (*fontface .ParsedFace , error ) {
if b .fontFaces == nil {
b .fontFaces = make (map [d2scene .AssetID ]*fontface .ParsedFace )
}
if face := b .fontFaces [id ]; face != nil {
return face , nil
}
asset , ok := b .assets [id ].(d2scene .FontAsset )
if !ok {
return nil , fmt .Errorf ("font asset %q is missing or invalid" , id )
}
var (
face *fontface .ParsedFace
err error
)
source , matched , lookupErr := fontface .RegisteredBundledFace (asset .Data , asset .FaceIndex )
if lookupErr != nil {
return nil , fmt .Errorf ("parse face %d from font asset %q: %w" , asset .FaceIndex , id , lookupErr )
} else if matched {
face , err = source .CloneReadOnly ()
} else {
face , err = fontface .ParseFace (asset .Data , asset .FaceIndex )
}
if err != nil {
return nil , fmt .Errorf ("parse face %d from font asset %q: %w" , asset .FaceIndex , id , err )
}
b .fontFaces [id ] = face
return face , nil
}
func (b *builder ) ensureMissingGlyphFont (retainedAssets int , retainedBytes int64 ) (d2scene .AssetID , int64 , error ) {
id := d2scene .AssetID ("font:" + string (d2fonts .SourceSansPro ) + ":" + string (d2fonts .FONT_STYLE_REGULAR ))
if _ , exists := b .assets [id ]; exists {
face , err := b .fontFace (id )
if err != nil {
return "" , 0 , err
}
supported , err := supportsMissingGlyphPlaceholder (face )
if err != nil || !supported {
if err != nil {
return "" , 0 , err
}
return "" , 0 , fmt .Errorf ("bundled missing-glyph font has no drawable placeholder" )
}
return id , 0 , nil
}
fontSpec := d2fonts .Font {Family : d2fonts .SourceSansPro , Style : d2fonts .FONT_STYLE_REGULAR }
data , ok := d2fonts .FontFaces .Lookup (fontSpec )
if !ok || len (data ) == 0 {
return "" , 0 , fmt .Errorf ("bundled missing-glyph font is not loaded" )
}
if b .options .Fonts != nil {
if retainedAssets >= b .options .Fonts .MaxAssets {
return "" , 0 , fmt .Errorf ("retained font asset count exceeds limit %d while adding missing-glyph font" , b .options .Fonts .MaxAssets )
}
if int64 (len (data )) > b .options .Fonts .MaxBytes -retainedBytes {
return "" , 0 , fmt .Errorf ("retained font bytes exceed limit %d while adding missing-glyph font" , b .options .Fonts .MaxBytes )
}
}
b .assets [id ] = d2scene .FontAsset {MIMEType : "font/ttf" , Data : retainedFontBytes (data )}
face , err := b .fontFace (id )
if err != nil {
delete (b .assets , id )
delete (b .fontFaces , id )
return "" , 0 , err
}
supported , err := supportsMissingGlyphPlaceholder (face )
if err != nil || !supported {
delete (b .assets , id )
delete (b .fontFaces , id )
if err != nil {
return "" , 0 , err
}
return "" , 0 , fmt .Errorf ("bundled missing-glyph font has no drawable placeholder" )
}
return id , int64 (len (data )), nil
}
func supportsMissingGlyphPlaceholder(face *fontface .ParsedFace ) (bool , error ) {
for _ , value := range missingGlyphPlaceholders {
supported , err := face .SupportsRenderableRune (value )
if err != nil {
return false , err
}
if supported {
return true , nil
}
}
return false , nil
}
func (b *builder ) sceneTextReferences (root *d2scene .Node ) ([]*sceneTextReference , error ) {
roots := []*d2scene .Node {root }
assetIDs := make ([]string , 0 , len (b .assets ))
for id := range b .assets {
assetIDs = append (assetIDs , string (id ))
}
sort .Strings (assetIDs )
for _ , rawID := range assetIDs {
if vector , ok := b .assets [d2scene .AssetID (rawID )].(d2scene .VectorAsset ); ok {
roots = append (roots , vector .Root )
}
}
seen := make (map [*d2scene .Node ]bool )
stack := append ([]*d2scene .Node (nil ), roots ...)
var references []*sceneTextReference
for len (stack ) != 0 {
if err := b .ctx .Err (); err != nil {
return nil , err
}
node := stack [len (stack )-1 ]
stack = stack [:len (stack )-1 ]
if node == nil || seen [node ] {
continue
}
seen [node ] = true
stack = append (stack , node .Children ...)
if node .Mask != nil {
stack = append (stack , node .Mask .Root )
}
switch primitive := node .Primitive .(type ) {
case d2scene .TextRun :
references = append (references , &sceneTextReference {node : node , value : primitive })
stack = appendPaintRoots (stack , primitive .Fill , primitive .Stroke )
case *d2scene .TextRun :
if primitive != nil {
references = append (references , &sceneTextReference {node : node , value : *primitive , pointer : primitive })
stack = appendPaintRoots (stack , primitive .Fill , primitive .Stroke )
}
case d2scene .Rect :
stack = appendPaintRoots (stack , primitive .Fill , primitive .Stroke )
case *d2scene .Rect :
if primitive != nil {
stack = appendPaintRoots (stack , primitive .Fill , primitive .Stroke )
}
case d2scene .Ellipse :
stack = appendPaintRoots (stack , primitive .Fill , primitive .Stroke )
case *d2scene .Ellipse :
if primitive != nil {
stack = appendPaintRoots (stack , primitive .Fill , primitive .Stroke )
}
case d2scene .Path :
stack = appendPaintRoots (stack , primitive .Fill , primitive .Stroke )
case *d2scene .Path :
if primitive != nil {
stack = appendPaintRoots (stack , primitive .Fill , primitive .Stroke )
}
}
}
return references , nil
}
func appendPaintRoots(stack []*d2scene .Node , fill d2scene .Paint , stroke *d2scene .Stroke ) []*d2scene .Node {
appendRoot := func (paint d2scene .Paint ) {
switch pattern := paint .(type ) {
case d2scene .PatternPaint :
stack = append (stack , pattern .Root )
case *d2scene .PatternPaint :
if pattern != nil {
stack = append (stack , pattern .Root )
}
}
}
appendRoot (fill )
if stroke != nil {
appendRoot (stroke .Paint )
}
return stack
}
func hashFallbackFont(ctx context .Context , resource fontfallback .Font ) (d2scene .AssetID , error ) {
return fontfallback .AssetID (ctx , resource )
}
func fontRuneIsDefaultIgnorable(value rune ) bool {
return fontface .IsDefaultIgnorableRune (value )
}
func appendUniqueAssetID(values []d2scene .AssetID , value d2scene .AssetID ) []d2scene .AssetID {
for _ , existing := range values {
if existing == value {
return values
}
}
return append (values , value )
}
func sortedRunes(values map [rune ]struct {}) []rune {
result := make ([]rune , 0 , len (values ))
for value := range values {
result = append (result , value )
}
sort .Slice (result , func (i , j int ) bool { return result [i ] < result [j ] })
return result
}
func safeFallbackName(value string ) string {
value = strings .ToValidUTF8 (value , "?" )
value = strings .Map (func (r rune ) rune {
if r < 0x20 || r == 0x7f {
return '?'
}
return r
}, value )
runes := []rune (value )
if len (runes ) > 80 {
value = string (runes [:80 ])
}
if value == "" {
return "unnamed font"
}
return value
}
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 .