diff --git a/ofdgo_font.go b/ofdgo_font.go index 235fdfe..8e8d861 100644 --- a/ofdgo_font.go +++ b/ofdgo_font.go @@ -34,14 +34,31 @@ func FixFontDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, ma if isTTC { return false, data, nil, false, nil } - if !isTrueType && !isOpenType { - if data[0] == 1 && data[1] == 0 && data[2] == 4 { - otf, mapping, err := wrapCFFToOTF(data) - if err == nil { - return true, otf, mapping, false, nil - } + if isBareCFFData(data) { + otf, mapping, err := wrapCFFToOTF(data) + if err != nil { + return false, data, nil, false, err } + return true, otf, mapping, false, nil + } + if !isTrueType && !isOpenType { + return false, data, nil, false, nil } fixed, newData, mapping, mc, err := fixTrueType(data, fixCmap, fixName) return fixed, newData, mapping, mc, err } + +// isBareCFFData 检查是否为 CFF 裸字体数据 +// 入参: data 字体数据 +// 返回: bool 是否为 CFF 裸数据 +func isBareCFFData(data []byte) bool { + if len(data) < 4 { + return false + } + if data[0] != 1 || data[1] != 0 { + return false + } + hdrSize := int(data[2]) + offSize := int(data[3]) + return 4 <= hdrSize && hdrSize <= len(data) && 1 <= offSize && offSize <= 4 +} diff --git a/ofdgo_font_cff.go b/ofdgo_font_cff.go index 2125540..b515cb3 100644 --- a/ofdgo_font_cff.go +++ b/ofdgo_font_cff.go @@ -18,6 +18,7 @@ import ( "bytes" "encoding/binary" "fmt" + "golang.org/x/text/encoding/simplifiedchinese" "math" "sort" "strconv" @@ -28,16 +29,15 @@ import ( // 入参: cffData CFF字体数据 // 返回: []byte OTF字体数据, map[rune]uint16 字符映射, error 错误信息 func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) { - sanitized, err := sanitizeCFF(cffData) - if err == nil { - cffData = sanitized - } else { - return nil, nil, err - } numGlyphs, err := parseCFFAndCountGlyphs(cffData) if err != nil { return nil, nil, err } + mapping := getCmapFromCFF(cffData, int(numGlyphs)) + sanitized, err := sanitizeCFF(cffData) + if err == nil { + cffData = sanitized + } widths, err := parseCFFWidths(cffData, numGlyphs) if err != nil { widths = make([]uint16, numGlyphs) @@ -64,12 +64,11 @@ func wrapCFFToOTF(cffData []byte) ([]byte, map[rune]uint16, error) { } } } - mapping := getCmapFromCFF(cffData, int(numGlyphs)) tables := make(map[string][]byte) tables["CFF "] = cffData tables["head"] = buildHeadTable(unitsPerEm) tables["hhea"] = buildHheaTable(uint16(numGlyphs)) - tables["maxp"] = buildMaxpTable(uint16(numGlyphs)) + tables["maxp"] = buildCFFMaxpTable(uint16(numGlyphs)) tables["OS/2"] = buildOS2Table() tables["name"] = buildNameTable() tables["post"] = buildPostTable() @@ -747,12 +746,12 @@ func readCFFOffset(data []byte, pos, size int) int { return val } -// getCmapFromCFF 从 CFF 数据中恢复 Unicode 映射 +// getCFFCharsetInfo 读取 CFF 字符集和 ROS 信息 // 入参: data CFF数据, numGlyphs 字形数量 -// 返回: map[rune]uint16 恢复的映射表 -func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 { +// 返回: []int SID或CID列表, string Registry, string Ordering, int 字符串索引偏移, bool 是否成功 +func getCFFCharsetInfo(data []byte, numGlyphs int) ([]int, string, string, int, bool) { if len(data) < 4 { - return nil + return nil, "", "", 0, false } hdrSize := int(data[2]) offset := hdrSize @@ -763,9 +762,10 @@ func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 { offset += szTD stringIndexOff := offset if topDictData == nil { - return nil + return nil, "", "", 0, false } td := parseCFFDict(topDictData) + registry, ordering := getCFFROS(data, stringIndexOff, td) charsetOff := 0 if vals, ok := td[15]; ok && len(vals) > 0 { charsetOff = int(vals[0]) @@ -784,9 +784,42 @@ func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 { sids[i] = i } } else { + return nil, "", "", 0, false + } + return sids, registry, ordering, stringIndexOff, true +} + +// getCmapFromCFF 从 CFF 数据中恢复 Unicode 映射 +// 入参: data CFF数据, numGlyphs 字形数量 +// 返回: map[rune]uint16 恢复的映射表 +func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 { + sids, registry, ordering, stringIndexOff, ok := getCFFCharsetInfo(data, numGlyphs) + if !ok { return nil } mapping := make(map[rune]uint16) + if registry == "Adobe" && ordering == "GB1" { + for gid, cid := range sids { + if gid == 0 { + continue + } + if r, ok := adobeGB1CIDToUnicode(cid); ok { + mapping[r] = uint16(gid) + } else { + mapping[0xE000+rune(gid)] = uint16(gid) + } + } + return mapping + } + if registry != "" { + for gid := range sids { + if gid == 0 { + continue + } + mapping[0xE000+rune(gid)] = uint16(gid) + } + return mapping + } for gid, sid := range sids { if gid == 0 { continue @@ -812,6 +845,79 @@ func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 { return mapping } +// getCFFCIDRuneMap 获取 CID 到包装字体字符的映射 +// 入参: data CFF或OpenType字体数据 +// 返回: map[uint16]rune CID映射 +func getCFFCIDRuneMap(data []byte) map[uint16]rune { + cffData := getCFFData(data) + if cffData == nil { + return nil + } + numGlyphs, err := parseCFFAndCountGlyphs(cffData) + if err != nil { + return nil + } + sids, registry, _, _, ok := getCFFCharsetInfo(cffData, numGlyphs) + if !ok || registry == "" { + return nil + } + mapping := getCmapFromCFF(cffData, numGlyphs) + if len(mapping) == 0 { + return nil + } + gidRunes := make(map[uint16]rune) + for run, gid := range mapping { + gidRunes[gid] = run + } + result := make(map[uint16]rune) + for gid, cid := range sids { + if gid == 0 || cid < 0 || cid > 0xFFFF { + continue + } + if run, ok := gidRunes[uint16(gid)]; ok { + result[uint16(cid)] = run + } + } + if len(result) == 0 { + return nil + } + return result +} + +// getCFFData 获取字体中的 CFF 数据 +// 入参: data 字体数据 +// 返回: []byte CFF数据 +func getCFFData(data []byte) []byte { + if isBareCFFData(data) { + return data + } + if len(data) < 12 { + return nil + } + tag := string(data[0:4]) + u32Tag := binary.BigEndian.Uint32(data[0:4]) + if tag != "OTTO" && tag != "true" && u32Tag != 0x00010000 { + return nil + } + numTables := int(binary.BigEndian.Uint16(data[4:6])) + for i := 0; i < numTables; i++ { + pos := 12 + i*16 + if pos+16 > len(data) { + return nil + } + if string(data[pos:pos+4]) != "CFF " { + continue + } + offset := int(binary.BigEndian.Uint32(data[pos+8 : pos+12])) + length := int(binary.BigEndian.Uint32(data[pos+12 : pos+16])) + if offset < 0 || length < 0 || offset+length > len(data) { + return nil + } + return data[offset : offset+length] + } + return nil +} + // parseCFFCharset 解析 CFF 字符集并返回 SID 列表 // 入参: data CFF数据, offset 偏移量, numGlyphs 字形数量 // 返回: []int SID列表 @@ -884,6 +990,71 @@ func readStringIndexItem(data []byte, offset int, idx int) string { return string(data[start : start+length]) } +// getCFFROS 读取 CID 字体 ROS 信息 +// 入参: data CFF数据, stringIndexOff 字符串索引偏移, td 顶层字典 +// 返回: string Registry, string Ordering +func getCFFROS(data []byte, stringIndexOff int, td cffDict) (string, string) { + vals, ok := td[1230] + if !ok || len(vals) < 2 { + return "", "" + } + registry := getCFFSIDString(data, stringIndexOff, int(vals[0])) + ordering := getCFFSIDString(data, stringIndexOff, int(vals[1])) + return registry, ordering +} + +// getCFFSIDString 读取 CFF SID 字符串 +// 入参: data CFF数据, stringIndexOff 字符串索引偏移, sid 字符串ID +// 返回: string 字符串内容 +func getCFFSIDString(data []byte, stringIndexOff int, sid int) string { + if sid >= 0 && sid < len(cffStandardStrings) { + return cffStandardStrings[sid] + } + if sid > 390 { + return readStringIndexItem(data, stringIndexOff, sid-391) + } + return "" +} + +// adobeGB1CIDToUnicode 将 Adobe-GB1 CID 转为 Unicode +// 入参: cid 字符CID +// 返回: rune Unicode字符, bool 是否成功 +func adobeGB1CIDToUnicode(cid int) (rune, bool) { + switch cid { + case 329: + return '“', true + case 330: + return '”', true + case 821: + return '、', true + case 822: + return '。', true + case 829: + return '《', true + case 830: + return '》', true + } + n := cid + 471 + if n <= 0 { + return 0, false + } + row := (n-1)/94 + 1 + cell := (n-1)%94 + 1 + if row < 16 || row > 87 || cell < 1 || cell > 94 { + return 0, false + } + gbk := []byte{byte(row + 0xA0), byte(cell + 0xA0)} + decoded, err := simplifiedchinese.GBK.NewDecoder().Bytes(gbk) + if err != nil || len(decoded) == 0 { + return 0, false + } + rs := []rune(string(decoded)) + if len(rs) != 1 { + return 0, false + } + return rs[0], true +} + // getUnicodeFromName 根据字形名称获取对应的Unicode字符 // 入参: name 字形名称 // 返回: rune Unicode字符 diff --git a/ofdgo_font_ttf.go b/ofdgo_font_ttf.go index 272dfd5..192f466 100644 --- a/ofdgo_font_ttf.go +++ b/ofdgo_font_ttf.go @@ -25,11 +25,15 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin if len(data) < 12 { return false, data, nil, false, nil } + sfntTag := string(data[0:4]) + isCFFSfnt := sfntTag == "OTTO" numTables := binary.BigEndian.Uint16(data[4:6]) existingTables := make(map[string][]byte) + malformedDirectory := false pos := 12 for i := 0; i < int(numTables); i++ { if len(data) < pos+16 { + malformedDirectory = true break } tag := string(data[pos : pos+4]) @@ -37,9 +41,18 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin length := binary.BigEndian.Uint32(data[pos+12 : pos+16]) if uint32(len(data)) >= offset+length { existingTables[tag] = data[offset : offset+length] + padding := (4 - (length & 3)) & 3 + if offset%4 != 0 || uint32(len(data))-offset-length < padding { + malformedDirectory = true + } + } else { + malformedDirectory = true } pos += 16 } + if existingTables["CFF "] != nil { + isCFFSfnt = true + } missingHead := existingTables["head"] == nil missingMaxp := existingTables["maxp"] == nil missingHhea := existingTables["hhea"] == nil @@ -53,14 +66,6 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin } missingName := existingTables["name"] == nil missingPost := existingTables["post"] == nil - if !missingHead && !missingMaxp && !missingHhea && !missingHmtx && - !missingOS2 && !missingCmap && !missingName && !missingPost { - return false, data, nil, false, nil - } - newTables := make(map[string][]byte) - for k, v := range existingTables { - newTables[k] = v - } var numGlyphs uint16 = 0 if !missingMaxp { maxp := existingTables["maxp"] @@ -68,22 +73,56 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin numGlyphs = binary.BigEndian.Uint16(maxp[4:6]) } } + if numGlyphs == 0 && isCFFSfnt { + if cff := existingTables["CFF "]; cff != nil { + if n, err := parseCFFAndCountGlyphs(cff); err == nil && n > 0 { + if n > 0xFFFF { + numGlyphs = 0xFFFF + } else { + numGlyphs = uint16(n) + } + } + } + } if numGlyphs == 0 { numGlyphs = 255 } + if !missingPost && hasBadPostTable(existingTables["post"], numGlyphs, !isCFFSfnt) { + missingPost = true + } + if !missingHead && !missingMaxp && !missingHhea && !missingHmtx && + !missingOS2 && !missingCmap && !missingName && !missingPost && !malformedDirectory { + return false, data, nil, false, nil + } + newTables := make(map[string][]byte) + for k, v := range existingTables { + newTables[k] = v + } if missingHead { newTables["head"] = buildHeadTable(1000) } if missingMaxp { - newTables["maxp"] = buildMaxpTable(numGlyphs) + if isCFFSfnt { + newTables["maxp"] = buildCFFMaxpTable(numGlyphs) + } else { + newTables["maxp"] = buildTrueTypeMaxpTable(numGlyphs) + } } if missingHhea { newTables["hhea"] = buildHheaTable(numGlyphs) } if missingHmtx { - defWidths := make([]uint16, numGlyphs) - for i := range defWidths { - defWidths[i] = 500 + var defWidths []uint16 + if isCFFSfnt { + if widths, err := parseCFFWidths(newTables["CFF "], int(numGlyphs)); err == nil { + defWidths = widths + } + } + if len(defWidths) == 0 { + defWidths = make([]uint16, numGlyphs) + for i := range defWidths { + defWidths[i] = 500 + } } newTables["hmtx"] = buildHmtxTable(defWidths) } @@ -98,9 +137,14 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin } var mapping map[rune]uint16 if missingCmap && fixCmap { - mapping = make(map[rune]uint16) - for i := uint16(0); i < numGlyphs; i++ { - mapping[rune(i)] = i + if isCFFSfnt { + mapping = getCmapFromCFF(newTables["CFF "], int(numGlyphs)) + } + if len(mapping) == 0 { + mapping = make(map[rune]uint16) + for i := uint16(0); i < numGlyphs; i++ { + mapping[rune(i)] = i + } } newTables["cmap"] = buildCmapTable(numGlyphs, mapping) } @@ -117,6 +161,31 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin return true, finalData, mapping, missingCmap, nil } +// hasBadPostTable 检查 post 表是否会被 canvas/font 拒绝 +func hasBadPostTable(data []byte, numGlyphs uint16, isTrueType bool) bool { + if len(data) < 32 { + return true + } + version := binary.BigEndian.Uint32(data[0:4]) + switch version { + case 0x00010000: + return !isTrueType || len(data) != 32 + case 0x00020000: + if len(data) < 34 { + return true + } + postGlyphs := binary.BigEndian.Uint16(data[32:34]) + if postGlyphs != numGlyphs { + return true + } + return len(data) < 34+int(postGlyphs)*2 + case 0x00030000: + return len(data) != 32 + default: + return true + } +} + // hasUsableCmap 检查是否存在可用的 cmap 子表 // 入参: data cmap表数据 // 返回: bool 是否可用 diff --git a/ofdgo_font_util.go b/ofdgo_font_util.go index e30f275..80dee86 100644 --- a/ofdgo_font_util.go +++ b/ofdgo_font_util.go @@ -125,12 +125,32 @@ func buildHheaTable(numGlyphs uint16) []byte { // 入参: numGlyphs 字形数量 // 返回: []byte maxp表数据 func buildMaxpTable(numGlyphs uint16) []byte { + return buildCFFMaxpTable(numGlyphs) +} + +// buildCFFMaxpTable 构建 CFF 轮廓使用的 maxp 0.5 表 +// 入参: numGlyphs 字形数量 +// 返回: []byte maxp表数据 +func buildCFFMaxpTable(numGlyphs uint16) []byte { buf := new(bytes.Buffer) binary.Write(buf, binary.BigEndian, uint32(0x00005000)) binary.Write(buf, binary.BigEndian, uint16(numGlyphs)) return buf.Bytes() } +// buildTrueTypeMaxpTable 构建 TrueType 轮廓使用的 maxp 1.0 表 +// 入参: numGlyphs 字形数量 +// 返回: []byte maxp表数据 +func buildTrueTypeMaxpTable(numGlyphs uint16) []byte { + buf := new(bytes.Buffer) + binary.Write(buf, binary.BigEndian, uint32(0x00010000)) + binary.Write(buf, binary.BigEndian, uint16(numGlyphs)) + for i := 0; i < 13; i++ { + binary.Write(buf, binary.BigEndian, uint16(0)) + } + return buf.Bytes() +} + // buildOS2Table 构建 OS/2 表 (使用默认 Metrics) // 返回: []byte OS/2表数据 func buildOS2Table() []byte { @@ -236,6 +256,9 @@ type cmapSegment struct { // 入参: numGlyphs 字形数量, mapping 字符映射 // 返回: []byte cmap表数据 func buildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte { + if shouldBuildCmapFormat12(mapping) { + return buildCmapTableFormat12(numGlyphs, mapping) + } var segs []cmapSegment if mapping == nil { end := uint16(0xFFFF) @@ -335,6 +358,116 @@ func buildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte { return mainBuf.Bytes() } +// shouldBuildCmapFormat12 判断 format 4 是否会溢出 16 位 length +func shouldBuildCmapFormat12(mapping map[rune]uint16) bool { + if mapping == nil { + return false + } + var codes []int + for r := range mapping { + if r > 0xFFFF { + return true + } + if r != 0xFFFF { + codes = append(codes, int(r)) + } + } + if len(codes) == 0 { + return false + } + sort.Ints(codes) + segCount := 1 + glyphIDCount := 1 + prev := codes[0] + for i := 1; i < len(codes); i++ { + curr := codes[i] + if curr == prev { + continue + } + if curr != prev+1 { + segCount++ + } + glyphIDCount++ + prev = curr + } + segCount++ // sentinel segment + length := 16 + segCount*8 + glyphIDCount*2 + return length > 0xFFFF +} + +// buildCmapTableFormat12 构建 cmap 表 (Format 12) +// 入参: numGlyphs 字形数量, mapping 字符映射 +// 返回: []byte cmap表数据 +func buildCmapTableFormat12(numGlyphs uint16, mapping map[rune]uint16) []byte { + var codes []int + if mapping == nil { + for i := 0; i < int(numGlyphs); i++ { + codes = append(codes, i) + } + } else { + for r := range mapping { + if r >= 0 { + codes = append(codes, int(r)) + } + } + } + sort.Ints(codes) + type cmapGroup struct { + startChar uint32 + endChar uint32 + startGID uint32 + } + var groups []cmapGroup + for i := 0; i < len(codes); { + r := rune(codes[i]) + gid := uint16(codes[i]) + if mapping != nil { + gid = mapping[r] + } + group := cmapGroup{startChar: uint32(r), endChar: uint32(r), startGID: uint32(gid)} + prevRune := r + prevGID := gid + i++ + for i < len(codes) { + nextRune := rune(codes[i]) + nextGID := uint16(codes[i]) + if mapping != nil { + nextGID = mapping[nextRune] + } + if nextRune != prevRune+1 || nextGID != prevGID+1 { + break + } + group.endChar = uint32(nextRune) + prevRune = nextRune + prevGID = nextGID + i++ + } + groups = append(groups, group) + } + sub := new(bytes.Buffer) + binary.Write(sub, binary.BigEndian, uint16(12)) + binary.Write(sub, binary.BigEndian, uint16(0)) + binary.Write(sub, binary.BigEndian, uint32(16+12*len(groups))) + binary.Write(sub, binary.BigEndian, uint32(0)) + binary.Write(sub, binary.BigEndian, uint32(len(groups))) + for _, group := range groups { + binary.Write(sub, binary.BigEndian, group.startChar) + binary.Write(sub, binary.BigEndian, group.endChar) + binary.Write(sub, binary.BigEndian, group.startGID) + } + mainBuf := new(bytes.Buffer) + binary.Write(mainBuf, binary.BigEndian, uint16(0)) + binary.Write(mainBuf, binary.BigEndian, uint16(2)) + binary.Write(mainBuf, binary.BigEndian, uint16(0)) + binary.Write(mainBuf, binary.BigEndian, uint16(4)) + binary.Write(mainBuf, binary.BigEndian, uint32(20)) + binary.Write(mainBuf, binary.BigEndian, uint16(3)) + binary.Write(mainBuf, binary.BigEndian, uint16(10)) + binary.Write(mainBuf, binary.BigEndian, uint32(20)) + mainBuf.Write(sub.Bytes()) + return mainBuf.Bytes() +} + // otfTableRecord OTF 表记录结构 // 字段: tag 标签, checksum 校验和, offset 偏移, length 长度, data 数据 type otfTableRecord struct { diff --git a/ofdgo_geom.go b/ofdgo_geom.go index 11a16cd..087b483 100644 --- a/ofdgo_geom.go +++ b/ofdgo_geom.go @@ -149,3 +149,21 @@ func parseFloatsWithG(s string) []float64 { } return result } + +// parseInts 解析整数数组 +// 入参: s 字符串 +// 返回: []int 整数数组 +func parseInts(s string) []int { + if s == "" { + return nil + } + s = strings.ReplaceAll(s, ",", " ") + parts := strings.Fields(s) + result := make([]int, 0, len(parts)) + for _, p := range parts { + if v, err := strconv.Atoi(p); err == nil { + result = append(result, v) + } + } + return result +} diff --git a/ofdgo_object.go b/ofdgo_object.go new file mode 100644 index 0000000..8fcaea5 --- /dev/null +++ b/ofdgo_object.go @@ -0,0 +1,175 @@ +// 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 ( + "encoding/xml" + "strconv" +) + +// UnmarshalXML 解析图层并保留对象顺序 +// 入参: d XML解码器, start 起始节点 +// 返回: error 错误信息 +func (l *Layer) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + *l = Layer{} + l.ID = attrValue(start, "ID") + l.DrawParam = attrValue(start, "DrawParam") + for { + tok, err := d.Token() + if err != nil { + return err + } + switch node := tok.(type) { + case xml.StartElement: + if err := l.decodeObject(d, node); err != nil { + return err + } + case xml.EndElement: + if node.Name.Local == start.Name.Local { + return nil + } + } + } +} + +// decodeObject 解析图层子对象 +// 入参: d XML解码器, start 起始节点 +// 返回: error 错误信息 +func (l *Layer) decodeObject(d *xml.Decoder, start xml.StartElement) error { + switch start.Name.Local { + case "TextObject": + var obj TextObject + if err := d.DecodeElement(&obj, &start); err != nil { + return err + } + l.TextObject = append(l.TextObject, obj) + l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj}) + case "PathObject": + var obj PathObject + if err := d.DecodeElement(&obj, &start); err != nil { + return err + } + l.PathObject = append(l.PathObject, obj) + l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj}) + case "ImageObject": + var obj ImageObject + if err := d.DecodeElement(&obj, &start); err != nil { + return err + } + l.ImageObject = append(l.ImageObject, obj) + l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj}) + case "CompositeGraphicUnit", "CompositeObject": + var obj CompositeGraphicUnit + if err := d.DecodeElement(&obj, &start); err != nil { + return err + } + l.CompositeGraphicUnit = append(l.CompositeGraphicUnit, obj) + l.Objects = append(l.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj}) + default: + return d.Skip() + } + return nil +} + +// UnmarshalXML 解析复合图元并保留对象顺序 +// 入参: d XML解码器, start 起始节点 +// 返回: error 错误信息 +func (c *CompositeGraphicUnit) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + *c = CompositeGraphicUnit{} + c.ID = attrValue(start, "ID") + c.BaseLoc = attrValue(start, "BaseLoc") + c.ResourceID = attrValue(start, "ResourceID") + c.Boundary = attrValue(start, "Boundary") + c.CTM = attrValue(start, "CTM") + c.DrawParam = attrValue(start, "DrawParam") + if value := attrValue(start, "Alpha"); value != "" { + if alpha, err := strconv.Atoi(value); err == nil { + c.Alpha = &alpha + } + } + for { + tok, err := d.Token() + if err != nil { + return err + } + switch node := tok.(type) { + case xml.StartElement: + if err := c.decodeObject(d, node); err != nil { + return err + } + case xml.EndElement: + if node.Name.Local == start.Name.Local { + return nil + } + } + } +} + +// decodeObject 解析复合图元子对象 +// 入参: d XML解码器, start 起始节点 +// 返回: error 错误信息 +func (c *CompositeGraphicUnit) decodeObject(d *xml.Decoder, start xml.StartElement) error { + switch start.Name.Local { + case "TextObject": + var obj TextObject + if err := d.DecodeElement(&obj, &start); err != nil { + return err + } + c.TextObject = append(c.TextObject, obj) + c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj}) + case "PathObject": + var obj PathObject + if err := d.DecodeElement(&obj, &start); err != nil { + return err + } + c.PathObject = append(c.PathObject, obj) + c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj}) + case "ImageObject": + var obj ImageObject + if err := d.DecodeElement(&obj, &start); err != nil { + return err + } + c.ImageObject = append(c.ImageObject, obj) + c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj}) + case "CompositeGraphicUnit", "CompositeObject": + var obj CompositeGraphicUnit + if err := d.DecodeElement(&obj, &start); err != nil { + return err + } + c.CompositeGraphicUnit = append(c.CompositeGraphicUnit, obj) + c.Objects = append(c.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj}) + case "Clips": + var clips Clips + if err := d.DecodeElement(&clips, &start); err != nil { + return err + } + c.Clips = &clips + default: + return d.Skip() + } + return nil +} + +// attrValue 获取XML属性值 +// 入参: start 起始节点, name 属性名 +// 返回: string 属性值 +func attrValue(start xml.StartElement, name string) string { + for _, attr := range start.Attr { + if attr.Name.Local == name { + return attr.Value + } + } + return "" +} diff --git a/ofdgo_page.go b/ofdgo_page.go index 8427b7a..f2af44a 100644 --- a/ofdgo_page.go +++ b/ofdgo_page.go @@ -40,12 +40,22 @@ type Content struct { type Layer struct { ID string `xml:"ID,attr"` DrawParam string `xml:"DrawParam,attr"` + Objects []GraphicObject `xml:"-"` TextObject []TextObject `xml:"TextObject"` PathObject []PathObject `xml:"PathObject"` ImageObject []ImageObject `xml:"ImageObject"` CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"` } +// GraphicObject 图形对象 +type GraphicObject struct { + Type string + TextObject TextObject + PathObject PathObject + ImageObject ImageObject + CompositeGraphicUnit CompositeGraphicUnit +} + // Clips 裁剪区域集合 type Clips struct { Clip []Clip `xml:"Clip"` @@ -64,24 +74,25 @@ type ClipArea struct { // TextObject 文本对象 type TextObject struct { - ID string `xml:"ID,attr"` - Boundary string `xml:"Boundary,attr"` - DrawParam string `xml:"DrawParam,attr"` - LineWidth float64 `xml:"LineWidth,attr"` - Font string `xml:"Font,attr"` - Size float64 `xml:"Size,attr"` - Weight int `xml:"Weight,attr"` - Italic bool `xml:"Italic,attr"` - Decoration string `xml:"Decoration,attr"` - HScale float64 `xml:"HScale,attr"` - VScale float64 `xml:"VScale,attr"` - CTM string `xml:"CTM,attr"` - Fill *bool `xml:"Fill,attr"` - Stroke *bool `xml:"Stroke,attr"` - StrokeColor *StrokeColor `xml:"StrokeColor"` - FillColor *FillColor `xml:"FillColor"` - TextCode []TextCode `xml:"TextCode"` - Clips *Clips `xml:"Clips"` + ID string `xml:"ID,attr"` + Boundary string `xml:"Boundary,attr"` + DrawParam string `xml:"DrawParam,attr"` + LineWidth float64 `xml:"LineWidth,attr"` + Font string `xml:"Font,attr"` + Size float64 `xml:"Size,attr"` + Weight int `xml:"Weight,attr"` + Italic bool `xml:"Italic,attr"` + Decoration string `xml:"Decoration,attr"` + HScale float64 `xml:"HScale,attr"` + VScale float64 `xml:"VScale,attr"` + CTM string `xml:"CTM,attr"` + Fill *bool `xml:"Fill,attr"` + Stroke *bool `xml:"Stroke,attr"` + StrokeColor *StrokeColor `xml:"StrokeColor"` + FillColor *FillColor `xml:"FillColor"` + CGTransform []CGTransform `xml:"CGTransform"` + TextCode []TextCode `xml:"TextCode"` + Clips *Clips `xml:"Clips"` } // FillColor 填充颜色 @@ -100,6 +111,14 @@ type TextCode struct { Value string `xml:",chardata"` } +// CGTransform 字符到字形的映射 +type CGTransform struct { + CodePosition int `xml:"CodePosition,attr"` + CodeCount int `xml:"CodeCount,attr"` + GlyphCount int `xml:"GlyphCount,attr"` + Glyphs string `xml:"Glyphs"` +} + // PathObject 路径对象 type PathObject struct { ID string `xml:"ID,attr"` diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index 7bd5cde..60d61d1 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -43,6 +43,7 @@ type Renderer struct { CompositeGraphicUnits map[string]*CompositeGraphicUnit FontMap map[string]*canvas.FontFamily FontGIDMap map[string]map[uint16]rune + FontCIDMap map[string]map[uint16]rune fontDirs []string fontFS []fs.FS } @@ -262,6 +263,12 @@ func (r *Renderer) renderLayer(ctx *canvas.Context, layer Layer, pageH float64, } } } + if len(layer.Objects) > 0 { + for _, obj := range layer.Objects { + r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, parentCTM) + } + return + } for _, textObj := range layer.TextObject { r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, parentCTM) } @@ -303,6 +310,13 @@ func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu Composite } } } + if len(cgu.Objects) > 0 { + for _, obj := range cgu.Objects { + r.renderObject(ctx, obj, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM) + } + ctx.Pop() + return + } for _, imgObj := range cgu.ImageObject { r.renderImage(ctx, imgObj, pageH, ¤tCTM) } @@ -318,6 +332,21 @@ func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu Composite ctx.Pop() } +// renderObject 渲染图形对象 +// 入参: ctx 画布上下文, obj 图形对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM +func (r *Renderer) renderObject(ctx *canvas.Context, obj GraphicObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix) { + switch obj.Type { + case "TextObject": + r.renderText(ctx, obj.TextObject, pageH, defaultFill, defaultStroke, parentCTM) + case "PathObject": + r.renderPath(ctx, obj.PathObject, pageH, defaultFill, defaultStroke, defaultLW, parentCTM) + case "ImageObject": + r.renderImage(ctx, obj.ImageObject, pageH, parentCTM) + case "CompositeGraphicUnit", "CompositeObject": + r.renderCompositeGraphicUnit(ctx, obj.CompositeGraphicUnit, pageH, defaultFill, defaultStroke, defaultLW, parentCTM) + } +} + // getDrawParam 获取绘制参数逻辑 // 入参: id 参数ID, visited 访问记录 // 返回: *DrawParam 绘制参数 @@ -592,11 +621,10 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 if italic { fontStyle |= canvas.FontItalic } - fontID := obj.Font - if fontID == "" && dp != nil && dp.Font != "" { - fontID = dp.Font - } + fontID := r.textObjectFontID(obj) + embeddedFont := false if of, ok := r.Reader.fontCache[fontID]; ok { + embeddedFont = of.FontFile != "" if of.Bold { fontStyle |= canvas.FontBold } @@ -609,6 +637,8 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 return } face := ff.Face(sizePt, fillColor, fontStyle, canvas.FontNormal) + glyphRunes := r.textObjectGlyphRunes(fontID, obj) + codePos := 0 for _, tc := range obj.TextCode { var runes []rune if tc.Index != "" { @@ -627,6 +657,11 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 } for i, run := range runes { str := string(run) + if embeddedFont && tc.Index == "" && glyphRunes != nil { + if mapped, ok := glyphRunes[codePos+i]; ok { + str = string(mapped) + } + } if i < len(xs) { cx = xs[i] } else if i > 0 { @@ -645,10 +680,17 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 } tx, ty := ctm.Transform(cx, cy) canvasX, canvasY := tx+bx, pageH-(ty+by) - text := canvas.NewTextLine(face, str, canvas.Left) + textWidth := face.TextWidth(str) if fillColor != nil { ctx.SetFillColor(fillColor) - ctx.DrawText(canvasX, canvasY, text) + if embeddedFont { + path, width := face.ToPath(str) + textWidth = width + ctx.DrawPath(canvasX, canvasY, path) + } else { + text := canvas.NewTextLine(face, str, canvas.Left) + ctx.DrawText(canvasX, canvasY, text) + } } if strings.Contains(obj.Decoration, "Underline") { uw := sizeMM * 0.05 @@ -656,10 +698,11 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 ctx.SetStrokeColor(fillColor) off := sizeMM * 0.1 ctx.MoveTo(canvasX, canvasY-off) - ctx.LineTo(canvasX+face.TextWidth(str), canvasY-off) + ctx.LineTo(canvasX+textWidth, canvasY-off) ctx.Stroke() } } + codePos += len(runes) } ctx.Pop() } @@ -679,6 +722,12 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { ff := canvas.NewFontFamily(of.FontName) if of.FontFile != "" { if fontData, err := r.Reader.ResData(of.FontFile); err == nil { + if cidMap := getCFFCIDRuneMap(fontData); len(cidMap) > 0 { + if r.FontCIDMap == nil { + r.FontCIDMap = make(map[string]map[uint16]rune) + } + r.FontCIDMap[fontID] = cidMap + } if _, fixedData, mapping, _, err := FixFontDataAggressive(fontData, true, true); err == nil { fontData = fixedData if mapping != nil { @@ -826,6 +875,79 @@ func (r *Renderer) globFontFiles(dir, pattern string) []string { return result } +// textObjectFontID 获取文本对象字体ID +// 入参: text 文本对象 +// 返回: string 字体ID +func (r *Renderer) textObjectFontID(text TextObject) string { + fontID := text.Font + if fontID == "" && text.DrawParam != "" { + if dp := r.getDrawParam(text.DrawParam, nil); dp != nil && dp.Font != "" { + fontID = dp.Font + } + } + return fontID +} + +// textObjectGlyphRunes 获取文本对象的字形映射 +// 入参: fontID 字体ID, text 文本对象 +// 返回: map[int]rune 文本位置到包装字体字符的映射 +func (r *Renderer) textObjectGlyphRunes(fontID string, text TextObject) map[int]rune { + if fontID == "" || len(text.CGTransform) == 0 { + return nil + } + result := make(map[int]rune) + for _, transform := range text.CGTransform { + glyphs := parseInts(transform.Glyphs) + count := len(glyphs) + if transform.GlyphCount > 0 && transform.GlyphCount < count { + count = transform.GlyphCount + } + if transform.CodeCount > 0 && transform.CodeCount < count { + count = transform.CodeCount + } + if count == 0 { + continue + } + if transform.CodeCount > 0 && transform.GlyphCount > 0 && transform.CodeCount != transform.GlyphCount { + continue + } + for i := 0; i < count; i++ { + if mapped, ok := r.fontGlyphRune(fontID, glyphs[i]); ok { + result[transform.CodePosition+i] = mapped + } + } + } + if len(result) == 0 { + return nil + } + return result +} + +// fontGlyphRune 获取字形ID对应的包装字体字符 +// 入参: fontID 字体ID, glyphID 字形ID或CID +// 返回: rune 包装字体字符, bool 是否存在 +func (r *Renderer) fontGlyphRune(fontID string, glyphID int) (rune, bool) { + if glyphID < 0 || glyphID > 0xFFFF { + return 0, false + } + id := uint16(glyphID) + if r.FontCIDMap != nil { + if mapping := r.FontCIDMap[fontID]; mapping != nil { + if mapped, ok := mapping[id]; ok { + return mapped, true + } + } + } + if r.FontGIDMap != nil { + if mapping := r.FontGIDMap[fontID]; mapping != nil { + if mapped, ok := mapping[id]; ok { + return mapped, true + } + } + } + return 0, false +} + // renderStamp 渲染印章 // 入参: ctx 画布上下文, s 印章对象, pageH 页面高度 func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) { @@ -1003,14 +1125,11 @@ func (r *Renderer) parseIndexRunes(indexStr string, fontID string) []rune { gids = append(gids, val) } } - mapping := r.FontGIDMap[fontID] var res []rune for _, gid := range gids { - if mapping != nil { - if rVal, ok := mapping[uint16(gid)]; ok { - res = append(res, rVal) - continue - } + if rVal, ok := r.fontGlyphRune(fontID, gid); ok { + res = append(res, rVal) + continue } res = append(res, rune(gid)) } diff --git a/ofdgo_res.go b/ofdgo_res.go index 19bc48d..81b323b 100644 --- a/ofdgo_res.go +++ b/ofdgo_res.go @@ -97,6 +97,7 @@ type CompositeGraphicUnit struct { CTM string `xml:"CTM,attr"` DrawParam string `xml:"DrawParam,attr"` Alpha *int `xml:"Alpha,attr"` + Objects []GraphicObject `xml:"-"` TextObject []TextObject `xml:"TextObject"` PathObject []PathObject `xml:"PathObject"` ImageObject []ImageObject `xml:"ImageObject"`