fix(转换能力): 修复PDF转换回归
build.yaml / build (push) Successful in 4m39s

This commit is contained in:
xiaoqidun committed 2026-10-21 10:35:24 +08:00
1 parent 580592bbf8
commit b4d566019d
11 files changed
+793 -171

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@ require (
github.com/xiaoqidun/guigo v0.0.0-20261020075840-622fd8853d72 github.com/xiaoqidun/guigo v0.0.0-20261020075840-622fd8853d72
github.com/xiaoqidun/j2kgo v1.0.2 github.com/xiaoqidun/j2kgo v1.0.2
github.com/xiaoqidun/jbig2 v1.1.8 github.com/xiaoqidun/jbig2 v1.1.8
github.com/xiaoqidun/pdfgo v0.0.0-20261018110224-069b0fe96ea8 github.com/xiaoqidun/pdfgo v0.0.0-20261021023132-6452ac6abaef
golang.org/x/crypto v0.58.0 golang.org/x/crypto v0.58.0
golang.org/x/image v0.47.0 golang.org/x/image v0.47.0
golang.org/x/text v0.43.0 golang.org/x/text v0.43.0
+2 -2
View File
@@ -66,8 +66,8 @@ github.com/xiaoqidun/j2kgo v1.0.2 h1:OAjjEOfaBaQzBNcj9wx+eM68UI5pewR6SzNZa1R1DIw
github.com/xiaoqidun/j2kgo v1.0.2/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA= github.com/xiaoqidun/j2kgo v1.0.2/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
github.com/xiaoqidun/jbig2 v1.1.8 h1:dERPW7F8gs18a/DOD9gXHR3fP1Vl6KhCNsLnq5l3BIo= github.com/xiaoqidun/jbig2 v1.1.8 h1:dERPW7F8gs18a/DOD9gXHR3fP1Vl6KhCNsLnq5l3BIo=
github.com/xiaoqidun/jbig2 v1.1.8/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA= github.com/xiaoqidun/jbig2 v1.1.8/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA=
github.com/xiaoqidun/pdfgo v0.0.0-20261018110224-069b0fe96ea8 h1:vEcUgzXL2Qbk8iXTYEauKGFgyqfv2Ns9ZSy7UMBh3tk= github.com/xiaoqidun/pdfgo v0.0.0-20261021023132-6452ac6abaef h1:q0QmZrpkiTtTtNghOY7t+G30YrYu7V3/FgOi14NbdGc=
github.com/xiaoqidun/pdfgo v0.0.0-20261018110224-069b0fe96ea8/go.mod h1:AaabzsObZ8Z76AcaWbo6swRROrJsblyIGB7HbieeJA4= github.com/xiaoqidun/pdfgo v0.0.0-20261021023132-6452ac6abaef/go.mod h1:AaabzsObZ8Z76AcaWbo6swRROrJsblyIGB7HbieeJA4=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+69 -14
View File
@@ -18,6 +18,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"maps"
"slices" "slices"
"sort" "sort"
@@ -245,13 +246,14 @@ func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Ed
editor.SetRenderBackends(renderer.Backends()) editor.SetRenderBackends(renderer.Backends())
editor.fontDirs, editor.fontFS = renderer.FontSources() editor.fontDirs, editor.fontFS = renderer.FontSources()
editor.fontSourcesCache = renderer.fontSourcesCache editor.fontSourcesCache = renderer.fontSourcesCache
importer := pdfImporter{resources: options.Resources, ctx: ctx, editor: editor, renderer: renderer, rasterDPI: renderer.DPI, fontIDs: map[*pdfgo.Font]string{}, fonts: map[*pdfgo.Font]*pdfImportedFont{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}, structureBindings: &pdfStructureBindings{}} importer := pdfImporter{resources: options.Resources, ctx: ctx, editor: editor, renderer: renderer, rasterDPI: renderer.DPI, page: -1, fontIDs: map[*pdfgo.Font]string{}, fonts: map[*pdfgo.Font]*pdfImportedFont{}, pages: map[pdfgo.Reference]*pdfgo.Page{}, pageIDs: map[pdfgo.Reference]string{}, structureBindings: &pdfStructureBindings{}}
readOptions := pdfgo.ReaderOptions{Password: options.Password, PasswordUTF8: options.PasswordUTF8, ShapeText: importer.shapePDFText} readOptions := pdfgo.ReaderOptions{Password: options.Password, PasswordUTF8: options.PasswordUTF8, ShapeText: importer.shapePDFText}
var diagnostics []pdfgo.Diagnostic
var readWarning func(pdfgo.Diagnostic)
if !options.Strict { if !options.Strict {
readWarning = func(diagnostic pdfgo.Diagnostic) { diagnostics = append(diagnostics, diagnostic) } importer.warning = func(warning pdfgo.Diagnostic) {
readOptions.Warning = func(diagnostic pdfgo.Diagnostic) { readWarning(diagnostic) } warning.Page = importer.page + 1
importer.report.Warnings = append(importer.report.Warnings, warning)
}
readOptions.Warning = importer.warning
} }
reader, err := pdfgo.NewReaderWithOptions(convertReader{ctx: ctx, input: input}, input.Size(), readOptions) reader, err := pdfgo.NewReaderWithOptions(convertReader{ctx: ctx, input: input}, input.Size(), readOptions)
if err != nil { if err != nil {
@@ -266,12 +268,11 @@ func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Ed
if err != nil { if err != nil {
return nil, PDFImportReport{}, fmt.Errorf("import PDF output intents: %w", err) return nil, PDFImportReport{}, fmt.Errorf("import PDF output intents: %w", err)
} }
importer.report.Warnings = diagnostics
if len(outputIntents) != 0 { if len(outputIntents) != 0 {
if options.Strict { if options.Strict {
return nil, PDFImportReport{}, &pdfgo.UnsupportedError{Feature: "PDF output intent conversion"} return nil, PDFImportReport{}, &pdfgo.UnsupportedError{Feature: "PDF output intent conversion"}
} }
importer.report.Warnings = append(importer.report.Warnings, pdfgo.Diagnostic{Message: "PDF output intents not transferred to OFD"}) importer.report.Warnings = append(importer.report.Warnings, pdfgo.Diagnostic{Message: "PDF output intents preserved as metadata; not applied to rendering"})
} }
importer.pageIndexes = make(map[pdfgo.Reference]int) importer.pageIndexes = make(map[pdfgo.Reference]int)
importer.resolveFile = options.ResolveFile importer.resolveFile = options.ResolveFile
@@ -281,13 +282,6 @@ func ImportPDF(ctx context.Context, input *Input, options PDFImportOptions) (*Ed
importer.references = &codecpdf.ReferenceCache{} importer.references = &codecpdf.ReferenceCache{}
defer importer.references.Close() defer importer.references.Close()
importer.halftoneWarnings = make(map[*pdfgo.Halftone]bool) importer.halftoneWarnings = make(map[*pdfgo.Halftone]bool)
if !options.Strict {
importer.warning = func(warning pdfgo.Diagnostic) {
warning.Page = importer.page + 1
importer.report.Warnings = append(importer.report.Warnings, warning)
}
readWarning = importer.warning
}
if err := progress.report("pages", 0, 0); err != nil { if err := progress.report("pages", 0, 0); err != nil {
return nil, PDFImportReport{}, err return nil, PDFImportReport{}, err
} }
@@ -420,6 +414,67 @@ func (p *pdfImporter) referencePage(ctx context.Context, source *pdfgo.Reader, r
return p.references.Resolve(ctx, source, reference, p.resolveFile, p.resolveReference, p.warning) return p.references.Resolve(ctx, source, reference, p.resolveFile, p.resolveReference, p.warning)
} }
// pageAnnotations 保留不可显示的源注解声明,其他注解仍按原顺序读取
// 入参: ctx 取消上下文, page PDF页面
// 返回: []pdfgo.Annotation 可显示的注解, error 输入、取消或严格校验错误
func (p *pdfImporter) pageAnnotations(ctx context.Context, page *pdfgo.Page) ([]pdfgo.Annotation, error) {
annotations, err := page.AnnotationsContext(ctx)
if err == nil || p.warning == nil || !pdfAuxiliaryError(err) {
return annotations, err
}
object := page.Dictionary["Annots"]
if err := p.recoverPDFObject(ctx, "Page.Annots", object, err); err != nil {
return nil, err
}
value, err := p.reader.Resolve(object)
if err != nil {
return nil, p.recoverPDFObject(ctx, "Page.Annots", object, err)
}
array, ok := value.(pdfgo.Array)
if !ok {
return nil, ctx.Err()
}
for index, object := range array {
if err := ctx.Err(); err != nil {
return nil, err
}
candidate := *page
candidate.Dictionary = maps.Clone(page.Dictionary)
candidate.Dictionary["Annots"] = pdfgo.Array{object}
items, err := candidate.AnnotationsContext(ctx)
if err != nil && pdfAuxiliaryError(err) {
value, resolveErr := p.reader.Resolve(object)
if resolveErr != nil && !pdfAuxiliaryError(resolveErr) {
return nil, resolveErr
}
if dict, ok := value.(pdfgo.Dictionary); ok && dict["AP"] != nil {
subtype, resolveErr := p.reader.Resolve(dict["Subtype"])
if resolveErr != nil && !pdfAuxiliaryError(resolveErr) {
return nil, resolveErr
}
if _, named := subtype.(pdfgo.Name); !named && resolveErr == nil {
dict = maps.Clone(dict)
dict["Subtype"] = pdfgo.Name("Stamp")
candidate.Dictionary["Annots"] = pdfgo.Array{dict}
items, err = candidate.AnnotationsContext(ctx)
}
}
}
if err != nil {
if !pdfAuxiliaryError(err) {
return nil, err
}
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF annotation %d retained without appearance: %v", index+1, err)})
continue
}
if len(items) == 1 && items[0].Reference == (pdfgo.Reference{}) {
items[0].Reference, _ = object.(pdfgo.Reference)
}
annotations = append(annotations, items...)
}
return annotations, ctx.Err()
}
// objectValidation 在单次导入中共享不可变资源的校验结果,隔离局部编辑器 // objectValidation 在单次导入中共享不可变资源的校验结果,隔离局部编辑器
// 返回: *editorValidation 当前编辑器的构建校验会话 // 返回: *editorValidation 当前编辑器的构建校验会话
func (p *pdfImporter) objectValidation() *editorValidation { func (p *pdfImporter) objectValidation() *editorValidation {
+95 -69
View File
@@ -18,7 +18,6 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/xml" "encoding/xml"
"errors"
"fmt" "fmt"
"math" "math"
"strconv" "strconv"
@@ -36,12 +35,15 @@ type pdfDestinationFontBackend struct {
// 入参: ctx 取消上下文 // 入参: ctx 取消上下文
// 返回: error 动作、编码或取消错误 // 返回: error 动作、编码或取消错误
func (p *pdfImporter) documentOpenActions(ctx context.Context) error { func (p *pdfImporter) documentOpenActions(ctx context.Context) error {
raw, err := p.pdfCatalogObject("OpenAction")
if err != nil {
return err
}
opened, err := p.reader.ReadOpenAction(ctx) opened, err := p.reader.ReadOpenAction(ctx)
if err != nil { if err != nil {
if p.warning == nil || !errors.Is(err, pdfgo.ErrInvalidDestination) { if err := p.recoverPDFObject(ctx, "OpenAction", raw, err); err != nil {
return err return err
} }
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("invalid PDF initial destination ignored: %v", err)})
} }
var object pdfgo.Object var object pdfgo.Object
if opened.Action != nil { if opened.Action != nil {
@@ -52,7 +54,7 @@ func (p *pdfImporter) documentOpenActions(ctx context.Context) error {
object = pdfgo.Dictionary{"S": pdfgo.Name("GoTo"), "D": array} object = pdfgo.Dictionary{"S": pdfgo.Name("GoTo"), "D": array}
} }
currentPage := 0 currentPage := 0
actions, err := p.eventActions(ctx, object, "DO", pdfgo.Annotation{}, &currentPage) actions, err := p.eventActions(ctx, object, "DO", pdfgo.Annotation{}, &currentPage, raw)
if err != nil { if err != nil {
return err return err
} }
@@ -61,11 +63,18 @@ func (p *pdfImporter) documentOpenActions(ctx context.Context) error {
} }
triggers, err := p.reader.ReadDocumentTriggers(ctx) triggers, err := p.reader.ReadDocumentTriggers(ctx)
if err != nil { if err != nil {
return err object, readErr := p.pdfCatalogObject("AA")
if readErr != nil {
return readErr
}
return p.recoverPDFObject(ctx, "Document.AA", object, err)
} }
for _, trigger := range triggers { for _, trigger := range triggers {
if _, err := p.reader.ReadJavaScriptAction(ctx, trigger.Action); err != nil { if _, err := p.reader.ReadJavaScriptAction(ctx, trigger.Action); err != nil {
return err if err := p.recoverPDFObject(ctx, "Document.AA."+string(trigger.Event), trigger.Action, err); err != nil {
return err
}
continue
} }
if err := p.unmappedTrigger(ctx, "document", trigger); err != nil { if err := p.unmappedTrigger(ctx, "document", trigger); err != nil {
return err return err
@@ -81,7 +90,7 @@ func (p *pdfImporter) pageOpenActions(ctx context.Context, page *pdfgo.Page) err
p.pageActionPage = p.page p.pageActionPage = p.page
triggers, err := p.reader.ReadPageTriggers(ctx, page) triggers, err := p.reader.ReadPageTriggers(ctx, page)
if err != nil { if err != nil {
return err return p.recoverPDFObject(ctx, "Page.AA", page.Dictionary["AA"], err)
} }
for _, trigger := range triggers { for _, trigger := range triggers {
if trigger.Event != "O" { if trigger.Event != "O" {
@@ -102,32 +111,59 @@ func (p *pdfImporter) pageOpenActions(ctx context.Context, page *pdfgo.Page) err
} }
// eventActions 按原顺序转换动作链,保留事件类型与链内跳转页状态 // eventActions 按原顺序转换动作链,保留事件类型与链内跳转页状态
// 入参: ctx 取消上下文, object 动作对象, event OFD事件, source 触发注解,零值表示非图元动作, currentPage 当前页面 // 入参: ctx 取消上下文, object 动作对象, event OFD事件, source 触发注解,零值表示非图元动作, currentPage 当前页面, original 可选原始来源
// 返回: []Action 转换动作, error 动作或取消错误 // 返回: []Action 转换动作, error 动作或取消错误
func (p *pdfImporter) eventActions(ctx context.Context, object pdfgo.Object, event string, source pdfgo.Annotation, currentPage *int) ([]Action, error) { func (p *pdfImporter) eventActions(ctx context.Context, object pdfgo.Object, event string, source pdfgo.Annotation, currentPage *int, original ...pdfgo.Object) ([]Action, error) {
raw := object
if len(original) != 0 {
raw = original[0]
}
retained, warned := false, false
warning := p.warning
if warning != nil {
p.warning = func(diagnostic pdfgo.Diagnostic) {
warned = true
warning(diagnostic)
}
defer func() { p.warning = warning }()
}
retain := func(cause error) error {
if cause != nil && (p.warning == nil || !pdfAuxiliaryError(cause)) {
return cause
}
if retained {
if cause != nil {
p.warning(pdfgo.Diagnostic{Message: "PDF Action." + event + " retained as metadata: " + cause.Error()})
}
return ctx.Err()
}
var err error
if cause != nil {
err = p.recoverPDFObject(ctx, "Action."+event, raw, cause)
} else if p.warning != nil {
err = p.preservePDFObject(ctx, "Action."+event, raw)
}
retained = err == nil
return err
}
var actions []Action var actions []Action
var conversionErr error
err := p.reader.WalkActions(ctx, object, func(info pdfgo.ActionInfo) error { err := p.reader.WalkActions(ctx, object, func(info pdfgo.ActionInfo) error {
annotation := source annotation := source
annotation.Dictionary = pdfgo.Dictionary{"A": info.Dictionary} annotation.Dictionary = pdfgo.Dictionary{"A": info.Dictionary}
converted, err := p.linkActions(annotation, p.warning == nil, currentPage) warned = false
warnings := len(p.report.Warnings)
converted, err := p.convertLinkActions(annotation, p.warning == nil, currentPage)
if err != nil { if err != nil {
if errors.Is(err, errPDFMediaRead) { conversionErr = retain(err)
*currentPage = -1
return conversionErr
}
if p.warning != nil && (converted == nil || warned || len(p.report.Warnings) != warnings) {
if err := retain(nil); err != nil {
conversionErr = err
return err return err
} }
var unsupported *pdfgo.UnsupportedError
if event != "CLICK" && p.warning != nil && errors.Is(err, pdfgo.ErrInvalidAction) {
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("invalid PDF %s action ignored: %v", event, err)})
return nil
}
if event != "CLICK" && p.warning != nil && errors.Is(err, pdfgo.ErrInvalidDestination) {
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("invalid PDF %s destination ignored: %v", event, err)})
return nil
}
if event != "CLICK" && p.warning != nil && errors.As(err, &unsupported) {
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF %s action not transferred to OFD: %s", event, unsupported.Feature)})
return nil
}
return err
} }
for _, action := range converted { for _, action := range converted {
action.Event = event action.Event = event
@@ -135,8 +171,14 @@ func (p *pdfImporter) eventActions(ctx context.Context, object pdfgo.Object, eve
} }
return nil return nil
}) })
if conversionErr != nil {
return nil, conversionErr
}
if err != nil { if err != nil {
return nil, err if err := retain(err); err != nil {
return nil, err
}
*currentPage = -1
} }
return actions, ctx.Err() return actions, ctx.Err()
} }
@@ -146,14 +188,10 @@ func (p *pdfImporter) eventActions(ctx context.Context, object pdfgo.Object, eve
// 返回: error 动作链、取消或严格模式错误 // 返回: error 动作链、取消或严格模式错误
func (p *pdfImporter) unmappedTrigger(ctx context.Context, owner string, trigger pdfgo.ActionTrigger) error { func (p *pdfImporter) unmappedTrigger(ctx context.Context, owner string, trigger pdfgo.ActionTrigger) error {
if err := p.reader.WalkActions(ctx, trigger.Action, func(pdfgo.ActionInfo) error { return nil }); err != nil { if err := p.reader.WalkActions(ctx, trigger.Action, func(pdfgo.ActionInfo) error { return nil }); err != nil {
return err return p.recoverPDFObject(ctx, owner+".AA."+string(trigger.Event), trigger.Action, err)
} }
feature := owner + " trigger " + string(trigger.Event) feature := owner + " trigger " + string(trigger.Event)
if p.warning == nil { return p.recoverPDFObject(ctx, owner+".AA."+string(trigger.Event), trigger.Action, &pdfgo.UnsupportedError{Feature: feature})
return &pdfgo.UnsupportedError{Feature: feature}
}
p.warning(pdfgo.Diagnostic{Message: "PDF " + feature + " not transferred to OFD"})
return nil
} }
// annotationTriggerActions 按事件转换注解动作,页面打开动作独立于注解外观 // annotationTriggerActions 按事件转换注解动作,页面打开动作独立于注解外观
@@ -165,7 +203,7 @@ func (p *pdfImporter) annotationTriggerActions(ctx context.Context, annotation p
} }
triggers, err := p.reader.ReadAnnotationTriggers(ctx, annotation) triggers, err := p.reader.ReadAnnotationTriggers(ctx, annotation)
if err != nil { if err != nil {
return nil, err return nil, p.recoverPDFObject(ctx, "Annotation.AA", annotation.Dictionary["AA"], err)
} }
if len(triggers) == 0 { if len(triggers) == 0 {
return nil, ctx.Err() return nil, ctx.Err()
@@ -193,7 +231,10 @@ func (p *pdfImporter) annotationTriggerActions(ctx context.Context, annotation p
switch trigger.Event { switch trigger.Event {
case "U", "PO": case "U", "PO":
default: default:
return nil, &pdfgo.UnsupportedError{Feature: "annotation trigger " + string(trigger.Event)} if err := p.unmappedTrigger(ctx, "annotation", trigger); err != nil {
return nil, err
}
continue
} }
currentPage := p.page currentPage := p.page
if event == "PO" { if event == "PO" {
@@ -258,7 +299,8 @@ func (p *pdfImporter) destinationBounds(page *pdfgo.Page) (Box, error) {
local := pdfImporter{ local := pdfImporter{
resources: p.resources, resources: p.resources,
ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer, ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer,
matrix: matrix, pageBox: page.CropBox, pageWidth: width, pageHeight: height, warning: p.warning,
matrix: matrix, pageBox: page.CropBox, pageWidth: width, pageHeight: height,
rasterDPI: p.rasterDPI, fontIDs: make(map[*pdfgo.Font]string), rasterDPI: p.rasterDPI, fontIDs: make(map[*pdfgo.Font]string),
resolveFile: p.resolveFile, resolveReference: p.resolveReference, resolveFile: p.resolveFile, resolveReference: p.resolveReference,
references: &codecpdf.ReferenceCache{}, references: &codecpdf.ReferenceCache{},
@@ -320,17 +362,15 @@ func (p *pdfImporter) destinationBounds(page *pdfgo.Page) (Box, error) {
// 返回: error 严格模式转换错误或取消错误 // 返回: error 严格模式转换错误或取消错误
func (p *pdfImporter) outlines(ctx context.Context) error { func (p *pdfImporter) outlines(ctx context.Context) error {
count := 0 count := 0
var conversionErr error
data, err := encodeOFDXMLContext(ctx, func(x *ofdXML) { data, err := encodeOFDXMLContext(ctx, func(x *ofdXML) {
x.root("Outlines", nil) x.root("Outlines", nil)
depth := 0 depth := 0
err := p.reader.WalkOutlines(ctx, func(level int, item pdfgo.OutlineItem) error { err := p.reader.WalkOutlines(ctx, func(level int, item pdfgo.OutlineItem) error {
actions, err := p.outlineActions(ctx, item) actions, err := p.outlineActions(ctx, item)
if err != nil { if err != nil {
if p.warning == nil || ctx.Err() != nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, errPDFMediaRead) { conversionErr = err
return err return err
}
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF outline %q actions not transferred to OFD: %v", item.Title, err)})
actions = nil
} }
for depth > level { for depth > level {
x.end("OutlineElem") x.end("OutlineElem")
@@ -354,15 +394,21 @@ func (p *pdfImporter) outlines(ctx context.Context) error {
} }
x.end("Outlines") x.end("Outlines")
}) })
if conversionErr != nil {
return conversionErr
}
if err != nil { if err != nil {
if ctx.Err() != nil { if ctx.Err() != nil {
return ctx.Err() return ctx.Err()
} }
if p.warning == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) || errors.Is(err, errPDFMediaRead) { if p.warning == nil || !pdfAuxiliaryError(err) {
return err return err
} }
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF outlines not transferred to OFD: %v", err)}) object, readErr := p.pdfCatalogObject("Outlines")
return nil if readErr != nil {
return readErr
}
return p.recoverPDFObject(ctx, "Outlines", object, err)
} }
if count > 0 { if count > 0 {
p.editor.outlines = bytes.TrimPrefix(data, []byte(xml.Header)) p.editor.outlines = bytes.TrimPrefix(data, []byte(xml.Header))
@@ -374,42 +420,22 @@ func (p *pdfImporter) outlines(ctx context.Context) error {
// 入参: ctx 取消上下文, item PDF目录节点 // 入参: ctx 取消上下文, item PDF目录节点
// 返回: []Action OFD动作序列, error 动作或取消错误 // 返回: []Action OFD动作序列, error 动作或取消错误
func (p *pdfImporter) outlineActions(ctx context.Context, item pdfgo.OutlineItem) ([]Action, error) { func (p *pdfImporter) outlineActions(ctx context.Context, item pdfgo.OutlineItem) ([]Action, error) {
raw := item.Action
object, err := p.reader.Resolve(item.Action) object, err := p.reader.Resolve(item.Action)
if err != nil { if err != nil {
return nil, err return nil, p.recoverPDFObject(ctx, "Outline.A", item.Action, err)
} }
dest, err := p.reader.Resolve(item.Destination) dest, err := p.reader.Resolve(item.Destination)
if err != nil { if err != nil {
return nil, err return nil, p.recoverPDFObject(ctx, "Outline.Dest", item.Destination, err)
} }
if dest != nil { if dest != nil {
if object != nil { if object != nil {
return nil, fmt.Errorf("PDF outline has both destination and action") return nil, p.recoverPDFObject(ctx, "Outline", pdfgo.Dictionary{"A": item.Action, "Dest": item.Destination}, fmt.Errorf("PDF outline has both destination and action"))
} }
object = pdfgo.Dictionary{"S": pdfgo.Name("GoTo"), "D": dest} object = pdfgo.Dictionary{"S": pdfgo.Name("GoTo"), "D": dest}
raw = item.Destination
} }
currentPage := -1 currentPage := -1
var actions []Action return p.eventActions(ctx, object, "CLICK", pdfgo.Annotation{}, &currentPage, raw)
err = p.reader.WalkActions(ctx, object, func(info pdfgo.ActionInfo) error {
converted, err := p.linkActions(pdfgo.Annotation{Dictionary: pdfgo.Dictionary{"A": info.Dictionary}}, p.warning == nil, &currentPage)
if err != nil {
if errors.Is(err, errPDFMediaRead) {
return err
}
var unsupported *pdfgo.UnsupportedError
if p.warning != nil && (errors.As(err, &unsupported) || errors.Is(err, pdfgo.ErrInvalidDestination) || errors.Is(err, pdfgo.ErrInvalidAction)) {
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF outline action not transferred to OFD: %v", err)})
currentPage = -1
return nil
}
return err
}
if converted != nil {
actions = append(actions, converted...)
} else {
currentPage = -1
}
return nil
})
return actions, err
} }
+120 -13
View File
@@ -27,6 +27,95 @@ import (
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
// recoverAnnotationAuxiliary 仅在存在有效正常外观时保留辅助错误,外观仍由调用方实际绘制
// 入参: ctx 取消上下文, annotation 原始注解, name 辅助字段名, object 原始对象, cause 辅助错误
// 返回: error 严格模式、缺少有效外观或不可恢复错误
func (p *pdfImporter) recoverAnnotationAuxiliary(ctx context.Context, annotation pdfgo.Annotation, name string, object pdfgo.Object, cause error) error {
if err := ctx.Err(); err != nil {
return err
}
if p.warning == nil || !pdfAuxiliaryError(cause) {
return cause
}
appearance, err := p.reader.ReadAnnotationAppearance(annotation)
if err != nil {
return err
}
if appearance == nil {
return cause
}
return p.recoverPDFObject(ctx, name, object, cause)
}
// popupAnnotation 单独恢复弹窗的打开状态和父引用,不忽略外观或几何错误
// 入参: ctx 取消上下文, object 原始弹窗对象
// 返回: pdfgo.PopupAnnotation 弹窗信息, error 读取或不可恢复错误
func (p *pdfImporter) popupAnnotation(ctx context.Context, object pdfgo.Object) (pdfgo.PopupAnnotation, error) {
value, err := p.reader.Resolve(object)
if err != nil {
return pdfgo.PopupAnnotation{}, p.recoverPDFObject(ctx, "Annotation.Popup", object, err)
}
if value == nil {
return pdfgo.PopupAnnotation{}, nil
}
dict, ok := value.(pdfgo.Dictionary)
if !ok {
return pdfgo.PopupAnnotation{}, p.recoverPDFObject(ctx, "Annotation.Popup", object, fmt.Errorf("invalid popup annotation"))
}
dict = maps.Clone(dict)
open, err := p.reader.Resolve(dict["Open"])
if err == nil && open != nil && open != pdfgo.Boolean(false) && open != pdfgo.Boolean(true) {
err = fmt.Errorf("invalid popup open state")
}
if err != nil {
if err := p.recoverPDFObject(ctx, "Annotation.Popup.Open", dict["Open"], err); err != nil {
return pdfgo.PopupAnnotation{}, err
}
delete(dict, "Open")
}
if object := dict["Parent"]; object != nil {
parent, reference := object.(pdfgo.Reference)
value, err := p.reader.Resolve(object)
if err == nil && value != nil {
parentDict, ok := value.(pdfgo.Dictionary)
if !reference || !ok {
err = fmt.Errorf("invalid popup parent annotation")
} else {
subtype, resolveErr := p.reader.Resolve(parentDict["Subtype"])
err = resolveErr
if kind, ok := subtype.(pdfgo.Name); err == nil && (!ok || kind == "Popup") {
err = fmt.Errorf("invalid popup parent subtype")
}
}
} else if err == nil && !reference {
err = fmt.Errorf("invalid popup parent reference")
}
if err != nil {
if err := p.recoverPDFObject(ctx, "Annotation.Popup.Parent", object, err); err != nil {
return pdfgo.PopupAnnotation{}, err
}
delete(dict, "Parent")
} else {
dict["Parent"] = parent
}
}
return p.reader.ReadPopupAnnotation(dict)
}
// annotationOpen 读取文本注解初始打开状态,单独保留无效辅助字段
// 入参: ctx 取消上下文, dict 注解字典
// 返回: pdfgo.Object 打开状态, error 不可恢复错误
func (p *pdfImporter) annotationOpen(ctx context.Context, dict pdfgo.Dictionary) (pdfgo.Object, error) {
value, err := p.reader.Resolve(dict["Open"])
if err == nil && value != nil && value != pdfgo.Boolean(false) && value != pdfgo.Boolean(true) {
err = fmt.Errorf("invalid PDF text annotation open state")
}
if err != nil {
return nil, p.recoverPDFObject(ctx, "Annotation.Open", dict["Open"], err)
}
return value, nil
}
// appearanceAnnotation 将PDF静态外观转换为对应类型的OFD注解 // appearanceAnnotation 将PDF静态外观转换为对应类型的OFD注解
// 入参: ctx 取消上下文, page PDF页面, annotation PDF注解, actions 外观区域的动作 // 入参: ctx 取消上下文, page PDF页面, annotation PDF注解, actions 外观区域的动作
// 返回: error 外观或转换错误 // 返回: error 外观或转换错误
@@ -38,6 +127,21 @@ func (p *pdfImporter) appearanceAnnotation(ctx context.Context, page *pdfgo.Page
// 入参: ctx 取消上下文, page PDF页面, annotation PDF注解, paint 绘制回调,空值使用原外观, actions 外观区域的动作 // 入参: ctx 取消上下文, page PDF页面, annotation PDF注解, paint 绘制回调,空值使用原外观, actions 外观区域的动作
// 返回: error 外观或转换错误 // 返回: error 外观或转换错误
func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, paint func(*pdfImporter) error, actions ...Action) error { func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, annotation pdfgo.Annotation, paint func(*pdfImporter) error, actions ...Action) error {
if annotation.Subtype == "Projection" {
if _, err := p.reader.ReadProjectionAnnotation(annotation); err != nil {
if p.warning == nil || !pdfAuxiliaryError(err) {
return err
}
if _, err := p.reader.ReadAnnotationAppearance(annotation); err != nil {
return err
}
if err := p.recoverPDFObject(ctx, "Annotation.Projection.ExData", annotation.Dictionary["ExData"], err); err != nil {
return err
}
annotation.Dictionary = maps.Clone(annotation.Dictionary)
delete(annotation.Dictionary, "ExData")
}
}
clicks, err := p.annotationTriggerActions(ctx, annotation, "CLICK") clicks, err := p.annotationTriggerActions(ctx, annotation, "CLICK")
if err != nil { if err != nil {
return err return err
@@ -50,11 +154,13 @@ func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, a
continue continue
} }
value, err := p.reader.Resolve(annotation.Dictionary[key]) value, err := p.reader.Resolve(annotation.Dictionary[key])
if err != nil { if value != nil || err != nil {
return err if err == nil {
} err = &pdfgo.UnsupportedError{Feature: fmt.Sprintf("annotation field %q", key)}
if value != nil { }
return &pdfgo.UnsupportedError{Feature: fmt.Sprintf("annotation field %q", key)} if err := p.recoverPDFObject(ctx, "Annotation."+string(key), annotation.Dictionary[key], err); err != nil {
return err
}
} }
} }
remark, err := p.reader.ReadAnnotationTextContext(ctx, annotation, "Contents", p.warning) remark, err := p.reader.ReadAnnotationTextContext(ctx, annotation, "Contents", p.warning)
@@ -74,7 +180,9 @@ func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, a
} }
invisible := flags&1 != 0 && !annotation.IsStandard() invisible := flags&1 != 0 && !annotation.IsStandard()
if flags&256 != 0 { if flags&256 != 0 {
return &pdfgo.UnsupportedError{Feature: "annotation ToggleNoView flag"} if err := p.recoverPDFObject(ctx, "Annotation.F", annotation.Dictionary["F"], &pdfgo.UnsupportedError{Feature: "annotation ToggleNoView flag"}); err != nil {
return err
}
} }
box := pdfBounds([]pdfgo.Point{ box := pdfBounds([]pdfgo.Point{
p.matrix.Apply(pdfgo.Point{X: annotation.Rect.XMin, Y: annotation.Rect.YMin}), p.matrix.Apply(pdfgo.Point{X: annotation.Rect.XMin, Y: annotation.Rect.YMin}),
@@ -85,7 +193,9 @@ func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, a
if len(clicks) != 0 && annotation.Subtype == "Link" { if len(clicks) != 0 && annotation.Subtype == "Link" {
region, err := p.linkRegion(annotation, box) region, err := p.linkRegion(annotation, box)
if err != nil { if err != nil {
return err if err := p.recoverPDFObject(ctx, "Annotation.QuadPoints", annotation.Dictionary["QuadPoints"], err); err != nil {
return err
}
} }
for i := range clicks { for i := range clicks {
clicks[i].Region = region clicks[i].Region = region
@@ -151,21 +261,18 @@ func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, a
converted.Parameters = &AnnotationParameters{Parameter: []AnnotationParameter{{Name: "PDF.Redact.Pending", Value: "true"}}} converted.Parameters = &AnnotationParameters{Parameter: []AnnotationParameter{{Name: "PDF.Redact.Pending", Value: "true"}}}
} }
if annotation.Subtype == "Text" { if annotation.Subtype == "Text" {
value, err := p.reader.Resolve(annotation.Dictionary["Open"]) value, err := p.annotationOpen(ctx, annotation.Dictionary)
if err != nil { if err != nil {
return err return err
} }
converted.Parameters = &AnnotationParameters{Parameter: []AnnotationParameter{{Name: "PDF.Text.Open", Value: strconv.FormatBool(value == pdfgo.Boolean(true))}}} converted.Parameters = &AnnotationParameters{Parameter: []AnnotationParameter{{Name: "PDF.Text.Open", Value: strconv.FormatBool(value == pdfgo.Boolean(true))}}}
} }
popupObject, err := p.reader.Resolve(annotation.Dictionary["Popup"]) popupObject := annotation.Dictionary["Popup"]
if err != nil {
return err
}
if annotation.Subtype == "Popup" { if annotation.Subtype == "Popup" {
popupObject = annotation.Dictionary popupObject = annotation.Dictionary
} }
if popupObject != nil { if popupObject != nil {
popup, err := p.reader.ReadPopupAnnotation(popupObject) popup, err := p.popupAnnotation(ctx, popupObject)
if err != nil { if err != nil {
return err return err
} }
+38 -4
View File
@@ -54,6 +54,9 @@ func pdfGeometryPath(ctx context.Context, source pdfgo.Path, matrix pdfgo.Matrix
// 入参: node 图元, stroke 是否描边 // 入参: node 图元, stroke 是否描边
// 返回: *RasterPage 覆盖场景, Box 页面区域, error 编译错误 // 返回: *RasterPage 覆盖场景, Box 页面区域, error 编译错误
func (p *pdfImporter) compileCoverage(node pdfCompositeNode, stroke bool) (*RasterPage, Box, error) { func (p *pdfImporter) compileCoverage(node pdfCompositeNode, stroke bool) (*RasterPage, Box, error) {
if node.path != nil && pdfPaintedPathEmpty(node.path.Path) {
return nil, Box{}, p.emptyPaintedPath()
}
fallback := func() (*RasterPage, Box, error) { fallback := func() (*RasterPage, Box, error) {
return p.compileGroup(func(local *pdfImporter) error { return node.coverage(local, stroke) }) return p.compileGroup(func(local *pdfImporter) error { return node.coverage(local, stroke) })
} }
@@ -95,7 +98,7 @@ func (p *pdfImporter) compileCoverage(node pdfCompositeNode, stroke bool) (*Rast
return nil, Box{}, err return nil, Box{}, err
} }
if len(path) == 0 { if len(path) == 0 {
return nil, Box{}, fmt.Errorf("empty PDF painted path") return nil, Box{}, p.emptyPaintedPath()
} }
path = GeometryPath(basegeom.Close(basegeom.Path(path))) path = GeometryPath(basegeom.Close(basegeom.Path(path)))
clip, err := p.coverageClip(style.Clips, geometry) clip, err := p.coverageClip(style.Clips, geometry)
@@ -298,6 +301,12 @@ func (b *pdfPointBounds) box() Box {
// 入参: mark PDF路径绘制信息 // 入参: mark PDF路径绘制信息
// 返回: error 错误信息 // 返回: error 错误信息
func (p *pdfImporter) path(mark pdfgo.PathMark) error { func (p *pdfImporter) path(mark pdfgo.PathMark) error {
if err := p.ctx.Err(); err != nil {
return err
}
if pdfPaintedPathEmpty(mark.Path) {
return p.emptyPaintedPath()
}
if mark.Style.BlendMode != "" && mark.Style.BlendMode != "Normal" && mark.Style.BlendMode != "Compatible" { if mark.Style.BlendMode != "" && mark.Style.BlendMode != "Normal" && mark.Style.BlendMode != "Compatible" {
return &pdfgo.UnsupportedError{Feature: "path blend mode " + string(mark.Style.BlendMode)} return &pdfgo.UnsupportedError{Feature: "path blend mode " + string(mark.Style.BlendMode)}
} }
@@ -308,6 +317,9 @@ func (p *pdfImporter) path(mark pdfgo.PathMark) error {
// 入参: mark PDF路径绘制信息 // 入参: mark PDF路径绘制信息
// 返回: error 错误信息 // 返回: error 错误信息
func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error { func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
if pdfPaintedPathEmpty(mark.Path) {
return p.emptyPaintedPath()
}
var err error var err error
mark.Style, err = p.maskStyle(mark.Style) mark.Style, err = p.maskStyle(mark.Style)
if err != nil { if err != nil {
@@ -331,9 +343,6 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
if err != nil { if err != nil {
return err return err
} }
if box == (Box{}) {
return fmt.Errorf("empty PDF painted path")
}
scale := math.Hypot(p.matrix[0], p.matrix[1]) scale := math.Hypot(p.matrix[0], p.matrix[1])
drawing, origin, ctm := *p, box, "" drawing, origin, ctm := *p, box, ""
marginScale := scale marginScale := scale
@@ -410,6 +419,31 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
return nil return nil
} }
// pdfPaintedPathEmpty 判断无坐标路径,不排除具有坐标的退化描边
// 入参: path PDF路径
// 返回: bool 是否没有坐标
func pdfPaintedPathEmpty(path pdfgo.Path) bool {
for _, segment := range path.Segments {
if segment.Operator != "C" || len(segment.Points) != 0 {
return false
}
}
return true
}
// emptyPaintedPath 按诊断策略忽略不绘制内容的空路径,严格模式仍拒绝
// 返回: error 严格模式或取消错误
func (p *pdfImporter) emptyPaintedPath() error {
if err := p.ctx.Err(); err != nil {
return err
}
if p.warning == nil {
return fmt.Errorf("empty PDF painted path")
}
p.warning(pdfgo.Diagnostic{Message: "empty PDF painted path ignored"})
return p.ctx.Err()
}
// clipPath 复用页面内重复路径编码,按估算字节预算淘汰且不缓存错误 // clipPath 复用页面内重复路径编码,按估算字节预算淘汰且不缓存错误
// 入参: path 裁剪路径 // 入参: path 裁剪路径
// 返回: pdfClipPath 变换后的边界及编码, error 取消错误 // 返回: pdfClipPath 变换后的边界及编码, error 取消错误
+76 -31
View File
@@ -37,7 +37,7 @@ type pdfEmbeddedFile struct {
// 返回: error 错误信息 // 返回: error 错误信息
func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict bool) error { func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict bool) error {
p.movieBounds = nil p.movieBounds = nil
annotations, err := page.AnnotationsContext(ctx) annotations, err := p.pageAnnotations(ctx, page)
if err != nil { if err != nil {
return err return err
} }
@@ -70,7 +70,7 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
if annotation.Subtype != "Popup" { if annotation.Subtype != "Popup" {
continue continue
} }
popup, err := p.reader.ReadPopupAnnotation(annotation.Dictionary) popup, err := p.popupAnnotation(ctx, annotation.Dictionary)
if err != nil { if err != nil {
return err return err
} }
@@ -78,11 +78,15 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
dict := maps.Clone(annotations[parent].Dictionary) dict := maps.Clone(annotations[parent].Dictionary)
previous, err := p.reader.Resolve(dict["Popup"]) previous, err := p.reader.Resolve(dict["Popup"])
if err != nil { if err != nil {
return err if err := p.recoverPDFObject(ctx, "Annotation.Popup", dict["Popup"], err); err != nil {
return err
}
} }
if previous != nil { if previous != nil {
if reference, ok := dict["Popup"].(pdfgo.Reference); !ok || reference != annotation.Reference { if reference, ok := dict["Popup"].(pdfgo.Reference); !ok || reference != annotation.Reference {
return fmt.Errorf("inconsistent PDF popup parent") if err := p.recoverPDFObject(ctx, "Annotation.Popup", dict["Popup"], fmt.Errorf("inconsistent PDF popup parent")); err != nil {
return err
}
} }
} }
dict["Popup"] = annotation.Dictionary dict["Popup"] = annotation.Dictionary
@@ -127,13 +131,10 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
annotation.Dictionary = dict annotation.Dictionary = dict
} }
if annotation.Subtype == "Text" { if annotation.Subtype == "Text" {
value, err := p.reader.Resolve(dict["Open"]) value, err := p.annotationOpen(ctx, dict)
if err != nil { if err != nil {
return err return err
} }
if value != nil && value != pdfgo.Boolean(false) && value != pdfgo.Boolean(true) {
return fmt.Errorf("invalid PDF text annotation open state")
}
if value == pdfgo.Boolean(true) { if value == pdfgo.Boolean(true) {
if strict { if strict {
return &pdfgo.UnsupportedError{Feature: "initially open text annotation"} return &pdfgo.UnsupportedError{Feature: "initially open text annotation"}
@@ -141,20 +142,19 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF text annotation open state retained as annotation parameter"}) p.warning(pdfgo.Diagnostic{Page: p.page + 1, Message: "PDF text annotation open state retained as annotation parameter"})
} }
} }
popupObject, err := p.reader.Resolve(dict["Popup"]) popupObject := dict["Popup"]
if err != nil {
return err
}
if annotation.Subtype == "Popup" { if annotation.Subtype == "Popup" {
popupObject = dict popupObject = dict
} }
if popupObject != nil { if popupObject != nil {
popup, err := p.reader.ReadPopupAnnotation(popupObject) popup, err := p.popupAnnotation(ctx, popupObject)
if err != nil { if err != nil {
return err return err
} }
if annotation.Subtype != "Popup" && popup.Parent != (pdfgo.Reference{}) && popup.Parent != annotation.Reference { if annotation.Subtype != "Popup" && popup.Parent != (pdfgo.Reference{}) && popup.Parent != annotation.Reference {
return fmt.Errorf("inconsistent PDF popup parent") if err := p.recoverPDFObject(ctx, "Annotation.Popup.Parent", popupObject, fmt.Errorf("inconsistent PDF popup parent")); err != nil {
return err
}
} }
if popup.Open { if popup.Open {
if strict { if strict {
@@ -166,11 +166,20 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
if annotation.Subtype == "Sound" { if annotation.Subtype == "Sound" {
sound, err := p.reader.ReadSound(dict["Sound"]) sound, err := p.reader.ReadSound(dict["Sound"])
if err != nil { if err != nil {
return err if err := p.recoverPDFObject(ctx, "Annotation.Sound", dict["Sound"], err); err != nil {
return err
}
if err := p.appearanceAnnotation(ctx, page, annotation); err != nil {
return err
}
continue
} }
id, err := p.soundResource(ctx, sound) id, err := p.soundResource(ctx, sound)
if err != nil { if err != nil {
return err if err := p.recoverPDFObject(ctx, "Annotation.Sound", dict["Sound"], err); err != nil {
return err
}
id = ""
} }
if id == "" { if id == "" {
if err := p.appearanceAnnotation(ctx, page, annotation); err != nil { if err := p.appearanceAnnotation(ctx, page, annotation); err != nil {
@@ -228,7 +237,13 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
if annotation.Subtype == "Widget" { if annotation.Subtype == "Widget" {
field, err := p.reader.ReadField(annotation) field, err := p.reader.ReadField(annotation)
if err != nil { if err != nil {
return err if err := p.recoverAnnotationAuxiliary(ctx, annotation, "Annotation.Field", annotation.Dictionary, err); err != nil {
return err
}
if err := p.appearanceAnnotation(ctx, page, annotation); err != nil {
return err
}
continue
} }
annotation.Dictionary = field annotation.Dictionary = field
if field["FT"] == pdfgo.Name("Sig") && field["V"] != nil { if field["FT"] == pdfgo.Name("Sig") && field["V"] != nil {
@@ -273,10 +288,6 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
} }
continue continue
} }
primary, err := p.reader.Resolve(dict["A"])
if err != nil {
return err
}
var actions []Action var actions []Action
currentPage := p.page currentPage := p.page
appendAction := func(dict pdfgo.Dictionary) error { appendAction := func(dict pdfgo.Dictionary) error {
@@ -289,23 +300,29 @@ func (p *pdfImporter) annotations(ctx context.Context, page *pdfgo.Page, strict
actions = append(actions, converted...) actions = append(actions, converted...)
return nil return nil
} }
if primary == nil { primary, err := p.reader.Resolve(dict["A"])
if err != nil {
if err := p.recoverPDFObject(ctx, "Annotation.A", dict["A"], err); err != nil {
return err
}
} else if primary == nil {
if err := appendAction(dict); err != nil { if err := appendAction(dict); err != nil {
return err return err
} }
} else if err := p.reader.WalkActions(ctx, primary, func(info pdfgo.ActionInfo) error { } else {
current := maps.Clone(dict) actions, err = p.eventActions(ctx, dict["A"], "CLICK", annotation, &currentPage)
current["A"] = info.Dictionary if err != nil {
return appendAction(current) return err
}); err != nil { }
return err
} }
b := annotation.Rect b := annotation.Rect
box := pdfBounds([]pdfgo.Point{p.matrix.Apply(pdfgo.Point{X: b.XMin, Y: b.YMin}), p.matrix.Apply(pdfgo.Point{X: b.XMax, Y: b.YMax})}) box := pdfBounds([]pdfgo.Point{p.matrix.Apply(pdfgo.Point{X: b.XMin, Y: b.YMin}), p.matrix.Apply(pdfgo.Point{X: b.XMax, Y: b.YMax})})
region, err := p.linkRegion(annotation, box) region, err := p.linkRegion(annotation, box)
if err != nil { if err != nil {
return err if err := p.recoverPDFObject(ctx, "Annotation.QuadPoints", dict["QuadPoints"], err); err != nil {
return err
}
} }
for i := range actions { for i := range actions {
actions[i].Region = region actions[i].Region = region
@@ -366,7 +383,32 @@ func (p *pdfImporter) linkRegion(annotation pdfgo.Annotation, box Box) (*Region,
// linkActions 将单个PDF动作转换为有序OFD动作,保留动态媒体控制的目标范围 // linkActions 将单个PDF动作转换为有序OFD动作,保留动态媒体控制的目标范围
// 入参: annotation 触发注解, strict 严格检查开关, currentPage 动作链当前页 // 入参: annotation 触发注解, strict 严格检查开关, currentPage 动作链当前页
// 返回: []Action 动作序列,空切片表示无操作,nil表示未转换, error 转换错误 // 返回: []Action 动作序列,空切片表示无操作,nil表示未转换, error 转换错误
func (p *pdfImporter) linkActions(annotation pdfgo.Annotation, strict bool, currentPage *int) ([]Action, error) { func (p *pdfImporter) linkActions(annotation pdfgo.Annotation, strict bool, currentPage *int) (actions []Action, err error) {
object := pdfgo.Dictionary{}
for _, key := range []pdfgo.Name{"A", "Dest"} {
if value, ok := annotation.Dictionary[key]; ok {
object[key] = value
}
}
warnings := len(p.report.Warnings)
defer func() {
if err != nil && pdfAuxiliaryError(err) {
err = p.recoverPDFObject(p.ctx, "Link", object, err)
if err == nil {
actions = nil
*currentPage = -1
}
} else if err == nil && (actions == nil || len(p.report.Warnings) != warnings) && len(object) != 0 && p.warning != nil {
err = p.preservePDFObject(p.ctx, "Link", object)
}
}()
return p.convertLinkActions(annotation, strict, currentPage)
}
// convertLinkActions 转换单个动作,由调用方在辅助失败时保留原始来源
// 入参: annotation 触发注解, strict 严格检查开关, currentPage 动作链当前页
// 返回: []Action 转换动作, error 动作或读取错误
func (p *pdfImporter) convertLinkActions(annotation pdfgo.Annotation, strict bool, currentPage *int) ([]Action, error) {
value, err := p.reader.Resolve(annotation.Dictionary["A"]) value, err := p.reader.Resolve(annotation.Dictionary["A"])
if err != nil { if err != nil {
return nil, err return nil, err
@@ -521,6 +563,9 @@ func (p *pdfImporter) linkAction(annotation pdfgo.Annotation, strict bool, curre
if destination.Mode == "FitB" { if destination.Mode == "FitB" {
box, err := p.destinationBounds(targetPage) box, err := p.destinationBounds(targetPage)
if err != nil { if err != nil {
if !pdfAuxiliaryError(err) {
return nil, err
}
if canceled := p.ctx.Err(); canceled != nil { if canceled := p.ctx.Err(); canceled != nil {
return nil, canceled return nil, canceled
} }
@@ -641,7 +686,7 @@ func (p *pdfImporter) remoteLinkAction(object pdfgo.Object, strict bool) (*Actio
} }
data, err := p.reader.ReadFileData(p.ctx, *action.File, p.resolveFile) data, err := p.reader.ReadFileData(p.ctx, *action.File, p.resolveFile)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("%w: %w", errPDFMediaRead, err)
} }
id, err := p.attachmentFile(action.File.Name, data) id, err := p.attachmentFile(action.File.Name, data)
if err != nil { if err != nil {
+66 -7
View File
@@ -19,6 +19,7 @@ import (
"crypto/sha256" "crypto/sha256"
"errors" "errors"
"fmt" "fmt"
"maps"
"math" "math"
"path" "path"
"reflect" "reflect"
@@ -336,12 +337,39 @@ func (p *pdfImporter) movieAnnotation(ctx context.Context, page *pdfgo.Page, ann
return err return err
} }
movie, err := p.reader.ReadMovie(annotation.Dictionary["Movie"]) movie, err := p.reader.ReadMovie(annotation.Dictionary["Movie"])
if err != nil && p.warning != nil && pdfAuxiliaryError(err) {
value, resolveErr := p.reader.Resolve(annotation.Dictionary["Movie"])
if resolveErr != nil {
return resolveErr
}
if dict, ok := value.(pdfgo.Dictionary); ok && dict["Aspect"] != nil {
corrected := maps.Clone(dict)
delete(corrected, "Aspect")
retry, readErr := p.reader.ReadMovie(corrected)
if readErr == nil {
if err := p.recoverPDFObject(ctx, "Annotation.Movie", annotation.Dictionary["Movie"], err); err != nil {
return err
}
annotation.Dictionary = maps.Clone(annotation.Dictionary)
annotation.Dictionary["Movie"] = corrected
movie, err = retry, nil
} else if !pdfAuxiliaryError(readErr) {
return readErr
}
}
}
if err != nil { if err != nil {
return err if err := p.recoverAnnotationAuxiliary(ctx, annotation, "Annotation.Movie", annotation.Dictionary["Movie"], err); err != nil {
return err
}
return p.appearanceAnnotation(ctx, page, annotation)
} }
activation, err := p.reader.Resolve(annotation.Dictionary["A"]) activation, err := p.reader.Resolve(annotation.Dictionary["A"])
if err != nil { if err != nil {
return err if err := p.recoverAnnotationAuxiliary(ctx, annotation, "Annotation.Movie.A", annotation.Dictionary["A"], err); err != nil {
return err
}
return p.appearanceAnnotation(ctx, page, annotation)
} }
if activation == pdfgo.Boolean(false) { if activation == pdfgo.Boolean(false) {
return p.appearanceAnnotation(ctx, page, annotation) return p.appearanceAnnotation(ctx, page, annotation)
@@ -351,7 +379,10 @@ func (p *pdfImporter) movieAnnotation(ctx context.Context, page *pdfgo.Page, ann
} }
parameters, err := p.reader.ReadMovieActivation(ctx, activation) parameters, err := p.reader.ReadMovieActivation(ctx, activation)
if err != nil { if err != nil {
return err if err := p.recoverAnnotationAuxiliary(ctx, annotation, "Annotation.Movie.A", annotation.Dictionary["A"], err); err != nil {
return err
}
return p.appearanceAnnotation(ctx, page, annotation)
} }
if !pdfMoviePlaybackSupported(movie, parameters) { if !pdfMoviePlaybackSupported(movie, parameters) {
if err := p.moviePlaybackLoss(ctx, movie.File, strict); err != nil { if err := p.moviePlaybackLoss(ctx, movie.File, strict); err != nil {
@@ -523,7 +554,28 @@ func (p *pdfImporter) movieActionLoss(feature string, strict, retained bool) err
// 入参: ctx 取消上下文 // 入参: ctx 取消上下文
// 返回: error 名称树、附件读取或写入错误 // 返回: error 名称树、附件读取或写入错误
func (p *pdfImporter) documentAttachments(ctx context.Context) error { func (p *pdfImporter) documentAttachments(ctx context.Context) error {
return p.reader.WalkEmbeddedFiles(ctx, func(_ string, file pdfgo.FileSpecification) error { var original pdfgo.Object
if p.warning != nil {
names, err := p.pdfCatalogObject("Names")
if err != nil {
return err
}
value, err := p.reader.Resolve(names)
if err != nil {
return p.recoverPDFObject(ctx, "Names", names, err)
}
if dictionary, ok := value.(pdfgo.Dictionary); ok {
original = dictionary["EmbeddedFiles"]
if original != nil {
if err := p.preservePDFObject(ctx, "EmbeddedFiles", original); err != nil {
return err
}
}
} else if value != nil {
return p.recoverPDFObject(ctx, "Names", names, fmt.Errorf("invalid names dictionary"))
}
}
err := p.reader.WalkEmbeddedFiles(ctx, func(_ string, file pdfgo.FileSpecification) error {
if file.Embedded == nil { if file.Embedded == nil {
if p.warning == nil { if p.warning == nil {
return fmt.Errorf("PDF embedded file has no embedded data") return fmt.Errorf("PDF embedded file has no embedded data")
@@ -533,16 +585,23 @@ func (p *pdfImporter) documentAttachments(ctx context.Context) error {
} }
data, err := file.Embedded.DecodeContext(ctx) data, err := file.Embedded.DecodeContext(ctx)
if err != nil { if err != nil {
var unsupported *pdfgo.UnsupportedError if p.warning != nil && pdfAuxiliaryError(err) {
if p.warning != nil && errors.As(err, &unsupported) { p.warning(pdfgo.Diagnostic{Message: "PDF document attachment retained as metadata: " + err.Error()})
p.warning(pdfgo.Diagnostic{Message: "PDF document attachment not transferred to OFD: " + err.Error()})
return nil return nil
} }
return err return err
} }
_, err = p.attachmentFile(file.Name, data) _, err = p.attachmentFile(file.Name, data)
if err != nil && p.warning != nil && pdfAuxiliaryError(err) {
p.warning(pdfgo.Diagnostic{Message: "PDF document attachment retained as metadata: " + err.Error()})
return nil
}
return err return err
}) })
if err != nil {
return p.recoverPDFObject(ctx, "EmbeddedFiles", original, err)
}
return ctx.Err()
} }
// attachmentFile 按原名及数据复用附件,兼容末尾NUL,缺名使用固定名称 // attachmentFile 按原名及数据复用附件,兼容末尾NUL,缺名使用固定名称
+248 -26
View File
@@ -18,9 +18,11 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/xml" "encoding/xml"
"errors"
"fmt" "fmt"
"maps" "maps"
"math" "math"
"os"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@@ -49,12 +51,14 @@ const (
pdfMetadataTagType = "OFDGo.PDFMetadata" pdfMetadataTagType = "OFDGo.PDFMetadata"
pdfInformationTagType = "OFDGo.PDFInformation" pdfInformationTagType = "OFDGo.PDFInformation"
pdfInformationNamespace = "https://ofdgo.aite.me/ns/pdf/information/1" pdfInformationNamespace = "https://ofdgo.aite.me/ns/pdf/information/1"
pdfAuxiliaryTagType = "OFDGo.PDFAuxiliary"
) )
// pdfStructureBindings 保存来源区段与最终页面图元的关联,不记录临时渲染对象 // pdfStructureBindings 保存来源区段与最终页面图元的关联,不记录临时渲染对象
type pdfStructureBindings struct { type pdfStructureBindings struct {
parents map[*pdfgo.Reader]map[[2]int64]pdfgo.Reference parents map[*pdfgo.Reader]map[[2]int64]pdfgo.Reference
prepared map[*pdfgo.Reader]bool prepared map[*pdfgo.Reader]bool
invalid map[*pdfgo.Reader]bool
readers []*pdfgo.Reader readers []*pdfgo.Reader
marked map[pdfStructureContent]bool marked map[pdfStructureContent]bool
sequences map[*pdfgo.ContentSequence][]StructureReference sequences map[*pdfgo.ContentSequence][]StructureReference
@@ -104,11 +108,12 @@ type pdfStructureBinding struct {
// pdfStructureValues 保存属性值类型及间接对象共享关系 // pdfStructureValues 保存属性值类型及间接对象共享关系
type pdfStructureValues struct { type pdfStructureValues struct {
ctx context.Context ctx context.Context
reader *pdfgo.Reader reader *pdfgo.Reader
seen map[pdfgo.Reference]bool seen map[pdfgo.Reference]bool
elements map[pdfgo.Reference]bool elements map[pdfgo.Reference]bool
objects []StructureNode objects []StructureNode
auxiliary bool
} }
// pdfMetadataValue 保留字段是否声明及其来源,空文本不作为缺省值 // pdfMetadataValue 保留字段是否声明及其来源,空文本不作为缺省值
@@ -117,10 +122,39 @@ type pdfMetadataValue struct {
xmp bool xmp bool
} }
// prepareStructure 校验结构内容关联并登记引用区段,不提前写入标引 // prepareStructure 校验结构内容关联,无法映射时保留原始结构
// 入参: ctx 取消上下文 // 入参: ctx 取消上下文
// 返回: error 结构或内容容器错误 // 返回: error 读取、严格校验或取消错误
func (p *pdfImporter) prepareStructure(ctx context.Context) error { func (p *pdfImporter) prepareStructure(ctx context.Context) error {
err := p.readStructureBindings(ctx)
if err == nil {
return nil
}
if p.warning == nil || !pdfAuxiliaryError(err) {
return err
}
object, readErr := p.pdfCatalogObject("StructTreeRoot")
if readErr != nil {
return readErr
}
if err := p.recoverPDFObject(ctx, "Structure", object, err); err != nil {
return err
}
b := p.structureBindings
if b.prepared == nil {
b.prepared = make(map[*pdfgo.Reader]bool)
}
if b.invalid == nil {
b.invalid = make(map[*pdfgo.Reader]bool)
}
b.prepared[p.reader], b.invalid[p.reader] = true, true
return ctx.Err()
}
// readStructureBindings 校验结构内容关联并准备有效图元绑定
// 入参: ctx 取消上下文
// 返回: error 结构、内容容器或取消错误
func (p *pdfImporter) readStructureBindings(ctx context.Context) error {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
} }
@@ -233,10 +267,34 @@ func (p *pdfImporter) documentStructures(ctx context.Context) error {
return ctx.Err() return ctx.Err()
} }
// documentStructure 写出逻辑结构层级、文本属性及已提交的内容引用 // documentStructure 写出逻辑结构及已提交的图元引用,异常结构以类型化标引保留
// 入参: ctx 取消上下文 // 入参: ctx 取消上下文
// 返回: error 解析、未支持的结构属性或提交错误 // 返回: error 解析、未支持的结构属性或提交错误
func (p *pdfImporter) documentStructure(ctx context.Context) error { func (p *pdfImporter) documentStructure(ctx context.Context) error {
if err := ctx.Err(); err != nil {
return err
}
if p.structureBindings.invalid[p.reader] {
return ctx.Err()
}
err := p.projectStructure(ctx)
if err == nil {
return nil
}
if p.warning == nil || !pdfAuxiliaryError(err) {
return err
}
object, readErr := p.pdfCatalogObject("StructTreeRoot")
if readErr != nil {
return readErr
}
return p.recoverPDFObject(ctx, "Structure", object, err)
}
// projectStructure 将有效逻辑结构投影为OFD层级及实际图元引用
// 入参: ctx 取消上下文
// 返回: error 结构、绑定或提交错误
func (p *pdfImporter) projectStructure(ctx context.Context) error {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
} }
@@ -484,7 +542,7 @@ func (v *pdfStructureValues) encode(object pdfgo.Object, depth int) (StructureNo
if err := v.ctx.Err(); err != nil { if err := v.ctx.Err(); err != nil {
return node, err return node, err
} }
if depth > 128 { if depth > 128 && !v.auxiliary {
return node, fmt.Errorf("PDF structure attribute limit exceeded") return node, fmt.Errorf("PDF structure attribute limit exceeded")
} }
switch value := object.(type) { switch value := object.(type) {
@@ -516,8 +574,17 @@ func (v *pdfStructureValues) encode(object pdfgo.Object, depth int) (StructureNo
v.seen[value] = true v.seen[value] = true
resolved, err := v.reader.Resolve(value) resolved, err := v.reader.Resolve(value)
if err != nil { if err != nil {
if v.auxiliary && pdfAuxiliaryError(err) {
node.Attributes = append(node.Attributes, pdfStructureAttribute("Error", err.Error())...)
return node, nil
}
return node, err return node, err
} }
if v.auxiliary {
if dict, ok := resolved.(pdfgo.Dictionary); ok && (dict["Type"] == pdfgo.Name("Page") || dict["Type"] == pdfgo.Name("Pages") || dict["Type"] == pdfgo.Name("Catalog")) {
return node, nil
}
}
child, err := v.encode(resolved, depth+1) child, err := v.encode(resolved, depth+1)
if err != nil { if err != nil {
return node, err return node, err
@@ -548,17 +615,83 @@ func (v *pdfStructureValues) encode(object pdfgo.Object, depth int) (StructureNo
if err != nil { if err != nil {
return node, err return node, err
} }
data, err := value.DecodeContext(v.ctx) data := value.Data
dataName := "RawData"
if !v.auxiliary {
dataName = "DecodedData"
data, err = value.DecodeContext(v.ctx)
}
if err != nil { if err != nil {
return node, err return node, err
} }
node.Children = []StructureNode{dictionary, {Name: "DecodedData", Namespace: pdfStructureNamespace, Attributes: []StructureAttribute{{Name: "Encoding", Value: "base64"}}, Text: base64.StdEncoding.EncodeToString(data)}} node.Children = []StructureNode{dictionary, {Name: dataName, Namespace: pdfStructureNamespace, Attributes: []StructureAttribute{{Name: "Encoding", Value: "base64"}}, Text: base64.StdEncoding.EncodeToString(data)}}
default: default:
return node, fmt.Errorf("invalid PDF structure attribute value") return node, fmt.Errorf("invalid PDF structure attribute value")
} }
return node, v.ctx.Err() return node, v.ctx.Err()
} }
// pdfCatalogObject 获取文档级原始字段,不预先解析字段值
// 入参: name 目录字段名
// 返回: pdfgo.Object 原始字段, error 目录读取错误
func (p *pdfImporter) pdfCatalogObject(name pdfgo.Name) (pdfgo.Object, error) {
value, err := p.reader.Resolve(p.reader.Trailer["Root"])
if err != nil {
return nil, err
}
dict, ok := value.(pdfgo.Dictionary)
if !ok {
return nil, fmt.Errorf("invalid PDF catalog")
}
return dict[name], nil
}
// pdfAuxiliaryError 区分辅助格式错误与必须传递的输入、关闭和取消错误
// 入参: err 待检查错误
// 返回: bool 是否可以保留原文后继续
func pdfAuxiliaryError(err error) bool {
var read *pdfgo.ReadError
return err != nil && !errors.As(err, &read) && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, os.ErrClosed) && !errors.Is(err, errPDFMediaRead)
}
// preservePDFObject 保存辅助对象的原始类型、引用和流数据,不展开页面及目录
// 入参: ctx 取消上下文, name 来源字段, object 原始对象
// 返回: error 读取、编码或提交错误
func (p *pdfImporter) preservePDFObject(ctx context.Context, name string, object pdfgo.Object) error {
values := pdfStructureValues{ctx: ctx, reader: p.reader, seen: make(map[pdfgo.Reference]bool), auxiliary: true}
node, err := values.encode(object, 0)
if err != nil {
return err
}
root := StructureNode{Name: "Auxiliary", Namespace: pdfStructureNamespace, Attributes: pdfStructureAttribute("Name", name), Children: []StructureNode{node}}
if len(values.objects) != 0 {
root.Children = append(root.Children, StructureNode{Name: "Objects", Namespace: pdfStructureNamespace, Children: values.objects})
}
data, err := encodeStructureNode(root)
if err != nil {
return err
}
_, err = p.editor.addStructureXML(ctx, CustomTag{TypeID: pdfAuxiliaryTagType, NameSpace: pdfStructureNamespace}, data)
return err
}
// recoverPDFObject 按导入策略诊断并保留辅助对象,读取和取消错误不恢复
// 入参: ctx 取消上下文, name 来源字段, object 原始对象, cause 辅助错误
// 返回: error 严格模式、读取、取消或提交错误
func (p *pdfImporter) recoverPDFObject(ctx context.Context, name string, object pdfgo.Object, cause error) error {
if err := ctx.Err(); err != nil {
return err
}
if p.warning == nil || !pdfAuxiliaryError(cause) {
return cause
}
if err := p.preservePDFObject(ctx, name, object); err != nil {
return err
}
p.warning(pdfgo.Diagnostic{Message: "PDF " + name + " retained as metadata: " + cause.Error()})
return nil
}
// pdfStructureAttribute 保留XML不可表达的源字段,不替换原始字符 // pdfStructureAttribute 保留XML不可表达的源字段,不替换原始字符
// 入参: name 属性名, value 属性值 // 入参: name 属性名, value 属性值
// 返回: []StructureAttribute 属性及可选的编码声明 // 返回: []StructureAttribute 属性及可选的编码声明
@@ -924,7 +1057,12 @@ func (p *pdfImporter) documentMetadata(ctx context.Context, strict bool) error {
}{{"CreationDate", &info.CreationDate}, {"ModDate", &info.ModDate}} { }{{"CreationDate", &info.CreationDate}, {"ModDate", &info.ModDate}} {
text, err := codecpdf.NormalizeDate(*field.text, strict) text, err := codecpdf.NormalizeDate(*field.text, strict)
if err != nil { if err != nil {
return fmt.Errorf("PDF metadata %s: %w", field.name, err) if strict || !pdfAuxiliaryError(err) {
return fmt.Errorf("PDF metadata %s: %w", field.name, err)
}
p.warning(pdfgo.Diagnostic{Message: "PDF metadata " + field.name + " retained without date projection: " + err.Error()})
*field.text = ""
continue
} }
if text != *field.text { if text != *field.text {
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Message: "PDF metadata " + field.name + ": date normalized"}) p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Message: "PDF metadata " + field.name + ": date normalized"})
@@ -937,22 +1075,51 @@ func (p *pdfImporter) documentMetadata(ctx context.Context, strict bool) error {
} }
stream, err := p.reader.Metadata() stream, err := p.reader.Metadata()
if err != nil { if err != nil {
return err object, readErr := p.pdfCatalogObject("Metadata")
if readErr != nil {
return readErr
}
if err := p.recoverPDFObject(ctx, "Metadata", object, err); err != nil {
return err
}
stream = nil
} }
var packet []byte var packet []byte
var root *StructureNode var root *StructureNode
if stream != nil { if stream != nil {
packet, err = stream.DecodeContext(ctx) packet, err = stream.DecodeContext(ctx)
if err != nil { if err != nil {
return err if err := p.recoverPDFObject(ctx, "Metadata", stream, err); err != nil {
} return err
root, err = parseStructure(ctx, packet, CustomTag{}) }
if err != nil { } else {
return err root, err = parseStructure(ctx, packet, CustomTag{})
if err != nil {
if err := p.recoverPDFObject(ctx, "Metadata", stream, err); err != nil {
return err
}
root = nil
}
} }
}
if root != nil {
xmp, err := pdfMetadataFields(ctx, root) xmp, err := pdfMetadataFields(ctx, root)
if err != nil { if err != nil {
return err if err := p.recoverPDFObject(ctx, "Metadata", stream, err); err != nil {
return err
}
xmp = nil
}
for _, key := range []string{"CreationDate", "ModDate", "MetadataDate"} {
if value, ok := xmp[key]; ok {
if _, _, _, err := codecpdf.MetadataDate(value.text, true); err != nil {
if strict || !pdfAuxiliaryError(err) {
return fmt.Errorf("PDF metadata %s: %w", key, err)
}
p.warning(pdfgo.Diagnostic{Message: "PDF metadata " + key + " retained without date projection: " + err.Error()})
delete(xmp, key)
}
}
} }
newer := false newer := false
if stamp, ok := xmp["MetadataDate"]; ok && stamp.text != "" && info.ModDate != "" { if stamp, ok := xmp["MetadataDate"]; ok && stamp.text != "" && info.ModDate != "" {
@@ -1013,25 +1180,38 @@ func (p *pdfImporter) documentMetadata(ctx context.Context, strict bool) error {
if err != nil { if err != nil {
return err return err
} }
_, err = p.editor.addStructureXML(ctx, CustomTag{TypeID: pdfInformationTagType, NameSpace: pdfInformationNamespace}, data) if _, err = p.editor.addStructureXML(ctx, CustomTag{TypeID: pdfInformationTagType, NameSpace: pdfInformationNamespace}, data); err != nil {
return err return err
}
}
if !strict {
object, err := p.pdfCatalogObject("OutputIntents")
if err != nil {
return err
}
if object != nil {
return p.preservePDFObject(ctx, "OutputIntents", object)
}
} }
return ctx.Err() return ctx.Err()
} }
// informationMetadata 收集已声明字段并保存文本,日期使用校验结果,XML无法表示的字符使用Base64保留 // informationMetadata 收集已声明字段,日期分别保留原文及投影值,XML无法表示的文本使用Base64保留
// 入参: ctx 取消上下文, info 已解码的信息字典 // 入参: ctx 取消上下文, info 已解码的信息字典
// 返回: map[string]pdfMetadataValue 可映射字段, StructureNode 原始字段, error 读取或取消错误 // 返回: map[string]pdfMetadataValue 可映射字段, StructureNode 原始字段, error 读取或取消错误
func (p *pdfImporter) informationMetadata(ctx context.Context, info pdfgo.DocumentInfo) (map[string]pdfMetadataValue, StructureNode, error) { func (p *pdfImporter) informationMetadata(ctx context.Context, info pdfgo.DocumentInfo) (map[string]pdfMetadataValue, StructureNode, error) {
root := StructureNode{Name: "Information", Namespace: pdfInformationNamespace} root := StructureNode{Name: "Information", Namespace: pdfInformationNamespace}
values := make(map[string]pdfMetadataValue) values := make(map[string]pdfMetadataValue)
object, err := p.reader.Resolve(p.reader.Trailer["Info"]) object, err := p.reader.Resolve(p.reader.Trailer["Info"])
if err != nil || object == nil { if err != nil {
return values, root, err return values, root, p.recoverPDFObject(ctx, "Information", p.reader.Trailer["Info"], err)
}
if object == nil {
return values, root, nil
} }
dict, ok := object.(pdfgo.Dictionary) dict, ok := object.(pdfgo.Dictionary)
if !ok { if !ok {
return nil, root, fmt.Errorf("invalid PDF information dictionary") return values, root, p.recoverPDFObject(ctx, "Information", object, fmt.Errorf("invalid PDF information dictionary"))
} }
known := map[pdfgo.Name]string{ known := map[pdfgo.Name]string{
"Title": info.Title, "Author": info.Author, "Subject": info.Subject, "Keywords": info.Keywords, "Title": info.Title, "Author": info.Author, "Subject": info.Subject, "Keywords": info.Keywords,
@@ -1044,12 +1224,40 @@ func (p *pdfImporter) informationMetadata(ctx context.Context, info pdfgo.Docume
} }
value, err := p.reader.Resolve(dict[key]) value, err := p.reader.Resolve(dict[key])
if err != nil { if err != nil {
return nil, root, err if err := p.recoverPDFObject(ctx, "Information."+string(key), dict[key], err); err != nil {
return nil, root, err
}
continue
} }
if value == nil { if value == nil {
continue continue
} }
if text, ok := value.(pdfgo.String); ok {
if _, err := pdfgo.DecodeTextString(text); err != nil {
if err := p.recoverPDFObject(ctx, "Information."+string(key), dict[key], err); err != nil {
return nil, root, err
}
if key == "CreationDate" || key == "ModDate" {
values[string(key)] = pdfMetadataValue{}
}
continue
}
}
text, standard := known[key] text, standard := known[key]
_, stringValue := value.(pdfgo.String)
valid := stringValue
if key == "Trapped" {
valid = value == pdfgo.Name("True") || value == pdfgo.Name("False") || value == pdfgo.Name("Unknown")
}
if !valid {
if err := p.preservePDFObject(ctx, "Information."+string(key), dict[key]); err != nil {
return nil, root, err
}
if key == "CreationDate" || key == "ModDate" {
values[string(key)] = pdfMetadataValue{}
}
continue
}
if !standard { if !standard {
text = info.Custom[key] text = info.Custom[key]
} else { } else {
@@ -1069,6 +1277,20 @@ func (p *pdfImporter) informationMetadata(ctx context.Context, info pdfgo.Docume
text = base64.StdEncoding.EncodeToString([]byte(text)) text = base64.StdEncoding.EncodeToString([]byte(text))
field.Attributes = append(field.Attributes, StructureAttribute{Name: "Encoding", Value: "base64"}) field.Attributes = append(field.Attributes, StructureAttribute{Name: "Encoding", Value: "base64"})
} }
if key == "CreationDate" || key == "ModDate" {
original, err := pdfgo.DecodeTextString(value.(pdfgo.String))
if err != nil {
if err := p.recoverPDFObject(ctx, "Information."+string(key), dict[key], err); err != nil {
return nil, root, err
}
continue
}
text = original
if !structureXMLText(text) {
text = base64.StdEncoding.EncodeToString([]byte(text))
field.Attributes = append(field.Attributes, StructureAttribute{Name: "Encoding", Value: "base64"})
}
}
field.Text = text field.Text = text
root.Children = append(root.Children, field) root.Children = append(root.Children, field)
} }
+21 -2
View File
@@ -16,7 +16,6 @@ package ofdgo
import ( import (
"fmt" "fmt"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
"image" "image"
"image/color" "image/color"
"image/draw" "image/draw"
@@ -24,6 +23,7 @@ import (
"github.com/tdewolff/canvas" "github.com/tdewolff/canvas"
"github.com/tdewolff/canvas/renderers/rasterizer" "github.com/tdewolff/canvas/renderers/rasterizer"
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
) )
@@ -176,7 +176,26 @@ func (b CanvasBackend) renderImage(page *RasterPage, img *image.RGBA) (image.Ima
style.Dashes[i] = dash / options.Width style.Dashes[i] = dash / options.Width
} }
} }
if slices.Contains(options.Dashes, 0) { var pointPath GeometryPath
pointStroke := false
if options.Cap == "Round" {
pointPath, pointStroke = canvasPointPath(geometryFromRaster(cmd.Path))
}
if options.Cap == "Round" && (p.Empty() || pointStroke) {
path, err := pointPath.Transform(MatrixFromValues([6]float64(m)))
if err != nil {
return nil, err
}
path, err = path.Stroke(options)
if err != nil {
return nil, err
}
p, err = canvasObjectPath(path)
if err != nil {
return nil, err
}
style = canvas.Style{Fill: paint, FillRule: canvas.NonZero}
} else if slices.Contains(options.Dashes, 0) {
var err error var err error
p, err = canvasStrokePath(b, p, options) p, err = canvasStrokePath(b, p, options)
if err != nil { if err != nil {
+57 -2
View File
@@ -196,10 +196,18 @@ func (CanvasBackend) Stroke(path GeometryPath, options StrokeOptions) (result Ge
if slices.Contains(options.Dashes, 0) { if slices.Contains(options.Dashes, 0) {
return path.Stroke(options) return path.Stroke(options)
} }
if options.Cap == "Round" {
if pointPath, found := canvasPointPath(path); found {
return pointPath.Stroke(options)
}
}
p, err := geometryToCanvasPath(&path) p, err := geometryToCanvasPath(&path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if p.Empty() && options.Cap == "Round" {
return path.Stroke(options)
}
tolerance := options.Tolerance tolerance := options.Tolerance
if tolerance == 0 { if tolerance == 0 {
tolerance = canvas.Tolerance tolerance = canvas.Tolerance
@@ -222,6 +230,45 @@ func (CanvasBackend) Stroke(path GeometryPath, options StrokeOptions) (result Ge
return *geometryFromCanvasPath(p), nil return *geometryFromCanvasPath(p), nil
} }
// canvasPointPath 保留普通闭合轮廓,将闭合单点改为同位置绘制段
// 入参: path 原始几何路径
// 返回: GeometryPath 只在需要时复制的路径, bool 是否含实际绘制的单点子路径
func canvasPointPath(path GeometryPath) (GeometryPath, bool) {
result := path
var start Point
point, drawn, active, found, copied := false, false, false, false, false
for i, segment := range path {
switch segment.Verb {
case GeometryMove:
found = found || point && drawn
start = segment.End
point, drawn, active = true, false, true
case GeometryClose:
if point && active {
if !copied {
result = slices.Clone(path)
copied = true
}
result[i] = GeometrySegment{Verb: GeometryLine, End: start}
found = true
}
point, drawn, active = true, false, false
case GeometryLine:
point = point && segment.End == start
drawn, active = true, true
case GeometryQuad:
point = point && segment.End == start && segment.Control1 == start
drawn, active = true, true
case GeometryCubic:
point = point && segment.End == start && segment.Control1 == start && segment.Control2 == start
drawn, active = true, true
default:
point, drawn, active = false, true, true
}
}
return result, found || point && drawn
}
// Clip 解析对象裁剪并与父裁剪相交,保留未裁剪与完全裁去的区别 // Clip 解析对象裁剪并与父裁剪相交,保留未裁剪与完全裁去的区别
// 入参: r 渲染器, clips OFD裁剪, matrix 裁剪坐标到页面的变换, parent 父裁剪 // 入参: r 渲染器, clips OFD裁剪, matrix 裁剪坐标到页面的变换, parent 父裁剪
// 返回: *GeometryPath 页面裁剪, error 无效父路径错误 // 返回: *GeometryPath 页面裁剪, error 无效父路径错误
@@ -389,7 +436,11 @@ func geometryToCanvasPath(path *GeometryPath) (*canvas.Path, error) {
case GeometryArc: case GeometryArc:
p.ArcTo(s.RadiusX, s.RadiusY, -s.Rotation, s.Large, !s.Sweep, s.End.X, -s.End.Y) p.ArcTo(s.RadiusX, s.RadiusY, -s.Rotation, s.Large, !s.Sweep, s.End.X, -s.End.Y)
case GeometryClose: case GeometryClose:
p.Close() if data := p.Data(); len(data) != 0 && data[len(data)-1] == canvas.MoveToCmd {
p = canvas.NewPathFromData(append(data, canvas.CloseCmd, data[len(data)-3], data[len(data)-2], canvas.CloseCmd))
} else {
p.Close()
}
default: default:
return nil, fmt.Errorf("unsupported geometry command %d", s.Verb) return nil, fmt.Errorf("unsupported geometry command %d", s.Verb)
} }
@@ -419,7 +470,11 @@ func canvasObjectPath(path GeometryPath) (*canvas.Path, error) {
data = p.Data() data = p.Data()
case GeometryClose: case GeometryClose:
p := canvas.NewPathFromData(data) p := canvas.NewPathFromData(data)
p.Close() if len(data) != 0 && data[len(data)-1] == canvas.MoveToCmd {
p = canvas.NewPathFromData(append(data, canvas.CloseCmd, data[len(data)-3], data[len(data)-2], canvas.CloseCmd))
} else {
p.Close()
}
data = p.Data() data = p.Data()
default: default:
return nil, fmt.Errorf("unsupported geometry command %d", s.Verb) return nil, fmt.Errorf("unsupported geometry command %d", s.Verb)