mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
6926ceafc9
commit
0b1dc7e6c8
7 files changed
+143
-20
No files matched your search
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,20 +77,91 @@ 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 = 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
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 = ©
|
||||
}
|
||||
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 核对原生文字的字形和未裁剪范围,复用实际绘制的字形缓存
|
||||
// 入参: object 文字对象, fontID 字体标识, face 实际字体, transforms 字形映射, clip 裁剪路径, height 页面高度, matrix 对象变换, glyphMatrix 字形变换
|
||||
// 返回: bool 是否能完整保留字形外观
|
||||
|
||||
Reference in new issue
Block a user