// Copyright 2025-2026 肖其顿 (XIAO QI DUN) // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package ofdgo import ( "fmt" "sync" "github.com/tdewolff/canvas" canvastext "github.com/tdewolff/canvas/text" basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) // canvasFontLoadMu 保护绘图后端加载无名称字体时使用的共享状态 var canvasFontLoadMu sync.Mutex // textGlyphPathCacheKey 字形路径缓存键 type textGlyphPathCacheKey struct { font *canvas.Font size float64 fauxBold float64 fauxItalic float64 xOffset int32 yOffset int32 language string script canvastext.Script direction canvastext.Direction glyph textGlyph } // textGlyphPathCacheValue 字形路径缓存值 type textGlyphPathCacheValue struct { path *canvas.Path width float64 } // loadCanvasFont 串行加载绘图字体,不改动原始字体数据 // 入参: data 字体数据, index 集合索引, style 字体样式 // 返回: *canvas.Font 绘图字体, error 加载错误 func loadCanvasFont(data []byte, index int, style canvas.FontStyle) (*canvas.Font, error) { canvasFontLoadMu.Lock() defer canvasFontLoadMu.Unlock() return canvas.LoadFont(data, index, style) } // decodeFontContainer 展开字体封装,保留原始OpenType表及CFF2轮廓 // 入参: data 字体文件 // 返回: []byte OpenType数据, error 格式错误 func decodeFontContainer(data []byte) ([]byte, error) { if len(data) < 12 { return nil, fmt.Errorf("invalid font header") } switch string(data[:4]) { case "\x00\x01\x00\x00", "OTTO", "true", "ttcf": return data, nil case "wOFF": return basefont.DecodeWOFF(data) case "wOF2": decoded, handled, err := basefont.DecodeWOFF2(data) if handled { return decoded, err } } loaded, err := loadCanvasFont(data, 0, canvas.FontRegular) if err != nil { return nil, err } return basefont.SerializeOTF(loaded.Tables) } // loadFont 加载字体,绘图别名按数据和样式隔离,不改动原始字体名称 // 入参: fontID 字体ID // 返回: *canvas.FontFamily 字体族 func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { if ff, ok := r.canvasState().fontMap[fontID]; ok { return ff } resolved, err := r.PrepareFont(fontID) if err != nil { r.renderError = err return nil } of := r.Reader.fontDefinition(fontID) if len(resolved.Data) == 0 { return nil } if of == nil { of = &Font{ID: fontID, FontName: "OFDGoFallback"} } fontData := resolved.Data fontStyle := canvasFontStyle(of) if resolved.digest == ([32]byte{}) { resolved.digest = r.fontDigest(fontData) } key := canvasFontKey{digest: resolved.digest, name: of.FontName, style: fontStyle} cache := r.canvasFontFamilies() if ff, ok := cache.get(key); ok { r.canvasState().fontMap[fontID] = ff return ff } ff := canvas.NewFontFamily(fmt.Sprintf("ofdgo-%x-%d", resolved.digest, fontStyle)) canvasFontLoadMu.Lock() err = ff.LoadFont(fontData, 0, fontStyle) canvasFontLoadMu.Unlock() if err != nil { r.renderError = err return nil } r.canvasState().fontMap[fontID] = ff cache.put(key, ff, min(len(fontData), cache.limit)*8+256) return ff } // canvasFontStyle 获取Canvas字体样式 // 入参: font OFD字体定义 // 返回: canvas.FontStyle Canvas字体样式 func canvasFontStyle(font *Font) canvas.FontStyle { var style canvas.FontStyle if font.Bold { style |= canvas.FontBold } if font.Italic { style |= canvas.FontItalic } return style } // textGlyphWidth 获取绘制字形宽度 // 入参: face 字体, glyph 绘制字形 // 返回: float64 字形宽度 func textGlyphWidth(face *canvas.FontFace, glyph textGlyph) float64 { if glyph.GlyphID >= 0 && glyph.GlyphID <= 0xFFFF { return face.MmPerEm * float64(face.Font.GlyphAdvance(uint16(glyph.GlyphID))) } return face.TextWidth(glyph.Text) } // textGlyphPath 获取绘制字形路径 // 入参: face 字体, glyph 绘制字形, outlines 轮廓解析器 // 返回: *canvas.Path 字形路径, float64 字形宽度, error 字形解析错误 func textGlyphPath(face *canvas.FontFace, glyph textGlyph, outlines *basefont.Outliner) (*canvas.Path, float64, error) { p := &canvas.Path{} width := 0.0 if glyph.GlyphID < 0 || glyph.GlyphID > 0xFFFF { x, y := face.XOffset, face.YOffset for _, shaped := range face.Glyphs(glyph.Text) { part := &canvas.Path{} if err := outlines.Path(part, shaped.ID, face.MmPerEm); err != nil { return nil, 0, err } p = p.Append(part.Translate(face.MmPerEm*float64(x+shaped.XOffset), face.MmPerEm*float64(y+shaped.YOffset))) x, y = x+shaped.XAdvance, y+shaped.YAdvance } width = face.MmPerEm * float64(x) } else { glyphID := uint16(glyph.GlyphID) if err := outlines.Path(p, glyphID, face.MmPerEm); err != nil { return nil, 0, err } width = face.MmPerEm * float64(face.Font.GlyphAdvance(glyphID)) } if face.FauxBold != 0 { p = p.Offset(face.FauxBold*face.Size, canvas.Tolerance) } if face.FauxItalic != 0 { p = p.Transform(canvas.Identity.Shear(face.FauxItalic, 0)) } return p, width, nil } // cachedTextGlyphPath 获取缓存的字形路径 // 入参: face 字体, glyph 绘制字形 // 返回: *canvas.Path 字形路径, float64 字形宽度 func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (*canvas.Path, float64) { key := textGlyphPathCacheKey{ font: face.Font, size: face.Size, fauxBold: face.FauxBold, fauxItalic: face.FauxItalic, xOffset: face.XOffset, yOffset: face.YOffset, language: face.Language, script: face.Script, direction: face.Direction, glyph: glyph, } if cached, ok := r.canvasState().textGlyphPathCache.get(key); ok { return cached.path, cached.width } outlines := r.canvasState().fontOutlines[face.Font] if outlines == nil { cache := r.canvasFontOutlines() outlines, _ = cache.get(face.Font) if outlines == nil { metrics, err := basefont.ParseSFNTTables(face.Font.Tables) if err != nil { r.renderError = err return &canvas.Path{}, 0 } outlines = &basefont.Outliner{Font: metrics} cost := uint64(512) for _, data := range metrics.Tables { cost += uint64(len(data)) * 8 } cost += uint64(metrics.CmapEntries()) * 48 cache.put(face.Font, outlines, int(min(cost, uint64(cache.limit)+1))) } r.canvasState().fontOutlines[face.Font] = outlines } path, width, err := textGlyphPath(face, glyph, outlines) if err != nil { r.renderError = err return &canvas.Path{}, 0 } r.canvasState().textGlyphPathCache.put(key, textGlyphPathCacheValue{path: path, width: width}, 256+len(path.Data())*8+len(glyph.Text)+len(face.Language)) return path, width } // textMatrix 获取文本字形变换矩阵 // 入参: ctm OFD变换矩阵 // 返回: canvas.Matrix 画布变换矩阵 func textMatrix(ctm Matrix) canvas.Matrix { return canvas.Matrix{ {ctm.a, -ctm.c, 0}, {-ctm.b, ctm.d, 0}, } }