From 58ec89ba23a9122c4ab9ad4c6a8dd6227353c349 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Sun, 4 Oct 2026 19:39:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AD=97=E4=BD=93=E5=A4=84=E7=90=86):=20?= =?UTF-8?q?=E8=A1=A5=E9=BD=90=E5=AD=97=E4=BD=93=E8=BD=AC=E6=8D=A2=E5=B9=B6?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=9F=A9=E9=98=B5=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 4 +- ofdgo_font_cff.go | 46 +++++++++++++++-- ofdgo_font_cff_type1.go | 102 +++++++++++++++++++++++++++++++++++++ ofdgo_font_type1.go | 108 ++++++++++++++++++++++++++++++++++++---- 5 files changed, 244 insertions(+), 18 deletions(-) create mode 100644 ofdgo_font_cff_type1.go diff --git a/go.mod b/go.mod index 5d882a6..c194b9a 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 6c8811d..d40a3e9 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/ofdgo_font_cff.go b/ofdgo_font_cff.go index 21581a6..b950b95 100644 --- a/ofdgo_font_cff.go +++ b/ofdgo_font_cff.go @@ -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 { diff --git a/ofdgo_font_cff_type1.go b/ofdgo_font_cff_type1.go new file mode 100644 index 0000000..c1a2599 --- /dev/null +++ b/ofdgo_font_cff_type1.go @@ -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 +} diff --git a/ofdgo_font_type1.go b/ofdgo_font_type1.go index 7fdfee7..3e6b8b0 100644 --- a/ofdgo_font_type1.go +++ b/ofdgo_font_type1.go @@ -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)