feat(裁剪合成): 优化裁剪合成与取消响应
build.yaml / build (push) Successful in 56s

This commit is contained in:
xiaoqidun committed 2026-10-08 15:22:55 +08:00
1 parent 245fffed84
commit ee873443e0
10 files changed
+160 -13

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1 github.com/xiaoqidun/jbig2 v1.1.1
github.com/xiaoqidun/pdfgo v0.0.0-20261008045044-ada80114c916 github.com/xiaoqidun/pdfgo v0.0.0-20261008072006-d519e7a38749
golang.org/x/crypto v0.57.0 golang.org/x/crypto v0.57.0
golang.org/x/image v0.46.0 golang.org/x/image v0.46.0
golang.org/x/text v0.42.0 golang.org/x/text v0.42.0
+2 -2
View File
@@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
github.com/xiaoqidun/pdfgo v0.0.0-20261008045044-ada80114c916 h1:SwXbZ6zLtTCxMeV7A5GBSxgNhyVfdzWLhPh38IdkVTc= github.com/xiaoqidun/pdfgo v0.0.0-20261008072006-d519e7a38749 h1:jWImuld2SXZp826sudLJJUUOEEMPj/Jy77ItAE778Sg=
github.com/xiaoqidun/pdfgo v0.0.0-20261008045044-ada80114c916/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= github.com/xiaoqidun/pdfgo v0.0.0-20261008072006-d519e7a38749/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+23 -1
View File
@@ -15,6 +15,7 @@
package ofdgo package ofdgo
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"image" "image"
@@ -188,10 +189,31 @@ func WithRasterBackend(backend RasterBackend) RendererOption {
// 入参: page 页面内容 // 入参: page 页面内容
// 返回: *RasterPage 只读绘制页面, error 编译错误 // 返回: *RasterPage 只读绘制页面, error 编译错误
func (r *Renderer) CompilePage(page *PageContent) (*RasterPage, error) { func (r *Renderer) CompilePage(page *PageContent) (*RasterPage, error) {
return r.CompilePageContext(r.pageContext, page)
}
// CompilePageContext 编译页面并在对象遍历间响应取消,不返回未完成场景
// 单次后端操作完成后检查取消,nil使用后台上下文
// 入参: ctx 取消上下文, page 页面内容
// 返回: *RasterPage 只读绘制页面, error 编译或取消错误
func (r *Renderer) CompilePageContext(ctx context.Context, page *PageContent) (*RasterPage, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, err
}
if r.backends.Compiler == nil { if r.backends.Compiler == nil {
return nil, fmt.Errorf("page compiler: %w", ErrBackendUnavailable) return nil, fmt.Errorf("page compiler: %w", ErrBackendUnavailable)
} }
return r.backends.Compiler.CompilePage(r, page) previous := r.pageContext
r.pageContext = ctx
defer func() { r.pageContext = previous }()
result, err := r.backends.Compiler.CompilePage(r, page)
if canceled := ctx.Err(); canceled != nil {
return nil, canceled
}
return result, err
} }
// backendName 获取已配置的后端标识 // backendName 获取已配置的后端标识
+60 -5
View File
@@ -231,6 +231,38 @@ func (c *pdfCompositor) geometryBounds(node pdfCompositeNode, stroke bool) (Box,
return geometry.box, err return geometry.box, err
} }
// coverageBounds 合并实际覆盖边界并复用图元编译缓存,不采用字体估算范围
// 入参: node 图元或透明组
// 返回: Box 覆盖范围, error 编译或取消错误
func (c *pdfCompositor) coverageBounds(node pdfCompositeNode) (Box, error) {
if err := c.importer.ctx.Err(); err != nil {
return Box{}, err
}
var box Box
if node.group != nil {
for _, child := range node.children {
bounds, err := c.coverageBounds(child)
if err != nil {
return Box{}, err
}
box = unionTextBox(box, bounds)
}
return box, nil
}
_, fill, stroke := node.style()
for _, outline := range []bool{false, true} {
if outline && !stroke || !outline && !fill {
continue
}
bounds, err := c.geometryBounds(node, outline)
if err != nil {
return Box{}, err
}
box = unionTextBox(box, bounds)
}
return box, nil
}
// groupBounds 缓存组内实际覆盖范围,外部字体沿用描述符保守边界 // groupBounds 缓存组内实际覆盖范围,外部字体沿用描述符保守边界
// 入参: node 透明组 // 入参: node 透明组
// 返回: Box 组覆盖范围, error 几何编译错误 // 返回: Box 组覆盖范围, error 几何编译错误
@@ -749,12 +781,24 @@ func (n pdfCompositeNode) coverage(p *pdfImporter, stroke bool) error {
return p.path(pdfgo.PathMark{Path: path, Style: style, Fill: true}) return p.path(pdfgo.PathMark{Path: path, Style: style, Fill: true})
} }
// compositeRegion 按原始背景计算局部效果,不将组外文字和图形栅格化 // compositeRegionBounds 复用内置后端的图元覆盖,自定义后端保留整组度量
// 入参: backdrop 前序图元, node 当前效果, space 页面混合空间, flatten 是否合并页面背景 // 入参: node 当前效果
// 返回: error 合成或保存错误 // 返回: Box 覆盖范围, error 编译或取消错误
func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompositeNode, space *pdfgo.ColorSpace, flatten bool) error { func (p *pdfImporter) compositeRegionBounds(node pdfCompositeNode) (Box, error) {
if err := p.ctx.Err(); err != nil { if err := p.ctx.Err(); err != nil {
return err return Box{}, err
}
switch p.editor.Backends().Compiler.(type) {
case CanvasBackend, *CanvasBackend, OFDCompiler, *OFDCompiler:
geometry, err := p.renderer.Geometry()
if err != nil {
return Box{}, err
}
switch geometry.(type) {
case CanvasBackend, *CanvasBackend:
c := pdfCompositor{importer: p, cache: p.compositingCache()}
return c.coverageBounds(node)
}
} }
scene, box, err := p.compileGroup(func(local *pdfImporter) error { scene, box, err := p.compileGroup(func(local *pdfImporter) error {
if node.group != nil { if node.group != nil {
@@ -772,6 +816,17 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
return nil return nil
}) })
if err != nil || scene == nil { if err != nil || scene == nil {
return Box{}, err
}
return box, nil
}
// compositeRegion 按原始背景计算局部效果,不将组外文字和图形栅格化
// 入参: backdrop 前序图元, node 当前效果, space 页面混合空间, flatten 是否合并页面背景
// 返回: error 合成或保存错误
func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompositeNode, space *pdfgo.ColorSpace, flatten bool) error {
box, err := p.compositeRegionBounds(node)
if err != nil || box.W <= 0 || box.H <= 0 {
return err return err
} }
inverse, ok := p.matrix.Inverse() inverse, ok := p.matrix.Inverse()
+1 -1
View File
@@ -92,7 +92,7 @@ func (p *pdfImporter) destinationBounds(page *pdfgo.Page) (Box, error) {
if renderer.backends.Fonts != nil { if renderer.backends.Fonts != nil {
renderer.backends.Fonts = pdfDestinationFontBackend{renderer.backends.Fonts} renderer.backends.Fonts = pdfDestinationFontBackend{renderer.backends.Fonts}
} }
scene, err := renderer.CompilePage(&editor.pages[0]) scene, err := renderer.CompilePageContext(p.ctx, &editor.pages[0])
if err != nil { if err != nil {
return Box{}, err return Box{}, err
} }
+1 -1
View File
@@ -393,7 +393,7 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
page := &editor.pages[0] page := &editor.pages[0]
renderer := p.renderer.childRenderer(reader) renderer := p.renderer.childRenderer(reader)
renderer.TransparentBackground = true renderer.TransparentBackground = true
scene, err := renderer.CompilePage(page) scene, err := renderer.CompilePageContext(p.ctx, page)
if canceled := p.ctx.Err(); canceled != nil { if canceled := p.ctx.Err(); canceled != nil {
return nil, box, canceled return nil, box, canceled
} }
+15
View File
@@ -47,6 +47,11 @@ type PageGroups interface {
// 入参: page 页面内容, visitor 页面访问器 // 入参: page 页面内容, visitor 页面访问器
// 返回: error 遍历或访问器错误 // 返回: error 遍历或访问器错误
func (r *Renderer) WalkPage(page *PageContent, visitor PageVisitor) error { func (r *Renderer) WalkPage(page *PageContent, visitor PageVisitor) error {
if r.pageContext != nil {
if err := r.pageContext.Err(); err != nil {
return err
}
}
for order := range 3 { for order := range 3 {
if r.Reader.doc != nil { if r.Reader.doc != nil {
for _, ref := range page.Template { for _, ref := range page.Template {
@@ -83,6 +88,11 @@ func (r *Renderer) WalkPage(page *PageContent, visitor PageVisitor) error {
} }
} }
for _, stamp := range r.Reader.Stamps[page.ID] { for _, stamp := range r.Reader.Stamps[page.ID] {
if r.pageContext != nil {
if err := r.pageContext.Err(); err != nil {
return err
}
}
if err := visitor.DrawStamp(stamp); err != nil { if err := visitor.DrawStamp(stamp); err != nil {
return err return err
} }
@@ -160,6 +170,11 @@ func (r *Renderer) walkLayers(layers []Layer, order int, visitor PageVisitor) er
// 入参: object 源对象, state 继承状态, visitor 页面访问器, references 当前资源引用链 // 入参: object 源对象, state 继承状态, visitor 页面访问器, references 当前资源引用链
// 返回: error 遍历或访问器错误 // 返回: error 遍历或访问器错误
func (r *Renderer) walkObject(object *GraphicObject, state RenderState, visitor PageVisitor, references map[string]bool) error { func (r *Renderer) walkObject(object *GraphicObject, state RenderState, visitor PageVisitor, references map[string]bool) error {
if r.pageContext != nil {
if err := r.pageContext.Err(); err != nil {
return err
}
}
if groups, ok := visitor.(PageGroups); ok && groups.BeginObject(object) { if groups, ok := visitor.(PageGroups); ok && groups.BeginObject(object) {
defer groups.EndObject() defer groups.EndObject()
} }
+2
View File
@@ -43,6 +43,7 @@ type Renderer struct {
fontFS []fs.FS fontFS []fs.FS
decodeImages bool decodeImages bool
pageText *PageText pageText *PageText
pageContext context.Context
textOnly bool textOnly bool
backends RenderBackends backends RenderBackends
backendStates map[any]any backendStates map[any]any
@@ -107,6 +108,7 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer {
renderer.fontMetrics = r.fontMetrics renderer.fontMetrics = r.fontMetrics
renderer.fontSourcesCache = r.fontSourcesCache.child() renderer.fontSourcesCache = r.fontSourcesCache.child()
renderer.decodeImages = r.decodeImages renderer.decodeImages = r.decodeImages
renderer.pageContext = r.pageContext
renderer.TransparentBackground = r.TransparentBackground renderer.TransparentBackground = r.TransparentBackground
return renderer return renderer
} }
+35
View File
@@ -217,6 +217,8 @@ func intersectClipPath(parent, current *canvas.Path) *canvas.Path {
if slices.Equal(parent.Data(), current.Data()) { if slices.Equal(parent.Data(), current.Data()) {
return parent return parent
} }
parent = pruneCanvasContours(parent, current)
current = pruneCanvasContours(current, parent)
parentRect, parentOK := rectangularPath(parent) parentRect, parentOK := rectangularPath(parent)
currentRect, currentOK := rectangularPath(current) currentRect, currentOK := rectangularPath(current)
if parentOK && currentOK { if parentOK && currentOK {
@@ -295,6 +297,8 @@ func applyClipPath(path, clip *canvas.Path) *canvas.Path {
if path.Empty() || clip.Empty() { if path.Empty() || clip.Empty() {
return &canvas.Path{} return &canvas.Path{}
} }
path = pruneCanvasContours(path, clip)
clip = pruneCanvasContours(clip, path)
if rect, ok := rectangularPath(clip); ok { if rect, ok := rectangularPath(clip); ok {
bounds := path.Bounds() bounds := path.Bounds()
if rect.Contains(bounds) { if rect.Contains(bounds) {
@@ -319,6 +323,37 @@ func applyClipPath(path, clip *canvas.Path) *canvas.Path {
return path.And(clip) return path.And(clip)
} }
// pruneCanvasContours 排除与裁剪边界分离的完整轮廓,保留相交轮廓的曲线和绕向
// 入参: path 填充路径, clip 裁剪路径
// 返回: *canvas.Path 待求交路径
func pruneCanvasContours(path, clip *canvas.Path) *canvas.Path {
if path == nil || clip == nil || len(path.Data()) < 256 || !path.HasSubpaths() {
return path
}
bounds := clip.Bounds()
parts := path.Split()
count := 0
for _, part := range parts {
if !part.Closed() {
return path
}
box := part.Bounds()
if box.X1 < bounds.X0 || bounds.X1 < box.X0 || box.Y1 < bounds.Y0 || bounds.Y1 < box.Y0 {
continue
}
parts[count] = part
count++
}
if count == len(parts) {
return path
}
result := &canvas.Path{}
for _, part := range parts[:count] {
result = result.Append(part)
}
return result
}
// reduceRepeatedNonZeroPath 将完全相同的闭合轮廓约为一份,保持非零填充区域 // reduceRepeatedNonZeroPath 将完全相同的闭合轮廓约为一份,保持非零填充区域
// 入参: path 非零填充路径 // 入参: path 非零填充路径
// 返回: *canvas.Path 等价填充路径 // 返回: *canvas.Path 等价填充路径
+20 -2
View File
@@ -311,7 +311,7 @@ func geometryToCanvasPath(path *GeometryPath) (*canvas.Path, error) {
if err := path.validate(); err != nil { if err := path.validate(); err != nil {
return nil, err return nil, err
} }
p := &canvas.Path{} p := canvas.NewPathFromData(make([]float64, 0, canvasPathCapacity(*path)))
for _, s := range *path { for _, s := range *path {
switch s.Verb { switch s.Verb {
case GeometryMove: case GeometryMove:
@@ -337,7 +337,7 @@ func geometryToCanvasPath(path *GeometryPath) (*canvas.Path, error) {
// 入参: path 独立路径 // 入参: path 独立路径
// 返回: *canvas.Path 适配路径, error 不支持的指令 // 返回: *canvas.Path 适配路径, error 不支持的指令
func canvasObjectPath(path GeometryPath) (*canvas.Path, error) { func canvasObjectPath(path GeometryPath) (*canvas.Path, error) {
var data []float64 data := make([]float64, 0, canvasPathCapacity(path))
for _, s := range path { for _, s := range path {
end := s.End end := s.End
switch s.Verb { switch s.Verb {
@@ -364,6 +364,24 @@ func canvasObjectPath(path GeometryPath) (*canvas.Path, error) {
return canvas.NewPathFromData(data), nil return canvas.NewPathFromData(data), nil
} }
// canvasPathCapacity 按指令估算路径容量,避免长路径逐段扩容
// 入参: path 独立几何路径
// 返回: int 坐标存储容量
func canvasPathCapacity(path GeometryPath) int {
count := 0
for _, segment := range path {
switch segment.Verb {
case GeometryMove, GeometryLine, GeometryClose:
count += 4
case GeometryQuad:
count += 6
default:
count += 8
}
}
return count
}
// geometryFromCanvasPath 将默认引擎路径转换为独立的页面坐标路径,保留曲线和椭圆弧 // geometryFromCanvasPath 将默认引擎路径转换为独立的页面坐标路径,保留曲线和椭圆弧
// 入参: path 默认引擎路径 // 入参: path 默认引擎路径
// 返回: *GeometryPath 独立路径,nil表示不裁剪 // 返回: *GeometryPath 独立路径,nil表示不裁剪