mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-08-30 04:02:39 +08:00
454 lines
12 KiB
Go
454 lines
12 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"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
// Reader OFD文件阅读器
|
|
type Reader struct {
|
|
Path string
|
|
Zip *zip.Reader
|
|
Closer io.Closer
|
|
OFD *OFD
|
|
RootDir string
|
|
ResMap map[string]string
|
|
fontCache map[string]*Font
|
|
drawParamCache map[string]*DrawParam
|
|
compositeGraphicUnitCache map[string]*CompositeGraphicUnit
|
|
doc *Document
|
|
Stamps map[string][]Stamp
|
|
Annots map[string][]Annotation
|
|
fileIndex map[string]*zip.File
|
|
fileIndexFold map[string]*zip.File
|
|
}
|
|
|
|
// 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)
|
|
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.fontCache = make(map[string]*Font)
|
|
r.drawParamCache = make(map[string]*DrawParam)
|
|
r.compositeGraphicUnitCache = make(map[string]*CompositeGraphicUnit)
|
|
return nil
|
|
}
|
|
|
|
// readFile 读取压缩包内的文件
|
|
// 入参: name 文件名
|
|
// 返回: []byte 文件内容, error 错误信息
|
|
func (r *Reader) readFile(name string) ([]byte, error) {
|
|
name = cleanPackagePath(name)
|
|
if f, ok := r.packageFile(name); ok {
|
|
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 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) {
|
|
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
|
|
}
|
|
if doc.CommonData.DocumentRes != "" {
|
|
r.loadRes(doc.CommonData.DocumentRes)
|
|
}
|
|
if doc.CommonData.PublicRes != "" {
|
|
r.loadRes(doc.CommonData.PublicRes)
|
|
}
|
|
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)
|
|
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 _, mm := range res.MultiMedias.MultiMedia {
|
|
if mm.MediaFile != "" {
|
|
if finalPath := resolveResourcePath(resPath, baseLoc, mm.MediaFile); finalPath != "" {
|
|
r.ResMap[mm.ID] = finalPath
|
|
}
|
|
}
|
|
}
|
|
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
|
|
}
|
|
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
|
|
}
|
|
}
|
|
|
|
// 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 获取页面内容
|
|
// 入参: page 页面对象
|
|
// 返回: *PageContent 页面内容, error 错误信息
|
|
func (r *Reader) PageContent(page Page) (*PageContent, error) {
|
|
fullPath := r.ResPath(page.BaseLoc)
|
|
data, err := r.readFile(fullPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var content PageContent
|
|
if err := xml.Unmarshal(data, &content); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal page content: %w", err)
|
|
}
|
|
content.ID = page.ID
|
|
return &content, 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 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]
|
|
attachment.FileLoc = resolveResourcePath(partPath, "", attachment.FileLoc)
|
|
}
|
|
if attachments.Attachment == nil {
|
|
attachments.Attachment = []Attachment{}
|
|
}
|
|
doc.Attachments.Attachment = attachments.Attachment
|
|
}
|
|
return doc.Attachments.Attachment, nil
|
|
}
|
|
|
|
// 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
|
|
}
|