feat(文件归并): 归并PDF封装并下沉计算逻辑
build.yaml / build (push) Successful in 45s

This commit is contained in:
xiaoqidun committed 2026-10-19 10:10:27 +08:00
1 parent 84808cda61
commit ec24b6a17d
17 files changed
+1908 -2026

No files matched your search

+298
View File
@@ -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
}