From 5cc811c2a16bd0de5e7440af4845d1ca8bd495cb Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 14 Oct 2026 00:51:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=96=87=E4=BB=B6=E5=90=88=E5=B9=B6):=20?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E5=AD=97=E4=BD=93=E9=80=82=E9=85=8D=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_font.go | 41 +++++++++++++++++ ofdgo_font_file.go | 60 ------------------------- ofdgo_font_match.go | 44 ++++++++++++++++++ ofdgo_font_name.go | 64 -------------------------- ofdgo_font_resource.go | 74 ++++++++++++++++++++++++++++++ ofdgo_font_resource_sfnt.go | 89 ------------------------------------- 6 files changed, 159 insertions(+), 213 deletions(-) delete mode 100644 ofdgo_font_file.go delete mode 100644 ofdgo_font_name.go delete mode 100644 ofdgo_font_resource_sfnt.go diff --git a/ofdgo_font.go b/ofdgo_font.go index 4d3d385..c4dc71f 100644 --- a/ofdgo_font.go +++ b/ofdgo_font.go @@ -15,9 +15,15 @@ package ofdgo import ( + "fmt" + "unicode/utf8" + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" ) +// FontFace 字体文件中的字体名称与零起始索引,不依赖文件名 +type FontFace = basefont.Face + // FixFontDataAggressive 激进修复字体数据 // 尝试修复缺失表(OS/2, cmap等)的TrueType字体或包装CFF裸数据 // 对显式字形映射补充私有字符cmap,便于按glyph id渲染 @@ -26,3 +32,38 @@ import ( func FixFontDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uint16, bool, error) { return basefont.FixDataAggressive(data, fixCmap, fixName) } + +// Faces 读取字体名称表,集合文件按原顺序列出各项,不解析字形轮廓 +// 返回: []FontFace 字体列表, error 格式或名称表错误 +func (f FontFile) Faces() ([]FontFace, error) { + data, err := decodeFontContainer(f.Data) + if err != nil { + return nil, err + } + return basefont.ReadFaces(data) +} + +// Face 提取指定字体,返回独立的OpenType数据,可用于预览或AddFont,且不修改原文件 +// 入参: index 零起始字体索引,非集合文件只能为0 +// 返回: []byte 独立字体数据, error 格式或索引错误 +func (f FontFile) Face(index int) ([]byte, error) { + data, err := decodeFontContainer(f.Data) + if err != nil { + return nil, err + } + return basefont.ExtractFace(data, index) +} + +// MissingGlyphs 检查指定字体的字符覆盖,不注册资源或修改文档,忽略段落换行 +// 入参: index 零起始字体索引, text 待检查文字 +// 返回: string 按首次出现顺序去重的缺失字符, error 字体或文字格式错误 +func (f FontFile) MissingGlyphs(index int, text string) (string, error) { + if !utf8.ValidString(text) { + return "", fmt.Errorf("text must be UTF-8") + } + data, err := f.Face(index) + if err != nil { + return "", err + } + return basefont.MissingGlyphs(data, text) +} diff --git a/ofdgo_font_file.go b/ofdgo_font_file.go deleted file mode 100644 index a79ed14..0000000 --- a/ofdgo_font_file.go +++ /dev/null @@ -1,60 +0,0 @@ -// 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" - "unicode/utf8" - - basefont "github.com/xiaoqidun/ofdgo/internal/base/font" -) - -// FontFace 字体文件中的字体名称与零起始索引,不依赖文件名 -type FontFace = basefont.Face - -// Faces 读取字体名称表,集合文件按原顺序列出各项,不解析字形轮廓 -// 返回: []FontFace 字体列表, error 格式或名称表错误 -func (f FontFile) Faces() ([]FontFace, error) { - data, err := decodeFontContainer(f.Data) - if err != nil { - return nil, err - } - return basefont.ReadFaces(data) -} - -// Face 提取指定字体,返回独立的OpenType数据,可用于预览或AddFont,且不修改原文件 -// 入参: index 零起始字体索引,非集合文件只能为0 -// 返回: []byte 独立字体数据, error 格式或索引错误 -func (f FontFile) Face(index int) ([]byte, error) { - data, err := decodeFontContainer(f.Data) - if err != nil { - return nil, err - } - return basefont.ExtractFace(data, index) -} - -// MissingGlyphs 检查指定字体的字符覆盖,不注册资源或修改文档,忽略段落换行 -// 入参: index 零起始字体索引, text 待检查文字 -// 返回: string 按首次出现顺序去重的缺失字符, error 字体或文字格式错误 -func (f FontFile) MissingGlyphs(index int, text string) (string, error) { - if !utf8.ValidString(text) { - return "", fmt.Errorf("text must be UTF-8") - } - data, err := f.Face(index) - if err != nil { - return "", err - } - return basefont.MissingGlyphs(data, text) -} diff --git a/ofdgo_font_match.go b/ofdgo_font_match.go index 62d8738..5c15ca1 100644 --- a/ofdgo_font_match.go +++ b/ofdgo_font_match.go @@ -15,6 +15,7 @@ package ofdgo import ( + "bytes" "path" "slices" "strings" @@ -389,3 +390,46 @@ func appendFontPattern(patterns []string, seen map[string]bool, pattern string) seen[key] = true return append(patterns, pattern) } + +// appendFontFileNames 建立各字体的内部名称与文件别名匹配候选 +// 入参: candidates 字体候选, index 文件候选索引, faces 各字体内部名称 +// 返回: []fontFileCandidate 字体候选列表 +func appendFontFileNames(candidates []fontFileCandidate, index int, faces [][]string) []fontFileCandidate { + file := candidates[index] + replaced := false + for face, names := range faces { + file.face = face + file.names = make([]string, len(names)) + for i, name := range names { + file.names[i] = basefont.NormalizeName(name) + } + for _, name := range file.names { + file.normalized = name + if !replaced { + candidates[index] = file + replaced = true + } else { + candidates = append(candidates, file) + } + } + } + return candidates +} + +// fontCollectionIndex 按内部名称与样式选择集合项,未匹配时保留首项 +// 入参: data 集合数据, names 字体名称, bold 是否粗体, italic 是否斜体 +// 返回: int 零起始字体索引 +func fontCollectionIndex(data []byte, names []string, bold, italic bool) int { + return fontNameIndex(basefont.FileNames(bytes.NewReader(data)), names, bold, italic) +} + +// fontNameIndex 按名称与样式选择字体索引,未匹配时保留首项 +// 入参: faces 各字体名称, names 目标名称, bold 是否粗体, italic 是否斜体 +// 返回: int 零起始字体索引 +func fontNameIndex(faces [][]string, names []string, bold, italic bool) int { + candidates := appendFontFileNames([]fontFileCandidate{{}}, 0, faces) + if matches := fontFileMatches(candidates, names, bold, italic); len(matches) > 0 { + return matches[0].face + } + return 0 +} diff --git a/ofdgo_font_name.go b/ofdgo_font_name.go deleted file mode 100644 index 89c3dc0..0000000 --- a/ofdgo_font_name.go +++ /dev/null @@ -1,64 +0,0 @@ -// 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" - - basefont "github.com/xiaoqidun/ofdgo/internal/base/font" -) - -// appendFontFileNames 建立各字体的内部名称与文件别名匹配候选 -// 入参: candidates 字体候选, index 文件候选索引, faces 各字体内部名称 -// 返回: []fontFileCandidate 字体候选列表 -func appendFontFileNames(candidates []fontFileCandidate, index int, faces [][]string) []fontFileCandidate { - file := candidates[index] - replaced := false - for face, names := range faces { - file.face = face - file.names = make([]string, len(names)) - for i, name := range names { - file.names[i] = basefont.NormalizeName(name) - } - for _, name := range file.names { - file.normalized = name - if !replaced { - candidates[index] = file - replaced = true - } else { - candidates = append(candidates, file) - } - } - } - return candidates -} - -// fontCollectionIndex 按内部名称与样式选择集合项,未匹配时保留首项 -// 入参: data 集合数据, names 字体名称, bold 是否粗体, italic 是否斜体 -// 返回: int 零起始字体索引 -func fontCollectionIndex(data []byte, names []string, bold, italic bool) int { - return fontNameIndex(basefont.FileNames(bytes.NewReader(data)), names, bold, italic) -} - -// fontNameIndex 按名称与样式选择字体索引,未匹配时保留首项 -// 入参: faces 各字体名称, names 目标名称, bold 是否粗体, italic 是否斜体 -// 返回: int 零起始字体索引 -func fontNameIndex(faces [][]string, names []string, bold, italic bool) int { - candidates := appendFontFileNames([]fontFileCandidate{{}}, 0, faces) - if matches := fontFileMatches(candidates, names, bold, italic); len(matches) > 0 { - return matches[0].face - } - return 0 -} diff --git a/ofdgo_font_resource.go b/ofdgo_font_resource.go index 17cf660..22d6149 100644 --- a/ofdgo_font_resource.go +++ b/ofdgo_font_resource.go @@ -14,6 +14,13 @@ package ofdgo +import ( + "encoding/binary" + "fmt" + + basefont "github.com/xiaoqidun/ofdgo/internal/base/font" +) + // FontResource 保存独立字体数据、标准元信息与只读度量,不包含绘图库类型 type FontResource struct { FontMetrics @@ -38,3 +45,70 @@ type FontResourceBackend interface { SubsetFont(data []byte, glyphs []uint16, preserve bool) ([]byte, error) SubsetSourceFont(data []byte, usage FontUsage) ([]byte, error) } + +// SFNTBackend 提供独立的OpenType字体度量、轮廓和资源处理 +type SFNTBackend struct{} + +// sfntResourceMetrics 隔离资源解析器的度量实现 +type sfntResourceMetrics struct{ *basefont.SFNT } + +// Name 返回资源后端标识 +// 返回: string 后端标识 +func (SFNTBackend) Name() string { return "sfnt" } + +// OpenFontResource 提取独立字体并读取名称、样式和裁剪能力 +// CFF2使用默认实例的静态轮廓,不改变原字体文件 +// 入参: file 字体文件, index 集合索引 +// 返回: *FontResource 字体资源, error 解析错误 +func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, error) { + data, err := file.Face(index) + if err != nil { + return nil, err + } + data, err = basefont.DefaultCFF2Font(data) + if err != nil { + return nil, err + } + sfnt, err := basefont.ParseSFNT(data) + if err != nil { + return nil, err + } + name := basefont.SFNTName(sfnt, 6, 4, 1) + if name == "" { + return nil, fmt.Errorf("font has no name") + } + extension := ".ttf" + if sfnt.IsCFF { + extension = ".otf" + } + return &FontResource{ + FontMetrics: sfntResourceMetrics{sfnt}, Data: data, Extension: extension, + Font: Font{FontName: name, FamilyName: basefont.SFNTName(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: basefont.Subsettable(sfnt), + }, nil +} + +// SubsetFont 裁剪新建字体并按要求保留显式字形编号 +// 入参: data 字体数据, glyphs 用字, preserve 是否保留编号 +// 返回: []byte 子集, error 裁剪错误 +func (SFNTBackend) SubsetFont(data []byte, glyphs []uint16, preserve bool) ([]byte, error) { + return basefont.SubsetSFNT(data, glyphs, preserve) +} + +// SubsetSourceFont 按全包引用裁剪原字体,不确定时保留原资源 +// 入参: data 字体数据, usage 用字信息 +// 返回: []byte 子集, error 裁剪错误 +func (SFNTBackend) SubsetSourceFont(data []byte, usage FontUsage) ([]byte, error) { + u := &basefont.Usage{Characters: make(map[rune]bool), Glyphs: make(map[uint16]bool), Unsafe: usage.Unsafe} + for _, char := range usage.Characters { + u.Characters[char] = true + } + for _, glyph := range usage.Glyphs { + u.Glyphs[glyph] = true + } + return basefont.SubsetSourceSFNT(data, u), nil +} + +// Write 返回未改动元信息的字体数据 +// 返回: []byte 字体数据 +func (f sfntResourceMetrics) Write() []byte { return f.SFNT.Write() } diff --git a/ofdgo_font_resource_sfnt.go b/ofdgo_font_resource_sfnt.go deleted file mode 100644 index 97115ef..0000000 --- a/ofdgo_font_resource_sfnt.go +++ /dev/null @@ -1,89 +0,0 @@ -// 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" - - basefont "github.com/xiaoqidun/ofdgo/internal/base/font" -) - -// SFNTBackend 提供独立的OpenType字体度量、轮廓和资源处理 -type SFNTBackend struct{} - -// sfntResourceMetrics 隔离资源解析器的度量实现 -type sfntResourceMetrics struct{ *basefont.SFNT } - -// Name 返回资源后端标识 -// 返回: string 后端标识 -func (SFNTBackend) Name() string { return "sfnt" } - -// OpenFontResource 提取独立字体并读取名称、样式和裁剪能力 -// CFF2使用默认实例的静态轮廓,不改变原字体文件 -// 入参: file 字体文件, index 集合索引 -// 返回: *FontResource 字体资源, error 解析错误 -func (SFNTBackend) OpenFontResource(file FontFile, index int) (*FontResource, error) { - data, err := file.Face(index) - if err != nil { - return nil, err - } - data, err = basefont.DefaultCFF2Font(data) - if err != nil { - return nil, err - } - sfnt, err := basefont.ParseSFNT(data) - if err != nil { - return nil, err - } - name := basefont.SFNTName(sfnt, 6, 4, 1) - if name == "" { - return nil, fmt.Errorf("font has no name") - } - extension := ".ttf" - if sfnt.IsCFF { - extension = ".otf" - } - return &FontResource{ - FontMetrics: sfntResourceMetrics{sfnt}, Data: data, Extension: extension, - Font: Font{FontName: name, FamilyName: basefont.SFNTName(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: basefont.Subsettable(sfnt), - }, nil -} - -// SubsetFont 裁剪新建字体并按要求保留显式字形编号 -// 入参: data 字体数据, glyphs 用字, preserve 是否保留编号 -// 返回: []byte 子集, error 裁剪错误 -func (SFNTBackend) SubsetFont(data []byte, glyphs []uint16, preserve bool) ([]byte, error) { - return basefont.SubsetSFNT(data, glyphs, preserve) -} - -// SubsetSourceFont 按全包引用裁剪原字体,不确定时保留原资源 -// 入参: data 字体数据, usage 用字信息 -// 返回: []byte 子集, error 裁剪错误 -func (SFNTBackend) SubsetSourceFont(data []byte, usage FontUsage) ([]byte, error) { - u := &basefont.Usage{Characters: make(map[rune]bool), Glyphs: make(map[uint16]bool), Unsafe: usage.Unsafe} - for _, char := range usage.Characters { - u.Characters[char] = true - } - for _, glyph := range usage.Glyphs { - u.Glyphs[glyph] = true - } - return basefont.SubsetSourceSFNT(data, u), nil -} - -// Write 返回未改动元信息的字体数据 -// 返回: []byte 字体数据 -func (f sfntResourceMetrics) Write() []byte { return f.SFNT.Write() }