feat(字体处理): 补齐字体转换并修复矩阵处理
build.yaml / build (push) Successful in 50s

This commit is contained in:
xiaoqidun committed 2026-10-04 19:39:38 +08:00
1 parent be697e7045
commit 58ec89ba23
5 files changed
+244 -18

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1
github.com/xiaoqidun/pdfgo v0.0.0-20261004101230-2c0a061a6bb9
github.com/xiaoqidun/pdfgo v0.0.0-20261004113655-d823d12ace54
golang.org/x/crypto v0.57.0
golang.org/x/image v0.46.0
golang.org/x/text v0.42.0
+2 -2
View File
@@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
github.com/xiaoqidun/pdfgo v0.0.0-20261004101230-2c0a061a6bb9 h1:bIYVlgg5J18Az+lIMWJk1FgyitZMVMkF7zj5+LGkQoc=
github.com/xiaoqidun/pdfgo v0.0.0-20261004101230-2c0a061a6bb9/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
github.com/xiaoqidun/pdfgo v0.0.0-20261004113655-d823d12ace54 h1:j87SNncDFLPpUYwZb9HU6AZXMb+C4lQipQb5U6V4FAk=
github.com/xiaoqidun/pdfgo v0.0.0-20261004113655-d823d12ace54/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+42 -4
View File
@@ -155,8 +155,17 @@ func normalizeCFFCharstringsAt(data []byte, unitsPerEm uint16) ([]byte, error) {
if !changed {
return data, nil
}
return replaceCFFCharstrings(data, dict, topStart, topSize, charStart, charSize, chars, explicitCharset), nil
}
// replaceCFFCharstrings 替换字形索引并调整后续数据及顶层字典的偏移
// 入参: data 原始字体, dict 输出字典, topStart 顶层索引起点, topSize 顶层索引大小
// 入参: charStart 字形索引起点, charSize 字形索引大小, chars 字形程序, explicitCharset 追加字符集
// 返回: []byte 重建后的字体数据
func replaceCFFCharstrings(data []byte, dict cffDict, topStart, topSize, charStart, charSize int, chars [][]byte, explicitCharset []byte) []byte {
encodedChars := encodeCFFIndex(chars)
topEnd := topStart + topSize
topData, _ := getCFFIndexData(data, topStart)
originalDict := parseCFFDict(topData)
newTop := encodeCFFIndex([][]byte{encodeCFFDict(dict)})
for range 6 {
@@ -179,7 +188,7 @@ func normalizeCFFCharstringsAt(data []byte, unitsPerEm uint16) ([]byte, error) {
dict[op] = []float64{float64(offset + shift)}
}
}
if values := dict[18]; len(values) == 2 {
if values := dict[18]; len(values) == 2 && values[0] != 0 {
original := originalDict[18]
offset := int(original[1])
if offset >= charStart+charSize {
@@ -202,7 +211,7 @@ func normalizeCFFCharstringsAt(data []byte, unitsPerEm uint16) ([]byte, error) {
result = append(result, encodedChars...)
result = append(result, data[charStart+charSize:]...)
result = append(result, explicitCharset...)
return result, nil
return result
}
// sanitizeCFF 尝试清洗CFF数据,转换CID字体并合并FontMatrix
@@ -243,7 +252,14 @@ func sanitizeCFF(data []byte) ([]byte, error) {
if err != nil {
return nil, err
}
kind, err := cffCharstringType(topDict)
if err != nil {
return nil, err
}
if _, isCID := topDict[1230]; !isCID {
if kind == 1 {
return normalizeCFFType1(data, topDict)
}
return data, nil
}
fdArrOff, err := cffDictOffset(data, topDict, 1236)
@@ -266,7 +282,7 @@ func sanitizeCFF(data []byte) ([]byte, error) {
if err != nil {
return nil, err
}
if fdMatrix != [6]float64{1, 0, 0, 1, 0, 0} {
if kind == 1 || fdMatrix != [6]float64{1, 0, 0, 1, 0, 0} {
return sanitizeMultiFDCFF(data, hdrSize, nameIndexData, topDict, stringIndexData, globalSubrIndexData, fdArrOff, fdCount)
}
charStringsOff, err := cffDictOffset(data, topDict, 17)
@@ -344,6 +360,10 @@ func normalizeCFFPrivate(dict cffDict) error {
// 入参: stringIndexData 字符串索引, globalSubrIndexData 全局子程序索引, fdArrOff FDArray偏移, fdCount FD数量
// 返回: []byte 清洗后的CFF数据, error 错误信息
func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict cffDict, stringIndexData []byte, globalSubrIndexData []byte, fdArrOff int, fdCount int) ([]byte, error) {
kind, err := cffCharstringType(topDict)
if err != nil {
return nil, err
}
charStringsOff, err := cffDictOffset(data, topDict, 17)
if err != nil {
return nil, err
@@ -375,6 +395,10 @@ func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict
return nil, fmt.Errorf("invalid CFF FDArray count")
}
fonts := make([]type2Font, fdCount)
var type1Fonts []*type1Program
if kind == 1 {
type1Fonts = make([]*type1Program, fdCount)
}
privateDicts := make([]cffDict, fdCount)
matrices := make([][6]float64, fdCount)
transformed := false
@@ -408,6 +432,13 @@ func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict
if err := normalizeCFFPrivate(privateDicts[fd]); err != nil {
return nil, fmt.Errorf("CFF font dictionary %d: %w", fd, err)
}
if kind == 1 {
type1Fonts[fd], err = readCFFType1Program(data, charStrings, fontDict[18], true)
if err != nil {
return nil, fmt.Errorf("CFF font dictionary %d: %w", fd, err)
}
fonts[fd].def, fonts[fd].nominal = 0, 0
}
}
target := &fonts[0]
if transformed {
@@ -417,6 +448,12 @@ func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict
var stack [48]float64
for gid, cs := range charStrings {
font := &fonts[fdSelect[gid]]
if kind == 1 {
cs, err = type1Fonts[fdSelect[gid]].outline(type1Glyph{name: fmt.Sprintf("gid%d", gid), data: cs})
if err != nil {
return nil, err
}
}
var output bytes.Buffer
steps := 0
state := type2State{font: font, args: stack[:0], widthTarget: target, width: font.def, seed: font.seed + uint64(gid), steps: &steps, output: &output, stripHints: true}
@@ -437,6 +474,7 @@ func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict
delete(topDict, 1234)
delete(topDict, 15)
delete(topDict, 16)
delete(topDict, 1206)
if transformed {
scale := 1 / float64(units)
topDict[1207] = []float64{scale, 0, 0, scale, 0, 0}
@@ -451,7 +489,7 @@ func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict
}
}
delete(privateDict, 19)
if transformed {
if transformed || kind == 1 {
privateDict = cffDict{20: {0}, 21: {0}}
}
if err := normalizeCFFPrivate(privateDict); err != nil {
+102
View File
@@ -0,0 +1,102 @@
// 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"
// cffCharstringType 读取CFF字形程序类型,缺省为Type2
// 入参: dict 顶层字典
// 返回: int 字形程序类型, error 不支持的类型
func cffCharstringType(dict cffDict) (int, error) {
values, ok := dict[1206]
if !ok {
return 2, nil
}
if len(values) != 1 || values[0] != 1 && values[0] != 2 {
return 0, fmt.Errorf("unsupported CFF charstring type")
}
return int(values[0]), nil
}
// readCFFType1Program 读取Type1字形及无偏置局部子程序,保留源字形顺序
// 入参: data CFF字体, chars 字形程序, private 私有字典范围, cid 是否为CID字体
// 返回: *type1Program 执行资源, error 字符集或私有字典错误
func readCFFType1Program(data []byte, chars [][]byte, private []float64, cid bool) (*type1Program, error) {
resources := &type2Font{}
if _, err := readType2Private(data, private, resources); err != nil {
return nil, err
}
program := &type1Program{subrs: resources.locals, lenIV: -1}
if cid {
return program, nil
}
identifiers, _, _, stringsOffset, ok := getCFFCharsetInfo(data, len(chars))
if !ok {
return nil, fmt.Errorf("invalid CFF Type1 charset")
}
strings, _, err := readType2Index(data, stringsOffset)
if err != nil {
return nil, err
}
program.glyphs = make([]type1Glyph, len(chars))
for gid, charstring := range chars {
name := ".notdef"
if gid != 0 {
sid := identifiers[gid]
if sid < len(cffStandardStrings) {
name = cffStandardStrings[sid]
} else if sid-len(cffStandardStrings) < len(strings) {
name = string(strings[sid-len(cffStandardStrings)])
} else {
return nil, fmt.Errorf("invalid CFF Type1 glyph name")
}
if name == "" {
return nil, fmt.Errorf("invalid CFF Type1 glyph name")
}
}
program.glyphs[gid] = type1Glyph{name: name, data: charstring}
}
return program, nil
}
// normalizeCFFType1 转换非CID字体的Type1程序,保留字符集、编码及字形编号
// 入参: data CFF字体, dict 顶层字典
// 返回: []byte Type2格式CFF, error 字形程序或范围错误
func normalizeCFFType1(data []byte, dict cffDict) ([]byte, error) {
_, nameSize := getCFFIndexCount(data, int(data[2]))
topStart := int(data[2]) + nameSize
_, topSize := getCFFIndexCount(data, topStart)
charStart, err := cffDictOffset(data, dict, 17)
if err != nil {
return nil, err
}
chars, charEnd, err := readType2Index(data, charStart)
if err != nil || len(chars) == 0 || charStart < topStart+topSize {
return nil, fmt.Errorf("invalid CFF Type1 charstrings")
}
program, err := readCFFType1Program(data, chars, dict[18], false)
if err != nil {
return nil, err
}
converted := make([][]byte, len(chars))
for gid, glyph := range program.glyphs {
converted[gid], err = program.outline(glyph)
if err != nil {
return nil, err
}
}
dict[1206], dict[18] = []float64{2}, []float64{0, 0}
return replaceCFFCharstrings(data, dict, topStart, topSize, charStart, charEnd-charStart, converted, nil), nil
}
+97 -11
View File
@@ -46,6 +46,7 @@ type type1Program struct {
glyphs []type1Glyph
subrs [][]byte
lenIV int
matrix []float64
}
// type1Glyph 保存字形名称及指令
@@ -63,6 +64,7 @@ type type1Scanner struct {
// type1Outline 保存字形指令的操作数栈及轮廓位置
type type1Outline struct {
program *type1Program
steps *int
active map[string]bool
args []float64
postscript []float64
@@ -91,12 +93,10 @@ func parseType1Program(data []byte) (type1Program, error) {
if err != nil {
return type1Program{}, err
}
marker := []byte("eexec")
start := bytes.Index(data, marker)
if start < 0 {
return type1Program{}, fmt.Errorf("Type1 font lacks eexec data")
matrix, start, err := readType1Header(data)
if err != nil {
return type1Program{}, err
}
start += len(marker)
for start < len(data) && type1Space(data[start]) {
start++
}
@@ -129,7 +129,7 @@ func parseType1Program(data []byte) (type1Program, error) {
}
decrypted := type1Decrypt(encrypted, 55665)[4:]
scanner := type1Scanner{data: decrypted}
program := type1Program{lenIV: 4}
program := type1Program{lenIV: 4, matrix: matrix}
section := ""
for {
token := scanner.next()
@@ -212,6 +212,46 @@ func parseType1Program(data []byte) (type1Program, error) {
return program, nil
}
// readType1Header 读取明文字体矩阵及加密数据起点
// 入参: data Type1程序
// 返回: []float64 字体矩阵, int 加密起点, error 头部错误
func readType1Header(data []byte) ([]float64, int, error) {
scanner := type1Scanner{data: data}
var matrix []float64
for {
switch scanner.next() {
case "":
return nil, 0, fmt.Errorf("Type1 font lacks eexec data")
case "eexec":
return matrix, scanner.pos, nil
case "/FontMatrix":
closing := ""
switch scanner.next() {
case "[":
closing = "]"
case "{":
closing = "}"
default:
return nil, 0, fmt.Errorf("invalid Type1 FontMatrix array")
}
matrix = make([]float64, 6)
for index := range matrix {
value, err := strconv.ParseFloat(scanner.next(), 64)
if err != nil || !finite(value) {
return nil, 0, fmt.Errorf("invalid Type1 FontMatrix value")
}
matrix[index] = value
}
if scanner.next() != closing {
return nil, 0, fmt.Errorf("invalid Type1 FontMatrix length")
}
if _, err := readCFFMatrix(cffDict{1207: matrix}, [6]float64{}); err != nil {
return nil, 0, fmt.Errorf("invalid Type1 FontMatrix: %w", err)
}
}
}
}
// flattenType1 将PFB分段容器转换为连续的Type1程序
// 入参: data 字体数据
// 返回: []byte 连续程序, error 错误信息
@@ -312,11 +352,41 @@ func (s *type1Scanner) next() string {
return ""
}
start := s.pos
if bytes.IndexByte([]byte("[]{}"), s.data[s.pos]) >= 0 {
if s.data[s.pos] == '(' {
depth := 1
s.pos++
for s.pos < len(s.data) && depth > 0 {
switch s.data[s.pos] {
case '\\':
if s.pos+1 < len(s.data) {
s.pos++
}
case '(':
depth++
case ')':
depth--
}
s.pos++
}
return string(s.data[start:s.pos])
}
if s.data[s.pos] == '<' && (s.pos+1 == len(s.data) || s.data[s.pos+1] != '<') {
for s.pos < len(s.data) && s.data[s.pos] != '>' {
s.pos++
}
if s.pos < len(s.data) {
s.pos++
}
return string(s.data[start:s.pos])
}
if bytes.IndexByte([]byte("[]{}()<>"), s.data[s.pos]) >= 0 {
s.pos++
return string(s.data[start:s.pos])
}
for s.pos < len(s.data) && !type1Space(s.data[s.pos]) && bytes.IndexByte([]byte("[]{}%"), s.data[s.pos]) < 0 {
if s.data[s.pos] == '/' {
s.pos++
}
for s.pos < len(s.data) && !type1Space(s.data[s.pos]) && bytes.IndexByte([]byte("[]{}%()/<>"), s.data[s.pos]) < 0 {
s.pos++
}
return string(s.data[start:s.pos])
@@ -375,7 +445,14 @@ func (s *type1Outline) run(data []byte, depth int) (bool, error) {
if depth > 32 {
return false, fmt.Errorf("Type1 subroutine recursion")
}
if s.steps == nil {
s.steps = new(int)
}
for index := 0; index < len(data); index++ {
(*s.steps)++
if *s.steps > 1<<20 {
return false, fmt.Errorf("Type1 execution limit exceeded")
}
op := int(data[index])
if op >= 32 {
var value float64
@@ -401,6 +478,9 @@ func (s *type1Outline) run(data []byte, depth int) (bool, error) {
value = float64(int32(binary.BigEndian.Uint32(data[index+1 : index+5])))
index += 4
}
if len(s.args) >= 24 {
return false, fmt.Errorf("Type1 operand limit exceeded")
}
s.args = append(s.args, value)
continue
}
@@ -443,7 +523,7 @@ func (s *type1Outline) run(data []byte, depth int) (bool, error) {
return true, nil
}
}
return false, nil
return false, fmt.Errorf("unterminated Type1 charstring")
}
// operator 执行单个Type1绘制指令并清空已消费的操作数
@@ -601,7 +681,7 @@ func (s *type1Outline) operator(op int) error {
return fmt.Errorf("unsupported Type1 OtherSubr %d", number)
}
case 1217:
if len(s.postscript) == 0 {
if len(s.postscript) == 0 || len(a) >= 24 {
return fmt.Errorf("invalid Type1 pop")
}
s.args = append(a, s.postscript[0])
@@ -629,6 +709,9 @@ func (s *type1Outline) operator(op int) error {
// 入参: code 标准编码, x 横向原点, y 纵向原点
// 返回: error 字形缺失、循环或指令错误
func (s *type1Outline) component(code, x, y float64) error {
if len(s.active) >= 32 {
return fmt.Errorf("Type1 component recursion")
}
if code < 0 || code > 255 || code != math.Trunc(code) {
return fmt.Errorf("invalid Type1 component code")
}
@@ -643,7 +726,7 @@ func (s *type1Outline) component(code, x, y float64) error {
if glyph.name != name {
continue
}
child := type1Outline{program: s.program, active: s.active, originX: x, originY: y, pointX: s.pointX, pointY: s.pointY}
child := type1Outline{program: s.program, steps: s.steps, active: s.active, originX: x, originY: y, pointX: s.pointX, pointY: s.pointY}
s.active[name] = true
ended, err := child.run(glyph.data, 0)
delete(s.active, name)
@@ -798,6 +881,9 @@ func (p *type1Program) toCFF(name string) ([]byte, error) {
charIndex := encodeCFFIndex(chars)
topDict := make([]byte, 12)
topDict[0], topDict[5], topDict[6], topDict[11] = 29, 15, 29, 17
if p.matrix != nil {
topDict = append(topDict, encodeCFFDict(cffDict{1207: p.matrix})...)
}
topIndex := encodeCFFIndex([][]byte{topDict})
charsetOffset := 4 + len(nameIndex) + len(topIndex) + len(stringIndex) + len(globalSubrs)
charOffset := charsetOffset + len(charset)