// Package fontfallback contains the renderer-internal handoff used to retain // trusted fallback assets across multiple scene builds without widening D2's // public font resolver interface.
package fontfallback import ( ) // Font is one resolved font face. Data is owned by the result and may be // retained by a scene builder. FaceIndex selects a face in a collection. type Font struct { Name string MIMEType string Data []byte FaceIndex uint16 } // Request carries missing code points and the source text style. type Request struct { Runes []rune Family string Style string Weight int } // SceneFont is the internal scene-builder result. Shared is set only for data // copied into the resolver-owned immutable cache. Face is a build-local clone // whose mutable shaping caches are never shared between scene builds. type SceneFont struct { Font ID d2scene.AssetID Face *fontface.ParsedFace Shared bool } // CacheStats reports retained trusted-asset preparation work. type CacheStats struct { Assets int Hashes uint64 Copies uint64 CopiedBytes int64 } // SceneResolveFunc resolves fonts for the internal scene-builder handoff. type SceneResolveFunc func(context.Context, Request) ([]SceneFont, error) // SceneCache is embedded privately by a resolver that supports the internal // scene handoff. Its unexported marker keeps the capability out of public // resolver interfaces while allowing this package to recover the cache. type SceneCache struct { resolve SceneResolveFunc mu sync.Mutex trusted *SceneFont stats CacheStats } // NewSceneCache creates a private scene handoff backed by resolve. func ( SceneResolveFunc) *SceneCache { return &SceneCache{resolve: } } func ( *SceneCache) () *SceneCache { return } // HasTrusted reports whether PrepareTrusted has retained its trusted resource. func ( *SceneCache) () bool { if == nil { return false } .mu.Lock() defer .mu.Unlock() return .trusted != nil } type sceneCacheProvider interface { sceneFallbackCache() *SceneCache } // ResolveForScene uses a resolver's private scene handoff when available. func ( context.Context, any, Request) ([]SceneFont, bool, error) { , := .(sceneCacheProvider) if ! { return nil, false, nil } := .sceneFallbackCache() if == nil || .resolve == nil { return nil, false, nil } , := .resolve(, ) return , true, } // CacheStatsFor returns private cache accounting for validation and metrics. func ( any) (CacheStats, bool) { , := .(sceneCacheProvider) if ! { return CacheStats{}, false } := .sceneFallbackCache() if == nil { return CacheStats{}, false } .mu.Lock() defer .mu.Unlock() return .stats, true } // PrepareTrusted copies and hashes the resolver's trusted source exactly once // for this cache. A canceled attempt publishes neither data nor accounting. func ( *SceneCache) ( context.Context, Font) (SceneFont, bool, error) { if == nil { return SceneFont{}, false, fmt.Errorf("font fallback: nil scene cache") } if == nil { = context.Background() } if := .Err(); != nil { return SceneFont{}, false, } .mu.Lock() defer .mu.Unlock() if .trusted != nil { return *.trusted, false, nil } := make([]byte, len(.Data)) , := hashFont(, , ) if != nil { return SceneFont{}, false, } := SceneFont{ Font: Font{ Name: .Name, MIMEType: .MIMEType, Data: , FaceIndex: .FaceIndex, }, ID: , Shared: true, } .trusted = & .stats.Assets = 1 .stats.Hashes++ .stats.Copies++ .stats.CopiedBytes += int64(len()) return , true, nil } // AssetID computes the scene font identity without retaining caller data. func ( context.Context, Font) (d2scene.AssetID, error) { return hashFont(, , nil) } func hashFont( context.Context, Font, []byte) (d2scene.AssetID, error) { if == nil { = context.Background() } if != nil && len() != len(.Data) { return "", fmt.Errorf("font fallback: copy destination length %d differs from source %d", len(), len(.Data)) } := sha256.New() _, _ = .Write([]byte(.MIMEType)) _, _ = .Write([]byte{0}) var [2]byte binary.BigEndian.PutUint16([:], .FaceIndex) _, _ = .Write([:]) if := copyAndHash(, , .Data, ); != nil { return "", } return d2scene.AssetID(fmt.Sprintf("font:fallback:%x", .Sum(nil))), nil } func copyAndHash( context.Context, , []byte, hash.Hash) error { const = 32 * 1024 for := 0; < len(); += { if := .Err(); != nil { return } := min(+, len()) := [:] if != nil { copy([:], ) = [:] } _, _ = .Write() } return .Err() }