mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
1 parent
ee6c9f6a16
commit
1d365d29db
9 files changed
+132
-271
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user