mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:31:12 +08:00
1 parent
580592bbf8
commit
b4d566019d
11 files changed
+793
-171
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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{}, ¤tPage)
|
actions, err := p.eventActions(ctx, object, "DO", pdfgo.Annotation{}, ¤tPage, 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{}, ¤tPage, 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, ¤tPage)
|
|
||||||
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
@@ -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
@@ -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
@@ -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, ¤tPage)
|
||||||
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
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in new issue
Block a user