feat(版本读取): 完善版本提取与文档读取取消
build.yaml / build (push) Successful in 50s

This commit is contained in:
xiaoqidun committed 2026-10-11 20:52:18 +08:00
1 parent 2e3809179b
commit d7e9604282
4 files changed
+137 -43

No files matched your search

+99 -18
View File
@@ -21,6 +21,7 @@ import (
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io" "io"
"maps"
"path" "path"
"slices" "slices"
"strings" "strings"
@@ -91,17 +92,28 @@ func (r *Reader) DocumentIndex() int {
// 入参: index 文档索引,从0开始 // 入参: index 文档索引,从0开始
// 返回: int 页数, error 错误信息 // 返回: int 页数, error 错误信息
func (r *Reader) DocumentPageCount(index int) (int, error) { func (r *Reader) DocumentPageCount(index int) (int, error) {
return r.DocumentPageCountContext(context.Background(), index)
}
// DocumentPageCountContext 获取指定文档页数,读取和解析时响应取消,不加载关联资源
// 入参: ctx 取消上下文, index 文档索引,从0开始
// 返回: int 页数, error 选择、读取或取消错误
func (r *Reader) DocumentPageCountContext(ctx context.Context, index int) (int, error) {
if err := ctx.Err(); err != nil {
return 0, err
}
if index < 0 || index >= r.DocumentCount() { if index < 0 || index >= r.DocumentCount() {
return 0, fmt.Errorf("document index %d out of range", index) return 0, fmt.Errorf("document index %d out of range", index)
} }
if index == r.documentIndex && r.doc != nil { if index == r.documentIndex && r.doc != nil {
return len(r.doc.Pages.Page), nil return len(r.doc.Pages.Page), nil
} }
root, err := r.documentEntry(index) view := *r
root, err := view.documentEntryContext(ctx, index)
if err != nil { if err != nil {
return 0, err return 0, err
} }
data, err := r.readFile(root) data, err := r.readFileViewContext(ctx, root)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -109,9 +121,16 @@ func (r *Reader) DocumentPageCount(index int) (int, error) {
XMLName xml.Name `xml:"Document"` XMLName xml.Name `xml:"Document"`
Pages Pages `xml:"Pages"` Pages Pages `xml:"Pages"`
} }
if err := xml.Unmarshal(data, &doc); err != nil { decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
if err := decoder.Decode(&doc); err != nil {
return 0, fmt.Errorf("failed to unmarshal document.xml: %w", err) return 0, fmt.Errorf("failed to unmarshal document.xml: %w", err)
} }
if err := ctx.Err(); err != nil {
return 0, err
}
if index == r.documentIndex {
r.documentRoot, r.versionInfo = view.documentRoot, view.versionInfo
}
return len(doc.Pages.Page), nil return len(doc.Pages.Page), nil
} }
@@ -151,6 +170,16 @@ func (r *Reader) Doc() (*Document, error) {
// docStructure 读取文档及资源索引,关联注解和签名由Doc按需解析 // docStructure 读取文档及资源索引,关联注解和签名由Doc按需解析
// 返回: *Document 文档结构, error 错误信息 // 返回: *Document 文档结构, error 错误信息
func (r *Reader) docStructure() (*Document, error) { func (r *Reader) docStructure() (*Document, error) {
return r.docStructureContext(context.Background())
}
// docStructureContext 读取文档及资源索引,取消或失败时保留原阅读状态
// 入参: ctx 取消上下文
// 返回: *Document 文档结构, error 读取、解析或取消错误
func (r *Reader) docStructureContext(ctx context.Context) (*Document, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if r.doc != nil { if r.doc != nil {
return r.doc, nil return r.doc, nil
} }
@@ -158,30 +187,55 @@ func (r *Reader) docStructure() (*Document, error) {
return nil, fmt.Errorf("no docbody found") return nil, fmt.Errorf("no docbody found")
} }
docAttr := r.OFD.DocBody[r.documentIndex] docAttr := r.OFD.DocBody[r.documentIndex]
docRootPath, err := r.DocumentRoot() view := *r
docRootPath, err := view.DocumentRootContext(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
r.RootDir = path.Dir(docRootPath) view.RootDir = path.Dir(docRootPath)
data, err := r.readFile(docRootPath) data, err := view.readFileViewContext(ctx, docRootPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var doc Document var doc Document
if err := xml.Unmarshal(data, &doc); err != nil { decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
if err := decoder.Decode(&doc); err != nil {
return nil, fmt.Errorf("failed to unmarshal document.xml: %w", err) return nil, fmt.Errorf("failed to unmarshal document.xml: %w", err)
} }
if doc.Signatures == "" { if doc.Signatures == "" {
doc.Signatures = r.versionSignatures(docAttr.Signatures) doc.Signatures = view.versionSignatures(docAttr.Signatures)
} }
view.resourcesRead = maps.Clone(r.resourcesRead)
view.resourceFiles = maps.Clone(r.resourceFiles)
view.ResMap = maps.Clone(r.ResMap)
view.mediaCache = maps.Clone(r.mediaCache)
view.fontCache = maps.Clone(r.fontCache)
view.colorSpaceCache = maps.Clone(r.colorSpaceCache)
view.drawParamCache = maps.Clone(r.drawParamCache)
view.compositeGraphicUnitCache = maps.Clone(r.compositeGraphicUnitCache)
for _, res := range doc.CommonData.DocumentRes { for _, res := range doc.CommonData.DocumentRes {
r.loadRes(res) if err := view.loadResContext(ctx, res); err != nil {
return nil, err
}
} }
for _, res := range doc.CommonData.PublicRes { for _, res := range doc.CommonData.PublicRes {
r.loadRes(res) if err := view.loadResContext(ctx, res); err != nil {
return nil, err
}
} }
r.doc = &doc if err := ctx.Err(); err != nil {
r.relatedPending = true return nil, err
}
maps.Copy(r.resourcesRead, view.resourcesRead)
maps.Copy(r.resourceFiles, view.resourceFiles)
maps.Copy(r.ResMap, view.ResMap)
maps.Copy(r.mediaCache, view.mediaCache)
maps.Copy(r.fontCache, view.fontCache)
maps.Copy(r.colorSpaceCache, view.colorSpaceCache)
maps.Copy(r.drawParamCache, view.drawParamCache)
maps.Copy(r.compositeGraphicUnitCache, view.compositeGraphicUnitCache)
r.documentRoot, r.versionInfo, r.RootDir = view.documentRoot, view.versionInfo, view.RootDir
r.doc, r.relatedPending = &doc, true
return r.doc, nil return r.doc, nil
} }
@@ -666,23 +720,37 @@ func readZipFile(f *zip.File) ([]byte, error) {
// loadRes 加载资源文件 // loadRes 加载资源文件
// 入参: resPath 资源路径 // 入参: resPath 资源路径
func (r *Reader) loadRes(resPath string) { func (r *Reader) loadRes(resPath string) {
_ = r.loadResContext(context.Background(), resPath)
}
// loadResContext 加载可读资源的索引,读取、解析和遍历时响应取消
// 入参: ctx 取消上下文, resPath 资源路径
// 返回: error 取消错误
func (r *Reader) loadResContext(ctx context.Context, resPath string) error {
if err := ctx.Err(); err != nil {
return err
}
if resPath == "" { if resPath == "" {
return return nil
} }
fullPath := r.ResPath(resPath) fullPath := r.ResPath(resPath)
if r.resourcesRead[fullPath] { if r.resourcesRead[fullPath] {
return return nil
} }
data, err := r.readFile(fullPath) data, err := r.readFileViewContext(ctx, fullPath)
if err != nil { if err != nil {
return return ctx.Err()
} }
var res Res var res Res
if err := xml.Unmarshal(data, &res); err != nil { decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
return if err := decoder.Decode(&res); err != nil {
return ctx.Err()
} }
baseLoc := res.BaseLoc baseLoc := res.BaseLoc
for i := range res.ColorSpaces.ColorSpace { for i := range res.ColorSpaces.ColorSpace {
if err := ctx.Err(); err != nil {
return err
}
cs := &res.ColorSpaces.ColorSpace[i] cs := &res.ColorSpaces.ColorSpace[i]
if cs.Profile != "" { if cs.Profile != "" {
cs.Profile = r.resourceLocation(fullPath, baseLoc, cs.Profile) cs.Profile = r.resourceLocation(fullPath, baseLoc, cs.Profile)
@@ -691,6 +759,9 @@ func (r *Reader) loadRes(resPath string) {
r.resourceFiles[cs.ID] = fullPath r.resourceFiles[cs.ID] = fullPath
} }
for _, mm := range res.MultiMedias.MultiMedia { for _, mm := range res.MultiMedias.MultiMedia {
if err := ctx.Err(); err != nil {
return err
}
if mm.MediaFile != "" { if mm.MediaFile != "" {
if finalPath := r.resourceLocation(fullPath, baseLoc, mm.MediaFile); finalPath != "" { if finalPath := r.resourceLocation(fullPath, baseLoc, mm.MediaFile); finalPath != "" {
mm.MediaFile = finalPath mm.MediaFile = finalPath
@@ -704,6 +775,9 @@ func (r *Reader) loadRes(resPath string) {
} }
} }
for i := range res.Fonts.Font { for i := range res.Fonts.Font {
if err := ctx.Err(); err != nil {
return err
}
f := &res.Fonts.Font[i] f := &res.Fonts.Font[i]
if f.FontFile != "" { if f.FontFile != "" {
f.FontFile = r.resourceLocation(fullPath, baseLoc, f.FontFile) f.FontFile = r.resourceLocation(fullPath, baseLoc, f.FontFile)
@@ -712,16 +786,23 @@ func (r *Reader) loadRes(resPath string) {
r.resourceFiles[f.ID] = fullPath r.resourceFiles[f.ID] = fullPath
} }
for i := range res.DrawParams.DrawParam { for i := range res.DrawParams.DrawParam {
if err := ctx.Err(); err != nil {
return err
}
dp := &res.DrawParams.DrawParam[i] dp := &res.DrawParams.DrawParam[i]
r.drawParamCache[dp.ID] = dp r.drawParamCache[dp.ID] = dp
r.resourceFiles[dp.ID] = fullPath r.resourceFiles[dp.ID] = fullPath
} }
for i := range res.CompositeGraphicUnits.CompositeGraphicUnit { for i := range res.CompositeGraphicUnits.CompositeGraphicUnit {
if err := ctx.Err(); err != nil {
return err
}
cgu := &res.CompositeGraphicUnits.CompositeGraphicUnit[i] cgu := &res.CompositeGraphicUnits.CompositeGraphicUnit[i]
r.compositeGraphicUnitCache[cgu.ID] = cgu r.compositeGraphicUnitCache[cgu.ID] = cgu
r.resourceFiles[cgu.ID] = fullPath r.resourceFiles[cgu.ID] = fullPath
} }
r.resourcesRead[fullPath] = true r.resourcesRead[fullPath] = true
return ctx.Err()
} }
// resourceLocation 按资源XML所在位置解析文件,文档目录外的共享文件保留绝对路径 // resourceLocation 按资源XML所在位置解析文件,文档目录外的共享文件保留绝对路径
-7
View File
@@ -166,13 +166,6 @@ func parseVersionEntries(ctx context.Context, body *editorXML) ([]DocumentVersio
return result, nil return result, nil
} }
// readDocumentVersion 读取版本描述,按描述文件位置解析文件清单和文档入口
// 入参: id 入口中的版本标识, location 描述文件路径
// 返回: DocumentVersionInfo 版本信息, error 读取或描述错误
func (r *Reader) readDocumentVersion(id, location string) (DocumentVersionInfo, error) {
return r.readDocumentVersionContext(context.Background(), id, location)
}
// readDocumentVersionContext 读取版本描述,按描述文件位置解析清单和文档入口 // readDocumentVersionContext 读取版本描述,按描述文件位置解析清单和文档入口
// 入参: ctx 取消上下文, id 入口中的版本标识, location 描述文件路径 // 入参: ctx 取消上下文, id 入口中的版本标识, location 描述文件路径
// 返回: DocumentVersionInfo 版本信息, error 读取、描述或取消错误 // 返回: DocumentVersionInfo 版本信息, error 读取、描述或取消错误
+38 -11
View File
@@ -16,6 +16,7 @@ package ofdgo
import ( import (
"archive/zip" "archive/zip"
"context"
"fmt" "fmt"
) )
@@ -25,12 +26,23 @@ import (
// 入参: id 版本标识,空值选择唯一的Current版本,没有默认版本或存在歧义时返回错误 // 入参: id 版本标识,空值选择唯一的Current版本,没有默认版本或存在歧义时返回错误
// 返回: *Reader 版本阅读器, error 选择、读取或文件缺失错误 // 返回: *Reader 版本阅读器, error 选择、读取或文件缺失错误
func (r *Reader) ExtractVersion(id string) (*Reader, error) { func (r *Reader) ExtractVersion(id string) (*Reader, error) {
entries, err := r.documentVersionEntries() return r.ExtractVersionContext(context.Background(), id)
}
// ExtractVersionContext 将指定版本提取为独立阅读器,扫描、读取和解析时响应取消
// 文件保留范围、入口选择和输入文件生命周期与ExtractVersion相同,取消或失败不改变原包
// 入参: ctx 取消上下文, id 版本标识,空值选择唯一的Current版本
// 返回: *Reader 版本阅读器, error 选择、读取或取消错误
func (r *Reader) ExtractVersionContext(ctx context.Context, id string) (*Reader, error) {
entries, err := r.documentVersionEntriesContext(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var selected *DocumentVersionInfo var selected *DocumentVersionInfo
for i := range entries { for i := range entries {
if err := ctx.Err(); err != nil {
return nil, err
}
entry := &entries[i] entry := &entries[i]
if id != "" && entry.ID == id || id == "" && entry.Current { if id != "" && entry.ID == id || id == "" && entry.Current {
if selected != nil { if selected != nil {
@@ -45,23 +57,29 @@ func (r *Reader) ExtractVersion(id string) (*Reader, error) {
} }
return nil, fmt.Errorf("document version not found: %s", id) return nil, fmt.Errorf("document version not found: %s", id)
} }
version, err := r.readDocumentVersion(selected.ID, selected.Location) version, err := r.readDocumentVersionContext(ctx, selected.ID, selected.Location)
if err != nil { if err != nil {
return nil, fmt.Errorf("version %s: %w", selected.ID, err) return nil, fmt.Errorf("version %s: %w", selected.ID, err)
} }
files := make(map[string]bool, len(version.Files)) files := make(map[string]bool, len(version.Files))
for _, file := range version.Files { for _, file := range version.Files {
if err := ctx.Err(); err != nil {
return nil, err
}
if !r.fileNames[file.Location] { if !r.fileNames[file.Location] {
return nil, fmt.Errorf("version %s file not found: %s", selected.ID, file.Location) return nil, fmt.Errorf("version %s file not found: %s", selected.ID, file.Location)
} }
files[file.Location] = true files[file.Location] = true
} }
root, err := r.extractedVersionRoot(version, files) root, err := r.extractedVersionRoot(ctx, version, files)
if err != nil { if err != nil {
return nil, err return nil, err
} }
next := &Reader{Path: r.Path, files: map[string][]byte{"OFD.xml": root}, encryption: r.encryption} next := &Reader{Path: r.Path, files: map[string][]byte{"OFD.xml": root}, encryption: r.encryption}
for name := range files { for name := range files {
if err := ctx.Err(); err != nil {
return nil, err
}
if name != "OFD.xml" { if name != "OFD.xml" {
if data, ok := r.files[name]; ok { if data, ok := r.files[name]; ok {
next.files[name] = data next.files[name] = data
@@ -71,30 +89,33 @@ func (r *Reader) ExtractVersion(id string) (*Reader, error) {
if r.Zip != nil { if r.Zip != nil {
next.Zip = &zip.Reader{Comment: r.Zip.Comment} next.Zip = &zip.Reader{Comment: r.Zip.Comment}
for _, file := range r.Zip.File { for _, file := range r.Zip.File {
if err := ctx.Err(); err != nil {
return nil, err
}
name := cleanPackagePath(file.Name) name := cleanPackagePath(file.Name)
if name != "OFD.xml" && files[name] && !file.FileInfo().IsDir() { if name != "OFD.xml" && files[name] && !file.FileInfo().IsDir() {
next.Zip.File = append(next.Zip.File, file) next.Zip.File = append(next.Zip.File, file)
} }
} }
} }
if err := next.initRoot(); err != nil { if err := next.initRootContext(ctx); err != nil {
return nil, err return nil, err
} }
if _, err := next.docStructure(); err != nil { if _, err := next.docStructureContext(ctx); err != nil {
return nil, err return nil, err
} }
return next, nil return next, nil
} }
// extractedVersionRoot 重建版本副本入口,保留元数据及命名空间,移除其他文档与版本列表 // extractedVersionRoot 重建版本副本入口,保留元数据及命名空间,移除其他文档与版本列表
// 入参: version 版本描述, files 版本包含的文件 // 入参: ctx 取消上下文, version 版本描述, files 版本包含的文件
// 返回: []byte 主入口XML, error 读取或结构错误 // 返回: []byte 主入口XML, error 读取、结构或取消错误
func (r *Reader) extractedVersionRoot(version DocumentVersionInfo, files map[string]bool) ([]byte, error) { func (r *Reader) extractedVersionRoot(ctx context.Context, version DocumentVersionInfo, files map[string]bool) ([]byte, error) {
data, err := r.readFile("OFD.xml") data, err := r.readFileViewContext(ctx, "OFD.xml")
if err != nil { if err != nil {
return nil, err return nil, err
} }
root, err := parseEditorXML(data) root, err := parseEditorXMLContext(ctx, data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -104,6 +125,9 @@ func (r *Reader) extractedVersionRoot(version DocumentVersionInfo, files map[str
} }
var patches []editorXMLPatch var patches []editorXMLPatch
for _, child := range root.children { for _, child := range root.children {
if err := ctx.Err(); err != nil {
return nil, err
}
if child.matchesOFD("DocBody") && child != body { if child.matchesOFD("DocBody") && child != body {
patches = append(patches, editorXMLPatch{start: child.start, end: child.end}) patches = append(patches, editorXMLPatch{start: child.start, end: child.end})
} }
@@ -130,11 +154,14 @@ func (r *Reader) extractedVersionRoot(version DocumentVersionInfo, files map[str
} }
data = editorPatchXML(data, patches) data = editorPatchXML(data, patches)
if version.Version != "" { if version.Version != "" {
root, err = parseEditorXML(data) root, err = parseEditorXMLContext(ctx, data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
data, err = editorXMLAttribute(data, root, "Version", version.Version) data, err = editorXMLAttribute(data, root, "Version", version.Version)
} }
if err == nil {
err = ctx.Err()
}
return data, err return data, err
} }
-7
View File
@@ -114,13 +114,6 @@ func (r *Reader) DocumentVersion(id string) (*Reader, error) {
return next, nil return next, nil
} }
// documentEntry 获取指定文档的阅读入口,当前文档保留显式选择,其他文档使用默认版本
// 入参: index 文档索引
// 返回: string 包内路径, error 选择错误
func (r *Reader) documentEntry(index int) (string, error) {
return r.documentEntryContext(context.Background(), index)
}
// documentEntryContext 解析指定文档的阅读入口,当前文档保留显式选择 // documentEntryContext 解析指定文档的阅读入口,当前文档保留显式选择
// 入参: ctx 取消上下文, index 文档索引 // 入参: ctx 取消上下文, index 文档索引
// 返回: string 包内路径, error 选择、读取或取消错误 // 返回: string 包内路径, error 选择、读取或取消错误