mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
84808cda61
commit
ec24b6a17d
17 files changed
+1908
-2026
No files matched your search
@@ -17,6 +17,7 @@ package ofdgo
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"maps"
|
||||
"math"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
|
||||
@@ -33,6 +35,22 @@ const (
|
||||
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"
|
||||
)
|
||||
|
||||
// pdfStructureBindings 保存来源区段与最终页面图元的关联,不记录临时渲染对象
|
||||
type pdfStructureBindings struct {
|
||||
parents map[*pdfgo.Reader]map[[2]int64]pdfgo.Reference
|
||||
@@ -93,6 +111,12 @@ type pdfStructureValues struct {
|
||||
objects []StructureNode
|
||||
}
|
||||
|
||||
// pdfMetadataValue 保留字段是否声明及其来源,空文本不作为缺省值
|
||||
type pdfMetadataValue struct {
|
||||
text string
|
||||
xmp bool
|
||||
}
|
||||
|
||||
// prepareStructure 校验结构内容关联并登记引用区段,不提前写入标引
|
||||
// 入参: ctx 取消上下文
|
||||
// 返回: error 结构或内容容器错误
|
||||
@@ -882,3 +906,277 @@ func (b *pdfStructureBindings) reference(pageID string, sequence *pdfgo.ContentS
|
||||
b.sequences[sequence] = refs
|
||||
return &refs[len(refs)-1]
|
||||
}
|
||||
|
||||
// documentMetadata 保留PDF元数据原文并映射文档描述,来源软件与扩展字段保存在独立标引中
|
||||
// 入参: ctx 取消上下文
|
||||
// 返回: error 读取、解析、日期或提交错误
|
||||
func (p *pdfImporter) documentMetadata(ctx context.Context) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := p.reader.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
values, original, err := p.informationMetadata(ctx, info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stream, err := p.reader.Metadata()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var packet []byte
|
||||
var root *StructureNode
|
||||
if stream != nil {
|
||||
packet, err = stream.DecodeContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root, err = parseStructure(ctx, packet, CustomTag{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
xmp, err := pdfMetadataFields(ctx, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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
|
||||
}
|
||||
_, err = p.editor.addStructureXML(ctx, CustomTag{TypeID: pdfInformationTagType, NameSpace: pdfInformationNamespace}, data)
|
||||
return err
|
||||
}
|
||||
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 || object == nil {
|
||||
return values, root, err
|
||||
}
|
||||
dict, ok := object.(pdfgo.Dictionary)
|
||||
if !ok {
|
||||
return nil, root, 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 {
|
||||
return nil, root, err
|
||||
}
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
text, standard := known[key]
|
||||
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"})
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user