mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:59:27 +08:00
1418 lines
47 KiB
Go
1418 lines
47 KiB
Go
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package ofdgo
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"math"
|
|
"os"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
|
"github.com/xiaoqidun/pdfgo"
|
|
)
|
|
|
|
// PDF逻辑结构标引使用独立命名空间,图元引用沿用ObjectRef语义
|
|
const (
|
|
pdfStructureTagType = "OFDGo.PDFStructure"
|
|
pdfStructureNamespace = "https://ofdgo.aite.me/ns/pdf/structure/1"
|
|
)
|
|
|
|
// PDF元数据使用的RDF、Dublin Core及Adobe命名空间
|
|
const (
|
|
pdfMetadataRDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
pdfMetadataDC = "http://purl.org/dc/elements/1.1/"
|
|
pdfMetadataXMP = "http://ns.adobe.com/xap/1.0/"
|
|
pdfMetadataPDF = "http://ns.adobe.com/pdf/1.3/"
|
|
pdfMetadataXML = "adobe:ns:meta/"
|
|
)
|
|
|
|
// PDF来源元数据的标引定义
|
|
const (
|
|
pdfMetadataTagType = "OFDGo.PDFMetadata"
|
|
pdfInformationTagType = "OFDGo.PDFInformation"
|
|
pdfInformationNamespace = "https://ofdgo.aite.me/ns/pdf/information/1"
|
|
pdfAuxiliaryTagType = "OFDGo.PDFAuxiliary"
|
|
)
|
|
|
|
// pdfStructureBindings 保存来源区段与最终页面图元的关联,不记录临时渲染对象
|
|
type pdfStructureBindings struct {
|
|
parents map[*pdfgo.Reader]map[[2]int64]pdfgo.Reference
|
|
prepared map[*pdfgo.Reader]bool
|
|
invalid map[*pdfgo.Reader]bool
|
|
readers []*pdfgo.Reader
|
|
marked map[pdfStructureContent]bool
|
|
sequences map[*pdfgo.ContentSequence][]StructureReference
|
|
order []*pdfgo.ContentSequence
|
|
contents []*pdfgo.ContentSequence
|
|
objects map[pdfStructureObject][]StructureReference
|
|
}
|
|
|
|
// pdfStructureObject 隔离不同文件及页面的整对象引用
|
|
type pdfStructureObject struct {
|
|
reader *pdfgo.Reader
|
|
page pdfgo.Reference
|
|
object pdfgo.Reference
|
|
}
|
|
|
|
// pdfStructureObjectBinding 保存尚未提交的注解外观引用
|
|
type pdfStructureObjectBinding struct {
|
|
source pdfStructureObject
|
|
ids []string
|
|
content []pdfStructureBinding
|
|
}
|
|
|
|
// pdfStructureContent 隔离不同文件、页面及内容流中的标记编号
|
|
type pdfStructureContent struct {
|
|
reader *pdfgo.Reader
|
|
page pdfgo.Reference
|
|
stream *pdfgo.Stream
|
|
mcid int64
|
|
}
|
|
|
|
// pdfStructureReferences 保存内容引用与已提交图元的索引
|
|
type pdfStructureReferences map[pdfStructureContent][]StructureReference
|
|
|
|
// pdfStructureReferenceSet 按首次出现顺序归并页内图元,单页时不建立页索引
|
|
type pdfStructureReferenceSet struct {
|
|
references []StructureReference
|
|
pages map[string]int
|
|
seen map[[2]string]bool
|
|
}
|
|
|
|
// pdfStructureBinding 保存一个来源区段对应的连续对象位置
|
|
type pdfStructureBinding struct {
|
|
sequence *pdfgo.ContentSequence
|
|
start int
|
|
end int
|
|
}
|
|
|
|
// pdfStructureValues 保存属性值类型及间接对象共享关系
|
|
type pdfStructureValues struct {
|
|
ctx context.Context
|
|
reader *pdfgo.Reader
|
|
seen map[pdfgo.Reference]bool
|
|
elements map[pdfgo.Reference]bool
|
|
objects []StructureNode
|
|
auxiliary bool
|
|
}
|
|
|
|
// pdfMetadataValue 保留字段是否声明及其来源,空文本不作为缺省值
|
|
type pdfMetadataValue struct {
|
|
text string
|
|
xmp bool
|
|
}
|
|
|
|
// prepareStructure 校验结构内容关联,无法映射时保留原始结构
|
|
// 入参: ctx 取消上下文
|
|
// 返回: 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 {
|
|
return err
|
|
}
|
|
if p.structureBindings.prepared[p.reader] {
|
|
return nil
|
|
}
|
|
tree, err := p.reader.StructureTree()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tree == nil {
|
|
if p.structureBindings.prepared == nil {
|
|
p.structureBindings.prepared = make(map[*pdfgo.Reader]bool)
|
|
}
|
|
p.structureBindings.prepared[p.reader] = true
|
|
return ctx.Err()
|
|
}
|
|
parents, err := p.structureParents(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
marked := make(map[pdfStructureContent]bool)
|
|
if err := p.reader.WalkStructure(ctx, func(_ int, item pdfgo.StructureItem) error {
|
|
if item.Type != "MCR" && item.Type != "OBJR" {
|
|
return nil
|
|
}
|
|
content, err := p.reader.StructureContent(item)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mcid := int64(-1)
|
|
if item.Type == "MCR" {
|
|
mcid = item.MCID
|
|
marked[pdfStructureContent{reader: p.reader, page: item.Page, stream: content.Stream, mcid: item.MCID}] = true
|
|
}
|
|
if parent, ok := parents[[2]int64{content.ParentKey, mcid}]; !ok || parent != item.Parent {
|
|
return fmt.Errorf("PDF structure content parent mismatch")
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
b := p.structureBindings
|
|
if b.parents == nil {
|
|
b.parents = make(map[*pdfgo.Reader]map[[2]int64]pdfgo.Reference)
|
|
}
|
|
if b.prepared == nil {
|
|
b.prepared = make(map[*pdfgo.Reader]bool)
|
|
}
|
|
if b.marked == nil {
|
|
b.marked = make(map[pdfStructureContent]bool)
|
|
}
|
|
b.parents[p.reader], b.prepared[p.reader] = parents, true
|
|
maps.Copy(b.marked, marked)
|
|
b.readers = append(b.readers, p.reader)
|
|
return nil
|
|
}
|
|
|
|
// prepareSourceStructure 使用来源阅读器校验引用文档,共享最终图元索引
|
|
// 入参: ctx 取消上下文, reader 来源阅读器
|
|
// 返回: error 结构或取消错误
|
|
func (p *pdfImporter) prepareSourceStructure(ctx context.Context, reader *pdfgo.Reader) error {
|
|
if p.structureBindings.prepared[reader] {
|
|
return ctx.Err()
|
|
}
|
|
source := *p
|
|
source.reader = reader
|
|
return source.prepareStructure(ctx)
|
|
}
|
|
|
|
// structureParents 复用预检父树索引,未预检时读取当前文档
|
|
// 入参: ctx 取消上下文
|
|
// 返回: map[[2]int64]pdfgo.Reference 父树键及区段编号对应的元素, error 解析或取消错误
|
|
func (p *pdfImporter) structureParents(ctx context.Context) (map[[2]int64]pdfgo.Reference, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if parents := p.structureBindings.parents[p.reader]; parents != nil {
|
|
return parents, nil
|
|
}
|
|
parents := make(map[[2]int64]pdfgo.Reference)
|
|
if err := p.reader.WalkStructureParents(ctx, func(parent pdfgo.StructureParent) error {
|
|
parents[[2]int64{parent.Key, parent.MCID}] = parent.Element
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
return parents, nil
|
|
}
|
|
|
|
// documentStructures 分别保存主文档与引用文档的结构,不合并来源对象编号
|
|
// 入参: ctx 取消上下文
|
|
// 返回: error 结构或提交错误
|
|
func (p *pdfImporter) documentStructures(ctx context.Context) error {
|
|
for _, sequence := range p.structureBindings.order {
|
|
if err := p.prepareSourceStructure(ctx, sequence.Reader); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, reader := range p.structureBindings.readers {
|
|
source := *p
|
|
source.reader = reader
|
|
if err := source.documentStructure(ctx); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return ctx.Err()
|
|
}
|
|
|
|
// documentStructure 写出逻辑结构及已提交的图元引用,异常结构以类型化标引保留
|
|
// 入参: ctx 取消上下文
|
|
// 返回: 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 {
|
|
return err
|
|
}
|
|
tree, err := p.reader.StructureTree()
|
|
if err != nil || tree == nil {
|
|
return err
|
|
}
|
|
refs, err := p.structureBindings.contentReferences(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
objects, err := p.structureBindings.objectReferences(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
parents, err := p.structureParents(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root := StructureNode{Name: "Structure", Namespace: pdfStructureNamespace}
|
|
values := pdfStructureValues{ctx: ctx, reader: p.reader, seen: make(map[pdfgo.Reference]bool), elements: make(map[pdfgo.Reference]bool)}
|
|
if tree.Reference != (pdfgo.Reference{}) {
|
|
values.elements[tree.Reference] = true
|
|
root.Attributes = pdfStructureSource(tree.Reference)
|
|
}
|
|
if err := p.reader.WalkStructure(ctx, func(_ int, item pdfgo.StructureItem) error {
|
|
if item.Type == "StructElem" && item.Reference != (pdfgo.Reference{}) {
|
|
values.elements[item.Reference] = true
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
properties, err := p.structureBindings.contentProperties(&values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, key := range []pdfgo.Name{"RoleMap", "ClassMap"} {
|
|
if value := tree.Dictionary[key]; value != nil {
|
|
node, err := values.encode(value, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root.Children = append(root.Children, StructureNode{Name: string(key), Namespace: pdfStructureNamespace, Children: []StructureNode{node}})
|
|
}
|
|
}
|
|
extensions, err := values.extensions(tree.Dictionary, []pdfgo.Name{"Type", "K", "ParentTree", "ParentTreeNextKey", "RoleMap", "ClassMap", "IDTree"})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root.Children = append(root.Children, extensions...)
|
|
stack := []*StructureNode{&root}
|
|
ids := make(map[string]pdfgo.Reference)
|
|
err = p.reader.WalkStructure(ctx, func(depth int, item pdfgo.StructureItem) error {
|
|
if depth >= len(stack) {
|
|
return fmt.Errorf("invalid PDF structure depth")
|
|
}
|
|
stack = stack[:depth+1]
|
|
parent := stack[depth]
|
|
node := StructureNode{Name: string(item.Type), Namespace: pdfStructureNamespace}
|
|
var content pdfgo.StructureContent
|
|
if item.Type == "MCR" || item.Type == "OBJR" {
|
|
content, err = p.reader.StructureContent(item)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mcid := int64(-1)
|
|
if item.Type == "MCR" {
|
|
mcid = item.MCID
|
|
}
|
|
if parent, ok := parents[[2]int64{content.ParentKey, mcid}]; !ok || parent != item.Parent {
|
|
return fmt.Errorf("PDF structure content parent mismatch")
|
|
}
|
|
}
|
|
switch item.Type {
|
|
case "StructElem":
|
|
extensions, err := values.extensions(item.Dictionary, []pdfgo.Name{"Type", "S", "P", "K", "Pg", "R", "ID", "T", "Lang", "Alt", "ActualText", "E", "A", "C"})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
node.Children = append(node.Children, extensions...)
|
|
if item.Reference != (pdfgo.Reference{}) {
|
|
node.Attributes = pdfStructureSource(item.Reference)
|
|
}
|
|
if _, err := p.reader.StructureAttributes(ctx, item); err != nil {
|
|
return err
|
|
}
|
|
for _, key := range []pdfgo.Name{"A", "C"} {
|
|
if value := item.Dictionary[key]; value != nil {
|
|
child, err := values.encode(value, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
node.Children = append(node.Children, StructureNode{Name: string(key), Namespace: pdfStructureNamespace, Children: []StructureNode{child}})
|
|
}
|
|
}
|
|
node.Attributes = append(node.Attributes, pdfStructureAttribute("Tag", string(item.Tag))...)
|
|
node.Attributes = append(node.Attributes, StructureAttribute{Name: "Revision", Value: strconv.FormatInt(item.Revision, 10)})
|
|
for _, field := range []pdfgo.Name{"ID", "T", "Lang", "Alt", "ActualText", "E"} {
|
|
value, err := p.reader.Resolve(item.Dictionary[field])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if value == nil {
|
|
continue
|
|
}
|
|
text, ok := value.(pdfgo.String)
|
|
if !ok {
|
|
return fmt.Errorf("invalid PDF structure text: %s", field)
|
|
}
|
|
decoded := string(text)
|
|
if field == "ID" {
|
|
if _, exists := ids[decoded]; exists {
|
|
return fmt.Errorf("duplicate PDF structure identifier")
|
|
}
|
|
ids[decoded] = item.Reference
|
|
} else {
|
|
decoded, err = pdfgo.DecodeTextString(text)
|
|
if err != nil {
|
|
original, err := values.encode(text, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
node.Children = append(node.Children, StructureNode{Name: string(field), Namespace: pdfStructureNamespace, Children: []StructureNode{original}})
|
|
continue
|
|
}
|
|
}
|
|
node.Attributes = append(node.Attributes, pdfStructureAttribute(string(field), decoded)...)
|
|
}
|
|
case "MCR":
|
|
extensions, err := values.extensions(item.Dictionary, []pdfgo.Name{"Type", "Pg", "Stm", "StmOwn", "MCID"})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
node.Children = append(node.Children, extensions...)
|
|
targets, err := refs.targets(ctx, p.reader, item)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
node.Attributes = []StructureAttribute{{Name: "MCID", Value: strconv.FormatInt(item.MCID, 10)}}
|
|
key := pdfStructureContent{reader: p.reader, page: item.Page, stream: content.Stream, mcid: item.MCID}
|
|
if property, ok := properties[key]; ok {
|
|
node.Children = append(node.Children, property)
|
|
}
|
|
for _, target := range targets {
|
|
node.Children = append(node.Children, StructureNode{Name: "ObjectRef", Namespace: pdfStructureNamespace, Attributes: []StructureAttribute{{Name: "PageRef", Value: target.PageID}}, Text: strings.Join(target.ObjectIDs, ",")})
|
|
}
|
|
case "OBJR":
|
|
extensions, err := values.extensions(item.Dictionary, []pdfgo.Name{"Type", "Pg", "Obj"})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
node.Children = append(node.Children, extensions...)
|
|
targets, exists := objects[pdfStructureObject{reader: p.reader, page: item.Page, object: item.Target}]
|
|
if !exists {
|
|
kind, err := p.reader.Resolve(content.Dictionary["Type"])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
subtype, err := p.reader.Resolve(content.Dictionary["Subtype"])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name, _ := subtype.(pdfgo.Name)
|
|
if content.Stream != nil {
|
|
if kind != nil && kind != pdfgo.Name("XObject") || name != "Form" && name != "Image" {
|
|
return fmt.Errorf("unsupported PDF structure object target")
|
|
}
|
|
} else if kind != nil && kind != pdfgo.Name("Annot") || !(pdfgo.Annotation{Subtype: name}).IsStandard() {
|
|
return fmt.Errorf("unsupported PDF structure object target")
|
|
}
|
|
}
|
|
for _, target := range targets {
|
|
if len(target.ObjectIDs) != 0 {
|
|
node.Children = append(node.Children, StructureNode{Name: "ObjectRef", Namespace: pdfStructureNamespace, Attributes: []StructureAttribute{{Name: "PageRef", Value: target.PageID}}, Text: strings.Join(target.ObjectIDs, ",")})
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported PDF structure item: %s", item.Type)
|
|
}
|
|
parent.Children = append(parent.Children, node)
|
|
stack = append(stack, &parent.Children[len(parent.Children)-1])
|
|
return ctx.Err()
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := p.reader.WalkStructureIDs(ctx, func(id string, item pdfgo.StructureItem) error {
|
|
ref, exists := ids[id]
|
|
if !exists || ref != item.Reference {
|
|
return fmt.Errorf("PDF structure identifier target is outside the hierarchy")
|
|
}
|
|
delete(ids, id)
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
if len(ids) != 0 {
|
|
return fmt.Errorf("missing PDF structure identifier index")
|
|
}
|
|
if len(values.objects) != 0 {
|
|
root.Children = append(root.Children, StructureNode{Name: "SourceObjects", Namespace: pdfStructureNamespace, Children: values.objects})
|
|
}
|
|
data, err := encodeStructureNodeContext(ctx, root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = p.editor.addStructureXML(ctx, CustomTag{TypeID: pdfStructureTagType, NameSpace: pdfStructureNamespace}, data)
|
|
return err
|
|
}
|
|
|
|
// pdfStructureSource 保存源结构标识,不与OFD图元标识混用
|
|
// 入参: ref 来源引用
|
|
// 返回: []StructureAttribute 对象编号及代数
|
|
func pdfStructureSource(ref pdfgo.Reference) []StructureAttribute {
|
|
return []StructureAttribute{{Name: "SourceNumber", Value: strconv.FormatInt(ref.Number, 10)}, {Name: "SourceGeneration", Value: strconv.FormatInt(ref.Generation, 10)}}
|
|
}
|
|
|
|
// extensions 保留未投影字段,字段名不作为XML元素名称
|
|
// 入参: dictionary 来源字典, known 已投影字段
|
|
// 返回: []StructureNode 扩展字段, error 读取或取消错误
|
|
func (v *pdfStructureValues) extensions(dictionary pdfgo.Dictionary, known []pdfgo.Name) ([]StructureNode, error) {
|
|
var result []StructureNode
|
|
for _, key := range slices.Sorted(maps.Keys(dictionary)) {
|
|
if err := v.ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if slices.Contains(known, key) {
|
|
continue
|
|
}
|
|
value, err := v.encode(dictionary[key], 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, StructureNode{Name: "Extension", Namespace: pdfStructureNamespace, Attributes: pdfStructureAttribute("Key", string(key)), Children: []StructureNode{value}})
|
|
}
|
|
return result, v.ctx.Err()
|
|
}
|
|
|
|
// encode 按原始类型保存属性对象,间接对象仅展开一次,字符串保留原始字节
|
|
// 入参: object 属性对象, depth 当前值层级
|
|
// 返回: StructureNode 类型化属性值, error 读取、层级或取消错误
|
|
func (v *pdfStructureValues) encode(object pdfgo.Object, depth int) (StructureNode, error) {
|
|
node := StructureNode{Namespace: pdfStructureNamespace}
|
|
if err := v.ctx.Err(); err != nil {
|
|
return node, err
|
|
}
|
|
if depth > 128 && !v.auxiliary {
|
|
return node, fmt.Errorf("PDF structure attribute limit exceeded")
|
|
}
|
|
switch value := object.(type) {
|
|
case nil:
|
|
node.Name = "Null"
|
|
case pdfgo.Boolean:
|
|
node.Name, node.Text = "Boolean", strconv.FormatBool(bool(value))
|
|
case pdfgo.Integer:
|
|
node.Name, node.Text = "Integer", strconv.FormatInt(int64(value), 10)
|
|
case pdfgo.Real:
|
|
if math.IsNaN(float64(value)) || math.IsInf(float64(value), 0) {
|
|
return node, fmt.Errorf("invalid PDF structure real")
|
|
}
|
|
node.Name, node.Text = "Real", strconv.FormatFloat(float64(value), 'g', -1, 64)
|
|
case pdfgo.Name:
|
|
node.Name = "Name"
|
|
node.Attributes = pdfStructureAttribute("Value", string(value))
|
|
case pdfgo.String:
|
|
node.Name, node.Text = "String", base64.StdEncoding.EncodeToString(value)
|
|
node.Attributes = []StructureAttribute{{Name: "Encoding", Value: "base64"}}
|
|
case pdfgo.Reference:
|
|
node.Name = "Reference"
|
|
node.Attributes = []StructureAttribute{{Name: "Number", Value: strconv.FormatInt(value.Number, 10)}, {Name: "Generation", Value: strconv.FormatInt(value.Generation, 10)}}
|
|
if v.elements[value] {
|
|
node.Name = "ElementReference"
|
|
return node, v.ctx.Err()
|
|
}
|
|
if !v.seen[value] {
|
|
v.seen[value] = true
|
|
resolved, err := v.reader.Resolve(value)
|
|
if err != nil {
|
|
if v.auxiliary && pdfAuxiliaryError(err) {
|
|
node.Attributes = append(node.Attributes, pdfStructureAttribute("Error", err.Error())...)
|
|
return node, nil
|
|
}
|
|
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)
|
|
if err != nil {
|
|
return node, err
|
|
}
|
|
v.objects = append(v.objects, StructureNode{Name: "Indirect", Namespace: pdfStructureNamespace, Attributes: slices.Clone(node.Attributes), Children: []StructureNode{child}})
|
|
}
|
|
case pdfgo.Array:
|
|
node.Name = "Array"
|
|
for _, object := range value {
|
|
child, err := v.encode(object, depth+1)
|
|
if err != nil {
|
|
return node, err
|
|
}
|
|
node.Children = append(node.Children, child)
|
|
}
|
|
case pdfgo.Dictionary:
|
|
node.Name = "Dictionary"
|
|
for _, key := range slices.Sorted(maps.Keys(value)) {
|
|
child, err := v.encode(value[key], depth+1)
|
|
if err != nil {
|
|
return node, err
|
|
}
|
|
node.Children = append(node.Children, StructureNode{Name: "Entry", Namespace: pdfStructureNamespace, Attributes: pdfStructureAttribute("Key", string(key)), Children: []StructureNode{child}})
|
|
}
|
|
case *pdfgo.Stream:
|
|
node.Name = "Stream"
|
|
dictionary, err := v.encode(value.Dictionary, depth+1)
|
|
if err != nil {
|
|
return node, err
|
|
}
|
|
data := value.Data
|
|
dataName := "RawData"
|
|
if !v.auxiliary {
|
|
dataName = "DecodedData"
|
|
data, err = value.DecodeContext(v.ctx)
|
|
}
|
|
if err != nil {
|
|
return node, err
|
|
}
|
|
node.Children = []StructureNode{dictionary, {Name: dataName, Namespace: pdfStructureNamespace, Attributes: []StructureAttribute{{Name: "Encoding", Value: "base64"}}, Text: base64.StdEncoding.EncodeToString(data)}}
|
|
default:
|
|
return node, fmt.Errorf("invalid PDF structure attribute value")
|
|
}
|
|
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不可表达的源字段,不替换原始字符
|
|
// 入参: name 属性名, value 属性值
|
|
// 返回: []StructureAttribute 属性及可选的编码声明
|
|
func pdfStructureAttribute(name, value string) []StructureAttribute {
|
|
if structureXMLText(value) {
|
|
return []StructureAttribute{{Name: name, Value: value}}
|
|
}
|
|
return []StructureAttribute{{Name: name, Value: base64.StdEncoding.EncodeToString([]byte(value))}, {Name: name + "Encoding", Value: "base64"}}
|
|
}
|
|
|
|
// targets 解析结构树中的内容流引用,不将未绘制内容绑定到其他同号区段
|
|
// 入参: ctx 取消上下文, reader 来源阅读器, item 标记内容引用
|
|
// 返回: []StructureReference 独立的目标引用, error 来源或取消错误
|
|
func (refs pdfStructureReferences) targets(ctx context.Context, reader *pdfgo.Reader, item pdfgo.StructureItem) ([]StructureReference, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if item.Type != "MCR" || item.MCID < 0 || item.Page == (pdfgo.Reference{}) {
|
|
return nil, fmt.Errorf("invalid PDF marked content reference")
|
|
}
|
|
key := pdfStructureContent{reader: reader, page: item.Page, mcid: item.MCID}
|
|
if item.Stream != (pdfgo.Reference{}) {
|
|
value, err := reader.Resolve(item.Stream)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var ok bool
|
|
key.stream, ok = value.(*pdfgo.Stream)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid PDF marked content stream")
|
|
}
|
|
}
|
|
var result []StructureReference
|
|
for _, ref := range refs[key] {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
copy := StructureReference{PageID: ref.PageID}
|
|
for _, id := range ref.ObjectIDs {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
copy.ObjectIDs = append(copy.ObjectIDs, id)
|
|
}
|
|
result = append(result, copy)
|
|
}
|
|
return result, ctx.Err()
|
|
}
|
|
|
|
// objectReferences 汇总注解及XObject调用对应的已提交图元
|
|
// 入参: ctx 取消上下文
|
|
// 返回: map[pdfStructureObject][]StructureReference 对象引用索引, error 来源或取消错误
|
|
func (b *pdfStructureBindings) objectReferences(ctx context.Context) (map[pdfStructureObject][]StructureReference, error) {
|
|
result := make(map[pdfStructureObject][]StructureReference)
|
|
sets := make(map[pdfStructureObject]*pdfStructureReferenceSet)
|
|
appendReferences := func(key pdfStructureObject, sources []StructureReference) error {
|
|
set := sets[key]
|
|
if set == nil {
|
|
set = &pdfStructureReferenceSet{}
|
|
sets[key] = set
|
|
}
|
|
if err := set.append(ctx, sources); err != nil {
|
|
return err
|
|
}
|
|
result[key] = set.references
|
|
return nil
|
|
}
|
|
for key, refs := range b.objects {
|
|
if err := appendReferences(key, refs); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
for _, sequence := range b.order {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if sequence.Object == (pdfgo.Reference{}) {
|
|
continue
|
|
}
|
|
if sequence.Reader == nil || sequence.Page == nil {
|
|
return nil, fmt.Errorf("missing PDF object source")
|
|
}
|
|
key := pdfStructureObject{reader: sequence.Reader, page: sequence.Page.Reference, object: sequence.Object}
|
|
if err := appendReferences(key, b.sequences[sequence]); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return result, ctx.Err()
|
|
}
|
|
|
|
// observe 保存已读取的标记区段,不登记尚未提交的图元
|
|
// 入参: sequence 来源区段
|
|
func (b *pdfStructureBindings) observe(sequence *pdfgo.ContentSequence) {
|
|
if sequence != nil && sequence.Properties["MCID"] != nil {
|
|
b.contents = append(b.contents, sequence)
|
|
}
|
|
}
|
|
|
|
// pdfStructureContentKey 解析来源区段中的MCID,空值表示非结构标记
|
|
// 入参: sequence 来源区段
|
|
// 返回: pdfStructureContent 来源键, bool 是否具有MCID, error 属性或来源错误
|
|
func pdfStructureContentKey(sequence *pdfgo.ContentSequence) (pdfStructureContent, bool, error) {
|
|
var key pdfStructureContent
|
|
value := sequence.Properties["MCID"]
|
|
if value == nil {
|
|
return key, false, nil
|
|
}
|
|
if sequence.Reader == nil || sequence.Page == nil {
|
|
return key, false, fmt.Errorf("missing PDF marked content source")
|
|
}
|
|
value, err := sequence.Reader.Resolve(value)
|
|
if err != nil || value == nil {
|
|
return key, false, err
|
|
}
|
|
mcid, ok := value.(pdfgo.Integer)
|
|
if !ok || mcid < 0 {
|
|
return key, false, fmt.Errorf("invalid PDF marked content ID")
|
|
}
|
|
return pdfStructureContent{reader: sequence.Reader, page: sequence.Page.Reference, stream: sequence.Stream, mcid: int64(mcid)}, true, nil
|
|
}
|
|
|
|
// contentProperties 保存区段标签及原始属性,重复调用须具有相同内容
|
|
// 入参: values 类型化属性编码器
|
|
// 返回: map[pdfStructureContent]StructureNode 属性索引, error 属性或取消错误
|
|
func (b *pdfStructureBindings) contentProperties(values *pdfStructureValues) (map[pdfStructureContent]StructureNode, error) {
|
|
result := make(map[pdfStructureContent]StructureNode)
|
|
seen := make(map[*pdfgo.ContentSequence]bool)
|
|
for _, sequences := range [][]*pdfgo.ContentSequence{b.contents, b.order} {
|
|
for _, sequence := range sequences {
|
|
if err := values.ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if seen[sequence] || sequence.Reader != values.reader {
|
|
continue
|
|
}
|
|
seen[sequence] = true
|
|
key, ok, err := pdfStructureContentKey(sequence)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
continue
|
|
}
|
|
node := StructureNode{Name: "MarkedContent", Namespace: pdfStructureNamespace, Attributes: pdfStructureAttribute("Tag", string(sequence.Tag))}
|
|
properties := sequence.Properties
|
|
if _, ok := properties["MCID"].(pdfgo.Integer); ok {
|
|
properties = maps.Clone(properties)
|
|
delete(properties, "MCID")
|
|
}
|
|
if len(properties) != 0 {
|
|
property, err := values.encode(properties, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
node.Children = append(node.Children, property)
|
|
}
|
|
if previous, ok := result[key]; ok {
|
|
equal, err := pdfStructurePropertiesEqual(values.ctx, previous, node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !equal {
|
|
return nil, fmt.Errorf("conflicting PDF marked content properties")
|
|
}
|
|
continue
|
|
}
|
|
result[key] = node
|
|
}
|
|
}
|
|
return result, values.ctx.Err()
|
|
}
|
|
|
|
// pdfStructurePropertiesEqual 比较类型化属性树,不生成临时XML
|
|
// 入参: ctx 取消上下文, left 原属性, right 待比较属性
|
|
// 返回: bool 属性是否一致, error 取消错误
|
|
func pdfStructurePropertiesEqual(ctx context.Context, left, right StructureNode) (bool, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return false, err
|
|
}
|
|
if left.Name != right.Name || left.Namespace != right.Namespace || left.Text != right.Text || !slices.Equal(left.Attributes, right.Attributes) || len(left.Children) != len(right.Children) {
|
|
return false, nil
|
|
}
|
|
for i := range left.Children {
|
|
equal, err := pdfStructurePropertiesEqual(ctx, left.Children[i], right.Children[i])
|
|
if err != nil || !equal {
|
|
return equal, err
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// contentReferences 按来源解释MCID并汇总重复调用的目标图元
|
|
// 入参: ctx 取消上下文
|
|
// 返回: pdfStructureReferences 内容引用索引, error 属性或取消错误
|
|
func (b *pdfStructureBindings) contentReferences(ctx context.Context) (pdfStructureReferences, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
result := make(pdfStructureReferences)
|
|
sets := make(map[pdfStructureContent]*pdfStructureReferenceSet)
|
|
for _, sequence := range b.order {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
key, ok, err := pdfStructureContentKey(sequence)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !ok {
|
|
continue
|
|
}
|
|
set := sets[key]
|
|
if set == nil {
|
|
set = &pdfStructureReferenceSet{}
|
|
sets[key] = set
|
|
}
|
|
if err := set.append(ctx, b.sequences[sequence]); err != nil {
|
|
return nil, err
|
|
}
|
|
result[key] = set.references
|
|
}
|
|
return result, ctx.Err()
|
|
}
|
|
|
|
// append 归并引用并去重,不持有输入切片
|
|
// 入参: ctx 取消上下文, sources 来源引用
|
|
// 返回: error 取消错误
|
|
func (s *pdfStructureReferenceSet) append(ctx context.Context, sources []StructureReference) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
for _, source := range sources {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
index := -1
|
|
if last := len(s.references) - 1; last >= 0 && s.references[last].PageID == source.PageID {
|
|
index = last
|
|
} else if page, ok := s.pages[source.PageID]; ok {
|
|
index = page
|
|
}
|
|
for _, id := range source.ObjectIDs {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
key := [2]string{source.PageID, id}
|
|
if s.seen[key] {
|
|
continue
|
|
}
|
|
if s.seen == nil {
|
|
s.seen = make(map[[2]string]bool)
|
|
}
|
|
s.seen[key] = true
|
|
if index < 0 {
|
|
index = len(s.references)
|
|
if index == 1 {
|
|
s.pages = map[string]int{s.references[0].PageID: 0}
|
|
}
|
|
if s.pages != nil {
|
|
s.pages[source.PageID] = index
|
|
}
|
|
s.references = append(s.references, StructureReference{PageID: source.PageID})
|
|
}
|
|
s.references[index].ObjectIDs = append(s.references[index].ObjectIDs, id)
|
|
}
|
|
}
|
|
return ctx.Err()
|
|
}
|
|
|
|
// structureBindings 在提交前收集直接来源及含MCID的外层区段
|
|
// 入参: ctx 取消上下文
|
|
// 返回: []pdfStructureBinding 绑定位置, error 取消错误
|
|
func (buffer *pdfObjectBuffer) structureBindings(ctx context.Context) ([]pdfStructureBinding, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if buffer == nil {
|
|
return nil, nil
|
|
}
|
|
var result []pdfStructureBinding
|
|
for index, run := range buffer.sequences {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
end := buffer.len()
|
|
if index+1 < len(buffer.sequences) {
|
|
end = buffer.sequences[index+1].start
|
|
}
|
|
var sources pdfOriginSet
|
|
collect := func(sequence *pdfgo.ContentSequence) error {
|
|
for current := sequence; current != nil; current = current.Parent {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if current == sequence || current.Properties["MCID"] != nil || current.Object != (pdfgo.Reference{}) {
|
|
sources.add(current)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := collect(run.sequence); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, sequence := range run.related {
|
|
if err := collect(sequence); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if !sources.started {
|
|
continue
|
|
}
|
|
result = append(result, pdfStructureBinding{sequence: sources.origin.sequence, start: run.start, end: end})
|
|
for _, sequence := range sources.origin.related {
|
|
result = append(result, pdfStructureBinding{sequence: sequence, start: run.start, end: end})
|
|
}
|
|
}
|
|
return result, ctx.Err()
|
|
}
|
|
|
|
// append 在页面提交成功后登记已分配的图元标识
|
|
// 入参: pageID 页面标识, objects 已分配标识的对象, bindings 已准备的绑定位置
|
|
func (b *pdfStructureBindings) append(pageID string, objects []GraphicObject, bindings []pdfStructureBinding) {
|
|
for _, binding := range bindings {
|
|
last := b.reference(pageID, binding.sequence)
|
|
for _, object := range objects[binding.start:binding.end] {
|
|
last.ObjectIDs = append(last.ObjectIDs, editorObjectID(object))
|
|
}
|
|
}
|
|
}
|
|
|
|
// reference 返回已提交来源区段在页面中的引用
|
|
// 入参: pageID 页面标识, sequence 来源区段
|
|
// 返回: *StructureReference 页面引用
|
|
func (b *pdfStructureBindings) reference(pageID string, sequence *pdfgo.ContentSequence) *StructureReference {
|
|
if b.sequences == nil {
|
|
b.sequences = make(map[*pdfgo.ContentSequence][]StructureReference)
|
|
}
|
|
refs := b.sequences[sequence]
|
|
if len(refs) == 0 {
|
|
b.order = append(b.order, sequence)
|
|
}
|
|
if len(refs) == 0 || refs[len(refs)-1].PageID != pageID {
|
|
refs = append(refs, StructureReference{PageID: pageID})
|
|
}
|
|
b.sequences[sequence] = refs
|
|
return &refs[len(refs)-1]
|
|
}
|
|
|
|
// documentMetadata 保留PDF元数据原文并映射文档描述,来源软件与扩展字段保存在独立标引中
|
|
// 入参: ctx 取消上下文, strict 是否严格校验
|
|
// 返回: error 读取、解析、日期或提交错误
|
|
func (p *pdfImporter) documentMetadata(ctx context.Context, strict bool) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
info, err := p.reader.Info()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, field := range []struct {
|
|
name string
|
|
text *string
|
|
}{{"CreationDate", &info.CreationDate}, {"ModDate", &info.ModDate}} {
|
|
text, err := codecpdf.NormalizeDate(*field.text, strict)
|
|
if err != nil {
|
|
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 {
|
|
p.report.Warnings = append(p.report.Warnings, pdfgo.Diagnostic{Message: "PDF metadata " + field.name + ": date normalized"})
|
|
*field.text = text
|
|
}
|
|
}
|
|
values, original, err := p.informationMetadata(ctx, info)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stream, err := p.reader.Metadata()
|
|
if err != nil {
|
|
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 root *StructureNode
|
|
if stream != nil {
|
|
packet, err = stream.DecodeContext(ctx)
|
|
if err != nil {
|
|
if err := p.recoverPDFObject(ctx, "Metadata", stream, err); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
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)
|
|
if err != nil {
|
|
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
|
|
if stamp, ok := xmp["MetadataDate"]; ok && stamp.text != "" && info.ModDate != "" {
|
|
date, _, zoned, err := codecpdf.MetadataDate(stamp.text, stamp.xmp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
modified, err := pdfgo.ParseDate(info.ModDate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newer = zoned && modified.After(date)
|
|
}
|
|
for key, value := range xmp {
|
|
if _, exists := values[key]; !newer || !exists {
|
|
values[key] = value
|
|
}
|
|
}
|
|
}
|
|
projected := p.editor.Info
|
|
for _, field := range []struct {
|
|
name string
|
|
text *string
|
|
}{{"Title", &projected.Title}, {"Author", &projected.Author}, {"Subject", &projected.Subject}} {
|
|
if value, ok := values[field.name]; ok && structureXMLText(value.text) {
|
|
*field.text = value.text
|
|
}
|
|
}
|
|
if value, ok := values["Keywords"]; ok && structureXMLText(value.text) {
|
|
projected.Keywords = Keywords{value.text}
|
|
}
|
|
for _, field := range []struct {
|
|
name string
|
|
text *string
|
|
}{{"CreationDate", &projected.CreationDate}, {"ModDate", &projected.ModDate}} {
|
|
if value, ok := values[field.name]; ok {
|
|
date, day, _, err := codecpdf.MetadataDate(value.text, value.xmp)
|
|
if err != nil {
|
|
return fmt.Errorf("PDF metadata %s: %w", field.name, err)
|
|
}
|
|
*field.text = ""
|
|
if day {
|
|
*field.text = date.Format("2006-01-02")
|
|
}
|
|
}
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
p.editor.SetInfo(projected)
|
|
if root != nil {
|
|
if _, err := p.editor.addStructureXML(ctx, CustomTag{TypeID: pdfMetadataTagType, NameSpace: root.Namespace}, packet); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if len(original.Children) != 0 {
|
|
data, err := encodeStructureNode(original)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err = p.editor.addStructureXML(ctx, CustomTag{TypeID: pdfInformationTagType, NameSpace: pdfInformationNamespace}, data); err != nil {
|
|
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()
|
|
}
|
|
|
|
// informationMetadata 收集已声明字段,日期分别保留原文及投影值,XML无法表示的文本使用Base64保留
|
|
// 入参: ctx 取消上下文, info 已解码的信息字典
|
|
// 返回: 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}
|
|
values := make(map[string]pdfMetadataValue)
|
|
object, err := p.reader.Resolve(p.reader.Trailer["Info"])
|
|
if err != nil {
|
|
return values, root, p.recoverPDFObject(ctx, "Information", p.reader.Trailer["Info"], err)
|
|
}
|
|
if object == nil {
|
|
return values, root, nil
|
|
}
|
|
dict, ok := object.(pdfgo.Dictionary)
|
|
if !ok {
|
|
return values, root, p.recoverPDFObject(ctx, "Information", object, fmt.Errorf("invalid PDF information dictionary"))
|
|
}
|
|
known := map[pdfgo.Name]string{
|
|
"Title": info.Title, "Author": info.Author, "Subject": info.Subject, "Keywords": info.Keywords,
|
|
"Creator": info.Creator, "Producer": info.Producer, "CreationDate": info.CreationDate, "ModDate": info.ModDate,
|
|
"Trapped": string(info.Trapped),
|
|
}
|
|
for _, key := range slices.Sorted(maps.Keys(dict)) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, root, err
|
|
}
|
|
value, err := p.reader.Resolve(dict[key])
|
|
if err != nil {
|
|
if err := p.recoverPDFObject(ctx, "Information."+string(key), dict[key], err); err != nil {
|
|
return nil, root, err
|
|
}
|
|
continue
|
|
}
|
|
if value == nil {
|
|
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]
|
|
_, 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 {
|
|
text = info.Custom[key]
|
|
} else {
|
|
values[string(key)] = pdfMetadataValue{text: text}
|
|
}
|
|
name := string(key)
|
|
field := StructureNode{Name: "Field", Namespace: pdfInformationNamespace}
|
|
field.Attributes = append(field.Attributes, StructureAttribute{Name: "Name", Value: name})
|
|
if !structureXMLText(name) {
|
|
field.Attributes[0].Value = base64.StdEncoding.EncodeToString([]byte(name))
|
|
field.Attributes = append(field.Attributes, StructureAttribute{Name: "NameEncoding", Value: "base64"})
|
|
}
|
|
if key == "Trapped" {
|
|
field.Attributes = append(field.Attributes, StructureAttribute{Name: "Type", Value: "name"})
|
|
}
|
|
if !structureXMLText(text) {
|
|
text = base64.StdEncoding.EncodeToString([]byte(text))
|
|
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
|
|
root.Children = append(root.Children, field)
|
|
}
|
|
return values, root, nil
|
|
}
|
|
|
|
// pdfMetadataFields 读取文档级RDF描述,日期及PDF关键词不与其他同名扩展混用
|
|
// 入参: ctx 取消上下文, root 元数据根节点
|
|
// 返回: map[string]pdfMetadataValue 已声明的对应字段, error 取消错误
|
|
func pdfMetadataFields(ctx context.Context, root *StructureNode) (map[string]pdfMetadataValue, error) {
|
|
result := make(map[string]pdfMetadataValue)
|
|
var rdf *StructureNode
|
|
if root.Namespace == pdfMetadataRDF && root.Name == "RDF" {
|
|
rdf = root
|
|
} else if root.Namespace == pdfMetadataXML && (root.Name == "xmpmeta" || root.Name == "xapmeta") {
|
|
for i := range root.Children {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
child := &root.Children[i]
|
|
if child.Namespace == pdfMetadataRDF && child.Name == "RDF" {
|
|
rdf = child
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if rdf == nil {
|
|
return result, ctx.Err()
|
|
}
|
|
fields := map[xml.Name]string{
|
|
{Space: pdfMetadataDC, Local: "title"}: "Title", {Space: pdfMetadataDC, Local: "creator"}: "Author",
|
|
{Space: pdfMetadataDC, Local: "description"}: "Subject", {Space: pdfMetadataPDF, Local: "Keywords"}: "Keywords",
|
|
{Space: pdfMetadataXMP, Local: "CreateDate"}: "CreationDate", {Space: pdfMetadataXMP, Local: "ModifyDate"}: "ModDate",
|
|
{Space: pdfMetadataXMP, Local: "MetadataDate"}: "MetadataDate",
|
|
}
|
|
for _, description := range rdf.Children {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if description.Namespace != pdfMetadataRDF || description.Name != "Description" {
|
|
continue
|
|
}
|
|
for _, attr := range description.Attributes {
|
|
if key, ok := fields[xml.Name{Space: attr.Namespace, Local: attr.Name}]; ok && attr.Namespace != pdfMetadataDC {
|
|
result[key] = pdfMetadataValue{text: attr.Value, xmp: true}
|
|
}
|
|
}
|
|
for _, property := range description.Children {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
key, ok := fields[xml.Name{Space: property.Namespace, Local: property.Name}]
|
|
if !ok {
|
|
continue
|
|
}
|
|
arrayValue := property.Namespace == pdfMetadataDC
|
|
property = pdfMetadataProperty(property)
|
|
if arrayValue {
|
|
kind := "Alt"
|
|
if key == "Author" {
|
|
kind = "Seq"
|
|
}
|
|
found := false
|
|
for _, array := range property.Children {
|
|
if array.Namespace == pdfMetadataRDF && array.Name == kind && len(array.Children) != 0 {
|
|
item := array.Children[0]
|
|
if item.Namespace == pdfMetadataRDF && item.Name == "li" {
|
|
property = item
|
|
found = true
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
continue
|
|
}
|
|
}
|
|
if text, ok := pdfMetadataText(property); ok {
|
|
result[key] = pdfMetadataValue{text: text, xmp: true}
|
|
}
|
|
}
|
|
}
|
|
return result, ctx.Err()
|
|
}
|
|
|
|
// pdfMetadataProperty 提取带限定符的rdf:value,保留数组及结构边界
|
|
// 入参: node 属性节点
|
|
// 返回: StructureNode 属性值节点
|
|
func pdfMetadataProperty(node StructureNode) StructureNode {
|
|
for {
|
|
if len(node.Children) == 1 && node.Children[0].Namespace == pdfMetadataRDF && node.Children[0].Name == "Description" {
|
|
node = node.Children[0]
|
|
continue
|
|
}
|
|
for _, attr := range node.Attributes {
|
|
if attr.Namespace == pdfMetadataRDF && attr.Name == "value" {
|
|
return StructureNode{Text: attr.Value}
|
|
}
|
|
}
|
|
found := false
|
|
for _, child := range node.Children {
|
|
if child.Namespace == pdfMetadataRDF && child.Name == "value" {
|
|
node, found = child, true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return node
|
|
}
|
|
}
|
|
}
|
|
|
|
// pdfMetadataText 读取简单值,不拼接结构或数组文本,也不将资源URI当作文本
|
|
// 入参: node 属性节点
|
|
// 返回: string 文本, bool 是否为支持的简单值
|
|
func pdfMetadataText(node StructureNode) (string, bool) {
|
|
node = pdfMetadataProperty(node)
|
|
for _, attr := range node.Attributes {
|
|
if attr.Namespace == pdfMetadataRDF && (attr.Name == "resource" || attr.Name == "nodeID" || attr.Name == "parseType") {
|
|
return "", false
|
|
}
|
|
}
|
|
return node.Text, len(node.Children) == 0
|
|
}
|