feat(转换能力): 支持引用页面与代理转换
build.yaml / build (push) Successful in 52s

This commit is contained in:
xiaoqidun committed 2026-10-03 00:49:15 +08:00
1 parent 012569dbc0
commit 1945ea355a
7 files changed
+142 -22

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1 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/crypto v0.57.0
golang.org/x/image v0.46.0 golang.org/x/image v0.46.0
golang.org/x/text v0.42.0 golang.org/x/text v0.42.0
+2 -2
View File
@@ -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/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 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= 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-20261002164637-41541220b1c3 h1:mrWCBsxLuBf1sjeEK9r+1KZa5WpER5J3sDPVicajM1E=
github.com/xiaoqidun/pdfgo v0.0.0-20261002153346-125ae06f23bf/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= 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 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=
+22 -11
View File
@@ -35,17 +35,19 @@ var ErrPDFPassword = pdfgo.ErrPassword
// RendererOptions复用渲染器字体来源和后端配置,Backends优先于其中的后端设置 // RendererOptions复用渲染器字体来源和后端配置,Backends优先于其中的后端设置
// RasterDPI指定局部透明效果合成精度,0沿用渲染器DPI,默认300dpi,Strict禁用局部合成 // RasterDPI指定局部透明效果合成精度,0沿用渲染器DPI,默认300dpi,Strict禁用局部合成
// OnProgress按open、pages、convert及write.*阶段报告进度,total为0表示总量未知 // OnProgress按open、pages、convert及write.*阶段报告进度,total为0表示总量未知
// ResolveFile读取PDF引用的外部媒体和附件,默认不访问网络或本地路径 // ResolveFile读取PDF引用的外部媒体、附件和页面,默认不访问网络或本地路径
// ResolveReference可提供已解密的引用页面,目标阅读器由调用方维护
type PDFImportOptions struct { type PDFImportOptions struct {
Password []byte Password []byte
PasswordUTF8 bool PasswordUTF8 bool
Backends *RenderBackends Backends *RenderBackends
RendererOptions []RendererOption RendererOptions []RendererOption
Progress func(int) error Progress func(int) error
OnProgress func(stage string, completed, total int) error OnProgress func(stage string, completed, total int) error
Strict bool Strict bool
RasterDPI float64 RasterDPI float64
ResolveFile pdfgo.FileResolver ResolveFile pdfgo.FileResolver
ResolveReference pdfgo.ReferenceResolver
} }
// PDFImportReport 汇总转换页数、对象数、链接数和转换警告 // PDFImportReport 汇总转换页数、对象数、链接数和转换警告
@@ -91,6 +93,10 @@ type pdfImporter struct {
compositeSpace *pdfgo.ColorSpace compositeSpace *pdfgo.ColorSpace
annotationData []byte annotationData []byte
resolveFile pdfgo.FileResolver 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 halftoneWarnings map[*pdfgo.Halftone]bool
transferBackdrop bool transferBackdrop bool
} }
@@ -137,6 +143,11 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
importer.report.Warnings = diagnostics importer.report.Warnings = diagnostics
importer.pageIndexes = make(map[pdfgo.Reference]int) importer.pageIndexes = make(map[pdfgo.Reference]int)
importer.resolveFile = options.ResolveFile 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) importer.halftoneWarnings = make(map[*pdfgo.Halftone]bool)
if security := reader.Encryption(); security != nil && !security.Owner && security.Permissions&0xf3c != 0xf3c { if security := reader.Encryption(); security != nil && !security.Owner && security.Permissions&0xf3c != 0xf3c {
if options.Strict { 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.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
cell.objects, cell.pendingPath = nil, nil 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 { visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
return cell.group(mark, walk, visitor) return cell.group(mark, walk, visitor)
} }
+1 -1
View File
@@ -245,7 +245,7 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
var collect func(func(pdfgo.Visitor) error) ([]pdfCompositeNode, error) var collect func(func(pdfgo.Visitor) error) ([]pdfCompositeNode, error)
collect = func(walk func(pdfgo.Visitor) error) ([]pdfCompositeNode, error) { collect = func(walk func(pdfgo.Visitor) error) ([]pdfCompositeNode, error) {
var nodes []pdfCompositeNode 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 { v.Path = func(mark pdfgo.PathMark) error {
if err := p.halftone(mark.Style); err != nil { if err := p.halftone(mark.Style); err != nil {
return err return err
+10 -6
View File
@@ -93,7 +93,9 @@ func pdfOpaqueVisitor(visitor pdfgo.Visitor) pdfgo.Visitor {
return nil return nil
} }
v := pdfgo.Visitor{ 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 { 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) 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 opaque := true
scale := math.Hypot(p.matrix[0], p.matrix[1]) scale := math.Hypot(p.matrix[0], p.matrix[1])
collect := pdfgo.Visitor{ collect := pdfgo.Visitor{
Warning: visitor.Warning, Reference: visitor.Reference,
Text: func(pdfgo.TextMark) error { return errPDFGroupRaster }, OptionalContent: visitor.OptionalContent,
Image: func(pdfgo.ImageMark) error { return errPDFGroupRaster }, Warning: visitor.Warning,
Group: func(pdfgo.GroupMark, func(pdfgo.Visitor) error) error { return errPDFGroupRaster }, 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 { collect.Path = func(path pdfgo.PathMark) error {
if path.Style.SoftMask != nil || path.Style.BlendMode != "" && path.Style.BlendMode != "Normal" && path.Style.BlendMode != "Compatible" { 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 绑定当前导入器的图元与透明组访问函数 // visitor 绑定当前导入器的图元与透明组访问函数
// 返回: pdfgo.Visitor 页面访问器 // 返回: pdfgo.Visitor 页面访问器
func (p *pdfImporter) 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) } v.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { return p.group(mark, walk, v) }
return v return v
} }
+1 -1
View File
@@ -188,7 +188,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
if err := p.flushPath(); err != nil { if err := p.flushPath(); err != nil {
return err 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 { visitor.Group = func(group pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
return p.group(group, walk, visitor) return p.group(group, walk, visitor)
} }
+105
View File
@@ -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
}