mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-08-30 12:12:40 +08:00
feat(强化兼容): 改进字体渲染
This commit is contained in:
@@ -65,10 +65,11 @@ func NewReader(r io.ReaderAt, size int64) (*Reader, error) {
|
|||||||
// 返回: *Renderer 渲染器实例
|
// 返回: *Renderer 渲染器实例
|
||||||
func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
|
func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer {
|
||||||
r := &Renderer{
|
r := &Renderer{
|
||||||
Reader: reader,
|
Reader: reader,
|
||||||
DPI: 300.0,
|
DPI: 300.0,
|
||||||
DrawParams: reader.drawParamCache,
|
DrawParams: reader.drawParamCache,
|
||||||
FontMap: make(map[string]*canvas.FontFamily),
|
CompositeGraphicUnits: reader.compositeGraphicUnitCache,
|
||||||
|
FontMap: make(map[string]*canvas.FontFamily),
|
||||||
}
|
}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(r)
|
opt(r)
|
||||||
|
|||||||
+21
-197
@@ -15,209 +15,33 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// FixFontData 修复字体数据
|
// FixFontDataAggressive 激进修复字体数据
|
||||||
// 本函数主要解决嵌入字体缺失 OS/2 表导致部分渲染库(如 tdewolff/font)Panic 的问题
|
// 尝试修复缺失表(OS/2, cmap等)的TrueType字体或包装CFF裸数据
|
||||||
// 通过解析 hhea 表获取 Ascender/Descender 等度量信息,构造并注入一个标准的 OS/2 表
|
// 入参: data 原始字体数据, fixCmap 是否修复cmap, fixName 是否修复name
|
||||||
// 入参: data 原始字体文件数据
|
// 返回: bool 是否修复, []byte 修复后数据, map[rune]uint16 字符映射, bool 是否缺失cmap, error 错误信息
|
||||||
// 返回: bool 是否进行了修补, []byte 修复后的字体数据, error 错误信息
|
func FixFontDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uint16, bool, error) {
|
||||||
func FixFontData(data []byte) (bool, []byte, error) {
|
if len(data) < 4 {
|
||||||
if len(data) < 12 {
|
return false, data, nil, false, nil
|
||||||
return false, data, nil
|
|
||||||
}
|
}
|
||||||
numTables := binary.BigEndian.Uint16(data[4:6])
|
tag := string(data[0:4])
|
||||||
hasOS2 := false
|
u32Tag := binary.BigEndian.Uint32(data[0:4])
|
||||||
type TableRecord struct {
|
isTrueType := u32Tag == 0x00010000 || tag == "true"
|
||||||
Tag string
|
isOpenType := tag == "OTTO"
|
||||||
CheckSum uint32
|
isTTC := tag == "ttcf"
|
||||||
Offset uint32
|
if isTTC {
|
||||||
Length uint32
|
return false, data, nil, false, nil
|
||||||
}
|
}
|
||||||
tables := make([]TableRecord, 0, numTables)
|
if !isTrueType && !isOpenType {
|
||||||
var hhea TableRecord
|
if data[0] == 1 && data[1] == 0 && data[2] == 4 {
|
||||||
hasHhea := false
|
otf, mapping, err := wrapCFFToOTF(data)
|
||||||
pos := 12
|
if err == nil {
|
||||||
for i := 0; i < int(numTables); i++ {
|
return true, otf, mapping, false, nil
|
||||||
if len(data) < pos+16 {
|
|
||||||
return false, data, nil
|
|
||||||
}
|
|
||||||
tag := string(data[pos : pos+4])
|
|
||||||
checkSum := binary.BigEndian.Uint32(data[pos+4 : pos+8])
|
|
||||||
offset := binary.BigEndian.Uint32(data[pos+8 : pos+12])
|
|
||||||
length := binary.BigEndian.Uint32(data[pos+12 : pos+16])
|
|
||||||
tables = append(tables, TableRecord{tag, checkSum, offset, length})
|
|
||||||
if tag == "OS/2" {
|
|
||||||
hasOS2 = true
|
|
||||||
}
|
|
||||||
if tag == "hhea" {
|
|
||||||
hhea = TableRecord{tag, checkSum, offset, length}
|
|
||||||
hasHhea = true
|
|
||||||
}
|
|
||||||
pos += 16
|
|
||||||
}
|
|
||||||
if hasOS2 {
|
|
||||||
return false, data, nil
|
|
||||||
}
|
|
||||||
if !hasHhea {
|
|
||||||
return false, data, nil
|
|
||||||
}
|
|
||||||
if uint32(len(data)) < hhea.Offset+hhea.Length {
|
|
||||||
return false, data, nil
|
|
||||||
}
|
|
||||||
hheaData := data[hhea.Offset : hhea.Offset+hhea.Length]
|
|
||||||
if len(hheaData) < 10 {
|
|
||||||
return false, data, nil
|
|
||||||
}
|
|
||||||
ascender := int16(binary.BigEndian.Uint16(hheaData[4:6]))
|
|
||||||
descender := int16(binary.BigEndian.Uint16(hheaData[6:8]))
|
|
||||||
os2 := new(bytes.Buffer)
|
|
||||||
binary.Write(os2, binary.BigEndian, uint16(3))
|
|
||||||
binary.Write(os2, binary.BigEndian, int16(500))
|
|
||||||
binary.Write(os2, binary.BigEndian, uint16(400))
|
|
||||||
binary.Write(os2, binary.BigEndian, uint16(5))
|
|
||||||
binary.Write(os2, binary.BigEndian, uint16(0))
|
|
||||||
for i := 0; i < 11; i++ {
|
|
||||||
binary.Write(os2, binary.BigEndian, int16(0))
|
|
||||||
}
|
|
||||||
os2.Write(make([]byte, 10))
|
|
||||||
binary.Write(os2, binary.BigEndian, uint32(0))
|
|
||||||
binary.Write(os2, binary.BigEndian, uint32(0))
|
|
||||||
binary.Write(os2, binary.BigEndian, uint32(0))
|
|
||||||
binary.Write(os2, binary.BigEndian, uint32(0))
|
|
||||||
os2.WriteString("PfEd")
|
|
||||||
binary.Write(os2, binary.BigEndian, uint16(0x0040))
|
|
||||||
binary.Write(os2, binary.BigEndian, uint16(0x0020))
|
|
||||||
binary.Write(os2, binary.BigEndian, uint16(0xFFFD))
|
|
||||||
binary.Write(os2, binary.BigEndian, ascender)
|
|
||||||
binary.Write(os2, binary.BigEndian, descender)
|
|
||||||
binary.Write(os2, binary.BigEndian, int16(0))
|
|
||||||
binary.Write(os2, binary.BigEndian, uint16(ascender))
|
|
||||||
if descender < 0 {
|
|
||||||
binary.Write(os2, binary.BigEndian, uint16(-descender))
|
|
||||||
} else {
|
|
||||||
binary.Write(os2, binary.BigEndian, uint16(descender))
|
|
||||||
}
|
|
||||||
binary.Write(os2, binary.BigEndian, uint32(1))
|
|
||||||
binary.Write(os2, binary.BigEndian, uint32(0))
|
|
||||||
binary.Write(os2, binary.BigEndian, int16(0))
|
|
||||||
binary.Write(os2, binary.BigEndian, int16(0))
|
|
||||||
binary.Write(os2, binary.BigEndian, uint16(0))
|
|
||||||
binary.Write(os2, binary.BigEndian, uint16(0x0020))
|
|
||||||
binary.Write(os2, binary.BigEndian, uint16(0))
|
|
||||||
os2Data := os2.Bytes()
|
|
||||||
os2CheckSum := calcTableChecksum(os2Data)
|
|
||||||
newNumTables := numTables + 1
|
|
||||||
entrySelector := 0
|
|
||||||
for 1<<(entrySelector+1) <= int(newNumTables) {
|
|
||||||
entrySelector++
|
|
||||||
}
|
|
||||||
searchRange := 1 << (entrySelector + 4)
|
|
||||||
rangeShift := int(newNumTables)*16 - searchRange
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
buf.Write(data[0:4])
|
|
||||||
binary.Write(buf, binary.BigEndian, uint16(newNumTables))
|
|
||||||
binary.Write(buf, binary.BigEndian, uint16(searchRange))
|
|
||||||
binary.Write(buf, binary.BigEndian, uint16(entrySelector))
|
|
||||||
binary.Write(buf, binary.BigEndian, uint16(rangeShift))
|
|
||||||
newTables := append(tables, TableRecord{
|
|
||||||
Tag: "OS/2",
|
|
||||||
CheckSum: os2CheckSum,
|
|
||||||
Offset: 0,
|
|
||||||
Length: uint32(len(os2Data)),
|
|
||||||
})
|
|
||||||
for i := 0; i < len(newTables); i++ {
|
|
||||||
for j := i + 1; j < len(newTables); j++ {
|
|
||||||
if newTables[i].Tag > newTables[j].Tag {
|
|
||||||
newTables[i], newTables[j] = newTables[j], newTables[i]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
headerSize := 12 + 16*uint32(len(newTables))
|
fixed, newData, mapping, mc, err := fixTrueType(data, fixCmap, fixName)
|
||||||
currentOffset := headerSize
|
return fixed, newData, mapping, mc, err
|
||||||
for i := range newTables {
|
|
||||||
t := &newTables[i]
|
|
||||||
t.Offset = currentOffset
|
|
||||||
currentOffset += align4(t.Length)
|
|
||||||
}
|
|
||||||
for _, t := range newTables {
|
|
||||||
buf.WriteString(t.Tag)
|
|
||||||
binary.Write(buf, binary.BigEndian, t.CheckSum)
|
|
||||||
binary.Write(buf, binary.BigEndian, t.Offset)
|
|
||||||
binary.Write(buf, binary.BigEndian, t.Length)
|
|
||||||
}
|
|
||||||
for _, t := range newTables {
|
|
||||||
var tableData []byte
|
|
||||||
if t.Tag == "OS/2" {
|
|
||||||
tableData = os2Data
|
|
||||||
} else {
|
|
||||||
found := false
|
|
||||||
for _, old := range tables {
|
|
||||||
if old.Tag == t.Tag {
|
|
||||||
if uint32(len(data)) < old.Offset+old.Length {
|
|
||||||
return false, data, nil
|
|
||||||
}
|
|
||||||
tableData = data[old.Offset : old.Offset+old.Length]
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return false, data, fmt.Errorf("lost table %s", t.Tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buf.Write(tableData)
|
|
||||||
padding := align4(t.Length) - t.Length
|
|
||||||
for k := uint32(0); k < padding; k++ {
|
|
||||||
buf.WriteByte(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fullData := buf.Bytes()
|
|
||||||
var newHeadOffset uint32
|
|
||||||
var newHeadLength uint32
|
|
||||||
for _, t := range newTables {
|
|
||||||
if t.Tag == "head" {
|
|
||||||
newHeadOffset = t.Offset
|
|
||||||
newHeadLength = t.Length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if newHeadLength >= 12 {
|
|
||||||
checkSumAdjPos := newHeadOffset + 8
|
|
||||||
binary.BigEndian.PutUint32(fullData[checkSumAdjPos:checkSumAdjPos+4], 0)
|
|
||||||
cs := calcTableChecksum(fullData)
|
|
||||||
adjustment := 0xB1B0AFBA - cs
|
|
||||||
binary.BigEndian.PutUint32(fullData[checkSumAdjPos:checkSumAdjPos+4], adjustment)
|
|
||||||
}
|
|
||||||
return true, fullData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// align4 计算 4 字节对齐后的长度
|
|
||||||
// 入参: n 原始长度
|
|
||||||
// 返回: uint32 对齐后的长度
|
|
||||||
func align4(n uint32) uint32 {
|
|
||||||
return (n + 3) & ^uint32(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// calcTableChecksum 计算字体表校验和
|
|
||||||
// 入参: data 表数据
|
|
||||||
// 返回: uint32 校验和
|
|
||||||
func calcTableChecksum(data []byte) uint32 {
|
|
||||||
var sum uint32
|
|
||||||
length := len(data)
|
|
||||||
for i := 0; i < length; i += 4 {
|
|
||||||
if i+4 <= length {
|
|
||||||
sum += binary.BigEndian.Uint32(data[i : i+4])
|
|
||||||
} else {
|
|
||||||
var val uint32
|
|
||||||
rem := data[i:]
|
|
||||||
for j, b := range rem {
|
|
||||||
val |= uint32(b) << (24 - 8*j)
|
|
||||||
}
|
|
||||||
sum += val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sum
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,981 @@
|
|||||||
|
// 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 (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// wrapCFFToOTF 将 CFF 裸数据包装为 OpenType 字体格式
|
||||||
|
// 入参: 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
|
||||||
|
}
|
||||||
|
widths, err := parseCFFWidths(cffData, numGlyphs)
|
||||||
|
if err != nil {
|
||||||
|
widths = make([]uint16, numGlyphs)
|
||||||
|
for i := range widths {
|
||||||
|
widths[i] = 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var unitsPerEm uint16 = 1000
|
||||||
|
if len(cffData) > 4 {
|
||||||
|
hdrSize := int(cffData[2])
|
||||||
|
off := hdrSize
|
||||||
|
_, sz := getCFFIndexCount(cffData, off)
|
||||||
|
off += sz
|
||||||
|
topDictData, _ := getCFFIndexData(cffData, off)
|
||||||
|
if topDictData != nil {
|
||||||
|
td := parseCFFDict(topDictData)
|
||||||
|
if mat, ok := td[1207]; ok && len(mat) > 0 {
|
||||||
|
if mat[0] != 0 {
|
||||||
|
val := 1.0 / mat[0]
|
||||||
|
if val > 0 {
|
||||||
|
unitsPerEm = uint16(math.Round(val))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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["OS/2"] = buildOS2Table()
|
||||||
|
tables["name"] = buildNameTable()
|
||||||
|
tables["post"] = buildPostTable()
|
||||||
|
tables["hmtx"] = buildHmtxTable(widths)
|
||||||
|
tables["cmap"] = buildCmapTable(uint16(numGlyphs), mapping)
|
||||||
|
data, err := serializeOTF(tables)
|
||||||
|
return data, mapping, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// cffDict 使用 float64 存储所有数值,以统一处理整数和实数
|
||||||
|
type cffDict map[int][]float64
|
||||||
|
|
||||||
|
// sanitizeCFF 尝试清洗CFF数据,转换CID字体并合并FontMatrix
|
||||||
|
// 入参: data 原始CFF数据
|
||||||
|
// 返回: []byte 清洗后的CFF数据, error 错误信息
|
||||||
|
func sanitizeCFF(data []byte) ([]byte, error) {
|
||||||
|
if len(data) < 4 {
|
||||||
|
return nil, fmt.Errorf("data too short")
|
||||||
|
}
|
||||||
|
hdrSize := int(data[2])
|
||||||
|
offset := hdrSize
|
||||||
|
if offset >= len(data) {
|
||||||
|
return nil, fmt.Errorf("truncated")
|
||||||
|
}
|
||||||
|
nameCount, nameSz := getCFFIndexCount(data, offset)
|
||||||
|
if nameCount != 1 {
|
||||||
|
return nil, fmt.Errorf("multi-font cff not supported")
|
||||||
|
}
|
||||||
|
nameIndexData := data[offset : offset+nameSz]
|
||||||
|
offset += nameSz
|
||||||
|
if offset >= len(data) {
|
||||||
|
return nil, fmt.Errorf("truncated")
|
||||||
|
}
|
||||||
|
topCount, topSz := getCFFIndexCount(data, offset)
|
||||||
|
if topCount != 1 {
|
||||||
|
return nil, fmt.Errorf("top dict count != 1")
|
||||||
|
}
|
||||||
|
topDictData, _ := getCFFIndexData(data, offset)
|
||||||
|
offset += topSz
|
||||||
|
if offset >= len(data) {
|
||||||
|
return nil, fmt.Errorf("truncated")
|
||||||
|
}
|
||||||
|
_, strSz := getCFFIndexCount(data, offset)
|
||||||
|
stringIndexData := data[offset : offset+strSz]
|
||||||
|
offset += strSz
|
||||||
|
if offset >= len(data) {
|
||||||
|
return nil, fmt.Errorf("truncated")
|
||||||
|
}
|
||||||
|
_, glbSz := getCFFIndexCount(data, offset)
|
||||||
|
globalSubrIndexData := data[offset : offset+glbSz]
|
||||||
|
topDict := parseCFFDict(topDictData)
|
||||||
|
if _, isCID := topDict[1230]; !isCID {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
fdArrOffs, ok := topDict[1236]
|
||||||
|
if !ok || len(fdArrOffs) == 0 {
|
||||||
|
return nil, fmt.Errorf("cid without fdarray")
|
||||||
|
}
|
||||||
|
fdArrOff := int(fdArrOffs[0])
|
||||||
|
if fdArrOff >= len(data) {
|
||||||
|
return nil, fmt.Errorf("fdarray offset oob")
|
||||||
|
}
|
||||||
|
fdCount, _ := getCFFIndexCount(data, fdArrOff)
|
||||||
|
if fdCount != 1 {
|
||||||
|
return nil, fmt.Errorf("complex cid (fdarray count=%d) not supported", fdCount)
|
||||||
|
}
|
||||||
|
fontDictData, _ := getCFFIndexData(data, fdArrOff)
|
||||||
|
fontDict := parseCFFDict(fontDictData)
|
||||||
|
if fdMat, ok := fontDict[1207]; ok && len(fdMat) == 6 {
|
||||||
|
topMat, hasTop := topDict[1207]
|
||||||
|
if !hasTop || len(topMat) != 6 {
|
||||||
|
topMat = []float64{0.001, 0, 0, 0.001, 0, 0}
|
||||||
|
}
|
||||||
|
newMat := multiplyAffine(topMat, fdMat)
|
||||||
|
topDict[1207] = newMat
|
||||||
|
}
|
||||||
|
privVals, ok := fontDict[18]
|
||||||
|
if !ok || len(privVals) != 2 {
|
||||||
|
privVals = []float64{0, 0}
|
||||||
|
}
|
||||||
|
privSize := int(privVals[0])
|
||||||
|
privOff := int(privVals[1])
|
||||||
|
var localSubrData []byte
|
||||||
|
var privDictData []byte
|
||||||
|
if privSize > 0 && privOff < len(data) && privOff+privSize <= len(data) {
|
||||||
|
privDictData = data[privOff : privOff+privSize]
|
||||||
|
}
|
||||||
|
var subrsOffRel int
|
||||||
|
if len(privDictData) > 0 {
|
||||||
|
pDict := parseCFFDict(privDictData)
|
||||||
|
if sVals, ok := pDict[19]; ok && len(sVals) > 0 {
|
||||||
|
subrsOffRel = int(sVals[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if subrsOffRel > 0 {
|
||||||
|
subrsAbs := privOff + subrsOffRel
|
||||||
|
if subrsAbs < len(data) {
|
||||||
|
_, subSz := getCFFIndexCount(data, subrsAbs)
|
||||||
|
if subrsAbs+subSz <= len(data) {
|
||||||
|
localSubrData = data[subrsAbs : subrsAbs+subSz]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
charStringsOffs, ok := topDict[17]
|
||||||
|
if !ok || len(charStringsOffs) == 0 {
|
||||||
|
return nil, fmt.Errorf("missing charstrings")
|
||||||
|
}
|
||||||
|
charStringsOff := int(charStringsOffs[0])
|
||||||
|
_, charStrSz := getCFFIndexCount(data, charStringsOff)
|
||||||
|
charStringsData := data[charStringsOff : charStringsOff+charStrSz]
|
||||||
|
delete(topDict, 1230)
|
||||||
|
delete(topDict, 1236)
|
||||||
|
delete(topDict, 1237)
|
||||||
|
delete(topDict, 1234)
|
||||||
|
delete(topDict, 15)
|
||||||
|
delete(topDict, 16)
|
||||||
|
topDict[18] = []float64{float64(privSize), 0}
|
||||||
|
var newCFF bytes.Buffer
|
||||||
|
newCFF.Write(data[:hdrSize])
|
||||||
|
newCFF.Write(nameIndexData)
|
||||||
|
dummyDict := make(map[int][]float64)
|
||||||
|
for k, v := range topDict {
|
||||||
|
dummyDict[k] = v
|
||||||
|
}
|
||||||
|
dummyDict[17] = []float64{0}
|
||||||
|
dummyDict[18] = []float64{float64(privSize), 0}
|
||||||
|
dummyTopData := encodeCFFDict(dummyDict)
|
||||||
|
topIdxSize := 2 + 1 + 8 + len(dummyTopData)
|
||||||
|
dataStart := hdrSize + len(nameIndexData) + topIdxSize + len(stringIndexData) + len(globalSubrIndexData)
|
||||||
|
charStringsPos := dataStart
|
||||||
|
privatePos := charStringsPos + len(charStringsData)
|
||||||
|
privateLen := privSize
|
||||||
|
var finalPrivData []byte
|
||||||
|
if len(privDictData) > 0 {
|
||||||
|
pDict := parseCFFDict(privDictData)
|
||||||
|
if _, ok := pDict[19]; ok || len(localSubrData) > 0 {
|
||||||
|
pDict[19] = []float64{float64(privateLen)}
|
||||||
|
}
|
||||||
|
finalPrivData = encodeCFFDict(pDict)
|
||||||
|
privateLen = len(finalPrivData)
|
||||||
|
}
|
||||||
|
localSubrsPos := privatePos + privateLen
|
||||||
|
topDict[17] = []float64{float64(charStringsPos)}
|
||||||
|
topDict[18] = []float64{float64(privateLen), float64(privatePos)}
|
||||||
|
finalTopData := encodeCFFDict(topDict)
|
||||||
|
topIndex := encodeCFFIndex([]([]byte){finalTopData})
|
||||||
|
newCFF.Reset()
|
||||||
|
newCFF.Write(data[:hdrSize])
|
||||||
|
newCFF.Write(nameIndexData)
|
||||||
|
newCFF.Write(topIndex)
|
||||||
|
newCFF.Write(stringIndexData)
|
||||||
|
newCFF.Write(globalSubrIndexData)
|
||||||
|
if newCFF.Len() != dataStart {
|
||||||
|
diff := newCFF.Len() - dataStart
|
||||||
|
charStringsPos += diff
|
||||||
|
privatePos += diff
|
||||||
|
localSubrsPos += diff
|
||||||
|
topDict[17] = []float64{float64(charStringsPos)}
|
||||||
|
topDict[18] = []float64{float64(privateLen), float64(privatePos)}
|
||||||
|
finalTopData = encodeCFFDict(topDict)
|
||||||
|
topIndex = encodeCFFIndex([]([]byte){finalTopData})
|
||||||
|
newCFF.Reset()
|
||||||
|
newCFF.Write(data[:hdrSize])
|
||||||
|
newCFF.Write(nameIndexData)
|
||||||
|
newCFF.Write(topIndex)
|
||||||
|
newCFF.Write(stringIndexData)
|
||||||
|
newCFF.Write(globalSubrIndexData)
|
||||||
|
}
|
||||||
|
newCFF.Write(charStringsData)
|
||||||
|
newCFF.Write(finalPrivData)
|
||||||
|
if len(localSubrData) > 0 {
|
||||||
|
newCFF.Write(localSubrData)
|
||||||
|
}
|
||||||
|
return newCFF.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCFFAndCountGlyphs 解析 CFF 头部并统计字形数量
|
||||||
|
// 入参: data CFF数据
|
||||||
|
// 返回: int 字形数量, error 错误信息
|
||||||
|
func parseCFFAndCountGlyphs(data []byte) (int, error) {
|
||||||
|
if len(data) < 4 {
|
||||||
|
return 0, fmt.Errorf("data too short")
|
||||||
|
}
|
||||||
|
hdrSize := int(data[2])
|
||||||
|
offset := hdrSize
|
||||||
|
if offset >= len(data) {
|
||||||
|
return 0, fmt.Errorf("truncated")
|
||||||
|
}
|
||||||
|
count, sz := getCFFIndexCount(data, offset)
|
||||||
|
if count != 1 {
|
||||||
|
return 0, fmt.Errorf("multi-font cff not supported")
|
||||||
|
}
|
||||||
|
offset += sz
|
||||||
|
if offset >= len(data) {
|
||||||
|
return 0, fmt.Errorf("truncated")
|
||||||
|
}
|
||||||
|
count, _ = getCFFIndexCount(data, offset)
|
||||||
|
if count != 1 {
|
||||||
|
return 0, fmt.Errorf("top dict count mismatch")
|
||||||
|
}
|
||||||
|
topDictData, _ := getCFFIndexData(data, offset)
|
||||||
|
if topDictData != nil {
|
||||||
|
dict := parseCFFDict(topDictData)
|
||||||
|
if offsetVals, ok := dict[17]; ok && len(offsetVals) > 0 {
|
||||||
|
charStrOff := int(offsetVals[0])
|
||||||
|
if charStrOff > 0 && charStrOff < len(data) {
|
||||||
|
count, _ := getCFFIndexCount(data, charStrOff)
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("failed to parse top dict")
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiplyAffine 2x3 仿射矩阵乘法
|
||||||
|
// 入参: a 矩阵A, b 矩阵B
|
||||||
|
// 返回: []float64 结果矩阵
|
||||||
|
func multiplyAffine(a, b []float64) []float64 {
|
||||||
|
return []float64{
|
||||||
|
a[0]*b[0] + a[2]*b[1],
|
||||||
|
a[1]*b[0] + a[3]*b[1],
|
||||||
|
a[0]*b[2] + a[2]*b[3],
|
||||||
|
a[1]*b[2] + a[3]*b[3],
|
||||||
|
a[0]*b[4] + a[2]*b[5] + a[4],
|
||||||
|
a[1]*b[4] + a[3]*b[5] + a[5],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCFFDict 解析 CFF 字典数据
|
||||||
|
// 入参: data 字典数据
|
||||||
|
// 返回: cffDict 解析后的字典映射
|
||||||
|
func parseCFFDict(data []byte) cffDict {
|
||||||
|
dict := make(cffDict)
|
||||||
|
var operands []float64
|
||||||
|
i := 0
|
||||||
|
for i < len(data) {
|
||||||
|
b := data[i]
|
||||||
|
i++
|
||||||
|
if b <= 27 {
|
||||||
|
op := int(b)
|
||||||
|
if b == 12 {
|
||||||
|
if i >= len(data) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
op = 1200 + int(data[i])
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
dict[op] = operands
|
||||||
|
operands = nil
|
||||||
|
} else if b == 28 {
|
||||||
|
if i+1 < len(data) {
|
||||||
|
val := int(int16(binary.BigEndian.Uint16(data[i:])))
|
||||||
|
operands = append(operands, float64(val))
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
} else if b == 29 {
|
||||||
|
if i+3 < len(data) {
|
||||||
|
val := int(int32(binary.BigEndian.Uint32(data[i:])))
|
||||||
|
operands = append(operands, float64(val))
|
||||||
|
i += 4
|
||||||
|
}
|
||||||
|
} else if b == 30 {
|
||||||
|
s, n := parseCFFReal(data[i:])
|
||||||
|
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||||
|
operands = append(operands, f)
|
||||||
|
}
|
||||||
|
i += n
|
||||||
|
} else if b >= 32 && b <= 246 {
|
||||||
|
operands = append(operands, float64(int(b)-139))
|
||||||
|
} else if b >= 247 && b <= 250 {
|
||||||
|
if i < len(data) {
|
||||||
|
b1 := int(data[i])
|
||||||
|
i++
|
||||||
|
operands = append(operands, float64((int(b)-247)*256+b1+108))
|
||||||
|
}
|
||||||
|
} else if b >= 251 && b <= 254 {
|
||||||
|
if i < len(data) {
|
||||||
|
b1 := int(data[i])
|
||||||
|
i++
|
||||||
|
operands = append(operands, float64(-(int(b)-251)*256-b1-108))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dict
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCFFReal 解析 CFF 实数编码
|
||||||
|
// 入参: data 数据切片
|
||||||
|
// 返回: string 实数字符串, int 消耗字节数
|
||||||
|
func parseCFFReal(data []byte) (string, int) {
|
||||||
|
var sb strings.Builder
|
||||||
|
i := 0
|
||||||
|
done := false
|
||||||
|
for i < len(data) && !done {
|
||||||
|
b := data[i]
|
||||||
|
i++
|
||||||
|
nibbles := []byte{b >> 4, b & 0x0F}
|
||||||
|
for _, n := range nibbles {
|
||||||
|
if n == 0xF {
|
||||||
|
done = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if n <= 9 {
|
||||||
|
sb.WriteString(strconv.Itoa(int(n)))
|
||||||
|
}
|
||||||
|
if n == 0xA {
|
||||||
|
sb.WriteString(".")
|
||||||
|
}
|
||||||
|
if n == 0xB {
|
||||||
|
sb.WriteString("E")
|
||||||
|
}
|
||||||
|
if n == 0xC {
|
||||||
|
sb.WriteString("E-")
|
||||||
|
}
|
||||||
|
if n == 0xE {
|
||||||
|
sb.WriteString("-")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String(), i
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeCFFDict 编码 CFF 字典 (仅使用 float64 操作数)
|
||||||
|
// 入参: dict CFF字典映射
|
||||||
|
// 返回: []byte 编码后的字典数据
|
||||||
|
func encodeCFFDict(dict cffDict) []byte {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
var keys []int
|
||||||
|
for k := range dict {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Ints(keys)
|
||||||
|
for _, op := range keys {
|
||||||
|
vals := dict[op]
|
||||||
|
for _, val := range vals {
|
||||||
|
encodeNumberCFF(buf, val)
|
||||||
|
}
|
||||||
|
if op >= 1200 {
|
||||||
|
buf.WriteByte(12)
|
||||||
|
buf.WriteByte(byte(op - 1200))
|
||||||
|
} else {
|
||||||
|
buf.WriteByte(byte(op))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeNumberCFF 编码单个数值到 CFF 格式
|
||||||
|
// 入参: buf 缓冲区, val 数值
|
||||||
|
func encodeNumberCFF(buf *bytes.Buffer, val float64) {
|
||||||
|
if val == math.Trunc(val) {
|
||||||
|
iv := int(val)
|
||||||
|
if iv >= -107 && iv <= 107 {
|
||||||
|
buf.WriteByte(byte(iv + 139))
|
||||||
|
} else if iv >= 108 && iv <= 1131 {
|
||||||
|
iv -= 108
|
||||||
|
buf.WriteByte(byte((iv >> 8) + 247))
|
||||||
|
buf.WriteByte(byte(iv & 0xFF))
|
||||||
|
} else if iv >= -1131 && iv <= -108 {
|
||||||
|
iv = -iv - 108
|
||||||
|
buf.WriteByte(byte((iv >> 8) + 251))
|
||||||
|
buf.WriteByte(byte(iv & 0xFF))
|
||||||
|
} else if iv >= -32768 && iv <= 32767 {
|
||||||
|
buf.WriteByte(28)
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(iv))
|
||||||
|
} else {
|
||||||
|
buf.WriteByte(29)
|
||||||
|
binary.Write(buf, binary.BigEndian, int32(iv))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s := fmt.Sprintf("%g", val)
|
||||||
|
buf.WriteByte(30)
|
||||||
|
var nibbles []byte
|
||||||
|
for _, c := range s {
|
||||||
|
var n byte
|
||||||
|
switch c {
|
||||||
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||||
|
n = byte(c - '0')
|
||||||
|
case '.':
|
||||||
|
n = 0xA
|
||||||
|
case 'E', 'e':
|
||||||
|
n = 0xB
|
||||||
|
case '-':
|
||||||
|
n = 0xE
|
||||||
|
}
|
||||||
|
nibbles = append(nibbles, n)
|
||||||
|
}
|
||||||
|
nibbles = append(nibbles, 0xF)
|
||||||
|
if len(nibbles)%2 != 0 {
|
||||||
|
nibbles = append(nibbles, 0xF)
|
||||||
|
}
|
||||||
|
for i := 0; i < len(nibbles); i += 2 {
|
||||||
|
b := nibbles[i] << 4
|
||||||
|
if i+1 < len(nibbles) {
|
||||||
|
b |= nibbles[i+1]
|
||||||
|
}
|
||||||
|
buf.WriteByte(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeCFFIndex 编码 CFF 索引结构
|
||||||
|
// 入参: items 数据项列表
|
||||||
|
// 返回: []byte 编码后的索引数据
|
||||||
|
func encodeCFFIndex(items []([]byte)) []byte {
|
||||||
|
count := len(items)
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(count))
|
||||||
|
if count == 0 {
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
totalSize := 0
|
||||||
|
for _, item := range items {
|
||||||
|
totalSize += len(item)
|
||||||
|
}
|
||||||
|
offSize := 1
|
||||||
|
if totalSize+1 > 255 {
|
||||||
|
offSize = 2
|
||||||
|
}
|
||||||
|
if totalSize+1 > 65535 {
|
||||||
|
offSize = 3
|
||||||
|
}
|
||||||
|
if totalSize+1 > 16777215 {
|
||||||
|
offSize = 4
|
||||||
|
}
|
||||||
|
buf.WriteByte(byte(offSize))
|
||||||
|
offset := 1
|
||||||
|
putOffset(buf, offset, offSize)
|
||||||
|
for _, item := range items {
|
||||||
|
offset += len(item)
|
||||||
|
putOffset(buf, offset, offSize)
|
||||||
|
}
|
||||||
|
for _, item := range items {
|
||||||
|
buf.Write(item)
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// putOffset 写入指定大小的偏移量
|
||||||
|
// 入参: buf 缓冲区, val 偏移值, size 字节大小
|
||||||
|
func putOffset(buf *bytes.Buffer, val int, size int) {
|
||||||
|
tmp := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(tmp, uint32(val))
|
||||||
|
buf.Write(tmp[4-size:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCFFIndexCount 读取 CFF 索引的计数和大小
|
||||||
|
// 入参: data CFF数据, offset 偏移量
|
||||||
|
// 返回: int 数量, int 索引结构总大小
|
||||||
|
func getCFFIndexCount(data []byte, offset int) (int, int) {
|
||||||
|
if offset+2 > len(data) {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
count := int(binary.BigEndian.Uint16(data[offset:]))
|
||||||
|
if count == 0 {
|
||||||
|
return 0, 2
|
||||||
|
}
|
||||||
|
if offset+3 > len(data) {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
offSize := int(data[offset+2])
|
||||||
|
if offSize < 1 || offSize > 4 {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
dataSizeLen := (count + 1) * offSize
|
||||||
|
if offset+3+dataSizeLen > len(data) {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
endOffsetPos := offset + 3 + count*offSize
|
||||||
|
if endOffsetPos+offSize > len(data) {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
dataEnd := readCFFOffset(data, endOffsetPos, offSize)
|
||||||
|
if dataEnd < 1 {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
return count, 3 + (count+1)*offSize + (dataEnd - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCFFIndexData 读取 CFF 索引的数据块
|
||||||
|
// 入参: data CFF数据, offset 偏移量
|
||||||
|
// 返回: []byte 索引数据(已去除offsets), int 索引结构总大小
|
||||||
|
func getCFFIndexData(data []byte, offset int) ([]byte, int) {
|
||||||
|
count, size := getCFFIndexCount(data, offset)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, size
|
||||||
|
}
|
||||||
|
if offset+3 > len(data) {
|
||||||
|
return nil, size
|
||||||
|
}
|
||||||
|
offSize := int(data[offset+2])
|
||||||
|
if offset+3+offSize > len(data) {
|
||||||
|
return nil, size
|
||||||
|
}
|
||||||
|
off0 := readCFFOffset(data, offset+3, offSize)
|
||||||
|
if offset+3+offSize*2 > len(data) {
|
||||||
|
return nil, size
|
||||||
|
}
|
||||||
|
off1 := readCFFOffset(data, offset+3+offSize, offSize)
|
||||||
|
dataStartRel := 3 + (count+1)*offSize
|
||||||
|
dataStartAbs := offset + dataStartRel
|
||||||
|
start := dataStartAbs + (off0 - 1)
|
||||||
|
length := off1 - off0
|
||||||
|
if start < 0 || length < 0 || start+length > len(data) {
|
||||||
|
return nil, size
|
||||||
|
}
|
||||||
|
return data[start : start+length], size
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCFFWidths 从 CFF 数据中解析 Glyph 宽度
|
||||||
|
// 入参: data CFF数据, numGlyphs 字形数量
|
||||||
|
// 返回: []uint16 宽度列表, error 错误信息
|
||||||
|
func parseCFFWidths(data []byte, numGlyphs int) ([]uint16, error) {
|
||||||
|
if len(data) < 4 {
|
||||||
|
return nil, fmt.Errorf("data too short")
|
||||||
|
}
|
||||||
|
hdrSize := int(data[2])
|
||||||
|
offset := hdrSize
|
||||||
|
_, sz := getCFFIndexCount(data, offset)
|
||||||
|
offset += sz
|
||||||
|
topDictData, sz := getCFFIndexData(data, offset)
|
||||||
|
offset += sz
|
||||||
|
if topDictData == nil {
|
||||||
|
return nil, fmt.Errorf("failed to read top dict")
|
||||||
|
}
|
||||||
|
topDict := parseCFFDict(topDictData)
|
||||||
|
_, sz = getCFFIndexCount(data, offset)
|
||||||
|
offset += sz
|
||||||
|
_, sz = getCFFIndexCount(data, offset)
|
||||||
|
offset += sz
|
||||||
|
var nominalWidthX float64 = 0
|
||||||
|
var defaultWidthX float64 = 0
|
||||||
|
if vals, ok := topDict[18]; ok && len(vals) == 2 {
|
||||||
|
privSize := int(vals[0])
|
||||||
|
privOff := int(vals[1])
|
||||||
|
if privSize > 0 && privOff+privSize <= len(data) {
|
||||||
|
privData := data[privOff : privOff+privSize]
|
||||||
|
privDict := parseCFFDict(privData)
|
||||||
|
if v, ok := privDict[20]; ok && len(v) > 0 {
|
||||||
|
defaultWidthX = v[0]
|
||||||
|
}
|
||||||
|
if v, ok := privDict[21]; ok && len(v) > 0 {
|
||||||
|
nominalWidthX = v[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if vals, ok := topDict[17]; ok && len(vals) > 0 {
|
||||||
|
charStrOff := int(vals[0])
|
||||||
|
count, _ := getCFFIndexCount(data, charStrOff)
|
||||||
|
limit := count
|
||||||
|
if numGlyphs < limit {
|
||||||
|
limit = numGlyphs
|
||||||
|
}
|
||||||
|
widths := make([]uint16, numGlyphs)
|
||||||
|
for i := range widths {
|
||||||
|
widths[i] = uint16(defaultWidthX)
|
||||||
|
}
|
||||||
|
for i := 0; i < limit; i++ {
|
||||||
|
offSize := int(data[charStrOff+2])
|
||||||
|
p1 := charStrOff + 3 + i*offSize
|
||||||
|
p2 := p1 + offSize
|
||||||
|
off1 := readCFFOffset(data, p1, offSize)
|
||||||
|
off2 := readCFFOffset(data, p2, offSize)
|
||||||
|
dataStartBase := charStrOff + 3 + (count+1)*offSize
|
||||||
|
start := dataStartBase + (off1 - 1)
|
||||||
|
length := off2 - off1
|
||||||
|
if start < 0 || start+length > len(data) {
|
||||||
|
widths[i] = uint16(defaultWidthX)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
csData := data[start : start+length]
|
||||||
|
w := scanCharStringWidth(csData, nominalWidthX, defaultWidthX)
|
||||||
|
widths[i] = uint16(w)
|
||||||
|
}
|
||||||
|
return widths, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no charstrings")
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanCharStringWidth 扫描 CharString 获取宽度
|
||||||
|
// 入参: data CharString数据, nominal, def 默认宽度值
|
||||||
|
// 返回: float64 宽度值
|
||||||
|
func scanCharStringWidth(data []byte, nominal, def float64) float64 {
|
||||||
|
stackDepth := 0
|
||||||
|
i := 0
|
||||||
|
firstVal := 0.0
|
||||||
|
for i < len(data) {
|
||||||
|
b := data[i]
|
||||||
|
if b <= 31 {
|
||||||
|
if b == 28 {
|
||||||
|
i += 3
|
||||||
|
stackDepth++
|
||||||
|
if stackDepth == 1 {
|
||||||
|
firstVal = parseShortInt(data, i-3)
|
||||||
|
}
|
||||||
|
} else if b == 29 {
|
||||||
|
i += 5
|
||||||
|
stackDepth++
|
||||||
|
} else if b == 12 {
|
||||||
|
i += 2
|
||||||
|
if stackDepth%2 != 0 {
|
||||||
|
return nominal + firstVal
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
} else if b == 19 || b == 20 {
|
||||||
|
if stackDepth%2 != 0 {
|
||||||
|
return nominal + firstVal
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
} else {
|
||||||
|
if stackDepth%2 != 0 {
|
||||||
|
return nominal + firstVal
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stackDepth++
|
||||||
|
if stackDepth == 1 {
|
||||||
|
firstVal = parseNumberType2(data, i)
|
||||||
|
}
|
||||||
|
if b >= 32 && b <= 246 {
|
||||||
|
i++
|
||||||
|
} else if b >= 247 && b <= 250 {
|
||||||
|
i += 2
|
||||||
|
} else if b >= 251 && b <= 254 {
|
||||||
|
i += 2
|
||||||
|
} else if b == 255 {
|
||||||
|
i += 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseShortInt 解析短整数 (Type 2 CharString)
|
||||||
|
// 入参: data 数据, idx 索引
|
||||||
|
// 返回: float64 浮点值
|
||||||
|
func parseShortInt(data []byte, idx int) float64 {
|
||||||
|
return float64(int16(binary.BigEndian.Uint16(data[idx+1:])))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseNumberType2 解析 Number (Type 2)
|
||||||
|
// 入参: data 数据, idx 索引
|
||||||
|
// 返回: float64 浮点值
|
||||||
|
func parseNumberType2(data []byte, idx int) float64 {
|
||||||
|
b := data[idx]
|
||||||
|
if b >= 32 && b <= 246 {
|
||||||
|
return float64(int(b) - 139)
|
||||||
|
}
|
||||||
|
if b >= 247 && b <= 250 {
|
||||||
|
return float64((int(b)-247)*256 + int(data[idx+1]) + 108)
|
||||||
|
}
|
||||||
|
if b >= 251 && b <= 254 {
|
||||||
|
return float64(-(int(b)-251)*256 - int(data[idx+1]) - 108)
|
||||||
|
}
|
||||||
|
if b == 28 {
|
||||||
|
return float64(int16(binary.BigEndian.Uint16(data[idx+1:])))
|
||||||
|
}
|
||||||
|
if b == 255 {
|
||||||
|
return float64(int16(binary.BigEndian.Uint16(data[idx+1:]))) + float64(binary.BigEndian.Uint16(data[idx+3:]))/65536.0
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// readCFFOffset 读取指定大小的偏移量
|
||||||
|
// 入参: data 数据, pos 位置, size 大小
|
||||||
|
// 返回: int 偏移量
|
||||||
|
func readCFFOffset(data []byte, pos, size int) int {
|
||||||
|
var val int
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
if pos+i < len(data) {
|
||||||
|
val = (val << 8) | int(data[pos+i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCmapFromCFF 从 CFF 数据中恢复 Unicode 映射
|
||||||
|
// 入参: data CFF数据, numGlyphs 字形数量
|
||||||
|
// 返回: map[rune]uint16 恢复的映射表
|
||||||
|
func getCmapFromCFF(data []byte, numGlyphs int) map[rune]uint16 {
|
||||||
|
if len(data) < 4 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
hdrSize := int(data[2])
|
||||||
|
offset := hdrSize
|
||||||
|
_, sz := getCFFIndexCount(data, offset)
|
||||||
|
offset += sz
|
||||||
|
_, szTD := getCFFIndexCount(data, offset)
|
||||||
|
topDictData, _ := getCFFIndexData(data, offset)
|
||||||
|
offset += szTD
|
||||||
|
stringIndexOff := offset
|
||||||
|
if topDictData == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
td := parseCFFDict(topDictData)
|
||||||
|
charsetOff := 0
|
||||||
|
if vals, ok := td[15]; ok && len(vals) > 0 {
|
||||||
|
charsetOff = int(vals[0])
|
||||||
|
}
|
||||||
|
sids := make([]int, numGlyphs)
|
||||||
|
sids[0] = 0
|
||||||
|
if charsetOff > 2 {
|
||||||
|
sidsParsed := parseCFFCharset(data, charsetOff, numGlyphs)
|
||||||
|
copy(sids[1:], sidsParsed)
|
||||||
|
} else if charsetOff == 0 {
|
||||||
|
count := 228
|
||||||
|
if numGlyphs-1 < count {
|
||||||
|
count = numGlyphs - 1
|
||||||
|
}
|
||||||
|
for i := 1; i <= count; i++ {
|
||||||
|
sids[i] = i
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mapping := make(map[rune]uint16)
|
||||||
|
for gid, sid := range sids {
|
||||||
|
if gid == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var name string
|
||||||
|
if sid <= 390 {
|
||||||
|
if sid >= 0 && sid < len(cffStandardStrings) {
|
||||||
|
name = cffStandardStrings[sid]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
idx := sid - 391
|
||||||
|
name = readStringIndexItem(data, stringIndexOff, idx)
|
||||||
|
}
|
||||||
|
r := rune(0)
|
||||||
|
if name != "" {
|
||||||
|
r = getUnicodeFromName(name)
|
||||||
|
}
|
||||||
|
if r == 0 {
|
||||||
|
r = 0xE000 + rune(gid)
|
||||||
|
}
|
||||||
|
mapping[r] = uint16(gid)
|
||||||
|
}
|
||||||
|
return mapping
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCFFCharset 解析 CFF 字符集并返回 SID 列表
|
||||||
|
// 入参: data CFF数据, offset 偏移量, numGlyphs 字形数量
|
||||||
|
// 返回: []int SID列表
|
||||||
|
func parseCFFCharset(data []byte, offset int, numGlyphs int) []int {
|
||||||
|
if offset >= len(data) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
format := data[offset]
|
||||||
|
var sids []int
|
||||||
|
count := numGlyphs - 1
|
||||||
|
pos := offset + 1
|
||||||
|
switch format {
|
||||||
|
case 0:
|
||||||
|
for i := 0; i < count && pos+2 <= len(data); i++ {
|
||||||
|
sid := int(binary.BigEndian.Uint16(data[pos:]))
|
||||||
|
sids = append(sids, sid)
|
||||||
|
pos += 2
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
|
for len(sids) < count && pos+3 <= len(data) {
|
||||||
|
first := int(binary.BigEndian.Uint16(data[pos:]))
|
||||||
|
nLeft := int(data[pos+2])
|
||||||
|
pos += 3
|
||||||
|
for j := 0; j <= nLeft; j++ {
|
||||||
|
sids = append(sids, first+j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
for len(sids) < count && pos+4 <= len(data) {
|
||||||
|
first := int(binary.BigEndian.Uint16(data[pos:]))
|
||||||
|
nLeft := int(binary.BigEndian.Uint16(data[pos+2:]))
|
||||||
|
pos += 4
|
||||||
|
for j := 0; j <= nLeft; j++ {
|
||||||
|
sids = append(sids, first+j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(sids) > count {
|
||||||
|
sids = sids[:count]
|
||||||
|
}
|
||||||
|
return sids
|
||||||
|
}
|
||||||
|
|
||||||
|
// readStringIndexItem 读取 CFF 字符串索引项
|
||||||
|
// 入参: data CFF数据, offset 索引偏移, idx 索引号
|
||||||
|
// 返回: string 读取的字符串
|
||||||
|
func readStringIndexItem(data []byte, offset int, idx int) string {
|
||||||
|
if offset >= len(data) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
count := int(binary.BigEndian.Uint16(data[offset:]))
|
||||||
|
offSize := int(data[offset+2])
|
||||||
|
if idx >= count {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
offArrayStart := offset + 3
|
||||||
|
p1 := offArrayStart + idx*offSize
|
||||||
|
p2 := p1 + offSize
|
||||||
|
if p2+offSize > len(data) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
loc1 := readCFFOffset(data, p1, offSize)
|
||||||
|
loc2 := readCFFOffset(data, p2, offSize)
|
||||||
|
dataStart := offArrayStart + (count+1)*offSize
|
||||||
|
start := dataStart + loc1 - 1
|
||||||
|
length := loc2 - loc1
|
||||||
|
if start < 0 || start+length > len(data) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(data[start : start+length])
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUnicodeFromName 根据字形名称获取对应的Unicode字符
|
||||||
|
// 入参: name 字形名称
|
||||||
|
// 返回: rune Unicode字符
|
||||||
|
func getUnicodeFromName(name string) rune {
|
||||||
|
if strings.HasPrefix(name, "uni") && len(name) == 7 {
|
||||||
|
hexStr := strings.ToUpper(name[3:])
|
||||||
|
if val, err := strconv.ParseInt(hexStr, 16, 32); err == nil {
|
||||||
|
return rune(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(name, "u") && len(name) >= 5 && len(name) <= 7 && !strings.HasPrefix(name, "uni") {
|
||||||
|
hexStr := strings.ToUpper(name[1:])
|
||||||
|
if val, err := strconv.ParseInt(hexStr, 16, 32); err == nil {
|
||||||
|
return rune(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch name {
|
||||||
|
case "space":
|
||||||
|
return ' '
|
||||||
|
case "exclam":
|
||||||
|
return '!'
|
||||||
|
case "quotedbl":
|
||||||
|
return '"'
|
||||||
|
case "numbersign":
|
||||||
|
return '#'
|
||||||
|
case "dollar":
|
||||||
|
return '$'
|
||||||
|
case "percent":
|
||||||
|
return '%'
|
||||||
|
case "ampersand":
|
||||||
|
return '&'
|
||||||
|
case "quotesingle":
|
||||||
|
return '\''
|
||||||
|
case "parenleft":
|
||||||
|
return '('
|
||||||
|
case "parenright":
|
||||||
|
return ')'
|
||||||
|
case "asterisk":
|
||||||
|
return '*'
|
||||||
|
case "plus":
|
||||||
|
return '+'
|
||||||
|
case "comma":
|
||||||
|
return ','
|
||||||
|
case "hyphen":
|
||||||
|
return '-'
|
||||||
|
case "period":
|
||||||
|
return '.'
|
||||||
|
case "slash":
|
||||||
|
return '/'
|
||||||
|
case "colon":
|
||||||
|
return ':'
|
||||||
|
case "semicolon":
|
||||||
|
return ';'
|
||||||
|
case "less":
|
||||||
|
return '<'
|
||||||
|
case "equal":
|
||||||
|
return '='
|
||||||
|
case "greater":
|
||||||
|
return '>'
|
||||||
|
case "question":
|
||||||
|
return '?'
|
||||||
|
case "at":
|
||||||
|
return '@'
|
||||||
|
case "bracketleft":
|
||||||
|
return '['
|
||||||
|
case "backslash":
|
||||||
|
return '\\'
|
||||||
|
case "bracketright":
|
||||||
|
return ']'
|
||||||
|
case "asciicircum":
|
||||||
|
return '^'
|
||||||
|
case "underscore":
|
||||||
|
return '_'
|
||||||
|
case "grave":
|
||||||
|
return '`'
|
||||||
|
case "braceleft":
|
||||||
|
return '{'
|
||||||
|
case "bar":
|
||||||
|
return '|'
|
||||||
|
case "braceright":
|
||||||
|
return '}'
|
||||||
|
case "asciitilde":
|
||||||
|
return '~'
|
||||||
|
}
|
||||||
|
if len(name) == 1 {
|
||||||
|
return rune(name[0])
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// cffStandardStrings CFF 标准字符串表
|
||||||
|
var cffStandardStrings = []string{
|
||||||
|
".notdef", "space", "exclam", "quotedbl", "numbersign", "dollar", "percent", "ampersand", "quoteright", "parenleft", "parenright", "asterisk", "plus", "comma", "hyphen", "period", "slash", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "colon", "semicolon", "less", "equal", "greater", "question", "at", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "bracketleft", "backslash", "bracketright", "asciicircum", "underscore", "quoteleft", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde", "exclamdown", "cent", "sterling", "fraction", "yen", "florin", "section", "currency", "quotesingle", "quotedblleft", "quotedblright", "guillemotleft", "guillemotright", "dagger", "daggerdbl", "fi", "fl", "endash", "emdash", "paragraph", "bullet", "quotesinglbase", "quotedblbase", "second", "circumflex", "breve", "dotaccent", "dieresis", "grave", "ring", "cedilla", "hungarumlaut", "ogonek", "caron", "emspace",
|
||||||
|
"AE", "ordfeminine", "Lslash", "Oslash", "OE", "ordmasculine", "ae", "dotlessi", "lslash", "oslash", "oe", "germandbls", "onesuperior", "logicalnot", "mu", "trademark", "Eth", "onehalf", "plusminus", "Thorn", "onequarter", "divide", "brokenbar", "degree", "thorn", "threequarters", "twosuperior", "registered", "minus", "eth", "multiply", "threesuperior", "copyright", "Aacute", "Acircumflex", "Adieresis", "Agrave", "Aring", "Atilde", "Ccedilla", "Eacute", "Ecircumflex", "Edieresis", "Egrave", "Iacute", "Icircumflex", "Idieresis", "Igrave", "Ntilde", "Oacute", "Ocircumflex", "Odieresis", "Ograve", "Otilde", "Scaron", "Uacute", "Ucircumflex", "Udieresis", "Ugrave", "Yacute", "Ydieresis", "Zcaron", "aacute", "acircumflex", "adieresis", "agrave", "aring", "atilde", "ccedilla", "eacute", "ecircumflex", "edieresis", "egrave", "iacute", "icircumflex", "idieresis", "igrave", "ntilde", "oacute", "ocircumflex", "odieresis", "ograve", "otilde", "scaron", "uacute", "ucircumflex", "udieresis", "ugrave", "yacute", "ydieresis", "zcaron", "exclamsmall", "Hungarumlautsmall", "dollaroldstyle", "dollarsuperior", "ampersandsmall", "Acutesmall", "parenleftsuperior", "parenrightsuperior", "2dotlead", "nbspace", "1dotlead", "zerooldstyle", "oneoldstyle", "twooldstyle", "threeoldstyle", "fouroldstyle", "fiveoldstyle", "sixoldstyle", "sevenoldstyle", "eightoldstyle", "nineoldstyle", "commasuperior", "threequartersemdash", "periodsuperior", "questionsmall", "asuperior", "bsuperior", "centsuperior", "dsuperior", "esuperior", "isuperior", "lsuperior", "msuperior", "nsuperior", "osuperior", "rsuperior", "ssuperior", "tsuperior", "ff", "ffi", "ffl", "parenleftinferior", "parenrightinferior", "Circumflexsmall", "hyphensuperior", "Gravesmall", "Asmall", "Bsmall", "Csmall", "Dsmall", "Esmall", "Fsmall", "Gsmall", "Hsmall", "Ismall", "Jsmall", "Ksmall", "Lsmall", "Msmall", "Nsmall", "Osmall", "Psmall", "Qsmall", "Rsmall", "Ssmall", "Tsmall", "Usmall", "Vsmall", "Wsmall", "Xsmall", "Ysmall", "Zsmall", "colonmonetary", "onefitted", "rupiah", "Tildesmall", "exclamdownsmall", "centoldstyle", "Lslashsmall", "Scaronsmall", "Zcaronsmall", "Dieresissmall", "Brevesmall", "Caronsmall", "Dotaccentsmall", "Macronsmall", "figuredash", "hypheninferior", "Ogoneksmall", "Ringsmall", "Cedillasmall", "questiondownsmall", "oneeighth", "threeeighths", "fiveeighths", "seveneighths", "onethird", "twothirds", "zerosuperior", "foursuperior", "fivesuperior", "sixsuperior", "sevensuperior", "eightsuperior", "ninesuperior", "zeroinferior", "oneinferior", "twoinferior", "threeinferior", "fourinferior", "fiveinferior", "sixinferior", "seveninferior", "eightinferior", "nineinferior", "centinferior", "dollarinferior", "periodinferior", "commainferior", "Agravesmall", "Aacutesmall", "Acircumflexsmall", "Atildesmall", "Adieresissmall", "Aringsmall", "AEsmall", "Ccedillasmall", "Egravesmall", "Eacutesmall", "Ecircumflexsmall", "Edieresissmall", "Igravesmall", "Iacutesmall", "Icircumflexsmall", "Idieresissmall", "Ethsmall", "Ntildesmall", "Ogravesmall", "Oacutesmall", "Ocircumflexsmall", "Otildesmall", "Odieresissmall", "OEsmall", "Oslashsmall", "Ugravesmall", "Uacutesmall", "Ucircumflexsmall", "Udieresissmall", "Yacutesmall", "Thornsmall", "Ydieresissmall", "001.000", "001.001", "001.002", "001.003", "Black", "Bold", "Book", "Light", "Medium", "Regular", "Roman", "Semibold",
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
// 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/binary"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fixTrueType 尝试修复 TrueType/OpenType 字体数据
|
||||||
|
// 入参: data 原始字体数据, fixCmap 是否修复cmap, fixName 是否修复name
|
||||||
|
// 返回: bool 是否修复, []byte 修复后数据, map[rune]uint16 字符映射, bool 是否缺失cmap, error 错误信息
|
||||||
|
func fixTrueType(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uint16, bool, error) {
|
||||||
|
if len(data) < 12 {
|
||||||
|
return false, data, nil, false, nil
|
||||||
|
}
|
||||||
|
numTables := binary.BigEndian.Uint16(data[4:6])
|
||||||
|
existingTables := make(map[string][]byte)
|
||||||
|
pos := 12
|
||||||
|
for i := 0; i < int(numTables); i++ {
|
||||||
|
if len(data) < pos+16 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
tag := string(data[pos : pos+4])
|
||||||
|
offset := binary.BigEndian.Uint32(data[pos+8 : pos+12])
|
||||||
|
length := binary.BigEndian.Uint32(data[pos+12 : pos+16])
|
||||||
|
if uint32(len(data)) >= offset+length {
|
||||||
|
existingTables[tag] = data[offset : offset+length]
|
||||||
|
}
|
||||||
|
pos += 16
|
||||||
|
}
|
||||||
|
missingHead := existingTables["head"] == nil
|
||||||
|
missingMaxp := existingTables["maxp"] == nil
|
||||||
|
missingHhea := existingTables["hhea"] == nil
|
||||||
|
missingHmtx := existingTables["hmtx"] == nil
|
||||||
|
missingOS2 := existingTables["OS/2"] == nil
|
||||||
|
missingCmap := existingTables["cmap"] == nil
|
||||||
|
if !missingCmap {
|
||||||
|
if !hasUsableCmap(existingTables["cmap"]) {
|
||||||
|
missingCmap = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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"]
|
||||||
|
if len(maxp) >= 6 {
|
||||||
|
numGlyphs = binary.BigEndian.Uint16(maxp[4:6])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if numGlyphs == 0 {
|
||||||
|
numGlyphs = 255
|
||||||
|
}
|
||||||
|
if missingHead {
|
||||||
|
newTables["head"] = buildHeadTable(1000)
|
||||||
|
}
|
||||||
|
if missingMaxp {
|
||||||
|
newTables["maxp"] = buildMaxpTable(numGlyphs)
|
||||||
|
}
|
||||||
|
if missingHhea {
|
||||||
|
newTables["hhea"] = buildHheaTable(numGlyphs)
|
||||||
|
}
|
||||||
|
if missingHmtx {
|
||||||
|
defWidths := make([]uint16, numGlyphs)
|
||||||
|
for i := range defWidths {
|
||||||
|
defWidths[i] = 500
|
||||||
|
}
|
||||||
|
newTables["hmtx"] = buildHmtxTable(defWidths)
|
||||||
|
}
|
||||||
|
ascender := int16(800)
|
||||||
|
descender := int16(-200)
|
||||||
|
if hhea, ok := newTables["hhea"]; ok && len(hhea) >= 10 {
|
||||||
|
ascender = int16(binary.BigEndian.Uint16(hhea[4:6]))
|
||||||
|
descender = int16(binary.BigEndian.Uint16(hhea[6:8]))
|
||||||
|
}
|
||||||
|
if missingOS2 {
|
||||||
|
newTables["OS/2"] = buildOS2TableWithMetrics(ascender, descender)
|
||||||
|
}
|
||||||
|
var mapping map[rune]uint16
|
||||||
|
if missingCmap && fixCmap {
|
||||||
|
mapping = make(map[rune]uint16)
|
||||||
|
for i := uint16(0); i < numGlyphs; i++ {
|
||||||
|
mapping[rune(i)] = i
|
||||||
|
}
|
||||||
|
newTables["cmap"] = buildCmapTable(numGlyphs, mapping)
|
||||||
|
}
|
||||||
|
if missingName && fixName {
|
||||||
|
newTables["name"] = buildNameTable()
|
||||||
|
}
|
||||||
|
if missingPost {
|
||||||
|
newTables["post"] = buildPostTable()
|
||||||
|
}
|
||||||
|
finalData, err := serializeOTF(newTables)
|
||||||
|
if err != nil {
|
||||||
|
return false, data, nil, missingCmap, err
|
||||||
|
}
|
||||||
|
return true, finalData, mapping, missingCmap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasUsableCmap 检查是否存在可用的 cmap 子表
|
||||||
|
// 入参: data cmap表数据
|
||||||
|
// 返回: bool 是否可用
|
||||||
|
func hasUsableCmap(data []byte) bool {
|
||||||
|
if len(data) < 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
numTables := binary.BigEndian.Uint16(data[2:4])
|
||||||
|
pos := 4
|
||||||
|
for i := 0; i < int(numTables); i++ {
|
||||||
|
if len(data) < pos+8 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
platformID := binary.BigEndian.Uint16(data[pos : pos+2])
|
||||||
|
if platformID == 0 || platformID == 3 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
pos += 8
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
// 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 (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// align4 计算 4 字节对齐后的长度
|
||||||
|
// 入参: n 原始长度
|
||||||
|
// 返回: uint32 对齐后的长度
|
||||||
|
func align4(n uint32) uint32 {
|
||||||
|
return (n + 3) & ^uint32(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// calcTableChecksum 计算字体表校验和
|
||||||
|
// 入参: data 表数据
|
||||||
|
// 返回: uint32 校验和
|
||||||
|
func calcTableChecksum(data []byte) uint32 {
|
||||||
|
var sum uint32
|
||||||
|
length := len(data)
|
||||||
|
for i := 0; i < length; i += 4 {
|
||||||
|
if i+4 <= length {
|
||||||
|
sum += binary.BigEndian.Uint32(data[i : i+4])
|
||||||
|
} else {
|
||||||
|
var val uint32
|
||||||
|
rem := data[i:]
|
||||||
|
for j, b := range rem {
|
||||||
|
val |= uint32(b) << (24 - 8*j)
|
||||||
|
}
|
||||||
|
sum += val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sum
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkMissingCmap 检查是否缺失 cmap 表
|
||||||
|
// 入参: data 字体数据
|
||||||
|
// 返回: bool 是否缺失
|
||||||
|
func checkMissingCmap(data []byte) bool {
|
||||||
|
if len(data) < 12 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
numTables := binary.BigEndian.Uint16(data[4:6])
|
||||||
|
for i := 0; i < int(numTables); i++ {
|
||||||
|
pos := 12 + i*16
|
||||||
|
if len(data) < pos+4 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
tag := string(data[pos : pos+4])
|
||||||
|
if tag == "cmap" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildHeadTable 构建 head 表
|
||||||
|
// 入参: unitsPerEm 每em单位数
|
||||||
|
// 返回: []byte head表数据
|
||||||
|
func buildHeadTable(unitsPerEm uint16) []byte {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(1))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(0x5F0F3CF5))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, unitsPerEm)
|
||||||
|
binary.Write(buf, binary.BigEndian, int64(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int64(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(-500))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(1000))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(1000))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(2))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildHheaTable 构建 hhea 表
|
||||||
|
// 入参: numGlyphs 字形数量
|
||||||
|
// 返回: []byte hhea表数据
|
||||||
|
func buildHheaTable(numGlyphs uint16) []byte {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(1))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(800))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(-200))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(1000))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(1000))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(numGlyphs))
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildMaxpTable 构建 maxp 表
|
||||||
|
// 入参: numGlyphs 字形数量
|
||||||
|
// 返回: []byte maxp表数据
|
||||||
|
func buildMaxpTable(numGlyphs uint16) []byte {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(0x00005000))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(numGlyphs))
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildOS2Table 构建 OS/2 表 (使用默认 Metrics)
|
||||||
|
// 返回: []byte OS/2表数据
|
||||||
|
func buildOS2Table() []byte {
|
||||||
|
return buildOS2TableWithMetrics(800, -200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildOS2TableWithMetrics 构建 OS/2 表
|
||||||
|
// 入参: ascender 上升部, descender 下降部
|
||||||
|
// 返回: []byte OS/2表数据
|
||||||
|
func buildOS2TableWithMetrics(ascender, descender int16) []byte {
|
||||||
|
os2 := new(bytes.Buffer)
|
||||||
|
binary.Write(os2, binary.BigEndian, uint16(3))
|
||||||
|
binary.Write(os2, binary.BigEndian, int16(500))
|
||||||
|
binary.Write(os2, binary.BigEndian, uint16(400))
|
||||||
|
binary.Write(os2, binary.BigEndian, uint16(5))
|
||||||
|
binary.Write(os2, binary.BigEndian, uint16(0))
|
||||||
|
binary.Write(os2, binary.BigEndian, int16(250))
|
||||||
|
binary.Write(os2, binary.BigEndian, int16(250))
|
||||||
|
binary.Write(os2, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(os2, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(os2, binary.BigEndian, int16(250))
|
||||||
|
binary.Write(os2, binary.BigEndian, int16(250))
|
||||||
|
binary.Write(os2, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(os2, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(os2, binary.BigEndian, int16(50))
|
||||||
|
binary.Write(os2, binary.BigEndian, int16(250))
|
||||||
|
binary.Write(os2, binary.BigEndian, int16(0))
|
||||||
|
os2.Write(make([]byte, 10))
|
||||||
|
binary.Write(os2, binary.BigEndian, uint32(0))
|
||||||
|
binary.Write(os2, binary.BigEndian, uint32(0))
|
||||||
|
binary.Write(os2, binary.BigEndian, uint32(0))
|
||||||
|
binary.Write(os2, binary.BigEndian, uint32(0))
|
||||||
|
os2.WriteString("PfEd")
|
||||||
|
binary.Write(os2, binary.BigEndian, uint16(0x0040))
|
||||||
|
binary.Write(os2, binary.BigEndian, uint16(0))
|
||||||
|
binary.Write(os2, binary.BigEndian, uint16(255))
|
||||||
|
binary.Write(os2, binary.BigEndian, ascender)
|
||||||
|
binary.Write(os2, binary.BigEndian, descender)
|
||||||
|
binary.Write(os2, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(os2, binary.BigEndian, uint16(ascender))
|
||||||
|
if descender < 0 {
|
||||||
|
binary.Write(os2, binary.BigEndian, uint16(-descender))
|
||||||
|
} else {
|
||||||
|
binary.Write(os2, binary.BigEndian, uint16(descender))
|
||||||
|
}
|
||||||
|
binary.Write(os2, binary.BigEndian, uint32(0))
|
||||||
|
binary.Write(os2, binary.BigEndian, uint32(0))
|
||||||
|
binary.Write(os2, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(os2, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(os2, binary.BigEndian, uint16(0))
|
||||||
|
binary.Write(os2, binary.BigEndian, uint16(0))
|
||||||
|
binary.Write(os2, binary.BigEndian, uint16(0))
|
||||||
|
return os2.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildNameTable 构建 name 表 (最小化)
|
||||||
|
// 返回: []byte name表数据
|
||||||
|
func buildNameTable() []byte {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(6))
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildPostTable 构建 post 表 (版本 3.0, 无字形名称)
|
||||||
|
// 返回: []byte post表数据
|
||||||
|
func buildPostTable() []byte {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(0x00030000))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(0))
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildHmtxTable 构建 hmtx 表
|
||||||
|
// 入参: widths 宽度列表
|
||||||
|
// 返回: []byte hmtx表数据
|
||||||
|
func buildHmtxTable(widths []uint16) []byte {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
for _, w := range widths {
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(w))
|
||||||
|
binary.Write(buf, binary.BigEndian, int16(0))
|
||||||
|
}
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmapSegment cmap 表段结构
|
||||||
|
// 字段: start 开始字符, end 结束字符, delta 增量, offset 偏移
|
||||||
|
type cmapSegment struct {
|
||||||
|
start, end uint16
|
||||||
|
delta int16
|
||||||
|
offset uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildCmapTable 构建 cmap 表 (Format 4)
|
||||||
|
// 入参: numGlyphs 字形数量, mapping 字符映射
|
||||||
|
// 返回: []byte cmap表数据
|
||||||
|
func buildCmapTable(numGlyphs uint16, mapping map[rune]uint16) []byte {
|
||||||
|
var segs []cmapSegment
|
||||||
|
if mapping == nil {
|
||||||
|
end := uint16(0xFFFF)
|
||||||
|
if numGlyphs > 0 {
|
||||||
|
end = numGlyphs - 1
|
||||||
|
}
|
||||||
|
segs = append(segs, cmapSegment{start: 0, end: end, delta: 0, offset: 0})
|
||||||
|
} else {
|
||||||
|
var codes []int
|
||||||
|
for r := range mapping {
|
||||||
|
if r <= 0xFFFF {
|
||||||
|
codes = append(codes, int(r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Ints(codes)
|
||||||
|
if len(codes) > 0 {
|
||||||
|
start := codes[0]
|
||||||
|
prev := start
|
||||||
|
for i := 1; i < len(codes); i++ {
|
||||||
|
curr := codes[i]
|
||||||
|
if curr != prev+1 {
|
||||||
|
segs = append(segs, cmapSegment{start: uint16(start), end: uint16(prev), delta: 0, offset: 0})
|
||||||
|
start = curr
|
||||||
|
}
|
||||||
|
prev = curr
|
||||||
|
}
|
||||||
|
segs = append(segs, cmapSegment{start: uint16(start), end: uint16(prev), delta: 0, offset: 0})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
segs = append(segs, cmapSegment{start: 0xFFFF, end: 0xFFFF, delta: 1, offset: 0})
|
||||||
|
segCount := uint16(len(segs))
|
||||||
|
searchRange := uint16(1)
|
||||||
|
entrySelector := uint16(0)
|
||||||
|
for searchRange*2 <= segCount {
|
||||||
|
searchRange *= 2
|
||||||
|
entrySelector++
|
||||||
|
}
|
||||||
|
searchRange *= 2
|
||||||
|
rangeShift := segCount*2 - searchRange
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(4))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(0))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(segCount*2))
|
||||||
|
binary.Write(buf, binary.BigEndian, searchRange)
|
||||||
|
binary.Write(buf, binary.BigEndian, entrySelector)
|
||||||
|
binary.Write(buf, binary.BigEndian, rangeShift)
|
||||||
|
var endCounts, startCounts, idDeltas, idRangeOffsets []uint16
|
||||||
|
var glyphIds []uint16
|
||||||
|
for _, s := range segs {
|
||||||
|
endCounts = append(endCounts, s.end)
|
||||||
|
startCounts = append(startCounts, s.start)
|
||||||
|
if mapping == nil {
|
||||||
|
idDeltas = append(idDeltas, 0)
|
||||||
|
idRangeOffsets = append(idRangeOffsets, 0)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s.start == 0xFFFF {
|
||||||
|
idDeltas = append(idDeltas, 1)
|
||||||
|
idRangeOffsets = append(idRangeOffsets, 0)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
idDeltas = append(idDeltas, 0)
|
||||||
|
currentGlyphIdx := len(glyphIds)
|
||||||
|
for c := int(s.start); c <= int(s.end); c++ {
|
||||||
|
gid := mapping[rune(c)]
|
||||||
|
glyphIds = append(glyphIds, gid)
|
||||||
|
}
|
||||||
|
offset := (int(segCount)-int(len(idRangeOffsets))-1)*2 + 2 + currentGlyphIdx*2
|
||||||
|
idRangeOffsets = append(idRangeOffsets, uint16(offset))
|
||||||
|
}
|
||||||
|
for _, v := range endCounts {
|
||||||
|
binary.Write(buf, binary.BigEndian, v)
|
||||||
|
}
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(0))
|
||||||
|
for _, v := range startCounts {
|
||||||
|
binary.Write(buf, binary.BigEndian, v)
|
||||||
|
}
|
||||||
|
for _, v := range idDeltas {
|
||||||
|
binary.Write(buf, binary.BigEndian, v)
|
||||||
|
}
|
||||||
|
for _, v := range idRangeOffsets {
|
||||||
|
binary.Write(buf, binary.BigEndian, v)
|
||||||
|
}
|
||||||
|
for _, v := range glyphIds {
|
||||||
|
binary.Write(buf, binary.BigEndian, v)
|
||||||
|
}
|
||||||
|
data := buf.Bytes()
|
||||||
|
binary.BigEndian.PutUint16(data[2:4], uint16(len(data)))
|
||||||
|
mainBuf := new(bytes.Buffer)
|
||||||
|
binary.Write(mainBuf, binary.BigEndian, uint16(0))
|
||||||
|
binary.Write(mainBuf, binary.BigEndian, uint16(1))
|
||||||
|
binary.Write(mainBuf, binary.BigEndian, uint16(3))
|
||||||
|
binary.Write(mainBuf, binary.BigEndian, uint16(1))
|
||||||
|
binary.Write(mainBuf, binary.BigEndian, uint32(12))
|
||||||
|
mainBuf.Write(data)
|
||||||
|
return mainBuf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// otfTableRecord OTF 表记录结构
|
||||||
|
// 字段: tag 标签, checksum 校验和, offset 偏移, length 长度, data 数据
|
||||||
|
type otfTableRecord struct {
|
||||||
|
tag string
|
||||||
|
checksum uint32
|
||||||
|
offset int
|
||||||
|
length int
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// serializeOTF 序列化 OpenType 字体结构
|
||||||
|
// 入参: tables 表数据映射
|
||||||
|
// 返回: []byte 完整字体数据, error 错误信息
|
||||||
|
func serializeOTF(tables map[string][]byte) ([]byte, error) {
|
||||||
|
numTables := uint16(len(tables))
|
||||||
|
entrySelector := 0
|
||||||
|
for 1<<(entrySelector+1) <= int(numTables) {
|
||||||
|
entrySelector++
|
||||||
|
}
|
||||||
|
searchRange := 1 << (entrySelector + 4)
|
||||||
|
rangeShift := int(numTables)*16 - searchRange
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
if _, ok := tables["CFF "]; ok {
|
||||||
|
buf.WriteString("OTTO")
|
||||||
|
} else {
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(0x00010000))
|
||||||
|
}
|
||||||
|
binary.Write(buf, binary.BigEndian, numTables)
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(searchRange))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(entrySelector))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint16(rangeShift))
|
||||||
|
var tags []string
|
||||||
|
for t := range tables {
|
||||||
|
tags = append(tags, t)
|
||||||
|
}
|
||||||
|
sort.Strings(tags)
|
||||||
|
headerSize := 12 + 16*int(numTables)
|
||||||
|
offset := headerSize
|
||||||
|
var records []otfTableRecord
|
||||||
|
for _, tag := range tags {
|
||||||
|
data := tables[tag]
|
||||||
|
pad := (4 - (len(data) % 4)) % 4
|
||||||
|
padded := make([]byte, len(data)+pad)
|
||||||
|
copy(padded, data)
|
||||||
|
cs := calcTableChecksum(padded)
|
||||||
|
records = append(records, otfTableRecord{tag, cs, offset, len(data), padded})
|
||||||
|
offset += len(padded)
|
||||||
|
}
|
||||||
|
for _, r := range records {
|
||||||
|
buf.WriteString(r.tag)
|
||||||
|
binary.Write(buf, binary.BigEndian, r.checksum)
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(r.offset))
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(r.length))
|
||||||
|
}
|
||||||
|
for _, r := range records {
|
||||||
|
buf.Write(r.data)
|
||||||
|
}
|
||||||
|
fullData := buf.Bytes()
|
||||||
|
for _, r := range records {
|
||||||
|
if r.tag == "head" {
|
||||||
|
adjOffset := r.offset + 8
|
||||||
|
if adjOffset+4 <= len(fullData) {
|
||||||
|
binary.BigEndian.PutUint32(fullData[adjOffset:], 0)
|
||||||
|
cs := calcTableChecksum(fullData)
|
||||||
|
checksumAdj := 0xB1B0AFBA - cs
|
||||||
|
binary.BigEndian.PutUint32(fullData[adjOffset:], checksumAdj)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fullData, nil
|
||||||
|
}
|
||||||
@@ -63,6 +63,20 @@ func NewMatrix(s string) Matrix {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multiply 矩阵乘法 (m * o)
|
||||||
|
// 入参: o 右侧矩阵
|
||||||
|
// 返回: Matrix 结果矩阵
|
||||||
|
func (m Matrix) Multiply(o Matrix) Matrix {
|
||||||
|
return Matrix{
|
||||||
|
a: m.a*o.a + m.c*o.b,
|
||||||
|
b: m.b*o.a + m.d*o.b,
|
||||||
|
c: m.a*o.c + m.c*o.d,
|
||||||
|
d: m.b*o.c + m.d*o.d,
|
||||||
|
e: m.a*o.e + m.c*o.f + m.e,
|
||||||
|
f: m.b*o.e + m.d*o.f + m.f,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Transform 应用变换矩阵
|
// Transform 应用变换矩阵
|
||||||
// 入参: x X坐标, y Y坐标
|
// 入参: x X坐标, y Y坐标
|
||||||
// 返回: float64 变换后X, float64 变换后Y
|
// 返回: float64 变换后X, float64 变换后Y
|
||||||
|
|||||||
+57
-22
@@ -38,41 +38,66 @@ type Content struct {
|
|||||||
|
|
||||||
// Layer 图层
|
// Layer 图层
|
||||||
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"`
|
||||||
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clips 裁剪区域集合
|
||||||
|
type Clips struct {
|
||||||
|
Clip []Clip `xml:"Clip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clip 裁剪
|
||||||
|
type Clip struct {
|
||||||
|
Area []ClipArea `xml:"Area"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClipArea 裁剪区域
|
||||||
|
type ClipArea struct {
|
||||||
|
Path []PathObject `xml:"Path"`
|
||||||
|
Text []TextObject `xml:"Text"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextObject 文本对象
|
// TextObject 文本对象
|
||||||
type TextObject struct {
|
type TextObject struct {
|
||||||
ID string `xml:"ID,attr"`
|
ID string `xml:"ID,attr"`
|
||||||
Boundary string `xml:"Boundary,attr"`
|
Boundary string `xml:"Boundary,attr"`
|
||||||
DrawParam string `xml:"DrawParam,attr"`
|
DrawParam string `xml:"DrawParam,attr"`
|
||||||
Font string `xml:"Font,attr"`
|
LineWidth float64 `xml:"LineWidth,attr"`
|
||||||
Size float64 `xml:"Size,attr"`
|
Font string `xml:"Font,attr"`
|
||||||
Weight int `xml:"Weight,attr"`
|
Size float64 `xml:"Size,attr"`
|
||||||
Italic bool `xml:"Italic,attr"`
|
Weight int `xml:"Weight,attr"`
|
||||||
Decoration string `xml:"Decoration,attr"`
|
Italic bool `xml:"Italic,attr"`
|
||||||
HScale float64 `xml:"HScale,attr"`
|
Decoration string `xml:"Decoration,attr"`
|
||||||
CTM string `xml:"CTM,attr"`
|
HScale float64 `xml:"HScale,attr"`
|
||||||
FillColor *FillColor `xml:"FillColor"`
|
VScale float64 `xml:"VScale,attr"`
|
||||||
TextCode []TextCode `xml:"TextCode"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FillColor 填充颜色
|
// FillColor 填充颜色
|
||||||
type FillColor struct {
|
type FillColor struct {
|
||||||
Value string `xml:"Value,attr"`
|
Value string `xml:"Value,attr"`
|
||||||
|
Alpha *int `xml:"Alpha,attr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextCode 文本内容节点
|
// TextCode 文本内容节点
|
||||||
type TextCode struct {
|
type TextCode struct {
|
||||||
X float64 `xml:"X,attr"`
|
X string `xml:"X,attr"`
|
||||||
Y float64 `xml:"Y,attr"`
|
Y string `xml:"Y,attr"`
|
||||||
DeltaX string `xml:"DeltaX,attr"`
|
DeltaX string `xml:"DeltaX,attr"`
|
||||||
DeltaY string `xml:"DeltaY,attr"`
|
DeltaY string `xml:"DeltaY,attr"`
|
||||||
Value string `xml:",chardata"`
|
Index string `xml:"Index,attr"`
|
||||||
|
Value string `xml:",chardata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PathObject 路径对象
|
// PathObject 路径对象
|
||||||
@@ -81,15 +106,24 @@ type PathObject struct {
|
|||||||
Boundary string `xml:"Boundary,attr"`
|
Boundary string `xml:"Boundary,attr"`
|
||||||
DrawParam string `xml:"DrawParam,attr"`
|
DrawParam string `xml:"DrawParam,attr"`
|
||||||
LineWidth float64 `xml:"LineWidth,attr"`
|
LineWidth float64 `xml:"LineWidth,attr"`
|
||||||
|
Join string `xml:"Join,attr"`
|
||||||
|
Cap string `xml:"Cap,attr"`
|
||||||
|
DashOffset float64 `xml:"DashOffset,attr"`
|
||||||
|
DashPattern string `xml:"DashPattern,attr"`
|
||||||
|
MiterLimit float64 `xml:"MiterLimit,attr"`
|
||||||
CTM string `xml:"CTM,attr"`
|
CTM string `xml:"CTM,attr"`
|
||||||
|
Stroke *bool `xml:"Stroke,attr"`
|
||||||
|
Fill *bool `xml:"Fill,attr"`
|
||||||
StrokeColor *StrokeColor `xml:"StrokeColor"`
|
StrokeColor *StrokeColor `xml:"StrokeColor"`
|
||||||
FillColor *FillColor `xml:"FillColor"`
|
FillColor *FillColor `xml:"FillColor"`
|
||||||
AbbreviatedData string `xml:"AbbreviatedData"`
|
AbbreviatedData string `xml:"AbbreviatedData"`
|
||||||
|
Clips *Clips `xml:"Clips"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StrokeColor 勾边颜色
|
// StrokeColor 勾边颜色
|
||||||
type StrokeColor struct {
|
type StrokeColor struct {
|
||||||
Value string `xml:"Value,attr"`
|
Value string `xml:"Value,attr"`
|
||||||
|
Alpha *int `xml:"Alpha,attr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageObject 图片对象
|
// ImageObject 图片对象
|
||||||
@@ -98,4 +132,5 @@ type ImageObject struct {
|
|||||||
Boundary string `xml:"Boundary,attr"`
|
Boundary string `xml:"Boundary,attr"`
|
||||||
ResourceID string `xml:"ResourceID,attr"`
|
ResourceID string `xml:"ResourceID,attr"`
|
||||||
CTM string `xml:"CTM,attr"`
|
CTM string `xml:"CTM,attr"`
|
||||||
|
Clips *Clips `xml:"Clips"`
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-10
@@ -25,16 +25,17 @@ import (
|
|||||||
|
|
||||||
// Reader OFD文件阅读器
|
// Reader OFD文件阅读器
|
||||||
type Reader struct {
|
type Reader struct {
|
||||||
Path string
|
Path string
|
||||||
Zip *zip.Reader
|
Zip *zip.Reader
|
||||||
Closer io.Closer
|
Closer io.Closer
|
||||||
OFD *OFD
|
OFD *OFD
|
||||||
RootDir string
|
RootDir string
|
||||||
ResMap map[string]string
|
ResMap map[string]string
|
||||||
fontCache map[string]*Font
|
fontCache map[string]*Font
|
||||||
drawParamCache map[string]*DrawParam
|
drawParamCache map[string]*DrawParam
|
||||||
doc *Document
|
compositeGraphicUnitCache map[string]*CompositeGraphicUnit
|
||||||
Stamps map[string][]Stamp
|
doc *Document
|
||||||
|
Stamps map[string][]Stamp
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close 关闭阅读器
|
// Close 关闭阅读器
|
||||||
@@ -61,6 +62,7 @@ func (r *Reader) initRoot() error {
|
|||||||
r.ResMap = make(map[string]string)
|
r.ResMap = make(map[string]string)
|
||||||
r.fontCache = make(map[string]*Font)
|
r.fontCache = make(map[string]*Font)
|
||||||
r.drawParamCache = make(map[string]*DrawParam)
|
r.drawParamCache = make(map[string]*DrawParam)
|
||||||
|
r.compositeGraphicUnitCache = make(map[string]*CompositeGraphicUnit)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,6 +187,10 @@ func (r *Reader) loadRes(resPath string) {
|
|||||||
dp := &res.DrawParams.DrawParam[i]
|
dp := &res.DrawParams.DrawParam[i]
|
||||||
r.drawParamCache[dp.ID] = dp
|
r.drawParamCache[dp.ID] = dp
|
||||||
}
|
}
|
||||||
|
for i := range res.CompositeGraphicUnits.CompositeGraphicUnit {
|
||||||
|
cgu := &res.CompositeGraphicUnits.CompositeGraphicUnit[i]
|
||||||
|
r.compositeGraphicUnitCache[cgu.ID] = cgu
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageContent 获取页面内容
|
// PageContent 获取页面内容
|
||||||
|
|||||||
+390
-107
@@ -21,6 +21,7 @@ import (
|
|||||||
"image/color"
|
"image/color"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"math"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -34,13 +35,15 @@ import (
|
|||||||
|
|
||||||
// Renderer 渲染器实现
|
// Renderer 渲染器实现
|
||||||
type Renderer struct {
|
type Renderer struct {
|
||||||
Reader *Reader
|
Reader *Reader
|
||||||
DPI float64
|
DPI float64
|
||||||
fontFamily *canvas.FontFamily
|
fontFamily *canvas.FontFamily
|
||||||
DrawParams map[string]*DrawParam
|
DrawParams map[string]*DrawParam
|
||||||
FontMap map[string]*canvas.FontFamily
|
CompositeGraphicUnits map[string]*CompositeGraphicUnit
|
||||||
fontDirs []string
|
FontMap map[string]*canvas.FontFamily
|
||||||
fontFS []fs.FS
|
FontGIDMap map[string]map[uint16]rune
|
||||||
|
fontDirs []string
|
||||||
|
fontFS []fs.FS
|
||||||
}
|
}
|
||||||
|
|
||||||
// RendererOption 渲染器配置选项
|
// RendererOption 渲染器配置选项
|
||||||
@@ -103,7 +106,7 @@ func (r *Renderer) RenderPageToContext(ctx *canvas.Context, page *PageContent) e
|
|||||||
}
|
}
|
||||||
if page.Content.Layer != nil {
|
if page.Content.Layer != nil {
|
||||||
for _, layer := range page.Content.Layer {
|
for _, layer := range page.Content.Layer {
|
||||||
r.renderLayer(ctx, layer, pageH, nil, nil, 0)
|
r.renderLayer(ctx, layer, pageH, nil, nil, 0, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(page.Template) > 0 && r.Reader.doc != nil {
|
if len(page.Template) > 0 && r.Reader.doc != nil {
|
||||||
@@ -237,36 +240,81 @@ func (r *Renderer) renderTemplate(ctx *canvas.Context, templateID string, pageH
|
|||||||
}
|
}
|
||||||
if tplContent.Content.Layer != nil {
|
if tplContent.Content.Layer != nil {
|
||||||
for _, layer := range tplContent.Content.Layer {
|
for _, layer := range tplContent.Content.Layer {
|
||||||
r.renderLayer(ctx, layer, pageH, nil, nil, 0)
|
r.renderLayer(ctx, layer, pageH, nil, nil, 0, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderLayer 渲染图层
|
// renderLayer 渲染图层
|
||||||
// 入参: ctx 画布上下文, layer 图层对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽
|
// 入参: ctx 画布上下文, layer 图层对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM
|
||||||
func (r *Renderer) renderLayer(ctx *canvas.Context, layer Layer, pageH float64, defaultFill color.Color, defaultStroke color.Color, defaultLW float64) {
|
func (r *Renderer) renderLayer(ctx *canvas.Context, layer Layer, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix) {
|
||||||
if layer.DrawParam != "" {
|
if layer.DrawParam != "" {
|
||||||
if dp := r.getDrawParam(layer.DrawParam, nil); dp != nil {
|
if dp := r.getDrawParam(layer.DrawParam, nil); dp != nil {
|
||||||
if dp.LineWidth > 0 {
|
if dp.LineWidth > 0 {
|
||||||
defaultLW = dp.LineWidth
|
defaultLW = dp.LineWidth
|
||||||
}
|
}
|
||||||
if dp.FillColor != nil {
|
if dp.FillColor != nil {
|
||||||
defaultFill = parseColor(dp.FillColor.Value)
|
defaultFill = parseColorWithAlpha(dp.FillColor.Value, dp.FillColor.Alpha)
|
||||||
}
|
}
|
||||||
if dp.StrokeColor != nil {
|
if dp.StrokeColor != nil {
|
||||||
defaultStroke = parseColor(dp.StrokeColor.Value)
|
defaultStroke = parseColorWithAlpha(dp.StrokeColor.Value, dp.StrokeColor.Alpha)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, imgObj := range layer.ImageObject {
|
for _, textObj := range layer.TextObject {
|
||||||
r.renderImage(ctx, imgObj, pageH)
|
r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, parentCTM)
|
||||||
}
|
}
|
||||||
for _, pathObj := range layer.PathObject {
|
for _, pathObj := range layer.PathObject {
|
||||||
r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW)
|
r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, parentCTM)
|
||||||
}
|
}
|
||||||
for _, textObj := range layer.TextObject {
|
for _, imgObj := range layer.ImageObject {
|
||||||
r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke)
|
r.renderImage(ctx, imgObj, pageH, parentCTM)
|
||||||
}
|
}
|
||||||
|
for _, cgu := range layer.CompositeGraphicUnit {
|
||||||
|
r.renderCompositeGraphicUnit(ctx, cgu, pageH, defaultFill, defaultStroke, defaultLW, parentCTM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderCompositeGraphicUnit 渲染复合图元
|
||||||
|
// 入参: ctx 画布上下文, cgu 复合图元对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM
|
||||||
|
func (r *Renderer) renderCompositeGraphicUnit(ctx *canvas.Context, cgu CompositeGraphicUnit, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix) {
|
||||||
|
ctx.Push()
|
||||||
|
currentCTM := NewMatrix(cgu.CTM)
|
||||||
|
if parentCTM != nil {
|
||||||
|
currentCTM = parentCTM.Multiply(currentCTM)
|
||||||
|
}
|
||||||
|
r.applyClips(ctx, cgu.Clips, pageH, ¤tCTM)
|
||||||
|
if cgu.ResourceID != "" {
|
||||||
|
if ref, ok := r.CompositeGraphicUnits[cgu.ResourceID]; ok {
|
||||||
|
r.renderCompositeGraphicUnit(ctx, *ref, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cgu.DrawParam != "" {
|
||||||
|
if dp := r.getDrawParam(cgu.DrawParam, nil); dp != nil {
|
||||||
|
if dp.LineWidth > 0 {
|
||||||
|
defaultLW = dp.LineWidth
|
||||||
|
}
|
||||||
|
if dp.FillColor != nil {
|
||||||
|
defaultFill = parseColorWithAlpha(dp.FillColor.Value, dp.FillColor.Alpha)
|
||||||
|
}
|
||||||
|
if dp.StrokeColor != nil {
|
||||||
|
defaultStroke = parseColorWithAlpha(dp.StrokeColor.Value, dp.StrokeColor.Alpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, imgObj := range cgu.ImageObject {
|
||||||
|
r.renderImage(ctx, imgObj, pageH, ¤tCTM)
|
||||||
|
}
|
||||||
|
for _, pathObj := range cgu.PathObject {
|
||||||
|
r.renderPath(ctx, pathObj, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM)
|
||||||
|
}
|
||||||
|
for _, textObj := range cgu.TextObject {
|
||||||
|
r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, ¤tCTM)
|
||||||
|
}
|
||||||
|
for _, subCgu := range cgu.CompositeGraphicUnit {
|
||||||
|
r.renderCompositeGraphicUnit(ctx, subCgu, pageH, defaultFill, defaultStroke, defaultLW, ¤tCTM)
|
||||||
|
}
|
||||||
|
ctx.Pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDrawParam 获取绘制参数逻辑
|
// getDrawParam 获取绘制参数逻辑
|
||||||
@@ -290,12 +338,37 @@ func (r *Renderer) getDrawParam(id string, visited map[string]bool) *DrawParam {
|
|||||||
if dp.LineWidth > 0 {
|
if dp.LineWidth > 0 {
|
||||||
merged.LineWidth = dp.LineWidth
|
merged.LineWidth = dp.LineWidth
|
||||||
}
|
}
|
||||||
|
if dp.Join != "" {
|
||||||
|
merged.Join = dp.Join
|
||||||
|
}
|
||||||
|
if dp.Cap != "" {
|
||||||
|
merged.Cap = dp.Cap
|
||||||
|
}
|
||||||
|
if dp.DashPattern != "" {
|
||||||
|
merged.DashPattern = dp.DashPattern
|
||||||
|
merged.DashOffset = dp.DashOffset
|
||||||
|
}
|
||||||
|
if dp.MiterLimit > 0 {
|
||||||
|
merged.MiterLimit = dp.MiterLimit
|
||||||
|
}
|
||||||
if dp.FillColor != nil {
|
if dp.FillColor != nil {
|
||||||
merged.FillColor = dp.FillColor
|
merged.FillColor = dp.FillColor
|
||||||
}
|
}
|
||||||
if dp.StrokeColor != nil {
|
if dp.StrokeColor != nil {
|
||||||
merged.StrokeColor = dp.StrokeColor
|
merged.StrokeColor = dp.StrokeColor
|
||||||
}
|
}
|
||||||
|
if dp.Font != "" {
|
||||||
|
merged.Font = dp.Font
|
||||||
|
}
|
||||||
|
if dp.Size > 0 {
|
||||||
|
merged.Size = dp.Size
|
||||||
|
}
|
||||||
|
if dp.Weight > 0 {
|
||||||
|
merged.Weight = dp.Weight
|
||||||
|
}
|
||||||
|
if dp.Italic {
|
||||||
|
merged.Italic = dp.Italic
|
||||||
|
}
|
||||||
return &merged
|
return &merged
|
||||||
}
|
}
|
||||||
return dp
|
return dp
|
||||||
@@ -318,8 +391,8 @@ func (r *Renderer) initCommon() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// renderImage 渲染图片
|
// renderImage 渲染图片
|
||||||
// 入参: ctx 画布上下文, obj 图片对象, pageH 页面高度
|
// 入参: ctx 画布上下文, obj 图片对象, pageH 页面高度, parentCTM 父级CTM
|
||||||
func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float64) {
|
func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float64, parentCTM *Matrix) {
|
||||||
resPath, ok := r.Reader.ResMap[obj.ResourceID]
|
resPath, ok := r.Reader.ResMap[obj.ResourceID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
@@ -342,9 +415,13 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float
|
|||||||
if obj.CTM == "" {
|
if obj.CTM == "" {
|
||||||
ctm = Matrix{a: box.W, d: box.H}
|
ctm = Matrix{a: box.W, d: box.H}
|
||||||
}
|
}
|
||||||
|
if parentCTM != nil {
|
||||||
|
ctm = parentCTM.Multiply(ctm)
|
||||||
|
}
|
||||||
tx, ty := ctm.Transform(0, 1)
|
tx, ty := ctm.Transform(0, 1)
|
||||||
canvasX, canvasY := tx+box.X, pageH-(ty+box.Y)
|
canvasX, canvasY := tx+box.X, pageH-(ty+box.Y)
|
||||||
ctx.Push()
|
ctx.Push()
|
||||||
|
r.applyClips(ctx, obj.Clips, pageH, &ctm)
|
||||||
ctx.Translate(canvasX, canvasY)
|
ctx.Translate(canvasX, canvasY)
|
||||||
ctx.Scale(ctm.a/imgW, ctm.d/imgH)
|
ctx.Scale(ctm.a/imgW, ctm.d/imgH)
|
||||||
ctx.DrawImage(0, 0, img, canvas.DPMM(1.0))
|
ctx.DrawImage(0, 0, img, canvas.DPMM(1.0))
|
||||||
@@ -352,34 +429,47 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float
|
|||||||
}
|
}
|
||||||
|
|
||||||
// renderPath 渲染路径
|
// renderPath 渲染路径
|
||||||
// 入参: ctx 画布上下文, obj 路径对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽
|
// 入参: ctx 画布上下文, obj 路径对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM
|
||||||
func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64) {
|
func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix) {
|
||||||
ctx.Push()
|
ctx.Push()
|
||||||
bx, by := 0.0, 0.0
|
|
||||||
if obj.Boundary != "" {
|
|
||||||
if box, err := ParseBox(obj.Boundary); err == nil {
|
|
||||||
bx, by = box.X, box.Y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctm := NewMatrix(obj.CTM)
|
ctm := NewMatrix(obj.CTM)
|
||||||
fillColor, strokeColor := defaultFill, defaultStroke
|
if parentCTM != nil {
|
||||||
if strokeColor == nil {
|
ctm = parentCTM.Multiply(ctm)
|
||||||
strokeColor = canvas.Black
|
|
||||||
}
|
}
|
||||||
|
r.applyClips(ctx, obj.Clips, pageH, &ctm)
|
||||||
|
fillColor, strokeColor := defaultFill, defaultStroke
|
||||||
lineWidth := defaultLW
|
lineWidth := defaultLW
|
||||||
if lineWidth == 0 {
|
if lineWidth == 0 {
|
||||||
lineWidth = 0.353
|
lineWidth = 0.353
|
||||||
}
|
}
|
||||||
|
lineCap := canvas.ButtCap
|
||||||
|
lineJoin := canvas.MiterJoin
|
||||||
|
dashOffset := 0.0
|
||||||
|
var dashPattern []float64
|
||||||
if obj.DrawParam != "" {
|
if obj.DrawParam != "" {
|
||||||
if dp := r.getDrawParam(obj.DrawParam, nil); dp != nil {
|
if dp := r.getDrawParam(obj.DrawParam, nil); dp != nil {
|
||||||
if dp.LineWidth > 0 {
|
if dp.LineWidth > 0 {
|
||||||
lineWidth = dp.LineWidth
|
lineWidth = dp.LineWidth
|
||||||
}
|
}
|
||||||
if dp.FillColor != nil {
|
if dp.FillColor != nil {
|
||||||
fillColor = parseColor(dp.FillColor.Value)
|
fillColor = parseColorWithAlpha(dp.FillColor.Value, dp.FillColor.Alpha)
|
||||||
}
|
}
|
||||||
if dp.StrokeColor != nil {
|
if dp.StrokeColor != nil {
|
||||||
strokeColor = parseColor(dp.StrokeColor.Value)
|
strokeColor = parseColorWithAlpha(dp.StrokeColor.Value, dp.StrokeColor.Alpha)
|
||||||
|
}
|
||||||
|
if dp.Cap == "Round" {
|
||||||
|
lineCap = canvas.RoundCap
|
||||||
|
} else if dp.Cap == "Square" {
|
||||||
|
lineCap = canvas.SquareCap
|
||||||
|
}
|
||||||
|
if dp.Join == "Round" {
|
||||||
|
lineJoin = canvas.RoundJoin
|
||||||
|
} else if dp.Join == "Bevel" {
|
||||||
|
lineJoin = canvas.BevelJoin
|
||||||
|
}
|
||||||
|
if dp.DashPattern != "" {
|
||||||
|
dashPattern = parseFloats(dp.DashPattern)
|
||||||
|
dashOffset = dp.DashOffset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -387,64 +477,65 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
|
|||||||
lineWidth = obj.LineWidth
|
lineWidth = obj.LineWidth
|
||||||
}
|
}
|
||||||
if obj.FillColor != nil {
|
if obj.FillColor != nil {
|
||||||
fillColor = parseColor(obj.FillColor.Value)
|
fillColor = parseColorWithAlpha(obj.FillColor.Value, obj.FillColor.Alpha)
|
||||||
}
|
}
|
||||||
if obj.StrokeColor != nil {
|
if obj.StrokeColor != nil {
|
||||||
strokeColor = parseColor(obj.StrokeColor.Value)
|
strokeColor = parseColorWithAlpha(obj.StrokeColor.Value, obj.StrokeColor.Alpha)
|
||||||
}
|
}
|
||||||
p := &canvas.Path{}
|
if obj.Cap != "" {
|
||||||
tokens := strings.Fields(obj.AbbreviatedData)
|
if obj.Cap == "Round" {
|
||||||
for i := 0; i < len(tokens); {
|
lineCap = canvas.RoundCap
|
||||||
cmd := tokens[i]
|
} else if obj.Cap == "Square" {
|
||||||
i++
|
lineCap = canvas.SquareCap
|
||||||
switch cmd {
|
} else {
|
||||||
case "M", "S":
|
lineCap = canvas.ButtCap
|
||||||
if i+1 < len(tokens) {
|
|
||||||
x, _ := strconv.ParseFloat(tokens[i], 64)
|
|
||||||
y, _ := strconv.ParseFloat(tokens[i+1], 64)
|
|
||||||
tx, ty := ctm.Transform(x, y)
|
|
||||||
p.MoveTo(tx+bx, pageH-(ty+by))
|
|
||||||
i += 2
|
|
||||||
}
|
|
||||||
case "L":
|
|
||||||
if i+1 < len(tokens) {
|
|
||||||
x, _ := strconv.ParseFloat(tokens[i], 64)
|
|
||||||
y, _ := strconv.ParseFloat(tokens[i+1], 64)
|
|
||||||
tx, ty := ctm.Transform(x, y)
|
|
||||||
p.LineTo(tx+bx, pageH-(ty+by))
|
|
||||||
i += 2
|
|
||||||
}
|
|
||||||
case "B":
|
|
||||||
if i+5 < len(tokens) {
|
|
||||||
x1, _ := strconv.ParseFloat(tokens[i], 64)
|
|
||||||
y1, _ := strconv.ParseFloat(tokens[i+1], 64)
|
|
||||||
x2, _ := strconv.ParseFloat(tokens[i+2], 64)
|
|
||||||
y2, _ := strconv.ParseFloat(tokens[i+3], 64)
|
|
||||||
x3, _ := strconv.ParseFloat(tokens[i+4], 64)
|
|
||||||
y3, _ := strconv.ParseFloat(tokens[i+5], 64)
|
|
||||||
tx1, ty1 := ctm.Transform(x1, y1)
|
|
||||||
tx2, ty2 := ctm.Transform(x2, y2)
|
|
||||||
tx3, ty3 := ctm.Transform(x3, y3)
|
|
||||||
p.CubeTo(tx1+bx, pageH-(ty1+by), tx2+bx, pageH-(ty2+by), tx3+bx, pageH-(ty3+by))
|
|
||||||
i += 6
|
|
||||||
}
|
|
||||||
case "C":
|
|
||||||
p.Close()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if fillColor != nil {
|
if obj.Join != "" {
|
||||||
|
if obj.Join == "Round" {
|
||||||
|
lineJoin = canvas.RoundJoin
|
||||||
|
} else if obj.Join == "Bevel" {
|
||||||
|
lineJoin = canvas.BevelJoin
|
||||||
|
} else {
|
||||||
|
lineJoin = canvas.MiterJoin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if obj.DashPattern != "" {
|
||||||
|
dashPattern = parseFloats(obj.DashPattern)
|
||||||
|
dashOffset = obj.DashOffset
|
||||||
|
}
|
||||||
|
p := r.buildPath(obj, pageH, ctm)
|
||||||
|
shouldFill := true
|
||||||
|
if obj.Fill != nil {
|
||||||
|
shouldFill = *obj.Fill
|
||||||
|
}
|
||||||
|
if shouldFill && fillColor != nil {
|
||||||
ctx.SetFillColor(fillColor)
|
ctx.SetFillColor(fillColor)
|
||||||
ctx.DrawPath(0, 0, p)
|
ctx.DrawPath(0, 0, p)
|
||||||
}
|
}
|
||||||
ctx.SetStrokeColor(strokeColor)
|
shouldStroke := true
|
||||||
ctx.SetStrokeWidth(lineWidth)
|
if obj.Stroke != nil {
|
||||||
ctx.DrawPath(0, 0, p)
|
shouldStroke = *obj.Stroke
|
||||||
|
}
|
||||||
|
if shouldStroke {
|
||||||
|
if strokeColor == nil {
|
||||||
|
strokeColor = color.Transparent
|
||||||
|
}
|
||||||
|
ctx.SetStrokeColor(strokeColor)
|
||||||
|
ctx.SetStrokeWidth(lineWidth)
|
||||||
|
ctx.SetStrokeCapper(lineCap)
|
||||||
|
ctx.SetStrokeJoiner(lineJoin)
|
||||||
|
if len(dashPattern) > 0 {
|
||||||
|
ctx.SetDashes(dashOffset, dashPattern...)
|
||||||
|
}
|
||||||
|
ctx.DrawPath(0, 0, p)
|
||||||
|
}
|
||||||
ctx.Pop()
|
ctx.Pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderText 渲染文本
|
// renderText 渲染文本
|
||||||
// 入参: ctx 画布上下文, obj 文本对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色
|
// 入参: ctx 画布上下文, obj 文本对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, parentCTM 父级CTM
|
||||||
func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64, defaultFill, defaultStroke color.Color) {
|
func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64, defaultFill, defaultStroke color.Color, parentCTM *Matrix) {
|
||||||
ctx.Push()
|
ctx.Push()
|
||||||
bx, by := 0.0, 0.0
|
bx, by := 0.0, 0.0
|
||||||
if obj.Boundary != "" {
|
if obj.Boundary != "" {
|
||||||
@@ -453,34 +544,58 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctm := NewMatrix(obj.CTM)
|
ctm := NewMatrix(obj.CTM)
|
||||||
|
if parentCTM != nil {
|
||||||
|
ctm = parentCTM.Multiply(ctm)
|
||||||
|
}
|
||||||
|
r.applyClips(ctx, obj.Clips, pageH, &ctm)
|
||||||
|
var dp *DrawParam
|
||||||
|
if obj.DrawParam != "" {
|
||||||
|
dp = r.getDrawParam(obj.DrawParam, nil)
|
||||||
|
}
|
||||||
sizeMM := obj.Size
|
sizeMM := obj.Size
|
||||||
|
if sizeMM == 0 && dp != nil && dp.Size > 0 {
|
||||||
|
sizeMM = dp.Size
|
||||||
|
}
|
||||||
if sizeMM == 0 {
|
if sizeMM == 0 {
|
||||||
sizeMM = 3.5
|
sizeMM = 3.5
|
||||||
}
|
}
|
||||||
|
if obj.VScale != 0 {
|
||||||
|
sizeMM *= obj.VScale
|
||||||
|
}
|
||||||
if scale := ctm.YScale(); scale > 0 {
|
if scale := ctm.YScale(); scale > 0 {
|
||||||
sizeMM *= scale
|
sizeMM *= scale
|
||||||
}
|
}
|
||||||
sizePt := sizeMM * 2.83465
|
sizePt := sizeMM * 2.83465
|
||||||
textColor := defaultFill
|
fillColor := defaultFill
|
||||||
if textColor == nil {
|
if fillColor == nil {
|
||||||
textColor = canvas.Black
|
fillColor = canvas.Black
|
||||||
}
|
}
|
||||||
if obj.DrawParam != "" {
|
if dp != nil && dp.FillColor != nil {
|
||||||
if dp := r.getDrawParam(obj.DrawParam, nil); dp != nil && dp.FillColor != nil {
|
fillColor = parseColorWithAlpha(dp.FillColor.Value, dp.FillColor.Alpha)
|
||||||
textColor = parseColor(dp.FillColor.Value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if obj.FillColor != nil {
|
if obj.FillColor != nil {
|
||||||
textColor = parseColor(obj.FillColor.Value)
|
fillColor = parseColorWithAlpha(obj.FillColor.Value, obj.FillColor.Alpha)
|
||||||
}
|
}
|
||||||
fontStyle := canvas.FontRegular
|
fontStyle := canvas.FontRegular
|
||||||
if obj.Weight >= 700 {
|
weight := obj.Weight
|
||||||
|
if weight == 0 && dp != nil && dp.Weight > 0 {
|
||||||
|
weight = dp.Weight
|
||||||
|
}
|
||||||
|
if weight >= 700 {
|
||||||
fontStyle |= canvas.FontBold
|
fontStyle |= canvas.FontBold
|
||||||
}
|
}
|
||||||
if obj.Italic {
|
italic := obj.Italic
|
||||||
|
if !italic && dp != nil && dp.Italic {
|
||||||
|
italic = true
|
||||||
|
}
|
||||||
|
if italic {
|
||||||
fontStyle |= canvas.FontItalic
|
fontStyle |= canvas.FontItalic
|
||||||
}
|
}
|
||||||
if of, ok := r.Reader.fontCache[obj.Font]; ok {
|
fontID := obj.Font
|
||||||
|
if fontID == "" && dp != nil && dp.Font != "" {
|
||||||
|
fontID = dp.Font
|
||||||
|
}
|
||||||
|
if of, ok := r.Reader.fontCache[fontID]; ok {
|
||||||
if of.Bold {
|
if of.Bold {
|
||||||
fontStyle |= canvas.FontBold
|
fontStyle |= canvas.FontBold
|
||||||
}
|
}
|
||||||
@@ -488,20 +603,41 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
|
|||||||
fontStyle |= canvas.FontItalic
|
fontStyle |= canvas.FontItalic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ff := r.loadFont(obj.Font)
|
ff := r.loadFont(fontID)
|
||||||
face := ff.Face(sizePt, textColor, fontStyle, canvas.FontNormal)
|
if ff == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
face := ff.Face(sizePt, fillColor, fontStyle, canvas.FontNormal)
|
||||||
for _, tc := range obj.TextCode {
|
for _, tc := range obj.TextCode {
|
||||||
runes := []rune(tc.Value)
|
var runes []rune
|
||||||
|
if tc.Index != "" {
|
||||||
|
runes = r.parseIndexRunes(tc.Index, fontID)
|
||||||
|
} else {
|
||||||
|
runes = []rune(tc.Value)
|
||||||
|
}
|
||||||
dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY)
|
dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY)
|
||||||
cx, cy := tc.X, tc.Y
|
xs, ys := parseFloats(tc.X), parseFloats(tc.Y)
|
||||||
|
cx, cy := 0.0, 0.0
|
||||||
|
if len(xs) > 0 {
|
||||||
|
cx = xs[0]
|
||||||
|
}
|
||||||
|
if len(ys) > 0 {
|
||||||
|
cy = ys[0]
|
||||||
|
}
|
||||||
for i, run := range runes {
|
for i, run := range runes {
|
||||||
str := string(run)
|
str := string(run)
|
||||||
if i > 0 {
|
if i < len(xs) {
|
||||||
|
cx = xs[i]
|
||||||
|
} else if i > 0 {
|
||||||
if i-1 < len(dxs) {
|
if i-1 < len(dxs) {
|
||||||
cx += dxs[i-1]
|
cx += dxs[i-1]
|
||||||
} else {
|
} else if len(dys) == 0 {
|
||||||
cx += face.TextWidth(str)
|
cx += face.TextWidth(str)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if i < len(ys) {
|
||||||
|
cy = ys[i]
|
||||||
|
} else if i > 0 {
|
||||||
if i-1 < len(dys) {
|
if i-1 < len(dys) {
|
||||||
cy += dys[i-1]
|
cy += dys[i-1]
|
||||||
}
|
}
|
||||||
@@ -509,14 +645,14 @@ 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)
|
text := canvas.NewTextLine(face, str, canvas.Left)
|
||||||
ctx.DrawText(canvasX, canvasY, text)
|
if fillColor != nil {
|
||||||
|
ctx.SetFillColor(fillColor)
|
||||||
|
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
|
||||||
if uw < 0.05 {
|
|
||||||
uw = 0.05
|
|
||||||
}
|
|
||||||
ctx.SetStrokeWidth(uw)
|
ctx.SetStrokeWidth(uw)
|
||||||
ctx.SetStrokeColor(textColor)
|
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+face.TextWidth(str), canvasY-off)
|
||||||
@@ -542,8 +678,18 @@ 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 _, fixedData, err := FixFontData(fontData); err == nil {
|
if _, fixedData, mapping, _, err := FixFontDataAggressive(fontData, true, true); err == nil {
|
||||||
fontData = fixedData
|
fontData = fixedData
|
||||||
|
if mapping != nil {
|
||||||
|
if r.FontGIDMap == nil {
|
||||||
|
r.FontGIDMap = make(map[string]map[uint16]rune)
|
||||||
|
}
|
||||||
|
inv := make(map[uint16]rune)
|
||||||
|
for k, v := range mapping {
|
||||||
|
inv[v] = k
|
||||||
|
}
|
||||||
|
r.FontGIDMap[fontID] = inv
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var fontStyle canvas.FontStyle
|
var fontStyle canvas.FontStyle
|
||||||
if of.Bold {
|
if of.Bold {
|
||||||
@@ -556,7 +702,9 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
|
|||||||
r.FontMap[fontID] = ff
|
r.FontMap[fontID] = ff
|
||||||
return ff
|
return ff
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
var fontStyle canvas.FontStyle
|
var fontStyle canvas.FontStyle
|
||||||
if of.Bold {
|
if of.Bold {
|
||||||
@@ -593,15 +741,15 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily {
|
|||||||
names := []string{of.FamilyName, of.FontName}
|
names := []string{of.FamilyName, of.FontName}
|
||||||
aliases := map[string]string{
|
aliases := map[string]string{
|
||||||
"simhei": "SimHei",
|
"simhei": "SimHei",
|
||||||
"黑体": "SimHei",
|
"黑体": "SimHei",
|
||||||
"microsoft yahei": "Microsoft YaHei",
|
"microsoft yahei": "Microsoft YaHei",
|
||||||
"微软雅黑": "Microsoft YaHei",
|
"微软雅黑": "Microsoft YaHei",
|
||||||
"simsun": "SimSun",
|
"simsun": "SimSun",
|
||||||
"宋体": "SimSun",
|
"宋体": "SimSun",
|
||||||
"kaiti": "KaiTi",
|
"kaiti": "KaiTi",
|
||||||
"楷体": "KaiTi",
|
"楷体": "KaiTi",
|
||||||
"fangsong": "FangSong",
|
"fangsong": "FangSong",
|
||||||
"仿宋": "FangSong",
|
"仿宋": "FangSong",
|
||||||
"arial": "Arial",
|
"arial": "Arial",
|
||||||
"segoe ui": "Segoe UI",
|
"segoe ui": "Segoe UI",
|
||||||
"times new roman": "Times New Roman",
|
"times new roman": "Times New Roman",
|
||||||
@@ -710,3 +858,138 @@ func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) {
|
|||||||
}
|
}
|
||||||
ctx.Pop()
|
ctx.Pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// applyClips 应用裁剪
|
||||||
|
// 入参: ctx 画布上下文, clips 裁剪对象, pageH 页面高度, parentCTM 父级CTM
|
||||||
|
func (r *Renderer) applyClips(ctx *canvas.Context, clips *Clips, pageH float64, parentCTM *Matrix) {
|
||||||
|
if clips == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, clip := range clips.Clip {
|
||||||
|
for _, area := range clip.Area {
|
||||||
|
for _, pathObj := range area.Path {
|
||||||
|
clipCTM := NewMatrix(pathObj.CTM)
|
||||||
|
if parentCTM != nil {
|
||||||
|
clipCTM = parentCTM.Multiply(clipCTM)
|
||||||
|
}
|
||||||
|
r.buildPath(pathObj, pageH, clipCTM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildPath 解析路径并返回Canvas Path
|
||||||
|
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵
|
||||||
|
// 返回: *canvas.Path 路径对象
|
||||||
|
func (r *Renderer) buildPath(obj PathObject, pageH float64, ctm Matrix) *canvas.Path {
|
||||||
|
bx, by := 0.0, 0.0
|
||||||
|
if obj.Boundary != "" {
|
||||||
|
if box, err := ParseBox(obj.Boundary); err == nil {
|
||||||
|
bx, by = box.X, box.Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p := &canvas.Path{}
|
||||||
|
tokens := strings.Fields(obj.AbbreviatedData)
|
||||||
|
for i := 0; i < len(tokens); {
|
||||||
|
cmd := tokens[i]
|
||||||
|
i++
|
||||||
|
switch cmd {
|
||||||
|
case "M", "S":
|
||||||
|
if i+1 < len(tokens) {
|
||||||
|
x, _ := strconv.ParseFloat(tokens[i], 64)
|
||||||
|
y, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||||
|
tx, ty := ctm.Transform(x, y)
|
||||||
|
p.MoveTo(tx+bx, pageH-(ty+by))
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
case "L":
|
||||||
|
if i+1 < len(tokens) {
|
||||||
|
x, _ := strconv.ParseFloat(tokens[i], 64)
|
||||||
|
y, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||||
|
tx, ty := ctm.Transform(x, y)
|
||||||
|
p.LineTo(tx+bx, pageH-(ty+by))
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
case "B":
|
||||||
|
if i+5 < len(tokens) {
|
||||||
|
x1, _ := strconv.ParseFloat(tokens[i], 64)
|
||||||
|
y1, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||||
|
x2, _ := strconv.ParseFloat(tokens[i+2], 64)
|
||||||
|
y2, _ := strconv.ParseFloat(tokens[i+3], 64)
|
||||||
|
x3, _ := strconv.ParseFloat(tokens[i+4], 64)
|
||||||
|
y3, _ := strconv.ParseFloat(tokens[i+5], 64)
|
||||||
|
tx1, ty1 := ctm.Transform(x1, y1)
|
||||||
|
tx2, ty2 := ctm.Transform(x2, y2)
|
||||||
|
tx3, ty3 := ctm.Transform(x3, y3)
|
||||||
|
p.CubeTo(tx1+bx, pageH-(ty1+by), tx2+bx, pageH-(ty2+by), tx3+bx, pageH-(ty3+by))
|
||||||
|
i += 6
|
||||||
|
}
|
||||||
|
case "Q":
|
||||||
|
if i+3 < len(tokens) {
|
||||||
|
x1, _ := strconv.ParseFloat(tokens[i], 64)
|
||||||
|
y1, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||||
|
x2, _ := strconv.ParseFloat(tokens[i+2], 64)
|
||||||
|
y2, _ := strconv.ParseFloat(tokens[i+3], 64)
|
||||||
|
tx1, ty1 := ctm.Transform(x1, y1)
|
||||||
|
tx2, ty2 := ctm.Transform(x2, y2)
|
||||||
|
p.QuadTo(tx1+bx, pageH-(ty1+by), tx2+bx, pageH-(ty2+by))
|
||||||
|
i += 4
|
||||||
|
}
|
||||||
|
case "A":
|
||||||
|
if i+6 < len(tokens) {
|
||||||
|
rx, _ := strconv.ParseFloat(tokens[i], 64)
|
||||||
|
ry, _ := strconv.ParseFloat(tokens[i+1], 64)
|
||||||
|
rot, _ := strconv.ParseFloat(tokens[i+2], 64)
|
||||||
|
large, _ := strconv.ParseBool(tokens[i+3])
|
||||||
|
sweep, _ := strconv.ParseBool(tokens[i+4])
|
||||||
|
x, _ := strconv.ParseFloat(tokens[i+5], 64)
|
||||||
|
y, _ := strconv.ParseFloat(tokens[i+6], 64)
|
||||||
|
sx := math.Hypot(ctm.a, ctm.c)
|
||||||
|
sy := math.Hypot(ctm.b, ctm.d)
|
||||||
|
ctmRot := math.Atan2(ctm.b, ctm.a) * 180 / math.Pi
|
||||||
|
tx, ty := ctm.Transform(x, y)
|
||||||
|
sweep = !sweep
|
||||||
|
p.ArcTo(rx*sx, ry*sy, -(rot + ctmRot), large, sweep, tx+bx, pageH-(ty+by))
|
||||||
|
i += 7
|
||||||
|
}
|
||||||
|
case "C":
|
||||||
|
p.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseIndexRunes 解析索引字形
|
||||||
|
// 入参: indexStr 索引字符串, fontID 字体ID
|
||||||
|
// 返回: []rune 字形列表
|
||||||
|
func (r *Renderer) parseIndexRunes(indexStr string, fontID string) []rune {
|
||||||
|
var gids []int
|
||||||
|
parts := strings.Fields(indexStr)
|
||||||
|
for _, p := range parts {
|
||||||
|
if strings.Contains(p, "-") {
|
||||||
|
sub := strings.Split(p, "-")
|
||||||
|
if len(sub) == 2 {
|
||||||
|
start, _ := strconv.Atoi(sub[0])
|
||||||
|
end, _ := strconv.Atoi(sub[1])
|
||||||
|
for k := start; k <= end; k++ {
|
||||||
|
gids = append(gids, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val, _ := strconv.Atoi(p)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res = append(res, rune(gid))
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|||||||
+22
-3
@@ -68,7 +68,17 @@ type DrawParam struct {
|
|||||||
Relative string `xml:"Relative,attr"`
|
Relative string `xml:"Relative,attr"`
|
||||||
ResourceID string `xml:"ResourceID,attr"`
|
ResourceID string `xml:"ResourceID,attr"`
|
||||||
BaseLoc string `xml:"BaseLoc,attr"`
|
BaseLoc string `xml:"BaseLoc,attr"`
|
||||||
|
Link string `xml:"Link,attr"`
|
||||||
LineWidth float64 `xml:"LineWidth,attr"`
|
LineWidth float64 `xml:"LineWidth,attr"`
|
||||||
|
Join string `xml:"Join,attr"`
|
||||||
|
Cap string `xml:"Cap,attr"`
|
||||||
|
DashOffset float64 `xml:"DashOffset,attr"`
|
||||||
|
DashPattern string `xml:"DashPattern,attr"`
|
||||||
|
MiterLimit float64 `xml:"MiterLimit,attr"`
|
||||||
|
Font string `xml:"Font,attr"`
|
||||||
|
Size float64 `xml:"Size,attr"`
|
||||||
|
Weight int `xml:"Weight,attr"`
|
||||||
|
Italic bool `xml:"Italic,attr"`
|
||||||
FillColor *FillColor `xml:"FillColor"`
|
FillColor *FillColor `xml:"FillColor"`
|
||||||
StrokeColor *StrokeColor `xml:"StrokeColor"`
|
StrokeColor *StrokeColor `xml:"StrokeColor"`
|
||||||
}
|
}
|
||||||
@@ -80,7 +90,16 @@ type CompositeGraphicUnits struct {
|
|||||||
|
|
||||||
// CompositeGraphicUnit 复合图元
|
// CompositeGraphicUnit 复合图元
|
||||||
type CompositeGraphicUnit struct {
|
type CompositeGraphicUnit struct {
|
||||||
ID string `xml:"ID,attr"`
|
ID string `xml:"ID,attr"`
|
||||||
BaseLoc string `xml:"BaseLoc,attr"`
|
BaseLoc string `xml:"BaseLoc,attr"`
|
||||||
ResourceID string `xml:"ResourceID,attr"`
|
ResourceID string `xml:"ResourceID,attr"`
|
||||||
|
Boundary string `xml:"Boundary,attr"`
|
||||||
|
CTM string `xml:"CTM,attr"`
|
||||||
|
DrawParam string `xml:"DrawParam,attr"`
|
||||||
|
Alpha *int `xml:"Alpha,attr"`
|
||||||
|
TextObject []TextObject `xml:"TextObject"`
|
||||||
|
PathObject []PathObject `xml:"PathObject"`
|
||||||
|
ImageObject []ImageObject `xml:"ImageObject"`
|
||||||
|
CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"`
|
||||||
|
Clips *Clips `xml:"Clips"`
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-1
@@ -24,12 +24,21 @@ import (
|
|||||||
// 入参: val 颜色值(R G B)
|
// 入参: val 颜色值(R G B)
|
||||||
// 返回: color.Color 颜色对象
|
// 返回: color.Color 颜色对象
|
||||||
func parseColor(val string) color.Color {
|
func parseColor(val string) color.Color {
|
||||||
|
return parseColorWithAlpha(val, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseColorWithAlpha 解析带透明度的颜色
|
||||||
|
func parseColorWithAlpha(val string, alpha *int) color.Color {
|
||||||
parts := strings.Fields(val)
|
parts := strings.Fields(val)
|
||||||
if len(parts) >= 3 {
|
if len(parts) >= 3 {
|
||||||
r, _ := strconv.Atoi(parts[0])
|
r, _ := strconv.Atoi(parts[0])
|
||||||
g, _ := strconv.Atoi(parts[1])
|
g, _ := strconv.Atoi(parts[1])
|
||||||
b, _ := strconv.Atoi(parts[2])
|
b, _ := strconv.Atoi(parts[2])
|
||||||
return color.RGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 255}
|
a := 255
|
||||||
|
if alpha != nil {
|
||||||
|
a = *alpha
|
||||||
|
}
|
||||||
|
return color.RGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: uint8(a)}
|
||||||
}
|
}
|
||||||
return color.Black
|
return color.Black
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user