mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:25:49 +08:00
353 lines
11 KiB
Go
353 lines
11 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"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/xiaoqidun/pdfgo"
|
|
)
|
|
|
|
// 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"
|
|
)
|
|
|
|
// pdfMetadataValue 保留字段是否声明及其来源,空文本不作为缺省值
|
|
type pdfMetadataValue struct {
|
|
text string
|
|
xmp bool
|
|
}
|
|
|
|
// 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 := pdfMetadataDate(stamp)
|
|
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 := pdfMetadataDate(value)
|
|
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
|
|
}
|
|
|
|
// pdfMetadataDate 保留当地日历日期,PDF省略时区按GMT,XMP省略日或时区时不推算
|
|
// 入参: value 来源日期
|
|
// 返回: time.Time 解析值, bool 是否精确到日, bool 是否可比较绝对时间, error 日期错误
|
|
func pdfMetadataDate(value pdfMetadataValue) (time.Time, bool, bool, error) {
|
|
if value.text == "" {
|
|
return time.Time{}, false, false, nil
|
|
}
|
|
if !value.xmp {
|
|
date, err := pdfgo.ParseDate(value.text)
|
|
return date, err == nil && date.Year() > 0, err == nil, err
|
|
}
|
|
if !strings.Contains(value.text, ",") {
|
|
for _, layout := range []string{time.RFC3339Nano, "2006-01-02T15:04Z07:00", "2006-01-02T15:04:05", "2006-01-02T15:04", "2006-01-02", "2006-01", "2006"} {
|
|
if date, err := time.Parse(layout, value.text); err == nil {
|
|
zoned := strings.Contains(layout, "Z07:00")
|
|
if zoned && !strings.HasSuffix(value.text, "Z") {
|
|
zone := value.text[len(value.text)-6:]
|
|
if zone[1:3] > "23" || zone[4:] > "59" {
|
|
return time.Time{}, false, false, fmt.Errorf("invalid XMP time zone")
|
|
}
|
|
}
|
|
return date, len(value.text) >= 10 && date.Year() > 0, zoned, nil
|
|
}
|
|
}
|
|
}
|
|
return time.Time{}, false, false, fmt.Errorf("invalid XMP date")
|
|
}
|