diff --git a/ofdgo.go b/ofdgo.go index b29eefa..9ca57b6 100644 --- a/ofdgo.go +++ b/ofdgo.go @@ -65,10 +65,11 @@ func NewReader(r io.ReaderAt, size int64) (*Reader, error) { // 返回: *Renderer 渲染器实例 func NewRenderer(reader *Reader, opts ...RendererOption) *Renderer { r := &Renderer{ - Reader: reader, - DPI: 300.0, - DrawParams: reader.drawParamCache, - FontMap: make(map[string]*canvas.FontFamily), + Reader: reader, + DPI: 300.0, + DrawParams: reader.drawParamCache, + CompositeGraphicUnits: reader.compositeGraphicUnitCache, + FontMap: make(map[string]*canvas.FontFamily), } for _, opt := range opts { opt(r) diff --git a/ofdgo_font.go b/ofdgo_font.go index 5fb8264..235fdfe 100644 --- a/ofdgo_font.go +++ b/ofdgo_font.go @@ -15,209 +15,33 @@ package ofdgo import ( - "bytes" "encoding/binary" - "fmt" ) -// FixFontData 修复字体数据 -// 本函数主要解决嵌入字体缺失 OS/2 表导致部分渲染库(如 tdewolff/font)Panic 的问题 -// 通过解析 hhea 表获取 Ascender/Descender 等度量信息,构造并注入一个标准的 OS/2 表 -// 入参: data 原始字体文件数据 -// 返回: bool 是否进行了修补, []byte 修复后的字体数据, error 错误信息 -func FixFontData(data []byte) (bool, []byte, error) { - if len(data) < 12 { - return false, data, nil +// FixFontDataAggressive 激进修复字体数据 +// 尝试修复缺失表(OS/2, cmap等)的TrueType字体或包装CFF裸数据 +// 入参: data 原始字体数据, fixCmap 是否修复cmap, fixName 是否修复name +// 返回: bool 是否修复, []byte 修复后数据, map[rune]uint16 字符映射, bool 是否缺失cmap, error 错误信息 +func FixFontDataAggressive(data []byte, fixCmap, fixName bool) (bool, []byte, map[rune]uint16, bool, error) { + if len(data) < 4 { + return false, data, nil, false, nil } - numTables := binary.BigEndian.Uint16(data[4:6]) - hasOS2 := false - type TableRecord struct { - Tag string - CheckSum uint32 - Offset uint32 - Length uint32 + tag := string(data[0:4]) + u32Tag := binary.BigEndian.Uint32(data[0:4]) + isTrueType := u32Tag == 0x00010000 || tag == "true" + isOpenType := tag == "OTTO" + isTTC := tag == "ttcf" + if isTTC { + return false, data, nil, false, nil } - tables := make([]TableRecord, 0, numTables) - var hhea TableRecord - hasHhea := false - pos := 12 - for i := 0; i < int(numTables); i++ { - 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] + if !isTrueType && !isOpenType { + if data[0] == 1 && data[1] == 0 && data[2] == 4 { + otf, mapping, err := wrapCFFToOTF(data) + if err == nil { + return true, otf, mapping, false, nil } } } - headerSize := 12 + 16*uint32(len(newTables)) - currentOffset := headerSize - 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 + fixed, newData, mapping, mc, err := fixTrueType(data, fixCmap, fixName) + return fixed, newData, mapping, mc, err } diff --git a/ofdgo_font_cff.go b/ofdgo_font_cff.go new file mode 100644 index 0000000..2125540 --- /dev/null +++ b/ofdgo_font_cff.go @@ -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", +} diff --git a/ofdgo_font_ttf.go b/ofdgo_font_ttf.go new file mode 100644 index 0000000..272dfd5 --- /dev/null +++ b/ofdgo_font_ttf.go @@ -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 +} diff --git a/ofdgo_font_util.go b/ofdgo_font_util.go new file mode 100644 index 0000000..e30f275 --- /dev/null +++ b/ofdgo_font_util.go @@ -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 +} diff --git a/ofdgo_geom.go b/ofdgo_geom.go index 1075dea..b45a4d1 100644 --- a/ofdgo_geom.go +++ b/ofdgo_geom.go @@ -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 应用变换矩阵 // 入参: x X坐标, y Y坐标 // 返回: float64 变换后X, float64 变换后Y diff --git a/ofdgo_page.go b/ofdgo_page.go index 49ec9c1..8427b7a 100644 --- a/ofdgo_page.go +++ b/ofdgo_page.go @@ -38,41 +38,66 @@ type Content struct { // Layer 图层 type Layer struct { - ID string `xml:"ID,attr"` - DrawParam string `xml:"DrawParam,attr"` - TextObject []TextObject `xml:"TextObject"` - PathObject []PathObject `xml:"PathObject"` - ImageObject []ImageObject `xml:"ImageObject"` + ID string `xml:"ID,attr"` + DrawParam string `xml:"DrawParam,attr"` + TextObject []TextObject `xml:"TextObject"` + PathObject []PathObject `xml:"PathObject"` + 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 文本对象 type TextObject struct { - ID string `xml:"ID,attr"` - Boundary string `xml:"Boundary,attr"` - DrawParam string `xml:"DrawParam,attr"` - Font string `xml:"Font,attr"` - Size float64 `xml:"Size,attr"` - Weight int `xml:"Weight,attr"` - Italic bool `xml:"Italic,attr"` - Decoration string `xml:"Decoration,attr"` - HScale float64 `xml:"HScale,attr"` - CTM string `xml:"CTM,attr"` - FillColor *FillColor `xml:"FillColor"` - TextCode []TextCode `xml:"TextCode"` + ID string `xml:"ID,attr"` + Boundary string `xml:"Boundary,attr"` + DrawParam string `xml:"DrawParam,attr"` + LineWidth float64 `xml:"LineWidth,attr"` + Font string `xml:"Font,attr"` + Size float64 `xml:"Size,attr"` + Weight int `xml:"Weight,attr"` + Italic bool `xml:"Italic,attr"` + Decoration string `xml:"Decoration,attr"` + HScale float64 `xml:"HScale,attr"` + VScale float64 `xml:"VScale,attr"` + CTM string `xml:"CTM,attr"` + Fill *bool `xml:"Fill,attr"` + Stroke *bool `xml:"Stroke,attr"` + StrokeColor *StrokeColor `xml:"StrokeColor"` + FillColor *FillColor `xml:"FillColor"` + TextCode []TextCode `xml:"TextCode"` + Clips *Clips `xml:"Clips"` } // FillColor 填充颜色 type FillColor struct { Value string `xml:"Value,attr"` + Alpha *int `xml:"Alpha,attr"` } // TextCode 文本内容节点 type TextCode struct { - X float64 `xml:"X,attr"` - Y float64 `xml:"Y,attr"` - DeltaX string `xml:"DeltaX,attr"` - DeltaY string `xml:"DeltaY,attr"` - Value string `xml:",chardata"` + X string `xml:"X,attr"` + Y string `xml:"Y,attr"` + DeltaX string `xml:"DeltaX,attr"` + DeltaY string `xml:"DeltaY,attr"` + Index string `xml:"Index,attr"` + Value string `xml:",chardata"` } // PathObject 路径对象 @@ -81,15 +106,24 @@ type PathObject struct { Boundary string `xml:"Boundary,attr"` DrawParam string `xml:"DrawParam,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"` + Stroke *bool `xml:"Stroke,attr"` + Fill *bool `xml:"Fill,attr"` StrokeColor *StrokeColor `xml:"StrokeColor"` FillColor *FillColor `xml:"FillColor"` AbbreviatedData string `xml:"AbbreviatedData"` + Clips *Clips `xml:"Clips"` } // StrokeColor 勾边颜色 type StrokeColor struct { Value string `xml:"Value,attr"` + Alpha *int `xml:"Alpha,attr"` } // ImageObject 图片对象 @@ -98,4 +132,5 @@ type ImageObject struct { Boundary string `xml:"Boundary,attr"` ResourceID string `xml:"ResourceID,attr"` CTM string `xml:"CTM,attr"` + Clips *Clips `xml:"Clips"` } diff --git a/ofdgo_reader.go b/ofdgo_reader.go index 516d439..9cb2532 100644 --- a/ofdgo_reader.go +++ b/ofdgo_reader.go @@ -25,16 +25,17 @@ import ( // Reader OFD文件阅读器 type Reader struct { - Path string - Zip *zip.Reader - Closer io.Closer - OFD *OFD - RootDir string - ResMap map[string]string - fontCache map[string]*Font - drawParamCache map[string]*DrawParam - doc *Document - Stamps map[string][]Stamp + Path string + Zip *zip.Reader + Closer io.Closer + OFD *OFD + RootDir string + ResMap map[string]string + fontCache map[string]*Font + drawParamCache map[string]*DrawParam + compositeGraphicUnitCache map[string]*CompositeGraphicUnit + doc *Document + Stamps map[string][]Stamp } // Close 关闭阅读器 @@ -61,6 +62,7 @@ func (r *Reader) initRoot() error { r.ResMap = make(map[string]string) r.fontCache = make(map[string]*Font) r.drawParamCache = make(map[string]*DrawParam) + r.compositeGraphicUnitCache = make(map[string]*CompositeGraphicUnit) return nil } @@ -185,6 +187,10 @@ func (r *Reader) loadRes(resPath string) { dp := &res.DrawParams.DrawParam[i] r.drawParamCache[dp.ID] = dp } + for i := range res.CompositeGraphicUnits.CompositeGraphicUnit { + cgu := &res.CompositeGraphicUnits.CompositeGraphicUnit[i] + r.compositeGraphicUnitCache[cgu.ID] = cgu + } } // PageContent 获取页面内容 diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index 0e36c56..2a9c1cd 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -21,6 +21,7 @@ import ( "image/color" "io" "io/fs" + "math" "path/filepath" "strconv" "strings" @@ -34,13 +35,15 @@ import ( // Renderer 渲染器实现 type Renderer struct { - Reader *Reader - DPI float64 - fontFamily *canvas.FontFamily - DrawParams map[string]*DrawParam - FontMap map[string]*canvas.FontFamily - fontDirs []string - fontFS []fs.FS + Reader *Reader + DPI float64 + fontFamily *canvas.FontFamily + DrawParams map[string]*DrawParam + CompositeGraphicUnits map[string]*CompositeGraphicUnit + FontMap map[string]*canvas.FontFamily + FontGIDMap map[string]map[uint16]rune + fontDirs []string + fontFS []fs.FS } // RendererOption 渲染器配置选项 @@ -103,7 +106,7 @@ func (r *Renderer) RenderPageToContext(ctx *canvas.Context, page *PageContent) e } if page.Content.Layer != nil { 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 { @@ -237,36 +240,81 @@ func (r *Renderer) renderTemplate(ctx *canvas.Context, templateID string, pageH } if tplContent.Content.Layer != nil { for _, layer := range tplContent.Content.Layer { - r.renderLayer(ctx, layer, pageH, nil, nil, 0) + r.renderLayer(ctx, layer, pageH, nil, nil, 0, nil) } } } // renderLayer 渲染图层 -// 入参: ctx 画布上下文, layer 图层对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽 -func (r *Renderer) renderLayer(ctx *canvas.Context, layer Layer, pageH float64, defaultFill color.Color, defaultStroke color.Color, defaultLW float64) { +// 入参: ctx 画布上下文, layer 图层对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽, parentCTM 父级CTM +func (r *Renderer) renderLayer(ctx *canvas.Context, layer Layer, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64, parentCTM *Matrix) { if layer.DrawParam != "" { if dp := r.getDrawParam(layer.DrawParam, nil); dp != nil { if dp.LineWidth > 0 { defaultLW = dp.LineWidth } if dp.FillColor != nil { - defaultFill = parseColor(dp.FillColor.Value) + defaultFill = parseColorWithAlpha(dp.FillColor.Value, dp.FillColor.Alpha) } if dp.StrokeColor != nil { - defaultStroke = parseColor(dp.StrokeColor.Value) + defaultStroke = parseColorWithAlpha(dp.StrokeColor.Value, dp.StrokeColor.Alpha) } } } - for _, imgObj := range layer.ImageObject { - r.renderImage(ctx, imgObj, pageH) + for _, textObj := range layer.TextObject { + r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke, parentCTM) } 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 { - r.renderText(ctx, textObj, pageH, defaultFill, defaultStroke) + for _, imgObj := range layer.ImageObject { + 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 获取绘制参数逻辑 @@ -290,12 +338,37 @@ func (r *Renderer) getDrawParam(id string, visited map[string]bool) *DrawParam { if dp.LineWidth > 0 { 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 { merged.FillColor = dp.FillColor } if dp.StrokeColor != nil { 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 dp @@ -318,8 +391,8 @@ func (r *Renderer) initCommon() { } // renderImage 渲染图片 -// 入参: ctx 画布上下文, obj 图片对象, pageH 页面高度 -func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float64) { +// 入参: ctx 画布上下文, obj 图片对象, pageH 页面高度, parentCTM 父级CTM +func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float64, parentCTM *Matrix) { resPath, ok := r.Reader.ResMap[obj.ResourceID] if !ok { return @@ -342,9 +415,13 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float if obj.CTM == "" { ctm = Matrix{a: box.W, d: box.H} } + if parentCTM != nil { + ctm = parentCTM.Multiply(ctm) + } tx, ty := ctm.Transform(0, 1) canvasX, canvasY := tx+box.X, pageH-(ty+box.Y) ctx.Push() + r.applyClips(ctx, obj.Clips, pageH, &ctm) ctx.Translate(canvasX, canvasY) ctx.Scale(ctm.a/imgW, ctm.d/imgH) 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 渲染路径 -// 入参: ctx 画布上下文, obj 路径对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, defaultLW 默认线宽 -func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64, defaultFill, defaultStroke color.Color, defaultLW float64) { +// 入参: 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, parentCTM *Matrix) { 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) - fillColor, strokeColor := defaultFill, defaultStroke - if strokeColor == nil { - strokeColor = canvas.Black + if parentCTM != nil { + ctm = parentCTM.Multiply(ctm) } + r.applyClips(ctx, obj.Clips, pageH, &ctm) + fillColor, strokeColor := defaultFill, defaultStroke lineWidth := defaultLW if lineWidth == 0 { lineWidth = 0.353 } + lineCap := canvas.ButtCap + lineJoin := canvas.MiterJoin + dashOffset := 0.0 + var dashPattern []float64 if obj.DrawParam != "" { if dp := r.getDrawParam(obj.DrawParam, nil); dp != nil { if dp.LineWidth > 0 { lineWidth = dp.LineWidth } if dp.FillColor != nil { - fillColor = parseColor(dp.FillColor.Value) + fillColor = parseColorWithAlpha(dp.FillColor.Value, dp.FillColor.Alpha) } 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 } if obj.FillColor != nil { - fillColor = parseColor(obj.FillColor.Value) + fillColor = parseColorWithAlpha(obj.FillColor.Value, obj.FillColor.Alpha) } if obj.StrokeColor != nil { - strokeColor = parseColor(obj.StrokeColor.Value) + strokeColor = parseColorWithAlpha(obj.StrokeColor.Value, obj.StrokeColor.Alpha) } - 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 "C": - p.Close() + if obj.Cap != "" { + if obj.Cap == "Round" { + lineCap = canvas.RoundCap + } else if obj.Cap == "Square" { + lineCap = canvas.SquareCap + } else { + lineCap = canvas.ButtCap } } - 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.DrawPath(0, 0, p) } - ctx.SetStrokeColor(strokeColor) - ctx.SetStrokeWidth(lineWidth) - ctx.DrawPath(0, 0, p) + shouldStroke := true + if obj.Stroke != nil { + 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() } // renderText 渲染文本 -// 入参: ctx 画布上下文, obj 文本对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色 -func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64, defaultFill, defaultStroke color.Color) { +// 入参: ctx 画布上下文, obj 文本对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, parentCTM 父级CTM +func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64, defaultFill, defaultStroke color.Color, parentCTM *Matrix) { ctx.Push() bx, by := 0.0, 0.0 if obj.Boundary != "" { @@ -453,34 +544,58 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 } } 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 + if sizeMM == 0 && dp != nil && dp.Size > 0 { + sizeMM = dp.Size + } if sizeMM == 0 { sizeMM = 3.5 } + if obj.VScale != 0 { + sizeMM *= obj.VScale + } if scale := ctm.YScale(); scale > 0 { sizeMM *= scale } sizePt := sizeMM * 2.83465 - textColor := defaultFill - if textColor == nil { - textColor = canvas.Black + fillColor := defaultFill + if fillColor == nil { + fillColor = canvas.Black } - if obj.DrawParam != "" { - if dp := r.getDrawParam(obj.DrawParam, nil); dp != nil && dp.FillColor != nil { - textColor = parseColor(dp.FillColor.Value) - } + if dp != nil && dp.FillColor != nil { + fillColor = parseColorWithAlpha(dp.FillColor.Value, dp.FillColor.Alpha) } if obj.FillColor != nil { - textColor = parseColor(obj.FillColor.Value) + fillColor = parseColorWithAlpha(obj.FillColor.Value, obj.FillColor.Alpha) } 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 } - if obj.Italic { + italic := obj.Italic + if !italic && dp != nil && dp.Italic { + italic = true + } + if italic { 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 { fontStyle |= canvas.FontBold } @@ -488,20 +603,41 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 fontStyle |= canvas.FontItalic } } - ff := r.loadFont(obj.Font) - face := ff.Face(sizePt, textColor, fontStyle, canvas.FontNormal) + ff := r.loadFont(fontID) + if ff == nil { + return + } + face := ff.Face(sizePt, fillColor, fontStyle, canvas.FontNormal) 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) - 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 { str := string(run) - if i > 0 { + if i < len(xs) { + cx = xs[i] + } else if i > 0 { if i-1 < len(dxs) { cx += dxs[i-1] - } else { + } else if len(dys) == 0 { cx += face.TextWidth(str) } + } + if i < len(ys) { + cy = ys[i] + } else if i > 0 { if i-1 < len(dys) { 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) canvasX, canvasY := tx+bx, pageH-(ty+by) 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") { uw := sizeMM * 0.05 - if uw < 0.05 { - uw = 0.05 - } ctx.SetStrokeWidth(uw) - ctx.SetStrokeColor(textColor) + ctx.SetStrokeColor(fillColor) off := sizeMM * 0.1 ctx.MoveTo(canvasX, 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) if of.FontFile != "" { 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 + 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 if of.Bold { @@ -556,7 +702,9 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { r.FontMap[fontID] = ff return ff } + return nil } + return nil } var fontStyle canvas.FontStyle if of.Bold { @@ -593,15 +741,15 @@ func (r *Renderer) loadFont(fontID string) *canvas.FontFamily { names := []string{of.FamilyName, of.FontName} aliases := map[string]string{ "simhei": "SimHei", - "黑体": "SimHei", + "黑体": "SimHei", "microsoft yahei": "Microsoft YaHei", - "微软雅黑": "Microsoft YaHei", + "微软雅黑": "Microsoft YaHei", "simsun": "SimSun", - "宋体": "SimSun", + "宋体": "SimSun", "kaiti": "KaiTi", - "楷体": "KaiTi", + "楷体": "KaiTi", "fangsong": "FangSong", - "仿宋": "FangSong", + "仿宋": "FangSong", "arial": "Arial", "segoe ui": "Segoe UI", "times new roman": "Times New Roman", @@ -710,3 +858,138 @@ func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) { } 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 +} diff --git a/ofdgo_res.go b/ofdgo_res.go index e569158..19bc48d 100644 --- a/ofdgo_res.go +++ b/ofdgo_res.go @@ -68,7 +68,17 @@ type DrawParam struct { Relative string `xml:"Relative,attr"` ResourceID string `xml:"ResourceID,attr"` BaseLoc string `xml:"BaseLoc,attr"` + Link string `xml:"Link,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"` StrokeColor *StrokeColor `xml:"StrokeColor"` } @@ -80,7 +90,16 @@ type CompositeGraphicUnits struct { // CompositeGraphicUnit 复合图元 type CompositeGraphicUnit struct { - ID string `xml:"ID,attr"` - BaseLoc string `xml:"BaseLoc,attr"` - ResourceID string `xml:"ResourceID,attr"` + ID string `xml:"ID,attr"` + BaseLoc string `xml:"BaseLoc,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"` } diff --git a/ofdgo_text.go b/ofdgo_text.go index efde935..6060b30 100644 --- a/ofdgo_text.go +++ b/ofdgo_text.go @@ -24,12 +24,21 @@ import ( // 入参: val 颜色值(R G B) // 返回: 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) if len(parts) >= 3 { r, _ := strconv.Atoi(parts[0]) g, _ := strconv.Atoi(parts[1]) 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 }