feat(文件合并): 合并字体适配文件
build.yaml / build (push) Successful in 48s

This commit is contained in:
xiaoqidun committed 2026-10-14 00:51:39 +08:00
1 parent 0be872691d
commit 5cc811c2a1
6 files changed
+159 -213

No files matched your search

+41
View File
@@ -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)
}
-60
View File
@@ -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)
}
+44
View File
@@ -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
}
-64
View File
@@ -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
}
+74
View File
@@ -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() }
-89
View File
@@ -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() }