mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:59:27 +08:00
1235 lines
39 KiB
Go
1235 lines
39 KiB
Go
// 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 (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"maps"
|
|
"path"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// Reader OFD文件阅读器
|
|
// Reader及共享它的Renderer需串行调用
|
|
type Reader struct {
|
|
Path string
|
|
Zip *zip.Reader
|
|
Closer io.Closer
|
|
OFD *OFD
|
|
RootDir string
|
|
ResMap map[string]string
|
|
resourcesRead map[string]bool
|
|
resourceFiles map[string]string
|
|
imageCatalog []ImageInfo
|
|
resourceInfoCache []ResourceInfo
|
|
mediaCache map[string]MultiMedia
|
|
fontCache map[string]*Font
|
|
fontResourcesRead bool
|
|
fontFaces map[string][]*FontFace
|
|
colorSpaceCache map[string]*ColorSpace
|
|
drawParamCache map[string]*DrawParam
|
|
compositeGraphicUnitCache map[string]*CompositeGraphicUnit
|
|
pageHeaderCache map[string]PageContent
|
|
doc *Document
|
|
relatedPending bool
|
|
documentIndex int
|
|
documentRoot string
|
|
selectedVersion *string
|
|
versionInfo *DocumentVersionInfo
|
|
Stamps map[string][]Stamp
|
|
Annots map[string][]Annotation
|
|
annotationFiles map[string][]string
|
|
fileIndex map[string]*zip.File
|
|
files map[string][]byte
|
|
fileNames map[string]bool
|
|
encryption *encryptionState
|
|
}
|
|
|
|
// documentPageList 仅读取文档中的页面索引,统计页数时不构建其他结构
|
|
type documentPageList struct {
|
|
Pages Pages
|
|
}
|
|
|
|
// UnmarshalXML 按文档命名空间读取页面列表,跳过其他文档结构和扩展
|
|
// 入参: d XML解码器, start 起始节点
|
|
// 返回: error 解析错误
|
|
func (list *documentPageList) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
if start.Name.Local != "Document" {
|
|
return fmt.Errorf("expected Document root, got %s", start.Name.Local)
|
|
}
|
|
*list = documentPageList{}
|
|
return decodeOFDChildren(d, start, func(d *xml.Decoder, node xml.StartElement) error {
|
|
if node.Name.Local == "Pages" {
|
|
return d.DecodeElement(&list.Pages, &node)
|
|
}
|
|
return d.Skip()
|
|
})
|
|
}
|
|
|
|
// Close 关闭阅读器持有的文件,重复调用不再关闭底层文件
|
|
// 返回: error 错误信息
|
|
func (r *Reader) Close() error {
|
|
if r.Closer != nil {
|
|
closer := r.Closer
|
|
r.Closer = nil
|
|
return closer.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DocumentCount 获取包内文档数量
|
|
// 返回: int 文档数量
|
|
func (r *Reader) DocumentCount() int {
|
|
if r.OFD == nil {
|
|
return 0
|
|
}
|
|
return len(r.OFD.DocBody)
|
|
}
|
|
|
|
// DocumentIndex 获取当前文档索引,从0开始
|
|
// 返回: int 文档索引
|
|
func (r *Reader) DocumentIndex() int {
|
|
return r.documentIndex
|
|
}
|
|
|
|
// DocumentPageCount 获取指定文档页数,不切换当前文档或加载资源、注释和签名
|
|
// 入参: 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
|
|
}
|
|
view := *r
|
|
root, err := view.documentEntryContext(ctx, index)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
data, err := r.readFileViewContext(ctx, root)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
var doc documentPageList
|
|
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
|
|
}
|
|
|
|
// Document 创建指定文档的独立阅读器,不改变当前文档或混用资源缓存
|
|
// 返回的阅读器不关闭输入文件,使用期间不得关闭持有输入文件的原阅读器
|
|
// 入参: index 文档索引,从0开始
|
|
// 返回: *Reader 文档阅读器, error 错误信息
|
|
func (r *Reader) Document(index int) (*Reader, error) {
|
|
return r.DocumentContext(context.Background(), index)
|
|
}
|
|
|
|
// DocumentContext 创建指定文档的独立阅读器,读取结构和关联内容时响应取消
|
|
// 返回值不关闭输入文件,使用期间不得关闭持有输入文件的原阅读器
|
|
// 入参: ctx 取消上下文, index 文档索引,从0开始
|
|
// 返回: *Reader 文档阅读器, error 读取或取消错误
|
|
func (r *Reader) DocumentContext(ctx context.Context, index int) (*Reader, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if index < 0 || index >= r.DocumentCount() {
|
|
return nil, fmt.Errorf("document index %d out of range", index)
|
|
}
|
|
next := &Reader{Path: r.Path, Zip: r.Zip, files: r.files, encryption: r.encryption, documentIndex: index}
|
|
if err := next.initRootContext(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := next.DocContext(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
return next, nil
|
|
}
|
|
|
|
// Doc 获取当前文档结构
|
|
// 返回: *Document 文档结构, error 错误信息
|
|
func (r *Reader) Doc() (*Document, error) {
|
|
return r.DocContext(context.Background())
|
|
}
|
|
|
|
// DocContext 获取当前文档结构及关联内容,取消或失败时保留原阅读状态
|
|
// 入参: ctx 取消上下文
|
|
// 返回: *Document 文档结构, error 读取或取消错误
|
|
func (r *Reader) DocContext(ctx context.Context) (*Document, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if r.doc != nil && !r.relatedPending {
|
|
return r.doc, nil
|
|
}
|
|
view := *r
|
|
if r.doc == nil {
|
|
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)
|
|
}
|
|
doc, err := view.docStructureContext(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if view.relatedPending {
|
|
view.Annots = maps.Clone(r.Annots)
|
|
for page, entries := range view.Annots {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
view.Annots[page] = slices.Clone(entries)
|
|
}
|
|
view.Stamps = maps.Clone(r.Stamps)
|
|
for page, entries := range view.Stamps {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
view.Stamps[page] = slices.Clone(entries)
|
|
}
|
|
_ = view.parseAnnotations(ctx, doc)
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
_ = view.parseSignatures(ctx, doc)
|
|
view.relatedPending = false
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if r.doc == nil {
|
|
r.commitDocumentStructure(&view)
|
|
} else {
|
|
r.relatedPending = view.relatedPending
|
|
}
|
|
r.Annots, r.Stamps, r.annotationFiles = view.Annots, view.Stamps, view.annotationFiles
|
|
return doc, nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
if r.OFD == nil || r.documentIndex < 0 || r.documentIndex >= len(r.OFD.DocBody) {
|
|
return nil, fmt.Errorf("no docbody found")
|
|
}
|
|
docAttr := r.OFD.DocBody[r.documentIndex]
|
|
view := *r
|
|
docRootPath, err := view.DocumentRootContext(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
view.RootDir = path.Dir(docRootPath)
|
|
data, err := view.readFileViewContext(ctx, docRootPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var doc Document
|
|
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)
|
|
}
|
|
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 {
|
|
if err := view.loadResContext(ctx, res); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
for _, res := range doc.CommonData.PublicRes {
|
|
if err := view.loadResContext(ctx, res); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
view.doc, view.relatedPending = &doc, true
|
|
r.commitDocumentStructure(&view)
|
|
return r.doc, nil
|
|
}
|
|
|
|
// commitDocumentStructure 提交已完成的文档读取,保留渲染器共享的资源索引
|
|
// 入参: view 已验证的独立读取状态
|
|
func (r *Reader) commitDocumentStructure(view *Reader) {
|
|
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 = view.doc, view.relatedPending
|
|
}
|
|
|
|
// PageContent 获取页面内容
|
|
// 每次返回独立的页面内容,修改后可传入Renderer重绘,不会写回原文件
|
|
// 入参: page 页面对象
|
|
// 返回: *PageContent 页面内容, error 错误信息
|
|
func (r *Reader) PageContent(page Page) (*PageContent, error) {
|
|
return r.PageContentContext(context.Background(), page)
|
|
}
|
|
|
|
// PageContentContext 读取独立页面内容,取消时不返回部分页面
|
|
// 入参: ctx 取消上下文, page 页面对象
|
|
// 返回: *PageContent 页面内容, error 读取、解析或取消错误
|
|
func (r *Reader) PageContentContext(ctx context.Context, page Page) (*PageContent, error) {
|
|
return r.readPageContent(ctx, page, false, false)
|
|
}
|
|
|
|
// PageCount 获取当前文档总页数,不加载注解或签名
|
|
// 返回: int 页数, error 错误信息
|
|
func (r *Reader) PageCount() (int, error) {
|
|
doc, err := r.docStructure()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return len(doc.Pages.Page), nil
|
|
}
|
|
|
|
// PageContentByIndex 按当前文档页面顺序读取页面内容
|
|
// 入参: index 页面索引,从0开始
|
|
// 返回: *PageContent 页面内容, error 错误信息
|
|
func (r *Reader) PageContentByIndex(index int) (*PageContent, error) {
|
|
return r.PageContentByIndexContext(context.Background(), index)
|
|
}
|
|
|
|
// PageContentByIndexContext 按当前文档页面顺序读取独立页面内容
|
|
// 入参: ctx 取消上下文, index 页面索引,从0开始
|
|
// 返回: *PageContent 页面内容, error 索引、读取或取消错误
|
|
func (r *Reader) PageContentByIndexContext(ctx context.Context, index int) (*PageContent, error) {
|
|
doc, err := r.DocContext(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if index < 0 || index >= len(doc.Pages.Page) {
|
|
return nil, fmt.Errorf("page index %d out of range", index)
|
|
}
|
|
return r.PageContentContext(ctx, doc.Pages.Page[index])
|
|
}
|
|
|
|
// PageArea 读取页面区域,不解析页面图元
|
|
// 入参: page 页面对象
|
|
// 返回: PageArea 页面区域, error 错误信息
|
|
func (r *Reader) PageArea(page Page) (PageArea, error) {
|
|
return r.PageAreaContext(context.Background(), page)
|
|
}
|
|
|
|
// PageAreaContext 读取页面区域并解析模板继承,不解析页面图元
|
|
// 入参: ctx 取消上下文, page 页面对象
|
|
// 返回: PageArea 页面区域, error 读取、解析或取消错误
|
|
func (r *Reader) PageAreaContext(ctx context.Context, page Page) (PageArea, error) {
|
|
content, err := r.readPageHeaderContext(ctx, page)
|
|
if err != nil {
|
|
return PageArea{}, err
|
|
}
|
|
return r.resolvePageAreaContext(ctx, content.Area, content.Template)
|
|
}
|
|
|
|
// ResPath 按当前文档目录解析ST_Loc,包内绝对路径以根目录为基准
|
|
// 入参: resLink 资源链接
|
|
// 返回: string 完整路径
|
|
func (r *Reader) ResPath(resLink string) string {
|
|
if resLink == "" {
|
|
return ""
|
|
}
|
|
resLink = strings.ReplaceAll(resLink, "\\", "/")
|
|
resLink = strings.TrimSpace(resLink)
|
|
if resLink == "" {
|
|
return ""
|
|
}
|
|
if strings.HasPrefix(resLink, "/") {
|
|
return cleanPackagePath(resLink)
|
|
}
|
|
resLink = path.Clean(resLink)
|
|
rootDir := strings.TrimPrefix(strings.ReplaceAll(r.RootDir, "\\", "/"), "/")
|
|
return cleanPackagePath(path.Join(rootDir, resLink))
|
|
}
|
|
|
|
// ResData 获取资源文件数据
|
|
// 入参: resLink 资源链接
|
|
// 返回: []byte 资源数据, error 错误信息
|
|
func (r *Reader) ResData(resLink string) ([]byte, error) {
|
|
fullPath := r.ResPath(resLink)
|
|
return r.readFile(fullPath)
|
|
}
|
|
|
|
// Version 获取OFD版本号
|
|
// 返回: string 版本号
|
|
func (r *Reader) Version() string {
|
|
if r.OFD == nil {
|
|
return ""
|
|
}
|
|
return r.OFD.Version
|
|
}
|
|
|
|
// DocType 获取文档类型
|
|
// 返回: string 文档类型
|
|
func (r *Reader) DocType() string {
|
|
if r.OFD == nil {
|
|
return ""
|
|
}
|
|
return r.OFD.DocType
|
|
}
|
|
|
|
// DocInfo 获取文档元数据
|
|
// 返回: *DocInfo 元数据, error 错误信息
|
|
func (r *Reader) DocInfo() (*DocInfo, error) {
|
|
if r.OFD == nil || r.documentIndex < 0 || r.documentIndex >= len(r.OFD.DocBody) {
|
|
return nil, fmt.Errorf("no docbody found")
|
|
}
|
|
return &r.OFD.DocBody[r.documentIndex].DocInfo, nil
|
|
}
|
|
|
|
// Permissions 获取文档权限信息,不加载注解或签名
|
|
// 返回: *Permissions 权限信息, error 错误信息
|
|
func (r *Reader) Permissions() (*Permissions, error) {
|
|
doc, err := r.docStructure()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &doc.Permissions, nil
|
|
}
|
|
|
|
// Outlines 获取文档大纲,不加载注解或签名
|
|
// 返回: []OutlineElem 大纲列表, error 错误信息
|
|
func (r *Reader) Outlines() ([]OutlineElem, error) {
|
|
doc, err := r.docStructure()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return doc.Outlines.OutlineElem, nil
|
|
}
|
|
|
|
// Actions 获取文档动作,不加载注解或签名
|
|
// 返回: []Action 动作列表, error 错误信息
|
|
func (r *Reader) Actions() ([]Action, error) {
|
|
doc, err := r.docStructure()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return doc.Actions, nil
|
|
}
|
|
|
|
// Bookmarks 获取文档书签,不加载注解或签名
|
|
// 返回: []Bookmark 书签列表, error 错误信息
|
|
func (r *Reader) Bookmarks() ([]Bookmark, error) {
|
|
doc, err := r.docStructure()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return doc.Bookmarks.Bookmark, nil
|
|
}
|
|
|
|
// Attachments 获取独立的附件列表,修改返回值不影响阅读器缓存
|
|
// 返回: []Attachment 附件列表, error 错误信息
|
|
func (r *Reader) Attachments() ([]Attachment, error) {
|
|
return r.AttachmentsContext(context.Background())
|
|
}
|
|
|
|
// AttachmentsContext 获取独立的附件列表,不加载无关注解或签名
|
|
// 取消或失败时不缓存未完成的附件清单,修改返回值不影响缓存
|
|
// 入参: ctx 取消上下文
|
|
// 返回: []Attachment 附件列表, error 读取、解析或取消错误
|
|
func (r *Reader) AttachmentsContext(ctx context.Context) ([]Attachment, error) {
|
|
return r.attachmentsContext(ctx, false)
|
|
}
|
|
|
|
// attachmentsContext 按调用方所有权读取附件清单,库内只读访问不复制完整清单
|
|
// 入参: ctx 取消上下文, readonly 是否允许借用只读清单
|
|
// 返回: []Attachment 附件列表, error 读取、解析或取消错误
|
|
func (r *Reader) attachmentsContext(ctx context.Context, readonly bool) ([]Attachment, error) {
|
|
doc, err := r.docStructureContext(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := doc.Attachments.Attachment
|
|
pending := strings.TrimSpace(doc.Attachments.Path) != "" && items == nil
|
|
if pending {
|
|
var attachments Attachments
|
|
partPath, err := r.readDocumentPartContext(ctx, doc.Attachments.Path, &attachments)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range attachments.Attachment {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
attachment := &attachments.Attachment[i]
|
|
if fileLoc := resolveResourcePath(partPath, "", attachment.FileLoc); fileLoc != "" {
|
|
attachment.FileLoc = "/" + fileLoc
|
|
}
|
|
}
|
|
if attachments.Attachment == nil {
|
|
attachments.Attachment = []Attachment{}
|
|
}
|
|
items = attachments.Attachment
|
|
}
|
|
attachments := items
|
|
if !readonly {
|
|
if items != nil {
|
|
attachments = make([]Attachment, len(items))
|
|
}
|
|
for i := range items {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
attachments[i] = items[i]
|
|
if attachments[i].Size != nil {
|
|
size := *attachments[i].Size
|
|
attachments[i].Size = &size
|
|
}
|
|
}
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if pending {
|
|
doc.Attachments.Attachment = items
|
|
}
|
|
return attachments, nil
|
|
}
|
|
|
|
// OpenAttachment 打开附件文件流,调用方负责关闭,完整读取后校验ZIP数据
|
|
// 入参: id 附件标识
|
|
// 返回: io.ReadCloser 附件文件流, error 错误信息
|
|
func (r *Reader) OpenAttachment(id string) (io.ReadCloser, error) {
|
|
return r.OpenAttachmentContext(context.Background(), id)
|
|
}
|
|
|
|
// OpenAttachmentContext 打开可取消读取的附件文件流,调用方负责关闭
|
|
// 完整读取后校验ZIP数据,不加载无关注解或签名
|
|
// 入参: ctx 取消上下文, id 附件标识
|
|
// 返回: io.ReadCloser 附件文件流, error 读取或取消错误
|
|
func (r *Reader) OpenAttachmentContext(ctx context.Context, id string) (io.ReadCloser, error) {
|
|
attachments, err := r.attachmentsContext(ctx, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, attachment := range attachments {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if attachment.ID == id {
|
|
stream, err := r.openFile(r.ResPath(attachment.FileLoc))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
stream.Close()
|
|
return nil, err
|
|
}
|
|
return imageInput{ReadCloser: stream, context: ctx}, nil
|
|
}
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, fmt.Errorf("attachment not found: %s", id)
|
|
}
|
|
|
|
// AttachmentData 获取附件文件数据
|
|
// 入参: id 附件标识
|
|
// 返回: []byte 附件数据, error 错误信息
|
|
func (r *Reader) AttachmentData(id string) ([]byte, error) {
|
|
return r.AttachmentDataContext(context.Background(), id)
|
|
}
|
|
|
|
// AttachmentDataContext 获取独立的附件数据,读取失败或取消时不返回部分内容
|
|
// 入参: ctx 取消上下文, id 附件标识
|
|
// 返回: []byte 附件数据, error 读取或取消错误
|
|
func (r *Reader) AttachmentDataContext(ctx context.Context, id string) ([]byte, error) {
|
|
stream, err := r.OpenAttachmentContext(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer stream.Close()
|
|
data, err := io.ReadAll(stream)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// CustomTags 获取自定义标引
|
|
// 返回的文件路径可直接传入ResData读取
|
|
// 返回: []CustomTag 自定义标引列表, error 错误信息
|
|
func (r *Reader) CustomTags() ([]CustomTag, error) {
|
|
return r.CustomTagsContext(context.Background())
|
|
}
|
|
|
|
// CustomTagsContext 读取当前文档及版本的标引清单,取消或失败时不缓存未完成的清单
|
|
// 不加载标引内容、页面、注解或签名;返回的文件路径可直接传入ResData读取
|
|
// 入参: ctx 取消上下文
|
|
// 返回: []CustomTag 自定义标引列表, error 读取、解析或取消错误
|
|
func (r *Reader) CustomTagsContext(ctx context.Context) ([]CustomTag, error) {
|
|
doc, err := r.docStructureContext(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if strings.TrimSpace(doc.CustomTags.Path) != "" && doc.CustomTags.CustomTag == nil {
|
|
var customTags CustomTags
|
|
partPath, err := r.readDocumentPartContext(ctx, doc.CustomTags.Path, &customTags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range customTags.CustomTag {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
tag := &customTags.CustomTag[i]
|
|
tag.SchemaLoc = r.resourceLocation(partPath, "", tag.SchemaLoc)
|
|
tag.FileLoc = r.resourceLocation(partPath, "", tag.FileLoc)
|
|
}
|
|
if customTags.CustomTag == nil {
|
|
customTags.CustomTag = []CustomTag{}
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
doc.CustomTags.CustomTag = customTags.CustomTag
|
|
}
|
|
return doc.CustomTags.CustomTag, nil
|
|
}
|
|
|
|
// CustomDatas 获取自定义数据
|
|
// 返回: []CustomData 自定义数据列表, error 错误信息
|
|
func (r *Reader) CustomDatas() ([]CustomData, error) {
|
|
info, err := r.DocInfo()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if info.CustomDatas == nil {
|
|
return nil, nil
|
|
}
|
|
return info.CustomDatas.CustomData, nil
|
|
}
|
|
|
|
// Extensions 获取扩展项,不加载注解或签名
|
|
// 返回的文件路径可直接传入ResData读取
|
|
// 返回: []Extension 扩展项列表, error 错误信息
|
|
func (r *Reader) Extensions() ([]Extension, error) {
|
|
doc, err := r.docStructure()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if strings.TrimSpace(doc.Extensions.Path) != "" && doc.Extensions.Extension == nil {
|
|
var extensions Extensions
|
|
partPath, err := r.readDocumentPart(doc.Extensions.Path, &extensions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range extensions.Extension {
|
|
extension := &extensions.Extension[i]
|
|
for j := range extension.ExtendData {
|
|
extension.ExtendData[j] = r.resourceLocation(partPath, "", extension.ExtendData[j])
|
|
}
|
|
}
|
|
if extensions.Extension == nil {
|
|
extensions.Extension = []Extension{}
|
|
}
|
|
doc.Extensions.Extension = extensions.Extension
|
|
}
|
|
return doc.Extensions.Extension, nil
|
|
}
|
|
|
|
// initRoot 读取根节点信息
|
|
// 返回: error 错误信息
|
|
func (r *Reader) initRoot() error {
|
|
return r.initRootContext(context.Background())
|
|
}
|
|
|
|
// initRootContext 重建包索引与主入口,取消或失败时保留原阅读状态
|
|
// 入参: ctx 取消上下文
|
|
// 返回: error 索引、读取、解析或取消错误
|
|
func (r *Reader) initRootContext(ctx context.Context) error {
|
|
view := *r
|
|
if err := view.indexPackageContext(ctx); err != nil {
|
|
return err
|
|
}
|
|
data, err := view.readFileViewContext(ctx, "OFD.xml")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read ofd.xml: %w", err)
|
|
}
|
|
var ofd OFD
|
|
decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
|
if err := decoder.Decode(&ofd); err != nil {
|
|
return fmt.Errorf("failed to unmarshal ofd.xml: %w", err)
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
r.fileIndex, r.fileNames = view.fileIndex, view.fileNames
|
|
r.OFD = &ofd
|
|
r.doc, r.versionInfo = nil, nil
|
|
r.documentRoot, r.RootDir = "", ""
|
|
r.relatedPending, r.fontResourcesRead = false, false
|
|
r.imageCatalog = nil
|
|
r.resourceInfoCache = nil
|
|
r.Stamps, r.Annots, r.annotationFiles = nil, nil, nil
|
|
r.ResMap = make(map[string]string)
|
|
r.resourcesRead = make(map[string]bool)
|
|
r.mediaCache = make(map[string]MultiMedia)
|
|
r.resourceFiles = make(map[string]string)
|
|
r.fontCache = make(map[string]*Font)
|
|
r.fontFaces = make(map[string][]*FontFace)
|
|
r.colorSpaceCache = make(map[string]*ColorSpace)
|
|
r.drawParamCache = make(map[string]*DrawParam)
|
|
r.compositeGraphicUnitCache = make(map[string]*CompositeGraphicUnit)
|
|
r.pageHeaderCache = make(map[string]PageContent)
|
|
return nil
|
|
}
|
|
|
|
// indexPackage 按大小写敏感路径建立包索引,拒绝规范化后重复的ZIP条目
|
|
// 返回: error 错误信息
|
|
func (r *Reader) indexPackage() error {
|
|
return r.indexPackageContext(context.Background())
|
|
}
|
|
|
|
// indexPackageContext 建立大小写敏感的包索引,取消或路径校验失败时保留原索引
|
|
// 入参: ctx 取消上下文
|
|
// 返回: error 路径或取消错误
|
|
func (r *Reader) indexPackageContext(ctx context.Context) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
count := 0
|
|
if r.Zip != nil {
|
|
count = len(r.Zip.File)
|
|
}
|
|
index := make(map[string]*zip.File, count)
|
|
names := make(map[string]bool, count+len(r.files))
|
|
if r.Zip != nil {
|
|
for _, f := range r.Zip.File {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if f.FileInfo().IsDir() {
|
|
continue
|
|
}
|
|
name := cleanPackagePath(f.Name)
|
|
if !validPackagePath(name) {
|
|
return fmt.Errorf("invalid package path: %q", f.Name)
|
|
}
|
|
if previous, ok := index[name]; ok {
|
|
return fmt.Errorf("ambiguous package paths: %q and %q", previous.Name, f.Name)
|
|
}
|
|
index[name] = f
|
|
names[name] = true
|
|
}
|
|
}
|
|
for name := range r.files {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if name != cleanPackagePath(name) || !validPackagePath(name) {
|
|
return fmt.Errorf("invalid package path: %q", name)
|
|
}
|
|
names[name] = true
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
r.fileIndex, r.fileNames = index, names
|
|
return nil
|
|
}
|
|
|
|
// validPackagePath 判断规范化路径是否位于包内
|
|
// 入参: name 规范化路径
|
|
// 返回: bool 是否有效
|
|
func validPackagePath(name string) bool {
|
|
return name != "." && name != ".." && !strings.HasPrefix(name, "../") && !strings.HasPrefix(name, "/") && !strings.ContainsAny(name, ":\x00")
|
|
}
|
|
|
|
// readFile 读取文档内的文件
|
|
// 入参: name 文件名
|
|
// 返回: []byte 文件内容, error 错误信息
|
|
func (r *Reader) readFile(name string) ([]byte, error) {
|
|
data, borrowed, err := r.readFileData(name)
|
|
if borrowed {
|
|
data = bytes.Clone(data)
|
|
}
|
|
return data, err
|
|
}
|
|
|
|
// readFileView 借用已加载文件或读取压缩文件,返回数据不得修改
|
|
// 入参: name 文件名
|
|
// 返回: []byte 只读文件内容, error 错误信息
|
|
func (r *Reader) readFileView(name string) ([]byte, error) {
|
|
data, _, err := r.readFileData(name)
|
|
return data, err
|
|
}
|
|
|
|
// readFileViewContext 读取包内文件并响应取消,已加载数据直接借用
|
|
// 入参: ctx 取消上下文, name 文件名
|
|
// 返回: []byte 只读文件内容, error 读取或取消错误
|
|
func (r *Reader) readFileViewContext(ctx context.Context, name string) ([]byte, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if data, ok := r.files[cleanPackagePath(name)]; ok {
|
|
return data, nil
|
|
}
|
|
input, err := r.openFile(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer input.Close()
|
|
data, err := io.ReadAll(imageInput{ReadCloser: input, context: ctx})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// readFileData 统一解析包内路径并区分缓存借用与独立读取
|
|
// 入参: name 文件名
|
|
// 返回: []byte 文件内容, bool 是否借用缓存, error 错误信息
|
|
func (r *Reader) readFileData(name string) ([]byte, bool, error) {
|
|
name = cleanPackagePath(name)
|
|
if data, ok := r.files[name]; ok {
|
|
return data, true, nil
|
|
}
|
|
if f, ok := r.packageFile(name); ok {
|
|
data, err := readZipFile(f)
|
|
return data, false, err
|
|
}
|
|
return nil, false, fmt.Errorf("file not found: %s", name)
|
|
}
|
|
|
|
// openFile 打开文档内的文件流
|
|
// 入参: name 文件名
|
|
// 返回: io.ReadCloser 文件流, error 错误信息
|
|
func (r *Reader) openFile(name string) (io.ReadCloser, error) {
|
|
name = cleanPackagePath(name)
|
|
if data, ok := r.files[name]; ok {
|
|
return io.NopCloser(bytes.NewReader(data)), nil
|
|
}
|
|
if f, ok := r.packageFile(name); ok {
|
|
return f.Open()
|
|
}
|
|
return nil, fmt.Errorf("file not found: %s", name)
|
|
}
|
|
|
|
// cleanPackagePath 清理包内文件路径
|
|
// 入参: name 文件路径
|
|
// 返回: string 清理后的文件路径
|
|
func cleanPackagePath(name string) string {
|
|
name = strings.ReplaceAll(name, "\\", "/")
|
|
name = strings.TrimPrefix(name, "/")
|
|
return path.Clean(name)
|
|
}
|
|
|
|
// packageFile 获取包内文件
|
|
// 入参: name 文件路径
|
|
// 返回: *zip.File 压缩包文件, bool 是否存在
|
|
func (r *Reader) packageFile(name string) (*zip.File, bool) {
|
|
f, ok := r.fileIndex[cleanPackagePath(name)]
|
|
return f, ok
|
|
}
|
|
|
|
// readZipFile 读取zip文件内容
|
|
// 入参: f zip文件对象
|
|
// 返回: []byte 文件内容, error 错误信息
|
|
func readZipFile(f *zip.File) ([]byte, error) {
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rc.Close()
|
|
return io.ReadAll(rc)
|
|
}
|
|
|
|
// 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 nil
|
|
}
|
|
fullPath := r.ResPath(resPath)
|
|
if r.resourcesRead[fullPath] {
|
|
return nil
|
|
}
|
|
data, err := r.readFileViewContext(ctx, fullPath)
|
|
if err != nil {
|
|
return ctx.Err()
|
|
}
|
|
var res Res
|
|
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)
|
|
}
|
|
r.colorSpaceCache[cs.ID] = cs
|
|
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
|
|
if r.mediaCache == nil {
|
|
r.mediaCache = make(map[string]MultiMedia)
|
|
}
|
|
r.mediaCache[mm.ID] = mm
|
|
r.ResMap[mm.ID] = finalPath
|
|
r.resourceFiles[mm.ID] = fullPath
|
|
}
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
r.fontCache[f.ID] = f
|
|
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所在位置解析文件,文档目录外的共享文件保留绝对路径
|
|
// 入参: name 资源XML包内路径, base 资源基准目录, file 文件声明
|
|
// 返回: string 可供ResPath解析的文件位置
|
|
func (r *Reader) resourceLocation(name, base, file string) string {
|
|
location := resolveResourcePath(name, base, file)
|
|
if location == "" {
|
|
return ""
|
|
}
|
|
root := cleanPackagePath(r.RootDir)
|
|
if root != "." && strings.HasPrefix(location, root+"/") {
|
|
return strings.TrimPrefix(location, root+"/")
|
|
}
|
|
return "/" + location
|
|
}
|
|
|
|
// resolveResourcePath 解析资源文件路径
|
|
// 入参: resPath 资源文件路径, baseLoc 资源基准路径, filePath 文件路径
|
|
// 返回: string 资源文件路径
|
|
func resolveResourcePath(resPath, baseLoc, filePath string) string {
|
|
p := strings.TrimSpace(filePath)
|
|
if p == "" {
|
|
return ""
|
|
}
|
|
p = strings.ReplaceAll(p, "\\", "/")
|
|
if strings.HasPrefix(p, "/") {
|
|
return strings.TrimPrefix(path.Clean(p), "/")
|
|
}
|
|
dir := path.Dir(strings.ReplaceAll(strings.TrimSpace(resPath), "\\", "/"))
|
|
baseLoc = strings.ReplaceAll(strings.TrimSpace(baseLoc), "\\", "/")
|
|
if strings.HasPrefix(baseLoc, "/") {
|
|
dir = cleanPackagePath(baseLoc)
|
|
} else if baseLoc != "" {
|
|
dir = path.Join(dir, baseLoc)
|
|
}
|
|
return path.Join(dir, p)
|
|
}
|
|
|
|
// readPageContent 读取页面,可跳过文字提取不使用的独立路径和图片
|
|
// 入参: ctx 取消上下文, page 页面对象, textOnly 是否仅用于文字提取, template 是否按模板页读取
|
|
// 返回: *PageContent 页面内容, error 错误信息
|
|
func (r *Reader) readPageContent(ctx context.Context, page Page, textOnly, template bool) (*PageContent, error) {
|
|
fullPath := r.ResPath(page.BaseLoc)
|
|
data, err := r.readFileViewContext(ctx, fullPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var content PageContent
|
|
decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
|
if textOnly {
|
|
err = xml.NewTokenDecoder(&textPageTokens{Decoder: decoder}).Decode(&content)
|
|
} else {
|
|
err = decoder.Decode(&content)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal page content: %w", err)
|
|
}
|
|
content.ID = page.ID
|
|
if template {
|
|
content.Template = nil
|
|
}
|
|
for _, res := range content.PageRes {
|
|
if err := r.loadResContext(ctx, "/"+resolveResourcePath(fullPath, "", res)); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
content.Area, err = r.resolvePageAreaContext(ctx, content.Area, content.Template)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return &content, nil
|
|
}
|
|
|
|
// loadPageResources 加载页面资源,不解析图元
|
|
// 入参: page 页面引用
|
|
func (r *Reader) loadPageResources(page Page) {
|
|
header, err := r.readPageHeader(page)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, res := range header.PageRes {
|
|
r.loadRes("/" + resolveResourcePath(r.ResPath(page.BaseLoc), "", res))
|
|
}
|
|
}
|
|
|
|
// readPageHeader 读取页面区域、模板引用和资源声明
|
|
// 入参: page 页面对象
|
|
// 返回: PageContent 页面头部, error 错误信息
|
|
func (r *Reader) readPageHeader(page Page) (PageContent, error) {
|
|
return r.readPageHeaderContext(context.Background(), page)
|
|
}
|
|
|
|
// readPageHeaderContext 读取并缓存页面头部,取消时不缓存部分结果
|
|
// 入参: ctx 取消上下文, page 页面对象
|
|
// 返回: PageContent 页面头部, error 读取、解析或取消错误
|
|
func (r *Reader) readPageHeaderContext(ctx context.Context, page Page) (PageContent, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return PageContent{}, err
|
|
}
|
|
name := r.ResPath(page.BaseLoc)
|
|
if content, ok := r.pageHeaderCache[name]; ok {
|
|
return content, nil
|
|
}
|
|
f, err := r.openFile(name)
|
|
if err != nil {
|
|
return PageContent{}, err
|
|
}
|
|
defer f.Close()
|
|
var content PageContent
|
|
d := xml.NewDecoder(imageInput{ReadCloser: f, context: ctx})
|
|
var root xml.Name
|
|
scan:
|
|
for {
|
|
if err := ctx.Err(); err != nil {
|
|
return PageContent{}, err
|
|
}
|
|
token, err := d.Token()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return PageContent{}, err
|
|
}
|
|
if start, ok := token.(xml.StartElement); ok {
|
|
if root.Local == "" {
|
|
if start.Name.Local != "Page" {
|
|
return PageContent{}, fmt.Errorf("expected Page root, got %s", start.Name.Local)
|
|
}
|
|
root = start.Name
|
|
continue
|
|
}
|
|
if !ofdXMLChild(root, start.Name) {
|
|
if err := d.Skip(); err != nil {
|
|
return PageContent{}, err
|
|
}
|
|
continue
|
|
}
|
|
start = ofdXMLStart(start)
|
|
switch start.Name.Local {
|
|
case "Template":
|
|
var template Template
|
|
if err := d.DecodeElement(&template, &start); err != nil {
|
|
return PageContent{}, err
|
|
}
|
|
content.Template = append(content.Template, template)
|
|
case "Area":
|
|
if err := d.DecodeElement(&content.Area, &start); err != nil {
|
|
return PageContent{}, err
|
|
}
|
|
case "PageRes":
|
|
var res string
|
|
if err := d.DecodeElement(&res, &start); err != nil {
|
|
return PageContent{}, err
|
|
}
|
|
content.PageRes = append(content.PageRes, res)
|
|
case "Content":
|
|
break scan
|
|
default:
|
|
if err := d.Skip(); err != nil {
|
|
return PageContent{}, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return PageContent{}, err
|
|
}
|
|
r.pageHeaderCache[name] = content
|
|
return content, nil
|
|
}
|
|
|
|
// resolvePageArea 解析页面区域的继承关系
|
|
// 入参: area 页面区域, templates 模板引用
|
|
// 返回: PageArea 页面区域, error 错误信息
|
|
func (r *Reader) resolvePageArea(area PageArea, templates []Template) (PageArea, error) {
|
|
return r.resolvePageAreaContext(context.Background(), area, templates)
|
|
}
|
|
|
|
// resolvePageAreaContext 按模板顺序解析页面区域的继承,读取过程中响应取消
|
|
// 入参: ctx 取消上下文, area 页面区域, templates 模板引用
|
|
// 返回: PageArea 页面区域, error 读取或取消错误
|
|
func (r *Reader) resolvePageAreaContext(ctx context.Context, area PageArea, templates []Template) (PageArea, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return PageArea{}, err
|
|
}
|
|
if area != (PageArea{}) || r.doc == nil {
|
|
return area, nil
|
|
}
|
|
for _, ref := range templates {
|
|
for _, template := range r.doc.CommonData.TemplatePage {
|
|
if err := ctx.Err(); err != nil {
|
|
return PageArea{}, err
|
|
}
|
|
if !sameResourceID(template.ID, ref.TemplateID) {
|
|
continue
|
|
}
|
|
content, err := r.readPageHeaderContext(ctx, Page{BaseLoc: template.BaseLoc})
|
|
if err != nil {
|
|
return PageArea{}, err
|
|
}
|
|
if content.Area != (PageArea{}) {
|
|
return content.Area, nil
|
|
}
|
|
break
|
|
}
|
|
}
|
|
return r.doc.CommonData.PageArea, nil
|
|
}
|
|
|
|
// readDocumentPart 读取文档关联文件
|
|
// 入参: loc 文件位置, value 文档结构
|
|
// 返回: string 文件路径, error 错误信息
|
|
func (r *Reader) readDocumentPart(loc string, value any) (string, error) {
|
|
return r.readDocumentPartContext(context.Background(), loc, value)
|
|
}
|
|
|
|
// readDocumentPartContext 读取并解析文档关联文件,传递取消信号到文件读取和XML解码
|
|
// 入参: ctx 取消上下文, loc 文件位置, value 文档结构
|
|
// 返回: string 文件路径, error 读取、解析或取消错误
|
|
func (r *Reader) readDocumentPartContext(ctx context.Context, loc string, value any) (string, error) {
|
|
fullPath := r.ResPath(strings.TrimSpace(loc))
|
|
data, err := r.readFileViewContext(ctx, fullPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
|
if err := decoder.Decode(value); err != nil {
|
|
return "", fmt.Errorf("failed to unmarshal %s: %w", path.Base(fullPath), err)
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return "", err
|
|
}
|
|
return fullPath, nil
|
|
}
|