diff --git a/go.mod b/go.mod index d13f29b..49b8b60 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 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/image v0.46.0 golang.org/x/text v0.42.0 diff --git a/go.sum b/go.sum index 23a4e71..3945686 100644 --- a/go.sum +++ b/go.sum @@ -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/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= 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-20261008045044-ada80114c916/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +github.com/xiaoqidun/pdfgo v0.0.0-20261008072006-d519e7a38749 h1:jWImuld2SXZp826sudLJJUUOEEMPj/Jy77ItAE778Sg= +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/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= diff --git a/ofdgo_backend.go b/ofdgo_backend.go index 5796814..1fe6690 100644 --- a/ofdgo_backend.go +++ b/ofdgo_backend.go @@ -15,6 +15,7 @@ package ofdgo import ( + "context" "errors" "fmt" "image" @@ -188,10 +189,31 @@ func WithRasterBackend(backend RasterBackend) RendererOption { // 入参: page 页面内容 // 返回: *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 { 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 获取已配置的后端标识 diff --git a/ofdgo_pdf_composite.go b/ofdgo_pdf_composite.go index c2ef13e..922bbdc 100644 --- a/ofdgo_pdf_composite.go +++ b/ofdgo_pdf_composite.go @@ -231,6 +231,38 @@ func (c *pdfCompositor) geometryBounds(node pdfCompositeNode, stroke bool) (Box, 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 缓存组内实际覆盖范围,外部字体沿用描述符保守边界 // 入参: node 透明组 // 返回: 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}) } -// compositeRegion 按原始背景计算局部效果,不将组外文字和图形栅格化 -// 入参: backdrop 前序图元, node 当前效果, space 页面混合空间, flatten 是否合并页面背景 -// 返回: error 合成或保存错误 -func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompositeNode, space *pdfgo.ColorSpace, flatten bool) error { +// compositeRegionBounds 复用内置后端的图元覆盖,自定义后端保留整组度量 +// 入参: node 当前效果 +// 返回: Box 覆盖范围, error 编译或取消错误 +func (p *pdfImporter) compositeRegionBounds(node pdfCompositeNode) (Box, error) { 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 { if node.group != nil { @@ -772,6 +816,17 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo return 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 } inverse, ok := p.matrix.Inverse() diff --git a/ofdgo_pdf_destination.go b/ofdgo_pdf_destination.go index 0015d15..b506eea 100644 --- a/ofdgo_pdf_destination.go +++ b/ofdgo_pdf_destination.go @@ -92,7 +92,7 @@ func (p *pdfImporter) destinationBounds(page *pdfgo.Page) (Box, error) { if renderer.backends.Fonts != nil { 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 { return Box{}, err } diff --git a/ofdgo_pdf_group.go b/ofdgo_pdf_group.go index a06df13..7864755 100644 --- a/ofdgo_pdf_group.go +++ b/ofdgo_pdf_group.go @@ -393,7 +393,7 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage, page := &editor.pages[0] renderer := p.renderer.childRenderer(reader) renderer.TransparentBackground = true - scene, err := renderer.CompilePage(page) + scene, err := renderer.CompilePageContext(p.ctx, page) if canceled := p.ctx.Err(); canceled != nil { return nil, box, canceled } diff --git a/ofdgo_render_walk.go b/ofdgo_render_walk.go index ba378f0..e9260b7 100644 --- a/ofdgo_render_walk.go +++ b/ofdgo_render_walk.go @@ -47,6 +47,11 @@ type PageGroups interface { // 入参: page 页面内容, visitor 页面访问器 // 返回: 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 { if r.Reader.doc != nil { 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] { + if r.pageContext != nil { + if err := r.pageContext.Err(); err != nil { + return err + } + } if err := visitor.DrawStamp(stamp); err != nil { return err } @@ -160,6 +170,11 @@ func (r *Renderer) walkLayers(layers []Layer, order int, visitor PageVisitor) er // 入参: object 源对象, state 继承状态, visitor 页面访问器, references 当前资源引用链 // 返回: 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) { defer groups.EndObject() } diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index 321d7e7..1b0e813 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -43,6 +43,7 @@ type Renderer struct { fontFS []fs.FS decodeImages bool pageText *PageText + pageContext context.Context textOnly bool backends RenderBackends backendStates map[any]any @@ -107,6 +108,7 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer { renderer.fontMetrics = r.fontMetrics renderer.fontSourcesCache = r.fontSourcesCache.child() renderer.decodeImages = r.decodeImages + renderer.pageContext = r.pageContext renderer.TransparentBackground = r.TransparentBackground return renderer } diff --git a/ofdgo_renderer_clip_canvas.go b/ofdgo_renderer_clip_canvas.go index 6f843fa..fb8d71e 100644 --- a/ofdgo_renderer_clip_canvas.go +++ b/ofdgo_renderer_clip_canvas.go @@ -217,6 +217,8 @@ func intersectClipPath(parent, current *canvas.Path) *canvas.Path { if slices.Equal(parent.Data(), current.Data()) { return parent } + parent = pruneCanvasContours(parent, current) + current = pruneCanvasContours(current, parent) parentRect, parentOK := rectangularPath(parent) currentRect, currentOK := rectangularPath(current) if parentOK && currentOK { @@ -295,6 +297,8 @@ func applyClipPath(path, clip *canvas.Path) *canvas.Path { if path.Empty() || clip.Empty() { return &canvas.Path{} } + path = pruneCanvasContours(path, clip) + clip = pruneCanvasContours(clip, path) if rect, ok := rectangularPath(clip); ok { bounds := path.Bounds() if rect.Contains(bounds) { @@ -319,6 +323,37 @@ func applyClipPath(path, clip *canvas.Path) *canvas.Path { 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 将完全相同的闭合轮廓约为一份,保持非零填充区域 // 入参: path 非零填充路径 // 返回: *canvas.Path 等价填充路径 diff --git a/ofdgo_renderer_geom_canvas.go b/ofdgo_renderer_geom_canvas.go index deebd7f..f921f10 100644 --- a/ofdgo_renderer_geom_canvas.go +++ b/ofdgo_renderer_geom_canvas.go @@ -311,7 +311,7 @@ func geometryToCanvasPath(path *GeometryPath) (*canvas.Path, error) { if err := path.validate(); err != nil { return nil, err } - p := &canvas.Path{} + p := canvas.NewPathFromData(make([]float64, 0, canvasPathCapacity(*path))) for _, s := range *path { switch s.Verb { case GeometryMove: @@ -337,7 +337,7 @@ func geometryToCanvasPath(path *GeometryPath) (*canvas.Path, error) { // 入参: path 独立路径 // 返回: *canvas.Path 适配路径, error 不支持的指令 func canvasObjectPath(path GeometryPath) (*canvas.Path, error) { - var data []float64 + data := make([]float64, 0, canvasPathCapacity(path)) for _, s := range path { end := s.End switch s.Verb { @@ -364,6 +364,24 @@ func canvasObjectPath(path GeometryPath) (*canvas.Path, error) { 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 将默认引擎路径转换为独立的页面坐标路径,保留曲线和椭圆弧 // 入参: path 默认引擎路径 // 返回: *GeometryPath 独立路径,nil表示不裁剪