diff --git a/go.mod b/go.mod index 303389f..5d882a6 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-20261004082616-4fa2e8738fca + github.com/xiaoqidun/pdfgo v0.0.0-20261004101230-2c0a061a6bb9 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 36687d3..6c8811d 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-20261004082616-4fa2e8738fca h1:9Q47dgBr5UXsOcXZ9X5xYO4xzxaJYldslT07WRiFCak= -github.com/xiaoqidun/pdfgo v0.0.0-20261004082616-4fa2e8738fca/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +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/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 04471dc..21581a6 100644 --- a/ofdgo_font_cff.go +++ b/ofdgo_font_cff.go @@ -52,25 +52,17 @@ func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) { if err != nil { return nil, nil, fmt.Errorf("CFF glyph widths: %w", err) } - var unitsPerEm uint16 = 1000 - if len(cffData) > 4 { - hdrSize := int(cffData[2]) - off := hdrSize - _, sz := getCFFIndexCount(cffData, off) - off += sz - topDictData, _ := getCFFIndexData(cffData, off) - if topDictData != nil { - td := parseCFFDict(topDictData) - if mat, ok := td[1207]; ok && len(mat) > 0 { - if mat[0] != 0 { - val := 1.0 / mat[0] - if val > 0 { - unitsPerEm = uint16(math.Round(val)) - } - } - } - } + _, nameSize := getCFFIndexCount(cffData, int(cffData[2])) + topData, _ := getCFFIndexData(cffData, int(cffData[2])+nameSize) + top, err := readCFFDict(topData) + if err != nil { + return nil, nil, err } + matrix, err := readCFFMatrix(top, [6]float64{0.001, 0, 0, 0.001, 0, 0}) + if err != nil { + return nil, nil, err + } + unitsPerEm := cffUnitsPerEm(matrix) tables := make(map[string][]byte) tables["CFF "] = cffData tables["head"] = buildHeadTable(unitsPerEm) @@ -89,6 +81,13 @@ func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) { // 入参: data CFF字体数据 // 返回: []byte 标准化后的CFF数据, error 错误信息 func normalizeCFFCharstrings(data []byte) ([]byte, error) { + return normalizeCFFCharstringsAt(data, 0) +} + +// normalizeCFFCharstringsAt 统一CFF设计坐标,现有OpenType字体使用head表的设计单位 +// 入参: data CFF字体数据, unitsPerEm 设计单位,0时由字体矩阵确定 +// 返回: []byte 标准化后的CFF数据, error 错误信息 +func normalizeCFFCharstringsAt(data []byte, unitsPerEm uint16) ([]byte, error) { if len(data) < 4 || int(data[2]) >= len(data) { return nil, fmt.Errorf("invalid CFF header") } @@ -103,19 +102,44 @@ func normalizeCFFCharstrings(data []byte) ([]byte, error) { if err != nil { return nil, err } - charOffset := dict[17] - if len(charOffset) != 1 { - return nil, fmt.Errorf("missing CFF charstrings") + charStart, err := cffDictOffset(data, dict, 17) + if err != nil { + return nil, err } - charStart := int(charOffset[0]) _, charSize := getCFFIndexCount(data, charStart) if charStart < topStart+topSize || charSize == 0 || charStart+charSize > len(data) { return nil, fmt.Errorf("invalid CFF charstrings") } - chars, changed, err := normalizeType2Programs(data) + matrix, err := readCFFMatrix(dict, [6]float64{0.001, 0, 0, 0.001, 0, 0}) if err != nil { return nil, err } + if unitsPerEm == 0 { + unitsPerEm = cffUnitsPerEm(matrix) + } + if unitsPerEm < 16 || unitsPerEm > 16384 { + return nil, fmt.Errorf("invalid CFF design units") + } + var transform *[6]float64 + for i := range matrix { + matrix[i] *= float64(unitsPerEm) + } + if matrix != [6]float64{1, 0, 0, 1, 0, 0} { + transform = &matrix + } + chars, changed, err := normalizeType2ProgramsMatrix(data, transform) + if err != nil { + return nil, err + } + if transform != nil { + scale := 1 / float64(unitsPerEm) + dict[1207] = []float64{scale, 0, 0, scale, 0, 0} + delete(dict, 18) + if err := transformCFFBounds(dict, matrix); err != nil { + return nil, err + } + changed = true + } var explicitCharset []byte if charset := dict[15]; len(charset) == 1 && (charset[0] == 1 || charset[0] == 2) { sids, _, _, _, ok := getCFFCharsetInfo(data, len(chars)) @@ -185,109 +209,95 @@ func normalizeCFFCharstrings(data []byte) ([]byte, error) { // 入参: data 原始CFF数据 // 返回: []byte 清洗后的CFF数据, error 错误信息 func sanitizeCFF(data []byte) ([]byte, error) { - if len(data) < 4 { - return nil, fmt.Errorf("data too short") + if !isBareCFFData(data) { + return nil, fmt.Errorf("invalid CFF header") } hdrSize := int(data[2]) offset := hdrSize - if offset >= len(data) { - return nil, fmt.Errorf("truncated") + names, end, err := readType2Index(data, offset) + if err != nil { + return nil, err } - nameCount, nameSz := getCFFIndexCount(data, offset) - if nameCount != 1 { + if len(names) != 1 { return nil, fmt.Errorf("multi-font cff not supported") } - nameIndexData := data[offset : offset+nameSz] - offset += nameSz - if offset >= len(data) { - return nil, fmt.Errorf("truncated") + nameIndexData := data[offset:end] + tops, end, err := readType2Index(data, end) + if err != nil { + return nil, err } - topCount, topSz := getCFFIndexCount(data, offset) - if topCount != 1 { + if len(tops) != 1 { return nil, fmt.Errorf("top dict count != 1") } - topDictData, _ := getCFFIndexData(data, offset) - offset += topSz - if offset >= len(data) { - return nil, fmt.Errorf("truncated") + _, stringEnd, err := readType2Index(data, end) + if err != nil { + return nil, err } - _, strSz := getCFFIndexCount(data, offset) - stringIndexData := data[offset : offset+strSz] - offset += strSz - if offset >= len(data) { - return nil, fmt.Errorf("truncated") + stringIndexData := data[end:stringEnd] + _, globalEnd, err := readType2Index(data, stringEnd) + if err != nil { + return nil, err } - _, glbSz := getCFFIndexCount(data, offset) - globalSubrIndexData := data[offset : offset+glbSz] - topDict, err := readCFFDict(topDictData) + globalSubrIndexData := data[stringEnd:globalEnd] + topDict, err := readCFFDict(tops[0]) if err != nil { return nil, err } if _, isCID := topDict[1230]; !isCID { return data, nil } - fdArrOffs, ok := topDict[1236] - if !ok || len(fdArrOffs) == 0 { - return nil, fmt.Errorf("cid without fdarray") - } - fdArrOff := int(fdArrOffs[0]) - if fdArrOff >= len(data) { - return nil, fmt.Errorf("fdarray offset oob") - } - fdCount, _ := getCFFIndexCount(data, fdArrOff) - if fdCount != 1 { - return sanitizeMultiFDCFF(data, hdrSize, nameIndexData, topDict, stringIndexData, globalSubrIndexData, fdArrOff, fdCount) - } - fontDictData, _ := getCFFIndexData(data, fdArrOff) - fontDict, err := readCFFDict(fontDictData) + fdArrOff, err := cffDictOffset(data, topDict, 1236) if err != nil { return nil, err } - if fdMat, ok := fontDict[1207]; ok && len(fdMat) == 6 { - topMat, hasTop := topDict[1207] - if !hasTop || len(topMat) != 6 { - topMat = []float64{0.001, 0, 0, 0.001, 0, 0} - } - newMat := multiplyAffine(topMat, fdMat) - topDict[1207] = newMat + fdItems, _, err := readType2Index(data, fdArrOff) + if err != nil { + return nil, err } - privVals, ok := fontDict[18] - if !ok || len(privVals) != 2 { - privVals = []float64{0, 0} + fdCount := len(fdItems) + if fdCount != 1 { + return sanitizeMultiFDCFF(data, hdrSize, nameIndexData, topDict, stringIndexData, globalSubrIndexData, fdArrOff, fdCount) + } + fontDict, err := readCFFDict(fdItems[0]) + if err != nil { + return nil, err + } + fdMatrix, err := readCFFMatrix(fontDict, [6]float64{1, 0, 0, 1, 0, 0}) + if err != nil { + return nil, err + } + if 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) + if err != nil { + return nil, err + } + chars, charEnd, err := readType2Index(data, charStringsOff) + if err != nil || len(chars) == 0 { + return nil, fmt.Errorf("invalid CFF charstrings") + } + selectOffset, err := cffDictOffset(data, topDict, 1237) + if err != nil { + return nil, err + } + if _, err := readType2FDSelect(data, selectOffset, len(chars), 1); err != nil { + return nil, err + } + pDict, err := readType2Private(data, fontDict[18], &type2Font{}) + if err != nil { + return nil, err } - privSize := int(privVals[0]) - privOff := int(privVals[1]) var localSubrData []byte - var privDictData []byte - if privSize > 0 && privOff < len(data) && privOff+privSize <= len(data) { - privDictData = data[privOff : privOff+privSize] - } - var subrsOffRel int - if len(privDictData) > 0 { - pDict, err := readCFFDict(privDictData) + if values := pDict[19]; len(values) != 0 { + start := int(fontDict[18][1]) + int(values[0]) + _, end, err := readType2Index(data, start) if err != nil { return nil, err } - if sVals, ok := pDict[19]; ok && len(sVals) > 0 { - subrsOffRel = int(sVals[0]) - } + localSubrData = data[start:end] } - if subrsOffRel > 0 { - subrsAbs := privOff + subrsOffRel - if subrsAbs < len(data) { - _, subSz := getCFFIndexCount(data, subrsAbs) - if subrsAbs+subSz <= len(data) { - localSubrData = data[subrsAbs : subrsAbs+subSz] - } - } - } - charStringsOffs, ok := topDict[17] - if !ok || len(charStringsOffs) == 0 { - return nil, fmt.Errorf("missing charstrings") - } - charStringsOff := int(charStringsOffs[0]) - _, charStrSz := getCFFIndexCount(data, charStringsOff) - charStringsData := data[charStringsOff : charStringsOff+charStrSz] + charStringsData := data[charStringsOff:charEnd] delete(topDict, 1230) delete(topDict, 1236) delete(topDict, 1237) @@ -295,11 +305,7 @@ func sanitizeCFF(data []byte) ([]byte, error) { delete(topDict, 15) delete(topDict, 16) var finalPrivData []byte - if len(privDictData) > 0 { - pDict, err := readCFFDict(privDictData) - if err != nil { - return nil, err - } + if len(pDict) > 0 { if err := normalizeCFFPrivate(pDict); err != nil { return nil, err } @@ -338,11 +344,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) { - charStringsOffs, ok := topDict[17] - if !ok || len(charStringsOffs) == 0 { - return nil, fmt.Errorf("missing charstrings") + charStringsOff, err := cffDictOffset(data, topDict, 17) + if err != nil { + return nil, err } - charStringsOff := int(charStringsOffs[0]) charStrings, _, err := readType2Index(data, charStringsOff) if err != nil { return nil, err @@ -371,16 +376,42 @@ func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict } fonts := make([]type2Font, fdCount) privateDicts := make([]cffDict, fdCount) + matrices := make([][6]float64, fdCount) + transformed := false + topMatrix, err := readCFFMatrix(topDict, [6]float64{0.001, 0, 0, 0.001, 0, 0}) + if err != nil { + return nil, err + } + units := cffUnitsPerEm(topMatrix) + for i := range topMatrix { + topMatrix[i] *= float64(units) + } for fd, item := range fdItems { fonts[fd] = type2Font{data: data, chars: charStrings, globals: globalSubrs, seed: 1} fontDict, dictErr := readCFFDict(item) if dictErr != nil { return nil, dictErr } + fdMatrix, matrixErr := readCFFMatrix(fontDict, [6]float64{1, 0, 0, 1, 0, 0}) + if matrixErr != nil { + return nil, fmt.Errorf("CFF font dictionary %d: %w", fd, matrixErr) + } + matrices[fd], err = readCFFMatrix(cffDict{1207: multiplyAffine(topMatrix[:], fdMatrix[:])}, [6]float64{}) + if err != nil { + return nil, fmt.Errorf("CFF font dictionary %d: %w", fd, err) + } + transformed = transformed || matrices[fd] != [6]float64{1, 0, 0, 1, 0, 0} privateDicts[fd], err = readType2Private(data, fontDict[18], &fonts[fd]) if err != nil { return nil, fmt.Errorf("CFF font dictionary %d: %w", fd, err) } + if err := normalizeCFFPrivate(privateDicts[fd]); err != nil { + return nil, fmt.Errorf("CFF font dictionary %d: %w", fd, err) + } + } + target := &fonts[0] + if transformed { + target = &type2Font{} } inlined := make([][]byte, len(charStrings)) var stack [48]float64 @@ -388,7 +419,10 @@ func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict font := &fonts[fdSelect[gid]] var output bytes.Buffer steps := 0 - state := type2State{font: font, args: stack[:0], widthTarget: &fonts[0], width: font.def, seed: font.seed + uint64(gid), steps: &steps, output: &output, stripHints: true} + state := type2State{font: font, args: stack[:0], widthTarget: target, width: font.def, seed: font.seed + uint64(gid), steps: &steps, output: &output, stripHints: true} + if matrices[fdSelect[gid]] != [6]float64{1, 0, 0, 1, 0, 0} { + state.matrix = &matrices[fdSelect[gid]] + } if len(cs) == 0 { cs = []byte{14} } @@ -403,6 +437,13 @@ func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict delete(topDict, 1234) delete(topDict, 15) delete(topDict, 16) + if transformed { + scale := 1 / float64(units) + topDict[1207] = []float64{scale, 0, 0, scale, 0, 0} + if err := transformCFFBounds(topDict, topMatrix); err != nil { + return nil, err + } + } privateDict := make(cffDict) if len(privateDicts) > 0 { for k, v := range privateDicts[0] { @@ -410,6 +451,9 @@ func sanitizeMultiFDCFF(data []byte, hdrSize int, nameIndexData []byte, topDict } } delete(privateDict, 19) + if transformed { + privateDict = cffDict{20: {0}, 21: {0}} + } if err := normalizeCFFPrivate(privateDict); err != nil { return nil, err } @@ -449,8 +493,8 @@ func encodeSanitizedCFF(header, names []byte, dict cffDict, strings, globals, ch // 入参: data CFF数据 // 返回: int 字形数量, error 错误信息 func parseCFFAndCountGlyphs(data []byte) (int, error) { - if len(data) < 4 { - return 0, fmt.Errorf("data too short") + if !isBareCFFData(data) { + return 0, fmt.Errorf("invalid CFF header") } hdrSize := int(data[2]) offset := hdrSize @@ -475,12 +519,13 @@ func parseCFFAndCountGlyphs(data []byte) (int, error) { if err != nil { return 0, err } - if offsetVals, ok := dict[17]; ok && len(offsetVals) > 0 { - charStrOff := int(offsetVals[0]) - if charStrOff > 0 && charStrOff < len(data) { - count, _ := getCFFIndexCount(data, charStrOff) - return count, nil - } + charStrOff, err := cffDictOffset(data, dict, 17) + if err != nil { + return 0, err + } + count, size := getCFFIndexCount(data, charStrOff) + if count > 0 && size > 0 { + return count, nil } } return 0, fmt.Errorf("failed to parse top dict") @@ -527,7 +572,7 @@ func readCFFDict(data []byte) (cffDict, error) { op = 1200 + int(data[i]) i++ } - if len(operands) == 0 { + if len(operands) == 0 && !(op >= 6 && op <= 9 || op == 1212 || op == 1213) { return nil, fmt.Errorf("missing CFF dictionary operands") } dict[op] = operands @@ -640,6 +685,9 @@ func encodeCFFDict(dict cffDict) []byte { sort.Ints(keys) for _, op := range keys { vals := dict[op] + if len(vals) == 0 && (op >= 6 && op <= 9 || op == 1212 || op == 1213) { + continue + } for _, val := range vals { encodeNumberCFF(buf, val) } @@ -656,7 +704,7 @@ func encodeCFFDict(dict cffDict) []byte { // encodeNumberCFF 编码单个数值到CFF格式 // 入参: buf 缓冲区, val 数值 func encodeNumberCFF(buf *bytes.Buffer, val float64) { - if val == math.Trunc(val) { + if val == math.Trunc(val) && val >= math.MinInt32 && val <= math.MaxInt32 { iv := int(val) if iv >= -107 && iv <= 107 { buf.WriteByte(byte(iv + 139)) @@ -676,10 +724,11 @@ func encodeNumberCFF(buf *bytes.Buffer, val float64) { binary.Write(buf, binary.BigEndian, int32(iv)) } } else { - s := fmt.Sprintf("%g", val) + s := strconv.FormatFloat(val, 'g', -1, 64) buf.WriteByte(30) var nibbles []byte - for _, c := range s { + for i := 0; i < len(s); i++ { + c := s[i] var n byte switch c { case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': @@ -688,6 +737,12 @@ func encodeNumberCFF(buf *bytes.Buffer, val float64) { n = 0xA case 'E', 'e': n = 0xB + if i+1 < len(s) && s[i+1] == '-' { + n = 0xC + i++ + } else if i+1 < len(s) && s[i+1] == '+' { + i++ + } case '-': n = 0xE } @@ -756,95 +811,78 @@ func putOffset(buf *bytes.Buffer, val int, size int) { // 入参: data CFF数据, offset 偏移量 // 返回: int 数量, int 索引结构总大小 func getCFFIndexCount(data []byte, offset int) (int, int) { - if offset+2 > len(data) { + count, _, end, err := scanCFFIndex(data, offset) + if err != nil { return 0, 0 } + return count, end - offset +} + +// scanCFFIndex 无分配校验索引偏移、顺序和全部数据范围 +// 入参: data CFF数据, offset 索引偏移 +// 返回: int 项数, int 数据起点, int 索引终点, error 索引错误 +func scanCFFIndex(data []byte, offset int) (int, int, int, error) { + if offset < 0 || offset > len(data)-2 { + return 0, 0, 0, fmt.Errorf("truncated CFF index") + } count := int(binary.BigEndian.Uint16(data[offset:])) if count == 0 { - return 0, 2 + return 0, offset + 2, offset + 2, nil } - if offset+3 > len(data) { - return 0, 0 + if offset > len(data)-3 { + return 0, 0, 0, fmt.Errorf("truncated CFF index header") } - offSize := int(data[offset+2]) - if offSize < 1 || offSize > 4 { - return 0, 0 + width := int(data[offset+2]) + if width < 1 || width > 4 || count+1 > (len(data)-offset-3)/width { + return 0, 0, 0, fmt.Errorf("invalid CFF index offsets") } - dataSizeLen := (count + 1) * offSize - if offset+3+dataSizeLen > len(data) { - return 0, 0 + base := offset + 3 + (count+1)*width + previous := uint64(0) + 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 value < 1 || value-1 > uint64(len(data)-base) || value < previous || index == 0 && value != 1 { + return 0, 0, 0, fmt.Errorf("invalid CFF index range") + } + previous = value } - endOffsetPos := offset + 3 + count*offSize - if endOffsetPos+offSize > len(data) { - return 0, 0 + return count, base, base + int(previous) - 1, nil +} + +// cffDictOffset 读取必须为单个整数且位于字体数据内的字典偏移 +// 入参: data 字体数据, dict 字典, op 偏移操作符 +// 返回: int 偏移值, error 字典或范围错误 +func cffDictOffset(data []byte, dict cffDict, op int) (int, error) { + values := dict[op] + if len(values) != 1 || !finite(values[0]) || values[0] < 0 || values[0] >= float64(len(data)) || values[0] != math.Trunc(values[0]) { + return 0, fmt.Errorf("invalid CFF dictionary offset %d", op) } - dataEnd := readCFFOffset(data, endOffsetPos, offSize) - if dataEnd < 1 { - return 0, 0 - } - return count, 3 + (count+1)*offSize + (dataEnd - 1) + return int(values[0]), nil } // getCFFIndexData 读取CFF索引的数据块 // 入参: data CFF数据, offset 偏移量 // 返回: []byte 索引数据(已去除offsets), int 索引结构总大小 func getCFFIndexData(data []byte, offset int) ([]byte, int) { - count, size := getCFFIndexCount(data, offset) - if count == 0 { - return nil, size + count, base, end, err := scanCFFIndex(data, offset) + if err != nil { + return nil, 0 } - if offset+3 > len(data) { - return nil, size + if count == 0 { + return nil, end - offset } offSize := int(data[offset+2]) - if offset+3+offSize > len(data) { - return nil, size - } - off0 := readCFFOffset(data, offset+3, offSize) - if offset+3+offSize*2 > len(data) { - return nil, size - } off1 := readCFFOffset(data, offset+3+offSize, offSize) - dataStartRel := 3 + (count+1)*offSize - dataStartAbs := offset + dataStartRel - start := dataStartAbs + (off0 - 1) - length := off1 - off0 - if start < 0 || length < 0 || start+length > len(data) { - return nil, size - } - return data[start : start+length], size + return data[base : base+off1-1], end - offset } // readCFFIndexItems 读取CFF索引中的所有数据项 // 入参: data CFF数据, offset 索引偏移 // 返回: [][]byte 数据项列表 func readCFFIndexItems(data []byte, offset int) [][]byte { - count, _ := getCFFIndexCount(data, offset) - if count == 0 || offset+3 > len(data) { - return nil - } - offSize := int(data[offset+2]) - if offSize < 1 || offSize > 4 { - return nil - } - dataStart := offset + 3 + (count+1)*offSize - items := make([][]byte, 0, count) - for i := 0; i < count; i++ { - p1 := offset + 3 + i*offSize - p2 := p1 + offSize - if p2+offSize > len(data) { - return items - } - off1 := readCFFOffset(data, p1, offSize) - off2 := readCFFOffset(data, p2, offSize) - start := dataStart + off1 - 1 - length := off2 - off1 - if start < 0 || length < 0 || start+length > len(data) { - items = append(items, nil) - continue - } - items = append(items, data[start:start+length]) - } + items, _, _ := readType2Index(data, offset) return items } diff --git a/ofdgo_font_ttf.go b/ofdgo_font_ttf.go index 4a026cd..999cc9c 100644 --- a/ofdgo_font_ttf.go +++ b/ofdgo_font_ttf.go @@ -57,7 +57,11 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin if err != nil { return false, data, nil, false, err } - normalized, err := normalizeCFFCharstrings(sanitized) + var units uint16 + if head := existingTables["head"]; len(head) >= 20 { + units = binary.BigEndian.Uint16(head[18:20]) + } + normalized, err := normalizeCFFCharstringsAt(sanitized, units) if err != nil { return false, data, nil, false, err } diff --git a/ofdgo_font_type1.go b/ofdgo_font_type1.go index 49b5903..7fdfee7 100644 --- a/ofdgo_font_type1.go +++ b/ofdgo_font_type1.go @@ -742,8 +742,12 @@ func encodeType2Number(target *bytes.Buffer, value float64) error { } return nil } + fixed := math.Round(value * 65536) + if fixed < math.MinInt32 || fixed > math.MaxInt32 { + return fmt.Errorf("Type2 fixed operand out of range") + } target.WriteByte(255) - _ = binary.Write(target, binary.BigEndian, int32(math.Round(value*65536))) + _ = binary.Write(target, binary.BigEndian, int32(fixed)) return nil } diff --git a/ofdgo_font_type2.go b/ofdgo_font_type2.go index 2a61fe6..572b5c7 100644 --- a/ofdgo_font_type2.go +++ b/ofdgo_font_type2.go @@ -16,7 +16,6 @@ package ofdgo import ( "bytes" - "encoding/binary" "fmt" "math" "slices" @@ -37,6 +36,7 @@ type type2Font struct { type type2State struct { font *type2Font widthTarget *type2Font + matrix *[6]float64 args []float64 transient [32]float64 stored uint32 @@ -60,37 +60,24 @@ type type2State struct { // 入参: data 字体数据, offset 索引偏移 // 返回: [][]byte 索引项, int 索引结束位置, error 错误信息 func readType2Index(data []byte, offset int) ([][]byte, int, error) { - if offset < 0 || offset > len(data)-2 { - return nil, 0, fmt.Errorf("truncated CFF index") - } - count := int(binary.BigEndian.Uint16(data[offset:])) - if count == 0 { - return nil, offset + 2, nil - } - if offset > len(data)-3 { - return nil, 0, fmt.Errorf("truncated CFF index header") + count, base, end, err := scanCFFIndex(data, offset) + if err != nil || count == 0 { + return nil, end, err } width := int(data[offset+2]) - if width < 1 || width > 4 || count+1 > (len(data)-offset-3)/width { - return nil, 0, fmt.Errorf("invalid CFF index offsets") - } - base := offset + 3 + (count+1)*width items := make([][]byte, count) - previous := uint64(0) + 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 value < 1 || value-1 > uint64(len(data)-base) || value < previous || index == 0 && value != 1 { - return nil, 0, fmt.Errorf("invalid CFF index range") - } if index != 0 { - items[index-1] = data[base+int(previous)-1 : base+int(value)-1] + items[index-1] = data[base+previous-1 : base+int(value)-1] } - previous = value + previous = int(value) } - return items, base + int(previous) - 1, nil + return items, end, nil } // readType2Font 读取单FD字体的Type2执行资源,CID字体先经字体规范化 @@ -197,6 +184,13 @@ func readType2Private(data []byte, values []float64, font *type2Font) (cffDict, // 入参: data CFF字体数据 // 返回: [][]byte 字形程序, bool 是否变更, error 错误信息 func normalizeType2Programs(data []byte) ([][]byte, bool, error) { + return normalizeType2ProgramsMatrix(data, 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 @@ -206,7 +200,10 @@ func normalizeType2Programs(data []byte) ([][]byte, bool, error) { var stack [48]float64 for gid, program := range font.chars { if len(program) == 0 { - continue + 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} @@ -214,12 +211,15 @@ func normalizeType2Programs(data []byte) ([][]byte, bool, error) { if err != nil { continue } - if !state.changed { + 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) } @@ -286,7 +286,11 @@ func (s *type2State) takeWidth(explicit bool) error { s.args = s.args[:len(s.args)-1] } if s.output != nil && !s.component && s.widthTarget != nil && s.width != s.widthTarget.def { - return encodeType2Number(s.output, s.width-s.widthTarget.nominal) + width := s.width + if s.matrix != nil { + width *= s.matrix[0] + } + return encodeType2Number(s.output, width-s.widthTarget.nominal) } return nil } @@ -420,14 +424,19 @@ func (s *type2State) run(data []byte, depth int) (bool, error) { if !finite(s.x) || !finite(s.y) { return false, fmt.Errorf("invalid Type2 current point") } - if s.component { + 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.x+s.originX, s.y+s.originY + s.outX, s.outY = s.transformedPoint() s.pathStarted = true s.args = s.args[:0] case 14: @@ -498,7 +507,7 @@ func (s *type2State) appendComponent(code, x, y float64) error { if !ok { return fmt.Errorf("missing Type2 component %s", name) } - child := type2State{font: s.font, 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} + 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} _, err := child.run(s.font.chars[gid], 0) if err != nil { return fmt.Errorf("invalid Type2 component %s: %w", name, err) diff --git a/ofdgo_font_type2_fd.go b/ofdgo_font_type2_fd.go index 9abf352..62d866c 100644 --- a/ofdgo_font_type2_fd.go +++ b/ofdgo_font_type2_fd.go @@ -20,6 +20,7 @@ import ( ) // readType2FDSelect 校验CID字体的逐字形FD归属和范围终点 +// 单FD子集缺少.notdef范围时使用唯一FD,不恢复其他缺失范围 // 入参: data 字体数据, offset FDSelect偏移, count 字形数, fonts FD数量 // 返回: []int FD索引, error 格式或范围错误 func readType2FDSelect(data []byte, offset, count, fonts int) ([]int, error) { @@ -47,7 +48,8 @@ func readType2FDSelect(data []byte, offset, count, fonts int) ([]int, error) { if ranges == 0 || ranges > (len(data)-start-2)/3 { return nil, fmt.Errorf("invalid CFF FDSelect ranges") } - if binary.BigEndian.Uint16(data[start:]) != 0 || int(binary.BigEndian.Uint16(data[start+3*ranges:])) != count { + first := int(binary.BigEndian.Uint16(data[start:])) + if first != 0 && !(first == 1 && fonts == 1) || int(binary.BigEndian.Uint16(data[start+3*ranges:])) != count { return nil, fmt.Errorf("invalid CFF FDSelect endpoints") } for index := 0; index < ranges; index++ { diff --git a/ofdgo_font_type2_matrix.go b/ofdgo_font_type2_matrix.go new file mode 100644 index 0000000..4a81812 --- /dev/null +++ b/ofdgo_font_type2_matrix.go @@ -0,0 +1,211 @@ +// 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" + "math" +) + +// readCFFMatrix 读取有限且可逆的字体矩阵,缺省时使用指定矩阵 +// 入参: dict 字体字典, fallback 缺省矩阵 +// 返回: [6]float64 字体矩阵, error 矩阵错误 +func readCFFMatrix(dict cffDict, fallback [6]float64) ([6]float64, error) { + values, ok := dict[1207] + if !ok { + return fallback, nil + } + if len(values) != 6 { + return [6]float64{}, fmt.Errorf("invalid CFF FontMatrix length") + } + for _, value := range values { + if !finite(value) { + return [6]float64{}, fmt.Errorf("invalid CFF FontMatrix value") + } + } + determinant := values[0]*values[3] - values[1]*values[2] + if !finite(determinant) || determinant == 0 { + return [6]float64{}, fmt.Errorf("singular CFF FontMatrix") + } + return [6]float64(values), nil +} + +// cffUnitsPerEm 保留有效的等比设计单位,其他矩阵使用1000单位封装 +// 入参: matrix 字体矩阵 +// 返回: uint16 OpenType设计单位 +func cffUnitsPerEm(matrix [6]float64) uint16 { + if matrix[0] > 0 && matrix[0] == matrix[3] && matrix[1] == 0 && matrix[2] == 0 { + units := 1 / matrix[0] + if units >= 16 && units <= 16384 && math.Abs(units-math.Round(units)) < 1e-8 { + return uint16(math.Round(units)) + } + } + return 1000 +} + +// transformCFFBounds 按变换后的四角更新字体整体边界 +// 入参: dict 顶层字典, matrix 设计坐标变换 +// 返回: error 边界或变换溢出错误 +func transformCFFBounds(dict cffDict, matrix [6]float64) error { + box := dict[5] + if box == nil { + return nil + } + if len(box) != 4 { + return fmt.Errorf("invalid CFF font bounds") + } + left, bottom, right, top := math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1) + for _, x := range []float64{box[0], box[2]} { + for _, y := range []float64{box[1], box[3]} { + tx, ty := matrix[0]*x+matrix[2]*y+matrix[4], matrix[1]*x+matrix[3]*y+matrix[5] + if !finite(tx) || !finite(ty) { + return fmt.Errorf("invalid CFF transformed bounds") + } + left, bottom, right, top = min(left, tx), min(bottom, ty), max(right, tx), max(top, ty) + } + } + dict[5] = []float64{left, bottom, right, top} + return nil +} + +// transformedPoint 将字形当前位置与组件原点变换到输出坐标 +// 返回: float64 横坐标, float64 纵坐标 +func (s *type2State) transformedPoint() (float64, float64) { + x, y := s.x+s.originX, s.y+s.originY + if s.matrix == nil { + return x, y + } + m := s.matrix + return m[0]*x + m[2]*y + m[4], m[1]*x + m[3]*y + m[5] +} + +// emitTransformedPath 展开紧凑路径指令并保留变换后的三次贝塞尔控制点 +// 入参: op 已校验的路径指令, a 操作数 +// 返回: error 输出编码错误 +func (s *type2State) emitTransformedPath(op int, a []float64) error { + emit := func(op int, values ...float64) error { + m := s.matrix + var transformed [12]float64 + for i := 0; i < len(values); i += 2 { + x, y := values[i], values[i+1] + transformed[i], transformed[i+1] = m[0]*x+m[2]*y, m[1]*x+m[3]*y + } + return s.emit(op, transformed[:len(values)]) + } + switch op { + case 5: + for i := 0; i < len(a); i += 2 { + if err := emit(5, a[i], a[i+1]); err != nil { + return err + } + } + case 6, 7: + for i, value := range a { + x, y := 0.0, 0.0 + if (i%2 == 0) == (op == 6) { + x = value + } else { + y = value + } + if err := emit(5, x, y); err != nil { + return err + } + } + case 8, 24, 25: + start, end := 0, len(a) + if op == 24 { + end -= 2 + } else if op == 25 { + start = len(a) - 6 + } + for i := 0; i < start; i += 2 { + if err := emit(5, a[i], a[i+1]); err != nil { + return err + } + } + for i := start; i < end; i += 6 { + if err := emit(8, a[i:i+6]...); err != nil { + return err + } + } + if end < len(a) { + return emit(5, a[end], a[end+1]) + } + case 26, 27: + first := 0.0 + if len(a)%4 == 1 { + first, a = a[0], a[1:] + } + for i := 0; i < len(a); i += 4 { + var err error + if op == 26 { + err = emit(8, first, a[i], a[i+1], a[i+2], 0, a[i+3]) + } else { + err = emit(8, a[i], first, a[i+1], a[i+2], a[i+3], 0) + } + if err != nil { + return err + } + first = 0 + } + case 30, 31: + horizontal := op == 31 + for i := 0; i+4 <= len(a); i += 4 { + last := 0.0 + if i+5 == len(a) { + last = a[i+4] + } + var err error + if horizontal { + err = emit(8, a[i], 0, a[i+1], a[i+2], last, a[i+3]) + } else { + err = emit(8, 0, a[i], a[i+1], a[i+2], a[i+3], last) + } + if err != nil { + return err + } + horizontal = !horizontal + } + case 1234: + if err := emit(8, a[0], 0, a[1], a[2], a[3], 0); err != nil { + return err + } + return emit(8, a[4], 0, a[5], -a[2], a[6], 0) + case 1235: + if err := emit(8, a[:6]...); err != nil { + return err + } + return emit(8, a[6:12]...) + case 1236: + if err := emit(8, a[0], a[1], a[2], a[3], a[4], 0); err != nil { + return err + } + return emit(8, a[5], 0, a[6], a[7], a[8], -a[1]-a[3]-a[7]) + case 1237: + x, y := 0.0, 0.0 + for i := 0; i < 10; i += 2 { + x, y = x+a[i], y+a[i+1] + } + lastX, lastY := -x, a[10] + if math.Abs(x) > math.Abs(y) { + lastX, lastY = a[10], -y + } + if err := emit(8, a[:6]...); err != nil { + return err + } + return emit(8, a[6], a[7], a[8], a[9], lastX, lastY) + } + return nil +} diff --git a/ofdgo_font_type2_stack.go b/ofdgo_font_type2_stack.go index b2228a1..a4ffe90 100644 --- a/ofdgo_font_type2_stack.go +++ b/ofdgo_font_type2_stack.go @@ -258,11 +258,17 @@ func (s *type2State) draw(op int) error { if !valid || !finite(s.x+dx) || !finite(s.y+dy) { return fmt.Errorf("invalid Type2 path operands for operator %d", op) } - if err := s.emit(op, a); err != nil { + var err error + if s.matrix != nil { + err = s.emitTransformedPath(op, a) + } else { + err = s.emit(op, a) + } + if err != nil { return err } s.x, s.y = s.x+dx, s.y+dy - s.outX, s.outY = s.x+s.originX, s.y+s.originY + s.outX, s.outY = s.transformedPoint() s.args = s.args[:0] return nil } diff --git a/ofdgo_pdf_font.go b/ofdgo_pdf_font.go index 5b9b104..5878db2 100644 --- a/ofdgo_pdf_font.go +++ b/ofdgo_pdf_font.go @@ -171,6 +171,20 @@ func pdfFontProgram(source *pdfgo.Font, type1 *type1Program) ([]byte, uint16, er return nil, 0, fmt.Errorf("embedded PDF font has no glyphs") } changed := false + if cff := tables["CFF "]; len(cff) != 0 { + sanitized, err := sanitizeCFF(cff) + if err != nil { + return nil, 0, err + } + normalized, err := normalizeCFFCharstringsAt(sanitized, binary.BigEndian.Uint16(tables["head"][18:20])) + if err != nil { + return nil, 0, err + } + if !bytes.Equal(normalized, cff) { + tables["CFF "] = normalized + changed = true + } + } var repairedLimit uint16 metrics := int(binary.BigEndian.Uint16(tables["hhea"][34:36])) if metrics == 0 || metrics > int(count) {