From d7e9604282cdf7e86d362d6ea92f3c1a5f9a3585 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Sun, 11 Oct 2026 20:52:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=89=88=E6=9C=AC=E8=AF=BB=E5=8F=96):=20?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=89=88=E6=9C=AC=E6=8F=90=E5=8F=96=E4=B8=8E?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E8=AF=BB=E5=8F=96=E5=8F=96=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_reader.go | 117 +++++++++++++++++++++++++++++++++------ ofdgo_version.go | 7 --- ofdgo_version_extract.go | 49 ++++++++++++---- ofdgo_version_reader.go | 7 --- 4 files changed, 137 insertions(+), 43 deletions(-) diff --git a/ofdgo_reader.go b/ofdgo_reader.go index b96b4b4..6ebab8d 100644 --- a/ofdgo_reader.go +++ b/ofdgo_reader.go @@ -21,6 +21,7 @@ import ( "encoding/xml" "fmt" "io" + "maps" "path" "slices" "strings" @@ -91,17 +92,28 @@ func (r *Reader) DocumentIndex() int { // 入参: index 文档索引,从0开始 // 返回: 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() { return 0, fmt.Errorf("document index %d out of range", index) } if index == r.documentIndex && r.doc != nil { return len(r.doc.Pages.Page), nil } - root, err := r.documentEntry(index) + view := *r + root, err := view.documentEntryContext(ctx, index) if err != nil { return 0, err } - data, err := r.readFile(root) + data, err := r.readFileViewContext(ctx, root) if err != nil { return 0, err } @@ -109,9 +121,16 @@ func (r *Reader) DocumentPageCount(index int) (int, error) { XMLName xml.Name `xml:"Document"` 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) } + 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 } @@ -151,6 +170,16 @@ func (r *Reader) Doc() (*Document, error) { // docStructure 读取文档及资源索引,关联注解和签名由Doc按需解析 // 返回: *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 { return r.doc, nil } @@ -158,30 +187,55 @@ func (r *Reader) docStructure() (*Document, error) { return nil, fmt.Errorf("no docbody found") } docAttr := r.OFD.DocBody[r.documentIndex] - docRootPath, err := r.DocumentRoot() + view := *r + docRootPath, err := view.DocumentRootContext(ctx) if err != nil { return nil, err } - r.RootDir = path.Dir(docRootPath) - data, err := r.readFile(docRootPath) + view.RootDir = path.Dir(docRootPath) + data, err := view.readFileViewContext(ctx, docRootPath) if err != nil { return nil, err } 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) } 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 { - r.loadRes(res) + if err := view.loadResContext(ctx, res); err != nil { + return nil, err + } } for _, res := range doc.CommonData.PublicRes { - r.loadRes(res) + if err := view.loadResContext(ctx, res); err != nil { + return nil, err + } } - r.doc = &doc - r.relatedPending = true + if err := ctx.Err(); err != nil { + 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 } @@ -666,23 +720,37 @@ func readZipFile(f *zip.File) ([]byte, error) { // loadRes 加载资源文件 // 入参: resPath 资源路径 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 == "" { - return + return nil } fullPath := r.ResPath(resPath) if r.resourcesRead[fullPath] { - return + return nil } - data, err := r.readFile(fullPath) + data, err := r.readFileViewContext(ctx, fullPath) if err != nil { - return + return ctx.Err() } var res Res - if err := xml.Unmarshal(data, &res); err != nil { - return + decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx}) + if err := decoder.Decode(&res); err != nil { + return ctx.Err() } baseLoc := res.BaseLoc for i := range res.ColorSpaces.ColorSpace { + if err := ctx.Err(); err != nil { + return err + } cs := &res.ColorSpaces.ColorSpace[i] if 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 } for _, mm := range res.MultiMedias.MultiMedia { + if err := ctx.Err(); err != nil { + return err + } if mm.MediaFile != "" { if finalPath := r.resourceLocation(fullPath, baseLoc, mm.MediaFile); finalPath != "" { mm.MediaFile = finalPath @@ -704,6 +775,9 @@ func (r *Reader) loadRes(resPath string) { } } for i := range res.Fonts.Font { + if err := ctx.Err(); err != nil { + return err + } f := &res.Fonts.Font[i] if 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 } for i := range res.DrawParams.DrawParam { + if err := ctx.Err(); err != nil { + return err + } dp := &res.DrawParams.DrawParam[i] r.drawParamCache[dp.ID] = dp r.resourceFiles[dp.ID] = fullPath } for i := range res.CompositeGraphicUnits.CompositeGraphicUnit { + if err := ctx.Err(); err != nil { + return err + } cgu := &res.CompositeGraphicUnits.CompositeGraphicUnit[i] r.compositeGraphicUnitCache[cgu.ID] = cgu r.resourceFiles[cgu.ID] = fullPath } r.resourcesRead[fullPath] = true + return ctx.Err() } // resourceLocation 按资源XML所在位置解析文件,文档目录外的共享文件保留绝对路径 diff --git a/ofdgo_version.go b/ofdgo_version.go index f56edbf..a6fe6ee 100644 --- a/ofdgo_version.go +++ b/ofdgo_version.go @@ -166,13 +166,6 @@ func parseVersionEntries(ctx context.Context, body *editorXML) ([]DocumentVersio 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 读取版本描述,按描述文件位置解析清单和文档入口 // 入参: ctx 取消上下文, id 入口中的版本标识, location 描述文件路径 // 返回: DocumentVersionInfo 版本信息, error 读取、描述或取消错误 diff --git a/ofdgo_version_extract.go b/ofdgo_version_extract.go index 6f9d03f..daf5712 100644 --- a/ofdgo_version_extract.go +++ b/ofdgo_version_extract.go @@ -16,6 +16,7 @@ package ofdgo import ( "archive/zip" + "context" "fmt" ) @@ -25,12 +26,23 @@ import ( // 入参: id 版本标识,空值选择唯一的Current版本,没有默认版本或存在歧义时返回错误 // 返回: *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 { return nil, err } var selected *DocumentVersionInfo for i := range entries { + if err := ctx.Err(); err != nil { + return nil, err + } entry := &entries[i] if id != "" && entry.ID == id || id == "" && entry.Current { 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) } - version, err := r.readDocumentVersion(selected.ID, selected.Location) + version, err := r.readDocumentVersionContext(ctx, selected.ID, selected.Location) if err != nil { return nil, fmt.Errorf("version %s: %w", selected.ID, err) } files := make(map[string]bool, len(version.Files)) for _, file := range version.Files { + if err := ctx.Err(); err != nil { + return nil, err + } if !r.fileNames[file.Location] { return nil, fmt.Errorf("version %s file not found: %s", selected.ID, file.Location) } files[file.Location] = true } - root, err := r.extractedVersionRoot(version, files) + root, err := r.extractedVersionRoot(ctx, version, files) if err != nil { return nil, err } next := &Reader{Path: r.Path, files: map[string][]byte{"OFD.xml": root}, encryption: r.encryption} for name := range files { + if err := ctx.Err(); err != nil { + return nil, err + } if name != "OFD.xml" { if data, ok := r.files[name]; ok { next.files[name] = data @@ -71,30 +89,33 @@ func (r *Reader) ExtractVersion(id string) (*Reader, error) { if r.Zip != nil { next.Zip = &zip.Reader{Comment: r.Zip.Comment} for _, file := range r.Zip.File { + if err := ctx.Err(); err != nil { + return nil, err + } name := cleanPackagePath(file.Name) if name != "OFD.xml" && files[name] && !file.FileInfo().IsDir() { next.Zip.File = append(next.Zip.File, file) } } } - if err := next.initRoot(); err != nil { + if err := next.initRootContext(ctx); err != nil { return nil, err } - if _, err := next.docStructure(); err != nil { + if _, err := next.docStructureContext(ctx); err != nil { return nil, err } return next, nil } // extractedVersionRoot 重建版本副本入口,保留元数据及命名空间,移除其他文档与版本列表 -// 入参: version 版本描述, files 版本包含的文件 -// 返回: []byte 主入口XML, error 读取或结构错误 -func (r *Reader) extractedVersionRoot(version DocumentVersionInfo, files map[string]bool) ([]byte, error) { - data, err := r.readFile("OFD.xml") +// 入参: ctx 取消上下文, version 版本描述, files 版本包含的文件 +// 返回: []byte 主入口XML, error 读取、结构或取消错误 +func (r *Reader) extractedVersionRoot(ctx context.Context, version DocumentVersionInfo, files map[string]bool) ([]byte, error) { + data, err := r.readFileViewContext(ctx, "OFD.xml") if err != nil { return nil, err } - root, err := parseEditorXML(data) + root, err := parseEditorXMLContext(ctx, data) if err != nil { return nil, err } @@ -104,6 +125,9 @@ func (r *Reader) extractedVersionRoot(version DocumentVersionInfo, files map[str } var patches []editorXMLPatch for _, child := range root.children { + if err := ctx.Err(); err != nil { + return nil, err + } if child.matchesOFD("DocBody") && child != body { 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) if version.Version != "" { - root, err = parseEditorXML(data) + root, err = parseEditorXMLContext(ctx, data) if err != nil { return nil, err } data, err = editorXMLAttribute(data, root, "Version", version.Version) } + if err == nil { + err = ctx.Err() + } return data, err } diff --git a/ofdgo_version_reader.go b/ofdgo_version_reader.go index c391f3d..dded18d 100644 --- a/ofdgo_version_reader.go +++ b/ofdgo_version_reader.go @@ -114,13 +114,6 @@ func (r *Reader) DocumentVersion(id string) (*Reader, error) { return next, nil } -// documentEntry 获取指定文档的阅读入口,当前文档保留显式选择,其他文档使用默认版本 -// 入参: index 文档索引 -// 返回: string 包内路径, error 选择错误 -func (r *Reader) documentEntry(index int) (string, error) { - return r.documentEntryContext(context.Background(), index) -} - // documentEntryContext 解析指定文档的阅读入口,当前文档保留显式选择 // 入参: ctx 取消上下文, index 文档索引 // 返回: string 包内路径, error 选择、读取或取消错误