mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-08-30 12:12:40 +08:00
feat(字体渲染): 加强字体兼容性
This commit is contained in:
+23
-6
@@ -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
|
||||
}
|
||||
|
||||
+184
-13
@@ -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字符
|
||||
|
||||
+84
-15
@@ -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 是否可用
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+175
@@ -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 ""
|
||||
}
|
||||
+37
-18
@@ -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"`
|
||||
|
||||
+132
-13
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user