// 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" "fmt" "math" "slices" ) // type2Font 保存Type2字形程序、子程序及私有字宽 type type2Font struct { data []byte chars [][]byte locals [][]byte globals [][]byte names map[string]int nominal, def float64 seed uint64 } // type2State 按Type2规范保存跨子程序共享的执行状态 type type2State struct { font *type2Font widthTarget *type2Font matrix *[6]float64 args []float64 transient [32]float64 stored uint32 width float64 widthSet bool widthOnly bool hints int x, y float64 outX, outY float64 originX, originY float64 component bool stripHints bool pathStarted bool changed bool output *bytes.Buffer seed uint64 steps *int dependencies map[uint16]bool } // readType2Index 读取并校验CFF索引的全部偏移,不复制程序字节 // 入参: data 字体数据, offset 索引偏移 // 返回: [][]byte 索引项, int 索引结束位置, error 错误信息 func readType2Index(data []byte, offset int) ([][]byte, int, error) { count, base, end, err := scanCFFIndex(data, offset) if err != nil || count == 0 { return nil, end, err } width := int(data[offset+2]) items := make([][]byte, count) previous := 1 for index := 0; index <= count; index++ { value := uint64(0) for _, b := range data[offset+3+index*width : offset+3+(index+1)*width] { value = value<<8 | uint64(b) } if index != 0 { items[index-1] = data[base+previous-1 : base+int(value)-1] } previous = int(value) } return items, end, nil } // readType2Font 读取单FD字体的Type2执行资源,CID字体先经字体规范化 // 入参: data CFF字体数据 // 返回: *type2Font 执行资源, error 错误信息 func readType2Font(data []byte) (*type2Font, error) { if !isBareCFFData(data) { return nil, fmt.Errorf("invalid CFF header") } names, end, err := readType2Index(data, int(data[2])) if err != nil { return nil, fmt.Errorf("invalid CFF name index: %w", err) } if len(names) != 1 { return nil, fmt.Errorf("invalid CFF font count") } tops, end, err := readType2Index(data, end) if err != nil { return nil, fmt.Errorf("invalid CFF top index: %w", err) } if len(tops) != 1 { return nil, fmt.Errorf("invalid CFF top dictionary count") } _, end, err = readType2Index(data, end) if err != nil { return nil, err } globals, _, err := readType2Index(data, end) if err != nil { return nil, err } dict, err := readCFFDict(tops[0]) if err != nil { return nil, err } if value, ok := dict[1206]; ok && (len(value) != 1 || value[0] != 2) { return nil, fmt.Errorf("unsupported CFF charstring type") } if _, cid := dict[1230]; cid { return nil, fmt.Errorf("CID CFF requires FD normalization") } offset := dict[17] if len(offset) != 1 || !finite(offset[0]) || offset[0] < 0 || offset[0] > float64(len(data)) || offset[0] != math.Trunc(offset[0]) { return nil, fmt.Errorf("invalid CFF charstrings offset") } chars, _, err := readType2Index(data, int(offset[0])) if err != nil { return nil, fmt.Errorf("invalid CFF charstrings: %w", err) } if len(chars) == 0 { return nil, fmt.Errorf("empty CFF charstrings") } font := &type2Font{data: data, chars: chars, globals: globals, seed: 1} if _, err := readType2Private(data, dict[18], font); err != nil { return nil, err } return font, nil } // readType2Private 读取FD私有字宽、随机种子和局部子程序 // 入参: data 字体数据, values Private范围, font 执行资源 // 返回: cffDict 私有字典, error 范围或字段错误 func readType2Private(data []byte, values []float64, font *type2Font) (cffDict, error) { private := cffDict{} var err error if values != nil { if len(values) != 2 || !finite(values[0]) || !finite(values[1]) || values[0] < 0 || values[1] < 0 || values[0] > float64(len(data)) || values[1] > float64(len(data))-values[0] || values[0] != math.Trunc(values[0]) || values[1] != math.Trunc(values[1]) { return nil, fmt.Errorf("invalid CFF private range") } start := int(values[1]) private, err = readCFFDict(data[start : start+int(values[0])]) if err != nil { return nil, err } for op, target := range map[int]*float64{20: &font.def, 21: &font.nominal} { if value, ok := private[op]; ok { if len(value) != 1 || !finite(value[0]) { return nil, fmt.Errorf("invalid CFF private width") } *target = value[0] } } if value, ok := private[1219]; ok { if len(value) != 1 || !finite(value[0]) || value[0] < math.MinInt32 || value[0] > math.MaxInt32 || value[0] != math.Trunc(value[0]) { return nil, fmt.Errorf("invalid CFF random seed") } font.seed = uint64(int64(value[0])) | 1 } if value, ok := private[19]; ok { if len(value) != 1 || !finite(value[0]) || value[0] < 0 || value[0] > float64(len(data)-start) || value[0] != math.Trunc(value[0]) { return nil, fmt.Errorf("invalid CFF local subroutine offset") } font.locals, _, err = readType2Index(data, start+int(value[0])) if err != nil { return nil, err } } } return private, nil } // normalizeType2ProgramsMatrix 规范化字形指令并按需变换轮廓与字宽 // 入参: data CFF字体数据, matrix 设计坐标变换,nil时保留原坐标 // 返回: [][]byte 字形程序, bool 是否变更, error 错误信息 func normalizeType2ProgramsMatrix(data []byte, matrix *[6]float64) ([][]byte, bool, error) { font, err := readType2Font(data) if err != nil { return nil, false, err } result := slices.Clone(font.chars) changed := false var stack [48]float64 for gid, program := range font.chars { if len(program) == 0 { if matrix == nil { continue } program = []byte{14} } steps := 0 state := type2State{font: font, args: stack[:0], width: font.def, seed: font.seed + uint64(gid), steps: &steps} _, err := state.run(program, 0) if err != nil { continue } if !state.changed && matrix == nil { continue } var output bytes.Buffer steps = 0 state = type2State{font: font, args: stack[:0], width: font.def, seed: font.seed + uint64(gid), steps: &steps, output: &output} if matrix != nil { state.matrix, state.widthTarget, state.stripHints = matrix, &type2Font{}, true } if _, err := state.run(program, 0); err != nil { return nil, false, fmt.Errorf("CFF glyph %d: %w", gid, err) } result[gid] = output.Bytes() changed = true } return result, changed, nil } // push 按Type2规范附录B校验并追加操作数 // 入参: value 操作数 // 返回: error 栈或数值错误 func (s *type2State) push(value float64) error { if !finite(value) || len(s.args) >= 48 { return fmt.Errorf("invalid Type2 operand stack") } s.args = append(s.args, value) return nil } // emit 写入已经求值的操作数及指令,分析阶段不分配输出 // 入参: op 操作符, values 操作数 // 返回: error 数值编码或程序长度错误 func (s *type2State) emit(op int, values []float64) error { if s.output == nil { return nil } for _, value := range values { if err := encodeType2Number(s.output, value); err != nil { return err } } if op >= 1200 { s.output.WriteByte(12) s.output.WriteByte(byte(op - 1200)) } else { s.output.WriteByte(byte(op)) } if s.output.Len() > 65535 { return fmt.Errorf("expanded Type2 charstring exceeds 65535 bytes") } return nil } // takeWidth 在首次清栈指令前提取可选字宽,组件不写入自身字宽 // 入参: explicit 是否含字宽 // 返回: error 字宽编码错误 func (s *type2State) takeWidth(explicit bool) error { if s.widthSet { return nil } s.widthSet = true if explicit { s.width = s.font.nominal + s.args[0] if !finite(s.width) { return fmt.Errorf("invalid Type2 width") } if s.output != nil && !s.component && s.widthTarget == nil { if err := encodeType2Number(s.output, s.args[0]); err != nil { return err } } copy(s.args, s.args[1:]) s.args = s.args[:len(s.args)-1] } if s.output != nil && !s.component && s.widthTarget != nil && s.width != s.widthTarget.def { width := s.width if s.matrix != nil { width *= s.matrix[0] } return encodeType2Number(s.output, width-s.widthTarget.nominal) } return nil } // run 执行Type2程序,操作数和暂存数组在子程序间共享 // 入参: data 字形或子程序, depth 调用深度 // 返回: bool 是否结束字形, error 指令错误 func (s *type2State) run(data []byte, depth int) (bool, error) { if depth > 10 || len(data) > 65535 { return false, fmt.Errorf("Type2 implementation limit exceeded") } for index := 0; index < len(data); { (*s.steps)++ if *s.steps > 1<<20 { return false, fmt.Errorf("Type2 execution limit exceeded") } op := int(data[index]) start := index index++ if op == 28 || op >= 32 { length := 1 if op == 28 { length = 3 } else if op == 255 { length = 5 } else if op >= 247 { length = 2 } if length > len(data)-start { return false, fmt.Errorf("truncated Type2 number") } index = start + length if err := s.push(parseNumberType2(data, start)); err != nil { return false, err } continue } if op == 12 { if index == len(data) { return false, fmt.Errorf("truncated Type2 operator") } op = 1200 + int(data[index]) index++ } switch op { case 10, 29: if len(s.args) == 0 { return false, fmt.Errorf("missing Type2 subroutine number") } value := s.args[len(s.args)-1] s.args = s.args[:len(s.args)-1] subrs := s.font.locals if op == 29 { subrs = s.font.globals } value += float64(cffSubrBias(len(subrs))) if value < 0 || value >= float64(len(subrs)) || value != math.Trunc(value) { return false, fmt.Errorf("invalid Type2 subroutine number") } ended, err := s.run(subrs[int(value)], depth+1) if ended || err != nil { return ended, err } case 11: if depth == 0 { return false, fmt.Errorf("Type2 return outside subroutine") } return false, nil case 1, 3, 18, 23, 19, 20: if err := s.takeWidth(len(s.args)%2 == 1); err != nil { return false, err } if s.widthOnly { return true, nil } if len(s.args)%2 != 0 || s.hints+len(s.args)/2 > 96 { return false, fmt.Errorf("invalid Type2 stem hints") } s.hints += len(s.args) / 2 if op == 19 || op == 20 { if s.hints == 0 { return false, fmt.Errorf("Type2 mask has no stem hints") } length := (s.hints + 7) / 8 if length > len(data)-index { return false, fmt.Errorf("truncated Type2 hint mask") } if !s.component && !s.stripHints && len(s.args) > 0 { if err := s.emit(23, s.args); err != nil { return false, err } if op == 20 { s.changed = true } } if !s.component && !s.stripHints { if err := s.emit(op, nil); err != nil { return false, err } if s.output != nil { s.output.Write(data[index : index+length]) } } index += length } else if !s.component && !s.stripHints { if err := s.emit(op, s.args); err != nil { return false, err } } s.args = s.args[:0] case 4, 21, 22: count := 1 if op == 21 { count = 2 } if err := s.takeWidth(len(s.args) == count+1); err != nil { return false, err } if len(s.args) != count { return false, fmt.Errorf("invalid Type2 moveto operands") } if s.widthOnly { return true, nil } if op != 4 { s.x += s.args[0] } if op != 22 { s.y += s.args[count-1] } if !finite(s.x) || !finite(s.y) { return false, fmt.Errorf("invalid Type2 current point") } if s.matrix != nil { x, y := s.transformedPoint() if err := s.emit(21, []float64{x - s.outX, y - s.outY}); err != nil { return false, err } } else if s.component { if err := s.emit(21, []float64{s.x + s.originX - s.outX, s.y + s.originY - s.outY}); err != nil { return false, err } } else if err := s.emit(op, s.args); err != nil { return false, err } s.outX, s.outY = s.transformedPoint() s.pathStarted = true s.args = s.args[:0] case 14: if err := s.takeWidth(len(s.args) == 1 || len(s.args) == 5); err != nil { return false, err } if s.widthOnly { return true, nil } if len(s.args) == 4 { if s.component { return false, fmt.Errorf("nested Type2 composite glyph") } if err := s.appendComponent(s.args[2], 0, 0); err != nil { return false, err } if err := s.appendComponent(s.args[3], s.args[0], s.args[1]); err != nil { return false, err } s.changed = true } else if len(s.args) != 0 { return false, fmt.Errorf("invalid Type2 endchar operands") } if !s.component { if err := s.emit(14, nil); err != nil { return false, err } } return true, nil case 1200: s.changed = true case 5, 6, 7, 8, 24, 25, 26, 27, 30, 31, 1234, 1235, 1236, 1237: if !s.pathStarted { return false, fmt.Errorf("Type2 path has no moveto") } if err := s.draw(op); err != nil { return false, err } default: if err := s.calculate(op); err != nil { return false, err } s.changed = true } } return false, fmt.Errorf("Type2 program has no terminator") } // appendComponent 按StandardEncoding查找组合组件,保持源字宽与独立暂存状态 // 入参: code 标准字符码, x 横向偏移, y 纵向偏移 // 返回: error 组件或指令错误 func (s *type2State) appendComponent(code, x, y float64) error { if code < 0 || code > 255 || code != math.Trunc(code) || type1StandardNames[int(code)] == "" { return fmt.Errorf("invalid Type2 component code") } if s.font.names == nil { sids, _, _, index, ok := getCFFCharsetInfo(s.font.data, len(s.font.chars)) if !ok { return fmt.Errorf("invalid Type2 composite charset") } s.font.names = make(map[string]int, len(sids)) for gid, sid := range sids { s.font.names[getCFFSIDString(s.font.data, index, sid)] = gid } } name := type1StandardNames[int(code)] gid, ok := s.font.names[name] if !ok { return fmt.Errorf("missing Type2 component %s", name) } if s.dependencies != nil { s.dependencies[uint16(gid)] = true } child := type2State{font: s.font, matrix: s.matrix, width: s.font.def, component: true, output: s.output, originX: x, originY: y, outX: s.outX, outY: s.outY, seed: s.font.seed + uint64(gid), steps: s.steps, stripHints: s.stripHints, dependencies: s.dependencies} _, err := child.run(s.font.chars[gid], 0) if err != nil { return fmt.Errorf("invalid Type2 component %s: %w", name, err) } s.outX, s.outY = child.outX, child.outY return nil }