feat(注解转换): 完善注解合成并优化转换
build.yaml / build (push) Successful in 41s

This commit is contained in:
xiaoqidun committed 2026-09-28 16:16:05 +08:00
1 parent ee6c9f6a16
commit 1d365d29db
9 files changed
+132 -271

No files matched your search

+1 -1
View File
@@ -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
+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/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=
+10 -4
View File
@@ -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)
+18 -234
View File
@@ -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)
}
+59 -18
View File
@@ -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()
}
+20 -9
View File
@@ -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
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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"}
}
+20 -1
View File
@@ -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) {