diff --git a/internal/base/font/sfnt.go b/internal/base/font/sfnt.go index 1cfb680..11cf5ff 100644 --- a/internal/base/font/sfnt.go +++ b/internal/base/font/sfnt.go @@ -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] diff --git a/internal/base/font/subset.go b/internal/base/font/subset.go index 9763ce8..301bd57 100644 --- a/internal/base/font/subset.go +++ b/internal/base/font/subset.go @@ -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 diff --git a/internal/base/image/scale.go b/internal/base/image/scale.go index 7d48e90..f395084 100644 --- a/internal/base/image/scale.go +++ b/internal/base/image/scale.go @@ -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 } diff --git a/ofdgo_compression_resources.go b/ofdgo_compression_resources.go index da5ae30..78bda7a 100644 --- a/ofdgo_compression_resources.go +++ b/ofdgo_compression_resources.go @@ -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) diff --git a/ofdgo_renderer_font_canvas.go b/ofdgo_renderer_font_canvas.go index b2c83d8..7867979 100644 --- a/ofdgo_renderer_font_canvas.go +++ b/ofdgo_renderer_font_canvas.go @@ -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 diff --git a/ofdgo_renderer_pdf_canvas.go b/ofdgo_renderer_pdf_canvas.go index 270dc42..4af0083 100644 --- a/ofdgo_renderer_pdf_canvas.go +++ b/ofdgo_renderer_pdf_canvas.go @@ -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 diff --git a/ofdgo_renderer_pdf_text.go b/ofdgo_renderer_pdf_text.go index 27952e1..01c2053 100644 --- a/ofdgo_renderer_pdf_text.go +++ b/ofdgo_renderer_pdf_text.go @@ -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 = © + } + 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 核对原生文字的字形和未裁剪范围,复用实际绘制的字形缓存