From 59a2c4d8fe0218f3e014708161f5b2d20a941424 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 30 Sep 2026 16:35:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D):=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=A5=97=E5=8D=B0=E8=BD=AC=E6=8D=A2=E4=B8=8E?= =?UTF-8?q?=E7=BC=96=E8=BE=91=E7=BC=BA=E9=99=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 4 +- internal/webui/wasm.go | 46 ++++------- ofdgo_editor_batch.go | 6 +- ofdgo_editor_color.go | 5 +- ofdgo_editor_style.go | 40 ++++++++++ ofdgo_pdf.go | 1 + ofdgo_pdf_composite.go | 31 +++----- ofdgo_pdf_composite_pattern.go | 135 ++++++++++++++++++++++++++------- 9 files changed, 187 insertions(+), 83 deletions(-) diff --git a/go.mod b/go.mod index 18e96d1..a1407b2 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,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-20260930062146-7c39a20d9430 + github.com/xiaoqidun/pdfgo v0.0.0-20260930081439-0ce33d5532fb 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 6ffe5cb..77022e8 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-20260930062146-7c39a20d9430 h1:UBP+ZmqxoxQ7p6dFZI/6yGSLmgqfTQUvO7h+DUfWsWY= -github.com/xiaoqidun/pdfgo v0.0.0-20260930062146-7c39a20d9430/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +github.com/xiaoqidun/pdfgo v0.0.0-20260930081439-0ce33d5532fb h1:OKuM5n02cLQB0rIcJoNfnQl+udhBNWgjLMAUlAk087s= +github.com/xiaoqidun/pdfgo v0.0.0-20260930081439-0ce33d5532fb/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/internal/webui/wasm.go b/internal/webui/wasm.go index 593eedc..8f011bf 100644 --- a/internal/webui/wasm.go +++ b/internal/webui/wasm.go @@ -2987,14 +2987,18 @@ func styleObjects(args []js.Value) (any, error) { return nil, err } } - patterns := make(map[bool]*ofdgo.PatternStyle) - for key, stroke := range map[string]bool{"fillPatternStyle": false, "strokePatternStyle": true} { + appearance := ofdgo.ObjectAppearance{SetLink: !linkField.IsUndefined(), Link: link, Border: border} + for i, key := range []string{"fillPatternStyle", "strokePatternStyle"} { if field := args[2].Get(key); !field.IsUndefined() { var pattern ofdgo.PatternStyle if err := json.Unmarshal([]byte(field.String()), &pattern); err != nil { return nil, err } - patterns[stroke] = &pattern + if i == 0 { + appearance.FillPattern = &pattern + } else { + appearance.StrokePattern = &pattern + } } } if field := args[2].Get("fillPaint"); !field.IsUndefined() { @@ -3008,36 +3012,18 @@ func styleObjects(args []js.Value) (any, error) { } } return changeObjects(func() error { - return currentEditor.Transaction(func(editor *ofdgo.Editor) error { - page, ids := args[0].Int(), stringsFromJS(args[1]) - if !linkField.IsUndefined() { - if err := editor.SetObjectLink(page, ids, link); err != nil { - return err - } - } - if border != nil { - if err := resolveStylePaint((*ofdgo.FillColor)(border.Color)); err != nil { - return err - } - if err := editor.StyleImageBorders(page, ids, *border); err != nil { - return err - } - } - for _, paint := range []*ofdgo.FillColor{style.FillColor, (*ofdgo.FillColor)(style.StrokeColor)} { - if err := resolveStylePaint(paint); err != nil { - return err - } - } - if err := editor.StyleObjects(page, ids, style); err != nil { + if border != nil { + if err := resolveStylePaint((*ofdgo.FillColor)(border.Color)); err != nil { return err } - for stroke, pattern := range patterns { - if err := editor.StylePatterns(page, ids, stroke, *pattern); err != nil { - return err - } + } + for _, paint := range []*ofdgo.FillColor{style.FillColor, (*ofdgo.FillColor)(style.StrokeColor)} { + if err := resolveStylePaint(paint); err != nil { + return err } - return nil - }) + } + appearance.Style = style + return currentEditor.ApplyAppearance(args[0].Int(), stringsFromJS(args[1]), appearance) }) } diff --git a/ofdgo_editor_batch.go b/ofdgo_editor_batch.go index c63fcba..c2cf1ea 100644 --- a/ofdgo_editor_batch.go +++ b/ofdgo_editor_batch.go @@ -562,7 +562,11 @@ func (e *Editor) updateObjectOrigins(page int, objects []GraphicObject, preserve if origin != nil { capability := e.objectCapabilities(before[i], orderable) if !capability.Transform { - return fmt.Errorf("object %q is read-only for this operation: %w", ids[i], capability.editError()) + beforeBox, beforeCTM := editorGeometry(before[i]) + afterBox, afterCTM := editorGeometry(object) + if !capability.Update && !(preserved && capability.Delete) || before[i].Type != object.Type || beforeBox != afterBox || beforeCTM != afterCTM { + return fmt.Errorf("object %q is read-only for this operation: %w", ids[i], capability.editError()) + } } if !preserved && !capability.Update && editorPreservedObject(before[i], object) && capability.Paint { if err := e.validatePreservedAppearance(before[i], object); err != nil { diff --git a/ofdgo_editor_color.go b/ofdgo_editor_color.go index 7774ee0..5625978 100644 --- a/ofdgo_editor_color.go +++ b/ofdgo_editor_color.go @@ -208,8 +208,9 @@ func (e *Editor) RGBColor(value color.NRGBA) (*FillColor, error) { } } for _, resource := range e.resources { - if resource.space != nil { - result.ColorSpace = resource.space.ID + space := resource.space + if space != nil && space.Type == "RGB" && (space.BitsPerComponent == 0 || space.BitsPerComponent == 8) && len(space.Palette) == 0 { + result.ColorSpace = space.ID return result, nil } } diff --git a/ofdgo_editor_style.go b/ofdgo_editor_style.go index d7d696a..e77558c 100644 --- a/ofdgo_editor_style.go +++ b/ofdgo_editor_style.go @@ -46,6 +46,46 @@ type ImageBorderStyle struct { Color *StrokeColor } +// ObjectAppearance 对象外观组合更新,nil字段保持原值 +// SetLink为true时更新链接,Link为nil时删除链接 +type ObjectAppearance struct { + Style ObjectStyle + SetLink bool + Link *AnnotationLink + Border *ImageBorderStyle + FillPattern *PatternStyle + StrokePattern *PatternStyle +} + +// ApplyAppearance 原子更新外观、链接、图片边框及图案布局,支持一次撤销 +// 入参: page 页面索引, ids 对象标识, appearance 外观增量 +// 返回: error 错误信息 +func (e *Editor) ApplyAppearance(page int, ids []string, appearance ObjectAppearance) error { + return e.Transaction(func(editor *Editor) error { + if appearance.SetLink { + if err := editor.SetObjectLink(page, ids, appearance.Link); err != nil { + return err + } + } + if appearance.Border != nil { + if err := editor.StyleImageBorders(page, ids, *appearance.Border); err != nil { + return err + } + } + if err := editor.StyleObjects(page, ids, appearance.Style); err != nil { + return err + } + for i, pattern := range []*PatternStyle{appearance.FillPattern, appearance.StrokePattern} { + if pattern != nil { + if err := editor.StylePatterns(page, ids, i == 1, *pattern); err != nil { + return err + } + } + } + return nil + }) +} + // DrawParam 读取绘制参数,resolved为true时合并继承链,返回独立副本 // 入参: id 资源标识, resolved 是否解析继承 // 返回: *DrawParam 绘制参数, error 错误信息 diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index d29156b..65750b0 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -124,6 +124,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI if err != nil { return nil, PDFImportReport{}, err } + defer reader.Close() editor := NewEditor() editor.SetRenderBackends(renderer.Backends()) editor.fontDirs, editor.fontFS = renderer.FontSources() diff --git a/ofdgo_pdf_composite.go b/ofdgo_pdf_composite.go index bd79858..43d5320 100644 --- a/ofdgo_pdf_composite.go +++ b/ofdgo_pdf_composite.go @@ -207,25 +207,7 @@ func (p *pdfImporter) compositePage(space *pdfgo.ColorSpace, walk func(pdfgo.Vis } if space == nil { space = &pdfgo.ColorSpace{Model: "DeviceRGB"} - var needsProcess func([]pdfCompositeNode) (bool, error) - needsProcess = func(items []pdfCompositeNode) (bool, error) { - for _, item := range items { - if item.group != nil { - found, err := needsProcess(item.children) - if err != nil || found { - return found, err - } - } - if item.image != nil && item.image.Style.FillOverprint { - _, found, err := pdfImageProcessColorants(item.image.Image) - if err != nil || found { - return found, err - } - } - } - return false, nil - } - found, err := needsProcess(nodes) + found, err := p.processOverprint(nodes) if err != nil { return err } @@ -244,7 +226,11 @@ func (p *pdfImporter) compositePage(space *pdfgo.ColorSpace, walk func(pdfgo.Vis func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error { space := p.compositeSpace for i, node := range nodes { - if node.opaque(space) || space.SRGBEquivalent() && node.direct() { + direct, err := p.directCompositeNode(node, space) + if err != nil { + return fmt.Errorf("inspect graphic %d: %w", i+1, err) + } + if direct { if err := node.emit(p.visitor()); err != nil { return fmt.Errorf("convert graphic %d: %w", i+1, err) } @@ -445,11 +431,14 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo output := image.NewNRGBA(image.Rect(0, 0, w, h)) cache := p.compositingCache() step := 25.4 / p.rasterDPI + buffer := make([]pdfCompositePixel, min(256, w)*min(256, h)) for y := 0; y < h; y += 256 { for x := 0; x < w; x += 256 { + runtime.Gosched() width, height := min(256, w-x), min(256, h-y) c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, masks: map[*pdfgo.SoftMask][]float64{}} - pixels := make([]pdfCompositePixel, width*height) + pixels := buffer[:width*height] + clear(pixels) if err := c.draw(backdrop, pixels, space); err != nil { return err } diff --git a/ofdgo_pdf_composite_pattern.go b/ofdgo_pdf_composite_pattern.go index b1783ab..3658c23 100644 --- a/ofdgo_pdf_composite_pattern.go +++ b/ofdgo_pdf_composite_pattern.go @@ -33,37 +33,14 @@ type pdfCompositePattern struct { // 返回: []pdfCompositePixel 去除背景贡献的图案像素, error 解析或绘制错误 func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel, space *pdfgo.ColorSpace) ([]pdfCompositePixel, error) { source := paint.Tiling - paint.Alpha = 1 - if c.cache.patterns == nil { - c.cache.patterns = make(map[pdfgo.Paint]*pdfCompositePattern) - } - pattern := c.cache.patterns[paint] matrix := c.importer.matrix.Mul(source.Matrix) inverse, ok := matrix.Inverse() if !ok { return nil, fmt.Errorf("invalid PDF tiling pattern matrix") } - if pattern == nil { - nodes, err := c.importer.collectCompositeNodes(func(v pdfgo.Visitor) error { return source.Walk(c.importer.ctx, paint, v) }) - if err != nil { - return nil, err - } - b := source.BBox - box := pdfBounds([]pdfgo.Point{ - matrix.Apply(pdfgo.Point{X: b.XMin, Y: b.YMin}), matrix.Apply(pdfgo.Point{X: b.XMax, Y: b.YMin}), - matrix.Apply(pdfgo.Point{X: b.XMax, Y: b.YMax}), matrix.Apply(pdfgo.Point{X: b.XMin, Y: b.YMax}), - }) - if box.W <= 0 || box.H <= 0 { - return nil, fmt.Errorf("invalid PDF tiling pattern bounds") - } - local := *c.importer - local.matrix = matrix - local.matrix[4] -= box.X - local.matrix[5] -= box.Y - local.pageWidth, local.pageHeight = box.W, box.H - local.compositeCache = nil - pattern = &pdfCompositePattern{nodes: nodes, local: local, box: box} - c.cache.patterns[paint] = pattern + pattern, err := c.importer.compositePattern(paint) + if err != nil { + return nil, err } xstep, ystep := math.Abs(source.XStep), math.Abs(source.YStep) area := pdfBounds([]pdfgo.Point{ @@ -108,3 +85,109 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel, } return result, nil } + +// compositePattern 复用图案单元解析及几何上下文,供检查和局部合成共同使用 +// 入参: paint 图案画刷 +// 返回: *pdfCompositePattern 单元内容, error 解析或边界错误 +func (p *pdfImporter) compositePattern(paint pdfgo.Paint) (*pdfCompositePattern, error) { + paint.Alpha = 1 + cache := p.compositingCache() + if cache.patterns == nil { + cache.patterns = make(map[pdfgo.Paint]*pdfCompositePattern) + } + if pattern := cache.patterns[paint]; pattern != nil { + return pattern, nil + } + source := paint.Tiling + nodes, err := p.collectCompositeNodes(func(v pdfgo.Visitor) error { return source.Walk(p.ctx, paint, v) }) + if err != nil { + return nil, err + } + matrix := p.matrix.Mul(source.Matrix) + b := source.BBox + box := pdfBounds([]pdfgo.Point{ + matrix.Apply(pdfgo.Point{X: b.XMin, Y: b.YMin}), matrix.Apply(pdfgo.Point{X: b.XMax, Y: b.YMin}), + matrix.Apply(pdfgo.Point{X: b.XMax, Y: b.YMax}), matrix.Apply(pdfgo.Point{X: b.XMin, Y: b.YMax}), + }) + if box.W <= 0 || box.H <= 0 { + return nil, fmt.Errorf("invalid PDF tiling pattern bounds") + } + local := *p + local.matrix = matrix + local.matrix[4] -= box.X + local.matrix[5] -= box.Y + local.pageWidth, local.pageHeight = box.W, box.H + local.compositeCache = nil + pattern := &pdfCompositePattern{nodes: nodes, local: local, box: box} + cache.patterns[paint] = pattern + return pattern, nil +} + +// directCompositeNode 检查图元及其图案单元能否独立保留,不忽略单元内的背景混合 +// 入参: node 图元, space 混合空间 +// 返回: bool 是否可直接转换, error 图案解析错误 +func (p *pdfImporter) directCompositeNode(node pdfCompositeNode, space *pdfgo.ColorSpace) (bool, error) { + if !node.opaque(space) && !(space.SRGBEquivalent() && node.direct()) { + return false, nil + } + if node.group != nil { + for _, child := range node.children { + if direct, err := p.directCompositeNode(child, space); err != nil || !direct { + return direct, err + } + } + return true, nil + } + style, fill, stroke := node.style() + for i, paint := range []pdfgo.Paint{style.Fill, style.Stroke} { + if i == 0 && !fill || i == 1 && !stroke || paint.Tiling == nil { + continue + } + pattern, err := p.compositePattern(paint) + if err != nil { + return false, err + } + for _, child := range pattern.nodes { + if direct, err := pattern.local.directCompositeNode(child, space); err != nil || !direct { + return direct, err + } + } + } + return true, nil +} + +// processOverprint 检查图元、透明组和图案单元是否需要设备四色套印 +// 入参: nodes 原始图元 +// 返回: bool 是否需要四色混合, error 图像或图案解析错误 +func (p *pdfImporter) processOverprint(nodes []pdfCompositeNode) (bool, error) { + for _, node := range nodes { + if node.group != nil { + if found, err := p.processOverprint(node.children); err != nil || found { + return found, err + } + continue + } + style, fill, stroke := node.style() + if node.image != nil && style.FillOverprint { + if _, found, err := pdfImageProcessColorants(node.image.Image); err != nil || found { + return found, err + } + } + if fill && style.FillOverprint && pdfOverprintNeedsSeparation(style.Fill) || stroke && style.StrokeOverprint && pdfOverprintNeedsSeparation(style.Stroke) { + return true, nil + } + for i, paint := range []pdfgo.Paint{style.Fill, style.Stroke} { + if i == 0 && !fill || i == 1 && !stroke || paint.Tiling == nil { + continue + } + pattern, err := p.compositePattern(paint) + if err != nil { + return false, err + } + if found, err := pattern.local.processOverprint(pattern.nodes); err != nil || found { + return found, err + } + } + } + return false, nil +}