From cf8dfd046bb079617cdda65ad43da808bcf60fef Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Sun, 11 Oct 2026 00:52:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AD=97=E4=BD=93=E8=A7=A3=E8=80=A6):=20?= =?UTF-8?q?=E8=A7=A3=E8=80=A6=E5=AD=97=E4=BD=93=E4=BE=9D=E8=B5=96=EF=BC=8C?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=AD=97=E4=BD=93=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/ofdgo.js | 3 +- go.mod | 4 +- go.sum | 8 +- ofdgo_font.go | 8 +- ofdgo_font_cff2.go | 10 +- ofdgo_font_file.go | 8 +- ofdgo_font_glyf.go | 59 ++++- ofdgo_font_info.go | 4 +- ofdgo_font_metrics.go | 189 ++++++++++++++++ ofdgo_font_resource_sfnt.go | 169 ++++++++++---- ofdgo_font_sfnt.go | 323 +++++++++++++++++++++++++++ ofdgo_font_source.go | 74 ++++-- ofdgo_font_variable.go | 10 +- ofdgo_font_woff.go | 248 ++++++++++++++++++++ ofdgo_renderer_canvas.go | 16 +- ofdgo_renderer_font_canvas.go | 152 +++---------- ofdgo_renderer_font_loader_canvas.go | 5 +- ofdgo_renderer_glyph_canvas.go | 7 +- ofdgo_renderer_pdf_text.go | 5 +- ofdgo_renderer_svg_canvas.go | 2 +- ofdgo_renderer_text_canvas.go | 4 +- 21 files changed, 1071 insertions(+), 237 deletions(-) create mode 100644 ofdgo_font_metrics.go create mode 100644 ofdgo_font_sfnt.go create mode 100644 ofdgo_font_woff.go diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 906115f..ee8df0b 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -2957,8 +2957,7 @@ function refreshEditorFonts() { async function readTextFont(font) { 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 fontManager.read(font); + return (await callWASM("ofdgoFontFace", await fontManager.read(font.file || font), font.index || 0)).bytes; } function updateTextFonts(item, refresh = false) { diff --git a/go.mod b/go.mod index 726f4f3..7a7521b 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/xiaoqidun/ofdgo go 1.26.0 require ( + github.com/andybalholm/brotli v1.2.6 github.com/emmansun/gmsm v0.45.0 github.com/go-fonts/latin-modern v0.3.3 github.com/go-text/typesetting v0.3.5 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/jbig2 v1.1.4 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/xgbutil v0.0.0-20190907113008-ad855c713046 // 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/textprocessing v0.0.6 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // 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/parse/v2 v2.8.16 // indirect github.com/yuin/goldmark v1.8.5 // indirect diff --git a/go.sum b/go.sum index dd0dccd..ff5c237 100644 --- a/go.sum +++ b/go.sum @@ -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/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/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM= -github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/andybalholm/brotli v1.2.6 h1:ftYnfj6usCp+UGV5kSJ3+chpMQgU+gJf/AxsUQ52REI= +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/textlayout v0.3.2 h1:Y/LTkwpQ9G0Fia9yhPmZA9IR5AnE8Cq30j3C+Gx5/IE= 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/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/font v0.0.0-20260913163313-54f98bb59ee6 h1:phNTWRYLLTlPpUgxkd6FDmWLlrOzpEpQAqq1ryLXYS8= -github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6/go.mod h1:ipESUihcEdgRBhIwTRHmBTOMamccMQrp4iCxfa1aFws= +github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117 h1:5vSJjsbV9AH0U+Ex0uej8ROrpOBlOVBrw0mGlVef7oo= +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/go.mod h1:kVqn9vxXUKtlHexSNrWbYePqioOT5mc4ou/KVSMpfCM= github.com/tdewolff/parse/v2 v2.8.16 h1:bLk5svUOQRkW/Y2SJ+DeENSIkZBcTIkq+Atyv5D8feI= diff --git a/ofdgo_font.go b/ofdgo_font.go index ef4f4c8..a0b7547 100644 --- a/ofdgo_font.go +++ b/ofdgo_font.go @@ -16,8 +16,6 @@ package ofdgo import ( "encoding/binary" - - "github.com/tdewolff/font" ) // FixFontDataAggressive 激进修复字体数据 @@ -52,10 +50,10 @@ func FixFontDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, ma } // fontSFNTData 保留字体原始表数据和时间,重新计算目录与校验和 -// 入参: sfnt 已解析的字体 +// 入参: tables 原始字体表 // 返回: []byte 独立SFNT数据 -func fontSFNTData(sfnt *font.SFNT) []byte { - data, _ := serializeOTF(sfnt.Tables) +func fontSFNTData(tables map[string][]byte) []byte { + data, _ := serializeOTF(tables) return data } diff --git a/ofdgo_font_cff2.go b/ofdgo_font_cff2.go index e213f66..723d05c 100644 --- a/ofdgo_font_cff2.go +++ b/ofdgo_font_cff2.go @@ -23,7 +23,7 @@ import ( "github.com/go-text/typesetting/font/cff" "github.com/go-text/typesetting/font/opentype" - "github.com/tdewolff/font" + "github.com/go-text/typesetting/font/opentype/tables" ) // defaultCFF2Font 将CFF2默认实例转换为静态OpenType字体,保留字形编号和三次轮廓 @@ -90,7 +90,7 @@ func defaultCFF2Font(data []byte) (result []byte, err error) { } chars := make([][]byte, count) for gid := range chars { - segments, _, err := program.LoadGlyph(uint16(gid), nil) + segments, _, err := program.LoadGlyph(tables.GlyphID(gid), nil) if err != nil { 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保留原有保存策略 // 入参: sfnt 已解析字体 // 返回: [][]byte 独立字形程序,不符合条件时为空 -func staticCFFPrograms(sfnt *font.SFNT) [][]byte { +func staticCFFPrograms(sfnt *sfntFont) [][]byte { data := sfnt.Tables["CFF "] if len(data) < 4 { return nil @@ -262,7 +262,7 @@ func staticCFFPrograms(sfnt *font.SFNT) [][]byte { // subsetStaticCFFFont 裁剪静态CID字形并保留原编号、字符映射和度量 // 入参: sfnt 已解析字体, glyphs 使用的字形 // 返回: []byte 字体子集, error 裁剪错误 -func subsetStaticCFFFont(sfnt *font.SFNT, glyphs []uint16) ([]byte, error) { +func subsetStaticCFFFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) { chars := staticCFFPrograms(sfnt) if chars == nil { 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) for gid := range keep { - for _, char := range sfnt.Cmap.ToUnicode(gid) { + for _, char := range sfnt.glyphCharacters(gid) { mapping[char] = gid } } diff --git a/ofdgo_font_file.go b/ofdgo_font_file.go index 1934c01..e4bc38c 100644 --- a/ofdgo_font_file.go +++ b/ofdgo_font_file.go @@ -20,8 +20,6 @@ import ( "fmt" "strings" "unicode/utf8" - - "github.com/tdewolff/font" ) // FontFace 字体文件中的字体名称与零起始索引,不依赖文件名 @@ -37,7 +35,7 @@ type FontFace struct { // Faces 读取字体名称表,集合文件按原顺序列出各项,不解析字形轮廓 // 返回: []FontFace 字体列表, error 格式或名称表错误 func (f FontFile) Faces() ([]FontFace, error) { - data, err := font.ToSFNT(f.Data) + data, err := decodeFontContainer(f.Data) if err != nil { return nil, err } @@ -64,7 +62,7 @@ func (f FontFile) Faces() ([]FontFace, error) { // 入参: index 零起始字体索引,非集合文件只能为0 // 返回: []byte 独立字体数据, error 格式或索引错误 func (f FontFile) Face(index int) ([]byte, error) { - data, err := font.ToSFNT(f.Data) + data, err := decodeFontContainer(f.Data) if err != nil { return nil, err } @@ -105,7 +103,7 @@ func (f FontFile) MissingGlyphs(index int, text string) (string, error) { if err != nil { return "", err } - sfnt, err := font.ParseSFNT(data, 0) + sfnt, err := parseSFNTFont(data) if err != nil { return "", err } diff --git a/ofdgo_font_glyf.go b/ofdgo_font_glyf.go index 5b92e81..c933091 100644 --- a/ofdgo_font_glyf.go +++ b/ofdgo_font_glyf.go @@ -21,12 +21,16 @@ import ( "math" "sync" - "github.com/tdewolff/font" + "github.com/go-text/typesetting/font/cff" + "github.com/go-text/typesetting/font/opentype" ) // sfntOutliner 缓存字体程序并提取独立字形轮廓 type sfntOutliner struct { - font *font.SFNT + font *sfntFont + cffOnce sync.Once + cff *cff.CFF + cffErr error once sync.Once program *ttInterpreter err error @@ -42,12 +46,51 @@ type ttGlyph struct { warning error } -// path 按TrueType指令和隐含点规则生成闭合轮廓,CFF保留原有解析 +// path 按字体设计坐标提取闭合轮廓,TrueType保留字形指令处理 // 入参: path 路径接收器, glyph 字形编号, scale 字体单位缩放 // 返回: 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 { - 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)) if err != nil { @@ -183,10 +226,10 @@ func (f *sfntOutliner) glyph(id uint16, active map[uint16]bool) (ttGlyph, error) // 入参: id 字形编号, data 完整字形头 // 返回: [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:]))) - if f.font.Vmtx != nil { - top += int32(f.font.Vmtx.TopSideBearing(id)) + if f.font.vertical != 0 { + 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}} } diff --git a/ofdgo_font_info.go b/ofdgo_font_info.go index dca92db..f4e4825 100644 --- a/ofdgo_font_info.go +++ b/ofdgo_font_info.go @@ -22,8 +22,6 @@ import ( "path" "slices" "sort" - - "github.com/tdewolff/font" ) const ( @@ -246,7 +244,7 @@ func (r *Reader) embeddedFontFace(of Font) (*FontFace, error) { if err != nil { 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 { faces = make([]*FontFace, count) for index := range faces { diff --git a/ofdgo_font_metrics.go b/ofdgo_font_metrics.go new file mode 100644 index 0000000..5b402ed --- /dev/null +++ b/ofdgo_font_metrics.go @@ -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 +} diff --git a/ofdgo_font_resource_sfnt.go b/ofdgo_font_resource_sfnt.go index 27e5719..e4fea9f 100644 --- a/ofdgo_font_resource_sfnt.go +++ b/ofdgo_font_resource_sfnt.go @@ -20,15 +20,13 @@ import ( "fmt" "maps" "slices" - - "github.com/tdewolff/font" ) -// SFNTBackend 提供默认字体资源处理,不依赖Canvas绘图库 +// SFNTBackend 提供独立的OpenType字体度量、轮廓和资源处理 type SFNTBackend struct{} // sfntResourceMetrics 隔离资源解析器的度量实现 -type sfntResourceMetrics struct{ *font.SFNT } +type sfntResourceMetrics struct{ *sfntFont } // Name 返回资源后端标识 // 返回: string 后端标识 @@ -47,11 +45,11 @@ func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, er if err != nil { return nil, err } - sfnt, err := font.ParseSFNT(data, 0) + sfnt, err := parseSFNTFont(data) if err != nil { return nil, err } - name := sfntFontName(sfnt, font.NamePostScript, font.NameFull, font.NameFontFamily) + name := sfntFontName(sfnt, 6, 4, 1) if name == "" { return nil, fmt.Errorf("font has no name") } @@ -61,7 +59,7 @@ func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, er } return &FontResource{ 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), }, nil } @@ -90,12 +88,12 @@ func (SFNTBackend) SubsetSourceFont(data []byte, usage FontUsage) ([]byte, error // Write 返回未改动元信息的字体数据 // 返回: []byte 字体数据 -func (f sfntResourceMetrics) Write() []byte { return fontSFNTData(f.SFNT) } +func (f sfntResourceMetrics) Write() []byte { return f.sfntFont.Write() } // sfntSubsettable 判断默认资源后端能否无损裁剪该字体 // 入参: sfnt 已解析字体 // 返回: 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 }) { return false } @@ -108,7 +106,7 @@ func sfntSubsettable(sfnt *font.SFNT) bool { // fontEmbeddingFlags 读取OpenType的嵌入标志,未提供OS/2表时返回零 // 入参: sfnt 已解析字体 // 返回: uint16 嵌入标志 -func fontEmbeddingFlags(sfnt *font.SFNT) uint16 { +func fontEmbeddingFlags(sfnt *sfntFont) uint16 { if table := sfnt.Tables["OS/2"]; len(table) >= 10 { return binary.BigEndian.Uint16(table[8:10]) } @@ -119,7 +117,7 @@ func fontEmbeddingFlags(sfnt *font.SFNT) uint16 { // 入参: data 独立OpenType字体 // 返回: error 字体解析或嵌入限制 func validateFontEmbedding(data []byte) error { - sfnt, err := font.ParseSFNT(data, 0) + sfnt, err := parseSFNTFont(data) if err != nil { return err } @@ -133,10 +131,14 @@ func validateFontEmbedding(data []byte) error { // sfntFontName 获取字体中的名称 // 入参: sfnt 字体, names 名称类型,按顺序查找 // 返回: 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 _, record := range sfnt.Name.Get(name) { - if value := record.String(); value != "" { + for _, record := range records { + if record.Name != name { + continue + } + if value := values[[4]uint16{record.Platform, record.Encoding, record.Language, name}]; value != "" { return value } } @@ -152,7 +154,7 @@ func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) { if err != nil { return nil, err } - sfnt, err := font.ParseSFNT(bytes.Clone(data), 0) + sfnt, err := parseSFNTFont(data) if err != nil { return nil, err } @@ -169,7 +171,7 @@ func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) { usage := newEditorFontUsage() for _, id := range glyphs { usage.glyphs[id] = true - for _, char := range sfnt.Cmap.ToUnicode(id) { + for _, char := range sfnt.glyphCharacters(id) { usage.chars[char] = true } } @@ -188,15 +190,7 @@ func subsetSFNTFont(data []byte, glyphs []uint16, mapped bool) ([]byte, error) { } return fixed, nil } - subset, err := sfnt.Subset(glyphs, font.SubsetOptions{Tables: []string{ - "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) + return subsetCompactSFNTFont(sfnt, glyphs) } // 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")) { return nil } - sfnt, err := font.ParseSFNT(data, 0) + sfnt, err := parseSFNTFont(data) if err != nil || !sfnt.IsTrueType || fontEmbeddingFlags(sfnt)&0x0100 != 0 { return nil } @@ -218,20 +212,13 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte { return nil } } - var unicodeTables []uint16 - subtables := make(map[uint32]uint16) - for i, record := range sfnt.Cmap.EncodingRecords { - if record.Format == 14 { + var unicodeTables []sfntCmap + for _, record := range sfnt.cmaps { + if record.format == 14 { return nil } - offset := binary.BigEndian.Uint32(sfnt.Tables["cmap"][8+8*i:]) - index, ok := subtables[offset] - 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 record.platform == 0 || record.platform == 3 && (record.encoding == 1 || record.encoding == 10) { + unicodeTables = append(unicodeTables, record) } } if len(unicodeTables) == 0 { @@ -242,8 +229,8 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte { mapping := make(map[rune]uint16, len(usage.chars)) for char := range usage.chars { id := sfnt.GlyphIndex(char) - for _, index := range unicodeTables { - if alternate, ok := sfnt.Cmap.Subtables[index].Get(char); ok && alternate != id { + for _, cmap := range unicodeTables { + if alternate, ok := cmap.mapping[char]; ok && alternate != id { return nil } } @@ -261,7 +248,7 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte { if id >= sfnt.NumGlyphs() { return nil } - dependencies, err := sfnt.Glyf.Dependencies(id) + dependencies, err := sfnt.glyphDependencies(id) if err != nil { return nil } @@ -271,13 +258,14 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte { } var glyf, loca []byte 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)) } else { loca = binary.BigEndian.AppendUint32(loca, uint32(len(glyf))) } 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 { glyf = append(glyf, 0) } @@ -295,3 +283,98 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte { } 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) +} diff --git a/ofdgo_font_sfnt.go b/ofdgo_font_sfnt.go new file mode 100644 index 0000000..58f4903 --- /dev/null +++ b/ofdgo_font_sfnt.go @@ -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 +} diff --git a/ofdgo_font_source.go b/ofdgo_font_source.go index 187fe24..da79a59 100644 --- a/ofdgo_font_source.go +++ b/ofdgo_font_source.go @@ -26,8 +26,6 @@ import ( "runtime" "sort" "sync" - - "github.com/tdewolff/font" ) // 字体来源:文件、文件系统和系统字体 @@ -37,11 +35,8 @@ const ( fontSourceSystem ) -// systemFontIndex 串行初始化系统字体索引,不注册绘图后端 -var systemFontIndex struct { - sync.Mutex - fonts *font.SystemFonts -} +// systemFontIndex 按需建立只读系统字体索引 +var systemFontIndex = sync.OnceValue(indexSystemFonts) // fontSourceKind 字体来源类型 type fontSourceKind uint8 @@ -100,20 +95,11 @@ func (c *fontSourceCache) child() *fontSourceCache { func (r *Renderer) readFontSource(source fontSource, definition *Font) ([]byte, error) { key := fontSourceKey{kind: source.kind, index: source.index, face: source.face, name: source.name} if source.kind == fontSourceSystem { - systemFontIndex.Lock() - if systemFontIndex.fonts == nil { - 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 { + matches := fontFileMatches(systemFontIndex(), []string{source.name}, definition.Bold, definition.Italic) + if len(matches) == 0 { 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 { return cached.data, cached.err @@ -349,15 +335,59 @@ func (r *Renderer) matchFontFS(index int, names []string, bold, italic bool) []f // systemFontDirs 获取系统字体目录 // 返回: []string 字体目录列表 func systemFontDirs() []string { + home, _ := os.UserHomeDir() switch runtime.GOOS { case "android": return []string{"/product/fonts", "/system/fonts"} case "darwin": - return []string{`/Library/Fonts`} + return []string{"/System/Library/Fonts", "/Library/Fonts", filepath.Join(home, "Library", "Fonts")} 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": - 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 } + +// 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 +} diff --git a/ofdgo_font_variable.go b/ofdgo_font_variable.go index 6bae30e..fd65a59 100644 --- a/ofdgo_font_variable.go +++ b/ofdgo_font_variable.go @@ -17,14 +17,12 @@ package ofdgo import ( "encoding/binary" "fmt" - - "github.com/tdewolff/font" ) // variableSFNTSubsettable 核对TrueType可变轴与字形变化表的基本结构 // 入参: sfnt 已解析字体 // 返回: bool 是否具备可裁剪的变化数据 -func variableSFNTSubsettable(sfnt *font.SFNT) bool { +func variableSFNTSubsettable(sfnt *sfntFont) bool { 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 { return false @@ -38,7 +36,7 @@ func variableSFNTSubsettable(sfnt *font.SFNT) bool { // subsetVariableSFNTFont 保留字形编号及可变轴,仅裁剪未使用的轮廓和字形变化数据 // 入参: sfnt 已解析字体, glyphs 使用的字形 // 返回: []byte 字体子集, error 字形或变化表错误 -func subsetVariableSFNTFont(sfnt *font.SFNT, glyphs []uint16) ([]byte, error) { +func subsetVariableSFNTFont(sfnt *sfntFont, glyphs []uint16) ([]byte, error) { if !variableSFNTSubsettable(sfnt) { 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") } usage.glyphs[gid] = true - for _, char := range sfnt.Cmap.ToUnicode(gid) { + for _, char := range sfnt.glyphCharacters(gid) { usage.chars[char] = true } } for gid := range usage.glyphs { - dependencies, err := sfnt.Glyf.Dependencies(gid) + dependencies, err := sfnt.glyphDependencies(gid) if err != nil { return nil, err } diff --git a/ofdgo_font_woff.go b/ofdgo_font_woff.go new file mode 100644 index 0000000..c22427a --- /dev/null +++ b/ofdgo_font_woff.go @@ -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 +} diff --git a/ofdgo_renderer_canvas.go b/ofdgo_renderer_canvas.go index 51015ce..873642a 100644 --- a/ofdgo_renderer_canvas.go +++ b/ofdgo_renderer_canvas.go @@ -258,5 +258,19 @@ func init() { // 返回: RenderBackends 默认后端组合 func defaultRenderBackends() RenderBackends { 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) } diff --git a/ofdgo_renderer_font_canvas.go b/ofdgo_renderer_font_canvas.go index b0dfe48..25dd387 100644 --- a/ofdgo_renderer_font_canvas.go +++ b/ofdgo_renderer_font_canvas.go @@ -19,135 +19,41 @@ import ( "sync" "github.com/tdewolff/canvas" - "github.com/tdewolff/font" ) -// canvasGlyphBoundsLimit 限制单个字体保留的字形与字号边界数量 -const canvasGlyphBoundsLimit = 4096 +// canvasFontLoadMu 保护绘图后端加载无名称字体时使用的共享状态 +var canvasFontLoadMu sync.Mutex -// canvasFontMetrics 保留默认字体度量,写出时不更新原始时间 -type canvasFontMetrics struct { - *font.SFNT - *fontShaper - outlines *sfntOutliner - bounds *canvasFontBounds +// 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) } -// canvasFontBounds 缓存已成功提取的字形边界,不保留轮廓或失败结果 -type canvasFontBounds struct { - mu sync.Mutex - values map[canvasGlyphSize]Box -} - -// canvasGlyphSize 区分字形编号及精确毫米字号 -type canvasGlyphSize struct { - glyph uint16 - size float64 -} - -// ResolveFont 按需加载字体资源,保留内嵌数据及外部字体的名称、样式和集合索引匹配 -// 入参: r 渲染器, id 字体标识, exact 是否禁止无关回退 -// 返回: ResolvedFont 字体与来源, error 字体不可用错误 -func (b CanvasBackend) ResolveFont(r *Renderer, id string, exact bool) (ResolvedFont, error) { - return r.resolveFontSource(b, id, exact) -} - -// OpenFont 解析独立SFNT数据,度量与原有编辑排版保持一致 -// 入参: data 字体数据 -// 返回: FontMetrics 字体度量, error 解析错误 -func (CanvasBackend) OpenFont(data []byte) (FontMetrics, error) { - data, err := defaultCFF2Font(data) +// 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 decodeWOFF(data) + case "wOF2": + decoded, handled, err := decodeWOFF2(data) + if handled { + return decoded, err + } + } + loaded, err := loadCanvasFont(data, 0, canvas.FontRegular) if err != nil { return nil, err } - sfnt, err := font.ParseSFNT(data, 0) - 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 + return serializeOTF(loaded.Tables) } diff --git a/ofdgo_renderer_font_loader_canvas.go b/ofdgo_renderer_font_loader_canvas.go index 0ba9e20..7c08d68 100644 --- a/ofdgo_renderer_font_loader_canvas.go +++ b/ofdgo_renderer_font_loader_canvas.go @@ -51,7 +51,10 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { return ff } 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 return nil } diff --git a/ofdgo_renderer_glyph_canvas.go b/ofdgo_renderer_glyph_canvas.go index 71fab11..546ae4f 100644 --- a/ofdgo_renderer_glyph_canvas.go +++ b/ofdgo_renderer_glyph_canvas.go @@ -103,7 +103,12 @@ func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) ( } outlines := r.canvasState().fontOutlines[face.Font] 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 } path, width, err := textGlyphPath(face, glyph, outlines) diff --git a/ofdgo_renderer_pdf_text.go b/ofdgo_renderer_pdf_text.go index 5a3903b..fb889ea 100644 --- a/ofdgo_renderer_pdf_text.go +++ b/ofdgo_renderer_pdf_text.go @@ -17,7 +17,6 @@ package ofdgo import ( "github.com/tdewolff/canvas" canvastext "github.com/tdewolff/canvas/text" - canvasfont "github.com/tdewolff/font" "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) { font := r.fonts[face.Font] 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 { return nil, err } copy := *face.Font - copy.SFNT = sfnt + copy.SFNT = loaded.SFNT font = © if r.fonts == nil { r.fonts = make(map[*canvas.Font]*canvas.Font) diff --git a/ofdgo_renderer_svg_canvas.go b/ofdgo_renderer_svg_canvas.go index 8aefacb..c83577d 100644 --- a/ofdgo_renderer_svg_canvas.go +++ b/ofdgo_renderer_svg_canvas.go @@ -146,7 +146,7 @@ func (s *svgResourceRenderer) RenderText(text *canvas.Text, m canvas.Matrix) { font := text.MostCommonFontFace().Font resource, ok := s.renderer.canvasState().svgFontCache[font] if !ok { - data := fontSFNTData(font.SFNT) + data := fontSFNTData(font.Tables) resource = SVGFont{ Name: fmt.Sprintf("ofdgo-%x-%d", sha256.Sum256(data), font.Style()), Weight: font.Style().CSS(), diff --git a/ofdgo_renderer_text_canvas.go b/ofdgo_renderer_text_canvas.go index 0abaf67..4b80c5b 100644 --- a/ofdgo_renderer_text_canvas.go +++ b/ofdgo_renderer_text_canvas.go @@ -18,7 +18,6 @@ import ( "strings" "github.com/tdewolff/canvas" - "github.com/tdewolff/font" ) // ptPerMM 为毫米转换为排版点的比例 @@ -183,7 +182,8 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 ctx.Pop() 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 == "" var metrics canvas.FontMetrics if textRun != nil || recoverBaseline {