feat(字体解耦): 解耦字体依赖,完善字体处理
build.yaml / build (push) Successful in 47s

This commit is contained in:
xiaoqidun committed 2026-10-11 00:52:01 +08:00
1 parent 051fc3b65e
commit cf8dfd046b
21 files changed
+1068 -234

No files matched your search

+1 -2
View File
@@ -2957,8 +2957,7 @@ function refreshEditorFonts() {
async function readTextFont(font) { async function readTextFont(font) {
if (font.embedded) return (await callWASM("ofdgoEditorFont", font.id.slice(9))).bytes; if (font.embedded) return (await callWASM("ofdgoEditorFont", font.id.slice(9))).bytes;
if (font.file) return (await callWASM("ofdgoFontFace", await fontManager.read(font.file), font.index)).bytes; return (await callWASM("ofdgoFontFace", await fontManager.read(font.file || font), font.index || 0)).bytes;
return fontManager.read(font);
} }
function updateTextFonts(item, refresh = false) { function updateTextFonts(item, refresh = false) {
+2 -2
View File
@@ -3,11 +3,11 @@ module github.com/xiaoqidun/ofdgo
go 1.26.0 go 1.26.0
require ( require (
github.com/andybalholm/brotli v1.2.6
github.com/emmansun/gmsm v0.45.0 github.com/emmansun/gmsm v0.45.0
github.com/go-fonts/latin-modern v0.3.3 github.com/go-fonts/latin-modern v0.3.3
github.com/go-text/typesetting v0.3.5 github.com/go-text/typesetting v0.3.5
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/j2kgo v0.0.0-20261009063910-4865a21ef893 github.com/xiaoqidun/j2kgo v0.0.0-20261009063910-4865a21ef893
github.com/xiaoqidun/jbig2 v1.1.4 github.com/xiaoqidun/jbig2 v1.1.4
github.com/xiaoqidun/pdfgo v0.0.0-20261010065159-b32618b0bdd7 github.com/xiaoqidun/pdfgo v0.0.0-20261010065159-b32618b0bdd7
@@ -23,12 +23,12 @@ require (
github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc // indirect
github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect github.com/BurntSushi/xgbutil v0.0.0-20190907113008-ad855c713046 // indirect
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f // indirect github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f // indirect
github.com/andybalholm/brotli v1.2.2 // indirect
github.com/benoitkugler/textlayout v0.3.2 // indirect github.com/benoitkugler/textlayout v0.3.2 // indirect
github.com/benoitkugler/textprocessing v0.0.6 // indirect github.com/benoitkugler/textprocessing v0.0.6 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // indirect github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // indirect
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117 // indirect
github.com/tdewolff/minify/v2 v2.24.17 // indirect github.com/tdewolff/minify/v2 v2.24.17 // indirect
github.com/tdewolff/parse/v2 v2.8.16 // indirect github.com/tdewolff/parse/v2 v2.8.16 // indirect
github.com/yuin/goldmark v1.8.5 // indirect github.com/yuin/goldmark v1.8.5 // indirect
+4 -4
View File
@@ -20,8 +20,8 @@ github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f h1:l7moT9o/v
github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f/go.mod h1:vIOkSdX3NDCPwgu8FIuTat2zDF0FPXXQ0RYFRy+oQic= github.com/ByteArena/poly2tri-go v0.0.0-20170716161910-d102ad91854f/go.mod h1:vIOkSdX3NDCPwgu8FIuTat2zDF0FPXXQ0RYFRy+oQic=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM= github.com/andybalholm/brotli v1.2.6 h1:ftYnfj6usCp+UGV5kSJ3+chpMQgU+gJf/AxsUQ52REI=
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.6/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE= github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE=
github.com/benoitkugler/textlayout v0.3.2 h1:Y/LTkwpQ9G0Fia9yhPmZA9IR5AnE8Cq30j3C+Gx5/IE= github.com/benoitkugler/textlayout v0.3.2 h1:Y/LTkwpQ9G0Fia9yhPmZA9IR5AnE8Cq30j3C+Gx5/IE=
github.com/benoitkugler/textlayout v0.3.2/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w= github.com/benoitkugler/textlayout v0.3.2/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7sBPgEFcuSgfvi/w=
@@ -50,8 +50,8 @@ github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 h1:ZdkidVdpLW13BQ9a+
github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388/go.mod h1:C/WY5lmWfMtPFYYBTd3Lzdn4FTLr+RxlIeiBNye+/os= github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388/go.mod h1:C/WY5lmWfMtPFYYBTd3Lzdn4FTLr+RxlIeiBNye+/os=
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c h1:eXJXpQAo8l0AdVnsjJctG38GlBnHnwHfhLOTLYxZYNE= github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c h1:eXJXpQAo8l0AdVnsjJctG38GlBnHnwHfhLOTLYxZYNE=
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c/go.mod h1:K9Ux3cwFtyAFRITZ46g7v+ORXgI3ENtQa5rL9yjNz5Y= github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c/go.mod h1:K9Ux3cwFtyAFRITZ46g7v+ORXgI3ENtQa5rL9yjNz5Y=
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 h1:phNTWRYLLTlPpUgxkd6FDmWLlrOzpEpQAqq1ryLXYS8= github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117 h1:5vSJjsbV9AH0U+Ex0uej8ROrpOBlOVBrw0mGlVef7oo=
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6/go.mod h1:ipESUihcEdgRBhIwTRHmBTOMamccMQrp4iCxfa1aFws= github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117/go.mod h1:ipESUihcEdgRBhIwTRHmBTOMamccMQrp4iCxfa1aFws=
github.com/tdewolff/minify/v2 v2.24.17 h1:6AbitfVyq0M7aW6i+XL7+49DeTQZwloOMs9O574arBg= github.com/tdewolff/minify/v2 v2.24.17 h1:6AbitfVyq0M7aW6i+XL7+49DeTQZwloOMs9O574arBg=
github.com/tdewolff/minify/v2 v2.24.17/go.mod h1:kVqn9vxXUKtlHexSNrWbYePqioOT5mc4ou/KVSMpfCM= github.com/tdewolff/minify/v2 v2.24.17/go.mod h1:kVqn9vxXUKtlHexSNrWbYePqioOT5mc4ou/KVSMpfCM=
github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8feI= github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8feI=
+3 -5
View File
@@ -16,8 +16,6 @@ package ofdgo
import ( import (
"encoding/binary" "encoding/binary"
"github.com/tdewolff/font"
) )
// FixFontDataAggressive 激进修复字体数据 // FixFontDataAggressive 激进修复字体数据
@@ -52,10 +50,10 @@ func FixFontDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, ma
} }
// fontSFNTData 保留字体原始表数据和时间,重新计算目录与校验和 // fontSFNTData 保留字体原始表数据和时间,重新计算目录与校验和
// 入参: sfnt 已解析的字体 // 入参: tables 原始字体表
// 返回: []byte 独立SFNT数据 // 返回: []byte 独立SFNT数据
func fontSFNTData(sfnt *font.SFNT) []byte { func fontSFNTData(tables map[string][]byte) []byte {
data, _ := serializeOTF(sfnt.Tables) data, _ := serializeOTF(tables)
return data return data
} }
+5 -5
View File
@@ -23,7 +23,7 @@ import (
"github.com/go-text/typesetting/font/cff" "github.com/go-text/typesetting/font/cff"
"github.com/go-text/typesetting/font/opentype" "github.com/go-text/typesetting/font/opentype"
"github.com/tdewolff/font" "github.com/go-text/typesetting/font/opentype/tables"
) )
// defaultCFF2Font 将CFF2默认实例转换为静态OpenType字体,保留字形编号和三次轮廓 // defaultCFF2Font 将CFF2默认实例转换为静态OpenType字体,保留字形编号和三次轮廓
@@ -90,7 +90,7 @@ func defaultCFF2Font(data []byte) (result []byte, err error) {
} }
chars := make([][]byte, count) chars := make([][]byte, count)
for gid := range chars { for gid := range chars {
segments, _, err := program.LoadGlyph(uint16(gid), nil) segments, _, err := program.LoadGlyph(tables.GlyphID(gid), nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("CFF2 glyph %d: %w", gid, err) return nil, fmt.Errorf("CFF2 glyph %d: %w", gid, err)
} }
@@ -207,7 +207,7 @@ func staticCIDFont(table map[string][]byte, chars [][]byte) ([]byte, error) {
// staticCFFPrograms 读取无子程序和私有字典的CID字形,其他CFF保留原有保存策略 // staticCFFPrograms 读取无子程序和私有字典的CID字形,其他CFF保留原有保存策略
// 入参: sfnt 已解析字体 // 入参: sfnt 已解析字体
// 返回: [][]byte 独立字形程序,不符合条件时为空 // 返回: [][]byte 独立字形程序,不符合条件时为空
func staticCFFPrograms(sfnt *font.SFNT) [][]byte { func staticCFFPrograms(sfnt *sfntFont) [][]byte {
data := sfnt.Tables["CFF "] data := sfnt.Tables["CFF "]
if len(data) < 4 { if len(data) < 4 {
return nil return nil
@@ -262,7 +262,7 @@ func staticCFFPrograms(sfnt *font.SFNT) [][]byte {
// subsetStaticCFFFont 裁剪静态CID字形并保留原编号、字符映射和度量 // subsetStaticCFFFont 裁剪静态CID字形并保留原编号、字符映射和度量
// 入参: sfnt 已解析字体, glyphs 使用的字形 // 入参: sfnt 已解析字体, glyphs 使用的字形
// 返回: []byte 字体子集, error 裁剪错误 // 返回: []byte 字体子集, error 裁剪错误
func subsetStaticCFFFont(sfnt *font.SFNT, glyphs []uint16) ([]byte, error) { func subsetStaticCFFFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) {
chars := staticCFFPrograms(sfnt) chars := staticCFFPrograms(sfnt)
if chars == nil { if chars == nil {
return nil, fmt.Errorf("CFF font cannot be subset safely") return nil, fmt.Errorf("CFF font cannot be subset safely")
@@ -299,7 +299,7 @@ func subsetStaticCFFFont(sfnt *font.SFNT, glyphs []uint16) ([]byte, error) {
} }
mapping := make(map[rune]uint16) mapping := make(map[rune]uint16)
for gid := range keep { for gid := range keep {
for _, char := range sfnt.Cmap.ToUnicode(gid) { for _, char := range sfnt.glyphCharacters(gid) {
mapping[char] = gid mapping[char] = gid
} }
} }
+3 -5
View File
@@ -20,8 +20,6 @@ import (
"fmt" "fmt"
"strings" "strings"
"unicode/utf8" "unicode/utf8"
"github.com/tdewolff/font"
) )
// FontFace 字体文件中的字体名称与零起始索引,不依赖文件名 // FontFace 字体文件中的字体名称与零起始索引,不依赖文件名
@@ -37,7 +35,7 @@ type FontFace struct {
// Faces 读取字体名称表,集合文件按原顺序列出各项,不解析字形轮廓 // Faces 读取字体名称表,集合文件按原顺序列出各项,不解析字形轮廓
// 返回: []FontFace 字体列表, error 格式或名称表错误 // 返回: []FontFace 字体列表, error 格式或名称表错误
func (f FontFile) Faces() ([]FontFace, error) { func (f FontFile) Faces() ([]FontFace, error) {
data, err := font.ToSFNT(f.Data) data, err := decodeFontContainer(f.Data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -64,7 +62,7 @@ func (f FontFile) Faces() ([]FontFace, error) {
// 入参: index 零起始字体索引,非集合文件只能为0 // 入参: index 零起始字体索引,非集合文件只能为0
// 返回: []byte 独立字体数据, error 格式或索引错误 // 返回: []byte 独立字体数据, error 格式或索引错误
func (f FontFile) Face(index int) ([]byte, error) { func (f FontFile) Face(index int) ([]byte, error) {
data, err := font.ToSFNT(f.Data) data, err := decodeFontContainer(f.Data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -105,7 +103,7 @@ func (f FontFile) MissingGlyphs(index int, text string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
sfnt, err := font.ParseSFNT(data, 0) sfnt, err := parseSFNTFont(data)
if err != nil { if err != nil {
return "", err return "", err
} }
+51 -8
View File
@@ -21,12 +21,16 @@ import (
"math" "math"
"sync" "sync"
"github.com/tdewolff/font" "github.com/go-text/typesetting/font/cff"
"github.com/go-text/typesetting/font/opentype"
) )
// sfntOutliner 缓存字体程序并提取独立字形轮廓 // sfntOutliner 缓存字体程序并提取独立字形轮廓
type sfntOutliner struct { type sfntOutliner struct {
font *font.SFNT font *sfntFont
cffOnce sync.Once
cff *cff.CFF
cffErr error
once sync.Once once sync.Once
program *ttInterpreter program *ttInterpreter
err error err error
@@ -42,12 +46,51 @@ type ttGlyph struct {
warning error warning error
} }
// path 按TrueType指令和隐含点规则生成闭合轮廓,CFF保留原有解析 // path 按字体设计坐标提取闭合轮廓,TrueType保留字形指令处理
// 入参: path 路径接收器, glyph 字形编号, scale 字体单位缩放 // 入参: path 路径接收器, glyph 字形编号, scale 字体单位缩放
// 返回: error 字形解析错误 // 返回: error 字形解析错误
func (f *sfntOutliner) path(path font.Pather, glyph uint16, scale float64) error { func (f *sfntOutliner) path(path glyphPather, glyph uint16, scale float64) error {
if !f.font.IsTrueType { if !f.font.IsTrueType {
return f.font.GlyphPath(path, glyph, 0, 0, 0, scale, font.NoHinting) f.cffOnce.Do(func() {
data, err := sanitizeCFF(f.font.Tables["CFF "])
if err == nil {
data, err = normalizeCFFCharstringsAt(data, f.font.UnitsPerEm())
}
if err != nil {
f.cffErr = err
return
}
f.cff, f.cffErr = cff.Parse(data)
})
if f.cffErr != nil {
return f.cffErr
}
segments, _, err := f.cff.LoadGlyph(glyph)
if err != nil {
return err
}
open := false
for _, segment := range segments {
p := segment.Args
switch segment.Op {
case opentype.SegmentOpMoveTo:
if open {
path.Close()
}
path.MoveTo(float64(p[0].X)*scale, float64(p[0].Y)*scale)
open = true
case opentype.SegmentOpLineTo:
path.LineTo(float64(p[0].X)*scale, float64(p[0].Y)*scale)
case opentype.SegmentOpCubeTo:
path.CubeTo(float64(p[0].X)*scale, float64(p[0].Y)*scale, float64(p[1].X)*scale, float64(p[1].Y)*scale, float64(p[2].X)*scale, float64(p[2].Y)*scale)
default:
return fmt.Errorf("invalid CFF curve")
}
}
if open {
path.Close()
}
return nil
} }
contour, err := f.glyph(glyph, make(map[uint16]bool)) contour, err := f.glyph(glyph, make(map[uint16]bool))
if err != nil { if err != nil {
@@ -183,10 +226,10 @@ func (f *sfntOutliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error)
// 入参: id 字形编号, data 完整字形头 // 入参: id 字形编号, data 完整字形头
// 返回: [4]ttPoint 左右边距与上下原点 // 返回: [4]ttPoint 左右边距与上下原点
func (f *sfntOutliner) phantomPoints(id uint16, data []byte) [4]ttPoint { func (f *sfntOutliner) phantomPoints(id uint16, data []byte) [4]ttPoint {
left := int32(int16(binary.BigEndian.Uint16(data[2:]))) - int32(f.font.Hmtx.LeftSideBearing(id)) left := int32(int16(binary.BigEndian.Uint16(data[2:]))) - int32(f.font.sideBearing(id, false))
top := int32(int16(binary.BigEndian.Uint16(data[8:]))) top := int32(int16(binary.BigEndian.Uint16(data[8:])))
if f.font.Vmtx != nil { if f.font.vertical != 0 {
top += int32(f.font.Vmtx.TopSideBearing(id)) top += int32(f.font.sideBearing(id, true))
} }
return [4]ttPoint{{x: left * 64, ox: left * 64}, {x: (left + int32(f.font.GlyphAdvance(id))) * 64, ox: (left + int32(f.font.GlyphAdvance(id))) * 64}, {y: top * 64, oy: top * 64}, {y: (top - int32(f.font.GlyphVerticalAdvance(id))) * 64, oy: (top - int32(f.font.GlyphVerticalAdvance(id))) * 64}} return [4]ttPoint{{x: left * 64, ox: left * 64}, {x: (left + int32(f.font.GlyphAdvance(id))) * 64, ox: (left + int32(f.font.GlyphAdvance(id))) * 64}, {y: top * 64, oy: top * 64}, {y: (top - int32(f.font.GlyphVerticalAdvance(id))) * 64, oy: (top - int32(f.font.GlyphVerticalAdvance(id))) * 64}}
} }
+1 -3
View File
@@ -22,8 +22,6 @@ import (
"path" "path"
"slices" "slices"
"sort" "sort"
"github.com/tdewolff/font"
) )
const ( const (
@@ -246,7 +244,7 @@ func (r *Reader) embeddedFontFace(of Font) (*FontFace, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if data, err = font.ToSFNT(data); err == nil { if data, err = decodeFontContainer(data); err == nil {
if count, err := fontFileCount(data); err == nil { if count, err := fontFileCount(data); err == nil {
faces = make([]*FontFace, count) faces = make([]*FontFace, count)
for index := range faces { for index := range faces {
+189
View File
@@ -0,0 +1,189 @@
// 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"
)
// sfntGlyphBoundsLimit 限制单个字体保留的字形与字号边界数量
const sfntGlyphBoundsLimit = 4096
// sfntFontMetrics 保留默认字体度量,写出时不更新原始时间
type sfntFontMetrics struct {
*sfntFont
*fontShaper
outlines *sfntOutliner
bounds *sfntFontBounds
}
// sfntFontBounds 缓存已成功提取的字形边界,不保留轮廓或失败结果
type sfntFontBounds struct {
mu sync.Mutex
values map[sfntGlyphSize]Box
}
// sfntGlyphSize 区分字形编号及精确毫米字号
type sfntGlyphSize struct {
glyph uint16
size float64
}
// glyphPather 接收字体设计坐标中的轮廓指令
type glyphPather interface {
MoveTo(float64, float64)
LineTo(float64, float64)
QuadTo(float64, float64, float64, float64)
CubeTo(float64, float64, float64, float64, float64, float64)
Close()
}
// glyphGeometry 将字体轮廓转换为纵轴向下的几何路径
type glyphGeometry struct{ path GeometryPath }
// MoveTo 开始子路径
// 入参: x 横坐标, y 纵坐标
func (p *glyphGeometry) MoveTo(x, y float64) {
p.path = append(p.path, GeometrySegment{Verb: GeometryMove, End: Point{x, -y}})
}
// LineTo 添加直线
// 入参: x 横坐标, y 纵坐标
func (p *glyphGeometry) LineTo(x, y float64) {
p.path = append(p.path, GeometrySegment{Verb: GeometryLine, End: Point{x, -y}})
}
// QuadTo 添加二次贝塞尔曲线
// 入参: cx 控制点横坐标, cy 控制点纵坐标, x 终点横坐标, y 终点纵坐标
func (p *glyphGeometry) QuadTo(cx, cy, x, y float64) {
p.path = append(p.path, GeometrySegment{Verb: GeometryQuad, Control1: Point{cx, -cy}, End: Point{x, -y}})
}
// CubeTo 添加三次贝塞尔曲线
// 入参: ax 第一控制点横坐标, ay 第一控制点纵坐标, bx 第二控制点横坐标, by 第二控制点纵坐标, x 终点横坐标, y 终点纵坐标
func (p *glyphGeometry) CubeTo(ax, ay, bx, by, x, y float64) {
p.path = append(p.path, GeometrySegment{Verb: GeometryCubic, Control1: Point{ax, -ay}, Control2: Point{bx, -by}, End: Point{x, -y}})
}
// Close 闭合当前子路径
func (p *glyphGeometry) Close() { p.path = append(p.path, GeometrySegment{Verb: GeometryClose}) }
// ResolveFont 按需加载字体资源,保留内嵌数据及外部字体的名称、样式和集合索引匹配
// 入参: r 渲染器, id 字体标识, exact 是否禁止无关回退
// 返回: ResolvedFont 字体与来源, error 字体不可用错误
func (b SFNTBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFont, error) {
return r.resolveFontSource(b, id, exact)
}
// OpenFont 解析独立SFNT数据,度量与原有编辑排版保持一致
// 入参: data 字体数据
// 返回: FontMetrics 字体度量, error 解析错误
func (SFNTBackend) OpenFont(data []byte) (FontMetrics, error) {
data, err := defaultCFF2Font(data)
if err != nil {
return nil, err
}
sfnt, err := parseSFNTFont(data)
if err != nil {
return nil, err
}
metrics := &sfntFontMetrics{sfntFont: sfnt, outlines: &sfntOutliner{font: sfnt}, bounds: &sfntFontBounds{}}
metrics.fontShaper = &fontShaper{metrics: metrics}
return metrics, nil
}
// Write 返回字体数据,不修改字体元信息
// 返回: []byte 独立SFNT数据
func (f sfntFontMetrics) Write() []byte {
return f.sfntFont.Write()
}
// GlyphOutline 返回基线原点、纵轴向下的字形轮廓
// 入参: glyph 字形编号, size 字号,单位为毫米
// 返回: GeometryPath 字形路径, error 字形解析错误
func (f sfntFontMetrics) GlyphOutline(glyph uint16, size float64) (GeometryPath, error) {
if !rasterPositive(size) || glyph >= f.NumGlyphs() {
return nil, fmt.Errorf("invalid glyph or size")
}
path := &glyphGeometry{}
if err := f.outlines.path(path, glyph, size/float64(f.UnitsPerEm())); err != nil {
return nil, err
}
return path.path, nil
}
// GlyphBounds 返回字形轮廓的精确边界,重复编号及字号复用有限缓存
// 入参: glyph 字形编号, size 字号,单位为毫米
// 返回: Box 字形边界, error 字形解析错误
func (f sfntFontMetrics) GlyphBounds(glyph uint16, size float64) (Box, error) {
key := sfntGlyphSize{glyph: glyph, size: size}
if f.bounds != nil {
f.bounds.mu.Lock()
value, ok := f.bounds.values[key]
f.bounds.mu.Unlock()
if ok {
return value, nil
}
}
outline, err := f.GlyphOutline(glyph, size)
if err != nil {
return Box{}, err
}
value, err := outline.Bounds()
if err != nil {
return Box{}, err
}
if f.bounds != nil {
f.bounds.mu.Lock()
if f.bounds.values == nil || len(f.bounds.values) >= sfntGlyphBoundsLimit {
f.bounds.values = make(map[sfntGlyphSize]Box)
}
f.bounds.values[key] = value
f.bounds.mu.Unlock()
}
return value, nil
}
// GlyphWarning 返回无效字形指令后保留设计轮廓的提示,原始字体不变
// 入参: glyph 字形编号
// 返回: error 恢复原因,无恢复时为空
func (f sfntFontMetrics) GlyphWarning(glyph uint16) error {
return f.outlines.glyphWarning(glyph)
}
// GlyphOutlines 批量提取字形轮廓,重复编号只解析一次
// 入参: glyphs 字形编号, size 毫米字号
// 返回: []GeometryPath 同序只读轮廓, error 轮廓错误
func (f sfntFontMetrics) GlyphOutlines(glyphs []uint16, size float64) ([]GeometryPath, error) {
if !rasterPositive(size) {
return nil, fmt.Errorf("invalid glyph size")
}
result := make([]GeometryPath, len(glyphs))
paths := make(map[uint16]GeometryPath, len(glyphs))
for i, glyph := range glyphs {
path, ok := paths[glyph]
if !ok {
var err error
path, err = f.GlyphOutline(glyph, size)
if err != nil {
return nil, err
}
paths[glyph] = path
}
result[i] = path
}
return result, nil
}
+126 -43
View File
@@ -20,15 +20,13 @@ import (
"fmt" "fmt"
"maps" "maps"
"slices" "slices"
"github.com/tdewolff/font"
) )
// SFNTBackend 提供默认字体资源处理,不依赖Canvas绘图库 // SFNTBackend 提供独立的OpenType字体度量、轮廓和资源处理
type SFNTBackend struct{} type SFNTBackend struct{}
// sfntResourceMetrics 隔离资源解析器的度量实现 // sfntResourceMetrics 隔离资源解析器的度量实现
type sfntResourceMetrics struct{ *font.SFNT } type sfntResourceMetrics struct{ *sfntFont }
// Name 返回资源后端标识 // Name 返回资源后端标识
// 返回: string 后端标识 // 返回: string 后端标识
@@ -47,11 +45,11 @@ func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, er
if err != nil { if err != nil {
return nil, err return nil, err
} }
sfnt, err := font.ParseSFNT(data, 0) sfnt, err := parseSFNTFont(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
name := sfntFontName(sfnt, font.NamePostScript, font.NameFull, font.NameFontFamily) name := sfntFontName(sfnt, 6, 4, 1)
if name == "" { if name == "" {
return nil, fmt.Errorf("font has no name") return nil, fmt.Errorf("font has no name")
} }
@@ -61,7 +59,7 @@ func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, er
} }
return &FontResource{ return &FontResource{
FontMetrics: sfntResourceMetrics{sfnt}, Data: data, Extension: extension, FontMetrics: sfntResourceMetrics{sfnt}, Data: data, Extension: extension,
Font: Font{FontName: name, FamilyName: sfntFontName(sfnt, font.NamePreferredFamily, font.NameFontFamily), Charset: "unicode", Bold: sfnt.Head.MacStyle[0], Italic: sfnt.Head.MacStyle[1], FixedWidth: sfnt.Post.IsFixedPitch != 0}, Font: Font{FontName: name, FamilyName: sfntFontName(sfnt, 16, 1), Charset: "unicode", Bold: binary.BigEndian.Uint16(sfnt.Tables["head"][44:])&1 != 0, Italic: binary.BigEndian.Uint16(sfnt.Tables["head"][44:])&2 != 0, FixedWidth: len(sfnt.Tables["post"]) >= 16 && binary.BigEndian.Uint32(sfnt.Tables["post"][12:]) != 0},
CanSubset: sfntSubsettable(sfnt), CanSubset: sfntSubsettable(sfnt),
}, nil }, nil
} }
@@ -90,12 +88,12 @@ func (SFNTBackend) SubsetSourceFont(data []byte, usage FontUsage) ([]byte, error
// Write 返回未改动元信息的字体数据 // Write 返回未改动元信息的字体数据
// 返回: []byte 字体数据 // 返回: []byte 字体数据
func (f sfntResourceMetrics) Write() []byte { return fontSFNTData(f.SFNT) } func (f sfntResourceMetrics) Write() []byte { return f.sfntFont.Write() }
// sfntSubsettable 判断默认资源后端能否无损裁剪该字体 // sfntSubsettable 判断默认资源后端能否无损裁剪该字体
// 入参: sfnt 已解析字体 // 入参: sfnt 已解析字体
// 返回: bool 是否支持裁剪 // 返回: bool 是否支持裁剪
func sfntSubsettable(sfnt *font.SFNT) bool { func sfntSubsettable(sfnt *sfntFont) bool {
if fontEmbeddingFlags(sfnt)&0x0100 != 0 || slices.ContainsFunc([]string{"COLR", "CBDT", "sbix", "SVG "}, func(tag string) bool { return sfnt.Tables[tag] != nil }) { if fontEmbeddingFlags(sfnt)&0x0100 != 0 || slices.ContainsFunc([]string{"COLR", "CBDT", "sbix", "SVG "}, func(tag string) bool { return sfnt.Tables[tag] != nil }) {
return false return false
} }
@@ -108,7 +106,7 @@ func sfntSubsettable(sfnt *font.SFNT) bool {
// fontEmbeddingFlags 读取OpenType的嵌入标志,未提供OS/2表时返回零 // fontEmbeddingFlags 读取OpenType的嵌入标志,未提供OS/2表时返回零
// 入参: sfnt 已解析字体 // 入参: sfnt 已解析字体
// 返回: uint16 嵌入标志 // 返回: uint16 嵌入标志
func fontEmbeddingFlags(sfnt *font.SFNT) uint16 { func fontEmbeddingFlags(sfnt *sfntFont) uint16 {
if table := sfnt.Tables["OS/2"]; len(table) >= 10 { if table := sfnt.Tables["OS/2"]; len(table) >= 10 {
return binary.BigEndian.Uint16(table[8:10]) return binary.BigEndian.Uint16(table[8:10])
} }
@@ -119,7 +117,7 @@ func fontEmbeddingFlags(sfnt *font.SFNT) uint16 {
// 入参: data 独立OpenType字体 // 入参: data 独立OpenType字体
// 返回: error 字体解析或嵌入限制 // 返回: error 字体解析或嵌入限制
func validateFontEmbedding(data []byte) error { func validateFontEmbedding(data []byte) error {
sfnt, err := font.ParseSFNT(data, 0) sfnt, err := parseSFNTFont(data)
if err != nil { if err != nil {
return err return err
} }
@@ -133,10 +131,14 @@ func validateFontEmbedding(data []byte) error {
// sfntFontName 获取字体中的名称 // sfntFontName 获取字体中的名称
// 入参: sfnt 字体, names 名称类型,按顺序查找 // 入参: sfnt 字体, names 名称类型,按顺序查找
// 返回: string 字体名称 // 返回: string 字体名称
func sfntFontName(sfnt *font.SFNT, names ...font.NameID) string { func sfntFontName(sfnt *sfntFont, names ...uint16) string {
records, values := fontNameValues(sfnt.Tables["name"])
for _, name := range names { for _, name := range names {
for _, record := range sfnt.Name.Get(name) { for _, record := range records {
if value := record.String(); value != "" { if record.Name != name {
continue
}
if value := values[[4]uint16{record.Platform, record.Encoding, record.Language, name}]; value != "" {
return value return value
} }
} }
@@ -152,7 +154,7 @@ func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
sfnt, err := font.ParseSFNT(bytes.Clone(data), 0) sfnt, err := parseSFNTFont(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -169,7 +171,7 @@ func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
usage := newEditorFontUsage() usage := newEditorFontUsage()
for _, id := range glyphs { for _, id := range glyphs {
usage.glyphs[id] = true usage.glyphs[id] = true
for _, char := range sfnt.Cmap.ToUnicode(id) { for _, char := range sfnt.glyphCharacters(id) {
usage.chars[char] = true usage.chars[char] = true
} }
} }
@@ -188,15 +190,7 @@ func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
} }
return fixed, nil return fixed, nil
} }
subset, err := sfnt.Subset(glyphs, font.SubsetOptions{Tables: []string{ return subsetCompactSFNTFont(sfnt, glyphs)
"cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "glyf", "loca", "cvt ", "fpgm", "prep", "gasp",
}})
if err != nil {
return nil, err
}
subset.Tables["name"] = sfnt.Tables["name"]
clear(subset.Tables["head"][8:12])
return serializeOTF(subset.Tables)
} }
// subsetSourceSFNTFont 裁剪静态TrueType原有字体,保留字形编号、复合依赖、度量及提示指令 // subsetSourceSFNTFont 裁剪静态TrueType原有字体,保留字形编号、复合依赖、度量及提示指令
@@ -207,7 +201,7 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
if usage.unsafe || len(usage.chars)+len(usage.glyphs) == 0 || bytes.HasPrefix(data, []byte("ttcf")) { if usage.unsafe || len(usage.chars)+len(usage.glyphs) == 0 || bytes.HasPrefix(data, []byte("ttcf")) {
return nil return nil
} }
sfnt, err := font.ParseSFNT(data, 0) sfnt, err := parseSFNTFont(data)
if err != nil || !sfnt.IsTrueType || fontEmbeddingFlags(sfnt)&0x0100 != 0 { if err != nil || !sfnt.IsTrueType || fontEmbeddingFlags(sfnt)&0x0100 != 0 {
return nil return nil
} }
@@ -218,20 +212,13 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
return nil return nil
} }
} }
var unicodeTables []uint16 var unicodeTables []sfntCmap
subtables := make(map[uint32]uint16) for _, record := range sfnt.cmaps {
for i, record := range sfnt.Cmap.EncodingRecords { if record.format == 14 {
if record.Format == 14 {
return nil return nil
} }
offset := binary.BigEndian.Uint32(sfnt.Tables["cmap"][8+8*i:]) if record.platform == 0 || record.platform == 3 && (record.encoding == 1 || record.encoding == 10) {
index, ok := subtables[offset] unicodeTables = append(unicodeTables, record)
if !ok {
index = uint16(len(subtables))
subtables[offset] = index
}
if record.PlatformID == 0 || record.PlatformID == 3 && (record.EncodingID == 1 || record.EncodingID == 10) {
unicodeTables = append(unicodeTables, index)
} }
} }
if len(unicodeTables) == 0 { if len(unicodeTables) == 0 {
@@ -242,8 +229,8 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
mapping := make(map[rune]uint16, len(usage.chars)) mapping := make(map[rune]uint16, len(usage.chars))
for char := range usage.chars { for char := range usage.chars {
id := sfnt.GlyphIndex(char) id := sfnt.GlyphIndex(char)
for _, index := range unicodeTables { for _, cmap := range unicodeTables {
if alternate, ok := sfnt.Cmap.Subtables[index].Get(char); ok && alternate != id { if alternate, ok := cmap.mapping[char]; ok && alternate != id {
return nil return nil
} }
} }
@@ -261,7 +248,7 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
if id >= sfnt.NumGlyphs() { if id >= sfnt.NumGlyphs() {
return nil return nil
} }
dependencies, err := sfnt.Glyf.Dependencies(id) dependencies, err := sfnt.glyphDependencies(id)
if err != nil { if err != nil {
return nil return nil
} }
@@ -271,13 +258,14 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
} }
var glyf, loca []byte var glyf, loca []byte
for id := 0; id <= int(sfnt.NumGlyphs()); id++ { for id := 0; id <= int(sfnt.NumGlyphs()); id++ {
if sfnt.Head.IndexToLocFormat == 0 { if binary.BigEndian.Uint16(sfnt.Tables["head"][50:]) == 0 {
loca = binary.BigEndian.AppendUint16(loca, uint16(len(glyf)/2)) loca = binary.BigEndian.AppendUint16(loca, uint16(len(glyf)/2))
} else { } else {
loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf))) loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf)))
} }
if id < int(sfnt.NumGlyphs()) && glyphs[uint16(id)] { if id < int(sfnt.NumGlyphs()) && glyphs[uint16(id)] {
glyf = append(glyf, sfnt.Glyf.Get(uint16(id))...) data, _ := trueTypeGlyphData(sfnt.Tables, uint16(id))
glyf = append(glyf, data...)
if len(glyf)%2 != 0 { if len(glyf)%2 != 0 {
glyf = append(glyf, 0) glyf = append(glyf, 0)
} }
@@ -295,3 +283,98 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
} }
return result return result
} }
// subsetCompactSFNTFont 重排新建静态字体的字形,同步复合引用、字符映射和度量
// 入参: sfnt 字体, glyphs 使用字形
// 返回: []byte 紧凑字体子集, error 字形数据错误
func subsetCompactSFNTFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) {
ids := []uint16{0}
remap := map[uint16]uint16{0: 0}
for _, id := range glyphs {
if id >= sfnt.NumGlyphs() {
return nil, fmt.Errorf("font glyph index out of range")
}
if _, ok := remap[id]; !ok {
remap[id] = uint16(len(ids))
ids = append(ids, id)
}
}
for index := 0; index < len(ids); index++ {
dependencies, err := sfnt.glyphDependencies(ids[index])
if err != nil {
return nil, err
}
for _, id := range dependencies {
if _, ok := remap[id]; !ok {
remap[id] = uint16(len(ids))
ids = append(ids, id)
}
}
}
table := make(map[string][]byte)
for _, tag := range []string{"head", "hhea", "maxp", "OS/2", "post", "name", "cvt ", "fpgm", "prep", "gasp", "vhea"} {
if data := sfnt.Tables[tag]; data != nil {
table[tag] = bytes.Clone(data)
}
}
var glyf, loca, hmtx, vmtx []byte
mapping := make(map[rune]uint16)
for _, id := range ids {
loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf)))
data, err := trueTypeGlyphData(sfnt.Tables, id)
if err != nil {
return nil, err
}
data = bytes.Clone(data)
if len(data) >= 10 && int16(binary.BigEndian.Uint16(data)) < 0 {
for pos := 10; ; {
flags := binary.BigEndian.Uint16(data[pos:])
child := binary.BigEndian.Uint16(data[pos+2:])
binary.BigEndian.PutUint16(data[pos+2:], remap[child])
pos += 6
if flags&1 != 0 {
pos += 2
}
switch flags & 0xc8 {
case 8:
pos += 2
case 0x40:
pos += 4
case 0x80:
pos += 8
}
if flags&0x20 == 0 {
break
}
}
}
glyf = append(glyf, data...)
for len(glyf)%4 != 0 {
glyf = append(glyf, 0)
}
hmtx = binary.BigEndian.AppendUint16(hmtx, sfnt.GlyphAdvance(id))
hmtx = binary.BigEndian.AppendUint16(hmtx, uint16(sfnt.sideBearing(id, false)))
if sfnt.vertical != 0 {
vmtx = binary.BigEndian.AppendUint16(vmtx, sfnt.GlyphVerticalAdvance(id))
vmtx = binary.BigEndian.AppendUint16(vmtx, uint16(sfnt.sideBearing(id, true)))
}
for _, char := range sfnt.glyphCharacters(id) {
mapping[char] = remap[id]
}
}
loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf)))
table["glyf"], table["loca"], table["hmtx"] = glyf, loca, hmtx
table["cmap"] = buildCmapTable(uint16(len(ids)), mapping)
binary.BigEndian.PutUint16(table["head"][50:], 1)
binary.BigEndian.PutUint16(table["hhea"][34:], uint16(len(ids)))
binary.BigEndian.PutUint16(table["maxp"][4:], uint16(len(ids)))
if len(table["post"]) >= 32 {
table["post"] = table["post"][:32]
binary.BigEndian.PutUint32(table["post"], 0x30000)
}
if sfnt.vertical != 0 {
table["vmtx"] = vmtx
binary.BigEndian.PutUint16(table["vhea"][34:], uint16(len(ids)))
}
return serializeOTF(table)
}
+323
View File
@@ -0,0 +1,323 @@
// 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 (
"encoding/binary"
"fmt"
"sort"
"sync"
"github.com/go-text/typesetting/font/opentype/tables"
)
// sfntFont 保存独立字体表和基础度量,不包含绘图库对象
type sfntFont struct {
Tables map[string][]byte
IsTrueType, IsCFF bool
cmaps []sfntCmap
units, count, horizontal, vertical uint16
reverseOnce sync.Once
reverse map[uint16][]rune
}
// sfntCmap 保存编码记录及对应字符映射
type sfntCmap struct {
platform, encoding, format uint16
mapping map[rune]uint16
}
// parseSFNTFont 校验独立字体的必需表、度量范围及字符映射
// 入参: data 独立SFNT数据
// 返回: *sfntFont 字体, error 格式错误
func parseSFNTFont(data []byte) (*sfntFont, error) {
table, err := fontFileTables(data, 0)
if err != nil {
return nil, err
}
head, maxp, hhea := table["head"], table["maxp"], table["hhea"]
if len(head) != 54 || len(maxp) < 6 || len(hhea) < 36 {
return nil, fmt.Errorf("missing or truncated font metrics")
}
f := &sfntFont{Tables: table, IsTrueType: table["glyf"] != nil, IsCFF: table["CFF "] != nil}
f.units, f.count = binary.BigEndian.Uint16(head[18:]), binary.BigEndian.Uint16(maxp[4:])
f.horizontal = binary.BigEndian.Uint16(hhea[34:])
if f.units < 16 || f.units > 16384 || f.count == 0 || f.IsTrueType == f.IsCFF || !validSFNTMetrics(table["hmtx"], f.count, f.horizontal) {
return nil, fmt.Errorf("invalid font outlines or horizontal metrics")
}
if vhea := table["vhea"]; vhea != nil {
if len(vhea) < 36 {
return nil, fmt.Errorf("truncated vertical metrics header")
}
f.vertical = binary.BigEndian.Uint16(vhea[34:])
if !validSFNTMetrics(table["vmtx"], f.count, f.vertical) {
return nil, fmt.Errorf("invalid vertical metrics")
}
}
if f.IsTrueType {
format := binary.BigEndian.Uint16(head[50:])
if format > 1 || len(table["loca"]) < (int(f.count)+1)*(2+2*int(format)) {
return nil, fmt.Errorf("invalid glyph location table")
}
}
f.cmaps, err = readSFNTCmaps(table["cmap"])
if err != nil {
return nil, err
}
for _, cmap := range f.cmaps {
for _, glyph := range cmap.mapping {
if glyph >= f.count {
return nil, fmt.Errorf("cmap glyph index out of range")
}
}
}
return f, nil
}
// validSFNTMetrics 校验完整度量记录和末尾边距数组的范围
// 入参: data 度量表, count 字形数, metrics 完整度量记录数
// 返回: bool 是否有效
func validSFNTMetrics(data []byte, count, metrics uint16) bool {
return metrics != 0 && metrics <= count && len(data) >= int(metrics)*4+int(count-metrics)*2
}
// readSFNTCmaps 校验各编码子表并保留原顺序,共享子表只解析一次
// 入参: data cmap表
// 返回: []sfntCmap 编码记录, error 表结构错误
func readSFNTCmaps(data []byte) ([]sfntCmap, error) {
data, err := normalizeCmap(data)
if err != nil {
return nil, err
}
if _, _, err := tables.ParseCmap(data); err != nil {
return nil, err
}
count := int(binary.BigEndian.Uint16(data[2:]))
result := make([]sfntCmap, 0, count)
parsed := make(map[uint32]map[rune]uint16)
for i := range count {
record := data[4+8*i:]
offset := binary.BigEndian.Uint32(record[4:])
if uint64(offset)+2 > uint64(len(data)) {
return nil, fmt.Errorf("invalid cmap offset")
}
sub := data[offset:]
format := binary.BigEndian.Uint16(sub)
mapping, found := parsed[offset]
if !found {
mapping = make(map[rune]uint16)
var err error
switch format {
case 0:
parseCmapFormat0(sub, mapping)
case 4:
parseCmapFormat4(sub, mapping)
case 6:
parseCmapFormat6(sub, mapping)
case 12:
parseCmapFormat12(sub, mapping)
case 2:
mapping, err = parseCmapFormat2(sub)
case 8, 10, 13:
mapping, err = parseCmapExtended(sub)
case 14:
default:
return nil, fmt.Errorf("unsupported cmap format %d", format)
}
if err != nil {
return nil, err
}
parsed[offset] = mapping
}
result = append(result, sfntCmap{binary.BigEndian.Uint16(record), binary.BigEndian.Uint16(record[2:]), format, mapping})
}
return result, nil
}
// UnitsPerEm 返回字体设计单位
// 返回: uint16 每em单位数
func (f *sfntFont) UnitsPerEm() uint16 { return f.units }
// NumGlyphs 返回字形总数
// 返回: uint16 字形数
func (f *sfntFont) NumGlyphs() uint16 { return f.count }
// GlyphIndex 按编码表顺序查找字符,不替换缺失字形
// 入参: char 字符
// 返回: uint16 字形编号
func (f *sfntFont) GlyphIndex(char rune) uint16 {
for _, cmap := range f.cmaps {
if gid := cmap.mapping[char]; gid != 0 {
return gid
}
}
return 0
}
// glyphCharacters 返回指向指定字形的全部字符
// 入参: gid 字形编号
// 返回: []rune 排序后的字符
func (f *sfntFont) glyphCharacters(gid uint16) []rune {
f.reverseOnce.Do(func() {
f.reverse = make(map[uint16][]rune)
seen := make(map[rune]bool)
for _, cmap := range f.cmaps {
for char, glyph := range cmap.mapping {
if glyph != 0 && !seen[char] {
f.reverse[glyph] = append(f.reverse[glyph], char)
seen[char] = true
}
}
}
for _, chars := range f.reverse {
sort.Slice(chars, func(i, j int) bool { return chars[i] < chars[j] })
}
})
return f.reverse[gid]
}
// GlyphAdvance 返回水平字宽
// 入参: gid 字形编号
// 返回: uint16 字宽
func (f *sfntFont) GlyphAdvance(gid uint16) uint16 {
if gid >= f.count {
return 0
}
return binary.BigEndian.Uint16(f.Tables["hmtx"][int(min(gid, f.horizontal-1))*4:])
}
// GlyphVerticalAdvance 返回纵向字距,无垂直度量时使用em高度
// 入参: gid 字形编号
// 返回: uint16 纵向字距
func (f *sfntFont) GlyphVerticalAdvance(gid uint16) uint16 {
if f.vertical == 0 {
return f.units
}
if gid >= f.count {
return 0
}
return binary.BigEndian.Uint16(f.Tables["vmtx"][int(min(gid, f.vertical-1))*4:])
}
// sideBearing 返回指定方向的字形边距
// 入参: gid 字形编号, vertical 是否读取顶部边距
// 返回: int16 边距
func (f *sfntFont) sideBearing(gid uint16, vertical bool) int16 {
data, count := f.Tables["hmtx"], f.horizontal
if vertical {
data, count = f.Tables["vmtx"], f.vertical
}
if count == 0 || gid >= f.count {
return 0
}
offset := int(gid)*4 + 2
if gid >= count {
offset = int(count)*4 + int(gid-count)*2
}
return int16(binary.BigEndian.Uint16(data[offset:]))
}
// VerticalMetrics 依据OS/2排版标志选择行度量,缺失时使用hhea
// 返回: uint16 上升高度, uint16 下降高度, uint16 行间隙
func (f *sfntFont) VerticalMetrics() (uint16, uint16, uint16) {
hhea, os2 := f.Tables["hhea"], f.Tables["OS/2"]
a, d, gap := int(int16(binary.BigEndian.Uint16(hhea[4:]))), int(int16(binary.BigEndian.Uint16(hhea[6:]))), int(int16(binary.BigEndian.Uint16(hhea[8:])))
if len(os2) >= 78 {
if binary.BigEndian.Uint16(os2[62:])&0x80 != 0 {
ta, td := int(int16(binary.BigEndian.Uint16(os2[68:]))), int(int16(binary.BigEndian.Uint16(os2[70:])))
if ta > 0 && td < 0 {
a, d, gap = ta, td, int(int16(binary.BigEndian.Uint16(os2[72:])))
}
} else {
wa, wd := int(binary.BigEndian.Uint16(os2[74:])), int(binary.BigEndian.Uint16(os2[76:]))
if wa != 0 && wd != 0 {
gap, a, d = a-d+gap-wa-wd, wa, -wd
}
}
}
return uint16(max(0, a)), uint16(max(0, -d)), uint16(min(65535, max(0, gap)))
}
// Write 保留原始表数据并重新计算字体校验和
// 返回: []byte 独立字体
func (f *sfntFont) Write() []byte { data, _ := serializeOTF(f.Tables); return data }
// glyphDependencies 收集复合字形依赖,拒绝循环引用和截断记录
// 入参: gid 字形编号
// 返回: []uint16 依赖字形, error 字形错误
func (f *sfntFont) glyphDependencies(gid uint16) ([]uint16, error) {
seen, active := make(map[uint16]bool), make(map[uint16]bool)
var result []uint16
var visit func(uint16) error
visit = func(id uint16) error {
if id >= f.count || active[id] || len(active) > 64 {
return fmt.Errorf("invalid composite glyph reference")
}
if seen[id] {
return nil
}
seen[id], active[id] = true, true
defer delete(active, id)
data, err := trueTypeGlyphData(f.Tables, id)
if err != nil {
return err
}
if len(data) == 0 {
return nil
}
if len(data) < 10 {
return fmt.Errorf("truncated glyph header")
}
if int16(binary.BigEndian.Uint16(data)) >= 0 {
return nil
}
for pos := 10; ; {
if len(data)-pos < 4 {
return fmt.Errorf("truncated composite glyph")
}
flags, child := binary.BigEndian.Uint16(data[pos:]), binary.BigEndian.Uint16(data[pos+2:])
pos += 6
if flags&1 != 0 {
pos += 2
}
switch flags & 0xc8 {
case 8:
pos += 2
case 0x40:
pos += 4
case 0x80:
pos += 8
case 0:
default:
return fmt.Errorf("invalid composite transform")
}
if pos > len(data) {
return fmt.Errorf("truncated composite transform")
}
if !seen[child] {
result = append(result, child)
}
if err := visit(child); err != nil {
return err
}
if flags&0x20 == 0 {
break
}
}
return nil
}
err := visit(gid)
return result, err
}
+52 -22
View File
@@ -26,8 +26,6 @@ import (
"runtime" "runtime"
"sort" "sort"
"sync" "sync"
"github.com/tdewolff/font"
) )
// 字体来源:文件、文件系统和系统字体 // 字体来源:文件、文件系统和系统字体
@@ -37,11 +35,8 @@ const (
fontSourceSystem fontSourceSystem
) )
// systemFontIndex 串行初始化系统字体索引,不注册绘图后端 // systemFontIndex 按需建立只读系统字体索引
var systemFontIndex struct { var systemFontIndex = sync.OnceValue(indexSystemFonts)
sync.Mutex
fonts *font.SystemFonts
}
// fontSourceKind 字体来源类型 // fontSourceKind 字体来源类型
type fontSourceKind uint8 type fontSourceKind uint8
@@ -100,20 +95,11 @@ func (c *fontSourceCache) child() *fontSourceCache {
func (r *Renderer) readFontSource(source fontSource, definition *Font) ([]byte, error) { func (r *Renderer) readFontSource(source fontSource, definition *Font) ([]byte, error) {
key := fontSourceKey{kind: source.kind, index: source.index, face: source.face, name: source.name} key := fontSourceKey{kind: source.kind, index: source.index, face: source.face, name: source.name}
if source.kind == fontSourceSystem { if source.kind == fontSourceSystem {
systemFontIndex.Lock() matches := fontFileMatches(systemFontIndex(), []string{source.name}, definition.Bold, definition.Italic)
if systemFontIndex.fonts == nil { if len(matches) == 0 {
systemFontIndex.fonts, _ = font.FindSystemFonts(font.DefaultFontDirs())
}
weight := 400
if definition.Bold {
weight = 700
}
match, ok := systemFontIndex.fonts.Match(source.name, font.ParseStyleCSS(weight, definition.Italic))
systemFontIndex.Unlock()
if !ok {
return nil, fmt.Errorf("font %q not found", source.name) return nil, fmt.Errorf("font %q not found", source.name)
} }
key = fontSourceKey{kind: fontSourceFile, name: match.Filename} key = fontSourceKey{kind: fontSourceFile, name: matches[0].name, face: matches[0].face}
} }
if cached, ok := r.fontSourcesCache.data[key]; ok { if cached, ok := r.fontSourcesCache.data[key]; ok {
return cached.data, cached.err return cached.data, cached.err
@@ -349,15 +335,59 @@ func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []f
// systemFontDirs 获取系统字体目录 // systemFontDirs 获取系统字体目录
// 返回: []string 字体目录列表 // 返回: []string 字体目录列表
func systemFontDirs() []string { func systemFontDirs() []string {
home, _ := os.UserHomeDir()
switch runtime.GOOS { switch runtime.GOOS {
case "android": case "android":
return []string{"/product/fonts", "/system/fonts"} return []string{"/product/fonts", "/system/fonts"}
case "darwin": case "darwin":
return []string{`/Library/Fonts`} return []string{"/System/Library/Fonts", "/Library/Fonts", filepath.Join(home, "Library", "Fonts")}
case "linux": case "linux":
return []string{`/usr/share/fonts`} dirs := []string{"/usr/share/fonts", "/usr/local/share/fonts", filepath.Join(home, ".fonts")}
data := os.Getenv("XDG_DATA_HOME")
if data == "" {
data = filepath.Join(home, ".local", "share")
}
dirs = append(dirs, filepath.Join(data, "fonts"))
for _, dir := range filepath.SplitList(os.Getenv("XDG_DATA_DIRS")) {
dirs = append(dirs, filepath.Join(dir, "fonts"))
}
return dirs
case "windows": case "windows":
return []string{`C:\Windows\Fonts`} windows := os.Getenv("WINDIR")
if windows == "" {
windows = `C:\Windows`
}
dirs := []string{filepath.Join(windows, "Fonts")}
if local := os.Getenv("LOCALAPPDATA"); local != "" {
dirs = append(dirs, filepath.Join(local, "Microsoft", "Windows", "Fonts"))
}
return dirs
} }
return nil return nil
} }
// indexSystemFonts 递归读取系统字体名称,保留集合索引
// 返回: []fontFileCandidate 字体候选
func indexSystemFonts() []fontFileCandidate {
var files []string
seen := make(map[string]bool)
for _, dir := range systemFontDirs() {
_ = filepath.WalkDir(dir, func(name string, entry fs.DirEntry, err error) error {
if err == nil && !entry.IsDir() && isFontFileName(name) && !seen[name] {
seen[name] = true
files = append(files, name)
}
return nil
})
}
candidates := fontFileCandidates(files, filepath.Base)
for i, candidate := range candidates {
file, err := os.Open(candidate.name)
if err != nil {
continue
}
candidates = appendFontFileNames(candidates, i, fontFileNames(file))
file.Close()
}
return candidates
}
+4 -6
View File
@@ -17,14 +17,12 @@ package ofdgo
import ( import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"github.com/tdewolff/font"
) )
// variableSFNTSubsettable 核对TrueType可变轴与字形变化表的基本结构 // variableSFNTSubsettable 核对TrueType可变轴与字形变化表的基本结构
// 入参: sfnt 已解析字体 // 入参: sfnt 已解析字体
// 返回: bool 是否具备可裁剪的变化数据 // 返回: bool 是否具备可裁剪的变化数据
func variableSFNTSubsettable(sfnt *font.SFNT) bool { func variableSFNTSubsettable(sfnt *sfntFont) bool {
fvar, gvar := sfnt.Tables["fvar"], sfnt.Tables["gvar"] fvar, gvar := sfnt.Tables["fvar"], sfnt.Tables["gvar"]
if len(fvar) < 16 || len(gvar) < 20 || binary.BigEndian.Uint32(fvar) != 0x10000 || binary.BigEndian.Uint32(gvar) != 0x10000 { if len(fvar) < 16 || len(gvar) < 20 || binary.BigEndian.Uint32(fvar) != 0x10000 || binary.BigEndian.Uint32(gvar) != 0x10000 {
return false return false
@@ -38,7 +36,7 @@ func variableSFNTSubsettable(sfnt *font.SFNT) bool {
// subsetVariableSFNTFont 保留字形编号及可变轴,仅裁剪未使用的轮廓和字形变化数据 // subsetVariableSFNTFont 保留字形编号及可变轴,仅裁剪未使用的轮廓和字形变化数据
// 入参: sfnt 已解析字体, glyphs 使用的字形 // 入参: sfnt 已解析字体, glyphs 使用的字形
// 返回: []byte 字体子集, error 字形或变化表错误 // 返回: []byte 字体子集, error 字形或变化表错误
func subsetVariableSFNTFont(sfnt *font.SFNT, glyphs []uint16) ([]byte, error) { func subsetVariableSFNTFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) {
if !variableSFNTSubsettable(sfnt) { if !variableSFNTSubsettable(sfnt) {
return nil, fmt.Errorf("invalid TrueType variation tables") return nil, fmt.Errorf("invalid TrueType variation tables")
} }
@@ -49,12 +47,12 @@ func subsetVariableSFNTFont(sfnt *font.SFNT, glyphs []uint16) ([]byte, error) {
return nil, fmt.Errorf("variable font glyph index out of range") return nil, fmt.Errorf("variable font glyph index out of range")
} }
usage.glyphs[gid] = true usage.glyphs[gid] = true
for _, char := range sfnt.Cmap.ToUnicode(gid) { for _, char := range sfnt.glyphCharacters(gid) {
usage.chars[char] = true usage.chars[char] = true
} }
} }
for gid := range usage.glyphs { for gid := range usage.glyphs {
dependencies, err := sfnt.Glyf.Dependencies(gid) dependencies, err := sfnt.glyphDependencies(gid)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+248
View File
@@ -0,0 +1,248 @@
// 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 (
"bytes"
"compress/zlib"
"encoding/binary"
"fmt"
"io"
"sort"
"github.com/andybalholm/brotli"
)
// fontContainerLimit 限制压缩字体展开后的表数据总量
const fontContainerLimit = 256 << 20
// woff2Tags 保存WOFF2表目录的标准标签顺序
var woff2Tags = [...]string{
"cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post",
"cvt ", "fpgm", "glyf", "loca", "prep", "CFF ", "VORG", "EBDT",
"EBLC", "gasp", "hdmx", "kern", "LTSH", "PCLT", "VDMX", "vhea",
"vmtx", "BASE", "GDEF", "GPOS", "GSUB", "EBSC", "JSTF", "MATH",
"CBDT", "CBLC", "COLR", "CPAL", "SVG ", "sbix", "acnt", "avar",
"bdat", "bloc", "bsln", "cvar", "fdsc", "feat", "fmtx", "fvar",
"gvar", "hsty", "just", "lcar", "mort", "morx", "opbd", "prop",
"trak", "Zapf", "Silf", "Glat", "Gloc", "Feat", "Sill",
}
// decodeWOFF2 展开未变换的WOFF2表,字体集合及字形变换交由绘图适配器处理
// 入参: data WOFF2文件
// 返回: []byte OpenType数据, bool 是否已处理, error 封装错误
func decodeWOFF2(data []byte) ([]byte, bool, error) {
if len(data) < 48 || string(data[:4]) != "wOF2" || uint64(binary.BigEndian.Uint32(data[8:])) != uint64(len(data)) {
return nil, true, fmt.Errorf("invalid WOFF2 header")
}
if string(data[4:8]) == "ttcf" {
return nil, false, nil
}
count := int(binary.BigEndian.Uint16(data[12:]))
if count == 0 || count > (len(data)-48)/2 {
return nil, true, fmt.Errorf("invalid WOFF2 table count")
}
tags := make([]string, 0, count)
sizes := make([]uint32, 0, count)
table := make(map[string][]byte, count)
pos, total, transformed := 48, uint64(0), false
for range count {
if pos >= len(data) {
return nil, true, fmt.Errorf("truncated WOFF2 directory")
}
flags := data[pos]
pos++
var tag string
if index := flags & 63; index == 63 {
if len(data)-pos < 4 {
return nil, true, fmt.Errorf("truncated WOFF2 tag")
}
tag = string(data[pos : pos+4])
pos += 4
} else {
tag = woff2Tags[index]
}
if _, exists := table[tag]; exists {
return nil, true, fmt.Errorf("duplicate WOFF2 table %q", tag)
}
table[tag] = nil
size, err := readWOFF2Base128(data, &pos)
if err != nil {
return nil, true, err
}
version := flags >> 6
changed := version != 0
if tag == "glyf" || tag == "loca" {
if version != 0 && version != 3 {
return nil, true, fmt.Errorf("invalid WOFF2 glyph transform")
}
changed = version == 0
} else if version != 0 && (tag != "hmtx" || version != 1) {
return nil, true, fmt.Errorf("invalid WOFF2 table transform %q", tag)
}
if changed {
transformed = true
size, err = readWOFF2Base128(data, &pos)
if err != nil {
return nil, true, err
}
if tag == "loca" && size != 0 {
return nil, true, fmt.Errorf("invalid WOFF2 loca length")
}
}
total += uint64(size)
if total+uint64(12+19*count) > fontContainerLimit {
return nil, true, fmt.Errorf("WOFF2 font exceeds size limit")
}
tags = append(tags, tag)
sizes = append(sizes, size)
}
end := uint64(pos) + uint64(binary.BigEndian.Uint32(data[20:]))
if end > uint64(len(data)) {
return nil, true, fmt.Errorf("invalid WOFF2 compressed length")
}
if err := validateWOFFBlocks(data, end, 28); err != nil {
return nil, true, err
}
if transformed {
return nil, false, nil
}
reader := brotli.NewReader(bytes.NewReader(data[pos:end]))
body, err := io.ReadAll(io.LimitReader(reader, int64(total)+1))
if err != nil {
return nil, true, fmt.Errorf("decode WOFF2 data: %w", err)
}
if uint64(len(body)) != total {
return nil, true, fmt.Errorf("invalid WOFF2 expanded length")
}
for i, tag := range tags {
size := int(sizes[i])
table[tag], body = body[:size], body[size:]
}
decoded, err := serializeOTF(table)
return decoded, true, err
}
// readWOFF2Base128 读取WOFF2无符号变长整数,拒绝溢出和前导零
// 入参: data 编码数据, pos 当前读取位置
// 返回: uint32 整数值, error 编码错误
func readWOFF2Base128(data []byte, pos *int) (uint32, error) {
var value uint32
for i := range 5 {
if *pos >= len(data) {
return 0, fmt.Errorf("truncated WOFF2 integer")
}
b := data[*pos]
*pos++
if i == 0 && b == 0x80 || value > 0x1ffffff {
return 0, fmt.Errorf("invalid WOFF2 integer")
}
value = value<<7 | uint32(b&127)
if b&128 == 0 {
return value, nil
}
}
return 0, fmt.Errorf("invalid WOFF2 integer")
}
// decodeWOFF 展开WOFF表数据,不解析或更改字体轮廓
// 入参: data WOFF文件
// 返回: []byte OpenType数据, error 封装错误
func decodeWOFF(data []byte) ([]byte, error) {
if len(data) < 44 || uint64(binary.BigEndian.Uint32(data[8:])) != uint64(len(data)) || binary.BigEndian.Uint16(data[14:]) != 0 {
return nil, fmt.Errorf("invalid WOFF header")
}
count := int(binary.BigEndian.Uint16(data[12:]))
if count == 0 || count > (len(data)-44)/20 {
return nil, fmt.Errorf("invalid WOFF table count")
}
table := make(map[string][]byte, count)
ranges := make([][2]uint64, 0, count)
total := uint64(12 + 16*count)
for i := range count {
record := data[44+i*20:]
tag := string(record[:4])
offset, packed, size := uint64(binary.BigEndian.Uint32(record[4:])), uint64(binary.BigEndian.Uint32(record[8:])), uint64(binary.BigEndian.Uint32(record[12:]))
total += (size + 3) &^ 3
if _, exists := table[tag]; exists || offset < uint64(44+20*count) || offset%4 != 0 || packed > size || offset+packed > uint64(len(data)) || total > fontContainerLimit {
return nil, fmt.Errorf("invalid WOFF table %q", tag)
}
body := data[offset : offset+packed]
if packed < size {
reader, err := zlib.NewReader(bytes.NewReader(body))
if err != nil {
return nil, err
}
body, err = io.ReadAll(io.LimitReader(reader, int64(size)+1))
reader.Close()
if err != nil {
return nil, err
}
if uint64(len(body)) != size {
return nil, fmt.Errorf("invalid WOFF expanded length")
}
}
table[tag] = body
ranges = append(ranges, [2]uint64{offset, offset + packed})
}
if total != uint64(binary.BigEndian.Uint32(data[16:])) {
return nil, fmt.Errorf("invalid WOFF SFNT size")
}
sort.Slice(ranges, func(i, j int) bool { return ranges[i][0] < ranges[j][0] })
end := uint64(44 + 20*count)
for _, interval := range ranges {
if interval[0] != (end+3)&^3 {
return nil, fmt.Errorf("invalid WOFF table ranges")
}
end = interval[1]
}
if err := validateWOFFBlocks(data, end, 24); err != nil {
return nil, err
}
return serializeOTF(table)
}
// validateWOFFBlocks 校验附加数据块的边界及对齐,字体元数据不写入输出
// 入参: data 字体文件, end 字体数据结束位置, fields 元数据偏移字段位置
// 返回: error 附加块错误
func validateWOFFBlocks(data []byte, end uint64, fields int) error {
for _, pos := range []int{fields, fields + 12} {
offset, length := uint64(binary.BigEndian.Uint32(data[pos:])), uint64(binary.BigEndian.Uint32(data[pos+4:]))
if offset == 0 {
if length != 0 || pos == fields && binary.BigEndian.Uint32(data[pos+8:]) != 0 {
return fmt.Errorf("invalid WOFF optional block")
}
continue
}
if length == 0 || offset != (end+3)&^3 || offset+length > uint64(len(data)) {
return fmt.Errorf("invalid WOFF block range")
}
for _, b := range data[end:offset] {
if b != 0 {
return fmt.Errorf("invalid WOFF padding")
}
}
end = offset + length
}
if end > uint64(len(data)) || uint64(len(data)) > (end+3)&^3 {
return fmt.Errorf("invalid WOFF trailing data")
}
for _, b := range data[end:] {
if b != 0 {
return fmt.Errorf("invalid WOFF padding")
}
}
return nil
}
+15 -1
View File
@@ -258,5 +258,19 @@ func init() {
// 返回: RenderBackends 默认后端组合 // 返回: RenderBackends 默认后端组合
func defaultRenderBackends() RenderBackends { func defaultRenderBackends() RenderBackends {
backend := CanvasBackend{} backend := CanvasBackend{}
return RenderBackends{FontResources: SFNTBackend{}, Fonts: backend, Geometry: backend, Compiler: backend, Raster: backend, SVG: backend, PDF: backend, EPS: backend} return RenderBackends{FontResources: SFNTBackend{}, Fonts: SFNTBackend{}, Geometry: backend, Compiler: backend, Raster: backend, SVG: backend, PDF: backend, EPS: backend}
}
// OpenFont 使用独立字体后端读取字体度量
// 入参: data 字体数据
// 返回: FontMetrics 字体度量, error 解析错误
func (CanvasBackend) OpenFont(data []byte) (FontMetrics, error) {
return (SFNTBackend{}).OpenFont(data)
}
// ResolveFont 使用独立字体后端解析字体来源
// 入参: r 渲染器, id 字体标识, exact 是否禁止无关回退
// 返回: ResolvedFont 字体与来源, error 解析错误
func (CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFont, error) {
return (SFNTBackend{}).ResolveFont(r, id, exact)
} }
+26 -120
View File
@@ -19,135 +19,41 @@ import (
"sync" "sync"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
"github.com/tdewolff/font"
) )
// canvasGlyphBoundsLimit 限制单个字体保留的字形与字号边界数量 // canvasFontLoadMu 保护绘图后端加载无名称字体时使用的共享状态
const canvasGlyphBoundsLimit = 4096 var canvasFontLoadMu sync.Mutex
// canvasFontMetrics 保留默认字体度量,写出时不更新原始时间 // loadCanvasFont 串行加载绘图字体,不改动原始字体数据
type canvasFontMetrics struct { // 入参: data 字体数据, index 集合索引, style 字体样式
*font.SFNT // 返回: *canvas.Font 绘图字体, error 加载错误
*fontShaper func loadCanvasFont(data []byte, index int, style canvas.FontStyle) (*canvas.Font, error) {
outlines *sfntOutliner canvasFontLoadMu.Lock()
bounds *canvasFontBounds defer canvasFontLoadMu.Unlock()
return canvas.LoadFont(data, index, style)
} }
// canvasFontBounds 缓存已成功提取的字形边界,不保留轮廓或失败结果 // decodeFontContainer 展开字体封装,保留原始OpenType表及CFF2轮廓
type canvasFontBounds struct { // 入参: data 字体文件
mu sync.Mutex // 返回: []byte OpenType数据, error 格式错误
values map[canvasGlyphSize]Box func decodeFontContainer(data []byte) ([]byte, error) {
if len(data) < 12 {
return nil, fmt.Errorf("invalid font header")
} }
switch string(data[:4]) {
// canvasGlyphSize 区分字形编号及精确毫米字号 case "\x00\x01\x00\x00", "OTTO", "true", "ttcf":
type canvasGlyphSize struct { return data, nil
glyph uint16 case "wOFF":
size float64 return decodeWOFF(data)
case "wOF2":
decoded, handled, err := decodeWOFF2(data)
if handled {
return decoded, err
} }
// ResolveFont 按需加载字体资源,保留内嵌数据及外部字体的名称、样式和集合索引匹配
// 入参: r 渲染器, id 字体标识, exact 是否禁止无关回退
// 返回: ResolvedFont 字体与来源, error 字体不可用错误
func (b CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFont, error) {
return r.resolveFontSource(b, id, exact)
} }
loaded, err := loadCanvasFont(data, 0, canvas.FontRegular)
// OpenFont 解析独立SFNT数据,度量与原有编辑排版保持一致
// 入参: data 字体数据
// 返回: FontMetrics 字体度量, error 解析错误
func (CanvasBackend) OpenFont(data []byte) (FontMetrics, error) {
data, err := defaultCFF2Font(data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
sfnt, err := font.ParseSFNT(data, 0) return serializeOTF(loaded.Tables)
if err != nil {
return nil, err
}
metrics := &canvasFontMetrics{SFNT: sfnt, outlines: &sfntOutliner{font: sfnt}, bounds: &canvasFontBounds{}}
metrics.fontShaper = &fontShaper{metrics: metrics}
return metrics, nil
}
// Write 返回字体数据,不修改字体元信息
// 返回: []byte 独立SFNT数据
func (f canvasFontMetrics) Write() []byte {
return fontSFNTData(f.SFNT)
}
// GlyphOutline 返回基线原点、纵轴向下的字形轮廓
// 入参: glyph 字形编号, size 字号,单位为毫米
// 返回: GeometryPath 字形路径, error 字形解析错误
func (f canvasFontMetrics) GlyphOutline(glyph uint16, size float64) (GeometryPath, error) {
if !rasterPositive(size) || glyph >= f.NumGlyphs() {
return nil, fmt.Errorf("invalid glyph or size")
}
path := &canvas.Path{}
if err := f.outlines.path(path, glyph, size/float64(f.UnitsPerEm())); err != nil {
return nil, err
}
return *geometryFromCanvasPath(path), nil
}
// GlyphBounds 返回字形轮廓的精确边界,重复编号及字号复用有限缓存
// 入参: glyph 字形编号, size 字号,单位为毫米
// 返回: Box 字形边界, error 字形解析错误
func (f canvasFontMetrics) GlyphBounds(glyph uint16, size float64) (Box, error) {
key := canvasGlyphSize{glyph: glyph, size: size}
if f.bounds != nil {
f.bounds.mu.Lock()
value, ok := f.bounds.values[key]
f.bounds.mu.Unlock()
if ok {
return value, nil
}
}
outline, err := f.GlyphOutline(glyph, size)
if err != nil {
return Box{}, err
}
value, err := outline.Bounds()
if err != nil {
return Box{}, err
}
if f.bounds != nil {
f.bounds.mu.Lock()
if f.bounds.values == nil || len(f.bounds.values) >= canvasGlyphBoundsLimit {
f.bounds.values = make(map[canvasGlyphSize]Box)
}
f.bounds.values[key] = value
f.bounds.mu.Unlock()
}
return value, nil
}
// GlyphWarning 返回无效字形指令后保留设计轮廓的提示,原始字体不变
// 入参: glyph 字形编号
// 返回: error 恢复原因,无恢复时为空
func (f canvasFontMetrics) GlyphWarning(glyph uint16) error {
return f.outlines.glyphWarning(glyph)
}
// GlyphOutlines 批量提取字形轮廓,重复编号只解析一次
// 入参: glyphs 字形编号, size 毫米字号
// 返回: []GeometryPath 同序只读轮廓, error 轮廓错误
func (f canvasFontMetrics) GlyphOutlines(glyphs []uint16, size float64) ([]GeometryPath, error) {
if !rasterPositive(size) {
return nil, fmt.Errorf("invalid glyph size")
}
result := make([]GeometryPath, len(glyphs))
paths := make(map[uint16]GeometryPath, len(glyphs))
for i, glyph := range glyphs {
path, ok := paths[glyph]
if !ok {
var err error
path, err = f.GlyphOutline(glyph, size)
if err != nil {
return nil, err
}
paths[glyph] = path
}
result[i] = path
}
return result, nil
} }
+4 -1
View File
@@ -51,7 +51,10 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
return ff return ff
} }
ff := canvas.NewFontFamily(fmt.Sprintf("ofdgo-%x-%d", resolved.digest, fontStyle)) ff := canvas.NewFontFamily(fmt.Sprintf("ofdgo-%x-%d", resolved.digest, fontStyle))
if err := ff.LoadFont(fontData, 0, fontStyle); err != nil { canvasFontLoadMu.Lock()
err = ff.LoadFont(fontData, 0, fontStyle)
canvasFontLoadMu.Unlock()
if err != nil {
r.renderError = err r.renderError = err
return nil return nil
} }
+6 -1
View File
@@ -103,7 +103,12 @@ func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (
} }
outlines := r.canvasState().fontOutlines[face.Font] outlines := r.canvasState().fontOutlines[face.Font]
if outlines == nil { if outlines == nil {
outlines = &sfntOutliner{font: face.Font.SFNT} metrics, err := parseSFNTFont(fontSFNTData(face.Font.Tables))
if err != nil {
r.renderError = err
return &canvas.Path{}, 0
}
outlines = &sfntOutliner{font: metrics}
r.canvasState().fontOutlines[face.Font] = outlines r.canvasState().fontOutlines[face.Font] = outlines
} }
path, width, err := textGlyphPath(face, glyph, outlines) path, width, err := textGlyphPath(face, glyph, outlines)
+2 -3
View File
@@ -17,7 +17,6 @@ package ofdgo
import ( import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
canvastext "github.com/tdewolff/canvas/text" canvastext "github.com/tdewolff/canvas/text"
canvasfont "github.com/tdewolff/font"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -52,12 +51,12 @@ func (r *pdfRenderer) preserveText(first int, text string) {
func (r *pdfRenderer) glyphText(face *canvas.FontFace, glyph textGlyph) (*canvas.Text, error) { func (r *pdfRenderer) glyphText(face *canvas.FontFace, glyph textGlyph) (*canvas.Text, error) {
font := r.fonts[face.Font] font := r.fonts[face.Font]
if font == nil { if font == nil {
sfnt, err := canvasfont.ParseSFNT(fontSFNTData(face.Font.SFNT), 0) loaded, err := loadCanvasFont(fontSFNTData(face.Font.Tables), 0, face.Font.Style())
if err != nil { if err != nil {
return nil, err return nil, err
} }
copy := *face.Font copy := *face.Font
copy.SFNT = sfnt copy.SFNT = loaded.SFNT
font = &copy font = &copy
if r.fonts == nil { if r.fonts == nil {
r.fonts = make(map[*canvas.Font]*canvas.Font) r.fonts = make(map[*canvas.Font]*canvas.Font)
+1 -1
View File
@@ -146,7 +146,7 @@ func (s *svgResourceRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
font := text.MostCommonFontFace().Font font := text.MostCommonFontFace().Font
resource, ok := s.renderer.canvasState().svgFontCache[font] resource, ok := s.renderer.canvasState().svgFontCache[font]
if !ok { if !ok {
data := fontSFNTData(font.SFNT) data := fontSFNTData(font.Tables)
resource = SVGFont{ resource = SVGFont{
Name: fmt.Sprintf("ofdgo-%x-%d", sha256.Sum256(data), font.Style()), Name: fmt.Sprintf("ofdgo-%x-%d", sha256.Sum256(data), font.Style()),
Weight: font.Style().CSS(), Weight: font.Style().CSS(),
+2 -2
View File
@@ -18,7 +18,6 @@ import (
"strings" "strings"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
"github.com/tdewolff/font"
) )
// ptPerMM 为毫米转换为排版点的比例 // ptPerMM 为毫米转换为排版点的比例
@@ -183,7 +182,8 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
ctx.Pop() ctx.Pop()
return return
} }
face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal, font.NoHinting) face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal)
face.Hinting = 0
recoverBaseline := len(obj.TextCode) > 0 && obj.TextCode[0].Y == "" recoverBaseline := len(obj.TextCode) > 0 && obj.TextCode[0].Y == ""
var metrics canvas.FontMetrics var metrics canvas.FontMetrics
if textRun != nil || recoverBaseline { if textRun != nil || recoverBaseline {