// 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" "encoding/xml" "fmt" "io" "path" "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 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 Stamps map[string][]Stamp Annots map[string][]Annotation fileIndex map[string]*zip.File fileIndexFold map[string]*zip.File files map[string][]byte } // Close 关闭阅读器 // 返回: error 错误信息 func (r *Reader) Close() error { if r.Closer != nil { return r.Closer.Close() } return nil } // initRoot 读取根节点信息 // 返回: error 错误信息 func (r *Reader) initRoot() error { r.fileIndex = make(map[string]*zip.File) r.fileIndexFold = make(map[string]*zip.File) if r.Zip != nil { for _, f := range r.Zip.File { name := cleanPackagePath(f.Name) r.fileIndex[name] = f fold := strings.ToLower(name) if _, ok := r.fileIndexFold[fold]; !ok { r.fileIndexFold[fold] = f } } } data, err := r.readFile("OFD.xml") if err != nil { return fmt.Errorf("failed to read ofd.xml: %w", err) } var ofd OFD if err := xml.Unmarshal(data, &ofd); err != nil { return fmt.Errorf("failed to unmarshal ofd.xml: %w", err) } r.OFD = &ofd r.ResMap = make(map[string]string) r.resourcesRead = make(map[string]bool) 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 } // readFile 读取文档内的文件 // 入参: name 文件名 // 返回: []byte 文件内容, error 错误信息 func (r *Reader) readFile(name string) ([]byte, error) { name = cleanPackagePath(name) if data, ok := r.files[name]; ok { return bytes.Clone(data), nil } if f, ok := r.packageFile(name); ok { if data, ok := r.files[cleanPackagePath(f.Name)]; ok { return bytes.Clone(data), nil } return readZipFile(f) } return nil, 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 { if data, ok := r.files[cleanPackagePath(f.Name)]; ok { return io.NopCloser(bytes.NewReader(data)), nil } 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) { if f, ok := r.fileIndex[name]; ok { return f, true } if f, ok := r.fileIndexFold[strings.ToLower(name)]; ok { return f, true } return nil, false } // 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) } // Doc 获取主文档结构 // 返回: *Document 文档结构, error 错误信息 func (r *Reader) Doc() (*Document, error) { if r.doc != nil { return r.doc, nil } if r.OFD == nil || len(r.OFD.DocBody) == 0 { return nil, fmt.Errorf("no docbody found") } docAttr := r.OFD.DocBody[0] docRootPath := docAttr.DocRoot r.RootDir = path.Dir(docRootPath) data, err := r.readFile(docRootPath) if err != nil { return nil, err } var doc Document if err := xml.Unmarshal(data, &doc); err != nil { return nil, fmt.Errorf("failed to unmarshal document.xml: %w", err) } if doc.Signatures == "" { doc.Signatures = docAttr.Signatures } for _, res := range doc.CommonData.DocumentRes { r.loadRes(res) } for _, res := range doc.CommonData.PublicRes { r.loadRes(res) } r.doc = &doc _ = r.parseAnnotations(&doc) _ = r.parseSignatures(&doc) return r.doc, nil } // loadRes 加载资源文件 // 入参: resPath 资源路径 func (r *Reader) loadRes(resPath string) { if resPath == "" { return } fullPath := r.ResPath(resPath) if r.resourcesRead[fullPath] { return } data, err := r.readFile(fullPath) if err != nil { return } var res Res if err := xml.Unmarshal(data, &res); err != nil { return } baseLoc := res.BaseLoc for i := range res.ColorSpaces.ColorSpace { cs := &res.ColorSpaces.ColorSpace[i] r.colorSpaceCache[cs.ID] = cs r.resourceFiles[cs.ID] = fullPath } for _, mm := range res.MultiMedias.MultiMedia { if mm.MediaFile != "" { if finalPath := resolveResourcePath(resPath, baseLoc, mm.MediaFile); finalPath != "" { r.ResMap[mm.ID] = finalPath r.resourceFiles[mm.ID] = fullPath } } } for i := range res.Fonts.Font { f := &res.Fonts.Font[i] if f.FontFile != "" { f.FontFile = resolveResourcePath(resPath, baseLoc, f.FontFile) } r.fontCache[f.ID] = f r.resourceFiles[f.ID] = fullPath } for i := range res.DrawParams.DrawParam { dp := &res.DrawParams.DrawParam[i] r.drawParamCache[dp.ID] = dp } for i := range res.CompositeGraphicUnits.CompositeGraphicUnit { cgu := &res.CompositeGraphicUnits.CompositeGraphicUnit[i] r.compositeGraphicUnitCache[cgu.ID] = cgu } r.resourcesRead[fullPath] = true } // 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(resPath) if baseLoc != "" { if dir != baseLoc { dir = path.Join(dir, baseLoc) } } return path.Join(dir, p) } // PageContent 获取页面内容 // 每次返回独立的页面内容,修改后可传入Renderer重绘,不会写回原文件 // 入参: page 页面对象 // 返回: *PageContent 页面内容, error 错误信息 func (r *Reader) PageContent(page Page) (*PageContent, error) { return r.readPageContent(page, false) } // readPageContent 读取页面,可跳过文字提取不使用的独立路径和图片 // 入参: page 页面对象, textOnly 是否仅用于文字提取 // 返回: *PageContent 页面内容, error 错误信息 func (r *Reader) readPageContent(page Page, textOnly bool) (*PageContent, error) { fullPath := r.ResPath(page.BaseLoc) data, err := r.readFile(fullPath) if err != nil { return nil, err } var content PageContent if textOnly { decoder := &textPageTokens{Decoder: xml.NewDecoder(bytes.NewReader(data))} err = xml.NewTokenDecoder(decoder).Decode(&content) } else { err = xml.Unmarshal(data, &content) } if err != nil { return nil, fmt.Errorf("failed to unmarshal page content: %w", err) } content.ID = page.ID for _, res := range content.PageRes { r.loadRes(resolveResourcePath(page.BaseLoc, "", res)) } content.Area, err = r.resolvePageArea(content.Area, content.Template) if err != nil { return nil, err } return &content, nil } // PageCount 获取当前文档总页数 // 返回: int 页数, error 错误信息 func (r *Reader) PageCount() (int, error) { doc, err := r.Doc() 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) { doc, err := r.Doc() 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.PageContent(doc.Pages.Page[index]) } // PageArea 读取页面区域,不解析页面图元 // 入参: page 页面对象 // 返回: PageArea 页面区域, error 错误信息 func (r *Reader) PageArea(page Page) (PageArea, error) { content, err := r.readPageHeader(page) if err != nil { return PageArea{}, err } return r.resolvePageArea(content.Area, content.Template) } // 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(page.BaseLoc, "", res)) } } // readPageHeader 读取页面区域、模板引用和资源声明 // 入参: page 页面对象 // 返回: PageContent 页面头部, error 错误信息 func (r *Reader) readPageHeader(page Page) (PageContent, error) { 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(f) scan: for { token, err := d.Token() if err == io.EOF { break } if err != nil { return PageContent{}, err } if start, ok := token.(xml.StartElement); ok { switch start.Name.Local { case "Page": continue 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 } } } } r.pageHeaderCache[name] = content return content, nil } // resolvePageArea 解析页面区域的继承关系 // 入参: area 页面区域, templates 模板引用 // 返回: PageArea 页面区域, error 错误信息 func (r *Reader) resolvePageArea(area PageArea, templates []Template) (PageArea, error) { if area != (PageArea{}) || r.doc == nil { return area, nil } for _, ref := range templates { for _, template := range r.doc.CommonData.TemplatePage { if template.ID != ref.TemplateID { continue } content, err := r.readPageHeader(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 } // ResPath 获取资源的完整路径 // 入参: 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, "\\", "/"), "/") if rootDir != "" && (resLink == rootDir || strings.HasPrefix(resLink, rootDir+"/")) { return resLink } return cleanPackagePath(path.Join(r.RootDir, resLink)) } // ResData 获取资源文件数据 // 入参: resLink 资源链接 // 返回: []byte 资源数据, error 错误信息 func (r *Reader) ResData(resLink string) ([]byte, error) { fullPath := r.ResPath(resLink) return r.readFile(fullPath) } // readDocumentPart 读取文档关联文件 // 入参: loc 文件位置, value 文档结构 // 返回: string 文件路径, error 错误信息 func (r *Reader) readDocumentPart(loc string, value any) (string, error) { fullPath := r.ResPath(strings.TrimSpace(loc)) data, err := r.readFile(fullPath) if err != nil { return "", err } if err := xml.Unmarshal(data, value); err != nil { return "", fmt.Errorf("failed to unmarshal %s: %w", path.Base(fullPath), err) } return fullPath, nil } // 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 || len(r.OFD.DocBody) == 0 { return nil, fmt.Errorf("no docbody found") } return &r.OFD.DocBody[0].DocInfo, nil } // Permissions 获取文档权限信息 // 返回: *Permissions 权限信息, error 错误信息 func (r *Reader) Permissions() (*Permissions, error) { doc, err := r.Doc() if err != nil { return nil, err } return &doc.Permissions, nil } // Outlines 获取文档大纲 // 返回: []OutlineElem 大纲列表, error 错误信息 func (r *Reader) Outlines() ([]OutlineElem, error) { doc, err := r.Doc() if err != nil { return nil, err } return doc.Outlines.OutlineElem, nil } // Actions 获取文档动作 // 返回: []Action 动作列表, error 错误信息 func (r *Reader) Actions() ([]Action, error) { doc, err := r.Doc() if err != nil { return nil, err } return doc.Actions, nil } // Bookmarks 获取文档书签 // 返回: []Bookmark 书签列表, error 错误信息 func (r *Reader) Bookmarks() ([]Bookmark, error) { doc, err := r.Doc() if err != nil { return nil, err } return doc.Bookmarks.Bookmark, nil } // Attachments 获取附件列表 // 返回: []Attachment 附件列表, error 错误信息 func (r *Reader) Attachments() ([]Attachment, error) { doc, err := r.Doc() if err != nil { return nil, err } if strings.TrimSpace(doc.Attachments.Path) != "" && doc.Attachments.Attachment == nil { var attachments Attachments partPath, err := r.readDocumentPart(doc.Attachments.Path, &attachments) if err != nil { return nil, err } for i := range attachments.Attachment { attachment := &attachments.Attachment[i] if fileLoc := resolveResourcePath(partPath, "", attachment.FileLoc); fileLoc != "" { attachment.FileLoc = "/" + fileLoc } } if attachments.Attachment == nil { attachments.Attachment = []Attachment{} } doc.Attachments.Attachment = attachments.Attachment } return doc.Attachments.Attachment, nil } // OpenAttachment 打开附件文件流,调用方负责关闭,完整读取后校验ZIP数据 // 入参: id 附件标识 // 返回: io.ReadCloser 附件文件流, error 错误信息 func (r *Reader) OpenAttachment(id string) (io.ReadCloser, error) { attachments, err := r.Attachments() if err != nil { return nil, err } for _, attachment := range attachments { if attachment.ID == id { return r.openFile(r.ResPath(attachment.FileLoc)) } } return nil, fmt.Errorf("attachment not found: %s", id) } // AttachmentData 获取附件文件数据 // 入参: id 附件标识 // 返回: []byte 附件数据, error 错误信息 func (r *Reader) AttachmentData(id string) ([]byte, error) { stream, err := r.OpenAttachment(id) if err != nil { return nil, err } defer stream.Close() return io.ReadAll(stream) } // CustomTags 获取自定义标引 // 返回: []CustomTag 自定义标引列表, error 错误信息 func (r *Reader) CustomTags() ([]CustomTag, error) { doc, err := r.Doc() if err != nil { return nil, err } if strings.TrimSpace(doc.CustomTags.Path) != "" && doc.CustomTags.CustomTag == nil { var customTags CustomTags partPath, err := r.readDocumentPart(doc.CustomTags.Path, &customTags) if err != nil { return nil, err } for i := range customTags.CustomTag { tag := &customTags.CustomTag[i] tag.SchemaLoc = resolveResourcePath(partPath, "", tag.SchemaLoc) tag.FileLoc = resolveResourcePath(partPath, "", tag.FileLoc) } if customTags.CustomTag == nil { customTags.CustomTag = []CustomTag{} } 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 获取扩展项 // 返回: []Extension 扩展项列表, error 错误信息 func (r *Reader) Extensions() ([]Extension, error) { doc, err := r.Doc() 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] = resolveResourcePath(partPath, "", extension.ExtendData[j]) } } if extensions.Extension == nil { extensions.Extension = []Extension{} } doc.Extensions.Extension = extensions.Extension } return doc.Extensions.Extension, nil }