From 9856dd3bd5772d5663c5ee6b09b82229470cd033 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Fri, 9 Oct 2026 15:25:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AD=97=E4=BD=93=E5=A4=84=E7=90=86):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=AC=A6=E5=8F=B7=E5=AD=97=E4=BD=93=E7=BC=BA?= =?UTF-8?q?=E5=AD=97=E9=94=99=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_font_source.go | 6 +- ofdgo_font_symbol.go | 134 +++++++++++++++++++++++++++++++++++++++++++ ofdgo_pdf_font.go | 77 ++----------------------- 3 files changed, 145 insertions(+), 72 deletions(-) create mode 100644 ofdgo_font_symbol.go diff --git a/ofdgo_font_source.go b/ofdgo_font_source.go index a7055e8..5985bf9 100644 --- a/ofdgo_font_source.go +++ b/ofdgo_font_source.go @@ -16,6 +16,7 @@ package ofdgo import ( "bytes" + "context" "fmt" "io" "io/fs" @@ -93,7 +94,7 @@ func (c *fontSourceCache) child() *fontSourceCache { return cache } -// readFontSource 读取并拆出独立字体,保留集合中的名称和样式选择 +// readFontSource 读取独立字体并规范符号映射,保留集合中的名称和样式选择 // 入参: source 字体来源, definition 字体定义 // 返回: []byte 字体数据, error 读取或集合错误 func (r *Renderer) readFontSource(source fontSource, definition *Font) ([]byte, error) { @@ -127,6 +128,9 @@ func (r *Renderer) readFontSource(source fontSource, definition *Font) ([]byte, if err == nil && bytes.HasPrefix(data, []byte("ttcf")) { data, err = extractCollectionFont(data, key.face) } + if err == nil { + data, _, err = symbolFontProgram(context.Background(), data, "") + } r.fontSourcesCache.data[key] = fontSourceData{data, err} return data, err } diff --git a/ofdgo_font_symbol.go b/ofdgo_font_symbol.go new file mode 100644 index 0000000..3c64a71 --- /dev/null +++ b/ofdgo_font_symbol.go @@ -0,0 +1,134 @@ +// 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 ( + "context" + "encoding/binary" + "fmt" + "unicode/utf8" + + "github.com/xiaoqidun/pdfgo" +) + +// symbolFontProgram 为标准符号字体补齐Unicode映射,保留字形编号和轮廓 +// 入参: ctx 取消上下文, data 独立字体数据, name 标准字体名,为空时从字体名称表识别 +// 返回: []byte 字体数据, map[string]uint16 标准字形编号,不适用时为空, error 映射或取消错误 +func symbolFontProgram(ctx context.Context, data []byte, name string) ([]byte, map[string]uint16, error) { + if err := ctx.Err(); err != nil { + return nil, nil, err + } + tables, err := fontFileTables(data, 0) + if err != nil { + return data, nil, nil + } + legacy := symbolFontCmap(tables["cmap"]) + if name == "" && !legacy { + return data, nil, nil + } + actual := symbolFontName(tables["name"]) + if name == "" { + name = actual + } + if name != "Symbol" && name != "ZapfDingbats" || !legacy && actual != name { + return data, nil, nil + } + standard, err := new(pdfgo.Reader).ReadFontContext(ctx, pdfgo.Dictionary{"Type": pdfgo.Name("Font"), "Subtype": pdfgo.Name("Type1"), "BaseFont": pdfgo.Name(name)}) + if err != nil { + return nil, nil, err + } + codes := make([]byte, 256) + for index := range codes { + codes[index] = byte(index) + } + glyphs, err := standard.DecodeContext(ctx, codes) + if err != nil { + return nil, nil, err + } + original := parseCmapMappings(tables["cmap"]) + mapping := make(map[rune]uint16) + names := make(map[string]uint16) + for _, glyph := range glyphs { + if glyph.Name == ".notdef" || glyph.Name == "" { + continue + } + char, size := utf8.DecodeRuneInString(glyph.Text) + var id uint16 + if legacy { + id = original[rune(glyph.Code)] + for _, base := range [...]rune{0xf000, 0xf100, 0xf200} { + if candidate := original[base+rune(glyph.Code)]; candidate != 0 { + if id != 0 && id != candidate { + return nil, nil, fmt.Errorf("ambiguous symbol font mapping") + } + id = candidate + } + } + } else if size != 0 && size == len(glyph.Text) { + id = original[char] + } + if id == 0 { + continue + } + names[glyph.Name] = id + if size != 0 && size == len(glyph.Text) { + mapping[char] = id + } + } + if !legacy || len(mapping) == 0 { + return data, names, nil + } + tables["cmap"] = buildCmapTable(0, mapping) + program, err := serializeOTF(tables) + return program, names, err +} + +// symbolFontName 从字体名称表识别标准符号字体,不依据文件名推断编码 +// 入参: data 名称表数据 +// 返回: string 标准字体名,无法识别时为空 +func symbolFontName(data []byte) string { + for _, name := range fontNamesFromTable(data) { + switch fontNormalizeName(name) { + case "symbol", "symbolmt", "symbolregular": + return "Symbol" + case "zapfdingbats", "zapfdingbatsregular", "itczapfdingbats": + return "ZapfDingbats" + } + } + return "" +} + +// symbolFontCmap 识别不含Unicode字符表的Windows符号字体 +// 入参: data 字符映射表 +// 返回: bool 是否为旧式符号字体 +func symbolFontCmap(data []byte) bool { + if len(data) < 4 { + return false + } + count := int(binary.BigEndian.Uint16(data[2:])) + if count > (len(data)-4)/8 { + return false + } + symbol := false + for i := range count { + pos := 4 + i*8 + platform, encoding := binary.BigEndian.Uint16(data[pos:]), binary.BigEndian.Uint16(data[pos+2:]) + if platform == 0 || platform == 3 && (encoding == 1 || encoding == 10) { + return false + } + symbol = symbol || platform == 3 && encoding == 0 + } + return symbol +} diff --git a/ofdgo_pdf_font.go b/ofdgo_pdf_font.go index 2f0a3b8..21e0352 100644 --- a/ofdgo_pdf_font.go +++ b/ofdgo_pdf_font.go @@ -143,7 +143,7 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error) return result, nil } -// symbolFont 将标准符号字体的旧式字符表转换为Unicode映射,保留原字形编号 +// symbolFont 加载标准符号字体,保留原字形编号和源字形名称 // 入参: source 未嵌入的PDF字体 // 返回: *pdfImportedFont 可用的符号字体,无匹配时为空, error 字体或取消错误 func (p *pdfImporter) symbolFont(source *pdfgo.Font) (*pdfImportedFont, error) { @@ -167,56 +167,14 @@ func (p *pdfImporter) symbolFont(source *pdfgo.Font) (*pdfImportedFont, error) { p.symbolFonts[source] = nil return nil, nil } - tables, err := fontFileTables(resolved.Data, 0) - if err != nil || !pdfSymbolCmap(tables["cmap"]) { + program, names, err := symbolFontProgram(p.ctx, resolved.Data, name) + if err != nil { + return nil, err + } + if len(names) == 0 { p.symbolFonts[source] = nil return nil, nil } - standard, err := p.reader.ReadFontContext(p.ctx, pdfgo.Dictionary{"Type": pdfgo.Name("Font"), "Subtype": pdfgo.Name("Type1"), "BaseFont": pdfgo.Name(name)}) - if err != nil { - return nil, err - } - codes := make([]byte, 256) - for index := range codes { - codes[index] = byte(index) - } - glyphs, err := standard.DecodeContext(p.ctx, codes) - if err != nil { - return nil, err - } - original := parseCmapMappings(tables["cmap"]) - mapping := make(map[rune]uint16) - names := make(map[string]uint16) - for _, glyph := range glyphs { - if glyph.Name == ".notdef" || glyph.Name == "" { - continue - } - id := original[rune(glyph.Code)] - for _, base := range [...]rune{0xf000, 0xf100, 0xf200} { - if candidate := original[base+rune(glyph.Code)]; candidate != 0 { - if id != 0 && id != candidate { - return nil, fmt.Errorf("ambiguous PDF symbol font mapping") - } - id = candidate - } - } - if id == 0 { - continue - } - names[glyph.Name] = id - if char, size := utf8.DecodeRuneInString(glyph.Text); size != 0 && size == len(glyph.Text) { - mapping[char] = id - } - } - if len(mapping) == 0 { - p.symbolFonts[source] = nil - return nil, nil - } - tables["cmap"] = buildCmapTable(0, mapping) - program, err := serializeOTF(tables) - if err != nil { - return nil, err - } resource, err := p.editor.backends.FontResources.OpenFontResource(FontFile{Name: name + ".ttf", Data: program}, 0) if err != nil { return nil, err @@ -234,29 +192,6 @@ func (p *pdfImporter) symbolFont(source *pdfgo.Font) (*pdfImportedFont, error) { return result, nil } -// pdfSymbolCmap 识别不含Unicode字符表的Windows符号字体 -// 入参: data 字符映射表 -// 返回: bool 是否为旧式符号字体 -func pdfSymbolCmap(data []byte) bool { - if len(data) < 4 { - return false - } - count := int(binary.BigEndian.Uint16(data[2:])) - if count > (len(data)-4)/8 { - return false - } - symbol := false - for i := range count { - pos := 4 + i*8 - platform, encoding := binary.BigEndian.Uint16(data[pos:]), binary.BigEndian.Uint16(data[pos+2:]) - if platform == 0 || platform == 3 && (encoding == 1 || encoding == 10) { - return false - } - symbol = symbol || platform == 3 && encoding == 0 - } - return symbol -} - // pdfFontProgram 为PDF子集字体补齐封装表,保留字形轮廓和编号 // 无效字符表按PDF已解码字形重建,不改变字符对应的字形编号 // 入参: source PDF字体, type1 已解析的Type1程序,nil时按需解析