mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 15:06:46 +08:00
296 lines
8.6 KiB
Go
296 lines
8.6 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"
|
|
"path"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// ImageInfo 描述文档内的原始图片资源,独立蒙版保留为独立资源
|
|
// 不包含对象裁剪、旋转、透明度及显示尺寸,不代表页面合成后的外观
|
|
type ImageInfo struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Format string `json:"format"`
|
|
Location string `json:"-"`
|
|
}
|
|
|
|
// imageOutput 在分块写出图片时检查调用方取消状态
|
|
type imageOutput struct {
|
|
context context.Context
|
|
writer io.Writer
|
|
}
|
|
|
|
// imageInput 在读取资源数据时检查调用方取消状态
|
|
type imageInput struct {
|
|
io.ReadCloser
|
|
context context.Context
|
|
}
|
|
|
|
// imageResourceKey 合并有效十进制标识,非标准标识保留原值,不相互混淆
|
|
// 入参: id 图片标识
|
|
// 返回: string 查找键
|
|
func imageResourceKey(id string) string {
|
|
if key := editorResourceID(id); key != "" {
|
|
return key
|
|
}
|
|
return id
|
|
}
|
|
|
|
// Read 检查取消状态后读取原始数据
|
|
// 入参: data 数据缓冲区
|
|
// 返回: int 读取字节数, error 读取或取消错误
|
|
func (r imageInput) Read(data []byte) (int, error) {
|
|
if err := r.context.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
return r.ReadCloser.Read(data)
|
|
}
|
|
|
|
// Write 检查取消状态后写出原始数据
|
|
// 入参: data 数据块
|
|
// 返回: int 写入字节数, error 写入或取消错误
|
|
func (w imageOutput) Write(data []byte) (int, error) {
|
|
if err := w.context.Err(); err != nil {
|
|
return 0, err
|
|
}
|
|
return w.writer.Write(data)
|
|
}
|
|
|
|
// Images 列出文档、页面和模板声明的图片资源,不解码图片像素
|
|
// 包含未使用及重复引用的资源声明,同一资源标识只返回一次,不加载注解或签名
|
|
// 入参: ctx 取消上下文
|
|
// 返回: []ImageInfo 图片资源, error 读取或取消错误
|
|
func (r *Reader) Images(ctx context.Context) ([]ImageInfo, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if r.imageCatalog != nil {
|
|
return slices.Clone(r.imageCatalog), nil
|
|
}
|
|
doc, err := r.docStructureContext(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resources := make([]string, 0)
|
|
for _, name := range append(slices.Clone(doc.CommonData.DocumentRes), doc.CommonData.PublicRes...) {
|
|
resources = append(resources, r.ResPath(name))
|
|
}
|
|
pages := slices.Clone(doc.Pages.Page)
|
|
for _, template := range doc.CommonData.TemplatePage {
|
|
pages = append(pages, Page{BaseLoc: template.BaseLoc})
|
|
}
|
|
for _, page := range pages {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
header, err := r.readPageHeaderContext(ctx, page)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, name := range header.PageRes {
|
|
resources = append(resources, resolveResourcePath(r.ResPath(page.BaseLoc), "", name))
|
|
}
|
|
}
|
|
seen := make(map[string]bool)
|
|
ids := make(map[string]ImageInfo)
|
|
items := make([]ImageInfo, 0)
|
|
for _, name := range resources {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if seen[name] {
|
|
continue
|
|
}
|
|
seen[name] = true
|
|
data, err := r.readFileViewContext(ctx, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var res Res
|
|
decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
|
if err := decoder.Decode(&res); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, media := range res.MultiMedias.MultiMedia {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
if media.Type != "Image" {
|
|
continue
|
|
}
|
|
location := resolveResourcePath(name, res.BaseLoc, media.MediaFile)
|
|
if media.ID == "" || location == "" {
|
|
return nil, fmt.Errorf("invalid image resource")
|
|
}
|
|
item := ImageInfo{ID: media.ID, Name: path.Base(location), Format: media.Format, Location: location}
|
|
key := imageResourceKey(item.ID)
|
|
if previous, ok := ids[key]; ok {
|
|
if previous.Name != item.Name || previous.Format != item.Format || previous.Location != item.Location {
|
|
return nil, fmt.Errorf("conflicting image resource %q", item.ID)
|
|
}
|
|
continue
|
|
}
|
|
ids[key] = item
|
|
items = append(items, item)
|
|
}
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
r.imageCatalog = items
|
|
return slices.Clone(items), nil
|
|
}
|
|
|
|
// Image 查找指定图片,复用媒体索引或图片清单,尚未建立索引时读取资源声明
|
|
// 入参: ctx 取消上下文, id 图片资源标识
|
|
// 返回: ImageInfo 图片资源, error 读取或取消错误
|
|
func (r *Reader) Image(ctx context.Context, id string) (ImageInfo, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return ImageInfo{}, err
|
|
}
|
|
name, _ := resourceValue(r.resourceFiles, id)
|
|
if name != "" {
|
|
if r.resourcesRead[name] {
|
|
if media, ok := resourceValue(r.mediaCache, id); ok && media.Type == "Image" && media.MediaFile != "" {
|
|
location := r.ResPath(media.MediaFile)
|
|
if err := ctx.Err(); err != nil {
|
|
return ImageInfo{}, err
|
|
}
|
|
return ImageInfo{ID: id, Name: path.Base(location), Format: media.Format, Location: location}, nil
|
|
}
|
|
}
|
|
data, err := r.readFileViewContext(ctx, name)
|
|
if err != nil {
|
|
return ImageInfo{}, err
|
|
}
|
|
var res Res
|
|
decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
|
if err := decoder.Decode(&res); err != nil {
|
|
return ImageInfo{}, err
|
|
}
|
|
for _, media := range res.MultiMedias.MultiMedia {
|
|
if err := ctx.Err(); err != nil {
|
|
return ImageInfo{}, err
|
|
}
|
|
if imageResourceKey(media.ID) == imageResourceKey(id) && media.Type == "Image" && media.MediaFile != "" {
|
|
location := resolveResourcePath(name, res.BaseLoc, media.MediaFile)
|
|
return ImageInfo{ID: id, Name: path.Base(location), Format: media.Format, Location: location}, nil
|
|
}
|
|
}
|
|
return ImageInfo{}, fmt.Errorf("image resource %q not found", id)
|
|
}
|
|
items := r.imageCatalog
|
|
if items == nil {
|
|
var err error
|
|
items, err = r.Images(ctx)
|
|
if err != nil {
|
|
return ImageInfo{}, err
|
|
}
|
|
}
|
|
key := imageResourceKey(id)
|
|
for _, item := range items {
|
|
if err := ctx.Err(); err != nil {
|
|
return ImageInfo{}, err
|
|
}
|
|
if imageResourceKey(item.ID) == key {
|
|
return item, nil
|
|
}
|
|
}
|
|
return ImageInfo{}, fmt.Errorf("image resource %q not found", id)
|
|
}
|
|
|
|
// OpenImage 打开原始图片资源流,不解码、不重采样或重新压缩
|
|
// 加密文档返回解密后的资源,调用方负责关闭并完整读取以校验ZIP数据
|
|
// 入参: ctx 取消上下文, id 图片资源标识
|
|
// 返回: io.ReadCloser 原始图片流, error 读取错误
|
|
func (r *Reader) OpenImage(ctx context.Context, id string) (io.ReadCloser, error) {
|
|
item, err := r.Image(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
input, err := r.openFile(item.Location)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return imageInput{ReadCloser: input, context: ctx}, nil
|
|
}
|
|
|
|
// WriteImages 将原始图片逐个写入ZIP,不改变图片编码,文件序号避免同名覆盖
|
|
// 入参: ctx 取消上下文, writer 输出流, ids 资源标识,省略则导出全部
|
|
// 返回: error 读取、写出或取消错误
|
|
func (r *Reader) WriteImages(ctx context.Context, writer io.Writer, ids ...string) error {
|
|
var items []ImageInfo
|
|
if len(ids) == 0 {
|
|
var err error
|
|
items, err = r.Images(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
seen := make(map[string]bool)
|
|
for _, id := range ids {
|
|
key := imageResourceKey(id)
|
|
if seen[key] {
|
|
continue
|
|
}
|
|
item, err := r.Image(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
items = append(items, item)
|
|
seen[key] = true
|
|
}
|
|
}
|
|
archive := zip.NewWriter(imageOutput{ctx, writer})
|
|
for i, item := range items {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
name := strings.Map(func(c rune) rune {
|
|
if c < 32 || strings.ContainsRune(`<>:"/\|?*`, c) {
|
|
return '_'
|
|
}
|
|
return c
|
|
}, item.Name)
|
|
header := &zip.FileHeader{Name: fmt.Sprintf("%04d-%s", i+1, name), Method: zip.Store}
|
|
output, err := archive.CreateHeader(header)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
input, err := r.openFile(item.Location)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = io.Copy(imageOutput{ctx, output}, input)
|
|
closeErr := input.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if closeErr != nil {
|
|
return closeErr
|
|
}
|
|
}
|
|
return archive.Close()
|
|
}
|