mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-08-30 12:12:40 +08:00
feat(字体渲染): 加强字体兼容性
This commit is contained in:
+21
-4
@@ -34,14 +34,31 @@ func FixFontDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, ma
|
|||||||
if isTTC {
|
if isTTC {
|
||||||
return false, data, nil, false, nil
|
return false, data, nil, false, nil
|
||||||
}
|
}
|
||||||
if !isTrueType && !isOpenType {
|
if isBareCFFData(data) {
|
||||||
if data[0] == 1 && data[1] == 0 && data[2] == 4 {
|
|
||||||
otf, mapping, err := wrapCFFToOTF(data)
|
otf, mapping, err := wrapCFFToOTF(data)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
|
return false, data, nil, false, err
|
||||||
|
}
|
||||||
return true, otf, mapping, false, nil
|
return true, otf, mapping, false, nil
|
||||||
}
|
}
|
||||||
}
|
if !isTrueType && !isOpenType {
|
||||||
|
return false, data, nil, false, nil
|
||||||
}
|
}
|
||||||
fixed, newData, mapping, mc, err := fixTrueType(data, fixCmap, fixName)
|
fixed, newData, mapping, mc, err := fixTrueType(data, fixCmap, fixName)
|
||||||
return fixed, newData, mapping, mc, err
|
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"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"golang.org/x/text/encoding/simplifiedchinese"
|
||||||
"math"
|
"math"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -28,16 +29,15 @@ import (
|
|||||||
// 入参: cffData CFF字体数据
|
// 入参: cffData CFF字体数据
|
||||||
// 返回: []byte OTF字体数据, map[rune]uint16 字符映射, error 错误信息
|
// 返回: []byte OTF字体数据, map[rune]uint16 字符映射, error 错误信息
|
||||||
func wrapCFFToOTF(cffData []byte) ([]byte, 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)
|
numGlyphs, err := parseCFFAndCountGlyphs(cffData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
mapping := getCmapFromCFF(cffData, int(numGlyphs))
|
||||||
|
sanitized, err := sanitizeCFF(cffData)
|
||||||
|
if err == nil {
|
||||||
|
cffData = sanitized
|
||||||
|
}
|
||||||
widths, err := parseCFFWidths(cffData, numGlyphs)
|
widths, err := parseCFFWidths(cffData, numGlyphs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
widths = make([]uint16, numGlyphs)
|
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 := make(map[string][]byte)
|
||||||
tables["CFF "] = cffData
|
tables["CFF "] = cffData
|
||||||
tables["head"] = buildHeadTable(unitsPerEm)
|
tables["head"] = buildHeadTable(unitsPerEm)
|
||||||
tables["hhea"] = buildHheaTable(uint16(numGlyphs))
|
tables["hhea"] = buildHheaTable(uint16(numGlyphs))
|
||||||
tables["maxp"] = buildMaxpTable(uint16(numGlyphs))
|
tables["maxp"] = buildCFFMaxpTable(uint16(numGlyphs))
|
||||||
tables["OS/2"] = buildOS2Table()
|
tables["OS/2"] = buildOS2Table()
|
||||||
tables["name"] = buildNameTable()
|
tables["name"] = buildNameTable()
|
||||||
tables["post"] = buildPostTable()
|
tables["post"] = buildPostTable()
|
||||||
@@ -747,12 +746,12 @@ func readCFFOffset(data []byte, pos, size int) int {
|
|||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCmapFromCFF 从 CFF 数据中恢复 Unicode 映射
|
// getCFFCharsetInfo 读取 CFF 字符集和 ROS 信息
|
||||||
// 入参: data CFF数据, numGlyphs 字形数量
|
// 入参: data CFF数据, numGlyphs 字形数量
|
||||||
// 返回: map[rune]uint16 恢复的映射表
|
// 返回: []int SID或CID列表, string Registry, string Ordering, int 字符串索引偏移, bool 是否成功
|
||||||
func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 {
|
func getCFFCharsetInfo(data []byte, numGlyphs int) ([]int, string, string, int, bool) {
|
||||||
if len(data) < 4 {
|
if len(data) < 4 {
|
||||||
return nil
|
return nil, "", "", 0, false
|
||||||
}
|
}
|
||||||
hdrSize := int(data[2])
|
hdrSize := int(data[2])
|
||||||
offset := hdrSize
|
offset := hdrSize
|
||||||
@@ -763,9 +762,10 @@ func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 {
|
|||||||
offset += szTD
|
offset += szTD
|
||||||
stringIndexOff := offset
|
stringIndexOff := offset
|
||||||
if topDictData == nil {
|
if topDictData == nil {
|
||||||
return nil
|
return nil, "", "", 0, false
|
||||||
}
|
}
|
||||||
td := parseCFFDict(topDictData)
|
td := parseCFFDict(topDictData)
|
||||||
|
registry, ordering := getCFFROS(data, stringIndexOff, td)
|
||||||
charsetOff := 0
|
charsetOff := 0
|
||||||
if vals, ok := td[15]; ok && len(vals) > 0 {
|
if vals, ok := td[15]; ok && len(vals) > 0 {
|
||||||
charsetOff = int(vals[0])
|
charsetOff = int(vals[0])
|
||||||
@@ -784,9 +784,42 @@ func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 {
|
|||||||
sids[i] = i
|
sids[i] = i
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
return nil
|
||||||
}
|
}
|
||||||
mapping := make(map[rune]uint16)
|
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 {
|
for gid, sid := range sids {
|
||||||
if gid == 0 {
|
if gid == 0 {
|
||||||
continue
|
continue
|
||||||
@@ -812,6 +845,79 @@ func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 {
|
|||||||
return mapping
|
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 列表
|
// parseCFFCharset 解析 CFF 字符集并返回 SID 列表
|
||||||
// 入参: data CFF数据, offset 偏移量, numGlyphs 字形数量
|
// 入参: data CFF数据, offset 偏移量, numGlyphs 字形数量
|
||||||
// 返回: []int SID列表
|
// 返回: []int SID列表
|
||||||
@@ -884,6 +990,71 @@ func readStringIndexItem(data []byte, offset int, idx int) string {
|
|||||||
return string(data[start : start+length])
|
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字符
|
// getUnicodeFromName 根据字形名称获取对应的Unicode字符
|
||||||
// 入参: name 字形名称
|
// 入参: name 字形名称
|
||||||
// 返回: rune Unicode字符
|
// 返回: rune Unicode字符
|
||||||
|
|||||||
+79
-10
@@ -25,11 +25,15 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin
|
|||||||
if len(data) < 12 {
|
if len(data) < 12 {
|
||||||
return false, data, nil, false, nil
|
return false, data, nil, false, nil
|
||||||
}
|
}
|
||||||
|
sfntTag := string(data[0:4])
|
||||||
|
isCFFSfnt := sfntTag == "OTTO"
|
||||||
numTables := binary.BigEndian.Uint16(data[4:6])
|
numTables := binary.BigEndian.Uint16(data[4:6])
|
||||||
existingTables := make(map[string][]byte)
|
existingTables := make(map[string][]byte)
|
||||||
|
malformedDirectory := false
|
||||||
pos := 12
|
pos := 12
|
||||||
for i := 0; i < int(numTables); i++ {
|
for i := 0; i < int(numTables); i++ {
|
||||||
if len(data) < pos+16 {
|
if len(data) < pos+16 {
|
||||||
|
malformedDirectory = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
tag := string(data[pos : pos+4])
|
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])
|
length := binary.BigEndian.Uint32(data[pos+12 : pos+16])
|
||||||
if uint32(len(data)) >= offset+length {
|
if uint32(len(data)) >= offset+length {
|
||||||
existingTables[tag] = data[offset : 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
|
pos += 16
|
||||||
}
|
}
|
||||||
|
if existingTables["CFF "] != nil {
|
||||||
|
isCFFSfnt = true
|
||||||
|
}
|
||||||
missingHead := existingTables["head"] == nil
|
missingHead := existingTables["head"] == nil
|
||||||
missingMaxp := existingTables["maxp"] == nil
|
missingMaxp := existingTables["maxp"] == nil
|
||||||
missingHhea := existingTables["hhea"] == 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
|
missingName := existingTables["name"] == nil
|
||||||
missingPost := existingTables["post"] == 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
|
var numGlyphs uint16 = 0
|
||||||
if !missingMaxp {
|
if !missingMaxp {
|
||||||
maxp := existingTables["maxp"]
|
maxp := existingTables["maxp"]
|
||||||
@@ -68,23 +73,57 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin
|
|||||||
numGlyphs = binary.BigEndian.Uint16(maxp[4:6])
|
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 {
|
if numGlyphs == 0 {
|
||||||
numGlyphs = 255
|
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 {
|
if missingHead {
|
||||||
newTables["head"] = buildHeadTable(1000)
|
newTables["head"] = buildHeadTable(1000)
|
||||||
}
|
}
|
||||||
if missingMaxp {
|
if missingMaxp {
|
||||||
newTables["maxp"] = buildMaxpTable(numGlyphs)
|
if isCFFSfnt {
|
||||||
|
newTables["maxp"] = buildCFFMaxpTable(numGlyphs)
|
||||||
|
} else {
|
||||||
|
newTables["maxp"] = buildTrueTypeMaxpTable(numGlyphs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if missingHhea {
|
if missingHhea {
|
||||||
newTables["hhea"] = buildHheaTable(numGlyphs)
|
newTables["hhea"] = buildHheaTable(numGlyphs)
|
||||||
}
|
}
|
||||||
if missingHmtx {
|
if missingHmtx {
|
||||||
defWidths := make([]uint16, numGlyphs)
|
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 {
|
for i := range defWidths {
|
||||||
defWidths[i] = 500
|
defWidths[i] = 500
|
||||||
}
|
}
|
||||||
|
}
|
||||||
newTables["hmtx"] = buildHmtxTable(defWidths)
|
newTables["hmtx"] = buildHmtxTable(defWidths)
|
||||||
}
|
}
|
||||||
ascender := int16(800)
|
ascender := int16(800)
|
||||||
@@ -98,10 +137,15 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin
|
|||||||
}
|
}
|
||||||
var mapping map[rune]uint16
|
var mapping map[rune]uint16
|
||||||
if missingCmap && fixCmap {
|
if missingCmap && fixCmap {
|
||||||
|
if isCFFSfnt {
|
||||||
|
mapping = getCmapFromCFF(newTables["CFF "], int(numGlyphs))
|
||||||
|
}
|
||||||
|
if len(mapping) == 0 {
|
||||||
mapping = make(map[rune]uint16)
|
mapping = make(map[rune]uint16)
|
||||||
for i := uint16(0); i < numGlyphs; i++ {
|
for i := uint16(0); i < numGlyphs; i++ {
|
||||||
mapping[rune(i)] = i
|
mapping[rune(i)] = i
|
||||||
}
|
}
|
||||||
|
}
|
||||||
newTables["cmap"] = buildCmapTable(numGlyphs, mapping)
|
newTables["cmap"] = buildCmapTable(numGlyphs, mapping)
|
||||||
}
|
}
|
||||||
if missingName && fixName {
|
if missingName && fixName {
|
||||||
@@ -117,6 +161,31 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin
|
|||||||
return true, finalData, mapping, missingCmap, nil
|
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 子表
|
// hasUsableCmap 检查是否存在可用的 cmap 子表
|
||||||
// 入参: data cmap表数据
|
// 入参: data cmap表数据
|
||||||
// 返回: bool 是否可用
|
// 返回: bool 是否可用
|
||||||
|
|||||||
@@ -125,12 +125,32 @@ func buildHheaTable(numGlyphs uint16) []byte {
|
|||||||
// 入参: numGlyphs 字形数量
|
// 入参: numGlyphs 字形数量
|
||||||
// 返回: []byte maxp表数据
|
// 返回: []byte maxp表数据
|
||||||
func buildMaxpTable(numGlyphs uint16) []byte {
|
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)
|
buf := new(bytes.Buffer)
|
||||||
binary.Write(buf, binary.BigEndian, uint32(0x00005000))
|
binary.Write(buf, binary.BigEndian, uint32(0x00005000))
|
||||||
binary.Write(buf, binary.BigEndian, uint16(numGlyphs))
|
binary.Write(buf, binary.BigEndian, uint16(numGlyphs))
|
||||||
return buf.Bytes()
|
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)
|
// buildOS2Table 构建 OS/2 表 (使用默认 Metrics)
|
||||||
// 返回: []byte OS/2表数据
|
// 返回: []byte OS/2表数据
|
||||||
func buildOS2Table() []byte {
|
func buildOS2Table() []byte {
|
||||||
@@ -236,6 +256,9 @@ type cmapSegment struct {
|
|||||||
// 入参: numGlyphs 字形数量, mapping 字符映射
|
// 入参: numGlyphs 字形数量, mapping 字符映射
|
||||||
// 返回: []byte cmap表数据
|
// 返回: []byte cmap表数据
|
||||||
func buildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte {
|
func buildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte {
|
||||||
|
if shouldBuildCmapFormat12(mapping) {
|
||||||
|
return buildCmapTableFormat12(numGlyphs, mapping)
|
||||||
|
}
|
||||||
var segs []cmapSegment
|
var segs []cmapSegment
|
||||||
if mapping == nil {
|
if mapping == nil {
|
||||||
end := uint16(0xFFFF)
|
end := uint16(0xFFFF)
|
||||||
@@ -335,6 +358,116 @@ func buildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte {
|
|||||||
return mainBuf.Bytes()
|
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 表记录结构
|
// otfTableRecord OTF 表记录结构
|
||||||
// 字段: tag 标签, checksum 校验和, offset 偏移, length 长度, data 数据
|
// 字段: tag 标签, checksum 校验和, offset 偏移, length 长度, data 数据
|
||||||
type otfTableRecord struct {
|
type otfTableRecord struct {
|
||||||
|
|||||||
@@ -149,3 +149,21 @@ func parseFloatsWithG(s string) []float64 {
|
|||||||
}
|
}
|
||||||
return result
|
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 ""
|
||||||
|
}
|
||||||
@@ -40,12 +40,22 @@ type Content struct {
|
|||||||
type Layer struct {
|
type Layer struct {
|
||||||
ID string `xml:"ID,attr"`
|
ID string `xml:"ID,attr"`
|
||||||
DrawParam string `xml:"DrawParam,attr"`
|
DrawParam string `xml:"DrawParam,attr"`
|
||||||
|
Objects []GraphicObject `xml:"-"`
|
||||||
TextObject []TextObject `xml:"TextObject"`
|
TextObject []TextObject `xml:"TextObject"`
|
||||||
PathObject []PathObject `xml:"PathObject"`
|
PathObject []PathObject `xml:"PathObject"`
|
||||||
ImageObject []ImageObject `xml:"ImageObject"`
|
ImageObject []ImageObject `xml:"ImageObject"`
|
||||||
CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"`
|
CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphicObject 图形对象
|
||||||
|
type GraphicObject struct {
|
||||||
|
Type string
|
||||||
|
TextObject TextObject
|
||||||
|
PathObject PathObject
|
||||||
|
ImageObject ImageObject
|
||||||
|
CompositeGraphicUnit CompositeGraphicUnit
|
||||||
|
}
|
||||||
|
|
||||||
// Clips 裁剪区域集合
|
// Clips 裁剪区域集合
|
||||||
type Clips struct {
|
type Clips struct {
|
||||||
Clip []Clip `xml:"Clip"`
|
Clip []Clip `xml:"Clip"`
|
||||||
@@ -80,6 +90,7 @@ type TextObject struct {
|
|||||||
Stroke *bool `xml:"Stroke,attr"`
|
Stroke *bool `xml:"Stroke,attr"`
|
||||||
StrokeColor *StrokeColor `xml:"StrokeColor"`
|
StrokeColor *StrokeColor `xml:"StrokeColor"`
|
||||||
FillColor *FillColor `xml:"FillColor"`
|
FillColor *FillColor `xml:"FillColor"`
|
||||||
|
CGTransform []CGTransform `xml:"CGTransform"`
|
||||||
TextCode []TextCode `xml:"TextCode"`
|
TextCode []TextCode `xml:"TextCode"`
|
||||||
Clips *Clips `xml:"Clips"`
|
Clips *Clips `xml:"Clips"`
|
||||||
}
|
}
|
||||||
@@ -100,6 +111,14 @@ type TextCode struct {
|
|||||||
Value string `xml:",chardata"`
|
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 路径对象
|
// PathObject 路径对象
|
||||||
type PathObject struct {
|
type PathObject struct {
|
||||||
ID string `xml:"ID,attr"`
|
ID string `xml:"ID,attr"`
|
||||||
|
|||||||
+129
-10
@@ -43,6 +43,7 @@ type Renderer struct {
|
|||||||
CompositeGraphicUnits map[string]*CompositeGraphicUnit
|
CompositeGraphicUnits map[string]*CompositeGraphicUnit
|
||||||
FontMap map[string]*canvas.FontFamily
|
FontMap map[string]*canvas.FontFamily
|
||||||
FontGIDMap map[string]map[uint16]rune
|
FontGIDMap map[string]map[uint16]rune
|
||||||
|
FontCIDMap map[string]map[uint16]rune
|
||||||
fontDirs []string
|
fontDirs []string
|
||||||
fontFS []fs.FS
|
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 {
|
for _, textObj := range layer.TextObject {
|
||||||
r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, parentCTM)
|
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 {
|
for _, imgObj := range cgu.ImageObject {
|
||||||
r.renderImage(ctx, imgObj, pageH, ¤tCTM)
|
r.renderImage(ctx, imgObj, pageH, ¤tCTM)
|
||||||
}
|
}
|
||||||
@@ -318,6 +332,21 @@ func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu Composite
|
|||||||
ctx.Pop()
|
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 获取绘制参数逻辑
|
// getDrawParam 获取绘制参数逻辑
|
||||||
// 入参: id 参数ID, visited 访问记录
|
// 入参: id 参数ID, visited 访问记录
|
||||||
// 返回: *DrawParam 绘制参数
|
// 返回: *DrawParam 绘制参数
|
||||||
@@ -592,11 +621,10 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
if italic {
|
if italic {
|
||||||
fontStyle |= canvas.FontItalic
|
fontStyle |= canvas.FontItalic
|
||||||
}
|
}
|
||||||
fontID := obj.Font
|
fontID := r.textObjectFontID(obj)
|
||||||
if fontID == "" && dp != nil && dp.Font != "" {
|
embeddedFont := false
|
||||||
fontID = dp.Font
|
|
||||||
}
|
|
||||||
if of, ok := r.Reader.fontCache[fontID]; ok {
|
if of, ok := r.Reader.fontCache[fontID]; ok {
|
||||||
|
embeddedFont = of.FontFile != ""
|
||||||
if of.Bold {
|
if of.Bold {
|
||||||
fontStyle |= canvas.FontBold
|
fontStyle |= canvas.FontBold
|
||||||
}
|
}
|
||||||
@@ -609,6 +637,8 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
face := ff.Face(sizePt, fillColor, fontStyle, canvas.FontNormal)
|
face := ff.Face(sizePt, fillColor, fontStyle, canvas.FontNormal)
|
||||||
|
glyphRunes := r.textObjectGlyphRunes(fontID, obj)
|
||||||
|
codePos := 0
|
||||||
for _, tc := range obj.TextCode {
|
for _, tc := range obj.TextCode {
|
||||||
var runes []rune
|
var runes []rune
|
||||||
if tc.Index != "" {
|
if tc.Index != "" {
|
||||||
@@ -627,6 +657,11 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
}
|
}
|
||||||
for i, run := range runes {
|
for i, run := range runes {
|
||||||
str := string(run)
|
str := string(run)
|
||||||
|
if embeddedFont && tc.Index == "" && glyphRunes != nil {
|
||||||
|
if mapped, ok := glyphRunes[codePos+i]; ok {
|
||||||
|
str = string(mapped)
|
||||||
|
}
|
||||||
|
}
|
||||||
if i < len(xs) {
|
if i < len(xs) {
|
||||||
cx = xs[i]
|
cx = xs[i]
|
||||||
} else if i > 0 {
|
} else if i > 0 {
|
||||||
@@ -645,21 +680,29 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
}
|
}
|
||||||
tx, ty := ctm.Transform(cx, cy)
|
tx, ty := ctm.Transform(cx, cy)
|
||||||
canvasX, canvasY := tx+bx, pageH-(ty+by)
|
canvasX, canvasY := tx+bx, pageH-(ty+by)
|
||||||
text := canvas.NewTextLine(face, str, canvas.Left)
|
textWidth := face.TextWidth(str)
|
||||||
if fillColor != nil {
|
if fillColor != nil {
|
||||||
ctx.SetFillColor(fillColor)
|
ctx.SetFillColor(fillColor)
|
||||||
|
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)
|
ctx.DrawText(canvasX, canvasY, text)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if strings.Contains(obj.Decoration, "Underline") {
|
if strings.Contains(obj.Decoration, "Underline") {
|
||||||
uw := sizeMM * 0.05
|
uw := sizeMM * 0.05
|
||||||
ctx.SetStrokeWidth(uw)
|
ctx.SetStrokeWidth(uw)
|
||||||
ctx.SetStrokeColor(fillColor)
|
ctx.SetStrokeColor(fillColor)
|
||||||
off := sizeMM * 0.1
|
off := sizeMM * 0.1
|
||||||
ctx.MoveTo(canvasX, canvasY-off)
|
ctx.MoveTo(canvasX, canvasY-off)
|
||||||
ctx.LineTo(canvasX+face.TextWidth(str), canvasY-off)
|
ctx.LineTo(canvasX+textWidth, canvasY-off)
|
||||||
ctx.Stroke()
|
ctx.Stroke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
codePos += len(runes)
|
||||||
}
|
}
|
||||||
ctx.Pop()
|
ctx.Pop()
|
||||||
}
|
}
|
||||||
@@ -679,6 +722,12 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
|
|||||||
ff := canvas.NewFontFamily(of.FontName)
|
ff := canvas.NewFontFamily(of.FontName)
|
||||||
if of.FontFile != "" {
|
if of.FontFile != "" {
|
||||||
if fontData, err := r.Reader.ResData(of.FontFile); err == nil {
|
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 {
|
if _, fixedData, mapping, _, err := FixFontDataAggressive(fontData, true, true); err == nil {
|
||||||
fontData = fixedData
|
fontData = fixedData
|
||||||
if mapping != nil {
|
if mapping != nil {
|
||||||
@@ -826,6 +875,79 @@ func (r *Renderer) globFontFiles(dir, pattern string) []string {
|
|||||||
return result
|
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 渲染印章
|
// renderStamp 渲染印章
|
||||||
// 入参: ctx 画布上下文, s 印章对象, pageH 页面高度
|
// 入参: ctx 画布上下文, s 印章对象, pageH 页面高度
|
||||||
func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) {
|
func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) {
|
||||||
@@ -1003,15 +1125,12 @@ func (r *Renderer) parseIndexRunes(indexStr string, fontID string) []rune {
|
|||||||
gids = append(gids, val)
|
gids = append(gids, val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mapping := r.FontGIDMap[fontID]
|
|
||||||
var res []rune
|
var res []rune
|
||||||
for _, gid := range gids {
|
for _, gid := range gids {
|
||||||
if mapping != nil {
|
if rVal, ok := r.fontGlyphRune(fontID, gid); ok {
|
||||||
if rVal, ok := mapping[uint16(gid)]; ok {
|
|
||||||
res = append(res, rVal)
|
res = append(res, rVal)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
|
||||||
res = append(res, rune(gid))
|
res = append(res, rune(gid))
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ type CompositeGraphicUnit struct {
|
|||||||
CTM string `xml:"CTM,attr"`
|
CTM string `xml:"CTM,attr"`
|
||||||
DrawParam string `xml:"DrawParam,attr"`
|
DrawParam string `xml:"DrawParam,attr"`
|
||||||
Alpha *int `xml:"Alpha,attr"`
|
Alpha *int `xml:"Alpha,attr"`
|
||||||
|
Objects []GraphicObject `xml:"-"`
|
||||||
TextObject []TextObject `xml:"TextObject"`
|
TextObject []TextObject `xml:"TextObject"`
|
||||||
PathObject []PathObject `xml:"PathObject"`
|
PathObject []PathObject `xml:"PathObject"`
|
||||||
ImageObject []ImageObject `xml:"ImageObject"`
|
ImageObject []ImageObject `xml:"ImageObject"`
|
||||||
|
|||||||
Reference in New Issue
Block a user