From 1945ea355ad216d7e6c5a0fc5fbfc506e166caff Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Sat, 3 Oct 2026 00:49:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=BD=AC=E6=8D=A2=E8=83=BD=E5=8A=9B):=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=BC=95=E7=94=A8=E9=A1=B5=E9=9D=A2=E4=B8=8E?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 4 +- ofdgo_pdf.go | 33 ++++++++----- ofdgo_pdf_composite.go | 2 +- ofdgo_pdf_group.go | 16 ++++--- ofdgo_pdf_object.go | 2 +- ofdgo_pdf_reference.go | 105 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 142 insertions(+), 22 deletions(-) create mode 100644 ofdgo_pdf_reference.go diff --git a/go.mod b/go.mod index ba4917c..0d5bbf7 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/xiaoqidun/jbig2 v1.1.1 - github.com/xiaoqidun/pdfgo v0.0.0-20261002153346-125ae06f23bf + github.com/xiaoqidun/pdfgo v0.0.0-20261002164637-41541220b1c3 golang.org/x/crypto v0.57.0 golang.org/x/image v0.46.0 golang.org/x/text v0.42.0 diff --git a/go.sum b/go.sum index 98e3931..1b4d88c 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk= github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= -github.com/xiaoqidun/pdfgo v0.0.0-20261002153346-125ae06f23bf h1:AjfoqpGGjeyWFH1ikxmZJ9n+hG1Qaqolaqlv9kGX91w= -github.com/xiaoqidun/pdfgo v0.0.0-20261002153346-125ae06f23bf/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= +github.com/xiaoqidun/pdfgo v0.0.0-20261002164637-41541220b1c3 h1:mrWCBsxLuBf1sjeEK9r+1KZa5WpER5J3sDPVicajM1E= +github.com/xiaoqidun/pdfgo v0.0.0-20261002164637-41541220b1c3/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index 119215f..013224a 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -35,17 +35,19 @@ var ErrPDFPassword = pdfgo.ErrPassword // RendererOptions复用渲染器字体来源和后端配置,Backends优先于其中的后端设置 // RasterDPI指定局部透明效果合成精度,0沿用渲染器DPI,默认300dpi,Strict禁用局部合成 // OnProgress按open、pages、convert及write.*阶段报告进度,total为0表示总量未知 -// ResolveFile读取PDF引用的外部媒体和附件,默认不访问网络或本地路径 +// ResolveFile读取PDF引用的外部媒体、附件和页面,默认不访问网络或本地路径 +// ResolveReference可提供已解密的引用页面,目标阅读器由调用方维护 type PDFImportOptions struct { - Password []byte - PasswordUTF8 bool - Backends *RenderBackends - RendererOptions []RendererOption - Progress func(int) error - OnProgress func(stage string, completed, total int) error - Strict bool - RasterDPI float64 - ResolveFile pdfgo.FileResolver + Password []byte + PasswordUTF8 bool + Backends *RenderBackends + RendererOptions []RendererOption + Progress func(int) error + OnProgress func(stage string, completed, total int) error + Strict bool + RasterDPI float64 + ResolveFile pdfgo.FileResolver + ResolveReference pdfgo.ReferenceResolver } // PDFImportReport 汇总转换页数、对象数、链接数和转换警告 @@ -91,6 +93,10 @@ type pdfImporter struct { compositeSpace *pdfgo.ColorSpace annotationData []byte resolveFile pdfgo.FileResolver + resolveReference pdfgo.ReferenceResolver + referenceReaders map[[32]byte]*pdfgo.Reader + referenceStreams map[*pdfgo.Stream]*pdfgo.Reader + referencePages map[pdfReferencePageKey]*pdfgo.Page halftoneWarnings map[*pdfgo.Halftone]bool transferBackdrop bool } @@ -137,6 +143,11 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI importer.report.Warnings = diagnostics importer.pageIndexes = make(map[pdfgo.Reference]int) importer.resolveFile = options.ResolveFile + importer.resolveReference = options.ResolveReference + importer.referenceReaders = make(map[[32]byte]*pdfgo.Reader) + importer.referenceStreams = make(map[*pdfgo.Stream]*pdfgo.Reader) + importer.referencePages = make(map[pdfReferencePageKey]*pdfgo.Page) + defer importer.closeReferenceReaders() importer.halftoneWarnings = make(map[*pdfgo.Halftone]bool) if security := reader.Encryption(); security != nil && !security.Owner && security.Permissions&0xf3c != 0xf3c { if options.Strict { @@ -657,7 +668,7 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain cell.matrix = pdfgo.Matrix{unit, 0, 0, -unit, -box.XMin * unit, box.YMax * unit} cell.pageWidth, cell.pageHeight = width*unit, height*unit cell.objects, cell.pendingPath = nil, nil - visitor := pdfgo.Visitor{Path: cell.path, Text: cell.text, Image: cell.image} + visitor := pdfgo.Visitor{Path: cell.path, Text: cell.text, Image: cell.image, Reference: p.referencePage} visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { return cell.group(mark, walk, visitor) } diff --git a/ofdgo_pdf_composite.go b/ofdgo_pdf_composite.go index 6d13fec..043e44d 100644 --- a/ofdgo_pdf_composite.go +++ b/ofdgo_pdf_composite.go @@ -245,7 +245,7 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p var collect func(func(pdfgo.Visitor) error) ([]pdfCompositeNode, error) collect = func(walk func(pdfgo.Visitor) error) ([]pdfCompositeNode, error) { var nodes []pdfCompositeNode - v := pdfgo.Visitor{Warning: p.warning} + v := pdfgo.Visitor{Warning: p.warning, Reference: p.referencePage} v.Path = func(mark pdfgo.PathMark) error { if err := p.halftone(mark.Style); err != nil { return err diff --git a/ofdgo_pdf_group.go b/ofdgo_pdf_group.go index f36f2fa..313b94a 100644 --- a/ofdgo_pdf_group.go +++ b/ofdgo_pdf_group.go @@ -93,7 +93,9 @@ func pdfOpaqueVisitor(visitor pdfgo.Visitor) pdfgo.Visitor { return nil } v := pdfgo.Visitor{ - Path: func(m pdfgo.PathMark) error { return check(m.Style, m.Fill, m.Stroke) }, + Reference: visitor.Reference, + OptionalContent: visitor.OptionalContent, + Path: func(m pdfgo.PathMark) error { return check(m.Style, m.Fill, m.Stroke) }, Text: func(m pdfgo.TextMark) error { return check(m.Style, m.Mode%4 == 0 || m.Mode%4 == 2, m.Mode%4 == 1 || m.Mode%4 == 2) }, @@ -134,10 +136,12 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) opaque := true scale := math.Hypot(p.matrix[0], p.matrix[1]) collect := pdfgo.Visitor{ - Warning: visitor.Warning, - Text: func(pdfgo.TextMark) error { return errPDFGroupRaster }, - Image: func(pdfgo.ImageMark) error { return errPDFGroupRaster }, - Group: func(pdfgo.GroupMark, func(pdfgo.Visitor) error) error { return errPDFGroupRaster }, + Reference: visitor.Reference, + OptionalContent: visitor.OptionalContent, + Warning: visitor.Warning, + Text: func(pdfgo.TextMark) error { return errPDFGroupRaster }, + Image: func(pdfgo.ImageMark) error { return errPDFGroupRaster }, + Group: func(pdfgo.GroupMark, func(pdfgo.Visitor) error) error { return errPDFGroupRaster }, } collect.Path = func(path pdfgo.PathMark) error { if path.Style.SoftMask != nil || path.Style.BlendMode != "" && path.Style.BlendMode != "Normal" && path.Style.BlendMode != "Compatible" { @@ -297,7 +301,7 @@ func (p *pdfImporter) rasterGroup(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) // visitor 绑定当前导入器的图元与透明组访问函数 // 返回: pdfgo.Visitor 页面访问器 func (p *pdfImporter) visitor() pdfgo.Visitor { - v := pdfgo.Visitor{Path: p.path, Text: p.text, Image: p.image, Warning: p.warning} + v := pdfgo.Visitor{Path: p.path, Text: p.text, Image: p.image, Warning: p.warning, Reference: p.referencePage} v.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { return p.group(mark, walk, v) } return v } diff --git a/ofdgo_pdf_object.go b/ofdgo_pdf_object.go index 51b7dc9..56dc391 100644 --- a/ofdgo_pdf_object.go +++ b/ofdgo_pdf_object.go @@ -188,7 +188,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error { if err := p.flushPath(); err != nil { return err } - visitor := pdfgo.Visitor{Path: p.path, Image: p.image, Warning: p.warning} + visitor := pdfgo.Visitor{Path: p.path, Image: p.image, Warning: p.warning, Reference: p.referencePage} visitor.Group = func(group pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { return p.group(group, walk, visitor) } diff --git a/ofdgo_pdf_reference.go b/ofdgo_pdf_reference.go new file mode 100644 index 0000000..3fcb52a --- /dev/null +++ b/ofdgo_pdf_reference.go @@ -0,0 +1,105 @@ +// 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 ( + "bytes" + "context" + "crypto/sha256" + "errors" + "fmt" + + "github.com/xiaoqidun/pdfgo" +) + +// pdfReferencePageKey 区分目标文档、索引和文本标签,避免重复遍历页面树 +type pdfReferencePageKey struct { + reader *pdfgo.Reader + index int64 + label string + labeled bool +} + +// referencePage 解析引用目标,目标不可用时按PDF标准保留代理内容 +// 入参: ctx 取消上下文, source 来源文档, reference 引用目标 +// 返回: *pdfgo.Page 目标页面,不可用时为空, error 取消或调用方解析错误 +func (p *pdfImporter) referencePage(ctx context.Context, source *pdfgo.Reader, reference pdfgo.ReferenceXObject) (*pdfgo.Page, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if p.resolveReference != nil { + return p.resolveReference(ctx, source, reference) + } + reader := p.referenceStreams[reference.File.Embedded] + if reader == nil { + if reference.File.Embedded == nil && p.resolveFile == nil { + return p.referenceProxy(ctx, fmt.Errorf("external reference file unavailable")) + } + data, err := source.ReadFileData(ctx, reference.File, p.resolveFile) + if err != nil { + return p.referenceProxy(ctx, err) + } + key := sha256.Sum256(data) + reader = p.referenceReaders[key] + if reader == nil { + reader, err = pdfgo.NewReaderWithOptions(bytes.NewReader(data), int64(len(data)), pdfgo.ReaderOptions{Warning: p.warning}) + if err != nil { + return p.referenceProxy(ctx, err) + } + p.referenceReaders[key] = reader + } + if reference.File.Embedded != nil { + p.referenceStreams[reference.File.Embedded] = reader + } + } + key := pdfReferencePageKey{reader: reader, index: reference.PageIndex} + if reference.PageLabel != nil { + key.index, key.label, key.labeled = 0, *reference.PageLabel, true + } + if page := p.referencePages[key]; page != nil { + return page, ctx.Err() + } + page, err := reader.ResolveReferencePage(ctx, reference) + if err != nil { + return p.referenceProxy(ctx, err) + } + p.referencePages[key] = page + return page, ctx.Err() +} + +// referenceProxy 记录目标不可用原因,不将合法的代理绘制视为转换失败 +// 入参: ctx 取消上下文, cause 目标不可用原因 +// 返回: *pdfgo.Page 空目标, error 取消错误 +func (p *pdfImporter) referenceProxy(ctx context.Context, cause error) (*pdfgo.Page, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if errors.Is(cause, context.Canceled) || errors.Is(cause, context.DeadlineExceeded) { + return nil, cause + } + if p.warning != nil { + p.warning(pdfgo.Diagnostic{Message: "reference XObject proxy retained: " + cause.Error()}) + } + return nil, nil +} + +// closeReferenceReaders 释放本次转换打开的引用文档,不关闭调用方提供的阅读器 +func (p *pdfImporter) closeReferenceReaders() { + for _, reader := range p.referenceReaders { + reader.Close() + } + p.referenceReaders, p.referenceStreams = nil, nil + p.referencePages = nil +}