fix(回归优化): 修复浏览器回归并优化内存
build.yaml / build (push) Successful in 49s

This commit is contained in:
xiaoqidun committed 2026-10-21 18:28:57 +08:00
1 parent 974b515374
commit 67f4cf9d2c
13 files changed
+401 -44

No files matched your search

+7 -2
View File
@@ -7687,13 +7687,18 @@ function chooseResourceFile(action) {
async function changeManagedResource(action, item, file) { async function changeManagedResource(action, item, file) {
if (!item || !state.editing) return; if (!item || !state.editing) return;
releaseResourcePreview(); releaseResourcePreview();
const page = Number(resourceElements.Page.value), object = resourceElements.Object.value;
const format = file?.name.includes(".") ? file.name.split(".").at(-1).toUpperCase() : ""; const format = file?.name.includes(".") ? file.name.split(".").at(-1).toUpperCase() : "";
const changed = item.kind === "Attachment" && action !== "bind" && action !== "unbind" const changed = item.kind === "Attachment" && action !== "bind" && action !== "unbind"
? await changeDocument("ofdgoChangeAttachment", null, action, item.id, file ? [file.name] : [], file ? [file] : []) ? await changeDocument("ofdgoChangeAttachment", null, action, item.id, file ? [file.name] : [], file ? [file] : [])
: await changeDocument("ofdgoChangeResource", null, action, item.kind, item.id, format, file || null, Number(resourceElements.Page.value), resourceElements.Object.value); : await changeDocument("ofdgoChangeResource", null, action, item.kind, item.id, format, file || null, page, object);
if (changed) { if (changed) {
await loadManagedResources(); await loadManagedResources();
if (action === "bind" || action === "unbind") resourceElements.Status.textContent = action === "bind" ? "已绑定" : "已解绑"; if (action === "bind" || action === "unbind") {
resourceElements.Page.value = String(page);
await loadResourceTargets(object);
resourceElements.Status.textContent = action === "bind" ? "已绑定" : "已解绑";
}
} }
} }
+118 -18
View File
@@ -17,9 +17,11 @@ package font
import ( import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"iter"
"maps" "maps"
"slices" "slices"
"sync" "sync"
"unicode/utf8"
"github.com/go-text/typesetting/font/opentype/tables" "github.com/go-text/typesetting/font/opentype/tables"
) )
@@ -34,10 +36,12 @@ type SFNT struct {
reverse map[uint16][]rune reverse map[uint16][]rune
} }
// sfntCmap 保存编码记录及对应字符映射,标记已解析的共享子表 // sfntCmap 保存编码记录及字符映射,连续区间与共享子表不重复展开
type sfntCmap struct { type sfntCmap struct {
platform, encoding, format uint16 platform, encoding, format uint16
mapping map[rune]uint16 mapping map[rune]uint16
groups []byte
entries uint64
duplicate bool duplicate bool
} }
@@ -90,7 +94,7 @@ func ParseSFNTTables(table map[string][]byte) (*SFNT, error) {
if cmap.duplicate { if cmap.duplicate {
continue continue
} }
for _, glyph := range cmap.mapping { for _, glyph := range cmap.pairs() {
if glyph >= f.count { if glyph >= f.count {
return nil, fmt.Errorf("cmap glyph index out of range") return nil, fmt.Errorf("cmap glyph index out of range")
} }
@@ -119,7 +123,7 @@ func readSFNTCmaps(data []byte) ([]sfntCmap, error) {
} }
count := int(binary.BigEndian.Uint16(data[2:])) count := int(binary.BigEndian.Uint16(data[2:]))
result := make([]sfntCmap, 0, count) result := make([]sfntCmap, 0, count)
parsed := make(map[uint32]map[rune]uint16) parsed := make(map[uint32]sfntCmap)
for i := range count { for i := range count {
record := data[4+8*i:] record := data[4+8*i:]
offset := binary.BigEndian.Uint32(record[4:]) offset := binary.BigEndian.Uint32(record[4:])
@@ -128,23 +132,44 @@ func readSFNTCmaps(data []byte) ([]sfntCmap, error) {
} }
sub := data[offset:] sub := data[offset:]
format := binary.BigEndian.Uint16(sub) format := binary.BigEndian.Uint16(sub)
mapping, found := parsed[offset] cmap, found := parsed[offset]
if !found { if !found {
mapping = make(map[rune]uint16) cmap.mapping = make(map[rune]uint16)
var err error var err error
switch format { switch format {
case 0: case 0:
parseCmapFormat0(sub, mapping) parseCmapFormat0(sub, cmap.mapping)
case 4: case 4:
parseCmapFormat4(sub, mapping) parseCmapFormat4(sub, cmap.mapping)
case 6: case 6:
parseCmapFormat6(sub, mapping) parseCmapFormat6(sub, cmap.mapping)
case 12: case 12:
parseCmapFormat12(sub, mapping) if len(sub) >= 16 {
length := uint64(binary.BigEndian.Uint32(sub[4:]))
count := uint64(binary.BigEndian.Uint32(sub[12:]))
if length >= 16 && length <= uint64(len(sub)) && count <= (length-16)/12 {
cmap.groups = sub[16 : 16+12*count]
for offset := 0; offset < len(cmap.groups); offset += 12 {
first := binary.BigEndian.Uint32(cmap.groups[offset:])
last := binary.BigEndian.Uint32(cmap.groups[offset+4:])
if first > last || offset != 0 && first <= binary.BigEndian.Uint32(cmap.groups[offset-8:]) {
cmap.groups = nil
parseCmapFormat12(sub, cmap.mapping)
break
}
}
if cmap.groups != nil {
cmap.mapping = nil
for range cmap.pairs() {
cmap.entries++
}
}
}
}
case 2: case 2:
mapping, err = parseCmapFormat2(sub) cmap.mapping, err = parseCmapFormat2(sub)
case 8, 10, 13: case 8, 10, 13:
mapping, err = parseCmapExtended(sub) cmap.mapping, err = parseCmapExtended(sub)
case 14: case 14:
default: default:
return nil, fmt.Errorf("unsupported cmap format %d", format) return nil, fmt.Errorf("unsupported cmap format %d", format)
@@ -152,13 +177,67 @@ func readSFNTCmaps(data []byte) ([]sfntCmap, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
parsed[offset] = mapping parsed[offset] = cmap
} }
result = append(result, sfntCmap{platform: binary.BigEndian.Uint16(record), encoding: binary.BigEndian.Uint16(record[2:]), format: format, mapping: mapping, duplicate: found}) cmap.platform, cmap.encoding, cmap.format, cmap.duplicate = binary.BigEndian.Uint16(record), binary.BigEndian.Uint16(record[2:]), format, found
result = append(result, cmap)
} }
return result, nil return result, nil
} }
// pairs 按原映射返回字符与字形,连续编码段不展开存储
// 返回: iter.Seq2[rune, uint16] 非零有效字符映射
func (c sfntCmap) pairs() iter.Seq2[rune, uint16] {
return func(yield func(rune, uint16) bool) {
for char, glyph := range c.mapping {
if !yield(char, glyph) {
return
}
}
for offset := 0; offset+12 <= len(c.groups); offset += 12 {
group := c.groups[offset:]
first, last, glyph := binary.BigEndian.Uint32(group), binary.BigEndian.Uint32(group[4:]), binary.BigEndian.Uint32(group[8:])
if first > last || first > utf8.MaxRune || glyph > 65535 {
continue
}
end := min(uint64(last), uint64(first)+65535-uint64(glyph), uint64(utf8.MaxRune))
for char := uint64(first); char <= end; char++ {
id := uint16(uint64(glyph) + char - uint64(first))
if id != 0 && utf8.ValidRune(rune(char)) && !yield(rune(char), id) {
return
}
}
}
}
}
// lookup 查找原字符映射,连续编码段使用有序区间查询
// 入参: char 字符
// 返回: uint16 字形编号, bool 是否存在映射
func (c sfntCmap) lookup(char rune) (uint16, bool) {
if glyph, ok := c.mapping[char]; ok {
return glyph, true
}
if !utf8.ValidRune(char) {
return 0, false
}
low, high := 0, len(c.groups)/12
for low < high {
middle := low + (high-low)/2
group := c.groups[middle*12:]
first, last := binary.BigEndian.Uint32(group), binary.BigEndian.Uint32(group[4:])
if uint32(char) < first {
high = middle
} else if uint32(char) > last {
low = middle + 1
} else {
glyph := uint64(binary.BigEndian.Uint32(group[8:])) + uint64(uint32(char)-first)
return uint16(glyph), glyph > 0 && glyph <= 65535
}
}
return 0, false
}
// UnitsPerEm 返回字体设计单位 // UnitsPerEm 返回字体设计单位
// 返回: uint16 每em单位数 // 返回: uint16 每em单位数
func (f *SFNT) UnitsPerEm() uint16 { return f.units } func (f *SFNT) UnitsPerEm() uint16 { return f.units }
@@ -172,7 +251,7 @@ func (f *SFNT) NumGlyphs() uint16 { return f.count }
func (f *SFNT) CmapEntries() uint64 { func (f *SFNT) CmapEntries() uint64 {
var count uint64 var count uint64
for _, cmap := range f.cmaps { for _, cmap := range f.cmaps {
count += uint64(len(cmap.mapping)) count += uint64(len(cmap.mapping)) + cmap.entries
} }
return count return count
} }
@@ -185,14 +264,14 @@ func (f *SFNT) GlyphIndex(char rune) uint16 {
if cmap.duplicate { if cmap.duplicate {
continue continue
} }
if gid := cmap.mapping[char]; gid != 0 { if gid, ok := cmap.lookup(char); ok && gid != 0 {
return gid return gid
} }
} }
return 0 return 0
} }
// glyphCharacters 返回指向指定字形的全部字符 // glyphCharacters 返回指向指定字形的全部字符,连续区间按需反查
// 入参: gid 字形编号 // 入参: gid 字形编号
// 返回: []rune 排序后的字符 // 返回: []rune 排序后的字符
func (f *SFNT) glyphCharacters(gid uint16) []rune { func (f *SFNT) glyphCharacters(gid uint16) []rune {
@@ -204,7 +283,7 @@ func (f *SFNT) glyphCharacters(gid uint16) []rune {
continue continue
} }
for char, glyph := range cmap.mapping { for char, glyph := range cmap.mapping {
if glyph != 0 && !seen[char] { if glyph != 0 && !seen[char] && f.GlyphIndex(char) == glyph {
f.reverse[glyph] = append(f.reverse[glyph], char) f.reverse[glyph] = append(f.reverse[glyph], char)
seen[char] = true seen[char] = true
} }
@@ -214,7 +293,28 @@ func (f *SFNT) glyphCharacters(gid uint16) []rune {
slices.Sort(chars) slices.Sort(chars)
} }
}) })
return f.reverse[gid] if gid == 0 {
return nil
}
chars := slices.Clone(f.reverse[gid])
for _, cmap := range f.cmaps {
if cmap.duplicate {
continue
}
for offset := 0; offset+12 <= len(cmap.groups); offset += 12 {
group := cmap.groups[offset:]
first, last, glyph := binary.BigEndian.Uint32(group), binary.BigEndian.Uint32(group[4:]), binary.BigEndian.Uint32(group[8:])
if first > last || uint32(gid) < glyph {
continue
}
char := uint64(first) + uint64(uint32(gid)-glyph)
if char <= uint64(last) && char <= utf8.MaxRune && utf8.ValidRune(rune(char)) && f.GlyphIndex(rune(char)) == gid {
chars = append(chars, rune(char))
}
}
}
slices.Sort(chars)
return slices.Compact(chars)
} }
// GlyphAdvance 返回水平字宽 // GlyphAdvance 返回水平字宽
+1 -1
View File
@@ -197,7 +197,7 @@ func subsetSourceSFNT(sfnt *SFNT, usage *Usage, size int) []byte {
for char := range usage.Characters { for char := range usage.Characters {
id := sfnt.GlyphIndex(char) id := sfnt.GlyphIndex(char)
for _, cmap := range unicodeTables { for _, cmap := range unicodeTables {
if alternate, ok := cmap.mapping[char]; ok && alternate != id { if alternate, ok := cmap.lookup(char); ok && alternate != id {
return nil return nil
} }
} }
+128 -2
View File
@@ -291,6 +291,132 @@ func editorObjectFontScan(object GraphicObject) bool {
// 入参: progress 保存进度回调 // 入参: progress 保存进度回调
// 返回: map[string][]byte 包内字体子集, error 错误信息 // 返回: map[string][]byte 包内字体子集, error 错误信息
func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) { func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) {
result, err := e.subsetNewFonts(progress)
if err != nil {
return nil, err
}
ctx := context.Background()
if e.output != nil {
ctx = e.output.ctx
}
source, err := e.subsetSnapshotFonts(ctx, progress.withContext(ctx))
if err != nil {
return nil, err
}
if result == nil {
return source, nil
}
maps.Copy(result, source)
return result, nil
}
// subsetSnapshotFonts 在写出时扫描快照中新增字体的完整包引用,不改动既有字体
// 入参: ctx 取消上下文, progress 写出进度
// 返回: map[string][]byte 新增字体的安全子集, error 扫描或裁剪错误
func (e *Editor) subsetSnapshotFonts(ctx context.Context, progress editorProgress) (map[string][]byte, error) {
if e.source == nil || e.backends.FontResources == nil {
return nil, ctx.Err()
}
candidates := make(map[string]bool)
for _, resource := range e.resources {
if err := ctx.Err(); err != nil {
return nil, err
}
if resource.font != nil && resource.font.FontFile != "" && e.source.reader.fileNames[resource.name] {
candidates[resource.name] = true
}
}
if len(candidates) == 0 {
return nil, ctx.Err()
}
view, err := e.readerContext(ctx, progress)
if err != nil {
return nil, err
}
defer view.Close()
refs, resources, references, safe, err := scanEditorResources(ctx, view, view.files, nil, maps.Clone(view.fileNames), progress, true)
if err != nil || !safe {
return nil, err
}
scoped, err := resourceReferenceScopes(ctx, refs.documents, resources, references, "")
if err != nil || scoped == nil {
return nil, err
}
read := func(name string) ([]byte, error) { return view.readFileViewContext(ctx, name) }
usage, err := resourceFontUsage(ctx, resources, scoped, read)
if err != nil {
return nil, err
}
for _, source := range references {
for name := range source.files {
if font := usage[name]; font != nil && !source.versions[name] {
font.unsafe = true
}
}
for _, definition := range source.definitions {
for name := range definition.files {
if font := usage[name]; font != nil {
font.unsafe = true
}
}
}
}
result := make(map[string][]byte)
for i, name := range slices.Sorted(maps.Keys(candidates)) {
if err := progress.report("fonts", i, len(candidates)); err != nil {
return nil, err
}
font := usage[name]
if font == nil || font.unsafe || len(font.chars)+len(font.glyphs) == 0 {
continue
}
data, err := read(name)
if err != nil {
return nil, err
}
parsed, err := e.backends.FontResources.OpenFontResource(FontFile{Data: data}, 0)
if err != nil {
return nil, err
}
if !parsed.CanSubset {
continue
}
preserve := len(font.glyphs) != 0
glyphs := maps.Clone(font.glyphs)
for char := range font.chars {
if err := ctx.Err(); err != nil {
return nil, err
}
glyphs[parsed.GlyphIndex(char)] = true
}
glyphs[0] = true
for id := range glyphs {
if id >= parsed.NumGlyphs() {
font.unsafe = true
break
}
}
if font.unsafe {
continue
}
subset, err := e.backends.FontResources.SubsetFont(data, slices.Sorted(maps.Keys(glyphs)), preserve)
if err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
if len(subset) != 0 && len(subset) < len(data) {
result[name] = subset
}
}
return result, progress.report("fonts", len(candidates), len(candidates))
}
// subsetNewFonts 解析尚未物化为源资源的新增字体,保持编辑字体完整
// 入参: progress 写出进度
// 返回: map[string][]byte 包内字体子集, error 读取或裁剪错误
func (e *Editor) subsetNewFonts(progress editorProgress) (map[string][]byte, error) {
ctx := context.Background() ctx := context.Background()
if e.output != nil { if e.output != nil {
ctx = e.output.ctx ctx = e.output.ctx
@@ -303,7 +429,7 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
return nil, err return nil, err
} }
if !slices.ContainsFunc(e.resources, func(resource editorResource) bool { if !slices.ContainsFunc(e.resources, func(resource editorResource) bool {
return resource.font != nil && resource.font.FontFile != "" return resource.font != nil && resource.font.FontFile != "" && (e.source == nil || !e.source.reader.fileNames[resource.name])
}) { }) {
return nil, nil return nil, nil
} }
@@ -318,7 +444,7 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
if err := progress.report("fonts", i, total); err != nil { if err := progress.report("fonts", i, total); err != nil {
return nil, err return nil, err
} }
if resource.font.FontFile == "" { if resource.font.FontFile == "" || e.source != nil && e.source.reader.fileNames[resource.name] {
continue continue
} }
resourceFont := e.fonts[resource.font.ID] resourceFont := e.fonts[resource.font.ID]
+1
View File
@@ -82,6 +82,7 @@ type pdfImporter struct {
pageActionPage int pageActionPage int
fontIDs map[*pdfgo.Font]string fontIDs map[*pdfgo.Font]string
fonts map[*pdfgo.Font]*pdfImportedFont fonts map[*pdfgo.Font]*pdfImportedFont
fontPrograms map[pdfFontProgramKey]*pdfImportedFont
symbolFonts map[*pdfgo.Font]*pdfImportedFont symbolFonts map[*pdfgo.Font]*pdfImportedFont
fontWarnings map[string]bool fontWarnings map[string]bool
clipTexts map[*pdfgo.TextClip][]TextObject clipTexts map[*pdfgo.TextClip][]TextObject
+4 -1
View File
@@ -398,10 +398,13 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
if p.symbolFonts == nil { if p.symbolFonts == nil {
p.symbolFonts = make(map[*pdfgo.Font]*pdfImportedFont) p.symbolFonts = make(map[*pdfgo.Font]*pdfImportedFont)
} }
if p.fontPrograms == nil {
p.fontPrograms = make(map[pdfFontProgramKey]*pdfImportedFont)
}
if p.clipPaths == nil { if p.clipPaths == nil {
p.clipPaths = resourceCache[[32]byte, pdfClipPath](p.resources, 8<<20) p.clipPaths = resourceCache[[32]byte, pdfClipPath](p.resources, 8<<20)
} }
local := pdfImporter{resources: p.resources, ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer, matrix: p.matrix, pageBox: p.pageBox, pageWidth: p.pageWidth, pageHeight: p.pageHeight, rasterDPI: p.rasterDPI, warning: p.warning, rasterWarned: true, fontIDs: map[*pdfgo.Font]string{}, fonts: p.fonts, symbolFonts: p.symbolFonts, clipPaths: p.clipPaths, textOrigins: p.textOrigins} local := pdfImporter{resources: p.resources, ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer, matrix: p.matrix, pageBox: p.pageBox, pageWidth: p.pageWidth, pageHeight: p.pageHeight, rasterDPI: p.rasterDPI, warning: p.warning, rasterWarned: true, fontIDs: map[*pdfgo.Font]string{}, fonts: p.fonts, fontPrograms: p.fontPrograms, symbolFonts: p.symbolFonts, clipPaths: p.clipPaths, textOrigins: p.textOrigins}
if err := build(&local); err != nil { if err := build(&local); err != nil {
return nil, box, err return nil, box, err
} }
+30 -11
View File
@@ -38,6 +38,12 @@ type pdfImportedFont struct {
repairLimit uint16 repairLimit uint16
} }
// pdfFontProgramKey 区分字体封装内容及交给资源后端的来源名称
type pdfFontProgramKey struct {
checksum [32]byte
name string
}
// pdfTextReplacement 记录一个完整替换区段的文字数量与转换状态 // pdfTextReplacement 记录一个完整替换区段的文字数量与转换状态
type pdfTextReplacement struct { type pdfTextReplacement struct {
text *pdfgo.ReplacementText text *pdfgo.ReplacementText
@@ -661,21 +667,27 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error)
if p.editor.backends.FontResources == nil { if p.editor.backends.FontResources == nil {
return nil, fmt.Errorf("PDF font %s resource: font resources: %w", source.Name, ErrBackendUnavailable) return nil, fmt.Errorf("PDF font %s resource: font resources: %w", source.Name, ErrBackendUnavailable)
} }
result.resource, err = p.editor.backends.FontResources.OpenFontResource(FontFile{Name: source.Name + ".ttf", Data: program}, 0)
if err != nil {
return nil, fmt.Errorf("PDF font %s resource: %w", source.Name, err)
}
if p.editor.backends.Fonts == nil { if p.editor.backends.Fonts == nil {
return nil, fmt.Errorf("PDF font backend unavailable") return nil, fmt.Errorf("PDF font backend unavailable")
} }
result.metrics, err = p.editor.backends.Fonts.OpenFont(result.resource.Data) key := pdfFontProgramKey{checksum: sha256.Sum256(program), name: source.Name}
if err != nil { if cached := p.fontPrograms[key]; cached != nil {
return nil, fmt.Errorf("PDF font %s metrics: %w", source.Name, err) result.resource, result.metrics, result.checksum = cached.resource, cached.metrics, cached.checksum
} else {
result.resource, err = p.editor.backends.FontResources.OpenFontResource(FontFile{Name: source.Name + ".ttf", Data: program}, 0)
if err != nil {
return nil, fmt.Errorf("PDF font %s resource: %w", source.Name, err)
}
result.metrics, err = p.editor.backends.Fonts.OpenFont(result.resource.Data)
if err != nil {
return nil, fmt.Errorf("PDF font %s metrics: %w", source.Name, err)
}
if _, ok := result.metrics.(FontOutlines); !ok {
return nil, fmt.Errorf("PDF font backend does not provide glyph outlines")
}
result.checksum = sha256.Sum256(result.resource.Data)
} }
if _, ok := result.metrics.(FontOutlines); !ok { result.repairLimit = repair.MetricsLimit
return nil, fmt.Errorf("PDF font backend does not provide glyph outlines")
}
result.checksum, result.repairLimit = sha256.Sum256(result.resource.Data), repair.MetricsLimit
if source.Recovered || programRecovered { if source.Recovered || programRecovered {
if err := p.preservePDFObject(p.ctx, "Font", source.Dictionary); err != nil { if err := p.preservePDFObject(p.ctx, "Font", source.Dictionary); err != nil {
return nil, err return nil, err
@@ -690,6 +702,13 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error)
if p.fonts == nil { if p.fonts == nil {
p.fonts = make(map[*pdfgo.Font]*pdfImportedFont) p.fonts = make(map[*pdfgo.Font]*pdfImportedFont)
} }
if err := p.ctx.Err(); err != nil {
return nil, err
}
if p.fontPrograms == nil {
p.fontPrograms = make(map[pdfFontProgramKey]*pdfImportedFont)
}
p.fontPrograms[key] = result
p.fonts[source] = result p.fonts[source] = result
return result, nil return result, nil
} }
+50 -1
View File
@@ -19,6 +19,7 @@ import (
"image" "image"
"image/color" "image/color"
"image/draw" "image/draw"
"math"
"slices" "slices"
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
@@ -120,7 +121,7 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima
} }
continue continue
} }
p, err := rasterCanvasPath(cmd.Path, pathData) p, err := rasterCanvasPath(visibleRasterPath(cmd, page), pathData)
if err != nil { if err != nil {
return nil, fmt.Errorf("canvas command %d: %w", i, err) return nil, fmt.Errorf("canvas command %d: %w", i, err)
} }
@@ -228,6 +229,54 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima
return img, nil return img, nil
} }
// visibleRasterPath 排除填充区域外的完整子路径,保留曲线控制范围与抗锯齿边缘
// 入参: command 绘制指令, page 目标页面
// 返回: []RasterSegment 可见子路径,描边和短路径沿用原数据
func visibleRasterPath(command RasterCommand, page *RasterPage) []RasterSegment {
if command.Stroke != nil || len(command.Path) < 256 || command.Path[0].Verb != RasterMove {
return command.Path
}
padding := 25.4 / page.DPI
var visible []RasterSegment
filtered := false
start := 0
minX, minY, maxX, maxY := math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1)
flush := func(end int) {
if minX <= page.Width+padding && maxX >= -padding && minY <= page.Height+padding && maxY >= -padding {
if filtered {
visible = append(visible, command.Path[start:end]...)
}
} else if !filtered {
filtered = true
visible = append(visible, command.Path[:start]...)
}
}
include := func(point RasterPoint) bool {
point = command.Transform.Apply(point)
if !finite(point.X) || !finite(point.Y) {
return false
}
minX, minY = math.Min(minX, point.X), math.Min(minY, point.Y)
maxX, maxY = math.Max(maxX, point.X), math.Max(maxY, point.Y)
return true
}
for index, segment := range command.Path {
if segment.Verb == RasterMove && index != start {
flush(index)
start = index
minX, minY, maxX, maxY = math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1)
}
if segment.Verb > RasterClose || segment.Verb != RasterClose && !include(segment.End) || (segment.Verb == RasterQuad || segment.Verb == RasterCubic) && !include(segment.Control1) || segment.Verb == RasterCubic && !include(segment.Control2) {
return command.Path
}
}
flush(len(command.Path))
if !filtered {
return command.Path
}
return visible
}
// rasterCanvasPath 预留路径容量,保留光栅指令的构建及闭合规则 // rasterCanvasPath 预留路径容量,保留光栅指令的构建及闭合规则
// 入参: path 只读光栅路径, buffer 可复用的独占路径缓冲 // 入参: path 只读光栅路径, buffer 可复用的独占路径缓冲
// 返回: *canvas.Path 使用独占缓冲的绘制路径, error 不支持的指令 // 返回: *canvas.Path 使用独占缓冲的绘制路径, error 不支持的指令
+16 -5
View File
@@ -25,8 +25,9 @@ import (
// clipRenderer 收集图形和文字的绘制轮廓 // clipRenderer 收集图形和文字的绘制轮廓
type clipRenderer struct { type clipRenderer struct {
path *canvas.Path path *canvas.Path
clip *canvas.Path clip *canvas.Path
unions *renderCache[[32]byte, *canvas.Path]
} }
// Size 返回不限定边界的裁剪画布尺寸 // Size 返回不限定边界的裁剪画布尺寸
@@ -57,13 +58,23 @@ func (r *clipRenderer) RenderText(text *canvas.Text, m canvas.Matrix) {
// 入参: img 图像, m 变换矩阵 // 入参: img 图像, m 变换矩阵
func (r *clipRenderer) RenderImage(img image.Image, m canvas.Matrix) {} func (r *clipRenderer) RenderImage(img image.Image, m canvas.Matrix) {}
// add 合并裁剪轮廓 // add 合并裁剪轮廓,按完整输入复用已有并集
// 入参: path 裁剪路径 // 入参: path 裁剪路径
func (r *clipRenderer) add(path *canvas.Path) { func (r *clipRenderer) add(path *canvas.Path) {
if r.path == nil { if r.path == nil {
r.path = path r.path = path
} else { } else if r.unions == nil || path == nil {
r.path = unionClipPath(r.path, path) r.path = unionClipPath(r.path, path)
} else {
key := canvasGeometryDigest(r.path, path)
if merged, ok := r.unions.get(key); ok {
r.path = merged.Copy()
} else {
r.path = unionClipPath(r.path, path)
if cost := len(r.path.Data())*8 + 256; cost <= r.unions.limit {
r.unions.put(key, r.path.Copy(), cost)
}
}
} }
} }
@@ -96,7 +107,7 @@ func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, ma
} }
var p *canvas.Path var p *canvas.Path
for _, clip := range clips.Clip { for _, clip := range clips.Clip {
renderer := &clipRenderer{} renderer := &clipRenderer{unions: r.canvasClipUnions()}
for _, area := range clip.Area { for _, area := range clip.Area {
areaCTM := matrix.Multiply(NewMatrix(area.CTM)) areaCTM := matrix.Multiply(NewMatrix(area.CTM))
for _, pathObj := range area.Path { for _, pathObj := range area.Path {
+8
View File
@@ -556,6 +556,14 @@ func clipLinearFillGeometry(path, clip GeometryPath) (GeometryPath, bool, error)
return *geometryFromCanvasPath(result), true, nil return *geometryFromCanvasPath(result), true, nil
} }
// invertCanvasMatrix 对有限可逆矩阵求逆,保留非零小尺度变换
// 入参: matrix 画布仿射矩阵
// 返回: canvas.Matrix 逆矩阵, bool 是否存在有限逆矩阵
func invertCanvasMatrix(matrix canvas.Matrix) (canvas.Matrix, bool) {
inverse, ok := MatrixFromValues([6]float64{matrix[0][0], matrix[1][0], matrix[0][1], matrix[1][1], matrix[0][2], matrix[1][2]}).Invert()
return canvas.Matrix{{inverse.a, inverse.c, inverse.e}, {inverse.b, inverse.d, inverse.f}}, ok
}
// actionCanvasMatrix 转换页面坐标矩阵,不翻转Y轴 // actionCanvasMatrix 转换页面坐标矩阵,不翻转Y轴
// 入参: matrix 页面变换 // 入参: matrix 页面变换
// 返回: canvas.Matrix 路径变换 // 返回: canvas.Matrix 路径变换
+7 -3
View File
@@ -227,7 +227,11 @@ func (run *TextRun) addSpan(start, end int, bounds canvas.Rect, m canvas.Matrix,
if path.Empty() { if path.Empty() {
return return
} }
bounds = path.Transform(m.Inv()).Bounds() inverse, ok := invertCanvasMatrix(m)
if !ok {
return
}
bounds = path.Transform(inverse).Bounds()
} }
if bounds.W() < 0 || bounds.H() <= 0 { if bounds.W() < 0 || bounds.H() <= 0 {
return return
@@ -238,8 +242,8 @@ func (run *TextRun) addSpan(start, end int, bounds canvas.Rect, m canvas.Matrix,
span = run.Spans[n-1] span = run.Spans[n-1]
v := span.Matrix v := span.Matrix
previous := canvas.Matrix{{v[0], v[2], v[4]}, {v[1], v[3], v[5]}} previous := canvas.Matrix{{v[0], v[2], v[4]}, {v[1], v[3], v[5]}}
if previous.Det() != 0 { if inverse, ok := invertCanvasMatrix(previous); ok {
bounds = canvas.Rect{X1: 1, Y1: 1}.Add(canvas.Rect{X1: 1, Y1: 1}.Transform(previous.Inv().Mul(m))) bounds = canvas.Rect{X1: 1, Y1: 1}.Add(canvas.Rect{X1: 1, Y1: 1}.Transform(inverse.Mul(m)))
m = previous.Translate(bounds.X0, bounds.Y0).Scale(bounds.W(), bounds.H()) m = previous.Translate(bounds.X0, bounds.Y0).Scale(bounds.W(), bounds.H())
} }
run.Spans = run.Spans[:n-1] run.Spans = run.Spans[:n-1]
+18
View File
@@ -34,6 +34,9 @@ type canvasFontKey struct {
// canvasClipCacheKey 区分不依赖文档资源的共享裁剪缓存 // canvasClipCacheKey 区分不依赖文档资源的共享裁剪缓存
type canvasClipCacheKey struct{} type canvasClipCacheKey struct{}
// canvasClipUnionKey 区分文字和图形裁剪的共享并集缓存
type canvasClipUnionKey struct{}
// canvasPathCacheKey 区分不依赖文档资源的共享局部路径缓存 // canvasPathCacheKey 区分不依赖文档资源的共享局部路径缓存
type canvasPathCacheKey struct{} type canvasPathCacheKey struct{}
@@ -83,6 +86,21 @@ func (r *Renderer) canvasClipIntersections() *renderCache[[32]byte, *canvas.Path
return cache return cache
} }
// canvasClipUnions 复用相同输入轮廓的裁剪并集,不改变布尔运算顺序
// 返回: *renderCache[[32]byte, *canvas.Path] 只读裁剪并集缓存
func (r *Renderer) canvasClipUnions() *renderCache[[32]byte, *canvas.Path] {
key := canvasClipUnionKey{}
if state, ok := r.sharedBackendStates[key]; ok {
return state.(*renderCache[[32]byte, *canvas.Path])
}
if r.sharedBackendStates == nil {
r.sharedBackendStates = make(map[any]any)
}
cache := resourceCache[[32]byte, *canvas.Path](r.Reader.resources, 8<<20)
r.sharedBackendStates[key] = cache
return cache
}
// canvasParsedPaths 获取子渲染器共享的局部路径缓存,不保留对象变换 // canvasParsedPaths 获取子渲染器共享的局部路径缓存,不保留对象变换
// 返回: *renderCache[canvasPathKey, *canvas.Path] 路径缓存 // 返回: *renderCache[canvasPathKey, *canvas.Path] 路径缓存
func (r *Renderer) canvasParsedPaths() *renderCache[canvasPathKey, *canvas.Path] { func (r *Renderer) canvasParsedPaths() *renderCache[canvasPathKey, *canvas.Path] {
+13
View File
@@ -146,6 +146,19 @@ func (e *Editor) Snapshot(ctx context.Context) (*Editor, error) {
} }
snapshot.revision, snapshot.serial, snapshot.saveTarget = e.revision, e.serial, e.saveTarget snapshot.revision, snapshot.serial, snapshot.saveTarget = e.revision, e.serial, e.saveTarget
snapshot.backends, snapshot.fontDirs, snapshot.fontFS = e.backends, slices.Clone(e.fontDirs), slices.Clone(e.fontFS) snapshot.backends, snapshot.fontDirs, snapshot.fontFS = e.backends, slices.Clone(e.fontDirs), slices.Clone(e.fontFS)
for _, resource := range e.resources {
if err := ctx.Err(); err != nil {
return nil, err
}
if resource.font == nil || resource.font.FontFile == "" || !next.fileNames[resource.name] {
continue
}
data, err := next.readFileViewContext(ctx, resource.name)
if err != nil {
return nil, err
}
snapshot.resources = append(snapshot.resources, editorResource{name: resource.name, data: data, font: cloneEditorData(resource.font)})
}
if e.source != nil { if e.source != nil {
snapshot.source.retiredAttachments = maps.Clone(e.source.retiredAttachments) snapshot.source.retiredAttachments = maps.Clone(e.source.retiredAttachments)
snapshot.source.removedAttachments = maps.Clone(e.source.removedAttachments) snapshot.source.removedAttachments = maps.Clone(e.source.removedAttachments)