diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index 98551bf..d799400 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -7687,13 +7687,18 @@ function chooseResourceFile(action) { async function changeManagedResource(action, item, file) { if (!item || !state.editing) return; releaseResourcePreview(); + const page = Number(resourceElements.Page.value), object = resourceElements.Object.value; const format = file?.name.includes(".") ? file.name.split(".").at(-1).toUpperCase() : ""; const changed = item.kind === "Attachment" && action !== "bind" && action !== "unbind" ? 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) { 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" ? "已绑定" : "已解绑"; + } } } diff --git a/internal/base/font/sfnt.go b/internal/base/font/sfnt.go index 11cf5ff..85c0c50 100644 --- a/internal/base/font/sfnt.go +++ b/internal/base/font/sfnt.go @@ -17,9 +17,11 @@ package font import ( "encoding/binary" "fmt" + "iter" "maps" "slices" "sync" + "unicode/utf8" "github.com/go-text/typesetting/font/opentype/tables" ) @@ -34,10 +36,12 @@ type SFNT struct { reverse map[uint16][]rune } -// sfntCmap 保存编码记录及对应字符映射,标记已解析的共享子表 +// sfntCmap 保存编码记录及字符映射,连续区间与共享子表不重复展开 type sfntCmap struct { platform, encoding, format uint16 mapping map[rune]uint16 + groups []byte + entries uint64 duplicate bool } @@ -90,7 +94,7 @@ func ParseSFNTTables(table map[string][]byte) (*SFNT, error) { if cmap.duplicate { continue } - for _, glyph := range cmap.mapping { + for _, glyph := range cmap.pairs() { if glyph >= f.count { 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:])) result := make([]sfntCmap, 0, count) - parsed := make(map[uint32]map[rune]uint16) + parsed := make(map[uint32]sfntCmap) for i := range count { record := data[4+8*i:] offset := binary.BigEndian.Uint32(record[4:]) @@ -128,23 +132,44 @@ func readSFNTCmaps(data []byte) ([]sfntCmap, error) { } sub := data[offset:] format := binary.BigEndian.Uint16(sub) - mapping, found := parsed[offset] + cmap, found := parsed[offset] if !found { - mapping = make(map[rune]uint16) + cmap.mapping = make(map[rune]uint16) var err error switch format { case 0: - parseCmapFormat0(sub, mapping) + parseCmapFormat0(sub, cmap.mapping) case 4: - parseCmapFormat4(sub, mapping) + parseCmapFormat4(sub, cmap.mapping) case 6: - parseCmapFormat6(sub, mapping) + parseCmapFormat6(sub, cmap.mapping) 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: - mapping, err = parseCmapFormat2(sub) + cmap.mapping, err = parseCmapFormat2(sub) case 8, 10, 13: - mapping, err = parseCmapExtended(sub) + cmap.mapping, err = parseCmapExtended(sub) case 14: default: return nil, fmt.Errorf("unsupported cmap format %d", format) @@ -152,13 +177,67 @@ func readSFNTCmaps(data []byte) ([]sfntCmap, error) { if err != nil { 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 } +// 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 返回字体设计单位 // 返回: uint16 每em单位数 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 { var count uint64 for _, cmap := range f.cmaps { - count += uint64(len(cmap.mapping)) + count += uint64(len(cmap.mapping)) + cmap.entries } return count } @@ -185,14 +264,14 @@ func (f *SFNT) GlyphIndex(char rune) uint16 { if cmap.duplicate { continue } - if gid := cmap.mapping[char]; gid != 0 { + if gid, ok := cmap.lookup(char); ok && gid != 0 { return gid } } return 0 } -// glyphCharacters 返回指向指定字形的全部字符 +// glyphCharacters 返回指向指定字形的全部字符,连续区间按需反查 // 入参: gid 字形编号 // 返回: []rune 排序后的字符 func (f *SFNT) glyphCharacters(gid uint16) []rune { @@ -204,7 +283,7 @@ func (f *SFNT) glyphCharacters(gid uint16) []rune { continue } 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) seen[char] = true } @@ -214,7 +293,28 @@ func (f *SFNT) glyphCharacters(gid uint16) []rune { 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 返回水平字宽 diff --git a/internal/base/font/subset.go b/internal/base/font/subset.go index 301bd57..ab66f54 100644 --- a/internal/base/font/subset.go +++ b/internal/base/font/subset.go @@ -197,7 +197,7 @@ func subsetSourceSFNT(sfnt *SFNT, usage *Usage, size int) []byte { for char := range usage.Characters { id := sfnt.GlyphIndex(char) for _, cmap := range unicodeTables { - if alternate, ok := cmap.mapping[char]; ok && alternate != id { + if alternate, ok := cmap.lookup(char); ok && alternate != id { return nil } } diff --git a/ofdgo_editor_font.go b/ofdgo_editor_font.go index 2762975..5fc3025 100644 --- a/ofdgo_editor_font.go +++ b/ofdgo_editor_font.go @@ -291,6 +291,132 @@ func editorObjectFontScan(object GraphicObject) bool { // 入参: progress 保存进度回调 // 返回: 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() if e.output != nil { ctx = e.output.ctx @@ -303,7 +429,7 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) return nil, err } 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 } @@ -318,7 +444,7 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) if err := progress.report("fonts", i, total); err != nil { return nil, err } - if resource.font.FontFile == "" { + if resource.font.FontFile == "" || e.source != nil && e.source.reader.fileNames[resource.name] { continue } resourceFont := e.fonts[resource.font.ID] diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index 1989d62..6d43295 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -82,6 +82,7 @@ type pdfImporter struct { pageActionPage int fontIDs map[*pdfgo.Font]string fonts map[*pdfgo.Font]*pdfImportedFont + fontPrograms map[pdfFontProgramKey]*pdfImportedFont symbolFonts map[*pdfgo.Font]*pdfImportedFont fontWarnings map[string]bool clipTexts map[*pdfgo.TextClip][]TextObject diff --git a/ofdgo_pdf_group.go b/ofdgo_pdf_group.go index 42812d9..94a90c8 100644 --- a/ofdgo_pdf_group.go +++ b/ofdgo_pdf_group.go @@ -398,10 +398,13 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage, if p.symbolFonts == nil { p.symbolFonts = make(map[*pdfgo.Font]*pdfImportedFont) } + if p.fontPrograms == nil { + p.fontPrograms = make(map[pdfFontProgramKey]*pdfImportedFont) + } if p.clipPaths == nil { 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 { return nil, box, err } diff --git a/ofdgo_pdf_text.go b/ofdgo_pdf_text.go index f3f51a7..a988db2 100644 --- a/ofdgo_pdf_text.go +++ b/ofdgo_pdf_text.go @@ -38,6 +38,12 @@ type pdfImportedFont struct { repairLimit uint16 } +// pdfFontProgramKey 区分字体封装内容及交给资源后端的来源名称 +type pdfFontProgramKey struct { + checksum [32]byte + name string +} + // pdfTextReplacement 记录一个完整替换区段的文字数量与转换状态 type pdfTextReplacement struct { text *pdfgo.ReplacementText @@ -661,21 +667,27 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error) if p.editor.backends.FontResources == nil { 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 { return nil, fmt.Errorf("PDF font backend unavailable") } - 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) + key := pdfFontProgramKey{checksum: sha256.Sum256(program), name: source.Name} + if cached := p.fontPrograms[key]; cached != nil { + 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 { - return nil, fmt.Errorf("PDF font backend does not provide glyph outlines") - } - result.checksum, result.repairLimit = sha256.Sum256(result.resource.Data), repair.MetricsLimit + result.repairLimit = repair.MetricsLimit if source.Recovered || programRecovered { if err := p.preservePDFObject(p.ctx, "Font", source.Dictionary); err != nil { return nil, err @@ -690,6 +702,13 @@ func (p *pdfImporter) importedFont(source *pdfgo.Font) (*pdfImportedFont, error) if p.fonts == nil { 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 return result, nil } diff --git a/ofdgo_renderer_canvas.go b/ofdgo_renderer_canvas.go index 704ecaf..41fcf9f 100644 --- a/ofdgo_renderer_canvas.go +++ b/ofdgo_renderer_canvas.go @@ -19,6 +19,7 @@ import ( "image" "image/color" "image/draw" + "math" "slices" "github.com/tdewolff/canvas" @@ -120,7 +121,7 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima } continue } - p, err := rasterCanvasPath(cmd.Path, pathData) + p, err := rasterCanvasPath(visibleRasterPath(cmd, page), pathData) if err != nil { 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 } +// 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 预留路径容量,保留光栅指令的构建及闭合规则 // 入参: path 只读光栅路径, buffer 可复用的独占路径缓冲 // 返回: *canvas.Path 使用独占缓冲的绘制路径, error 不支持的指令 diff --git a/ofdgo_renderer_clip_canvas.go b/ofdgo_renderer_clip_canvas.go index b07004c..5260775 100644 --- a/ofdgo_renderer_clip_canvas.go +++ b/ofdgo_renderer_clip_canvas.go @@ -25,8 +25,9 @@ import ( // clipRenderer 收集图形和文字的绘制轮廓 type clipRenderer struct { - path *canvas.Path - clip *canvas.Path + path *canvas.Path + clip *canvas.Path + unions *renderCache[[32]byte, *canvas.Path] } // Size 返回不限定边界的裁剪画布尺寸 @@ -57,13 +58,23 @@ func (r *clipRenderer) RenderText(text *canvas.Text, m canvas.Matrix) { // 入参: img 图像, m 变换矩阵 func (r *clipRenderer) RenderImage(img image.Image, m canvas.Matrix) {} -// add 合并裁剪轮廓 +// add 合并裁剪轮廓,按完整输入复用已有并集 // 入参: path 裁剪路径 func (r *clipRenderer) add(path *canvas.Path) { if r.path == nil { r.path = path - } else { + } else if r.unions == nil || path == nil { 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 for _, clip := range clips.Clip { - renderer := &clipRenderer{} + renderer := &clipRenderer{unions: r.canvasClipUnions()} for _, area := range clip.Area { areaCTM := matrix.Multiply(NewMatrix(area.CTM)) for _, pathObj := range area.Path { diff --git a/ofdgo_renderer_geom_canvas.go b/ofdgo_renderer_geom_canvas.go index 5c94589..31eb081 100644 --- a/ofdgo_renderer_geom_canvas.go +++ b/ofdgo_renderer_geom_canvas.go @@ -556,6 +556,14 @@ func clipLinearFillGeometry(path, clip GeometryPath) (GeometryPath, bool, error) 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轴 // 入参: matrix 页面变换 // 返回: canvas.Matrix 路径变换 diff --git a/ofdgo_renderer_measure_canvas.go b/ofdgo_renderer_measure_canvas.go index 4fc4389..14fbd8e 100644 --- a/ofdgo_renderer_measure_canvas.go +++ b/ofdgo_renderer_measure_canvas.go @@ -227,7 +227,11 @@ func (run *TextRun) addSpan(start, end int, bounds canvas.Rect, m canvas.Matrix, if path.Empty() { 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 { return @@ -238,8 +242,8 @@ func (run *TextRun) addSpan(start, end int, bounds canvas.Rect, m canvas.Matrix, span = run.Spans[n-1] v := span.Matrix previous := canvas.Matrix{{v[0], v[2], v[4]}, {v[1], v[3], v[5]}} - if previous.Det() != 0 { - bounds = canvas.Rect{X1: 1, Y1: 1}.Add(canvas.Rect{X1: 1, Y1: 1}.Transform(previous.Inv().Mul(m))) + if inverse, ok := invertCanvasMatrix(previous); ok { + 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()) } run.Spans = run.Spans[:n-1] diff --git a/ofdgo_renderer_state_canvas.go b/ofdgo_renderer_state_canvas.go index 36f550c..e71cb38 100644 --- a/ofdgo_renderer_state_canvas.go +++ b/ofdgo_renderer_state_canvas.go @@ -34,6 +34,9 @@ type canvasFontKey struct { // canvasClipCacheKey 区分不依赖文档资源的共享裁剪缓存 type canvasClipCacheKey struct{} +// canvasClipUnionKey 区分文字和图形裁剪的共享并集缓存 +type canvasClipUnionKey struct{} + // canvasPathCacheKey 区分不依赖文档资源的共享局部路径缓存 type canvasPathCacheKey struct{} @@ -83,6 +86,21 @@ func (r *Renderer) canvasClipIntersections() *renderCache[[32]byte, *canvas.Path 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 获取子渲染器共享的局部路径缓存,不保留对象变换 // 返回: *renderCache[canvasPathKey, *canvas.Path] 路径缓存 func (r *Renderer) canvasParsedPaths() *renderCache[canvasPathKey, *canvas.Path] { diff --git a/ofdgo_writer.go b/ofdgo_writer.go index 6af2a6b..c064fad 100644 --- a/ofdgo_writer.go +++ b/ofdgo_writer.go @@ -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.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 { snapshot.source.retiredAttachments = maps.Clone(e.source.retiredAttachments) snapshot.source.removedAttachments = maps.Clone(e.source.removedAttachments)