feat(字体图像): 优化字体图像性能
build.yaml / build (push) Successful in 54s

This commit is contained in:
xiaoqidun committed 2026-10-15 21:58:12 +08:00
1 parent 6926ceafc9
commit 0b1dc7e6c8
7 files changed
+148 -25

No files matched your search

+18 -8
View File
@@ -18,7 +18,7 @@ import (
"encoding/binary"
"fmt"
"maps"
"sort"
"slices"
"sync"
"github.com/go-text/typesetting/font/opentype/tables"
@@ -34,10 +34,11 @@ type SFNT struct {
reverse map[uint16][]rune
}
// sfntCmap 保存编码记录及对应字符映射
// sfntCmap 保存编码记录及对应字符映射,标记已解析的共享子表
type sfntCmap struct {
platform, encoding, format uint16
mapping map[rune]uint16
duplicate bool
}
// ParseSFNT 校验独立字体的必需表、度量范围及字符映射
@@ -86,6 +87,9 @@ func ParseSFNTTables(table map[string][]byte) (*SFNT, error) {
}
f.cmaps = cmaps
for _, cmap := range f.cmaps {
if cmap.duplicate {
continue
}
for _, glyph := range cmap.mapping {
if glyph >= f.count {
return nil, fmt.Errorf("cmap glyph index out of range")
@@ -150,7 +154,7 @@ func readSFNTCmaps(data []byte) ([]sfntCmap, error) {
}
parsed[offset] = mapping
}
result = append(result, sfntCmap{binary.BigEndian.Uint16(record), binary.BigEndian.Uint16(record[2:]), format, mapping})
result = append(result, sfntCmap{platform: binary.BigEndian.Uint16(record), encoding: binary.BigEndian.Uint16(record[2:]), format: format, mapping: mapping, duplicate: found})
}
return result, nil
}
@@ -164,11 +168,11 @@ func (f *SFNT) UnitsPerEm() uint16 { return f.units }
func (f *SFNT) NumGlyphs() uint16 { return f.count }
// CmapEntries 返回各编码记录的映射项总数,共享子表按记录分别计数
// 返回: int 用于缓存预算的映射项数量
func (f *SFNT) CmapEntries() int {
count := 0
// 返回: uint64 用于缓存预算的映射项数量
func (f *SFNT) CmapEntries() uint64 {
var count uint64
for _, cmap := range f.cmaps {
count += len(cmap.mapping)
count += uint64(len(cmap.mapping))
}
return count
}
@@ -178,6 +182,9 @@ func (f *SFNT) CmapEntries() int {
// 返回: uint16 字形编号
func (f *SFNT) GlyphIndex(char rune) uint16 {
for _, cmap := range f.cmaps {
if cmap.duplicate {
continue
}
if gid := cmap.mapping[char]; gid != 0 {
return gid
}
@@ -193,6 +200,9 @@ func (f *SFNT) glyphCharacters(gid uint16) []rune {
f.reverse = make(map[uint16][]rune)
seen := make(map[rune]bool)
for _, cmap := range f.cmaps {
if cmap.duplicate {
continue
}
for char, glyph := range cmap.mapping {
if glyph != 0 && !seen[char] {
f.reverse[glyph] = append(f.reverse[glyph], char)
@@ -201,7 +211,7 @@ func (f *SFNT) glyphCharacters(gid uint16) []rune {
}
}
for _, chars := range f.reverse {
sort.Slice(chars, func(i, j int) bool { return chars[i] < chars[j] })
slices.Sort(chars)
}
})
return f.reverse[gid]
+19 -5
View File
@@ -134,7 +134,8 @@ func SubsetSFNT(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
if err != nil {
return nil, err
}
if subset := SubsetSourceSFNT(fixed, usage); subset != nil {
sfnt.Tables = tables
if subset := subsetSourceSFNT(sfnt, usage, len(fixed)); subset != nil {
return subset, nil
}
return fixed, nil
@@ -147,7 +148,7 @@ func SubsetSFNT(data []byte, glyphs []uint16, mapped bool) ([]byte, error) {
// 入参: data 字体数据, usage 全包用字记录
// 返回: []byte 更小的字体子集,无确定收益时为空
func SubsetSourceSFNT(data []byte, usage *Usage) []byte {
if usage.Unsafe || len(usage.Characters)+len(usage.Glyphs) == 0 || bytes.HasPrefix(data, []byte("ttcf")) {
if usage == nil || usage.Unsafe || len(usage.Characters)+len(usage.Glyphs) == 0 || bytes.HasPrefix(data, []byte("ttcf")) {
return nil
}
fontTables, err := FileTables(data, 0)
@@ -162,7 +163,17 @@ func SubsetSourceSFNT(data []byte, usage *Usage) []byte {
}
}
sfnt, err := ParseSFNTTables(fontTables)
if err != nil || !sfnt.IsTrueType && !sfnt.IsCFF || fontEmbeddingFlags(sfnt)&0x0100 != 0 {
if err != nil {
return nil
}
return subsetSourceSFNT(sfnt, usage, len(data))
}
// subsetSourceSFNT 复用已校验的字体表裁剪字形,仅返回小于原编码的结果
// 入参: sfnt 已解析字体, usage 用字记录, size 原编码长度
// 返回: []byte 更小的字体子集,无确定收益时为空
func subsetSourceSFNT(sfnt *SFNT, usage *Usage, size int) []byte {
if usage == nil || usage.Unsafe || len(usage.Characters)+len(usage.Glyphs) == 0 || !sfnt.IsTrueType && !sfnt.IsCFF || fontEmbeddingFlags(sfnt)&0x0100 != 0 {
return nil
}
var unicodeTables []sfntCmap
@@ -178,6 +189,9 @@ func SubsetSourceSFNT(data []byte, usage *Usage) []byte {
return nil
}
glyphs := maps.Clone(usage.Glyphs)
if glyphs == nil {
glyphs = make(map[uint16]bool)
}
glyphs[0] = true
mapping := make(map[rune]uint16, len(usage.Characters))
for char := range usage.Characters {
@@ -210,7 +224,7 @@ func SubsetSourceSFNT(data []byte, usage *Usage) []byte {
tables["cmap"] = buildCmapWithVariations(sfnt.NumGlyphs(), mapping, variations)
delete(tables, "DSIG")
result, err := SerializeOTF(tables)
if err != nil || len(result) >= len(data) {
if err != nil || len(result) >= size {
return nil
}
return result
@@ -258,7 +272,7 @@ func SubsetSourceSFNT(data []byte, usage *Usage) []byte {
clear(tables["head"][8:12])
delete(tables, "DSIG")
result, err := SerializeOTF(tables)
if err != nil || len(result) >= len(data) {
if err != nil || len(result) >= size {
return nil
}
return result
+14 -2
View File
@@ -147,6 +147,7 @@ func rasterImageAxis(first, end, source, limit int, scale, offset float64) []ras
result := make([]rasterAxisSample, 0, end-first)
argument := math.Min(1, math.Abs(scale))
support := draw.CatmullRom.Support / argument
count := 0
for pixel := first; pixel < end; pixel++ {
center := (float64(pixel) + .5 - offset) / scale
if center < float64(source) || center >= float64(limit) {
@@ -155,7 +156,18 @@ func rasterImageAxis(first, end, source, limit int, scale, offset float64) []ras
center -= .5
start := int(max(float64(source), math.Floor(center-support)))
stop := int(min(float64(limit), math.Ceil(center+support)))
weights := make([]float64, stop-start)
count += stop - start
result = append(result, rasterAxisSample{pixel: pixel, first: start})
}
buffer := make([]float64, count)
for index := range result {
sample := &result[index]
center := (float64(sample.pixel)+.5-offset)/scale - .5
start := sample.first
stop := int(min(float64(limit), math.Ceil(center+support)))
n := stop - start
weights := buffer[:n:n]
buffer = buffer[n:]
total := 0.0
for i := range weights {
distance := math.Abs((float64(start+i) - center) * argument)
@@ -167,7 +179,7 @@ func rasterImageAxis(first, end, source, limit int, scale, offset float64) []ras
for i := range weights {
weights[i] /= total
}
result = append(result, rasterAxisSample{pixel: pixel, first: start, weights: weights})
sample.weights = weights
}
return result
}
+7 -1
View File
@@ -194,6 +194,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
formats := make(map[string]string)
seen := make(map[resourceScope]map[[32]byte]string)
imageResults := make(map[outputImageKey][]byte)
imageDigests := make(map[outputImageKey][32]byte)
imageSources := make(map[string]outputImageSource)
ordered := slices.Sorted(maps.Keys(actual))
sizes := make(map[string]uint64, len(files))
@@ -307,7 +308,12 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
if candidate != nil {
data = candidate
parts[actual[key]] = data
source.key.hash = sha256.Sum256(data)
digest, ok := imageDigests[cacheKey]
if !ok {
digest = sha256.Sum256(data)
imageDigests[cacheKey] = digest
}
source.key.hash = digest
}
if data != nil {
source.format = outputImageFormat(data)
+1 -1
View File
@@ -214,7 +214,7 @@ func (r *Renderer) cachedTextGlyphPath(face *canvas.FontFace, glyph textGlyph) (
for _, data := range metrics.Tables {
cost += uint64(len(data)) * 8
}
cost += uint64(metrics.CmapEntries()) * 48
cost += metrics.CmapEntries() * 48
cache.put(face.Font, outlines, int(min(cost, uint64(cache.limit)+1)))
}
r.canvasState().fontOutlines[face.Font] = outlines
+1
View File
@@ -34,6 +34,7 @@ type pdfRenderer struct {
*pdf.PDF
glyphPaths renderCache[*canvas.Path, *canvas.Path]
fonts map[*canvas.Font]*canvas.Font
cffFonts map[*canvas.Font][]*pdfFontSubset
images [][]image.Image
exactImages bool
imageError error
+88 -8
View File
@@ -15,6 +15,9 @@
package ofdgo
import (
"fmt"
"slices"
"github.com/tdewolff/canvas"
canvastext "github.com/tdewolff/canvas/text"
basefont "github.com/xiaoqidun/ofdgo/internal/base/font"
@@ -22,6 +25,12 @@ import (
"github.com/xiaoqidun/pdfgo"
)
// pdfFontSubset 保存独立CFF输出字体及实际用字,包含缺省字形
type pdfFontSubset struct {
font *canvas.Font
glyphs map[uint16]bool
}
// RenderText 记录后端文字区段,写出时校验数量并关联原文
// 入参: text 画布文字, matrix 文字变换
func (r *pdfRenderer) RenderText(text *canvas.Text, matrix canvas.Matrix) {
@@ -68,18 +77,89 @@ func (r *pdfRenderer) glyphText(face *canvas.FontFace, glyph textGlyph) (*canvas
outputFace := *face
outputFace.Font = font
face = &outputFace
var text *canvas.Text
if glyph.GlyphID < 0 || glyph.GlyphID > 0xffff {
return canvas.NewTextLine(face, glyph.Text, canvas.Left), nil
text = canvas.NewTextLine(face, glyph.Text, canvas.Left)
} else {
id := uint16(glyph.GlyphID)
text = canvas.NewTextLine(face, " ", canvas.Left)
text.WalkLines(func(_ float64, spans []canvas.TextSpan) {
for i := range spans {
spans[i].Text = glyph.Text
spans[i].Glyphs = []canvastext.Glyph{{SFNT: face.Font.SFNT, Size: face.Size, ID: id, XAdvance: int32(face.Font.GlyphAdvance(id)), Text: glyph.Text}}
}
})
}
id := uint16(glyph.GlyphID)
text := canvas.NewTextLine(face, " ", canvas.Left)
text.WalkLines(func(_ float64, spans []canvas.TextSpan) {
for i := range spans {
spans[i].Text = glyph.Text
spans[i].Glyphs = []canvastext.Glyph{{SFNT: face.Font.SFNT, Size: face.Size, ID: id, XAdvance: int32(face.Font.GlyphAdvance(id)), Text: glyph.Text}}
if font.IsCFF {
if err := r.cffTextFont(font, text); err != nil {
return nil, err
}
}
return text, nil
}
// cffTextFont 按实际用字隔离CFF子集,避免后端清除字形名后超出ISOAdobe字符集容量
// 入参: font 独立输出字体, text 已定位文字
// 返回: error 字形数量或字体解析错误
func (r *pdfRenderer) cffTextFont(font *canvas.Font, text *canvas.Text) error {
const limit = 229
ids := []uint16{0}
text.WalkSpans(func(_, _ float64, span canvas.TextSpan) {
for _, glyph := range span.Glyphs {
if !slices.Contains(ids, glyph.ID) {
ids = append(ids, glyph.ID)
}
}
})
return text, nil
if len(ids) > limit {
return fmt.Errorf("CFF text glyph count exceeds output subset capacity")
}
var selected *pdfFontSubset
for _, subset := range r.cffFonts[font] {
count := len(subset.glyphs)
for _, id := range ids {
if !subset.glyphs[id] {
count++
}
}
if count <= limit {
selected = subset
break
}
}
if selected == nil {
output := font
if len(r.cffFonts[font]) != 0 {
loaded, err := loadCanvasFont(basefont.SFNTData(font.Tables), 0, font.Style())
if err != nil {
return err
}
copy := *font
copy.SFNT = loaded.SFNT
output = &copy
}
selected = &pdfFontSubset{font: output, glyphs: make(map[uint16]bool)}
if r.cffFonts == nil {
r.cffFonts = make(map[*canvas.Font][]*pdfFontSubset)
}
r.cffFonts[font] = append(r.cffFonts[font], selected)
}
for _, id := range ids {
selected.glyphs[id] = true
}
if selected.font != font {
text.WalkLines(func(_ float64, spans []canvas.TextSpan) {
for i := range spans {
face := *spans[i].Face
face.Font = selected.font
spans[i].Face = &face
for j := range spans[i].Glyphs {
spans[i].Glyphs[j].SFNT = selected.font.SFNT
}
}
})
}
return nil
}
// pdfTextSupported 核对原生文字的字形和未裁剪范围,复用实际绘制的字形缓存