package d2fonts
import (
"context"
"fmt"
"sort"
"sync"
"github.com/pancsta/d2/d2renderers/internal/fontface"
"github.com/pancsta/d2/d2renderers/internal/fontfallback"
)
type sceneCache = fontfallback .SceneCache
type BundledFallbackLimits struct {
MaxRequestedRunes int
MaxBundledBytes int64
MaxResolvedBytes int64
}
func (l BundledFallbackLimits ) normalized () (BundledFallbackLimits , error ) {
if l .MaxBundledBytes == 0 {
l .MaxBundledBytes = l .MaxResolvedBytes
}
if l .MaxRequestedRunes <= 0 || l .MaxBundledBytes <= 0 || l .MaxResolvedBytes <= 0 {
return BundledFallbackLimits {}, fmt .Errorf ("d2fonts: every bundled fallback limit must be positive" )
}
if l .MaxBundledBytes > l .MaxResolvedBytes {
return BundledFallbackLimits {}, fmt .Errorf ("d2fonts: bundled fallback byte limit %d exceeds aggregate resolved byte limit %d" , l .MaxBundledBytes , l .MaxResolvedBytes )
}
return l , nil
}
type BundledFallbackResolver struct {
*sceneCache
next FallbackResolver
limits BundledFallbackLimits
resolveOnce sync .Once
resolveGate chan struct {}
work bundledFallbackWork
bundledSource *fontface .BundledNotoColorEmojiSource
loadBundled func () ([]byte , error )
}
type bundledFallbackWork struct {
requestedRunes int64
bundledBytes int64
resolvedBytes int64
}
func NewBundledFallbackResolver (next FallbackResolver , limits BundledFallbackLimits ) (*BundledFallbackResolver , error ) {
limits , err := limits .normalized ()
if err != nil {
return nil , err
}
resolver := &BundledFallbackResolver {
next : next , limits : limits , loadBundled : bundledNotoColorEmoji ,
}
resolver .sceneCache = fontfallback .NewSceneCache (resolver .resolveForScene )
return resolver , nil
}
func (r *BundledFallbackResolver ) ResolveFallbacks (ctx context .Context , request FallbackRequest ) ([]FallbackFont , error ) {
fonts , err := r .resolve (ctx , request , false )
if err != nil {
return nil , err
}
result := make ([]FallbackFont , len (fonts ))
for index := range fonts {
result [index ] = fallbackFont (fonts [index ].Font )
}
return result , nil
}
func (r *BundledFallbackResolver ) resolveForScene (ctx context .Context , request fontfallback .Request ) ([]fontfallback .SceneFont , error ) {
return r .resolve (ctx , FallbackRequest {
Runes : request .Runes , Family : request .Family , Style : request .Style , Weight : request .Weight ,
}, true )
}
func (r *BundledFallbackResolver ) resolve (ctx context .Context , request FallbackRequest , forScene bool ) ([]fontfallback .SceneFont , error ) {
if r == nil {
return nil , fmt .Errorf ("d2fonts: nil bundled fallback resolver" )
}
if ctx == nil {
ctx = context .Background ()
}
if err := ctx .Err (); err != nil {
return nil , err
}
release , err := r .acquireResolve (ctx )
if err != nil {
return nil , err
}
defer release ()
if err := ctx .Err (); err != nil {
return nil , err
}
if len (request .Family )+len (request .Style ) > 1_024 {
return nil , fmt .Errorf ("d2fonts: fallback request font metadata exceeds 1024 bytes" )
}
if len (request .Runes ) > r .limits .MaxRequestedRunes {
return nil , fmt .Errorf ("d2fonts: requested fallback rune count %d exceeds limit %d" , len (request .Runes ), r .limits .MaxRequestedRunes )
}
if int64 (len (request .Runes )) > int64 (r .limits .MaxRequestedRunes )-r .work .requestedRunes {
return nil , fmt .Errorf ("d2fonts: requested fallback rune count exceeds cumulative resolver limit %d" , r .limits .MaxRequestedRunes )
}
r .work .requestedRunes += int64 (len (request .Runes ))
remaining , err := uniqueFallbackRunes (ctx , request .Runes )
if err != nil {
return nil , err
}
if len (remaining ) == 0 {
return nil , nil
}
var resolved []fontfallback .SceneFont
if shouldTryBundledEmoji (remaining ) {
loadBundled := r .loadBundled
if loadBundled == nil {
loadBundled = bundledNotoColorEmoji
}
data , err := loadBundled ()
if err != nil {
return nil , err
}
if len (data ) != 0 && r .bundledSource == nil {
source , matched , err := fontface .RegisteredBundledNotoColorEmoji (data , 0 )
if err != nil {
return nil , fmt .Errorf ("d2fonts: parse bundled Noto Color Emoji face: %w" , err )
}
if !matched {
return nil , fmt .Errorf ("d2fonts: bundled Noto Color Emoji face is not authenticated" )
}
r .bundledSource = source
}
var covered []rune
if r .bundledSource != nil {
covered , err = coveredBundledRunes (ctx , r .bundledSource , remaining )
if err != nil {
return nil , err
}
}
if len (data ) != 0 && len (covered ) != 0 {
font := fontfallback .Font {Name : "NotoColorEmoji-COLRv1-v2.051.ttf" , MIMEType : "font/ttf" , Data : data }
if forScene {
if r .sceneCache == nil {
return nil , fmt .Errorf ("d2fonts: bundled fallback scene cache is unavailable" )
}
sceneFace , err := r .bundledSource .CloneReadOnly ()
if err != nil {
return nil , fmt .Errorf ("d2fonts: clone bundled Noto Color Emoji face: %w" , err )
}
if !r .sceneCache .HasTrusted () {
if err := r .admitBundledBytes (int64 (len (data ))); err != nil {
return nil , err
}
}
shared , added , err := r .sceneCache .PrepareTrusted (ctx , font )
if err != nil {
return nil , err
}
if added {
r .work .bundledBytes += int64 (len (shared .Data ))
r .work .resolvedBytes += int64 (len (shared .Data ))
}
shared .Face = sceneFace
resolved = append (resolved , shared )
} else {
if err := r .admitBundledBytes (int64 (len (data ))); err != nil {
return nil , err
}
owned , err := cloneFallbackBytes (ctx , data )
if err != nil {
return nil , err
}
font .Data = owned
resolved = append (resolved , fontfallback .SceneFont {Font : font })
r .work .bundledBytes += int64 (len (owned ))
r .work .resolvedBytes += int64 (len (owned ))
}
for _ , value := range covered {
delete (remaining , value )
}
}
}
if len (remaining ) == 0 {
return resolved , nil
}
if r .next == nil {
return resolved , nil
}
downstreamRequest := request
downstreamRequest .Runes = sortedFallbackRunes (remaining )
downstream , err := r .next .ResolveFallbacks (ctx , downstreamRequest )
if err != nil {
return nil , err
}
for index , font := range downstream {
if int64 (len (font .Data )) > r .limits .MaxResolvedBytes -r .work .resolvedBytes {
return nil , fmt .Errorf ("d2fonts: resolved fallback font bytes exceed limit %d at downstream resource %d" , r .limits .MaxResolvedBytes , index )
}
font .Data , err = cloneFallbackBytes (ctx , font .Data )
if err != nil {
return nil , err
}
r .work .resolvedBytes += int64 (len (font .Data ))
resolved = append (resolved , fontfallback .SceneFont {Font : sceneFont (font )})
}
return resolved , nil
}
func sceneFont(font FallbackFont ) fontfallback .Font {
return fontfallback .Font {
Name : font .Name , MIMEType : font .MIMEType , Data : font .Data , FaceIndex : font .FaceIndex ,
}
}
func fallbackFont(font fontfallback .Font ) FallbackFont {
return FallbackFont {
Name : font .Name , MIMEType : font .MIMEType , Data : font .Data , FaceIndex : font .FaceIndex ,
}
}
func (r *BundledFallbackResolver ) admitBundledBytes (bytes int64 ) error {
if bytes > r .limits .MaxBundledBytes -r .work .bundledBytes {
return fmt .Errorf ("d2fonts: bundled fallback font bytes exceed limit %d while retaining Noto Color Emoji" , r .limits .MaxBundledBytes )
}
if bytes > r .limits .MaxResolvedBytes -r .work .resolvedBytes {
return fmt .Errorf ("d2fonts: resolved fallback font bytes exceed limit %d while retaining bundled Noto Color Emoji" , r .limits .MaxResolvedBytes )
}
return nil
}
func shouldTryBundledEmoji(values map [rune ]struct {}) bool {
for value := range values {
if fontface .BundledNotoColorEmojiCoversRune (value ) {
return true
}
}
return false
}
func coveredBundledRunes(ctx context .Context , source *fontface .BundledNotoColorEmojiSource , values map [rune ]struct {}) ([]rune , error ) {
covered := make ([]rune , 0 )
index := 0
for value := range values {
if index &255 == 0 {
if err := ctx .Err (); err != nil {
return nil , err
}
}
supported , err := source .SupportsRenderableRune (value )
if err != nil {
return nil , err
}
if supported {
covered = append (covered , value )
}
index ++
}
sort .Slice (covered , func (i , j int ) bool { return covered [i ] < covered [j ] })
if err := ctx .Err (); err != nil {
return nil , err
}
return covered , nil
}
func (r *BundledFallbackResolver ) acquireResolve (ctx context .Context ) (func (), error ) {
r .resolveOnce .Do (func () { r .resolveGate = make (chan struct {}, 1 ) })
select {
case r .resolveGate <- struct {}{}:
return func () { <-r .resolveGate }, nil
case <- ctx .Done ():
return nil , ctx .Err ()
}
}
func sortedFallbackRunes(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 cloneFallbackBytes(ctx context .Context , data []byte ) ([]byte , error ) {
result := make ([]byte , len (data ))
const chunkSize = 64 * 1024
for offset := 0 ; offset < len (data ); offset += chunkSize {
if err := ctx .Err (); err != nil {
return nil , err
}
end := min (offset +chunkSize , len (data ))
copy (result [offset :end ], data [offset :end ])
}
if err := ctx .Err (); err != nil {
return nil , err
}
return result , 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 .