diff --git a/go.mod b/go.mod index 27c7e02..eefea99 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-20260928071431-2842b88597d4 + github.com/xiaoqidun/pdfgo v0.0.0-20260928081150-262fbd884f34 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 f227247..58f8ddb 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-20260928071431-2842b88597d4 h1:R0jk+8LHdihgZdzvLHJQgX1ptkPfiK45rSvb7i9L3Fw= -github.com/xiaoqidun/pdfgo v0.0.0-20260928071431-2842b88597d4/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +github.com/xiaoqidun/pdfgo v0.0.0-20260928081150-262fbd884f34 h1:aZT7GSVDB8do4koXSACBncZDIHFpa8tG7ZEQeeOkZh8= +github.com/xiaoqidun/pdfgo v0.0.0-20260928081150-262fbd884f34/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_pdf.go b/ofdgo_pdf.go index a4e3d17..2b73bcc 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -85,6 +85,8 @@ type pdfImporter struct { rasterDPI float64 rasterWarned bool compositeCache *pdfCompositeCache + compositeNodes []pdfCompositeNode + compositeSpace *pdfgo.ColorSpace } // ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果 @@ -112,11 +114,12 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI return nil, PDFImportReport{}, err } } - open := pdfgo.NewReaderWithPassword - if options.PasswordUTF8 { - open = pdfgo.NewReaderWithUTF8Password + readOptions := pdfgo.ReaderOptions{Password: options.Password, PasswordUTF8: options.PasswordUTF8} + var diagnostics []pdfgo.Diagnostic + if !options.Strict { + readOptions.Warning = func(diagnostic pdfgo.Diagnostic) { diagnostics = append(diagnostics, diagnostic) } } - reader, err := open(source, size, options.Password) + reader, err := pdfgo.NewReaderWithOptions(source, size, readOptions) if err != nil { return nil, PDFImportReport{}, err } @@ -124,6 +127,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI editor.SetRenderBackends(renderer.Backends()) editor.fontDirs, editor.fontFS = renderer.FontSources() importer := pdfImporter{ctx: ctx, reader: reader, editor: editor, renderer: renderer, rasterDPI: renderer.DPI, fontIDs: map[*pdfgo.Font]string{}, fontMetrics: map[string]FontMetrics{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}} + importer.report.Warnings = diagnostics if security := reader.Encryption(); security != nil && !security.Owner && security.Permissions&0xf3c != 0xf3c { if options.Strict { return nil, PDFImportReport{}, &pdfgo.UnsupportedError{Feature: "PDF access permission conversion"} @@ -171,6 +175,8 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI importer.pageHeight = height importer.maskClips = make(map[*pdfgo.SoftMask]pdfgo.Path) importer.compositeCache = nil + importer.compositeNodes = nil + importer.compositeSpace = nil importer.clipTexts = make(map[*pdfgo.TextClip]TextObject) if err := importer.compositePage(nil, func(visitor pdfgo.Visitor) error { return reader.WalkPage(ctx, page, visitor) }); err != nil { return fmt.Errorf("import PDF page %d: %w", index+1, err) diff --git a/ofdgo_pdf_annot.go b/ofdgo_pdf_annot.go index 9f2516e..6dc19a8 100644 --- a/ofdgo_pdf_annot.go +++ b/ofdgo_pdf_annot.go @@ -15,13 +15,8 @@ package ofdgo import ( - "bytes" "context" "fmt" - "image" - "image/color" - "image/png" - "math" "strconv" "strings" "time" @@ -34,6 +29,9 @@ import ( // 返回: error 外观或转换错误 func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, actions ...Action) error { for _, key := range []pdfgo.Name{"AA", "OC", "A"} { + if annotation.Subtype == "Widget" && (key == "A" || key == "AA") { + continue + } if key == "A" && (annotation.Subtype == "Link" || annotation.Subtype == "Movie") { continue } @@ -67,49 +65,20 @@ func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page stamp.objects = nil stamp.pendingPath = nil stamp.report = PDFImportReport{} - var darkenImages []pdfgo.ImageMark - visitor := pdfgo.Visitor{ - Warning: p.warning, - Path: func(mark pdfgo.PathMark) error { - if mark.Style.BlendMode == "Darken" { - return &pdfgo.UnsupportedError{Feature: "darken stamp path"} - } - return stamp.path(mark) - }, - Text: func(mark pdfgo.TextMark) error { - if mark.Style.BlendMode == "Darken" { - return &pdfgo.UnsupportedError{Feature: "darken stamp text"} - } - if err := stamp.text(mark); err != nil { - return fmt.Errorf("annotation text: %w", err) - } - return nil - }, - Image: func(mark pdfgo.ImageMark) error { - if mark.Style.BlendMode == "Darken" { - darkenImages = append(darkenImages, mark) - return nil - } - return stamp.image(mark) - }, - } - visitor.Group = func(group pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { - return stamp.group(group, walk, visitor) - } - if err := p.reader.WalkAnnotationAppearance(ctx, page, annotation, visitor); err != nil { + stamp.compositeCache = nil + stamp.clipTexts = make(map[*pdfgo.TextClip]TextObject) + stamp.pageWidth, stamp.pageHeight = box.W, box.H + nodes, err := p.collectCompositeNodes(func(visitor pdfgo.Visitor) error { + return p.reader.WalkAnnotationAppearance(ctx, page, annotation, visitor) + }) + if err != nil { return err } - if err := stamp.flushPath(); err != nil { + if err := stamp.compositeObjects(nodes); err != nil { return err } - if len(darkenImages) != 0 { - if len(darkenImages) != 1 || len(stamp.objects) != 0 { - return &pdfgo.UnsupportedError{Feature: "mixed darken stamp appearance"} - } - if err := p.darkenStampAppearance(page, box, darkenImages[0], &stamp); err != nil { - return err - } - p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF darken stamp appearance flattened over page content"}) + if flags&(2|32) == 0 { + p.compositeNodes = stamp.compositeNodes } converted := Annotation{Type: pdfAnnotationType(annotation.Subtype), Subtype: string(annotation.Subtype), Appearance: Appearance{Boundary: pdfBoundary(box), Objects: stamp.objects}} popupObject := annotation.Dictionary["Popup"] @@ -234,77 +203,13 @@ func pdfAnnotationType(subtype pdfgo.Name) string { return "Watermark" case "Highlight", "Underline", "Squiggly", "StrikeOut": return "Highlight" - case "Text", "FreeText", "Line", "Square", "Circle", "Polygon", "PolyLine", "Caret", "Ink", "FileAttachment", "Sound", "Movie", "Screen", "Link", "Popup": + case "Text", "FreeText", "Line", "Square", "Circle", "Polygon", "PolyLine", "Caret", "Ink", "FileAttachment", "Sound", "Movie", "Screen", "Link", "Popup", "Widget": return "Path" } return "" } -// darkenStampAppearance 按PDF的Darken公式合成印章范围 -// 入参: page PDF页面, box印章范围, mark图像, stamp局部转换器 -// 返回: error 转换错误 -func (p *pdfImporter) darkenStampAppearance(page *pdfgo.Page, box Box, mark pdfgo.ImageMark, stamp *pdfImporter) error { - if err := p.commitObjects(); err != nil { - return err - } - base, err := renderImportedPage(p.editor, p.page, false) - if err != nil { - return err - } - _, width, height := pdfPageMatrix(page) - appearance := NewEditor() - appearance.SetRenderBackends(p.editor.backends) - if _, err := appearance.AddPage(width, height); err != nil { - return err - } - imageImporter := pdfImporter{reader: p.reader, editor: appearance, matrix: p.matrix, pageBox: page.CropBox} - mark.Style.BlendMode = "Normal" - if err := imageImporter.image(mark); err != nil { - return err - } - if _, err := appearance.CopyObjects(0, imageImporter.objects, 0, 0); err != nil { - return err - } - overlay, err := renderImportedPage(appearance, 0, true) - if err != nil { - return err - } - if base.Bounds() != overlay.Bounds() { - return fmt.Errorf("stamp appearance page size mismatch") - } - const dpi = 300.0 - pixelsPerMM := dpi / 25.4 - region := image.Rect(int(math.Floor(box.X*pixelsPerMM)), int(math.Floor(box.Y*pixelsPerMM)), int(math.Ceil((box.X+box.W)*pixelsPerMM)), int(math.Ceil((box.Y+box.H)*pixelsPerMM))).Intersect(base.Bounds()) - if region.Empty() { - return fmt.Errorf("stamp appearance outside page") - } - composite := image.NewRGBA(image.Rect(0, 0, region.Dx(), region.Dy())) - for y := region.Min.Y; y < region.Max.Y; y++ { - for x := region.Min.X; x < region.Max.X; x++ { - background := color.NRGBAModel.Convert(base.At(x, y)).(color.NRGBA) - foreground := color.NRGBAModel.Convert(overlay.At(x, y)).(color.NRGBA) - alpha := float64(foreground.A) / 255 - blend := func(back, front uint8) uint8 { - return uint8(math.Round((1-alpha)*float64(back) + alpha*math.Min(float64(back), float64(front)))) - } - composite.SetRGBA(x-region.Min.X, y-region.Min.Y, color.RGBA{R: blend(background.R, foreground.R), G: blend(background.G, foreground.G), B: blend(background.B, foreground.B), A: 255}) - } - } - var encoded bytes.Buffer - if err := png.Encode(&encoded, composite); err != nil { - return err - } - id, err := p.editor.AddImage(encoded.Bytes()) - if err != nil { - return err - } - regionBox := Box{X: float64(region.Min.X)/pixelsPerMM - box.X, Y: float64(region.Min.Y)/pixelsPerMM - box.Y, W: float64(region.Dx()) / pixelsPerMM, H: float64(region.Dy()) / pixelsPerMM} - stamp.objects = append(stamp.objects, GraphicObject{Type: "ImageObject", ImageObject: ImageObject{Boundary: pdfBoundary(regionBox), ResourceID: id}}) - stamp.report.ImageObjects++ - return nil -} - -// formWidget 将表单当前外观转换为页面对象并报告交互语义差异 +// formWidget 将表单当前外观转换为注解并报告交互语义差异 // 入参: ctx 取消上下文, page PDF页面, annotation 表单控件, strict 严格检查开关 // 返回: error 外观或转换错误 func (p *pdfImporter) formWidget(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, strict bool) error { @@ -326,12 +231,7 @@ func (p *pdfImporter) formWidget(ctx context.Context, page *pdfgo.Page, annotati if flags&(8|16) != 0 || annotation.Dictionary["OC"] != nil { return &pdfgo.UnsupportedError{Feature: "widget viewing behavior"} } - visitor := pdfgo.Visitor{Path: p.path, Text: p.text, Image: p.image, Warning: p.warning} - visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { return p.group(mark, walk, visitor) } - if err := p.reader.WalkAnnotationAppearance(ctx, page, annotation, visitor); err != nil { - return err - } - if err := p.flushPath(); err != nil { + if err := p.appearanceAnnotation(ctx, page, annotation); err != nil { return err } p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF form field appearance imported; interactive behavior not transferred"}) @@ -362,125 +262,9 @@ func (p *pdfImporter) signatureWidget(ctx context.Context, page *pdfgo.Page, ann p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "hidden PDF signature not transferred"}) return nil } - var images []pdfgo.ImageMark - visitor := pdfgo.Visitor{ - Path: func(pdfgo.PathMark) error { return &pdfgo.UnsupportedError{Feature: "signature appearance path"} }, - Text: func(pdfgo.TextMark) error { return &pdfgo.UnsupportedError{Feature: "signature appearance text"} }, - Image: func(mark pdfgo.ImageMark) error { - images = append(images, mark) - return nil - }, - } - if err := p.reader.WalkAnnotationAppearance(ctx, page, annotation, visitor); err != nil { + if err := p.appearanceAnnotation(ctx, page, annotation); err != nil { return err } - if len(images) != 1 { - return &pdfgo.UnsupportedError{Feature: "signature appearance composition"} - } - mark := images[0] - message := "PDF signature appearance imported; digital signature not transferred" - if mark.Style.BlendMode == "Multiply" { - if err := p.multiplySignatureAppearance(page, annotation, mark); err != nil { - return err - } - message = "PDF signature appearance flattened over page content; digital signature not transferred" - } else if mark.Style.BlendMode == "" || mark.Style.BlendMode == "Normal" || mark.Style.BlendMode == "Compatible" { - if err := p.image(mark); err != nil { - return err - } - } else { - return &pdfgo.UnsupportedError{Feature: "signature appearance blend mode"} - } - p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: message}) + p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF signature appearance imported; digital signature not transferred"}) return nil } - -// multiplySignatureAppearance 合成签名覆盖区域,正文及其他页面对象保持独立 -// 入参: page PDF页面, annotation 签名控件, mark 印章图像 -// 返回: error 图像或渲染错误 -func (p *pdfImporter) multiplySignatureAppearance(page *pdfgo.Page, annotation pdfgo.Annotation, mark pdfgo.ImageMark) error { - if err := p.commitObjects(); err != nil { - return err - } - base, err := renderImportedPage(p.editor, p.page, false) - if err != nil { - return err - } - _, width, height := pdfPageMatrix(page) - appearance := NewEditor() - appearance.SetRenderBackends(p.editor.backends) - if _, err := appearance.AddPage(width, height); err != nil { - return err - } - stamp := pdfImporter{reader: p.reader, editor: appearance, matrix: p.matrix, pageBox: page.CropBox} - mark.Style.BlendMode = "Normal" - if err := stamp.image(mark); err != nil { - return err - } - if _, err := appearance.CopyObjects(0, stamp.objects, 0, 0); err != nil { - return err - } - overlay, err := renderImportedPage(appearance, 0, false) - if err != nil { - return err - } - if base.Bounds() != overlay.Bounds() { - return fmt.Errorf("signature appearance page size mismatch") - } - annotationBox := annotation.Rect - box := pdfBounds([]pdfgo.Point{ - p.matrix.Apply(pdfgo.Point{X: annotationBox.XMin, Y: annotationBox.YMin}), - p.matrix.Apply(pdfgo.Point{X: annotationBox.XMax, Y: annotationBox.YMin}), - p.matrix.Apply(pdfgo.Point{X: annotationBox.XMax, Y: annotationBox.YMax}), - p.matrix.Apply(pdfgo.Point{X: annotationBox.XMin, Y: annotationBox.YMax}), - }) - const dpi = 300.0 - pixelsPerMM := dpi / 25.4 - region := image.Rect(int(math.Floor(box.X*pixelsPerMM)), int(math.Floor(box.Y*pixelsPerMM)), int(math.Ceil((box.X+box.W)*pixelsPerMM)), int(math.Ceil((box.Y+box.H)*pixelsPerMM))).Intersect(base.Bounds()) - if region.Empty() { - return fmt.Errorf("signature appearance outside page") - } - composite := image.NewRGBA(image.Rect(0, 0, region.Dx(), region.Dy())) - for y := region.Min.Y; y < region.Max.Y; y++ { - for x := region.Min.X; x < region.Max.X; x++ { - background := color.RGBAModel.Convert(base.At(x, y)).(color.RGBA) - foreground := color.RGBAModel.Convert(overlay.At(x, y)).(color.RGBA) - composite.SetRGBA(x-region.Min.X, y-region.Min.Y, color.RGBA{ - R: uint8((int(background.R)*int(foreground.R) + 127) / 255), - G: uint8((int(background.G)*int(foreground.G) + 127) / 255), - B: uint8((int(background.B)*int(foreground.B) + 127) / 255), - A: 255, - }) - } - } - var encoded bytes.Buffer - if err := png.Encode(&encoded, composite); err != nil { - return err - } - id, err := p.editor.AddImage(encoded.Bytes()) - if err != nil { - return err - } - regionBox := Box{X: float64(region.Min.X) / pixelsPerMM, Y: float64(region.Min.Y) / pixelsPerMM, W: float64(region.Dx()) / pixelsPerMM, H: float64(region.Dy()) / pixelsPerMM} - p.objects = append(p.objects, GraphicObject{Type: "ImageObject", ImageObject: ImageObject{Boundary: pdfBoundary(regionBox), ResourceID: id}}) - p.report.ImageObjects++ - return nil -} - -// renderImportedPage 在固定印刷分辨率绘制待合成页面 -// 入参: editor 编辑文档, page 页序号, transparent 是否保留透明背景 -// 返回: image.Image 页面像素, error 渲染错误 -func renderImportedPage(editor *Editor, page int, transparent bool) (image.Image, error) { - reader, err := editor.Reader() - if err != nil { - return nil, err - } - defer reader.Close() - content, err := reader.PageContentByIndex(page) - if err != nil { - return nil, err - } - renderer := NewRenderer(reader, WithDPI(300), WithRenderBackends(editor.backends), WithAnnotations(!transparent)) - renderer.TransparentBackground = transparent - return renderer.RenderToImage(content) -} diff --git a/ofdgo_pdf_composite.go b/ofdgo_pdf_composite.go index 9ac06f3..bd79858 100644 --- a/ofdgo_pdf_composite.go +++ b/ofdgo_pdf_composite.go @@ -140,32 +140,62 @@ func (p *pdfImporter) compositeTextBounds(mark pdfgo.TextMark, stroke bool) Box return Box{X: box.X - marginX, Y: box.Y - marginY, W: box.W + 2*marginX, H: box.H + 2*marginY} } -// pdfCompositeNodes 收集原始绘制顺序,语义警告只在首次解析时交付 -// 入参: walk 内容访问函数, warning 警告回调 +// collectCompositeNodes 收集原始绘制顺序并展开Type3字形,保留透明组层级 +// 入参: walk 内容访问函数 // 返回: []pdfCompositeNode 原始图元, error 解析错误 -func pdfCompositeNodes(walk func(pdfgo.Visitor) error, warning func(pdfgo.Diagnostic)) ([]pdfCompositeNode, error) { - var nodes []pdfCompositeNode - v := pdfgo.Visitor{Warning: warning} - v.Path = func(mark pdfgo.PathMark) error { nodes = append(nodes, pdfCompositeNode{path: &mark}); return nil } - v.Text = func(mark pdfgo.TextMark) error { nodes = append(nodes, pdfCompositeNode{text: &mark}); return nil } - v.Image = func(mark pdfgo.ImageMark) error { nodes = append(nodes, pdfCompositeNode{image: &mark}); return nil } - v.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { - children, err := pdfCompositeNodes(walk, warning) - if err != nil { - return err +func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]pdfCompositeNode, error) { + active := make(map[*pdfgo.Font]map[string]bool) + var collect func(func(pdfgo.Visitor) error) ([]pdfCompositeNode, error) + collect = func(walk func(pdfgo.Visitor) error) ([]pdfCompositeNode, error) { + var nodes []pdfCompositeNode + v := pdfgo.Visitor{Warning: p.warning} + v.Path = func(mark pdfgo.PathMark) error { nodes = append(nodes, pdfCompositeNode{path: &mark}); return nil } + v.Text = func(mark pdfgo.TextMark) error { + if mark.Font.Subtype != "Type3" { + nodes = append(nodes, pdfCompositeNode{text: &mark}) + return nil + } + if mark.Clip != nil { + return &pdfgo.UnsupportedError{Feature: "Type3 text clipping"} + } + glyphs := active[mark.Font] + if glyphs == nil { + glyphs = make(map[string]bool) + active[mark.Font] = glyphs + } + for index, glyph := range mark.Glyphs { + if glyphs[glyph.Name] { + return fmt.Errorf("recursive PDF Type3 glyph %q", glyph.Name) + } + glyphs[glyph.Name] = true + err := p.reader.WalkType3Glyph(p.ctx, mark, index, v) + delete(glyphs, glyph.Name) + if err != nil { + return err + } + } + return nil } - nodes = append(nodes, pdfCompositeNode{group: &mark, children: children}) - return nil + v.Image = func(mark pdfgo.ImageMark) error { nodes = append(nodes, pdfCompositeNode{image: &mark}); return nil } + v.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { + children, err := collect(walk) + if err != nil { + return err + } + nodes = append(nodes, pdfCompositeNode{group: &mark, children: children}) + return nil + } + err := walk(v) + return nodes, err } - err := walk(v) - return nodes, err + return collect(walk) } // compositePage 保留独立不透明对象,仅将需要背景参与的效果在局部区域合成 // 入参: space 页面混合空间, walk 页面内容 // 返回: error 转换错误 func (p *pdfImporter) compositePage(space *pdfgo.ColorSpace, walk func(pdfgo.Visitor) error) error { - nodes, err := pdfCompositeNodes(walk, p.warning) + nodes, err := p.collectCompositeNodes(walk) if err != nil { return err } @@ -203,6 +233,16 @@ func (p *pdfImporter) compositePage(space *pdfgo.ColorSpace, walk func(pdfgo.Vis space = &pdfgo.ColorSpace{Model: "DeviceCMYK"} } } + p.compositeNodes = nil + p.compositeSpace = space + return p.compositeObjects(nodes) +} + +// compositeObjects 按绘制顺序转换图元并保留后续混合所需的背景 +// 入参: nodes 待转换图元 +// 返回: error 转换或合成错误 +func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error { + space := p.compositeSpace for i, node := range nodes { if node.opaque(space) || space.SRGBEquivalent() && node.direct() { if err := node.emit(p.visitor()); err != nil { @@ -215,10 +255,11 @@ func (p *pdfImporter) compositePage(space *pdfgo.ColorSpace, walk func(pdfgo.Vis if err := p.flushPath(); err != nil { return err } - if err := p.compositeRegion(nodes[:i], node, space, true); err != nil { + if err := p.compositeRegion(p.compositeNodes, node, space, true); err != nil { return fmt.Errorf("composite graphic %d: %w", i+1, err) } } + p.compositeNodes = append(p.compositeNodes, node) } return p.flushPath() } diff --git a/ofdgo_pdf_composite_draw.go b/ofdgo_pdf_composite_draw.go index 4161c6e..1a46c7f 100644 --- a/ofdgo_pdf_composite_draw.go +++ b/ofdgo_pdf_composite_draw.go @@ -241,10 +241,16 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi } initial := make([]pdfCompositePixel, len(pixels)) if !g.Isolated { + sameSpace := space.Equal(parent) + components := parent.Components() for i, pixel := range pixels { - values, err := space.Convert(pixel.values[:parent.Components()], parent, "RelativeColorimetric") - if err != nil { - return err + values := pixel.values + if !sameSpace { + var err error + values, err = space.Convert(values[:components], parent, "RelativeColorimetric") + if err != nil { + return err + } } initial[i] = pdfCompositePixel{values: values, alpha: pixel.alpha} } @@ -313,18 +319,23 @@ func pdfCompositeKnockout(target *pdfCompositePixel, initial, source pdfComposit // 入参: pixels 父组输出, initial 初始背景, result 组结果, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式 // 返回: error 颜色或混合错误 func pdfCompositeGroup(pixels, initial, result []pdfCompositePixel, space, parent *pdfgo.ColorSpace, opacity float64, mask []float64, mode pdfgo.Name) error { + sameSpace := space.Equal(parent) + components := space.Components() for i, pixel := range result { pixels[i].shape += pixel.shape * (1 - pixels[i].shape) if pixel.effect == 0 { continue } values := pixel.values - for j := 0; j < space.Components(); j++ { + for j := 0; j < components; j++ { values[j] = math.Max(0, math.Min(1, (pixel.values[j]*pixel.alpha-initial[i].values[j]*initial[i].alpha*(1-pixel.effect))/pixel.effect)) } - values, err := parent.Convert(values[:space.Components()], space, "RelativeColorimetric") - if err != nil { - return err + if !sameSpace { + var err error + values, err = parent.Convert(values[:components], space, "RelativeColorimetric") + if err != nil { + return err + } } alpha := pixel.effect * opacity if mask != nil { @@ -350,7 +361,7 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace) nodes, ok := c.cache.masks[mask] var err error if !ok { - nodes, err = pdfCompositeNodes(mask.Walk, c.importer.warning) + nodes, err = c.importer.collectCompositeNodes(mask.Walk) if err != nil { return nil, err } @@ -379,7 +390,7 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace) return nil, err } } - result[i] = math.Max(0, math.Min(1, mask.Transfer[0]+v*(mask.Transfer[1]-mask.Transfer[0]))) + result[i] = mask.Transfer(v) } c.masks[mask] = result return result, nil diff --git a/ofdgo_pdf_composite_pattern.go b/ofdgo_pdf_composite_pattern.go index 0772172..b1783ab 100644 --- a/ofdgo_pdf_composite_pattern.go +++ b/ofdgo_pdf_composite_pattern.go @@ -44,7 +44,7 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel, return nil, fmt.Errorf("invalid PDF tiling pattern matrix") } if pattern == nil { - nodes, err := pdfCompositeNodes(func(v pdfgo.Visitor) error { return source.Walk(c.importer.ctx, paint, v) }, c.importer.warning) + nodes, err := c.importer.collectCompositeNodes(func(v pdfgo.Visitor) error { return source.Walk(c.importer.ctx, paint, v) }) if err != nil { return nil, err } diff --git a/ofdgo_pdf_group.go b/ofdgo_pdf_group.go index 6056623..da6fa64 100644 --- a/ofdgo_pdf_group.go +++ b/ofdgo_pdf_group.go @@ -522,7 +522,7 @@ func (p *pdfImporter) maskClip(mask *pdfgo.SoftMask) (pdfgo.Path, error) { return pdfgo.Path{}, err } opacity := func(v float64) (bool, error) { - v = math.Max(0, math.Min(1, mask.Transfer[0]+v*(mask.Transfer[1]-mask.Transfer[0]))) + v = mask.Transfer(v) if v != 0 && v != 1 { return false, &pdfgo.UnsupportedError{Feature: "nonbinary luminosity mask"} } diff --git a/ofdgo_pdf_link.go b/ofdgo_pdf_link.go index d456e7f..7afd500 100644 --- a/ofdgo_pdf_link.go +++ b/ofdgo_pdf_link.go @@ -176,6 +176,25 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict switch a["S"] { case pdfgo.Name("GoTo"): target = a["D"] + case pdfgo.Name("Named"): + value, err := p.reader.Resolve(a["N"]) + if err != nil { + return err + } + index := p.page + switch value { + case pdfgo.Name("FirstPage"): + index = 0 + case pdfgo.Name("LastPage"): + index = len(p.editor.pages) - 1 + case pdfgo.Name("NextPage"): + index = min(index+1, len(p.editor.pages)-1) + case pdfgo.Name("PrevPage"): + index = max(index-1, 0) + default: + return &pdfgo.UnsupportedError{Feature: "named link action"} + } + action.Goto = &Goto{Dest: &Dest{Type: "XYZ", PageID: p.editor.pages[index].ID, OmitLeft: true, OmitTop: true, OmitZoom: true}} case pdfgo.Name("URI"): value, err := p.reader.Resolve(a["URI"]) if err != nil { @@ -204,7 +223,7 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict return &pdfgo.UnsupportedError{Feature: "link action"} } } - if action.URI == nil { + if action.URI == nil && action.Goto == nil { destination, err := p.reader.ReadDestination(target) if err != nil { if !strict && errors.Is(err, pdfgo.ErrDestinationNotFound) {