mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
1 parent
627a06cda3
commit
a2de650b52
31 files changed
+2109
-192
No files matched your search
@@ -11,7 +11,7 @@ require (
|
|||||||
github.com/xiaoqidun/guigo v0.0.0-20261017165144-57ca5ff5f547
|
github.com/xiaoqidun/guigo v0.0.0-20261017165144-57ca5ff5f547
|
||||||
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-20261017161421-0b88b77eefc9
|
github.com/xiaoqidun/pdfgo v0.0.0-20261018100651-e306ca887a36
|
||||||
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
|
||||||
|
|||||||
@@ -64,8 +64,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-20261017161421-0b88b77eefc9 h1:DsWSdFwsDpI8k0jadqcnGbpJuOpT+tIVIX5JfDFS2GQ=
|
github.com/xiaoqidun/pdfgo v0.0.0-20261018100651-e306ca887a36 h1:PkJ4BNsjOgIlTIClR9DngaGyka9AYMb7d1GkhOuXzi8=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20261017161421-0b88b77eefc9/go.mod h1:AaabzsObZ8Z76AcaWbo6swRROrJsblyIGB7HbieeJA4=
|
github.com/xiaoqidun/pdfgo v0.0.0-20261018100651-e306ca887a36/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=
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ func NormalizeOS2(data []byte) []byte {
|
|||||||
var size int
|
var size int
|
||||||
switch binary.BigEndian.Uint16(data) {
|
switch binary.BigEndian.Uint16(data) {
|
||||||
case 0:
|
case 0:
|
||||||
// 旧式68字节表保持原样,不将截断的78字节表推断为旧式表
|
|
||||||
size = 78
|
size = 78
|
||||||
case 1:
|
case 1:
|
||||||
size = 86
|
size = 86
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package geom
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UnsupportedError 表示当前几何实现无法展开的描边形态
|
// UnsupportedError 表示当前几何实现无法展开的描边形态
|
||||||
@@ -347,7 +348,22 @@ func (line geometryPolyline) stroke(result Path, options StrokeOptions) (Path, e
|
|||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
directions := make([]Point, len(points)-1)
|
segments := len(points) - 1
|
||||||
|
joins := segments - 1
|
||||||
|
caps := 0
|
||||||
|
if line.closed {
|
||||||
|
joins++
|
||||||
|
} else if options.Cap == "Round" {
|
||||||
|
caps = 8
|
||||||
|
} else if options.Cap == "Square" {
|
||||||
|
caps = 10
|
||||||
|
}
|
||||||
|
joinSize := 4
|
||||||
|
if options.Join == "" || options.Join == "Miter" {
|
||||||
|
joinSize = 5
|
||||||
|
}
|
||||||
|
result = slices.Grow(result, min(segments*5+joins*joinSize+caps, 1<<20-len(result)))
|
||||||
|
directions := make([]Point, segments)
|
||||||
for i := range directions {
|
for i := range directions {
|
||||||
a, b := points[i], points[i+1]
|
a, b := points[i], points[i+1]
|
||||||
length := math.Hypot(b.X-a.X, b.Y-a.Y)
|
length := math.Hypot(b.X-a.X, b.Y-a.Y)
|
||||||
|
|||||||
@@ -31,10 +31,11 @@ type TextPage struct {
|
|||||||
Replacements []pdfgo.TextReplacement
|
Replacements []pdfgo.TextReplacement
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteOptions 保存PDF最终写出所需的像素、文字和导航修正
|
// WriteOptions 保存PDF最终写出所需的元数据、像素、文字和导航修正
|
||||||
// Images逐页记录绘制图像,nil图像表示不替换;Links保留全部链接位置,包括矩形区域的nil项
|
// Images逐页记录绘制图像,nil图像表示不替换;Links保留全部链接位置,包括矩形区域的nil项
|
||||||
// PrepareRewrite在页面引用确定后填充导航动作,不得提前写出PDF
|
// PrepareRewrite在页面引用确定后填充导航动作,不得提前写出PDF
|
||||||
type WriteOptions struct {
|
type WriteOptions struct {
|
||||||
|
Information pdfgo.Dictionary
|
||||||
Images [][]image.Image
|
Images [][]image.Image
|
||||||
Text []TextPage
|
Text []TextPage
|
||||||
Links map[int][]*pdfgo.LinkRegion
|
Links map[int][]*pdfgo.LinkRegion
|
||||||
@@ -49,7 +50,7 @@ type WriteOptions struct {
|
|||||||
// 返回: error 资源、压缩或写入错误
|
// 返回: error 资源、压缩或写入错误
|
||||||
func Write(ctx context.Context, data []byte, writer io.Writer, settings WriteOptions) error {
|
func Write(ctx context.Context, data []byte, writer io.Writer, settings WriteOptions) error {
|
||||||
nativeWrite := settings.PrepareRewrite != nil
|
nativeWrite := settings.PrepareRewrite != nil
|
||||||
if !settings.ExactImages && !settings.ExactText && !nativeWrite && settings.Optimization.Compression.Mode == pdfgo.CompressionUnchanged {
|
if !settings.ExactImages && !settings.ExactText && !nativeWrite && len(settings.Information) == 0 && settings.Optimization.Compression.Mode == pdfgo.CompressionUnchanged {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -65,6 +66,10 @@ func Write(ctx context.Context, data []byte, writer io.Writer, settings WriteOpt
|
|||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
if !settings.ExactImages && !settings.ExactText && !nativeWrite {
|
if !settings.ExactImages && !settings.ExactText && !nativeWrite {
|
||||||
|
if len(settings.Information) != 0 {
|
||||||
|
_, err := reader.RewriteTo(ctx, writer, pdfgo.RewriteOptions{Information: settings.Information, Optimization: settings.Optimization})
|
||||||
|
return err
|
||||||
|
}
|
||||||
_, err := reader.OptimizeTo(ctx, writer, settings.Optimization)
|
_, err := reader.OptimizeTo(ctx, writer, settings.Optimization)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -171,7 +176,7 @@ func Write(ctx context.Context, data []byte, writer io.Writer, settings WriteOpt
|
|||||||
if pages != len(settings.Images) {
|
if pages != len(settings.Images) {
|
||||||
return fmt.Errorf("PDF output page missing")
|
return fmt.Errorf("PDF output page missing")
|
||||||
}
|
}
|
||||||
options := pdfgo.RewriteOptions{Images: images, ImageOptions: pdfgo.ImageWriteOptions{PreblendBinary: true}, LinkRegions: links, TextReplacements: texts, Optimization: settings.Optimization}
|
options := pdfgo.RewriteOptions{Information: settings.Information, Images: images, ImageOptions: pdfgo.ImageWriteOptions{PreblendBinary: true}, LinkRegions: links, TextReplacements: texts, Optimization: settings.Optimization}
|
||||||
if nativeWrite {
|
if nativeWrite {
|
||||||
if err := settings.PrepareRewrite(ctx, reader, references, &options); err != nil {
|
if err := settings.PrepareRewrite(ctx, reader, references, &options); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// sourceInfoXML 更新文档描述字段,保留未识别内容
|
// sourceInfoXML 更新文档描述字段,保留未识别内容
|
||||||
@@ -54,6 +55,16 @@ func editorUpdateInfoXML(data []byte, docID string, index int, before, after Doc
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if !slices.Equal(before.Keywords, after.Keywords) {
|
||||||
|
root, err := parseEditorXML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data, err = editorKeywords(data, root.childAt("DocBody", index).child("DocInfo"), after.Keywords)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if !reflect.DeepEqual(before.CustomDatas, after.CustomDatas) {
|
if !reflect.DeepEqual(before.CustomDatas, after.CustomDatas) {
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXML(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -64,6 +75,69 @@ func editorUpdateInfoXML(data []byte, docID string, index int, before, after Doc
|
|||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editorKeywords 更新有序关键词,保留已有词条的属性及未修改内容
|
||||||
|
// 入参: data 原XML, info 文档信息节点, values 关键词
|
||||||
|
// 返回: []byte 修改后的XML, error 编码错误
|
||||||
|
func editorKeywords(data []byte, info *editorXML, values []string) ([]byte, error) {
|
||||||
|
container := info.child("Keywords")
|
||||||
|
if len(values) == 0 {
|
||||||
|
if container == nil {
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
return editorPatchXML(data, []editorXMLPatch{{container.start, container.end, nil}}), nil
|
||||||
|
}
|
||||||
|
if container == nil {
|
||||||
|
var content []byte
|
||||||
|
for _, value := range values {
|
||||||
|
content = append(content, info.textXML("Keyword", value)...)
|
||||||
|
}
|
||||||
|
encoded, err := info.containerXML("Keywords", nil, content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if info.open == info.end {
|
||||||
|
return editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, info, encoded)}), nil
|
||||||
|
}
|
||||||
|
position := info.close
|
||||||
|
for _, child := range info.children {
|
||||||
|
if child.matchesOFD("Creator") || child.matchesOFD("CreatorVersion") || child.matchesOFD("CustomDatas") {
|
||||||
|
position = child.start
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return editorPatchXML(data, []editorXMLPatch{{position, position, encoded}}), nil
|
||||||
|
}
|
||||||
|
var patches []editorXMLPatch
|
||||||
|
index := 0
|
||||||
|
for _, child := range container.children {
|
||||||
|
if !child.matchesOFD("Keyword") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var encoded []byte
|
||||||
|
if index < len(values) {
|
||||||
|
var err error
|
||||||
|
encoded, err = editorXMLTextValue(data, child, editorImportText(data, child), values[index])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
patches = append(patches, editorXMLPatch{child.start, child.end, encoded})
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
var added []byte
|
||||||
|
for ; index < len(values); index++ {
|
||||||
|
added = append(added, container.textXML("Keyword", values[index])...)
|
||||||
|
}
|
||||||
|
if len(added) != 0 {
|
||||||
|
if container.open == container.end {
|
||||||
|
patches = append(patches, editorXMLContent(data, container, added))
|
||||||
|
} else {
|
||||||
|
patches = append(patches, editorXMLPatch{container.close, container.close, added})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return editorPatchXML(data, patches), nil
|
||||||
|
}
|
||||||
|
|
||||||
// editorCreatorXML 更新指定文档的制作软件,移除原软件版本,保留其余根索引内容
|
// editorCreatorXML 更新指定文档的制作软件,移除原软件版本,保留其余根索引内容
|
||||||
// 入参: data 当前根索引XML, docID 缺少文档描述时使用的标识, index 文档索引
|
// 入参: data 当前根索引XML, docID 缺少文档描述时使用的标识, index 文档索引
|
||||||
// 返回: []byte 修改后的XML, error 解析错误
|
// 返回: []byte 修改后的XML, error 解析错误
|
||||||
|
|||||||
@@ -476,14 +476,12 @@ func rewriteStructureReferences(ctx context.Context, data []byte, tag CustomTag,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var patches []editorXMLPatch
|
var patches []editorXMLPatch
|
||||||
count := 0
|
|
||||||
var walk func(*editorXML, int) error
|
var walk func(*editorXML, int) error
|
||||||
walk = func(node *editorXML, depth int) error {
|
walk = func(node *editorXML, depth int) error {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
count++
|
if depth > 128 {
|
||||||
if depth > 128 || count > 100000 {
|
|
||||||
return fmt.Errorf("custom tag structure limit exceeded")
|
return fmt.Errorf("custom tag structure limit exceeded")
|
||||||
}
|
}
|
||||||
if node.name.Space != root.name.Space {
|
if node.name.Space != root.name.Space {
|
||||||
|
|||||||
@@ -40,39 +40,67 @@ func (e *Editor) Structure() ([]Structure, error) {
|
|||||||
// 入参: tag 类型、命名空间及可选Schema位置, root 根节点
|
// 入参: tag 类型、命名空间及可选Schema位置, root 根节点
|
||||||
// 返回: int 从0开始的标引索引, error 校验或提交错误
|
// 返回: int 从0开始的标引索引, error 校验或提交错误
|
||||||
func (e *Editor) AddStructure(tag CustomTag, root StructureNode) (int, error) {
|
func (e *Editor) AddStructure(tag CustomTag, root StructureNode) (int, error) {
|
||||||
|
data, err := encodeStructureNode(root)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return e.addStructureXML(context.Background(), tag, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddStructureXML 添加XML标引原文,保留处理指令、注释、前缀及混合文本
|
||||||
|
// FileLoc由库分配,SchemaLoc仅引用包内已有文件,返回后不再引用输入数据
|
||||||
|
// 入参: tag 标引入口, data 完整XML
|
||||||
|
// 返回: int 从0开始的标引索引, error 解析、引用或提交错误
|
||||||
|
func (e *Editor) AddStructureXML(tag CustomTag, data []byte) (int, error) {
|
||||||
|
return e.AddStructureXMLContext(context.Background(), tag, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddStructureXMLContext 校验并保存XML标引原文,取消或失败时不提交编辑状态
|
||||||
|
// 入参: ctx 取消上下文, tag 标引入口, data 完整XML
|
||||||
|
// 返回: int 从0开始的标引索引, error 解析、引用、提交或取消错误
|
||||||
|
func (e *Editor) AddStructureXMLContext(ctx context.Context, tag CustomTag, data []byte) (int, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return e.addStructureXML(ctx, tag, bytes.Clone(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// addStructureXML 接管独立XML数据并提交标引,复用编码或解码结果而不再次复制
|
||||||
|
// 入参: ctx 取消上下文, tag 标引入口, data 独立XML数据,提交后不再修改
|
||||||
|
// 返回: int 标引索引, error 校验、提交或取消错误
|
||||||
|
func (e *Editor) addStructureXML(ctx context.Context, tag CustomTag, data []byte) (int, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
if strings.TrimSpace(tag.TypeID) == "" || tag.FileLoc != "" {
|
if strings.TrimSpace(tag.TypeID) == "" || tag.FileLoc != "" {
|
||||||
return 0, fmt.Errorf("custom tag requires TypeID and an automatically assigned FileLoc")
|
return 0, fmt.Errorf("custom tag requires TypeID and an automatically assigned FileLoc")
|
||||||
}
|
}
|
||||||
if !structureXMLText(tag.TypeID) || !structureXMLText(tag.NameSpace) || !structureXMLText(tag.SchemaLoc) {
|
if !structureXMLText(tag.TypeID) || !structureXMLText(tag.NameSpace) || !structureXMLText(tag.SchemaLoc) {
|
||||||
return 0, fmt.Errorf("invalid custom tag metadata")
|
return 0, fmt.Errorf("invalid custom tag metadata")
|
||||||
}
|
}
|
||||||
data, err := encodeStructureNode(root)
|
tree, err := parseStructure(ctx, data, tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
tree, err := parseStructure(context.Background(), data, tag)
|
objects, err := e.validateStructureReferencesContext(ctx, tree, tag, nil)
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
objects, err := e.validateStructureReferences(tree, tag, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
base := e.source
|
base := e.source
|
||||||
if base == nil {
|
if base == nil {
|
||||||
base, err = e.importBase()
|
base, err = e.importBaseContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tag.SchemaLoc != "" {
|
if tag.SchemaLoc != "" {
|
||||||
name := base.reader.ResPath(tag.SchemaLoc)
|
name := base.reader.ResPath(tag.SchemaLoc)
|
||||||
if _, err := base.reader.readFile(name); err != nil {
|
if _, err := base.reader.readFileViewContext(ctx, name); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
tag.SchemaLoc = "/" + name
|
tag.SchemaLoc = "/" + name
|
||||||
}
|
}
|
||||||
name, index, container, entries, err := editorStructureIndex(base, nil)
|
name, index, container, entries, err := editorStructureIndexContext(ctx, base, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -96,11 +124,11 @@ func (e *Editor) AddStructure(tag CustomTag, root StructureNode) (int, error) {
|
|||||||
parts[name] = editorPatchXML(index, []editorXMLPatch{editorXMLContent(index, container, entry)})
|
parts[name] = editorPatchXML(index, []editorXMLPatch{editorXMLContent(index, container, entry)})
|
||||||
if base.document.CustomTags.Path == "" && name != base.reader.documentRoot {
|
if base.document.CustomTags.Path == "" && name != base.reader.documentRoot {
|
||||||
docName := base.reader.documentRoot
|
docName := base.reader.documentRoot
|
||||||
docData, err := base.reader.readFile(docName)
|
docData, err := base.reader.readFileViewContext(ctx, docName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
doc, err := parseEditorXML(docData)
|
doc, err := parseEditorXMLContext(ctx, docData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -109,7 +137,23 @@ func (e *Editor) AddStructure(tag CustomTag, root StructureNode) (int, error) {
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return len(entries), e.commitStructureParts(base, parts, objects, len(entries))
|
return len(entries), e.commitStructurePartsContext(ctx, base, parts, objects, len(entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStructureXML 替换标引XML原文,保留入口属性并隔离共享内容文件
|
||||||
|
// 入参: index 标引索引, data 完整XML,返回后不再引用输入数据
|
||||||
|
// 返回: error 索引、解析、引用或提交错误
|
||||||
|
func (e *Editor) UpdateStructureXML(index int, data []byte) error {
|
||||||
|
return e.UpdateStructureXMLContext(context.Background(), index, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateStructureXMLContext 校验并原子替换标引原文,相同内容不产生撤销记录
|
||||||
|
// 入参: ctx 取消上下文, index 标引索引, data 完整XML
|
||||||
|
// 返回: error 索引、解析、引用、提交或取消错误
|
||||||
|
func (e *Editor) UpdateStructureXMLContext(ctx context.Context, index int, data []byte) error {
|
||||||
|
return e.editStructureContext(ctx, index, func([]byte, *editorXML, CustomTag) ([]byte, error) {
|
||||||
|
return bytes.Clone(data), nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteStructure 删除标引入口,不删除正文或其他入口共享的资源
|
// DeleteStructure 删除标引入口,不删除正文或其他入口共享的资源
|
||||||
@@ -264,6 +308,16 @@ func (e *Editor) SetStructureReferences(index int, path []int, references []Stru
|
|||||||
// 入参: base 编辑包快照, parts 本次输出条目,可为nil
|
// 入参: base 编辑包快照, parts 本次输出条目,可为nil
|
||||||
// 返回: string 索引文件路径, []byte 原文, *editorXML 容器, []*editorXML 标引入口, error 读取错误
|
// 返回: string 索引文件路径, []byte 原文, *editorXML 容器, []*editorXML 标引入口, error 读取错误
|
||||||
func editorStructureIndex(base *editorSource, parts map[string][]byte) (string, []byte, *editorXML, []*editorXML, error) {
|
func editorStructureIndex(base *editorSource, parts map[string][]byte) (string, []byte, *editorXML, []*editorXML, error) {
|
||||||
|
return editorStructureIndexContext(context.Background(), base, parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorStructureIndexContext 读取或准备标引索引,文件读取及解析期间响应取消
|
||||||
|
// 入参: ctx 取消上下文, base 编辑包快照, parts 本次输出条目,可为nil
|
||||||
|
// 返回: string 索引路径, []byte 原文, *editorXML 容器, []*editorXML 入口, error 读取或取消错误
|
||||||
|
func editorStructureIndexContext(ctx context.Context, base *editorSource, parts map[string][]byte) (string, []byte, *editorXML, []*editorXML, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", nil, nil, nil, err
|
||||||
|
}
|
||||||
name := base.reader.documentRoot
|
name := base.reader.documentRoot
|
||||||
if base.document.CustomTags.Path != "" {
|
if base.document.CustomTags.Path != "" {
|
||||||
name = base.reader.ResPath(base.document.CustomTags.Path)
|
name = base.reader.ResPath(base.document.CustomTags.Path)
|
||||||
@@ -271,12 +325,12 @@ func editorStructureIndex(base *editorSource, parts map[string][]byte) (string,
|
|||||||
data, ok := parts[name]
|
data, ok := parts[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
var err error
|
var err error
|
||||||
data, err = base.reader.readFile(name)
|
data, err = base.reader.readFileViewContext(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, nil, nil, err
|
return "", nil, nil, nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, nil, nil, err
|
return "", nil, nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -286,7 +340,7 @@ func editorStructureIndex(base *editorSource, parts map[string][]byte) (string,
|
|||||||
name = packageAvailableName(base.reader, nil, base.reader.RootDir+"/CustomTags.xml")
|
name = packageAvailableName(base.reader, nil, base.reader.RootDir+"/CustomTags.xml")
|
||||||
data, err = root.containerXML("CustomTags", nil, nil)
|
data, err = root.containerXML("CustomTags", nil, nil)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
root, err = parseEditorXML(data)
|
root, err = parseEditorXMLContext(ctx, data)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, nil, nil, err
|
return "", nil, nil, nil, err
|
||||||
@@ -321,10 +375,20 @@ func editorStructureNode(root *editorXML, path []int) (*editorXML, error) {
|
|||||||
// 入参: index 标引索引, edit 内容变换
|
// 入参: index 标引索引, edit 内容变换
|
||||||
// 返回: error 校验、解析或提交错误
|
// 返回: error 校验、解析或提交错误
|
||||||
func (e *Editor) editStructure(index int, edit func([]byte, *editorXML, CustomTag) ([]byte, error)) error {
|
func (e *Editor) editStructure(index int, edit func([]byte, *editorXML, CustomTag) ([]byte, error)) error {
|
||||||
|
return e.editStructureContext(context.Background(), index, edit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// editStructureContext 校验并提交标引编辑,快照读取及引用遍历期间响应取消
|
||||||
|
// 入参: ctx 取消上下文, index 标引索引, edit 内容变换
|
||||||
|
// 返回: error 校验、解析、提交或取消错误
|
||||||
|
func (e *Editor) editStructureContext(ctx context.Context, index int, edit func([]byte, *editorXML, CustomTag) ([]byte, error)) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if e.source == nil {
|
if e.source == nil {
|
||||||
return fmt.Errorf("custom tag structure not found: %d", index)
|
return fmt.Errorf("custom tag structure not found: %d", index)
|
||||||
}
|
}
|
||||||
tags, err := e.source.reader.CustomTags()
|
tags, err := e.source.reader.CustomTagsContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -333,45 +397,51 @@ func (e *Editor) editStructure(index int, edit func([]byte, *editorXML, CustomTa
|
|||||||
}
|
}
|
||||||
tag := tags[index]
|
tag := tags[index]
|
||||||
name := e.source.reader.ResPath(tag.FileLoc)
|
name := e.source.reader.ResPath(tag.FileLoc)
|
||||||
snapshot, err := e.Reader()
|
snapshot, err := e.ReaderContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer snapshot.Close()
|
defer snapshot.Close()
|
||||||
current, err := snapshot.CustomTags()
|
current, err := snapshot.CustomTagsContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if index >= len(current) {
|
if index >= len(current) {
|
||||||
return fmt.Errorf("custom tag structure not found: %d", index)
|
return fmt.Errorf("custom tag structure not found: %d", index)
|
||||||
}
|
}
|
||||||
data, err := snapshot.readFile(snapshot.ResPath(current[index].FileLoc))
|
data, err := snapshot.readFileViewContext(ctx, snapshot.ResPath(current[index].FileLoc))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := parseStructure(context.Background(), data, tag); err != nil {
|
if _, err := parseStructure(ctx, data, tag); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
updated, err := edit(data, root, tag)
|
updated, err := edit(data, root, tag)
|
||||||
if err != nil || bytes.Equal(data, updated) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tree, err := parseStructure(context.Background(), updated, tag)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
objects, err := e.validateStructureReferences(tree, tag, snapshot)
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if bytes.Equal(data, updated) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tree, err := parseStructure(ctx, updated, tag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
objects, err := e.validateStructureReferencesContext(ctx, tree, tag, snapshot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
parts := make(map[string][]byte)
|
parts := make(map[string][]byte)
|
||||||
for i, other := range tags {
|
for i, other := range tags {
|
||||||
if i != index && e.source.reader.ResPath(other.FileLoc) == name {
|
if i != index && e.source.reader.ResPath(other.FileLoc) == name {
|
||||||
indexName, indexData, _, entries, err := editorStructureIndex(e.source, nil)
|
indexName, indexData, _, entries, err := editorStructureIndexContext(ctx, e.source, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -387,13 +457,20 @@ func (e *Editor) editStructure(index int, edit func([]byte, *editorXML, CustomTa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
parts[name] = updated
|
parts[name] = updated
|
||||||
return e.commitStructureParts(e.source, parts, objects, index)
|
return e.commitStructurePartsContext(ctx, e.source, parts, objects, index)
|
||||||
}
|
}
|
||||||
|
|
||||||
// commitStructureParts 提交标引并记录有效目标及已应用的编号迁移,供后续编辑维护引用
|
// commitStructureParts 提交标引并记录有效目标及已应用的编号迁移,供后续编辑维护引用
|
||||||
// 入参: base 原包, parts 修改条目, objects 按页面编号记录的有效图元, index 标引入口序号
|
// 入参: base 原包, parts 修改条目, objects 按页面编号记录的有效图元, index 标引入口序号
|
||||||
// 返回: error 提交错误
|
// 返回: error 提交错误
|
||||||
func (e *Editor) commitStructureParts(base *editorSource, parts map[string][]byte, objects map[string]map[string]bool, index int) error {
|
func (e *Editor) commitStructureParts(base *editorSource, parts map[string][]byte, objects map[string]map[string]bool, index int) error {
|
||||||
|
return e.commitStructurePartsContext(context.Background(), base, parts, objects, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// commitStructurePartsContext 准备标引引用记录并原子提交,取消时保留原编辑状态
|
||||||
|
// 入参: ctx 取消上下文, base 原包, parts 修改条目, objects 有效图元, index 标引入口序号
|
||||||
|
// 返回: error 提交或取消错误
|
||||||
|
func (e *Editor) commitStructurePartsContext(ctx context.Context, base *editorSource, parts map[string][]byte, objects map[string]map[string]bool, index int) error {
|
||||||
next := *base
|
next := *base
|
||||||
if len(e.structureRemaps) != 0 {
|
if len(e.structureRemaps) != 0 {
|
||||||
next.structureStarts = maps.Clone(base.structureStarts)
|
next.structureStarts = maps.Clone(base.structureStarts)
|
||||||
@@ -407,21 +484,34 @@ func (e *Editor) commitStructureParts(base *editorSource, parts map[string][]byt
|
|||||||
next.structureObjects = make(map[string]map[string]bool)
|
next.structureObjects = make(map[string]map[string]bool)
|
||||||
}
|
}
|
||||||
for page, ids := range objects {
|
for page, ids := range objects {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
known := make(map[string]bool, len(next.structureObjects[page])+len(ids))
|
known := make(map[string]bool, len(next.structureObjects[page])+len(ids))
|
||||||
maps.Copy(known, next.structureObjects[page])
|
maps.Copy(known, next.structureObjects[page])
|
||||||
maps.Copy(known, ids)
|
maps.Copy(known, ids)
|
||||||
next.structureObjects[page] = known
|
next.structureObjects[page] = known
|
||||||
}
|
}
|
||||||
return e.commitAnnotationParts(&next, parts)
|
return e.commitAnnotationPartsContext(ctx, &next, parts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateStructureReferences 检查标引目标是否位于当前文档和版本,不依赖字体或图像解码
|
// validateStructureReferences 检查标引目标是否位于当前文档和版本,不依赖字体或图像解码
|
||||||
// 入参: root 待提交标引树, tag 标引入口, r 当前操作的编辑快照,为nil时按需创建
|
// 入参: root 待提交标引树, tag 标引入口, r 当前操作的编辑快照,为nil时按需创建
|
||||||
// 返回: map[string]map[string]bool 已校验目标, error 页面或图元缺失错误
|
// 返回: map[string]map[string]bool 已校验目标, error 页面或图元缺失错误
|
||||||
func (e *Editor) validateStructureReferences(root *StructureNode, tag CustomTag, r *Reader) (map[string]map[string]bool, error) {
|
func (e *Editor) validateStructureReferences(root *StructureNode, tag CustomTag, r *Reader) (map[string]map[string]bool, error) {
|
||||||
|
return e.validateStructureReferencesContext(context.Background(), root, tag, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateStructureReferencesContext 校验当前文档及版本的引用,遍历及快照读取期间响应取消
|
||||||
|
// 入参: ctx 取消上下文, root 标引树, tag 标引入口, r 可复用的编辑快照
|
||||||
|
// 返回: map[string]map[string]bool 已校验目标, error 引用或取消错误
|
||||||
|
func (e *Editor) validateStructureReferencesContext(ctx context.Context, root *StructureNode, tag CustomTag, r *Reader) (map[string]map[string]bool, error) {
|
||||||
var refs []StructureReference
|
var refs []StructureReference
|
||||||
var collect func(*StructureNode, bool) error
|
var collect func(*StructureNode, bool) error
|
||||||
collect = func(node *StructureNode, official bool) error {
|
collect = func(node *StructureNode, official bool) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
official = official && node.Namespace == root.Namespace
|
official = official && node.Namespace == root.Namespace
|
||||||
if official && node.Name == "ObjectRef" && len(node.Children) != 0 {
|
if official && node.Name == "ObjectRef" && len(node.Children) != 0 {
|
||||||
return fmt.Errorf("ObjectRef cannot contain child nodes")
|
return fmt.Errorf("ObjectRef cannot contain child nodes")
|
||||||
@@ -442,28 +532,32 @@ func (e *Editor) validateStructureReferences(root *StructureNode, tag CustomTag,
|
|||||||
}
|
}
|
||||||
if r == nil {
|
if r == nil {
|
||||||
var err error
|
var err error
|
||||||
r, err = e.Reader()
|
r, err = e.ReaderContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
}
|
}
|
||||||
doc, err := r.Doc()
|
doc, err := r.DocContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
renderer := e.newRenderer(r)
|
renderer := e.newRenderer(r)
|
||||||
|
renderer.pageContext = ctx
|
||||||
pages := signaturePageNumbers(doc)
|
pages := signaturePageNumbers(doc)
|
||||||
checked := make(map[int]map[string]bool)
|
checked := make(map[int]map[string]bool)
|
||||||
result := make(map[string]map[string]bool)
|
result := make(map[string]map[string]bool)
|
||||||
for _, ref := range refs {
|
for _, ref := range refs {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
page, _ := resourceValue(pages, ref.PageID)
|
page, _ := resourceValue(pages, ref.PageID)
|
||||||
if page == 0 {
|
if page == 0 {
|
||||||
return nil, fmt.Errorf("custom tag page not found: %s", ref.PageID)
|
return nil, fmt.Errorf("custom tag page not found: %s", ref.PageID)
|
||||||
}
|
}
|
||||||
objects := checked[page]
|
objects := checked[page]
|
||||||
if objects == nil {
|
if objects == nil {
|
||||||
content, err := r.PageContentByIndex(page - 1)
|
content, err := r.PageContentByIndexContext(ctx, page-1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,11 +49,15 @@ type DocInfo struct {
|
|||||||
Abstract string `xml:"Abstract"`
|
Abstract string `xml:"Abstract"`
|
||||||
CreationDate string `xml:"CreationDate"`
|
CreationDate string `xml:"CreationDate"`
|
||||||
ModDate string `xml:"ModDate"`
|
ModDate string `xml:"ModDate"`
|
||||||
|
Keywords Keywords `xml:"Keywords,omitempty"`
|
||||||
Creator string `xml:"Creator"`
|
Creator string `xml:"Creator"`
|
||||||
CreatorVersion string `xml:"CreatorVersion"`
|
CreatorVersion string `xml:"CreatorVersion"`
|
||||||
CustomDatas *CustomDatas `xml:"CustomDatas"`
|
CustomDatas *CustomDatas `xml:"CustomDatas"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keywords 保存有序关键词,保留重复及空文本,空集合不生成XML节点
|
||||||
|
type Keywords []string
|
||||||
|
|
||||||
// CustomDatas 自定义数据集合
|
// CustomDatas 自定义数据集合
|
||||||
type CustomDatas struct {
|
type CustomDatas struct {
|
||||||
CustomData []CustomData `xml:"CustomData"`
|
CustomData []CustomData `xml:"CustomData"`
|
||||||
@@ -129,6 +133,8 @@ func (info *DocInfo) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
|
|||||||
target = &info.CreationDate
|
target = &info.CreationDate
|
||||||
case "ModDate":
|
case "ModDate":
|
||||||
target = &info.ModDate
|
target = &info.ModDate
|
||||||
|
case "Keywords":
|
||||||
|
target = &info.Keywords
|
||||||
case "Creator":
|
case "Creator":
|
||||||
target = &info.Creator
|
target = &info.Creator
|
||||||
case "CreatorVersion":
|
case "CreatorVersion":
|
||||||
@@ -142,6 +148,43 @@ func (info *DocInfo) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalXML 按原顺序读取标准关键词,跳过外部扩展条目
|
||||||
|
// 入参: d XML解码器, start 起始节点
|
||||||
|
// 返回: error 解析错误
|
||||||
|
func (keywords *Keywords) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||||
|
*keywords = nil
|
||||||
|
return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error {
|
||||||
|
if node.Name.Local != "Keyword" {
|
||||||
|
return d.Skip()
|
||||||
|
}
|
||||||
|
var value string
|
||||||
|
if err := d.DecodeElement(&value, &node); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*keywords = append(*keywords, value)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalXML 编码非空关键词集合,子节点沿用集合的命名空间
|
||||||
|
// 入参: e XML编码器, start 起始节点
|
||||||
|
// 返回: error 编码错误
|
||||||
|
func (keywords Keywords) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||||
|
if len(keywords) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := e.EncodeToken(start); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, value := range keywords {
|
||||||
|
child := xml.StartElement{Name: xml.Name{Space: start.Name.Space, Local: "Keyword"}}
|
||||||
|
if err := e.EncodeElement(value, child); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.EncodeToken(start.End())
|
||||||
|
}
|
||||||
|
|
||||||
// UnmarshalXML 按原顺序读取OFD自定义字段,跳过外部扩展条目
|
// UnmarshalXML 按原顺序读取OFD自定义字段,跳过外部扩展条目
|
||||||
// 入参: d XML解码器, start 起始节点
|
// 入参: d XML解码器, start 起始节点
|
||||||
// 返回: error 解析错误
|
// 返回: error 解析错误
|
||||||
|
|||||||
+211
-17
@@ -18,6 +18,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"slices"
|
||||||
|
"sort"
|
||||||
|
|
||||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
@@ -79,11 +81,14 @@ type pdfImporter struct {
|
|||||||
symbolFonts map[*pdfgo.Font]*pdfImportedFont
|
symbolFonts map[*pdfgo.Font]*pdfImportedFont
|
||||||
fontWarnings map[string]bool
|
fontWarnings map[string]bool
|
||||||
clipTexts map[*pdfgo.TextClip][]TextObject
|
clipTexts map[*pdfgo.TextClip][]TextObject
|
||||||
|
textOrigins map[*pdfgo.ReplacementText]pdfObjectOrigin
|
||||||
clipPaths *renderCache[[32]byte, pdfClipPath]
|
clipPaths *renderCache[[32]byte, pdfClipPath]
|
||||||
cmykSpace string
|
cmykSpace string
|
||||||
rgbSpace string
|
rgbSpace string
|
||||||
iccSpaces map[pdfICCKey]string
|
iccSpaces map[pdfICCKey]string
|
||||||
objects *pdfObjectBuffer
|
objects *pdfObjectBuffer
|
||||||
|
structureBindings *pdfStructureBindings
|
||||||
|
annotationBindings []pdfStructureObjectBinding
|
||||||
formResources *pdfFormCache
|
formResources *pdfFormCache
|
||||||
validation *editorValidation
|
validation *editorValidation
|
||||||
pages map[pdfgo.Reference]*pdfgo.Page
|
pages map[pdfgo.Reference]*pdfgo.Page
|
||||||
@@ -103,14 +108,14 @@ type pdfImporter struct {
|
|||||||
pageBox pdfgo.Rectangle
|
pageBox pdfgo.Rectangle
|
||||||
pageWidth float64
|
pageWidth float64
|
||||||
pageHeight float64
|
pageHeight float64
|
||||||
pendingPath *pdfgo.PathMark
|
|
||||||
warning func(pdfgo.Diagnostic)
|
warning func(pdfgo.Diagnostic)
|
||||||
rasterDPI float64
|
rasterDPI float64
|
||||||
rasterWarned bool
|
rasterWarned bool
|
||||||
compositeCache *pdfCompositeCache
|
compositeCache *pdfCompositeCache
|
||||||
compositeNodes []pdfCompositeNode
|
compositeNodes []pdfCompositeNode
|
||||||
compositeSpace *pdfgo.ColorSpace
|
compositeSpace *pdfgo.ColorSpace
|
||||||
patternCells *renderCache[pdfPatternCellKey, *Pattern]
|
patternCells *renderCache[pdfPatternCellKey, pdfPatternCell]
|
||||||
|
patternOrigins map[*Pattern]pdfObjectOrigin
|
||||||
annotationData []byte
|
annotationData []byte
|
||||||
resolveFile pdfgo.FileResolver
|
resolveFile pdfgo.FileResolver
|
||||||
resolveReference pdfgo.ReferenceResolver
|
resolveReference pdfgo.ReferenceResolver
|
||||||
@@ -125,9 +130,78 @@ type pdfImporter struct {
|
|||||||
// pdfObjectBuffer 分块收集导入对象,提交前只整理一次连续数据
|
// pdfObjectBuffer 分块收集导入对象,提交前只整理一次连续数据
|
||||||
type pdfObjectBuffer struct {
|
type pdfObjectBuffer struct {
|
||||||
parts [][]GraphicObject
|
parts [][]GraphicObject
|
||||||
|
sequences []pdfObjectSequence
|
||||||
size int
|
size int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pdfObjectSequence 记录连续对象的来源区段,仅在区段变化时追加
|
||||||
|
type pdfObjectSequence struct {
|
||||||
|
start int
|
||||||
|
pdfObjectOrigin
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfObjectOrigin 保存图元的来源区段,混合区域同时保留参与合成的区段
|
||||||
|
type pdfObjectOrigin struct {
|
||||||
|
sequence *pdfgo.ContentSequence
|
||||||
|
related []*pdfgo.ContentSequence
|
||||||
|
}
|
||||||
|
|
||||||
|
// pdfOriginSet 按首次出现顺序收集来源,仅在出现不同区段时分配去重表
|
||||||
|
type pdfOriginSet struct {
|
||||||
|
origin pdfObjectOrigin
|
||||||
|
seen map[*pdfgo.ContentSequence]bool
|
||||||
|
started bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// add 记录参与绘制的区段,空区段表示未标记内容
|
||||||
|
// 入参: sequence 来源区段
|
||||||
|
func (s *pdfOriginSet) add(sequence *pdfgo.ContentSequence) {
|
||||||
|
if !s.started {
|
||||||
|
s.origin.sequence, s.started = sequence, true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if sequence == s.origin.sequence || s.seen[sequence] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.seen == nil {
|
||||||
|
s.seen = make(map[*pdfgo.ContentSequence]bool)
|
||||||
|
}
|
||||||
|
s.seen[sequence] = true
|
||||||
|
s.origin.related = append(s.origin.related, sequence)
|
||||||
|
}
|
||||||
|
|
||||||
|
// equal 比较来源及其顺序,不合并不同来源的图元
|
||||||
|
// 入参: other 待比较来源
|
||||||
|
// 返回: bool 是否相同
|
||||||
|
func (o pdfObjectOrigin) equal(other pdfObjectOrigin) bool {
|
||||||
|
return o.sequence == other.sequence && slices.Equal(o.related, other.related)
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge 合并参与混色的来源,不修改已有区段切片
|
||||||
|
// 入参: other 另一来源
|
||||||
|
// 返回: pdfObjectOrigin 去重后的来源
|
||||||
|
func (o pdfObjectOrigin) merge(other pdfObjectOrigin) pdfObjectOrigin {
|
||||||
|
if o.equal(other) {
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
result := o
|
||||||
|
copied := false
|
||||||
|
appendSequence := func(sequence *pdfgo.ContentSequence) {
|
||||||
|
if sequence != result.sequence && !slices.Contains(result.related, sequence) {
|
||||||
|
if !copied {
|
||||||
|
result.related = slices.Clone(result.related)
|
||||||
|
copied = true
|
||||||
|
}
|
||||||
|
result.related = append(result.related, sequence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appendSequence(other.sequence)
|
||||||
|
for _, sequence := range other.related {
|
||||||
|
appendSequence(sequence)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果
|
// ImportPDF 将PDF内容转换为独立OFD编辑文档,失败时不返回部分结果
|
||||||
// 入参: ctx 取消上下文, source PDF数据, size 字节数, options 转换选项
|
// 入参: ctx 取消上下文, source PDF数据, size 字节数, options 转换选项
|
||||||
// 返回: *Editor 编辑文档, PDFImportReport 转换统计, error 错误信息
|
// 返回: *Editor 编辑文档, PDFImportReport 转换统计, error 错误信息
|
||||||
@@ -159,7 +233,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
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{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{}}
|
importer := pdfImporter{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{}}
|
||||||
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 diagnostics []pdfgo.Diagnostic
|
||||||
var readWarning func(pdfgo.Diagnostic)
|
var readWarning func(pdfgo.Diagnostic)
|
||||||
@@ -173,6 +247,9 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
importer.reader = reader
|
importer.reader = reader
|
||||||
|
if err := importer.prepareStructure(ctx); err != nil {
|
||||||
|
return nil, PDFImportReport{}, fmt.Errorf("import PDF structure: %w", err)
|
||||||
|
}
|
||||||
outputIntents, err := reader.OutputIntents()
|
outputIntents, err := reader.OutputIntents()
|
||||||
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)
|
||||||
@@ -192,12 +269,6 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
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 security := reader.Encryption(); security != nil && !security.Owner && security.Permissions&0xf3c != 0xf3c {
|
|
||||||
if options.Strict {
|
|
||||||
return nil, PDFImportReport{}, &pdfgo.UnsupportedError{Feature: "PDF access permission conversion"}
|
|
||||||
}
|
|
||||||
importer.report.Warnings = append(importer.report.Warnings, pdfgo.Diagnostic{Message: "PDF access permissions not transferred to OFD"})
|
|
||||||
}
|
|
||||||
if !options.Strict {
|
if !options.Strict {
|
||||||
importer.warning = func(warning pdfgo.Diagnostic) {
|
importer.warning = func(warning pdfgo.Diagnostic) {
|
||||||
warning.Page = importer.page + 1
|
warning.Page = importer.page + 1
|
||||||
@@ -224,6 +295,9 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
}
|
}
|
||||||
if len(editor.pages) != 0 {
|
if len(editor.pages) != 0 {
|
||||||
importer.page = -1
|
importer.page = -1
|
||||||
|
if err := importer.documentMetadata(ctx); err != nil {
|
||||||
|
return nil, PDFImportReport{}, fmt.Errorf("import PDF metadata: %w", err)
|
||||||
|
}
|
||||||
if err := importer.documentAttachments(ctx); err != nil {
|
if err := importer.documentAttachments(ctx); err != nil {
|
||||||
return nil, PDFImportReport{}, fmt.Errorf("import PDF attachments: %w", err)
|
return nil, PDFImportReport{}, fmt.Errorf("import PDF attachments: %w", err)
|
||||||
}
|
}
|
||||||
@@ -251,27 +325,24 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
importer.compositeNodes = nil
|
importer.compositeNodes = nil
|
||||||
importer.compositeSpace = nil
|
importer.compositeSpace = nil
|
||||||
importer.patternCells = nil
|
importer.patternCells = nil
|
||||||
|
importer.patternOrigins = nil
|
||||||
importer.clipTexts = make(map[*pdfgo.TextClip][]TextObject)
|
importer.clipTexts = make(map[*pdfgo.TextClip][]TextObject)
|
||||||
|
importer.textOrigins = nil
|
||||||
importer.clipPaths = nil
|
importer.clipPaths = nil
|
||||||
importer.annotationData = nil
|
importer.annotationData = nil
|
||||||
|
importer.annotationBindings = nil
|
||||||
if err := importer.pageOpenActions(ctx, page); err != nil {
|
if err := importer.pageOpenActions(ctx, page); err != nil {
|
||||||
return fmt.Errorf("import PDF page %d actions: %w", index+1, err)
|
return fmt.Errorf("import PDF page %d actions: %w", index+1, err)
|
||||||
}
|
}
|
||||||
if err := importer.compositePage(nil, func(visitor pdfgo.Visitor) error { return reader.WalkPage(ctx, page, visitor) }); err != nil {
|
if err := importer.compositePage(nil, func(visitor pdfgo.Visitor) error { return reader.WalkPage(ctx, page, visitor) }); err != nil {
|
||||||
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
||||||
}
|
}
|
||||||
if err := importer.flushPath(); err != nil {
|
|
||||||
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
|
||||||
}
|
|
||||||
if err := importer.annotations(ctx, page, options.Strict); err != nil {
|
if err := importer.annotations(ctx, page, options.Strict); err != nil {
|
||||||
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
||||||
}
|
}
|
||||||
if len(importer.annotationData) != 0 {
|
if err := importer.commitAnnotations(); err != nil {
|
||||||
if err := editor.appendAnnotations(index, importer.annotationData, true); err != nil {
|
|
||||||
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
||||||
}
|
}
|
||||||
importer.annotationData = nil
|
|
||||||
}
|
|
||||||
if err := importer.commitObjects(); err != nil {
|
if err := importer.commitObjects(); err != nil {
|
||||||
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
return fmt.Errorf("import PDF page %d: %w", index+1, err)
|
||||||
}
|
}
|
||||||
@@ -292,6 +363,9 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
|||||||
if importer.report.Pages == 0 {
|
if importer.report.Pages == 0 {
|
||||||
return nil, PDFImportReport{}, fmt.Errorf("PDF contains no pages")
|
return nil, PDFImportReport{}, fmt.Errorf("PDF contains no pages")
|
||||||
}
|
}
|
||||||
|
if err := importer.documentStructures(ctx); err != nil {
|
||||||
|
return nil, PDFImportReport{}, fmt.Errorf("import PDF structure: %w", err)
|
||||||
|
}
|
||||||
return editor, importer.report, nil
|
return editor, importer.report, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,15 +420,60 @@ func (p *pdfImporter) objectValidation() *editorValidation {
|
|||||||
// appendObject 追加本次导入对象,按需建立独立暂存缓冲
|
// appendObject 追加本次导入对象,按需建立独立暂存缓冲
|
||||||
// 入参: object 独立导入对象
|
// 入参: object 独立导入对象
|
||||||
func (p *pdfImporter) appendObject(object GraphicObject) {
|
func (p *pdfImporter) appendObject(object GraphicObject) {
|
||||||
|
p.appendSequenceObject(object, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendSequenceObject 追加对象并保留来源区段,供复合对象优化核对边界
|
||||||
|
// 入参: object 独立导入对象, sequence 来源区段
|
||||||
|
func (p *pdfImporter) appendSequenceObject(object GraphicObject, sequence *pdfgo.ContentSequence) {
|
||||||
|
p.appendOriginObject(object, pdfObjectOrigin{sequence: sequence})
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendOriginObject 追加对象并保留全部来源区段
|
||||||
|
// 入参: object 独立导入对象, origin 来源区段
|
||||||
|
func (p *pdfImporter) appendOriginObject(object GraphicObject, origin pdfObjectOrigin) {
|
||||||
|
if len(p.patternOrigins) != 0 {
|
||||||
|
var fill *FillColor
|
||||||
|
var stroke *StrokeColor
|
||||||
|
switch object.Type {
|
||||||
|
case "PathObject":
|
||||||
|
fill, stroke = object.PathObject.FillColor, object.PathObject.StrokeColor
|
||||||
|
case "TextObject":
|
||||||
|
fill, stroke = object.TextObject.FillColor, object.TextObject.StrokeColor
|
||||||
|
}
|
||||||
|
if fill != nil {
|
||||||
|
if source, ok := p.patternOrigins[fill.Pattern]; ok {
|
||||||
|
origin = origin.merge(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stroke != nil {
|
||||||
|
if source, ok := p.patternOrigins[stroke.Pattern]; ok {
|
||||||
|
origin = origin.merge(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if p.objects == nil {
|
if p.objects == nil {
|
||||||
p.objects = &pdfObjectBuffer{}
|
p.objects = &pdfObjectBuffer{}
|
||||||
}
|
}
|
||||||
p.objects.append(object)
|
p.objects.appendOrigin(object, origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// append 追加对象,小批量连续存储,超过阈值后不复制前序片段
|
// append 追加对象,小批量连续存储,超过阈值后不复制前序片段
|
||||||
// 入参: object 独立对象
|
// 入参: object 独立对象
|
||||||
func (b *pdfObjectBuffer) append(object GraphicObject) {
|
func (b *pdfObjectBuffer) append(object GraphicObject) {
|
||||||
|
b.appendSequence(object, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendSequence 按区段连续存储对象,不为未标记内容分配区段记录
|
||||||
|
// 入参: object 独立对象, sequence 来源区段
|
||||||
|
func (b *pdfObjectBuffer) appendSequence(object GraphicObject, sequence *pdfgo.ContentSequence) {
|
||||||
|
b.appendOrigin(object, pdfObjectOrigin{sequence: sequence})
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendOrigin 按来源连续存储对象,混合区域保留多个区段
|
||||||
|
// 入参: object 独立对象, origin 来源区段
|
||||||
|
func (b *pdfObjectBuffer) appendOrigin(object GraphicObject, origin pdfObjectOrigin) {
|
||||||
|
b.recordOrigin(b.size, origin)
|
||||||
index := len(b.parts) - 1
|
index := len(b.parts) - 1
|
||||||
if index == 0 && len(b.parts[0]) == cap(b.parts[0]) && cap(b.parts[0]) < pdfObjectChunkLimit {
|
if index == 0 && len(b.parts[0]) == cap(b.parts[0]) && cap(b.parts[0]) < pdfObjectChunkLimit {
|
||||||
part := b.parts[0]
|
part := b.parts[0]
|
||||||
@@ -374,6 +493,66 @@ func (b *pdfObjectBuffer) append(object GraphicObject) {
|
|||||||
b.size++
|
b.size++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recordOrigin 按递增对象位置记录完整来源变化
|
||||||
|
// 入参: index 对象位置, origin 来源区段
|
||||||
|
func (b *pdfObjectBuffer) recordOrigin(index int, origin pdfObjectOrigin) {
|
||||||
|
var previous pdfObjectOrigin
|
||||||
|
if len(b.sequences) != 0 {
|
||||||
|
previous = b.sequences[len(b.sequences)-1].pdfObjectOrigin
|
||||||
|
}
|
||||||
|
if !previous.equal(origin) {
|
||||||
|
b.sequences = append(b.sequences, pdfObjectSequence{start: index, pdfObjectOrigin: origin})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// origin 获取暂存对象的全部来源,未标记对象或空缓冲返回空来源
|
||||||
|
// 入参: index 对象位置
|
||||||
|
// 返回: pdfObjectOrigin 来源区段
|
||||||
|
func (b *pdfObjectBuffer) origin(index int) pdfObjectOrigin {
|
||||||
|
if b == nil {
|
||||||
|
return pdfObjectOrigin{}
|
||||||
|
}
|
||||||
|
next := sort.Search(len(b.sequences), func(i int) bool { return b.sequences[i].start > index })
|
||||||
|
if next == 0 {
|
||||||
|
return pdfObjectOrigin{}
|
||||||
|
}
|
||||||
|
return b.sequences[next-1].pdfObjectOrigin
|
||||||
|
}
|
||||||
|
|
||||||
|
// combinedOrigin 收集局部合成的来源区段,按首次出现顺序去重
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: pdfObjectOrigin 合并来源, error 取消错误
|
||||||
|
func (b *pdfObjectBuffer) combinedOrigin(ctx context.Context) (pdfObjectOrigin, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return pdfObjectOrigin{}, err
|
||||||
|
}
|
||||||
|
if b.len() == 0 {
|
||||||
|
return pdfObjectOrigin{}, nil
|
||||||
|
}
|
||||||
|
origin := b.origin(0)
|
||||||
|
if len(b.sequences) <= 1 && (len(b.sequences) == 0 || b.sequences[0].start == 0) {
|
||||||
|
return origin, nil
|
||||||
|
}
|
||||||
|
var result pdfOriginSet
|
||||||
|
result.add(origin.sequence)
|
||||||
|
for _, sequence := range origin.related {
|
||||||
|
result.add(sequence)
|
||||||
|
}
|
||||||
|
for _, run := range b.sequences {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return pdfObjectOrigin{}, err
|
||||||
|
}
|
||||||
|
result.add(run.sequence)
|
||||||
|
for _, sequence := range run.related {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return pdfObjectOrigin{}, err
|
||||||
|
}
|
||||||
|
result.add(sequence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.origin, nil
|
||||||
|
}
|
||||||
|
|
||||||
// len 获取暂存对象数量,空缓冲返回零
|
// len 获取暂存对象数量,空缓冲返回零
|
||||||
// 返回: int 对象数量
|
// 返回: int 对象数量
|
||||||
func (b *pdfObjectBuffer) len() int {
|
func (b *pdfObjectBuffer) len() int {
|
||||||
@@ -392,6 +571,11 @@ func (b *pdfObjectBuffer) truncate(size int) {
|
|||||||
if size == b.len() {
|
if size == b.len() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
for len(b.sequences) > 0 && b.sequences[len(b.sequences)-1].start >= size {
|
||||||
|
last := len(b.sequences) - 1
|
||||||
|
b.sequences[last] = pdfObjectSequence{}
|
||||||
|
b.sequences = b.sequences[:last]
|
||||||
|
}
|
||||||
remaining := size
|
remaining := size
|
||||||
for index, part := range b.parts {
|
for index, part := range b.parts {
|
||||||
if remaining <= len(part) {
|
if remaining <= len(part) {
|
||||||
@@ -450,9 +634,19 @@ func (p *pdfImporter) commitObjects() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
var bindings []pdfStructureBinding
|
||||||
|
if p.structureBindings != nil {
|
||||||
|
bindings, err = p.objects.structureBindings(p.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := p.objectValidation().appendOwnedObjects(p.ctx, p.page, objects); err != nil {
|
if err := p.objectValidation().appendOwnedObjects(p.ctx, p.page, objects); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if p.structureBindings != nil {
|
||||||
|
p.structureBindings.append(p.editor.pages[p.page].ID, objects, bindings)
|
||||||
|
}
|
||||||
p.objects = nil
|
p.objects = nil
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
+56
-1
@@ -104,7 +104,6 @@ func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, a
|
|||||||
stamp := *p
|
stamp := *p
|
||||||
stamp.matrix = (pdfgo.Matrix{1, 0, 0, 1, -box.X, -box.Y}).Mul(p.matrix)
|
stamp.matrix = (pdfgo.Matrix{1, 0, 0, 1, -box.X, -box.Y}).Mul(p.matrix)
|
||||||
stamp.objects = nil
|
stamp.objects = nil
|
||||||
stamp.pendingPath = nil
|
|
||||||
stamp.report = PDFImportReport{}
|
stamp.report = PDFImportReport{}
|
||||||
stamp.compositeCache = nil
|
stamp.compositeCache = nil
|
||||||
stamp.clipTexts = make(map[*pdfgo.TextClip][]TextObject)
|
stamp.clipTexts = make(map[*pdfgo.TextClip][]TextObject)
|
||||||
@@ -226,6 +225,33 @@ func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, a
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("PDF annotation: %w", err)
|
return fmt.Errorf("PDF annotation: %w", err)
|
||||||
}
|
}
|
||||||
|
var binding pdfStructureObjectBinding
|
||||||
|
if p.structureBindings != nil {
|
||||||
|
binding.content, err = stamp.objects.structureBindings(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if annotation.Reference != (pdfgo.Reference{}) && annotation.Dictionary["StructParent"] != nil {
|
||||||
|
binding.source = pdfStructureObject{reader: p.reader, page: page.Reference, object: annotation.Reference}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if binding.source.object != (pdfgo.Reference{}) || len(binding.content) != 0 {
|
||||||
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if appearance := root.child("Appearance"); appearance != nil {
|
||||||
|
for _, object := range appearance.children {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if id := object.attr("ID"); id != "" {
|
||||||
|
binding.ids = append(binding.ids, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.annotationBindings = append(p.annotationBindings, binding)
|
||||||
|
}
|
||||||
p.annotationData = append(p.annotationData, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
p.annotationData = append(p.annotationData, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
||||||
if len(actions) != 0 && annotation.Subtype == "Link" || len(clicks) != 0 {
|
if len(actions) != 0 && annotation.Subtype == "Link" || len(clicks) != 0 {
|
||||||
p.report.Links++
|
p.report.Links++
|
||||||
@@ -237,6 +263,35 @@ func (p *pdfImporter) paintedAnnotation(ctx context.Context, page *pdfgo.Page, a
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// commitAnnotations 提交注解后登记最终外观标识,失败时保留待提交状态
|
||||||
|
// 返回: error 注解提交错误
|
||||||
|
func (p *pdfImporter) commitAnnotations() error {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(p.annotationData) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := p.editor.appendAnnotations(p.page, p.annotationData, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, binding := range p.annotationBindings {
|
||||||
|
pageID := p.editor.pages[p.page].ID
|
||||||
|
if binding.source.object != (pdfgo.Reference{}) {
|
||||||
|
if p.structureBindings.objects == nil {
|
||||||
|
p.structureBindings.objects = make(map[pdfStructureObject][]StructureReference)
|
||||||
|
}
|
||||||
|
p.structureBindings.objects[binding.source] = append(p.structureBindings.objects[binding.source], StructureReference{PageID: pageID, ObjectIDs: binding.ids})
|
||||||
|
}
|
||||||
|
for _, content := range binding.content {
|
||||||
|
ref := p.structureBindings.reference(pageID, content.sequence)
|
||||||
|
ref.ObjectIDs = append(ref.ObjectIDs, binding.ids[content.start:content.end]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.annotationData, p.annotationBindings = nil, nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// pdfAnnotationType 将具有静态外观的PDF注解类别对应到OFD注解类别
|
// pdfAnnotationType 将具有静态外观的PDF注解类别对应到OFD注解类别
|
||||||
// 入参: subtype PDF注解类别
|
// 入参: subtype PDF注解类别
|
||||||
// 返回: string OFD注解类别,空字符串表示需单独处理
|
// 返回: string OFD注解类别,空字符串表示需单独处理
|
||||||
|
|||||||
+18
-5
@@ -61,11 +61,12 @@ func (p *pdfImporter) clipPath(path pdfgo.Path) (pdfClipPath, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sharedClipObjects 将连续重复的大裁剪区提取为复合对象,保留页面坐标和绘制顺序
|
// sharedClipObjects 将连续重复的大裁剪区提取为复合对象,同步缓冲区及来源区段
|
||||||
// 入参: objects 本库生成且尚未分配标识的对象
|
// 入参: objects 本库生成且尚未分配标识的对象
|
||||||
// 返回: []GraphicObject 提取后的对象, error 资源创建或取消错误
|
// 返回: []GraphicObject 提取后的对象, error 资源创建或取消错误
|
||||||
func (p *pdfImporter) sharedClipObjects(objects []GraphicObject) ([]GraphicObject, error) {
|
func (p *pdfImporter) sharedClipObjects(objects []GraphicObject) ([]GraphicObject, error) {
|
||||||
var output []GraphicObject
|
var output []GraphicObject
|
||||||
|
var mapped pdfObjectBuffer
|
||||||
for start := 0; start < len(objects); {
|
for start := 0; start < len(objects); {
|
||||||
if err := p.ctx.Err(); err != nil {
|
if err := p.ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -73,7 +74,7 @@ func (p *pdfImporter) sharedClipObjects(objects []GraphicObject) ([]GraphicObjec
|
|||||||
clips := pdfPageClip(objects[start])
|
clips := pdfPageClip(objects[start])
|
||||||
end := start + 1
|
end := start + 1
|
||||||
if clips != nil {
|
if clips != nil {
|
||||||
for end < len(objects) && pdfMatchesPageClip(objects[end], clips) {
|
for end < len(objects) && p.objects.origin(start).equal(p.objects.origin(end)) && pdfMatchesPageClip(objects[end], clips) {
|
||||||
if err := p.ctx.Err(); err != nil {
|
if err := p.ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -82,6 +83,7 @@ func (p *pdfImporter) sharedClipObjects(objects []GraphicObject) ([]GraphicObjec
|
|||||||
}
|
}
|
||||||
if end-start < 2 {
|
if end-start < 2 {
|
||||||
if output != nil {
|
if output != nil {
|
||||||
|
mapped.recordOrigin(len(output), p.objects.origin(start))
|
||||||
output = append(output, objects[start])
|
output = append(output, objects[start])
|
||||||
}
|
}
|
||||||
start = end
|
start = end
|
||||||
@@ -89,6 +91,14 @@ func (p *pdfImporter) sharedClipObjects(objects []GraphicObject) ([]GraphicObjec
|
|||||||
}
|
}
|
||||||
if output == nil {
|
if output == nil {
|
||||||
output = append(make([]GraphicObject, 0, len(objects)), objects[:start]...)
|
output = append(make([]GraphicObject, 0, len(objects)), objects[:start]...)
|
||||||
|
if p.objects != nil {
|
||||||
|
for _, run := range p.objects.sequences {
|
||||||
|
if run.start >= start {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
mapped.sequences = append(mapped.sequences, run)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
members := objects[start:end:end]
|
members := objects[start:end:end]
|
||||||
for index := range members {
|
for index := range members {
|
||||||
@@ -98,6 +108,7 @@ func (p *pdfImporter) sharedClipObjects(objects []GraphicObject) ([]GraphicObjec
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
mapped.recordOrigin(len(output), p.objects.origin(start))
|
||||||
output = append(output, GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: CompositeGraphicUnit{
|
output = append(output, GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: CompositeGraphicUnit{
|
||||||
Boundary: pdfBoundary(Box{W: p.pageWidth, H: p.pageHeight}), ResourceID: id, Clips: clips,
|
Boundary: pdfBoundary(Box{W: p.pageWidth, H: p.pageHeight}), ResourceID: id, Clips: clips,
|
||||||
}})
|
}})
|
||||||
@@ -106,6 +117,11 @@ func (p *pdfImporter) sharedClipObjects(objects []GraphicObject) ([]GraphicObjec
|
|||||||
if output == nil {
|
if output == nil {
|
||||||
return objects, nil
|
return objects, nil
|
||||||
}
|
}
|
||||||
|
if p.objects != nil {
|
||||||
|
mapped.parts = [][]GraphicObject{output}
|
||||||
|
mapped.size = len(output)
|
||||||
|
*p.objects = mapped
|
||||||
|
}
|
||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,9 +233,6 @@ func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
|
|||||||
for _, mark := range path.Text {
|
for _, mark := range path.Text {
|
||||||
objects, ok := p.clipTexts[mark]
|
objects, ok := p.clipTexts[mark]
|
||||||
if !ok {
|
if !ok {
|
||||||
if err := p.flushPath(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
count := p.objects.len()
|
count := p.objects.len()
|
||||||
textCount := p.report.TextObjects
|
textCount := p.report.TextObjects
|
||||||
if err := p.text(pdfgo.TextMark{Font: mark.Font, Glyphs: mark.Glyphs, Positions: mark.Positions, Matrix: mark.Matrix, Size: mark.Size, HorizontalScale: mark.HorizontalScale, Mode: 7, Clip: mark, Style: pdfgo.Style{Fill: pdfgo.Paint{Alpha: 1}}}); err != nil {
|
if err := p.text(pdfgo.TextMark{Font: mark.Font, Glyphs: mark.Glyphs, Positions: mark.Positions, Matrix: mark.Matrix, Size: mark.Size, HorizontalScale: mark.HorizontalScale, Mode: 7, Clip: mark, Style: pdfgo.Style{Fill: pdfgo.Paint{Alpha: 1}}}); err != nil {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ type pdfCompositor struct {
|
|||||||
inverse pdfgo.Matrix
|
inverse pdfgo.Matrix
|
||||||
cache *pdfCompositeCache
|
cache *pdfCompositeCache
|
||||||
scratch *pdfCompositeScratch
|
scratch *pdfCompositeScratch
|
||||||
|
origins *pdfOriginSet
|
||||||
masks map[*pdfgo.SoftMask][]float64
|
masks map[*pdfgo.SoftMask][]float64
|
||||||
meshes map[pdfMeshKey][]pdfShadingPixel
|
meshes map[pdfMeshKey][]pdfShadingPixel
|
||||||
gradients map[pdfGradientKey][]pdfShadingPixel
|
gradients map[pdfGradientKey][]pdfShadingPixel
|
||||||
@@ -628,9 +629,6 @@ func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error {
|
|||||||
if p.warning == nil {
|
if p.warning == nil {
|
||||||
return &pdfgo.UnsupportedError{Feature: "page color-space compositing"}
|
return &pdfgo.UnsupportedError{Feature: "page color-space compositing"}
|
||||||
}
|
}
|
||||||
if err := p.flushPath(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := p.compositeRegion(p.compositeNodes, node, space, true); err != nil {
|
if err := p.compositeRegion(p.compositeNodes, node, space, true); err != nil {
|
||||||
return fmt.Errorf("composite graphic %d: %w", i+1, err)
|
return fmt.Errorf("composite graphic %d: %w", i+1, err)
|
||||||
}
|
}
|
||||||
@@ -638,7 +636,7 @@ func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error {
|
|||||||
p.compositeNodes = append(p.compositeNodes, node)
|
p.compositeNodes = append(p.compositeNodes, node)
|
||||||
p.transferBackdrop = p.transferBackdrop || transfer
|
p.transferBackdrop = p.transferBackdrop || transfer
|
||||||
}
|
}
|
||||||
return p.flushPath()
|
return p.ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// opaque 检查图元是否无需与页面背景混合
|
// opaque 检查图元是否无需与页面背景混合
|
||||||
@@ -900,6 +898,7 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
|
|||||||
var colorValues [4]float64
|
var colorValues [4]float64
|
||||||
var colorRGB [3]float64
|
var colorRGB [3]float64
|
||||||
colorReady := false
|
colorReady := false
|
||||||
|
var origins pdfOriginSet
|
||||||
for y := 0; y < h; y += pdfCompositeTileSize {
|
for y := 0; y < h; y += pdfCompositeTileSize {
|
||||||
for x := 0; x < w; x += pdfCompositeTileSize {
|
for x := 0; x < w; x += pdfCompositeTileSize {
|
||||||
if err := p.ctx.Err(); err != nil {
|
if err := p.ctx.Err(); err != nil {
|
||||||
@@ -907,7 +906,7 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
|
|||||||
}
|
}
|
||||||
runtime.Gosched()
|
runtime.Gosched()
|
||||||
width, height := min(pdfCompositeTileSize, w-x), min(pdfCompositeTileSize, h-y)
|
width, height := min(pdfCompositeTileSize, w-x), min(pdfCompositeTileSize, h-y)
|
||||||
c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, scratch: scratch, masks: map[*pdfgo.SoftMask][]float64{}}
|
c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, scratch: scratch, origins: &origins, masks: map[*pdfgo.SoftMask][]float64{}}
|
||||||
if transfers != nil {
|
if transfers != nil {
|
||||||
c.transfers = transfers[:width*height]
|
c.transfers = transfers[:width*height]
|
||||||
c.transferModel = device
|
c.transferModel = device
|
||||||
@@ -983,5 +982,5 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
|
|||||||
c.releaseMasks()
|
c.releaseMasks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return p.appendRasterGroup(output, box, 1)
|
return p.appendRasterGroup(output, box, 1, origins.origin)
|
||||||
}
|
}
|
||||||
@@ -257,6 +257,7 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
var previousValues [4]float64
|
var previousValues [4]float64
|
||||||
var previousAlpha float64
|
var previousAlpha float64
|
||||||
composedReady := false
|
composedReady := false
|
||||||
|
originRecorded := c.origins == nil
|
||||||
for y := region.Min.Y; y < region.Max.Y; y++ {
|
for y := region.Min.Y; y < region.Max.Y; y++ {
|
||||||
if err := c.importer.ctx.Err(); err != nil {
|
if err := c.importer.ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -385,6 +386,21 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
|||||||
if style.AlphaIsShape {
|
if style.AlphaIsShape {
|
||||||
shape, opacity = shape*opacity, 1
|
shape, opacity = shape*opacity, 1
|
||||||
}
|
}
|
||||||
|
if !originRecorded && shape != 0 {
|
||||||
|
switch {
|
||||||
|
case node.path != nil:
|
||||||
|
c.origins.add(node.path.Sequence)
|
||||||
|
case node.text != nil:
|
||||||
|
origin := c.importer.textOrigin(*node.text)
|
||||||
|
c.origins.add(origin.sequence)
|
||||||
|
for _, sequence := range origin.related {
|
||||||
|
c.origins.add(sequence)
|
||||||
|
}
|
||||||
|
case node.image != nil:
|
||||||
|
c.origins.add(node.image.Sequence)
|
||||||
|
}
|
||||||
|
originRecorded = true
|
||||||
|
}
|
||||||
if initial != nil {
|
if initial != nil {
|
||||||
pixel := initial[index]
|
pixel := initial[index]
|
||||||
if err := codecpdf.CompositePaintOver(&pixel, values, opacity, space, mode, compatible, marked); err != nil {
|
if err := codecpdf.CompositePaintOver(&pixel, values, opacity, space, mode, compatible, marked); err != nil {
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cell := pdfCompositor{importer: &pattern.local, box: Box{X: c.box.X + float64(left)*pixelStep - dx, Y: c.box.Y + float64(top)*pixelStep - dy, W: float64(width) * pixelStep, H: float64(height) * pixelStep},
|
cell := pdfCompositor{importer: &pattern.local, box: Box{X: c.box.X + float64(left)*pixelStep - dx, Y: c.box.Y + float64(top)*pixelStep - dy, W: float64(width) * pixelStep, H: float64(height) * pixelStep},
|
||||||
width: width, height: height, inverse: localInverse, cache: pattern.local.compositingCache(), scratch: c.scratch, masks: map[*pdfgo.SoftMask][]float64{}, softMask: c.softMask, channels: cellChannels}
|
width: width, height: height, inverse: localInverse, cache: pattern.local.compositingCache(), scratch: c.scratch, origins: c.origins, masks: map[*pdfgo.SoftMask][]float64{}, softMask: c.softMask, channels: cellChannels}
|
||||||
err := cell.draw(pattern.nodes, pixels, space)
|
err := cell.draw(pattern.nodes, pixels, space)
|
||||||
cell.releaseMasks()
|
cell.releaseMasks()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+54
-8
@@ -19,6 +19,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
|
||||||
|
"github.com/xiaoqidun/pdfgo"
|
||||||
)
|
)
|
||||||
|
|
||||||
// pdfFormBufferLimit 限制表单编码暂存容量,不限制资源大小
|
// pdfFormBufferLimit 限制表单编码暂存容量,不限制资源大小
|
||||||
@@ -74,6 +76,10 @@ func (c *pdfFormCache) reset() {
|
|||||||
// 入参: nodes 普通表单连续图元
|
// 入参: nodes 普通表单连续图元
|
||||||
// 返回: bool 是否已保存为复合对象, error 检查或转换错误
|
// 返回: bool 是否已保存为复合对象, error 检查或转换错误
|
||||||
func (p *pdfImporter) compositeForm(nodes []pdfCompositeNode) (bool, error) {
|
func (p *pdfImporter) compositeForm(nodes []pdfCompositeNode) (bool, error) {
|
||||||
|
sequence, same, err := p.formSequence(nodes)
|
||||||
|
if err != nil || !same {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
if found, err := p.compositeHasTransfer(nodes, p.transferModel); err != nil || found {
|
if found, err := p.compositeHasTransfer(nodes, p.transferModel); err != nil || found {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -86,9 +92,6 @@ func (p *pdfImporter) compositeForm(nodes []pdfCompositeNode) (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := p.flushPath(); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
parent := p.objects
|
parent := p.objects
|
||||||
p.objects = nil
|
p.objects = nil
|
||||||
defer func() { p.objects = parent }()
|
defer func() { p.objects = parent }()
|
||||||
@@ -98,9 +101,6 @@ func (p *pdfImporter) compositeForm(nodes []pdfCompositeNode) (bool, error) {
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := p.flushPath(); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if err := p.ctx.Err(); err != nil {
|
if err := p.ctx.Err(); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -134,9 +134,55 @@ func (p *pdfImporter) compositeForm(nodes []pdfCompositeNode) (bool, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
p.objects = parent
|
p.objects = parent
|
||||||
p.appendObject(GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: CompositeGraphicUnit{
|
p.appendSequenceObject(GraphicObject{Type: "CompositeObject", CompositeGraphicUnit: CompositeGraphicUnit{
|
||||||
Boundary: pdfBoundary(Box{W: p.pageWidth, H: p.pageHeight}), ResourceID: resource.id,
|
Boundary: pdfBoundary(Box{W: p.pageWidth, H: p.pageHeight}), ResourceID: resource.id,
|
||||||
}})
|
}}, sequence)
|
||||||
parent = p.objects
|
parent = p.objects
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formSequence 检查表单图元是否属于同一区段,不跨区段合并可独立引用的内容
|
||||||
|
// 入参: nodes 表单图元
|
||||||
|
// 返回: *pdfgo.ContentSequence 来源区段, bool 是否相同, error 取消错误
|
||||||
|
func (p *pdfImporter) formSequence(nodes []pdfCompositeNode) (*pdfgo.ContentSequence, bool, error) {
|
||||||
|
var sequence *pdfgo.ContentSequence
|
||||||
|
seen := false
|
||||||
|
var visit func([]pdfCompositeNode) bool
|
||||||
|
visit = func(nodes []pdfCompositeNode) bool {
|
||||||
|
for _, node := range nodes {
|
||||||
|
if p.ctx.Err() != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var current *pdfgo.ContentSequence
|
||||||
|
switch {
|
||||||
|
case node.path != nil:
|
||||||
|
current = node.path.Sequence
|
||||||
|
case node.text != nil:
|
||||||
|
origin := p.textOrigin(*node.text)
|
||||||
|
if len(origin.related) != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
current = origin.sequence
|
||||||
|
case node.image != nil:
|
||||||
|
current = node.image.Sequence
|
||||||
|
case node.glyph != nil:
|
||||||
|
current = node.glyph.Sequence
|
||||||
|
default:
|
||||||
|
if !visit(node.children) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if seen && sequence != current {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sequence, seen = current, true
|
||||||
|
if !visit(node.children) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
same := visit(nodes)
|
||||||
|
return sequence, same, p.ctx.Err()
|
||||||
|
}
|
||||||
+2
-33
@@ -21,7 +21,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom"
|
||||||
@@ -351,43 +350,13 @@ func (p *pdfImporter) pathDataContext(ctx context.Context, path pdfgo.Path, orig
|
|||||||
return string(data), nil
|
return string(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// path 合并相同状态下的不透明描边,保留独立子路径与绘制顺序
|
// path 保留每次路径绘制操作,重叠描边不合并为单次绘制
|
||||||
// 入参: mark PDF路径绘制信息
|
// 入参: mark PDF路径绘制信息
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (p *pdfImporter) path(mark pdfgo.PathMark) error {
|
func (p *pdfImporter) path(mark pdfgo.PathMark) error {
|
||||||
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)}
|
||||||
}
|
}
|
||||||
if !mark.Stroke || mark.Fill || mark.Style.Stroke.Alpha != 1 {
|
|
||||||
if err := p.flushPath(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return p.appendPath(mark)
|
|
||||||
}
|
|
||||||
if p.pendingPath != nil && (p.pendingPath.StrokeMatrix != mark.StrokeMatrix || !reflect.DeepEqual(p.pendingPath.Style, mark.Style)) {
|
|
||||||
if err := p.flushPath(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if p.pendingPath == nil {
|
|
||||||
p.pendingPath = &mark
|
|
||||||
} else {
|
|
||||||
p.pendingPath.Path.Segments = append(p.pendingPath.Path.Segments, mark.Path.Segments...)
|
|
||||||
}
|
|
||||||
if len(p.pendingPath.Path.Segments) >= 512 {
|
|
||||||
return p.flushPath()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// flushPath 写入连续描边批次,保持后续图元的叠放位置
|
|
||||||
// 返回: error 转换错误
|
|
||||||
func (p *pdfImporter) flushPath() error {
|
|
||||||
if p.pendingPath == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
mark := *p.pendingPath
|
|
||||||
p.pendingPath = nil
|
|
||||||
return p.appendPath(mark)
|
return p.appendPath(mark)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,7 +461,7 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
|
|||||||
phase := mark.Style.DashPhase * scale
|
phase := mark.Style.DashPhase * scale
|
||||||
object.DashOffset = &phase
|
object.DashOffset = &phase
|
||||||
}
|
}
|
||||||
p.appendObject(GraphicObject{Type: "PathObject", PathObject: object})
|
p.appendSequenceObject(GraphicObject{Type: "PathObject", PathObject: object}, mark.Sequence)
|
||||||
p.report.PathObjects++
|
p.report.PathObjects++
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
+39
-23
@@ -35,6 +35,7 @@ type pdfPaintRegion struct {
|
|||||||
paint pdfgo.Paint
|
paint pdfgo.Paint
|
||||||
path GeometryPath
|
path GeometryPath
|
||||||
intent pdfgo.Name
|
intent pdfgo.Name
|
||||||
|
origin pdfObjectOrigin
|
||||||
}
|
}
|
||||||
|
|
||||||
// group 将单色图元的透明组转换为互不重叠的填充区域,保留矢量与组不透明度
|
// group 将单色图元的透明组转换为互不重叠的填充区域,保留矢量与组不透明度
|
||||||
@@ -47,9 +48,6 @@ func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error
|
|||||||
if mark.BlendMode != "" && mark.BlendMode != "Normal" && mark.BlendMode != "Compatible" {
|
if mark.BlendMode != "" && mark.BlendMode != "Normal" && mark.BlendMode != "Compatible" {
|
||||||
return &pdfgo.UnsupportedError{Feature: "group blend mode " + string(mark.BlendMode)}
|
return &pdfgo.UnsupportedError{Feature: "group blend mode " + string(mark.BlendMode)}
|
||||||
}
|
}
|
||||||
if err := p.flushPath(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if mark.ColorSpace != nil && !mark.ColorSpace.SRGBEquivalent() {
|
if mark.ColorSpace != nil && !mark.ColorSpace.SRGBEquivalent() {
|
||||||
if mark.Alpha != 1 || mark.SoftMask != nil {
|
if mark.Alpha != 1 || mark.SoftMask != nil {
|
||||||
return &pdfgo.UnsupportedError{Feature: "non-sRGB group color space"}
|
return &pdfgo.UnsupportedError{Feature: "non-sRGB group color space"}
|
||||||
@@ -72,10 +70,7 @@ func (p *pdfImporter) group(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if mark.Alpha == 1 {
|
if mark.Alpha == 1 {
|
||||||
if err := walk(visitor); err != nil {
|
return walk(visitor)
|
||||||
return err
|
|
||||||
}
|
|
||||||
return p.flushPath()
|
|
||||||
}
|
}
|
||||||
err := p.groupPaths(mark, walk, visitor)
|
err := p.groupPaths(mark, walk, visitor)
|
||||||
if errors.Is(err, errPDFGroupRaster) && p.warning != nil {
|
if errors.Is(err, errPDFGroupRaster) && p.warning != nil {
|
||||||
@@ -140,6 +135,7 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
|
|||||||
var combined GeometryPath
|
var combined GeometryPath
|
||||||
var paint *pdfgo.Paint
|
var paint *pdfgo.Paint
|
||||||
var intent pdfgo.Name
|
var intent pdfgo.Name
|
||||||
|
var origin pdfObjectOrigin
|
||||||
var clips []pdfgo.Path
|
var clips []pdfgo.Path
|
||||||
opaque := true
|
opaque := true
|
||||||
scale := math.Hypot(p.matrix[0], p.matrix[1])
|
scale := math.Hypot(p.matrix[0], p.matrix[1])
|
||||||
@@ -185,13 +181,15 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
|
|||||||
if paint == nil {
|
if paint == nil {
|
||||||
paint, clips = &color, path.Style.Clips
|
paint, clips = &color, path.Style.Clips
|
||||||
intent = path.Style.RenderingIntent
|
intent = path.Style.RenderingIntent
|
||||||
|
origin = pdfObjectOrigin{sequence: path.Sequence}
|
||||||
} else if !reflect.DeepEqual(clips, path.Style.Clips) {
|
} else if !reflect.DeepEqual(clips, path.Style.Clips) {
|
||||||
return fmt.Errorf("%w: independently clipped group content", errPDFGroupRaster)
|
return fmt.Errorf("%w: independently clipped group content", errPDFGroupRaster)
|
||||||
} else if color.Alpha != 1 || !reflect.DeepEqual(*paint, color) || intent != path.Style.RenderingIntent {
|
} else if color.Alpha != 1 || !reflect.DeepEqual(*paint, color) || intent != path.Style.RenderingIntent {
|
||||||
regions = append(regions, pdfPaintRegion{*paint, combined, intent})
|
regions = append(regions, pdfPaintRegion{*paint, combined, intent, origin})
|
||||||
combined = nil
|
combined = nil
|
||||||
paint = &color
|
paint = &color
|
||||||
intent = path.Style.RenderingIntent
|
intent = path.Style.RenderingIntent
|
||||||
|
origin = pdfObjectOrigin{sequence: path.Sequence}
|
||||||
}
|
}
|
||||||
outline := shape
|
outline := shape
|
||||||
evenOdd := path.Path.EvenOdd
|
evenOdd := path.Path.EvenOdd
|
||||||
@@ -242,8 +240,15 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
|
|||||||
if err := p.ctx.Err(); err != nil {
|
if err := p.ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(outline) != 0 {
|
||||||
|
if len(combined) == 0 {
|
||||||
|
origin = pdfObjectOrigin{sequence: path.Sequence}
|
||||||
|
} else {
|
||||||
|
origin = origin.merge(pdfObjectOrigin{sequence: path.Sequence})
|
||||||
|
}
|
||||||
combined = append(combined, outline...)
|
combined = append(combined, outline...)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := walk(collect); err != nil {
|
if err := walk(collect); err != nil {
|
||||||
@@ -252,10 +257,12 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
|
|||||||
if err := p.ctx.Err(); err != nil {
|
if err := p.ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(combined) == 0 {
|
if len(combined) != 0 {
|
||||||
|
regions = append(regions, pdfPaintRegion{*paint, combined, intent, origin})
|
||||||
|
}
|
||||||
|
if len(regions) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
regions = append(regions, pdfPaintRegion{*paint, combined, intent})
|
|
||||||
if !opaque {
|
if !opaque {
|
||||||
regions, err = pdfCompositeRegions(geometry, regions)
|
regions, err = pdfCompositeRegions(geometry, regions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -310,7 +317,7 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: data, Fill: &fill, Stroke: &stroke, FillColor: color, Clips: objectClips}
|
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: data, Fill: &fill, Stroke: &stroke, FillColor: color, Clips: objectClips}
|
||||||
p.appendObject(GraphicObject{Type: "PathObject", PathObject: object})
|
p.appendOriginObject(GraphicObject{Type: "PathObject", PathObject: object}, region.origin)
|
||||||
p.report.PathObjects++
|
p.report.PathObjects++
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -320,7 +327,15 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
|
|||||||
// 入参: mark 组状态, walk 内容访问函数
|
// 入参: mark 组状态, walk 内容访问函数
|
||||||
// 返回: error 转换或渲染错误
|
// 返回: error 转换或渲染错误
|
||||||
func (p *pdfImporter) rasterGroup(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
func (p *pdfImporter) rasterGroup(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
|
||||||
img, box, err := p.renderGroup(func(local *pdfImporter) error { return walk(local.visitor()) }, nil)
|
var origin pdfObjectOrigin
|
||||||
|
img, box, err := p.renderGroup(func(local *pdfImporter) error {
|
||||||
|
if err := walk(local.visitor()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
origin, err = local.objects.combinedOrigin(local.ctx)
|
||||||
|
return err
|
||||||
|
}, nil)
|
||||||
if err != nil || img == nil {
|
if err != nil || img == nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -330,7 +345,7 @@ func (p *pdfImporter) rasterGroup(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return p.appendRasterGroup(img, box, mark.Alpha)
|
return p.appendRasterGroup(img, box, mark.Alpha, origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
// visitor 绑定当前导入器的图元与透明组访问函数
|
// visitor 绑定当前导入器的图元与透明组访问函数
|
||||||
@@ -385,16 +400,13 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
|
|||||||
if p.clipPaths == nil {
|
if p.clipPaths == nil {
|
||||||
p.clipPaths = &renderCache[[32]byte, pdfClipPath]{limit: 8 << 20}
|
p.clipPaths = &renderCache[[32]byte, pdfClipPath]{limit: 8 << 20}
|
||||||
}
|
}
|
||||||
local := pdfImporter{ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer, matrix: p.matrix, pageBox: p.pageBox, pageWidth: p.pageWidth, pageHeight: p.pageHeight, rasterDPI: p.rasterDPI, warning: p.warning, rasterWarned: true, fontIDs: map[*pdfgo.Font]string{}, fonts: p.fonts, symbolFonts: p.symbolFonts, clipPaths: p.clipPaths}
|
local := pdfImporter{ctx: p.ctx, reader: p.reader, editor: editor, renderer: p.renderer, matrix: p.matrix, pageBox: p.pageBox, pageWidth: p.pageWidth, pageHeight: p.pageHeight, rasterDPI: p.rasterDPI, warning: p.warning, rasterWarned: true, fontIDs: map[*pdfgo.Font]string{}, fonts: p.fonts, symbolFonts: p.symbolFonts, clipPaths: p.clipPaths, textOrigins: p.textOrigins}
|
||||||
if err := build(&local); err != nil {
|
if err := build(&local); err != nil {
|
||||||
return nil, box, err
|
return nil, box, err
|
||||||
}
|
}
|
||||||
if err := p.ctx.Err(); err != nil {
|
if err := p.ctx.Err(); err != nil {
|
||||||
return nil, box, err
|
return nil, box, err
|
||||||
}
|
}
|
||||||
if err := local.flushPath(); err != nil {
|
|
||||||
return nil, box, err
|
|
||||||
}
|
|
||||||
if local.objects.len() == 0 {
|
if local.objects.len() == 0 {
|
||||||
return nil, box, nil
|
return nil, box, nil
|
||||||
}
|
}
|
||||||
@@ -492,9 +504,9 @@ func (p *pdfImporter) renderGroupSceneInto(source *RasterPage, box Box, target *
|
|||||||
}
|
}
|
||||||
|
|
||||||
// appendRasterGroup 保存局部合成结果并记录输出精度
|
// appendRasterGroup 保存局部合成结果并记录输出精度
|
||||||
// 入参: img 透明图像, box 页面区域, opacity 组透明度
|
// 入参: img 透明图像, box 页面区域, opacity 组透明度, origin 来源区段
|
||||||
// 返回: error 保存错误
|
// 返回: error 保存错误
|
||||||
func (p *pdfImporter) appendRasterGroup(img image.Image, box Box, opacity float64) error {
|
func (p *pdfImporter) appendRasterGroup(img image.Image, box Box, opacity float64, origin pdfObjectOrigin) error {
|
||||||
var data bytes.Buffer
|
var data bytes.Buffer
|
||||||
if err := pdfgo.EncodePNG(p.ctx, &data, img); err != nil {
|
if err := pdfgo.EncodePNG(p.ctx, &data, img); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -508,7 +520,7 @@ func (p *pdfImporter) appendRasterGroup(img image.Image, box Box, opacity float6
|
|||||||
}
|
}
|
||||||
alpha := int(math.Round(opacity * 255))
|
alpha := int(math.Round(opacity * 255))
|
||||||
object := ImageObject{ResourceID: id, Boundary: pdfBoundary(box), CTM: pdfNumbers(box.W, 0, 0, box.H, 0, 0), Alpha: &alpha}
|
object := ImageObject{ResourceID: id, Boundary: pdfBoundary(box), CTM: pdfNumbers(box.W, 0, 0, box.H, 0, 0), Alpha: &alpha}
|
||||||
p.appendObject(GraphicObject{Type: "ImageObject", ImageObject: object})
|
p.appendOriginObject(GraphicObject{Type: "ImageObject", ImageObject: object}, origin)
|
||||||
p.report.ImageObjects++
|
p.report.ImageObjects++
|
||||||
if !p.rasterWarned {
|
if !p.rasterWarned {
|
||||||
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF effect locally composited at %g DPI; other objects retained", p.rasterDPI)})
|
p.warning(pdfgo.Diagnostic{Message: fmt.Sprintf("PDF effect locally composited at %g DPI; other objects retained", p.rasterDPI)})
|
||||||
@@ -531,7 +543,7 @@ func pdfCompositeRegions(geometry GeometryBackend, regions []pdfPaintRegion) ([]
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(outside) != 0 {
|
if len(outside) != 0 {
|
||||||
next = append(next, pdfPaintRegion{backdrop.paint, outside, backdrop.intent})
|
next = append(next, pdfPaintRegion{backdrop.paint, outside, backdrop.intent, backdrop.origin})
|
||||||
}
|
}
|
||||||
overlap, err := geometry.Combine(backdrop.path, source.path, GeometryIntersect)
|
overlap, err := geometry.Combine(backdrop.path, source.path, GeometryIntersect)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -542,7 +554,11 @@ func pdfCompositeRegions(geometry GeometryBackend, regions []pdfPaintRegion) ([]
|
|||||||
for i := range paint.RGB {
|
for i := range paint.RGB {
|
||||||
paint.RGB[i] = (source.paint.RGB[i]*source.paint.Alpha + backdrop.paint.RGB[i]*backdrop.paint.Alpha*(1-source.paint.Alpha)) / paint.Alpha
|
paint.RGB[i] = (source.paint.RGB[i]*source.paint.Alpha + backdrop.paint.RGB[i]*backdrop.paint.Alpha*(1-source.paint.Alpha)) / paint.Alpha
|
||||||
}
|
}
|
||||||
next = append(next, pdfPaintRegion{paint, overlap, ""})
|
origin := source.origin
|
||||||
|
if source.paint.Alpha != 1 {
|
||||||
|
origin = backdrop.origin.merge(origin)
|
||||||
|
}
|
||||||
|
next = append(next, pdfPaintRegion{paint, overlap, "", origin})
|
||||||
}
|
}
|
||||||
remaining, err = geometry.Combine(remaining, backdrop.path, GeometrySubtract)
|
remaining, err = geometry.Combine(remaining, backdrop.path, GeometrySubtract)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -550,7 +566,7 @@ func pdfCompositeRegions(geometry GeometryBackend, regions []pdfPaintRegion) ([]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(remaining) != 0 {
|
if len(remaining) != 0 {
|
||||||
next = append(next, pdfPaintRegion{source.paint, remaining, source.intent})
|
next = append(next, pdfPaintRegion{source.paint, remaining, source.intent, source.origin})
|
||||||
}
|
}
|
||||||
result = next
|
result = next
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-4
@@ -69,9 +69,6 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
|||||||
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: "image blend mode " + string(mark.Style.BlendMode)}
|
return &pdfgo.UnsupportedError{Feature: "image blend mode " + string(mark.Style.BlendMode)}
|
||||||
}
|
}
|
||||||
if err := p.flushPath(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if mark.Image.ImageMask && (mark.Style.Fill.Axial != nil || mark.Style.Fill.Radial != nil || mark.Style.Fill.Function != nil || mark.Style.Fill.Mesh != nil || mark.Style.Fill.Shading != nil) {
|
if mark.Image.ImageMask && (mark.Style.Fill.Axial != nil || mark.Style.Fill.Radial != nil || mark.Style.Fill.Function != nil || mark.Style.Fill.Mesh != nil || mark.Style.Fill.Shading != nil) {
|
||||||
if p.warning != nil {
|
if p.warning != nil {
|
||||||
return p.compositeRegion(nil, pdfCompositeNode{image: &mark}, &pdfgo.ColorSpace{Model: "DeviceRGB"}, false)
|
return p.compositeRegion(nil, pdfCompositeNode{image: &mark}, &pdfgo.ColorSpace{Model: "DeviceRGB"}, false)
|
||||||
@@ -250,7 +247,7 @@ func (p *pdfImporter) appendImage(mark pdfgo.ImageMark, id string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
object := ImageObject{Boundary: pdfBoundary(box), ResourceID: id, CTM: pdfNumbers(m[0], m[1], m[2], m[3], m[4]-box.X, m[5]-box.Y), Alpha: &alpha, Clips: clips}
|
object := ImageObject{Boundary: pdfBoundary(box), ResourceID: id, CTM: pdfNumbers(m[0], m[1], m[2], m[3], m[4]-box.X, m[5]-box.Y), Alpha: &alpha, Clips: clips}
|
||||||
p.appendObject(GraphicObject{Type: "ImageObject", ImageObject: object})
|
p.appendSequenceObject(GraphicObject{Type: "ImageObject", ImageObject: object}, mark.Sequence)
|
||||||
p.report.ImageObjects++
|
p.report.ImageObjects++
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,352 @@
|
|||||||
|
// 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")
|
||||||
|
}
|
||||||
+38
-7
@@ -35,6 +35,12 @@ type pdfPatternCellKey struct {
|
|||||||
space *pdfgo.ColorSpace
|
space *pdfgo.ColorSpace
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pdfPatternCell 保存图案快照及单元内部的来源区段
|
||||||
|
type pdfPatternCell struct {
|
||||||
|
pattern *Pattern
|
||||||
|
origin pdfObjectOrigin
|
||||||
|
}
|
||||||
|
|
||||||
// pdfPatternInstanceRange 收缩已知路径及图片的实例范围,未知边界保留完整范围
|
// pdfPatternInstanceRange 收缩已知路径及图片的实例范围,未知边界保留完整范围
|
||||||
// 入参: columns 横向实例数, rows 纵向实例数, bounds 对象边界, width 单元宽, height 单元高, xstep 横向步距, ystep 纵向步距
|
// 入参: columns 横向实例数, rows 纵向实例数, bounds 对象边界, width 单元宽, height 单元高, xstep 横向步距, ystep 纵向步距
|
||||||
// 返回: int 左界, int 右界, int 上界, int 下界
|
// 返回: int 左界, int 右界, int 上界, int 下界
|
||||||
@@ -63,7 +69,7 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain
|
|||||||
return p.buildTilingPattern(source, base)
|
return p.buildTilingPattern(source, base)
|
||||||
}
|
}
|
||||||
if p.patternCells == nil {
|
if p.patternCells == nil {
|
||||||
p.patternCells = &renderCache[pdfPatternCellKey, *Pattern]{limit: pdfPatternCellLimit}
|
p.patternCells = &renderCache[pdfPatternCellKey, pdfPatternCell]{limit: pdfPatternCellLimit}
|
||||||
}
|
}
|
||||||
key := pdfPatternCellKey{source: source, matrix: p.matrix, dpi: p.rasterDPI, editor: p.editor, space: p.compositeSpace}
|
key := pdfPatternCellKey{source: source, matrix: p.matrix, dpi: p.rasterDPI, editor: p.editor, space: p.compositeSpace}
|
||||||
if source.PaintType == 2 {
|
if source.PaintType == 2 {
|
||||||
@@ -72,7 +78,11 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain
|
|||||||
key.base = base
|
key.base = base
|
||||||
}
|
}
|
||||||
if cached, ok := p.patternCells.get(key); ok {
|
if cached, ok := p.patternCells.get(key); ok {
|
||||||
return cloneEditorData(cached), p.ctx.Err()
|
pattern := cloneEditorData(cached.pattern)
|
||||||
|
if cached.origin.sequence != nil || len(cached.origin.related) != 0 {
|
||||||
|
p.patternOrigins[pattern] = cached.origin
|
||||||
|
}
|
||||||
|
return pattern, p.ctx.Err()
|
||||||
}
|
}
|
||||||
pattern, err := p.buildTilingPattern(source, base)
|
pattern, err := p.buildTilingPattern(source, base)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -82,8 +92,9 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
cost := pdfPatternCellCost(reflect.ValueOf(pattern), p.patternCells.limit, 0)
|
cost := pdfPatternCellCost(reflect.ValueOf(pattern), p.patternCells.limit, 0)
|
||||||
|
cost += int(reflect.TypeFor[pdfPatternCell]().Size()) + len(p.patternOrigins[pattern].related)*int(reflect.TypeFor[*pdfgo.ContentSequence]().Size())
|
||||||
if cost <= p.patternCells.limit {
|
if cost <= p.patternCells.limit {
|
||||||
p.patternCells.put(key, cloneEditorData(pattern), cost)
|
p.patternCells.put(key, pdfPatternCell{pattern: cloneEditorData(pattern), origin: p.patternOrigins[pattern]}, cost)
|
||||||
}
|
}
|
||||||
return pattern, p.ctx.Err()
|
return pattern, p.ctx.Err()
|
||||||
}
|
}
|
||||||
@@ -143,6 +154,9 @@ func (p *pdfImporter) buildTilingPattern(source *pdfgo.TilingPattern, base pdfgo
|
|||||||
localToPDF := pdfgo.Matrix{1 / unit, 0, 0, -1 / unit, box.XMin, box.YMax}
|
localToPDF := pdfgo.Matrix{1 / unit, 0, 0, -1 / unit, box.XMin, box.YMax}
|
||||||
m := p.matrix.Mul(source.Matrix).Mul(localToPDF)
|
m := p.matrix.Mul(source.Matrix).Mul(localToPDF)
|
||||||
pattern := &Pattern{Width: math.Min(width, xstep) * unit, Height: math.Min(height, ystep) * unit, XStep: xstep * unit, YStep: ystep * unit, RelativeTo: "Page", CTM: pdfNumbers(m[:]...)}
|
pattern := &Pattern{Width: math.Min(width, xstep) * unit, Height: math.Min(height, ystep) * unit, XStep: xstep * unit, YStep: ystep * unit, RelativeTo: "Page", CTM: pdfNumbers(m[:]...)}
|
||||||
|
if p.patternOrigins == nil {
|
||||||
|
p.patternOrigins = make(map[*Pattern]pdfObjectOrigin)
|
||||||
|
}
|
||||||
cell := *p
|
cell := *p
|
||||||
cell.matrix = pdfgo.Matrix{unit, 0, 0, -unit, -box.XMin * unit, box.YMax * unit}
|
cell.matrix = pdfgo.Matrix{unit, 0, 0, -unit, -box.XMin * unit, box.YMax * unit}
|
||||||
cell.pageWidth, cell.pageHeight = width*unit, height*unit
|
cell.pageWidth, cell.pageHeight = width*unit, height*unit
|
||||||
@@ -150,13 +164,22 @@ func (p *pdfImporter) buildTilingPattern(source *pdfgo.TilingPattern, base pdfgo
|
|||||||
if !finite(cell.rasterDPI) || cell.rasterDPI < 0 {
|
if !finite(cell.rasterDPI) || cell.rasterDPI < 0 {
|
||||||
return nil, fmt.Errorf("invalid PDF tiling pattern raster scale")
|
return nil, fmt.Errorf("invalid PDF tiling pattern raster scale")
|
||||||
}
|
}
|
||||||
cell.objects, cell.pendingPath = nil, nil
|
cell.objects = nil
|
||||||
if err := source.Walk(p.ctx, base, cell.visitor()); err != nil {
|
nodes, err := cell.collectCompositeNodes(func(visitor pdfgo.Visitor) error {
|
||||||
|
return source.Walk(p.ctx, base, visitor)
|
||||||
|
}, false)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := cell.flushPath(); err != nil {
|
visitor := cell.visitor()
|
||||||
|
for _, node := range nodes {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := node.emit(visitor); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
objects, err := cell.objects.data(p.ctx)
|
objects, err := cell.objects.data(p.ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -205,7 +228,7 @@ func (p *pdfImporter) buildTilingPattern(source *pdfgo.TilingPattern, base pdfgo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
expanded.append(translated)
|
expanded.appendOrigin(translated, cell.objects.origin(index))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,6 +236,14 @@ func (p *pdfImporter) buildTilingPattern(source *pdfgo.TilingPattern, base pdfgo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
cell.objects = &expanded
|
||||||
|
}
|
||||||
|
origin, err := cell.objects.combinedOrigin(p.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if origin.sequence != nil || len(origin.related) != 0 {
|
||||||
|
p.patternOrigins[pattern] = origin
|
||||||
}
|
}
|
||||||
return pattern, nil
|
return pattern, nil
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,879 @@
|
|||||||
|
// 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"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
"math"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/xiaoqidun/pdfgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PDF逻辑结构标引使用独立命名空间,图元引用沿用ObjectRef语义
|
||||||
|
const (
|
||||||
|
pdfStructureTagType = "OFDGo.PDFStructure"
|
||||||
|
pdfStructureNamespace = "https://ofdgo.aite.me/ns/pdf/structure/1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// pdfStructureBindings 保存来源区段与最终页面图元的关联,不记录临时渲染对象
|
||||||
|
type pdfStructureBindings struct {
|
||||||
|
parents map[*pdfgo.Reader]map[[2]int64]pdfgo.Reference
|
||||||
|
prepared 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareStructure 校验结构内容关联并登记引用区段,不提前写入标引
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: error 结构或内容容器错误
|
||||||
|
func (p *pdfImporter) prepareStructure(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
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
return node, err
|
||||||
|
}
|
||||||
|
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, err := value.DecodeContext(v.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return node, err
|
||||||
|
}
|
||||||
|
node.Children = []StructureNode{dictionary, {Name: "DecodedData", 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]
|
||||||
|
}
|
||||||
+3
-9
@@ -35,16 +35,13 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
|||||||
return &pdfgo.UnsupportedError{Feature: "text blend mode " + string(mark.Style.BlendMode)}
|
return &pdfgo.UnsupportedError{Feature: "text blend mode " + string(mark.Style.BlendMode)}
|
||||||
}
|
}
|
||||||
if mark.Font.Subtype == pdfgo.Name("Type3") {
|
if mark.Font.Subtype == pdfgo.Name("Type3") {
|
||||||
if err := p.flushPath(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
visitor := p.visitor()
|
visitor := p.visitor()
|
||||||
for index := range mark.Glyphs {
|
for index := range mark.Glyphs {
|
||||||
if err := p.reader.WalkType3Glyph(p.ctx, mark, index, visitor); err != nil {
|
if err := p.reader.WalkType3Glyph(p.ctx, mark, index, visitor); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return p.flushPath()
|
return p.ctx.Err()
|
||||||
}
|
}
|
||||||
if mark.Size == 0 || mark.HorizontalScale == 0 {
|
if mark.Size == 0 || mark.HorizontalScale == 0 {
|
||||||
return p.degenerateText(mark)
|
return p.degenerateText(mark)
|
||||||
@@ -59,9 +56,6 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
|||||||
}
|
}
|
||||||
mark.Positions = positions
|
mark.Positions = positions
|
||||||
}
|
}
|
||||||
if err := p.flushPath(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
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 {
|
||||||
@@ -377,14 +371,14 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
|||||||
if matrix == (pdfgo.Matrix{}) {
|
if matrix == (pdfgo.Matrix{}) {
|
||||||
matrix = pdfgo.Identity()
|
matrix = pdfgo.Identity()
|
||||||
}
|
}
|
||||||
if err := p.appendPath(pdfgo.PathMark{Path: path, Style: mark.Style, Fill: fill, Stroke: true, StrokeMatrix: matrix}); err != nil {
|
if err := p.appendPath(pdfgo.PathMark{Sequence: mark.Sequence, Path: path, Style: mark.Style, Fill: fill, Stroke: true, StrokeMatrix: matrix}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
object.Visible = new(bool)
|
object.Visible = new(bool)
|
||||||
object.Fill, object.Stroke = new(bool), new(bool)
|
object.Fill, object.Stroke = new(bool), new(bool)
|
||||||
}
|
}
|
||||||
p.appendObject(GraphicObject{Type: "TextObject", TextObject: object})
|
p.appendOriginObject(GraphicObject{Type: "TextObject", TextObject: object}, p.textOrigin(mark))
|
||||||
p.report.TextObjects++
|
p.report.TextObjects++
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,15 +78,30 @@ func (r *pdfTextReplacements) block(text *pdfgo.ReplacementText) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// markedContent 核对已接入的替换文本属性,不静默丢弃其他内容语义
|
// markedContent 处理结构区段、可选内容及替换文本属性
|
||||||
// 入参: mark 内容标记
|
// 入参: mark 内容标记
|
||||||
// 返回: error 未支持的语义错误
|
// 返回: error 未支持的语义错误
|
||||||
func (r *pdfTextReplacements) markedContent(mark pdfgo.MarkedContentMark) error {
|
func (r *pdfTextReplacements) markedContent(mark pdfgo.MarkedContentMark) error {
|
||||||
if mark.Operator == "EMC" {
|
if mark.Operator == "EMC" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if r.importer.structureBindings != nil {
|
||||||
|
if err := r.importer.prepareSourceStructure(r.importer.ctx, mark.Reader); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.importer.structureBindings.observe(mark.Sequence)
|
||||||
|
if mark.Sequence != nil && r.importer.structureBindings.marked != nil {
|
||||||
|
key, ok, err := pdfStructureContentKey(mark.Sequence)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ok && r.importer.structureBindings.marked[key] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if mark.Tag == "OC" {
|
if mark.Tag == "OC" {
|
||||||
kind, err := r.importer.reader.Resolve(mark.Properties["Type"])
|
kind, err := mark.Reader.Resolve(mark.Properties["Type"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -229,6 +244,23 @@ func (r *pdfTextReplacements) merge(nodes []pdfCompositeNode) ([]pdfCompositeNod
|
|||||||
end = max(end, i+1)
|
end = max(end, i+1)
|
||||||
if len(marks) == span.count {
|
if len(marks) == span.count {
|
||||||
if mark, ok := pdfMergeReplacement(marks, span.text); ok {
|
if mark, ok := pdfMergeReplacement(marks, span.text); ok {
|
||||||
|
var origins pdfOriginSet
|
||||||
|
for _, source := range marks {
|
||||||
|
if err := r.importer.ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
origin := r.importer.textOrigin(*source.text)
|
||||||
|
origins.add(origin.sequence)
|
||||||
|
for _, sequence := range origin.related {
|
||||||
|
origins.add(sequence)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(origins.origin.related) != 0 {
|
||||||
|
if r.importer.textOrigins == nil {
|
||||||
|
r.importer.textOrigins = make(map[*pdfgo.ReplacementText]pdfObjectOrigin)
|
||||||
|
}
|
||||||
|
r.importer.textOrigins[span.text] = origins.origin
|
||||||
|
}
|
||||||
if node.text != nil {
|
if node.text != nil {
|
||||||
node.text = mark
|
node.text = mark
|
||||||
} else {
|
} else {
|
||||||
@@ -267,6 +299,16 @@ func (r *pdfTextReplacements) clear(nodes []pdfCompositeNode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// textOrigin 获取合并文字的完整来源,普通文字沿用原始区段
|
||||||
|
// 入参: mark 文字图元
|
||||||
|
// 返回: pdfObjectOrigin 来源区段
|
||||||
|
func (p *pdfImporter) textOrigin(mark pdfgo.TextMark) pdfObjectOrigin {
|
||||||
|
if origin, ok := p.textOrigins[mark.Replacement]; ok {
|
||||||
|
return origin
|
||||||
|
}
|
||||||
|
return pdfObjectOrigin{sequence: mark.Sequence}
|
||||||
|
}
|
||||||
|
|
||||||
// pdfMergeReplacement 统一同一替换区段内的坐标原点,不重新塑形或调整字距
|
// pdfMergeReplacement 统一同一替换区段内的坐标原点,不重新塑形或调整字距
|
||||||
// 入参: nodes 连续文字图元, replacement 整段原文
|
// 入参: nodes 连续文字图元, replacement 整段原文
|
||||||
// 返回: *pdfgo.TextMark 合并文字, bool 是否可等价合并
|
// 返回: *pdfgo.TextMark 合并文字, bool 是否可等价合并
|
||||||
@@ -282,6 +324,7 @@ func pdfMergeReplacement(nodes []pdfCompositeNode, replacement *pdfgo.Replacemen
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
count := 0
|
count := 0
|
||||||
|
sequence := first.Sequence
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
mark := node.text
|
mark := node.text
|
||||||
if mark.Font != first.Font || mark.Size != first.Size || mark.HorizontalScale != first.HorizontalScale || mark.Mode != first.Mode || mark.Clip != nil || len(mark.Glyphs) != len(mark.Positions) || !reflect.DeepEqual(mark.Style, style) {
|
if mark.Font != first.Font || mark.Size != first.Size || mark.HorizontalScale != first.HorizontalScale || mark.Mode != first.Mode || mark.Clip != nil || len(mark.Glyphs) != len(mark.Positions) || !reflect.DeepEqual(mark.Style, style) {
|
||||||
@@ -291,12 +334,14 @@ func pdfMergeReplacement(nodes []pdfCompositeNode, replacement *pdfgo.Replacemen
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
count += len(mark.Glyphs)
|
count += len(mark.Glyphs)
|
||||||
|
sequence = pdfCommonSequence(sequence, mark.Sequence)
|
||||||
}
|
}
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
merged := *first
|
merged := *first
|
||||||
merged.Replacement = replacement
|
merged.Replacement = replacement
|
||||||
|
merged.Sequence = sequence
|
||||||
merged.Glyphs = make([]pdfgo.Glyph, 0, count)
|
merged.Glyphs = make([]pdfgo.Glyph, 0, count)
|
||||||
merged.Positions = make([]pdfgo.Point, 0, count)
|
merged.Positions = make([]pdfgo.Point, 0, count)
|
||||||
for _, node := range nodes {
|
for _, node := range nodes {
|
||||||
@@ -316,6 +361,36 @@ func pdfMergeReplacement(nodes []pdfCompositeNode, replacement *pdfgo.Replacemen
|
|||||||
return &merged, true
|
return &merged, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pdfCommonSequence 查找两个内容区段的最近共同外层,不将合并内容归入单个子区段
|
||||||
|
// 入参: first 首个区段, second 后续区段
|
||||||
|
// 返回: *pdfgo.ContentSequence 共同区段,没有共同区段时为空
|
||||||
|
func pdfCommonSequence(first, second *pdfgo.ContentSequence) *pdfgo.ContentSequence {
|
||||||
|
if first == second {
|
||||||
|
return first
|
||||||
|
}
|
||||||
|
if first == nil || second == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
depth := func(sequence *pdfgo.ContentSequence) int {
|
||||||
|
count := 0
|
||||||
|
for ; sequence != nil; sequence = sequence.Parent {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
a, b := depth(first), depth(second)
|
||||||
|
for ; a > b; a-- {
|
||||||
|
first = first.Parent
|
||||||
|
}
|
||||||
|
for ; b > a; b-- {
|
||||||
|
second = second.Parent
|
||||||
|
}
|
||||||
|
for first != second {
|
||||||
|
first, second = first.Parent, second.Parent
|
||||||
|
}
|
||||||
|
return first
|
||||||
|
}
|
||||||
|
|
||||||
// pdfApplyReplacement 将整段原文映射到全部字形,字形位移遵循GB/T 33190的文字定位规则
|
// pdfApplyReplacement 将整段原文映射到全部字形,字形位移遵循GB/T 33190的文字定位规则
|
||||||
// 入参: object 文字对象, text 整段原文, positions 原始字形位置
|
// 入参: object 文字对象, text 整段原文, positions 原始字形位置
|
||||||
func pdfApplyReplacement(object *TextObject, text string, positions []pdfgo.Point) {
|
func pdfApplyReplacement(object *TextObject, text string, positions []pdfgo.Point) {
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ func (CanvasBackend) Stroke(path GeometryPath, options StrokeOptions) (result Ge
|
|||||||
style.applyLineJoin(options.Join, options.MiterLimit, options.MiterLimitSet)
|
style.applyLineJoin(options.Join, options.MiterLimit, options.MiterLimitSet)
|
||||||
style.lineCap = pathLineCap(options.Cap, style.lineCap)
|
style.lineCap = pathLineCap(options.Cap, style.lineCap)
|
||||||
p = p.Dash(options.DashOffset, options.Dashes...)
|
p = p.Dash(options.DashOffset, options.Dashes...)
|
||||||
p = p.Flatten(tolerance)
|
p = flattenCanvasPath(p, tolerance)
|
||||||
p = p.Stroke(options.Width, style.lineCap, style.lineJoin, tolerance)
|
p = p.Stroke(options.Width, style.lineCap, style.lineJoin, tolerance)
|
||||||
return *geometryFromCanvasPath(p), nil
|
return *geometryFromCanvasPath(p), nil
|
||||||
}
|
}
|
||||||
@@ -199,6 +199,27 @@ func (CanvasBackend) Clip(r *Renderer, clips *Clips, matrix Matrix, parent *Geom
|
|||||||
return geometryFromCanvasPath(r.intersectCachedClipPath(p, clip)), nil
|
return geometryFromCanvasPath(r.intersectCachedClipPath(p, clip)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// flattenCanvasPath 按子路径展开曲线,避免每次替换都复制后续独立轮廓
|
||||||
|
// 入参: path 原始路径, tolerance 展开误差
|
||||||
|
// 返回: *canvas.Path 直线路径,不修改原始路径
|
||||||
|
func flattenCanvasPath(path *canvas.Path, tolerance float64) *canvas.Path {
|
||||||
|
if path.IsFlat() {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if data := path.Data(); len(data) > 0 && data[len(data)-1] == canvas.MoveToCmd {
|
||||||
|
return path.Flatten(tolerance)
|
||||||
|
}
|
||||||
|
parts := path.Split()
|
||||||
|
if len(parts) <= 1 {
|
||||||
|
return path.Flatten(tolerance)
|
||||||
|
}
|
||||||
|
data := make([]float64, 0, len(path.Data()))
|
||||||
|
for _, part := range parts {
|
||||||
|
data = append(data, part.Flatten(tolerance).Data()...)
|
||||||
|
}
|
||||||
|
return canvas.NewPathFromData(data)
|
||||||
|
}
|
||||||
|
|
||||||
// canvasNativeStroke 判断描边是否由Canvas提供
|
// canvasNativeStroke 判断描边是否由Canvas提供
|
||||||
// 入参: geometry 几何后端
|
// 入参: geometry 几何后端
|
||||||
// 返回: bool 是否采用原生描边
|
// 返回: bool 是否采用原生描边
|
||||||
|
|||||||
@@ -147,7 +147,21 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i
|
|||||||
if docInfo, err := r.Reader.DocInfo(); err == nil {
|
if docInfo, err := r.Reader.DocInfo(); err == nil {
|
||||||
info = *docInfo
|
info = *docInfo
|
||||||
}
|
}
|
||||||
p.SetInfo(info.Title, info.Subject, "", info.Author, ofdCreator)
|
renderer.information = make(pdfgo.Dictionary)
|
||||||
|
for _, field := range []struct {
|
||||||
|
key pdfgo.Name
|
||||||
|
value string
|
||||||
|
}{{"Title", info.Title}, {"Subject", info.Subject}, {"Author", info.Author}, {"Creator", ofdCreator}} {
|
||||||
|
if field.value == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
encoded, err := pdfgo.EncodeTextString(field.value)
|
||||||
|
if err != nil {
|
||||||
|
buf.Truncate(start)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
renderer.information[field.key] = encoded
|
||||||
|
}
|
||||||
for i, page := range pages {
|
for i, page := range pages {
|
||||||
content, err := page.ReadContent(ctx, r.Reader)
|
content, err := page.ReadContent(ctx, r.Reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -201,7 +215,7 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i
|
|||||||
buf.Truncate(start)
|
buf.Truncate(start)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
data := replacePDFProducer(buf.Bytes()[start:])
|
data := buf.Bytes()[start:]
|
||||||
options := pdfgo.OptimizeOptions{Compression: r.Compression}
|
options := pdfgo.OptimizeOptions{Compression: r.Compression}
|
||||||
if r.Compression.Mode != CompressionUnchanged && progress != nil {
|
if r.Compression.Mode != CompressionUnchanged && progress != nil {
|
||||||
options.OnProgress = func(_ string, _, _ int) error {
|
options.OnProgress = func(_ string, _, _ int) error {
|
||||||
@@ -209,13 +223,6 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if direct {
|
if direct {
|
||||||
if !renderer.exactImages && !renderer.exactText && !navigation.nativeWrite {
|
|
||||||
if err := r.outputContext().Err(); err != nil {
|
|
||||||
buf.Truncate(start)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var result bytes.Buffer
|
var result bytes.Buffer
|
||||||
err := renderer.writePDF(r.outputContext(), data, &result, options)
|
err := renderer.writePDF(r.outputContext(), data, &result, options)
|
||||||
buf.Truncate(start)
|
buf.Truncate(start)
|
||||||
@@ -227,17 +234,3 @@ func (CanvasBackend) RenderPDF(r *Renderer, pages []RenderDocumentPage, writer i
|
|||||||
}
|
}
|
||||||
return renderer.writePDF(r.outputContext(), data, writer, options)
|
return renderer.writePDF(r.outputContext(), data, writer, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// replacePDFProducer 替换PDF的Producer属性
|
|
||||||
// 入参: data PDF字节数据
|
|
||||||
// 返回: []byte 替换后的PDF字节数据
|
|
||||||
func replacePDFProducer(data []byte) []byte {
|
|
||||||
old := []byte("/Producer(tdewolff/canvas)")
|
|
||||||
idx := bytes.LastIndex(data, old)
|
|
||||||
if idx < 0 {
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
dst := []byte("/Producer(" + ofdCreator + ")")
|
|
||||||
copy(data[idx:idx+len(old)], dst)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
@@ -312,6 +312,12 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
|
|||||||
ctx.SetDashes(0)
|
ctx.SetDashes(0)
|
||||||
}
|
}
|
||||||
strokeClip := r.intersectCachedClipPath(clipPath, shadingClip)
|
strokeClip := r.intersectCachedClipPath(clipPath, shadingClip)
|
||||||
|
if bounds, ok := rectangularPath(strokeClip); ok {
|
||||||
|
margin := style.lineWidth / 2 * math.Max(math.Sqrt2, style.miterLimit)
|
||||||
|
if finite(margin) && bounds.Contains(sp.Bounds().Expand(margin)) {
|
||||||
|
strokeClip = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
_, repeat := strokePaint.(*repeatAxialGradient)
|
_, repeat := strokePaint.(*repeatAxialGradient)
|
||||||
if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil || !canvasNativeStroke(r.backends.Geometry) || zeroDash {
|
if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil || !canvasNativeStroke(r.backends.Geometry) || zeroDash {
|
||||||
sp = sp.Copy()
|
sp = sp.Copy()
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"io"
|
"io"
|
||||||
|
"maps"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
cimage "github.com/tdewolff/canvas/image"
|
cimage "github.com/tdewolff/canvas/image"
|
||||||
@@ -32,6 +34,7 @@ import (
|
|||||||
// pdfRenderer PDF渲染器
|
// pdfRenderer PDF渲染器
|
||||||
type pdfRenderer struct {
|
type pdfRenderer struct {
|
||||||
*pdf.PDF
|
*pdf.PDF
|
||||||
|
information pdfgo.Dictionary
|
||||||
glyphPaths renderCache[*canvas.Path, *canvas.Path]
|
glyphPaths renderCache[*canvas.Path, *canvas.Path]
|
||||||
fonts map[*canvas.Font]*canvas.Font
|
fonts map[*canvas.Font]*canvas.Font
|
||||||
cffFonts map[*canvas.Font][]*pdfFontSubset
|
cffFonts map[*canvas.Font][]*pdfFontSubset
|
||||||
@@ -159,7 +162,14 @@ func (r *pdfRenderer) writePDF(ctx context.Context, data []byte, writer io.Write
|
|||||||
if r.imageError != nil {
|
if r.imageError != nil {
|
||||||
return r.imageError
|
return r.imageError
|
||||||
}
|
}
|
||||||
|
information := maps.Clone(r.information)
|
||||||
|
if information == nil {
|
||||||
|
information = make(pdfgo.Dictionary)
|
||||||
|
}
|
||||||
|
information["Producer"] = pdfgo.String(ofdCreator)
|
||||||
|
information["CreationDate"] = pdfgo.String(time.Now().UTC().Format("D:20060102150405Z"))
|
||||||
options := codecpdf.WriteOptions{
|
options := codecpdf.WriteOptions{
|
||||||
|
Information: information,
|
||||||
Images: r.images,
|
Images: r.images,
|
||||||
Text: r.text,
|
Text: r.text,
|
||||||
ExactImages: r.exactImages,
|
ExactImages: r.exactImages,
|
||||||
|
|||||||
+12
-5
@@ -45,7 +45,7 @@ type Structure struct {
|
|||||||
|
|
||||||
// StructureNode 保存标引节点的名称、属性、直接文本和有序子节点,不改变原始XML
|
// StructureNode 保存标引节点的名称、属性、直接文本和有序子节点,不改变原始XML
|
||||||
// Text按文档顺序汇总直接文本并保留空白,不包含子节点文本
|
// Text按文档顺序汇总直接文本并保留空白,不包含子节点文本
|
||||||
// References仅解释已识别公文、富文本或发票标引中的ObjectRef;未知扩展保留结构,不推定其引用语义
|
// References仅解释已识别标引中的ObjectRef;未知扩展保留结构,不推定其引用语义
|
||||||
type StructureNode struct {
|
type StructureNode struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Namespace string `json:"namespace,omitempty"`
|
Namespace string `json:"namespace,omitempty"`
|
||||||
@@ -132,11 +132,11 @@ func (r *Reader) readStructureContext(ctx context.Context, tag CustomTag) (Struc
|
|||||||
// 入参: ctx 取消上下文, data 标引XML, tag 标引入口
|
// 入参: ctx 取消上下文, data 标引XML, tag 标引入口
|
||||||
// 返回: *StructureNode 根节点, error 解析、限额或取消错误
|
// 返回: *StructureNode 根节点, error 解析、限额或取消错误
|
||||||
func parseStructure(ctx context.Context, data []byte, tag CustomTag) (*StructureNode, error) {
|
func parseStructure(ctx context.Context, data []byte, tag CustomTag) (*StructureNode, error) {
|
||||||
|
data = bytes.TrimPrefix(data, []byte{0xef, 0xbb, 0xbf})
|
||||||
d := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
d := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
||||||
var root *StructureNode
|
var root *StructureNode
|
||||||
var stack []*StructureNode
|
var stack []*StructureNode
|
||||||
var text []*strings.Builder
|
var text []*strings.Builder
|
||||||
count := 0
|
|
||||||
for {
|
for {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -150,15 +150,19 @@ func parseStructure(ctx context.Context, data []byte, tag CustomTag) (*Structure
|
|||||||
}
|
}
|
||||||
switch token := token.(type) {
|
switch token := token.(type) {
|
||||||
case xml.StartElement:
|
case xml.StartElement:
|
||||||
count++
|
if len(stack) >= 128 {
|
||||||
if len(stack) >= 128 || count > 100000 {
|
|
||||||
return nil, fmt.Errorf("custom tag structure limit exceeded")
|
return nil, fmt.Errorf("custom tag structure limit exceeded")
|
||||||
}
|
}
|
||||||
if len(stack) == len(text) {
|
if len(stack) == len(text) {
|
||||||
text = append(text, new(strings.Builder))
|
text = append(text, new(strings.Builder))
|
||||||
}
|
}
|
||||||
node := StructureNode{Name: token.Name.Local, Namespace: token.Name.Space}
|
node := StructureNode{Name: token.Name.Local, Namespace: token.Name.Space}
|
||||||
|
attributes := make(map[xml.Name]bool, len(token.Attr))
|
||||||
for _, attr := range token.Attr {
|
for _, attr := range token.Attr {
|
||||||
|
if attributes[attr.Name] {
|
||||||
|
return nil, fmt.Errorf("duplicate custom tag attribute")
|
||||||
|
}
|
||||||
|
attributes[attr.Name] = true
|
||||||
if attr.Name.Space != "xmlns" && !(attr.Name.Space == "" && attr.Name.Local == "xmlns") {
|
if attr.Name.Space != "xmlns" && !(attr.Name.Space == "" && attr.Name.Local == "xmlns") {
|
||||||
node.Attributes = append(node.Attributes, StructureAttribute{Name: attr.Name.Local, Namespace: attr.Name.Space, Value: attr.Value})
|
node.Attributes = append(node.Attributes, StructureAttribute{Name: attr.Name.Local, Namespace: attr.Name.Space, Value: attr.Value})
|
||||||
}
|
}
|
||||||
@@ -301,7 +305,7 @@ func (r *Reader) structureReferenceTags(ctx context.Context, tags []CustomTag) (
|
|||||||
return known, nil
|
return known, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// structureOfficial 识别公文、富文本及发票标引的明确引用规则,不推断未知行业语义
|
// structureOfficial 识别公文及库内标引的明确引用规则,不推断未知行业语义
|
||||||
// 入参: tag 标引入口, namespace 内容根节点命名空间
|
// 入参: tag 标引入口, namespace 内容根节点命名空间
|
||||||
// 返回: bool 是否使用公文引用规则
|
// 返回: bool 是否使用公文引用规则
|
||||||
func structureOfficial(tag CustomTag, namespace string) bool {
|
func structureOfficial(tag CustomTag, namespace string) bool {
|
||||||
@@ -311,6 +315,9 @@ func structureOfficial(tag CustomTag, namespace string) bool {
|
|||||||
if tag.TypeID == richTextTagType && tag.NameSpace == richTextNamespace && namespace == richTextNamespace {
|
if tag.TypeID == richTextTagType && tag.NameSpace == richTextNamespace && namespace == richTextNamespace {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if tag.TypeID == pdfStructureTagType && tag.NameSpace == pdfStructureNamespace && namespace == pdfStructureNamespace {
|
||||||
|
return true
|
||||||
|
}
|
||||||
if tag.TypeID == structureTypeOfficial {
|
if tag.TypeID == structureTypeOfficial {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-3
@@ -29,13 +29,21 @@ import (
|
|||||||
// 入参: node 根节点
|
// 入参: node 根节点
|
||||||
// 返回: []byte XML片段, error 名称、属性或编码错误
|
// 返回: []byte XML片段, error 名称、属性或编码错误
|
||||||
func encodeStructureNode(node StructureNode) ([]byte, error) {
|
func encodeStructureNode(node StructureNode) ([]byte, error) {
|
||||||
|
return encodeStructureNodeContext(context.Background(), node)
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeStructureNodeContext 编码标引子树并检查取消状态
|
||||||
|
// 入参: ctx 取消上下文, node 根节点
|
||||||
|
// 返回: []byte XML片段, error 名称、深度、编码或取消错误
|
||||||
|
func encodeStructureNodeContext(ctx context.Context, node StructureNode) ([]byte, error) {
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
encoder := xml.NewEncoder(&output)
|
encoder := xml.NewEncoder(&output)
|
||||||
count := 0
|
|
||||||
var write func(StructureNode, int) error
|
var write func(StructureNode, int) error
|
||||||
write = func(node StructureNode, depth int) error {
|
write = func(node StructureNode, depth int) error {
|
||||||
count++
|
if err := ctx.Err(); err != nil {
|
||||||
if depth > 128 || count > 100000 {
|
return err
|
||||||
|
}
|
||||||
|
if depth > 128 {
|
||||||
return fmt.Errorf("custom tag structure limit exceeded")
|
return fmt.Errorf("custom tag structure limit exceeded")
|
||||||
}
|
}
|
||||||
if err := validateStructureNode(node); err != nil {
|
if err := validateStructureNode(node); err != nil {
|
||||||
|
|||||||
+8
-1
@@ -400,12 +400,19 @@ func (e *Editor) writePartsWithPages(ctx context.Context, write func(string, []b
|
|||||||
{"DocID", e.Info.DocID}, {"Title", e.Info.Title}, {"Author", e.Info.Author},
|
{"DocID", e.Info.DocID}, {"Title", e.Info.Title}, {"Author", e.Info.Author},
|
||||||
{"Subject", e.Info.Subject}, {"Abstract", e.Info.Abstract},
|
{"Subject", e.Info.Subject}, {"Abstract", e.Info.Abstract},
|
||||||
{"CreationDate", e.Info.CreationDate}, {"ModDate", e.Info.ModDate},
|
{"CreationDate", e.Info.CreationDate}, {"ModDate", e.Info.ModDate},
|
||||||
{"Creator", ofdCreator},
|
|
||||||
} {
|
} {
|
||||||
if field[1] != "" {
|
if field[1] != "" {
|
||||||
x.text(field[0], field[1])
|
x.text(field[0], field[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(e.Info.Keywords) != 0 {
|
||||||
|
x.start("Keywords", nil)
|
||||||
|
for _, keyword := range e.Info.Keywords {
|
||||||
|
x.text("Keyword", keyword)
|
||||||
|
}
|
||||||
|
x.end("Keywords")
|
||||||
|
}
|
||||||
|
x.text("Creator", ofdCreator)
|
||||||
if e.Info.CustomDatas != nil && len(e.Info.CustomDatas.CustomData) != 0 {
|
if e.Info.CustomDatas != nil && len(e.Info.CustomDatas.CustomData) != 0 {
|
||||||
x.start("CustomDatas", nil)
|
x.start("CustomDatas", nil)
|
||||||
for _, item := range e.Info.CustomDatas.CustomData {
|
for _, item := range e.Info.CustomDatas.CustomData {
|
||||||
|
|||||||
Reference in new issue
Block a user