feat(字体渲染): 加强字体兼容性

This commit is contained in:
2026-06-25 09:31:42 +08:00
parent 788767e8e6
commit 8edc2a0bb5
9 changed files with 787 additions and 65 deletions
+21 -4
View File
@@ -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 {
if isBareCFFData(data) {
otf, mapping, err := wrapCFFToOTF(data)
if err == nil {
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
View File
@@ -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字符
+79 -10
View File
@@ -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,23 +73,57 @@ 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)
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)
}
ascender := int16(800)
@@ -98,10 +137,15 @@ func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uin
}
var mapping map[rune]uint16
if missingCmap && fixCmap {
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)
}
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
}
// 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 是否可用
+133
View File
@@ -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 {
+18
View File
@@ -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
View File
@@ -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 ""
}
+19
View File
@@ -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"`
@@ -80,6 +90,7 @@ type TextObject struct {
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"`
}
@@ -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"`
+129 -10
View File
@@ -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, &currentCTM)
}
ctx.Pop()
return
}
for _, imgObj := range cgu.ImageObject {
r.renderImage(ctx, imgObj, pageH, &currentCTM)
}
@@ -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,21 +680,29 @@ 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)
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
ctx.SetStrokeWidth(uw)
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,15 +1125,12 @@ 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 {
if rVal, ok := r.fontGlyphRune(fontID, gid); ok {
res = append(res, rVal)
continue
}
}
res = append(res, rune(gid))
}
return res
+1
View File
@@ -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"`