feat(渲染编辑): 完善渲染压缩及文档版本编辑
build.yaml / build (push) Successful in 1m51s

This commit is contained in:
xiaoqidun committed 2026-10-11 18:45:54 +08:00
1 parent cfbaa8a177
commit 2c06e5ff69
43 files changed
+3211 -397

No files matched your search

+2 -2
View File
@@ -292,7 +292,7 @@
<label for="createName">名称</label>
<input id="createName" type="text" value="未命名" required autofocus>
</div>
<div id="createImagesRow" class="form-row" hidden><label for="createImages">图片</label><input id="createImages" type="file" accept=".png,.jpg,.jpeg,.jb2,.jbig2" multiple disabled></div>
<div id="createImagesRow" class="form-row" hidden><label for="createImages">图片</label><input id="createImages" type="file" accept=".png,.jpg,.jpeg,.bmp,.tif,.tiff,.jb2,.jbig2" multiple disabled></div>
<div id="createSizeRow" class="form-row" hidden><label for="createSize">尺寸</label><select id="createSize" disabled><option value="image">随图</option><option value="custom">自定</option></select></div>
<div id="createDimensions" class="form-pair">
<div class="form-row">
@@ -466,7 +466,7 @@
<form id="insertForm">
<div class="form-row">
<label for="insertImage">图片</label>
<input id="insertImage" type="file" accept=".png,.jpg,.jpeg,image/png,image/jpeg" required autofocus>
<input id="insertImage" type="file" accept=".png,.jpg,.jpeg,.bmp,.tif,.tiff,image/png,image/jpeg,image/bmp,image/tiff" required autofocus>
</div>
<div id="insertFitRow" class="form-row" hidden>
<label for="insertFit">缩放</label>
+1 -1
View File
@@ -9,7 +9,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e
github.com/xiaoqidun/jbig2 v1.1.4
github.com/xiaoqidun/pdfgo v0.0.0-20261011021850-35fe3f96d36f
github.com/xiaoqidun/pdfgo v0.0.0-20261011103616-6cafccc7f8f6
golang.org/x/crypto v0.58.0
golang.org/x/image v0.47.0
golang.org/x/text v0.43.0
+2 -2
View File
@@ -62,8 +62,8 @@ github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e h1:OuRcvG5g5AieBN3
github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
github.com/xiaoqidun/jbig2 v1.1.4 h1:LF6J1si4JY+TszTh9+m4KAg9/ba0JjfXoGtQTQC2S8c=
github.com/xiaoqidun/jbig2 v1.1.4/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA=
github.com/xiaoqidun/pdfgo v0.0.0-20261011021850-35fe3f96d36f h1:m3yM1CzrHWm+LW84WWcA2Kr1Ux8K/Znrgd0XVxGEtIw=
github.com/xiaoqidun/pdfgo v0.0.0-20261011021850-35fe3f96d36f/go.mod h1:hkZn8gHOI94K0fE2ynbXaE8J5T0/jCvoTS/yroKgb+E=
github.com/xiaoqidun/pdfgo v0.0.0-20261011103616-6cafccc7f8f6 h1:TW7d8qguwR6+WAxXw4ncAXdHTSrAgz6zjj7NmKbipyo=
github.com/xiaoqidun/pdfgo v0.0.0-20261011103616-6cafccc7f8f6/go.mod h1:hkZn8gHOI94K0fE2ynbXaE8J5T0/jCvoTS/yroKgb+E=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+25 -11
View File
@@ -25,7 +25,6 @@ import (
"encoding/json"
"errors"
"fmt"
"image"
"image/color"
"io"
"io/fs"
@@ -1570,11 +1569,11 @@ func writeAnnotation(args []js.Value) (any, error) {
if err != nil {
return err
}
config, _, err := image.DecodeConfig(bytes.NewReader(data))
size, err := currentEditor.ImageSize(resource)
if err != nil {
return err
}
options.Height = options.Width * float64(config.Height) / float64(config.Width)
options.Height = options.Width * float64(size.Y) / float64(size.X)
}
if options.Kind == "replace" {
style := ofdgo.TextStyle{Font: font}
@@ -3297,15 +3296,16 @@ func importPages(args []js.Value) (any, error) {
}
at := args[1].Int()
options := ofdgo.PageImportOptions{Outlines: len(args) > 2 && args[2].Bool()}
ctx := context.Background()
progressIndex := 3
if len(args) > 3 && args[3].Type() == js.TypeBoolean {
options.AllowDecrypted = args[3].Bool()
progressIndex = 4
}
if len(args) > progressIndex {
options.OnProgress = editorOperationProgress(args[progressIndex])
ctx, options.OnProgress = newEditorOperationContext(args[progressIndex])
}
if _, err := currentEditor.ImportPagesWithOptions(pendingImport, indexes, at, options); err != nil {
if _, err := currentEditor.ImportPagesContext(ctx, pendingImport, indexes, at, options); err != nil {
return nil, err
}
clearImport()
@@ -4103,12 +4103,12 @@ func insertImage(args []js.Value) (any, error) {
if err != nil {
return nil, err
}
config, _, err := image.DecodeConfig(bytes.NewReader(data))
size, err := currentEditor.ImageSize(resourceID)
if err != nil {
return nil, err
}
width := args[4].Float()
box := ofdgo.Box{X: args[2].Float(), Y: args[3].Float(), W: width, H: width * float64(config.Height) / float64(config.Width)}
box := ofdgo.Box{X: args[2].Float(), Y: args[3].Float(), W: width, H: width * float64(size.Y) / float64(size.X)}
content, err := currentSession.pageContent(args[0].Int())
if err != nil {
return nil, err
@@ -4593,11 +4593,11 @@ func compressionFromJS(value js.Value, options *ofdgo.CompressionOptions) error
return options.Validate()
}
// outputContext 定期让出浏览器线程,将取消请求传入库层压缩循环
// outputContext 定期让出浏览器线程,将取消请求传入库层长操作
type outputContext struct {
context.Context
cancel context.CancelFunc
checkpoint js.Value
checkpoint func() error
checked time.Time
}
@@ -4606,7 +4606,21 @@ type outputContext struct {
// 返回: context.Context 导出上下文
func newOutputContext(checkpoint js.Value) context.Context {
ctx, cancel := context.WithCancel(context.Background())
return &outputContext{Context: ctx, cancel: cancel, checkpoint: checkpoint}
return &outputContext{Context: ctx, cancel: cancel, checkpoint: func() error { return awaitExport(checkpoint) }}
}
// newEditorOperationContext 在底层取消检查时保持最近的进度,避免版本处理期间界面无响应
// 入参: callback 浏览器进度及取消回调
// 返回: context.Context 操作上下文, func(string, int, int) error 阶段进度回调
func newEditorOperationContext(callback js.Value) (context.Context, func(string, int, int) error) {
ctx, cancel := context.WithCancel(context.Background())
stage, completed, total := "ids", 0, 0
progress := editorOperationProgress(callback)
operation := &outputContext{Context: ctx, cancel: cancel, checkpoint: func() error { return progress(stage, completed, total) }}
return operation, func(next string, done, count int) error {
stage, completed, total = next, done, count
return progress(stage, completed, total)
}
}
// Err 在取消检查间隙处理浏览器消息,不为每个像素触发跨语言回调
@@ -4617,7 +4631,7 @@ func (c *outputContext) Err() error {
}
if time.Since(c.checked) >= 50*time.Millisecond {
c.checked = time.Now()
if err := awaitExport(c.checkpoint); err != nil {
if err := c.checkpoint(); err != nil {
c.cancel()
}
}
+14 -27
View File
@@ -101,14 +101,19 @@ func (e *Editor) WriteToWithOptions(ctx context.Context, writer io.Writer, optio
return snapshot.WriteTo(convertWriter{context: ctx, writer: writer, count: &count})
}
// WritePagesToWithOptions 按指定顺序另存页面并应用输出压缩
// WritePagesToWithOptions 按指定顺序另存页面,仅为所选页面准备资源和压缩计划
// 入参: ctx 取消上下文, writer 输出流, indexes 零基页码, options 输出配置
// 返回: int64 已写入字节数, error 错误信息
func (e *Editor) WritePagesToWithOptions(ctx context.Context, writer io.Writer, indexes []int, options WriteOptions) (int64, error) {
snapshot, err := e.outputSnapshot(ctx, options)
if err != nil {
if err := ctx.Err(); err != nil {
return 0, err
}
if err := options.Compression.Validate(); err != nil {
return 0, err
}
snapshot := *e
snapshot.OnWriteProgress = editorProgress(e.OnWriteProgress).withContext(ctx)
snapshot.output = &outputOptimization{ctx: ctx, options: options.Compression}
var count int64
return snapshot.WritePagesTo(convertWriter{context: ctx, writer: writer, count: &count}, indexes)
}
@@ -130,15 +135,10 @@ func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Edi
}
return editorProgress(e.OnWriteProgress).report(stage, completed, total)
}
snapshot.output = &outputOptimization{ctx: ctx, options: options.Compression, images: map[string]bool{}}
snapshot.output = &outputOptimization{ctx: ctx, options: options.Compression}
if options.Compression.Mode == CompressionUnchanged {
return &snapshot, nil
}
for _, resource := range e.resources {
if resource.image != nil {
snapshot.output.images[cleanPackagePath(resource.name)] = true
}
}
if e.source != nil {
protected, err := e.source.reader.hasOutputSignatures()
if err != nil {
@@ -146,19 +146,11 @@ func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Edi
}
snapshot.output.protected = protected
if protected {
snapshot.output.images = nil
return &snapshot, nil
}
images, err := e.source.reader.Images(ctx)
if err != nil {
return nil, err
}
for _, img := range images {
snapshot.output.images[cleanPackagePath(img.Location)] = true
}
}
if options.Compression.Mode == CompressionLossy {
reader, err := e.reader(editorProgress(snapshot.OnWriteProgress))
reader, err := e.readerContext(ctx, editorProgress(snapshot.OnWriteProgress))
if err != nil {
return nil, err
}
@@ -174,7 +166,7 @@ func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Edi
return &snapshot, nil
}
// compressionImageSafety 保护蒙版及其原图和替换图像,识别不能可靠降采样的图案及隐藏内容
// compressionImageSafety 保护蒙版及其原图和替换图像,不完整的文档结构不参与降采样
// 入参: ctx 取消上下文, images 图片资源
// 返回: map[string]bool 只能无损处理的资源路径, bool 是否可分析尺寸, error 读取或取消错误
func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo) (map[string]bool, bool, error) {
@@ -205,7 +197,6 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo)
}
}
masks := make(map[string]bool)
geometry := true
for _, name := range slices.Sorted(maps.Keys(names)) {
if !safe {
break
@@ -248,17 +239,11 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo)
break
}
stack = append(stack, element.Name)
if element.Name.Local == "Pattern" {
geometry = false
}
resource, substitution, masked := "", "", false
for _, attr := range element.Attr {
if attr.Name.Space != "" {
continue
}
if attr.Name.Local == "Visible" && (attr.Value == "false" || attr.Value == "0") {
geometry = false
}
if attr.Name.Local == "ResourceID" {
resource = editorResourceID(attr.Value)
}
@@ -278,6 +263,8 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo)
}
case xml.EndElement:
stack = stack[:len(stack)-1]
case xml.Directive:
safe = false
}
if !safe {
break
@@ -295,7 +282,7 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo)
paths[cleanPackagePath(img.Location)] = true
}
}
return paths, safe && geometry, ctx.Err()
return paths, safe, ctx.Err()
}
// hasOutputSignatures 检查包中各文档,避免改写其他文档签名保护的共享资源
+63 -16
View File
@@ -23,15 +23,22 @@ import (
// compressionImages 记录图片各处使用的最大像素需求
type compressionImages struct {
ctx context.Context
dpi int
sizes map[string]image.Point
ctx context.Context
dpi int
sizes map[string]image.Point
renderer *Renderer
patternPage *Matrix
patterns map[*Pattern]bool
patternVisits map[compressionPatternKey]bool
}
// compressionImagePlan 分文档分析标识,按共享路径合并最大尺寸和保护条件
// compressionImagePlan 为有损输出分文档分析图片,按共享路径合并最大尺寸和保护条件
// 入参: ctx 取消上下文, options 压缩配置
// 返回: map[string]bool 可有损处理的路径, map[string]image.Point 像素需求, error 读取或取消错误
func (r *Reader) compressionImagePlan(ctx context.Context, options CompressionOptions) (map[string]bool, map[string]image.Point, error) {
if options.Mode != CompressionLossy {
return nil, nil, ctx.Err()
}
images := make(map[string]bool)
sizes := make(map[string]image.Point)
blocked := make(map[string]bool)
@@ -54,14 +61,10 @@ func (r *Reader) compressionImagePlan(ctx context.Context, options CompressionOp
}
return nil, nil, err
}
var masks map[string]bool
var demands map[string]image.Point
if options.Mode == CompressionLossy {
var geometry bool
masks, geometry, err = document.compressionImageSafety(ctx, catalog)
if err == nil && geometry {
demands, err = document.compressionImageSizes(ctx, catalog, options.ImageDPI())
}
masks, geometry, err := document.compressionImageSafety(ctx, catalog)
if err == nil && geometry {
demands, err = document.compressionImageSizes(ctx, catalog, options.ImageDPI())
}
if document != r {
document.Close()
@@ -102,7 +105,7 @@ func compressionImageSize(width, height float64, dpi int) image.Point {
return image.Pt(int(w), int(h))
}
// compressionImageSizes 按正文、模板及注解的实际变换汇总共享图片尺寸
// compressionImageSizes 按正文、模板、注解及底纹的实际变换汇总图片尺寸,包括隐藏内容
// 入参: ctx 取消上下文, images 图片资源, dpi 分辨率上限
// 返回: map[string]image.Point 资源像素需求, error 取消错误
func (r *Reader) compressionImageSizes(ctx context.Context, images []ImageInfo, dpi int) (map[string]image.Point, error) {
@@ -113,9 +116,9 @@ func (r *Reader) compressionImageSizes(ctx context.Context, images []ImageInfo,
if err != nil {
return nil, ctx.Err()
}
renderer := NewRenderer(r)
renderer := NewRenderer(r, WithRenderBackends(RenderBackends{}))
defer renderer.ClearCache()
visitor := &compressionImages{ctx: ctx, dpi: dpi, sizes: make(map[string]image.Point)}
visitor := &compressionImages{ctx: ctx, dpi: dpi, sizes: make(map[string]image.Point), renderer: renderer}
for index := 0; index < count; index++ {
if err := ctx.Err(); err != nil {
return nil, err
@@ -157,14 +160,21 @@ func (r *Reader) compressionImageSizes(ctx context.Context, images []ImageInfo,
return result, ctx.Err()
}
// DrawObject 汇总原图和替换图像的完整显示尺寸,不使用裁剪后的局部面积
// DrawObject 汇总图片、替换图及文字和路径底纹的尺寸,不使用裁剪后的局部面积
// 入参: object 图形对象, state 继承状态
// 返回: error 取消或几何错误
func (v *compressionImages) DrawObject(object *GraphicObject, state RenderState) error {
if err := v.ctx.Err(); err != nil {
return err
}
if object.Type != "ImageObject" {
switch object.Type {
case "PathObject":
return v.objectPatterns(object.PathObject, state)
case "TextObject":
text := object.TextObject
return v.objectPatterns(PathObject{Boundary: text.Boundary, CTM: text.CTM, DrawParam: text.DrawParam, FillColor: text.FillColor, StrokeColor: text.StrokeColor}, state)
case "ImageObject":
default:
return nil
}
img := object.ImageObject
@@ -182,6 +192,35 @@ func (v *compressionImages) DrawObject(object *GraphicObject, state RenderState)
}
matrix, _ := renderObjectMatrix(img.Boundary, local, state)
size := compressionImageSize(math.Hypot(matrix.a, matrix.b), math.Hypot(matrix.c, matrix.d), v.dpi)
return v.recordSize(ids, size)
}
// VisitComposite 按矢量资源的完整变换统计缩略图和替换图,不以裁剪区域缩减尺寸
// 入参: object 矢量资源或复合图元, state 已包含当前图元变换的状态
// 返回: error 取消、资源标识或几何错误
func (v *compressionImages) VisitComposite(object *CompositeGraphicUnit, state RenderState) error {
if err := v.ctx.Err(); err != nil {
return err
}
if object.Thumbnail == "" && object.Substitution == "" {
return nil
}
ids := [2]string{editorResourceID(object.Thumbnail), editorResourceID(object.Substitution)}
if object.Thumbnail != "" && ids[0] == "" || object.Substitution != "" && ids[1] == "" {
return fmt.Errorf("invalid composite image resource")
}
matrix := IdentityMatrix
if state.Parent != nil {
matrix = *state.Parent
}
size := compressionImageSize(object.Width*math.Hypot(matrix.a, matrix.b), object.Height*math.Hypot(matrix.c, matrix.d), v.dpi)
return v.recordSize(ids, size)
}
// recordSize 合并同一图像资源在不同位置的最大像素需求
// 入参: ids 图像标识,空值不统计, size 当前像素需求
// 返回: error 尺寸无效时返回错误
func (v *compressionImages) recordSize(ids [2]string, size image.Point) error {
if size.X == 0 || size.Y == 0 {
return fmt.Errorf("invalid image placement")
}
@@ -199,3 +238,11 @@ func (v *compressionImages) DrawObject(object *GraphicObject, state RenderState)
// 入参: stamp 印章
// 返回: error 始终为空
func (v *compressionImages) DrawStamp(stamp Stamp) error { return nil }
// IncludeHidden 将隐藏内容纳入尺寸分析,保留图片在所有使用位置的精度
// 返回: bool 始终为真
func (v *compressionImages) IncludeHidden() bool { return true }
// SkipClips 按完整图片尺寸分析,不加载裁剪几何或文字轮廓
// 返回: bool 始终为真
func (v *compressionImages) SkipClips() bool { return true }
+4 -7
View File
@@ -15,13 +15,10 @@
package ofdgo
import (
"bytes"
"crypto/sha256"
"image"
"runtime"
"sync"
"github.com/xiaoqidun/pdfgo"
)
// outputImageBatchLimit 限制并行批次持有的原始编码,超限图片单独处理
@@ -37,8 +34,8 @@ type outputImageJob struct {
// outputImageSource 保存已读取资源的摘要和格式,不保留原始编码
type outputImageSource struct {
key outputImageKey
jpeg bool
key outputImageKey
format string
}
// outputImageOptions 统一资源压缩及像素需求,受保护图片只进行无损编码
@@ -92,7 +89,7 @@ func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, e
options, demand := e.outputImageOptions(key)
cacheKey := outputImageKey{hash: sha256.Sum256(data), options: options, size: demand}
if sources != nil {
sources[key] = outputImageSource{key: cacheKey, jpeg: bytes.HasPrefix(data, []byte{255, 216})}
sources[key] = outputImageSource{key: cacheKey, format: outputImageFormat(data)}
}
if _, cached := results[cacheKey]; cached {
continue
@@ -126,7 +123,7 @@ func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, e
func (e *Editor) runOutputImages(jobs []outputImageJob, results map[outputImageKey][]byte) error {
ctx := e.output.ctx
optimize := func(job *outputImageJob) {
job.candidate, job.err = pdfgo.OptimizeImageResource(ctx, job.data, job.key.options, job.key.size)
job.candidate, job.err = optimizeImageResource(ctx, job.data, job.key.options, job.key.size)
}
if len(jobs) == 1 {
optimize(&jobs[0])
+142
View File
@@ -0,0 +1,142 @@
// 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 (
"fmt"
"math"
)
// compressionPatternImageLimit 限制底纹尺寸分析的变换组合数,超限时保留原图尺寸
const compressionPatternImageLimit = 65536
// compressionPatternKey 复用相同底纹的尺寸分析,平移不影响像素需求
type compressionPatternKey struct {
pattern *Pattern
linear [4]float64
}
// objectPatterns 统计文字和路径的填充及描边底纹,保留绘制参数继承关系
// 入参: object 对象样式及坐标, state 继承状态
// 返回: error 底纹分析错误
func (v *compressionImages) objectPatterns(object PathObject, state RenderState) error {
fill, stroke := object.FillColor, (*FillColor)(object.StrokeColor)
if defaults := v.renderer.drawParamDefaults(object.DrawParam, state.Defaults); defaults != nil {
if fill == nil {
fill = defaults.FillColor
}
if stroke == nil {
stroke = (*FillColor)(defaults.StrokeColor)
}
}
if (fill == nil || fill.Pattern == nil) && (stroke == nil || stroke.Pattern == nil) {
return nil
}
matrix, _ := renderObjectMatrix(object.Boundary, NewMatrix(object.CTM), state)
for _, color := range [2]*FillColor{fill, stroke} {
if color != nil && color.Pattern != nil {
if err := v.patternImages(color, matrix); err != nil {
return err
}
}
}
return nil
}
// patternImages 按独立单元坐标汇总底纹图片及缩略图,不展开重复铺排的单元
// 入参: color 底纹颜色, object 对象到页面矩阵
// 返回: error 取消、循环引用或变换错误
func (v *compressionImages) patternImages(color *FillColor, object Matrix) error {
if err := v.ctx.Err(); err != nil {
return err
}
pattern := color.Pattern
if v.patterns[pattern] {
return fmt.Errorf("cyclic pattern image reference")
}
if !finite(pattern.Width) || !finite(pattern.Height) || pattern.Width <= 0 || pattern.Height <= 0 {
return fmt.Errorf("invalid pattern cell size")
}
matrix := NewMatrix(pattern.CTM)
switch pattern.RelativeTo {
case "", "Object":
matrix = object.Multiply(matrix)
case "Page":
if v.patternPage != nil {
matrix = v.patternPage.Multiply(matrix)
}
default:
return fmt.Errorf("invalid pattern coordinate system")
}
if !finite(matrix.a) || !finite(matrix.b) || !finite(matrix.c) || !finite(matrix.d) {
return fmt.Errorf("invalid pattern transform")
}
matrix.e, matrix.f = 0, 0
key := compressionPatternKey{pattern: pattern, linear: [4]float64{matrix.a, matrix.b, matrix.c, matrix.d}}
if v.patternVisits[key] {
return nil
}
if len(v.patternVisits) >= compressionPatternImageLimit {
return fmt.Errorf("pattern image analysis limit exceeded")
}
if v.patterns == nil {
v.patterns = make(map[*Pattern]bool)
v.patternVisits = make(map[compressionPatternKey]bool)
}
v.patternVisits[key] = true
v.patterns[pattern] = true
defer delete(v.patterns, pattern)
if pattern.CellContent.Thumbnail != "" {
id := editorResourceID(pattern.CellContent.Thumbnail)
if id == "" {
return fmt.Errorf("invalid pattern thumbnail resource")
}
size := compressionImageSize(pattern.Width*math.Hypot(matrix.a, matrix.b), pattern.Height*math.Hypot(matrix.c, matrix.d), v.dpi)
if err := v.recordSize([2]string{id}, size); err != nil {
return err
}
}
columns, rows := 1, 1
switch pattern.ReflectMethod {
case "", "Normal":
case "Column":
columns = 2
case "Row":
rows = 2
case "RowAndColumn":
columns, rows = 2, 2
default:
return fmt.Errorf("invalid pattern reflection")
}
parent := v.patternPage
defer func() { v.patternPage = parent }()
solid := FillColor{Value: color.Value, Index: color.Index, ColorSpace: color.ColorSpace}
defaults := &DrawParam{FillColor: &solid, StrokeColor: (*StrokeColor)(&solid)}
for column := range columns {
for row := range rows {
cell := matrix.Multiply(Matrix{a: float64(1 - 2*column), d: float64(1 - 2*row)})
v.patternPage = &cell
for member := range pattern.CellContent.objects() {
if err := v.ctx.Err(); err != nil {
return err
}
if err := v.renderer.WalkObject(&member, RenderState{Defaults: defaults, Parent: &cell, BoundaryInCTM: true}, v); err != nil {
return err
}
}
}
}
return nil
}
+135
View File
@@ -0,0 +1,135 @@
// 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 (
"bytes"
"context"
"crypto/sha256"
"fmt"
"io"
"maps"
"slices"
)
// editorProfileReference 记录颜色配置文件所在的资源索引及颜色空间标识
type editorProfileReference struct {
index string
id string
}
// compactResourceProfiles 合并相同颜色配置文件,保留颜色空间声明和独立文件引用
// 入参: ctx 取消上下文, reader 原包,可为空, parts 输出改动, profiles 配置文件引用, protected 不可替换的文件, removed 待移除文件
// 返回: error 读取、改写或取消错误
func compactResourceProfiles(ctx context.Context, reader *Reader, parts map[string][]byte, profiles map[string][]editorProfileReference, protected, removed map[string]bool) error {
if len(profiles) < 2 {
return ctx.Err()
}
seen := make(map[[32]byte]string)
aliases := make(map[string]string)
indexes := make(map[string]map[string]string)
buffer := make([]byte, 32*1024)
for _, name := range slices.Sorted(maps.Keys(profiles)) {
if err := ctx.Err(); err != nil {
return err
}
if protected[name] {
continue
}
var input io.ReadCloser
if data, ok := parts[name]; ok {
input = io.NopCloser(bytes.NewReader(data))
} else {
if reader == nil {
return fmt.Errorf("file not found: %s", name)
}
var err error
input, err = reader.openFile(name)
if err != nil {
return err
}
}
hash := sha256.New()
_, err := io.CopyBuffer(hash, imageInput{ReadCloser: input, context: ctx}, buffer)
closeErr := input.Close()
if err != nil {
return err
}
if closeErr != nil {
return closeErr
}
key := [32]byte(hash.Sum(nil))
first, found := seen[key]
if !found {
seen[key] = name
continue
}
aliases[name] = first
for _, reference := range profiles[name] {
if indexes[reference.index] == nil {
indexes[reference.index] = make(map[string]string)
}
indexes[reference.index][reference.id] = first
}
}
updates := make(map[string][]byte, len(indexes))
for _, name := range slices.Sorted(maps.Keys(indexes)) {
if err := ctx.Err(); err != nil {
return err
}
data, ok := parts[name]
if !ok {
if reader == nil {
return fmt.Errorf("file not found: %s", name)
}
var err error
data, err = reader.readFile(name)
if err != nil {
return err
}
}
root, err := parseEditorXMLContext(ctx, data)
if err != nil {
return err
}
var patches []editorXMLPatch
for _, group := range root.children {
if !group.matchesOFD("ColorSpaces") {
continue
}
for _, node := range group.children {
location := indexes[name][editorResourceID(node.attr("ID"))]
if !node.matchesOFD("ColorSpace") || location == "" {
continue
}
fragment, err := editorXMLAttribute(data, node, "Profile", "/"+location)
if err != nil {
return err
}
patches = append(patches, editorXMLPatch{start: node.start, end: node.end, data: fragment})
}
}
updates[name] = editorPatchXML(data, patches)
}
if err := ctx.Err(); err != nil {
return err
}
maps.Copy(parts, updates)
for name := range aliases {
removed[name] = true
delete(parts, name)
}
return nil
}
+183
View File
@@ -0,0 +1,183 @@
// 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 (
"bytes"
"context"
"encoding/binary"
"hash/crc32"
"image"
"io"
"math"
"github.com/xiaoqidun/pdfgo"
)
// rasterDensity 保存横纵像素密度,unit为0表示比例、1表示每米像素数
type rasterDensity struct {
x, y float64
unit byte
}
// outputImageFormat 识别可由资源压缩生成的图片格式
// 入参: data 图片编码
// 返回: string 标准资源格式,未识别时为空
func outputImageFormat(data []byte) string {
if isJPEGData(data) {
return "JPEG"
}
if isPNGData(data) {
return "PNG"
}
return ""
}
// optimizeImageResource 将可完整保留的BMP及TIFF转为PNG,再复用图像压缩和降采样策略
// 原始模式及无法保留的色彩配置不转换,仅采用小于原始编码的结果
// 入参: ctx 取消上下文, data 图片编码, options 压缩选项, size 像素需求
// 返回: []byte 图片编码,可能引用输入, error 解码、编码或取消错误
func optimizeImageResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]byte, error) {
if err := options.Validate(); err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
if options.Mode != CompressionUnchanged && isTIFFData(data) {
return optimizeTIFFResource(ctx, data, options, size)
}
if options.Mode == CompressionUnchanged || !bytes.HasPrefix(data, []byte("BM")) {
return pdfgo.OptimizeImageResource(ctx, data, options, size)
}
return optimizeBMPResource(ctx, data, options, size)
}
// optimizeBMPResource 压缩可保留色彩与密度的位图,未知元数据及高精度位域保持原样
// 入参: ctx 取消上下文, data BMP编码, options 压缩选项, size 像素需求
// 返回: []byte 图片编码, error 解码、编码或取消错误
func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]byte, error) {
offset, width, height, depth, compression, err := parseBMPHeader(data)
if err != nil {
return data, nil
}
if height < 0 {
height = -height
}
if int64(width)*int64(height) > outputOptimizationBufferLimit/4 || compression != 0 && compression != 3 {
return data, nil
}
header := binary.LittleEndian.Uint32(data[14:18])
if header != 40 && header != 108 && header != 124 {
return data, nil
}
if header >= 108 && binary.LittleEndian.Uint32(data[70:74]) != 0x73524742 {
return data, nil
}
if header == 124 && (binary.LittleEndian.Uint32(data[122:126]) != 0 || binary.LittleEndian.Uint32(data[126:130]) != 0 || binary.LittleEndian.Uint32(data[130:134]) != 0) {
return data, nil
}
stride := (int64(width)*int64(depth) + 31) / 32 * 4
if int64(offset)+stride*int64(height) != int64(len(data)) {
return data, nil
}
xdpi, ydpi := int32(binary.LittleEndian.Uint32(data[38:42])), int32(binary.LittleEndian.Uint32(data[42:46]))
if xdpi < 0 || ydpi < 0 {
return data, nil
}
if depth == 16 || depth == 32 {
masks, err := bmpColorMasks(data, offset, depth, compression)
if err != nil {
return data, nil
}
if options.Mode == CompressionLossless {
for _, mask := range masks {
if mask.max > 255 {
return data, nil
}
}
}
}
var pixels image.Image
if depth == 16 || compression == 3 {
pixels, err = decodeBMPImageContext(ctx, data)
} else {
pixels, _, err = image.Decode(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
}
if err != nil {
return nil, err
}
return optimizeRasterResource(ctx, data, pixels, options, size, rasterDensity{x: float64(xdpi), y: float64(ydpi), unit: 1})
}
// optimizeRasterResource 编码已解码的资源图片,同步降采样后的像素密度,仅采用更小结果
// 入参: ctx 取消上下文, data 原编码, pixels 像素, options 压缩选项, size 像素需求, density 像素密度
// 返回: []byte 图片编码, error 编码或取消错误
func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image, options CompressionOptions, size image.Point, density rasterDensity) ([]byte, error) {
width, height := pixels.Bounds().Dx(), pixels.Bounds().Dy()
var encoded bytes.Buffer
limit := &outputCompressionBuffer{buffer: &encoded, limit: outputOptimizationBufferLimit}
if err := pdfgo.EncodePNG(ctx, limit, pixels); err != nil {
if limit.exceeded {
return data, ctx.Err()
}
return nil, err
}
var candidate []byte
var err error
if density.x == 0 && density.y == 0 {
candidate, err = pdfgo.OptimizeImageResource(ctx, encoded.Bytes(), options, size)
} else {
candidate, err = pdfgo.OptimizeImageSize(ctx, encoded.Bytes(), options, size)
if err == nil {
config, _, configErr := image.DecodeConfig(bytes.NewReader(candidate))
if configErr != nil {
return nil, configErr
}
x := density.x * float64(config.Width) / float64(width)
y := density.y * float64(config.Height) / float64(height)
if density.unit == 0 {
factor := float64(math.MaxUint32) / max(x, y)
x, y = x*factor, y*factor
}
x, y = math.Round(x), math.Round(y)
if x > math.MaxUint32 || y > math.MaxUint32 || density.x > 0 && x < 1 || density.y > 0 && y < 1 {
return data, ctx.Err()
}
chunk := make([]byte, 21)
binary.BigEndian.PutUint32(chunk, 9)
copy(chunk[4:], "pHYs")
binary.BigEndian.PutUint32(chunk[8:], uint32(x))
binary.BigEndian.PutUint32(chunk[12:], uint32(y))
chunk[16] = density.unit
binary.BigEndian.PutUint32(chunk[17:], crc32.ChecksumIEEE(chunk[4:17]))
var output bytes.Buffer
writer := imageMetadataWriter{writer: &output, skip: 33, data: chunk}
if _, err = writer.Write(candidate); err == nil {
candidate = output.Bytes()
}
}
}
if err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
if len(candidate) < len(data) {
return candidate, nil
}
return data, nil
}
+18 -9
View File
@@ -24,8 +24,6 @@ import (
"path"
"slices"
"strings"
"github.com/xiaoqidun/pdfgo"
)
// outputImageKey 区分相同图像的压缩配置和像素需求,不跨写出保留状态
@@ -39,6 +37,13 @@ type outputImageKey struct {
// 入参: parts 输出改动, reader 原包,可为空, removed 已移除条目
// 返回: error 读取或取消错误
func (e *Editor) compressResourceParts(parts map[string][]byte, reader *Reader, removed map[string]bool) error {
if reader == nil && e.output != nil && e.output.options.Mode != CompressionUnchanged && !e.output.protected {
unused, err := e.compactResourceReferences(parts, nil, editorProgress(e.OnWriteProgress), nil, nil)
if err != nil {
return err
}
maps.Copy(removed, unused)
}
return e.compressResourceReferences(parts, reader, removed, nil)
}
@@ -225,7 +230,7 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
candidate, cached := imageResults[cacheKey]
var err error
if !cached {
candidate, err = pdfgo.OptimizeImageResource(e.output.ctx, data, options, demand)
candidate, err = optimizeImageResource(e.output.ctx, data, options, demand)
if err == nil {
imageResults[cacheKey] = nil
if len(candidate) < len(data) {
@@ -245,12 +250,16 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
source.key.hash = sha256.Sum256(data)
}
if data != nil {
source.jpeg = bytes.HasPrefix(data, []byte{255, 216})
source.format = outputImageFormat(data)
}
if source.jpeg {
formats[key] = "JPEG"
if source.format != "" {
formats[key] = source.format
suffix := ".png"
if source.format == "JPEG" {
suffix = ".jpg"
}
ext := path.Ext(actual[key])
if !strings.EqualFold(ext, ".jpg") && !strings.EqualFold(ext, ".jpeg") {
if !strings.EqualFold(ext, suffix) && !(source.format == "JPEG" && strings.EqualFold(ext, ".jpeg")) {
if data == nil {
data, err = read(actual[key])
if err != nil {
@@ -258,9 +267,9 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
}
}
stem := strings.TrimSuffix(actual[key], ext)
name := stem + ".jpg"
name := stem + suffix
for n := 1; reserved[name]; n++ {
name = fmt.Sprintf("%s-%d.jpg", stem, n)
name = fmt.Sprintf("%s-%d%s", stem, n, suffix)
}
reserved[name] = true
locations[key] = name
+167
View File
@@ -0,0 +1,167 @@
// 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 (
"bytes"
"context"
"encoding/binary"
"image"
"math"
)
// optimizeTIFFResource 压缩单目录TIFF,保留方向、透明度、采样精度及像素密度
// 入参: ctx 取消上下文, data TIFF编码, options 压缩选项, size 显示像素需求
// 返回: []byte 图片编码,不可完整转换时保留原文, error 编码或取消错误
func optimizeTIFFResource(ctx context.Context, data []byte, options CompressionOptions, size image.Point) ([]byte, error) {
layout, err := readTIFFLayout(bytes.NewReader(data))
if err != nil {
return data, ctx.Err()
}
pixelSize := int64(8)
if layout.planar {
pixelSize = 16
}
if int64(layout.width) > outputOptimizationBufferLimit/pixelSize/int64(layout.height) {
return data, ctx.Err()
}
density, ok := tiffCompressionDensity(data, layout)
if !ok {
return data, ctx.Err()
}
pixels, err := decodeTIFFImage(ctx, data, layout)
if err != nil {
return data, ctx.Err()
}
return optimizeRasterResource(ctx, data, pixels, options, size, density)
}
// tiffCompressionDensity 校验可转换的TIFF字段并提取规范化后的像素密度
// 色彩配置、预乘透明度、扩展字段及其他目录需保留原始编码
// 入参: data TIFF编码, layout 已校验的主图布局
// 返回: rasterDensity PNG像素密度, bool 是否可完整转换
func tiffCompressionDensity(data []byte, layout tiffImageLayout) (rasterDensity, bool) {
var density rasterDensity
var order binary.ByteOrder = binary.LittleEndian
if data[0] == 'M' {
order = binary.BigEndian
}
if layout.next != 0 || layout.offset != order.Uint32(data[4:]) || layout.reduced {
return density, false
}
offset := int(layout.offset)
count := int(order.Uint16(data[offset:]))
unit := uint32(2)
var resolutions [2][2]uint32
for i := range count {
entry := data[offset+2+i*12 : offset+14+i*12]
tag, kind, number := order.Uint16(entry), order.Uint16(entry[2:]), order.Uint32(entry[4:])
value := order.Uint32(entry[8:])
if kind == 3 && number == 1 {
value = uint32(order.Uint16(entry[8:]))
}
switch tag {
case 256, 257, 258, 259, 262, 273, 274, 277, 278, 279, 320, 322, 323, 324, 325:
case 254:
if value != 0 {
return density, false
}
case 255:
if value != 1 {
return density, false
}
case 266:
if kind != 3 || number != 1 || value != 1 && value != 2 {
return density, false
}
case 284:
if kind != 3 || number != 1 || value != 1 && value != 2 {
return density, false
}
case 292, 293:
mask := uint32(7)
if tag == 293 {
mask = 2
}
if kind != 4 || number != 1 || value & ^mask != 0 {
return density, false
}
case 317:
if kind != 3 || number != 1 || value != 1 && value != 2 {
return density, false
}
case 338:
if kind != 3 || number != 1 || value != 2 {
return density, false
}
case 339:
if kind != 3 || number == 0 || number > 4 {
return density, false
}
values := entry[8:]
if number > 2 {
if uint64(value)+uint64(number)*2 > uint64(len(data)) {
return density, false
}
values = data[value:]
}
for j := uint32(0); j < number; j++ {
if order.Uint16(values[j*2:]) != 1 {
return density, false
}
}
case 282, 283:
if kind != 5 || number != 1 || uint64(value)+8 > uint64(len(data)) {
return density, false
}
resolutions[tag-282] = [2]uint32{order.Uint32(data[value:]), order.Uint32(data[value+4:])}
case 296:
if kind != 3 || number != 1 || value < 1 || value > 3 {
return density, false
}
unit = value
default:
return density, false
}
}
if resolutions == [2][2]uint32{} {
return density, true
}
x, y := resolutions[0], resolutions[1]
if x[0] == 0 || x[1] == 0 || y[0] == 0 || y[1] == 0 {
return density, false
}
if unit == 1 {
a, b := uint64(x[0])*uint64(y[1]), uint64(y[0])*uint64(x[1])
divisor, next := a, b
for next != 0 {
divisor, next = next, divisor%next
}
density.x, density.y = float64(a/divisor), float64(b/divisor)
} else {
factor := 1 / 0.0254
if unit == 3 {
factor = 100
}
density = rasterDensity{x: float64(x[0]) / float64(x[1]) * factor, y: float64(y[0]) / float64(y[1]) * factor, unit: 1}
}
if math.Round(density.x) < 1 || math.Round(density.y) < 1 || math.Round(density.x) > math.MaxUint32 || math.Round(density.y) > math.MaxUint32 {
return density, false
}
if layout.orientation >= 5 {
density.x, density.y = density.y, density.x
}
return density, true
}
+25 -6
View File
@@ -16,6 +16,7 @@ package ofdgo
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
@@ -256,6 +257,16 @@ func (e *Editor) CopyPage(index int) (int, error) {
// 入参: indexes 不重复的原页面索引
// 返回: []int 副本页面索引, error 错误信息
func (e *Editor) CopyPages(indexes []int) ([]int, error) {
return e.CopyPagesContext(context.Background(), indexes)
}
// CopyPagesContext 复制页面并响应取消,准备期间不提交页面或撤销记录
// 入参: ctx 取消上下文, indexes 不重复的原页面索引
// 返回: []int 副本页面索引, error 校验、复制或取消错误
func (e *Editor) CopyPagesContext(ctx context.Context, indexes []int) ([]int, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if err := e.validatePageIndexes(indexes); err != nil {
return nil, err
}
@@ -263,11 +274,12 @@ func (e *Editor) CopyPages(indexes []int) ([]int, error) {
return nil, nil
}
at := len(e.pages)
source, err := e.Reader()
source, err := e.readerContext(ctx, nil)
if err != nil {
return nil, err
}
if _, err := e.importPages(source, indexes, at, true, PageImportOptions{}); err != nil {
defer source.Close()
if _, err := e.importPages(ctx, source, indexes, at, true, PageImportOptions{}); err != nil {
return nil, err
}
result := make([]int, len(indexes))
@@ -456,7 +468,7 @@ func (e *Editor) ResizePages(indexes []int, width, height float64) error {
return nil
}
// AddImage 注册PNG、JPEG或JBIG2图片,重复资源复用标识,引用后写入文档
// AddImage 注册PNG、JPEG、BMP、单页TIFF或JBIG2图片,重复资源复用标识,引用后写入文档
// 不透明纯黑白PNG仅在无损JBIG2编码更小时转换,其他图片保留原始编码
// 入参: data 图片数据
// 返回: string 图片资源标识, error 错误信息
@@ -464,11 +476,18 @@ func (e *Editor) AddImage(data []byte) (string, error) {
return e.addDecodedImage(data, nil)
}
// ImageSize 获取图片资源的像素尺寸,优先复用注册结果,不解码像素
// 入参: id 图片资源标识
// 返回: image.Point 像素宽高, error 资源或格式错误
func (e *Editor) ImageSize(id string) (image.Point, error) {
return e.editorImage(id)
}
// addDecodedImage 注册图片并复用与编码数据一致的像素
// 入参: data 图片数据, decoded 已解码像素,为nil时按需解码
// 返回: string 资源标识, error 注册错误
func (e *Editor) addDecodedImage(data []byte, decoded image.Image) (string, error) {
config, format, err := image.DecodeConfig(bytes.NewReader(data))
config, format, err := decodeImageConfigData(data)
if err != nil {
return "", err
}
@@ -479,8 +498,8 @@ func (e *Editor) addDecodedImage(data []byte, decoded image.Image) (string, erro
// 入参: data 图片编码数据, config 图片配置, format 图片格式, decoded 已解码图像,可为nil
// 返回: string 资源标识, error 格式或资源错误
func (e *Editor) addImage(data []byte, config image.Config, format string, decoded image.Image) (string, error) {
if (format != "png" && format != "jpeg" && format != "jbig2") || config.Width <= 0 || config.Height <= 0 {
return "", fmt.Errorf("only PNG, JPEG and JBIG2 images are supported")
if (format != "png" && format != "jpeg" && format != "bmp" && format != "tiff" && format != "jbig2") || config.Width <= 0 || config.Height <= 0 {
return "", fmt.Errorf("only PNG, JPEG, BMP, single-page TIFF and JBIG2 images are supported")
}
key := editorResourceKey{checksum: sha256.Sum256(data), index: -1}
if id, ok := e.resourceID[key]; ok {
+59 -21
View File
@@ -113,17 +113,15 @@ func (e *Editor) AddDocument(title string, width, height float64, at int) (int,
return 0, err
}
err := e.editDocuments(func(reader *Reader, data []byte, root *editorXML) (int, error) {
occupied := make(map[string]bool)
for name := range reader.fileNames {
directory, _, _ := strings.Cut(name, "/")
occupied[directory] = true
}
directory := ""
for number := 0; ; number++ {
candidate := fmt.Sprintf("Doc_%d", number)
occupied := false
for name := range reader.fileNames {
if name == candidate || strings.HasPrefix(name, candidate+"/") {
occupied = true
break
}
}
if !occupied {
if !occupied[candidate] {
directory = candidate
break
}
@@ -219,14 +217,25 @@ func (e *Editor) MoveDocument(from, to int) error {
// 入参: index 文档索引,从0开始
// 返回: error 删除错误
func (e *Editor) DeleteDocument(index int) error {
return e.DeleteDocumentContext(context.Background(), index)
}
// DeleteDocumentContext 删除指定文档及其独占文件,取消或失败时保留当前状态和撤销历史
// 包内至少保留一个文档,其他文档及版本仍在使用的文件不删除
// 入参: ctx 取消上下文, index 文档索引,从0开始
// 返回: error 删除或取消错误
func (e *Editor) DeleteDocumentContext(ctx context.Context, index int) error {
if err := ctx.Err(); err != nil {
return err
}
if index < 0 || index >= e.DocumentCount() {
return fmt.Errorf("document index %d out of range", index)
}
if e.DocumentCount() == 1 {
return fmt.Errorf("package must contain at least one document")
}
return e.editDocuments(func(reader *Reader, data []byte, root *editorXML) (int, error) {
removed, err := reader.documentFiles(context.Background(), index)
return e.editDocumentsContext(ctx, func(reader *Reader, data []byte, root *editorXML) (int, error) {
removed, err := reader.documentFiles(ctx, index)
if err != nil {
return 0, err
}
@@ -236,25 +245,35 @@ func (e *Editor) DeleteDocument(index int) error {
return name != cleanPackagePath(reader.OFD.DocBody[index].DocRoot) && strings.HasPrefix(name, directory+"/")
}) {
for name := range reader.fileNames {
if err := ctx.Err(); err != nil {
return 0, err
}
if strings.HasPrefix(name, directory+"/") {
removed[name] = true
}
}
}
var others []int
for other := range reader.OFD.DocBody {
if other == index {
continue
if other != index {
others = append(others, other)
}
kept, err := reader.documentFiles(context.Background(), other)
if err != nil {
}
kept, err := reader.documentFiles(ctx, others...)
if err != nil {
return 0, err
}
for name := range kept {
if err := ctx.Err(); err != nil {
return 0, err
}
for name := range kept {
delete(removed, name)
}
delete(removed, name)
}
delete(removed, "OFD.xml")
for name := range reader.files {
if err := ctx.Err(); err != nil {
return 0, err
}
if removed[name] {
delete(reader.files, name)
}
@@ -280,15 +299,26 @@ func (e *Editor) DeleteDocument(index int) error {
// 入参: edit 修改包快照并返回新的当前文档索引
// 返回: error 修改错误
func (e *Editor) editDocuments(edit func(*Reader, []byte, *editorXML) (int, error)) error {
reader, err := e.Reader()
ctx := context.Background()
if e.output != nil {
ctx = e.output.ctx
}
return e.editDocumentsContext(ctx, edit)
}
// editDocumentsContext 在独立快照中修改文档入口,确认未取消后提交完整状态
// 入参: ctx 取消上下文, edit 修改包快照并返回新的当前文档索引
// 返回: error 修改或取消错误
func (e *Editor) editDocumentsContext(ctx context.Context, edit func(*Reader, []byte, *editorXML) (int, error)) error {
reader, err := e.readerContext(ctx, nil)
if err != nil {
return err
}
data, err := reader.readFile("OFD.xml")
data, err := reader.readFileViewContext(ctx, "OFD.xml")
if err != nil {
return err
}
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(ctx, data)
if err != nil {
return err
}
@@ -298,6 +328,9 @@ func (e *Editor) editDocuments(edit func(*Reader, []byte, *editorXML) (int, erro
if err != nil {
return err
}
if err := ctx.Err(); err != nil {
return err
}
before := e.transactionSnapshot()
reader.documentIndex, reader.doc = index, nil
if err := reader.initRoot(); err != nil {
@@ -310,9 +343,14 @@ func (e *Editor) editDocuments(edit func(*Reader, []byte, *editorXML) (int, erro
if _, err := reader.Doc(); err != nil {
return err
}
if err := e.adoptDocumentReader(reader); err != nil {
next := *e
if err := next.adoptDocumentReader(reader); err != nil {
return err
}
if err := ctx.Err(); err != nil {
return err
}
*e = next
e.recordTransaction(before)
return nil
}
+24 -10
View File
@@ -175,17 +175,30 @@ func editorObjectFontScan(object GraphicObject) bool {
return clips != nil || fill != nil && fill.Pattern != nil || stroke != nil && stroke.Pattern != nil
}
// subsetFonts 收集新增字体实际使用的字形,仅裁剪保存结果,保留完整编辑字体
// subsetFonts 仅解析实际引用的新增字体,按用字裁剪保存结果,保留完整编辑字体
// 入参: progress 保存进度回调
// 返回: map[string][]byte 包内字体子集, error 错误信息
func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) {
if e.backends.FontResources == nil {
return nil, nil
}
if err := progress.report("fonts", 0, 0); err != nil {
return nil, err
}
if !slices.ContainsFunc(e.resources, func(resource editorResource) bool {
return resource.font != nil && resource.font.FontFile != ""
}) {
return nil, nil
}
fonts, _, _, files := e.usedResources()
total := len(fonts) + len(e.pages) + 2*len(e.resources)
used := make(map[string]map[uint16]bool)
mapped := make(map[string]bool)
for _, resource := range e.resources {
if resource.font == nil || resource.font.FontFile == "" {
for i, resource := range fonts {
if err := progress.report("fonts", i, total); err != nil {
return nil, err
}
if resource.font.FontFile == "" {
continue
}
resourceFont := e.fonts[resource.font.ID]
@@ -202,7 +215,10 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
}
}
if len(used) == 0 {
return nil, progress.report("fonts", 0, 0)
for i := range e.resources {
e.resources[i].subset = nil
}
return nil, progress.report("fonts", total, total)
}
refs := editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool)}
nested := false
@@ -222,9 +238,8 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
}
}
}
total := len(e.pages) + len(e.resources)
for i, page := range e.pages {
if err := progress.report("fonts", i, total); err != nil {
if err := progress.report("fonts", len(fonts)+i, total); err != nil {
return nil, err
}
for _, layer := range page.Content.Layer {
@@ -278,12 +293,11 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
}
}
if nested {
_, _, _, files := e.usedResources()
for i, resource := range e.resources {
if resource.definition() == "" || !slices.Contains(files, resource.name) {
if resource.definition() == "" || !files[resource.name] {
continue
}
if err := progress.report("fonts", len(e.pages)+i, total); err != nil {
if err := progress.report("fonts", len(fonts)+len(e.pages)+i, total); err != nil {
return nil, err
}
if resource.usage.matches(resource.name, resource.data) {
@@ -315,7 +329,7 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
}
result := make(map[string][]byte)
for i := range e.resources {
if err := progress.report("fonts", len(e.pages)+i, total); err != nil {
if err := progress.report("fonts", len(fonts)+len(e.pages)+len(e.resources)+i, total); err != nil {
return nil, err
}
resource := &e.resources[i]
+10 -5
View File
@@ -40,7 +40,7 @@ type ImagePageOptions struct {
OnProgress func(stage string, completed, total int) error
}
// ImportImages 将PNG、JPEG或JBIG2逐张建页并插入指定位置,一次操作计入一条撤销记录
// ImportImages 将PNG、JPEG、BMP、单页TIFF或JBIG2逐张建页并插入指定位置,一次操作计入一条撤销记录
// 保留像素及透明度,方向通过对象变换实现,不重新采样;图片压缩沿用AddImage规则
// 失败或取消时不修改文档,读取期间仍需容纳单张图片的解码数据
// 入参: ctx 取消上下文, sources 按页面顺序排列的图片, at 插入位置, options 建页选项
@@ -110,14 +110,19 @@ func (e *Editor) ImportImages(ctx context.Context, sources []ImagePageSource, at
// 入参: data 图片数据, options 建页选项
// 返回: int 从0开始的页面索引, error 错误信息
func (e *Editor) addImagePage(data []byte, options ImagePageOptions) (int, error) {
config, format, err := image.DecodeConfig(bytes.NewReader(data))
config, format, err := decodeImageConfigData(data)
if err != nil {
return 0, err
}
if format != "png" && format != "jpeg" && format != "jbig2" {
return 0, fmt.Errorf("only PNG, JPEG and JBIG2 images are supported")
if format != "png" && format != "jpeg" && format != "bmp" && format != "tiff" && format != "jbig2" {
return 0, fmt.Errorf("only PNG, JPEG, BMP, single-page TIFF and JBIG2 images are supported")
}
var decoded image.Image
if format == "bmp" || format == "tiff" {
decoded, _, err = decodeImageData(data)
} else {
decoded, _, err = image.Decode(bytes.NewReader(data))
}
decoded, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return 0, err
}
+163 -72
View File
@@ -32,15 +32,19 @@ type editorImportEntry struct {
name, base, group string
data []byte
node *editorXML
template bool
ids map[string]string
}
// editorPageImport 一次插页操作的独立资源图,提交前不修改目标文档
type editorPageImport struct {
ctx context.Context
reader *Reader
doc *Document
directory string
target *Reader
paths map[string]string
templatePaths map[string]string
evidence map[string]string
resourceXML []byte
pageParts []byte
@@ -95,6 +99,17 @@ type editorProgressReader struct {
// 入参: source 来源编辑器, sourcePage 来源页, ids 对象标识, page 目标页, options 迁移选项
// 返回: []string 新对象标识, error 错误信息
func (e *Editor) ImportObjects(source *Editor, sourcePage int, ids []string, page int, options ObjectImportOptions) ([]string, error) {
return e.ImportObjectsContext(context.Background(), source, sourcePage, ids, page, options)
}
// ImportObjectsContext 迁移对象及资源,取消时不提交修改或撤销记录
// 入参: ctx 取消上下文, source 来源编辑器, sourcePage 来源页, ids 对象标识, page 目标页, options 迁移选项
// 返回: []string 新对象标识, error 校验、迁移或取消错误
func (e *Editor) ImportObjectsContext(ctx context.Context, source *Editor, sourcePage int, ids []string, page int, options ObjectImportOptions) ([]string, error) {
progress := editorProgress(options.OnProgress).withContext(ctx)
if err := ctx.Err(); err != nil {
return nil, err
}
if !finite(options.DX) || !finite(options.DY) {
return nil, fmt.Errorf("import requires finite offsets")
}
@@ -110,19 +125,22 @@ func (e *Editor) ImportObjects(source *Editor, sourcePage int, ids []string, pag
return nil, err
}
if source == e {
return e.CopyObjects(page, objects, options.DX, options.DY)
if err := progress.report("commit", 0, 0); err != nil {
return nil, err
}
return e.CopyObjectsContext(ctx, page, objects, options.DX, options.DY)
}
reader, err := source.Reader()
reader, err := source.readerContext(ctx, progress)
if err != nil {
return nil, err
}
defer reader.Close()
doc, err := reader.Doc()
if err != nil {
return nil, err
}
ref := doc.Pages.Page[sourcePage]
progress := editorProgress(options.OnProgress)
base, migration, err := e.prepareImport(reader, doc, map[string]bool{ref.ID: true}, false, progress)
base, migration, err := e.prepareImport(ctx, reader, doc, map[string]bool{ref.ID: true}, false, progress)
if err != nil {
return nil, err
}
@@ -169,7 +187,7 @@ func (e *Editor) ImportObjects(source *Editor, sourcePage int, ids []string, pag
if err != nil {
return nil, err
}
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(ctx, data)
if err != nil {
return nil, err
}
@@ -207,7 +225,7 @@ func (e *Editor) ImportObjects(source *Editor, sourcePage int, ids []string, pag
return err
}
layer := content.Layer[0]
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(ctx, data)
if err != nil {
return err
}
@@ -221,7 +239,7 @@ func (e *Editor) ImportObjects(source *Editor, sourcePage int, ids []string, pag
imported[i].CompositeGraphicUnit.states = remapCompositeStates(objects[i].CompositeGraphicUnit.states, migration.ids)
imported[i].origin = &editorObjectOrigin{editor: editor, document: editor.documentIdentity(), data: data, node: root.child("Layer").children[0], object: object}
}
copied, err = editor.CopyObjects(page, imported, options.DX, options.DY)
copied, err = editor.CopyObjectsContext(ctx, page, imported, options.DX, options.DY)
return err
})
return copied, err
@@ -244,10 +262,21 @@ func (e *Editor) ImportPages(source *Reader, indexes []int, at int) ([]string, e
// 入参: source 来源阅读器, indexes 来源页面索引, at 目标插入位置, options 导入选项
// 返回: []string 新页面标识, error 错误信息
func (e *Editor) ImportPagesWithOptions(source *Reader, indexes []int, at int, options PageImportOptions) ([]string, error) {
return e.ImportPagesContext(context.Background(), source, indexes, at, options)
}
// ImportPagesContext 导入页面及关联资源,取消检查覆盖共享资源隔离和版本清单更新
// 取消时不提交页面、目录或撤销记录,其他行为与ImportPagesWithOptions相同
// 入参: ctx 取消上下文, source 来源阅读器, indexes 来源页索引, at 插入位置, options 导入选项
// 返回: []string 新页面标识, error 校验、导入或取消错误
func (e *Editor) ImportPagesContext(ctx context.Context, source *Reader, indexes []int, at int, options PageImportOptions) ([]string, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if source.encryption != nil && e.encryption == nil && !options.AllowDecrypted {
return nil, ErrEncryptionPolicyRequired
}
return e.importPages(source, indexes, at, false, options)
return e.importPages(ctx, source, indexes, at, false, options)
}
// Read 读取数据并在下一块开始前检查取消
@@ -263,9 +292,12 @@ func (r *editorProgressReader) Read(data []byte) (int, error) {
}
// importPages 迁移页面,同文档复制时复用原资源并保留其他页面的跳转
// 入参: source 来源阅读器, indexes 来源页索引, at 插入位置, copyPage 是否同文档复制, options 导入选项
// 入参: ctx 取消上下文, source 来源阅读器, indexes 来源页索引, at 插入位置, copyPage 是否同文档复制, options 导入选项
// 返回: []string 新页标识, error 错误信息
func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage bool, options PageImportOptions) ([]string, error) {
func (e *Editor) importPages(ctx context.Context, source *Reader, indexes []int, at int, copyPage bool, options PageImportOptions) ([]string, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if at < 0 || at > len(e.pages) {
return nil, fmt.Errorf("page index %d out of range", at)
}
@@ -287,8 +319,8 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
if len(indexes) == 0 {
return nil, nil
}
progress := editorProgress(options.OnProgress)
base, m, err := e.prepareImport(source, doc, selected, copyPage, progress)
progress := editorProgress(options.OnProgress).withContext(ctx)
base, m, err := e.prepareImport(ctx, source, doc, selected, copyPage, progress)
if err != nil {
return nil, err
}
@@ -300,7 +332,7 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
}
page := doc.Pages.Page[index]
ids[i] = m.id(page.ID)
name, err := m.copyXML(source.ResPath(page.BaseLoc), packagePagePath(m.directory, ids[i]))
name, err := m.copyXML(source.ResPath(page.BaseLoc), packagePagePath(m.directory, ids[i]), false)
if err != nil {
return nil, err
}
@@ -328,7 +360,7 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
if err != nil {
return nil, err
}
outlines, err = e.appendImportedOutlines(imported)
outlines, err = e.appendImportedOutlines(ctx, imported)
if err != nil {
return nil, err
}
@@ -353,6 +385,9 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
preview := *e
preview.source, preview.pages = next, added
for i, index := range indexes {
if err := ctx.Err(); err != nil {
return nil, err
}
states := make(map[string]editorCompositeState)
groups := make(map[string]map[string]editorCompositeState)
for _, layer := range e.pages[index].Content.Layer {
@@ -433,9 +468,9 @@ func (e *Editor) bindImportResources() {
}
// prepareImport 建立页面和对象迁移共用的资源图,不修改目标编辑器
// 入参: source 来源, doc 来源文档, selected 页面映射范围, copyPage 是否同文档, progress 进度
// 入参: ctx 取消上下文, source 来源, doc 来源文档, selected 页面映射范围, copyPage 是否同文档, progress 进度
// 返回: *editorSource 目标基础, *editorPageImport 资源图, error 错误信息
func (e *Editor) prepareImport(source *Reader, doc *Document, selected map[string]bool, copyPage bool, progress editorProgress) (*editorSource, *editorPageImport, error) {
func (e *Editor) prepareImport(ctx context.Context, source *Reader, doc *Document, selected map[string]bool, copyPage bool, progress editorProgress) (*editorSource, *editorPageImport, error) {
maximum, err := e.sourceMaxID(progress)
if err != nil {
return nil, nil, err
@@ -447,7 +482,7 @@ func (e *Editor) prepareImport(source *Reader, doc *Document, selected map[strin
return nil, nil, err
}
}
m := &editorPageImport{reader: source, doc: doc, directory: e.packageDirectory(), target: base.reader, maximum: maximum, copyPage: copyPage, progress: progress,
m := &editorPageImport{ctx: ctx, reader: source, doc: doc, directory: e.packageDirectory(), target: base.reader, maximum: maximum, copyPage: copyPage, progress: progress,
paths: make(map[string]string), evidence: make(map[string]string), ids: make(map[string]string), pages: make(map[string]bool, len(selected)),
files: make(map[string][]byte), resources: make(map[string]editorImportEntry), used: make(map[string][]byte),
templates: make(map[string]TemplatePage), templateParts: make(map[string][]byte), attachments: make(map[string][]byte)}
@@ -461,7 +496,7 @@ func (e *Editor) prepareImport(source *Reader, doc *Document, selected map[strin
if err != nil {
return nil, nil, err
}
m.documentRoot, err = parseEditorXML(m.documentXML)
m.documentRoot, err = parseEditorXMLContext(ctx, m.documentXML)
if err != nil {
return nil, nil, err
}
@@ -501,6 +536,9 @@ func (m *editorPageImport) encodeResources() error {
for _, group := range []string{"ColorSpaces", "DrawParams", "Fonts", "MultiMedias", "CompositeGraphicUnits"} {
var entries []byte
for _, id := range ids {
if err := m.ctx.Err(); err != nil {
return err
}
if m.resources[id].group == group {
entries = append(entries, m.used[id]...)
}
@@ -542,7 +580,7 @@ func (m *editorPageImport) outlines() ([]byte, error) {
if err != nil {
return nil, err
}
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(m.ctx, data)
if err != nil {
return nil, err
}
@@ -591,7 +629,7 @@ func (m *editorPageImport) outlines() ([]byte, error) {
}
}
data = editorPatchXML(data, patches)
root, err = parseEditorXML(data)
root, err = parseEditorXMLContext(m.ctx, data)
if err != nil {
return nil, err
}
@@ -624,13 +662,13 @@ func (m *editorPageImport) outlines() ([]byte, error) {
}
// appendImportedOutlines 将已迁移目录追加到目标根目录,规范计数且不修改当前编辑器
// 入参: imported 已迁移目录片段
// 入参: ctx 取消上下文, imported 已迁移目录片段
// 返回: []byte 合并后的目录, error 错误信息
func (e *Editor) appendImportedOutlines(imported []byte) ([]byte, error) {
func (e *Editor) appendImportedOutlines(ctx context.Context, imported []byte) ([]byte, error) {
if len(imported) == 0 {
return e.outlines, nil
}
added, err := parseEditorXML(imported)
added, err := parseEditorXMLContext(ctx, imported)
if err != nil {
return nil, err
}
@@ -638,7 +676,7 @@ func (e *Editor) appendImportedOutlines(imported []byte) ([]byte, error) {
if err != nil {
return nil, err
}
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(ctx, data)
if err != nil {
return nil, err
}
@@ -647,6 +685,9 @@ func (e *Editor) appendImportedOutlines(imported []byte) ([]byte, error) {
content = bytes.Clone(data[root.open:root.close])
}
for _, child := range editorOutlineChildren(added) {
if err := ctx.Err(); err != nil {
return nil, err
}
value, err := editorXMLStandalone(imported[child.start:child.end], child)
if err != nil {
return nil, err
@@ -732,7 +773,7 @@ func (m *editorPageImport) resourceIndex(name string) error {
if err != nil {
return err
}
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(m.ctx, data)
if err != nil {
return err
}
@@ -773,7 +814,7 @@ func (m *editorPageImport) reference(value string) (string, error) {
if template, ok := m.templates[key]; ok {
if _, seen := m.templateParts[key]; !seen {
m.templateParts[key] = nil
name, err := m.copyXML(m.reader.ResPath(template.BaseLoc), path.Join(m.directory, "Templates", "Template_"+m.id(value), "Content.xml"))
name, err := m.copyXML(m.reader.ResPath(template.BaseLoc), path.Join(m.directory, "Templates", "Template_"+m.id(value), "Content.xml"), true)
if err != nil {
return "", err
}
@@ -801,7 +842,7 @@ func (m *editorPageImport) documentReference(group, kind, source, id, loc string
return nil, err
}
for _, attr := range [][2]string{{"ID", id}, {"BaseLoc", loc}} {
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(m.ctx, data)
if err != nil {
return nil, err
}
@@ -857,11 +898,18 @@ func (m *editorPageImport) copyData(name, relative string, evidence bool) (strin
}
// copyXML 复制页面或标准关联XML并重映射引用
// 入参: name 来源路径, candidate 目标候选路径
// 入参: name 来源路径, candidate 目标候选路径, template 是否按模板页复制
// 返回: string 新路径, error 错误信息
func (m *editorPageImport) copyXML(name, candidate string) (string, error) {
func (m *editorPageImport) copyXML(name, candidate string, template bool) (string, error) {
paths := m.paths
if template {
if m.templatePaths == nil {
m.templatePaths = make(map[string]string)
}
paths = m.templatePaths
}
key := cleanPackagePath(name)
if target := m.paths[key]; target != "" {
if target := paths[key]; target != "" {
return "/" + target, nil
}
target := packageAvailableName(m.target, m.files, candidate)
@@ -869,11 +917,11 @@ func (m *editorPageImport) copyXML(name, candidate string) (string, error) {
if err != nil {
return "", err
}
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(m.ctx, data)
if err != nil {
return "", err
}
m.paths[key], m.files[target] = target, nil
paths[key], m.files[target] = target, nil
if root.name.Local == "Page" {
for _, child := range root.children {
if child.name.Local == "PageRes" {
@@ -887,7 +935,14 @@ func (m *editorPageImport) copyXML(name, candidate string) (string, error) {
}
}
}
encoded, err := m.encode(editorImportEntry{name: name, data: data}, root)
entry := editorImportEntry{name: name, data: data, template: template}
if template && slices.ContainsFunc(m.doc.Pages.Page, func(page Page) bool {
return cleanPackagePath(m.reader.ResPath(page.BaseLoc)) == key
}) {
entry.ids = make(map[string]string)
m.templateObjectIDs(root, entry.ids)
}
encoded, err := m.encode(entry, root)
if err != nil {
return "", err
}
@@ -895,6 +950,21 @@ func (m *editorPageImport) copyXML(name, candidate string) (string, error) {
return "/" + target, nil
}
// templateObjectIDs 为兼作普通页的模板内容分配独立对象标识
// 入参: node 当前节点, ids 模板副本的标识映射
func (m *editorPageImport) templateObjectIDs(node *editorXML, ids map[string]string) {
if value := node.attr("ID"); value != "" {
key := editorImportKey(value)
if ids[key] == "" {
m.maximum++
ids[key] = strconv.Itoa(m.maximum)
}
}
for _, child := range node.children {
m.templateObjectIDs(child, ids)
}
}
// editorImportText 获取节点直接文本,解码XML实体
// 入参: data XML, node 节点
// 返回: string 文本
@@ -910,6 +980,11 @@ func editorImportText(data []byte, node *editorXML) string {
// 入参: entry 来源上下文, node 当前节点
// 返回: []byte 独立XML片段, error 错误信息
func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]byte, error) {
if m.ctx != nil {
if err := m.ctx.Err(); err != nil {
return nil, err
}
}
if classifyOFDNamespace(node.name.Space) == ofdXMLUnknown {
return nil, fmt.Errorf("cannot remap extension namespace %q", node.name.Space)
}
@@ -961,7 +1036,9 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
var err error
switch key {
case "ID":
if name == "Attachment" {
if id := entry.ids[editorImportKey(value)]; id != "" {
value = id
} else if name == "Attachment" {
value, err = m.attachmentID(value)
} else if name == "Signature" || name == "StampAnnot" {
value, err = m.signatureID(entry.name, name, value)
@@ -992,7 +1069,7 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
}
directory = path.Join("Signs", "Sign_"+id)
}
value, err = m.copyXML(loc, path.Join(m.directory, directory, path.Base(loc)))
value, err = m.copyXML(loc, path.Join(m.directory, directory, path.Base(loc)), false)
}
case "AttachID":
value, err = m.attachment(value)
@@ -1032,15 +1109,24 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
var content []byte
pageContent := name == "Page" && node.parent == nil
if pageContent {
area := m.doc.CommonData.PageArea
local := node.child("Area")
var fields []byte
for _, field := range [][2]string{{"PhysicalBox", area.PhysicalBox}, {"ApplicationBox", area.ApplicationBox}, {"ContentBox", area.ContentBox}, {"BleedBox", area.BleedBox}} {
if field[1] != "" && (local == nil || local.child(field[0]) == nil) {
fields = append(fields, editorXMLText(field[0], field[1])...)
}
}
if local == nil {
var templates []Template
for _, child := range node.children {
if !entry.template && child.matchesOFD("Template") {
templates = append(templates, Template{TemplateID: child.attr("TemplateID")})
}
}
area, err := m.reader.resolvePageArea(PageArea{}, templates)
if err != nil {
return nil, err
}
var fields []byte
for _, field := range [][2]string{{"PhysicalBox", area.PhysicalBox}, {"ApplicationBox", area.ApplicationBox}, {"ContentBox", area.ContentBox}, {"BleedBox", area.BleedBox}} {
if field[1] != "" {
fields = append(fields, editorXMLText(field[0], field[1])...)
}
}
encoded, err := editorXMLContainer("Area", nil, fields)
if err != nil {
return nil, err
@@ -1051,17 +1137,13 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
if err != nil {
return nil, err
}
root, err := parseEditorXML(encoded)
if err != nil {
return nil, err
}
if root.open != root.end {
fields = append(bytes.Clone(encoded[root.open:root.close]), fields...)
}
content = editorPatchXML(encoded, []editorXMLPatch{editorXMLContent(encoded, root, fields)})
content = encoded
}
}
for _, child := range node.children {
if pageContent && entry.template && child.matchesOFD("Template") {
continue
}
if pageContent && child.name.Local == "Area" {
continue
}
@@ -1075,7 +1157,7 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
if err != nil {
return nil, err
}
root, err := parseEditorXML(encoded)
root, err := parseEditorXMLContext(m.ctx, encoded)
if err != nil {
return nil, err
}
@@ -1133,7 +1215,7 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
value, err = m.copyData(loc, path.Join("Attachments", path.Base(loc)), false)
} else {
candidate := path.Join(m.directory, "Annotations", "Page_"+m.id(node.parent.attr("PageID"))+".xml")
value, err = m.copyXML(loc, candidate)
value, err = m.copyXML(loc, candidate, false)
}
case "Font", "Substitution", "Thumbnail":
value, err = m.reference(strings.TrimSpace(value))
@@ -1162,7 +1244,7 @@ func (m *editorPageImport) annotations() ([]byte, error) {
if err != nil {
return nil, err
}
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(m.ctx, data)
if err != nil {
return nil, err
}
@@ -1198,7 +1280,7 @@ func (m *editorPageImport) attachment(id string) (string, error) {
if err != nil {
return "", err
}
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(m.ctx, data)
if err != nil {
return "", err
}
@@ -1261,7 +1343,7 @@ func (m *editorPageImport) signatureID(source, kind, id string) (string, error)
}
if doc.Signatures != "" {
name := m.target.ResPath(doc.Signatures)
data, err := m.target.readFile(name)
data, err := m.target.readFileViewContext(m.ctx, name)
if err != nil {
return "", err
}
@@ -1272,7 +1354,7 @@ func (m *editorPageImport) signatureID(source, kind, id string) (string, error)
sequence.observe(list.MaxSignID)
for _, ref := range list.List {
sequence.observe(ref.ID)
data, err := m.target.readFile(signatureRefPath(name, ref.BaseLoc))
data, err := m.target.readFileViewContext(m.ctx, signatureRefPath(name, ref.BaseLoc))
if err != nil {
return "", err
}
@@ -1306,7 +1388,7 @@ func (m *editorPageImport) signatures() ([]byte, error) {
if err != nil {
return nil, err
}
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(m.ctx, data)
if err != nil {
return nil, err
}
@@ -1347,6 +1429,9 @@ func (m *editorPageImport) signatures() ([]byte, error) {
// 入参: base 目标基底, refs 插入页面, annotations 注释索引, signatures 签名索引
// 返回: *editorSource 新基底, error 错误信息
func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, signatures []byte) (*editorSource, error) {
if err := m.ctx.Err(); err != nil {
return nil, err
}
files := maps.Clone(base.reader.files)
if files == nil {
files = make(map[string][]byte)
@@ -1356,11 +1441,11 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
if file, ok := base.reader.packageFile(name); ok {
name = cleanPackagePath(file.Name)
}
data, err := base.reader.readFile(name)
data, err := base.reader.readFileViewContext(m.ctx, name)
if err != nil {
return nil, err
}
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(m.ctx, data)
if err != nil {
return nil, err
}
@@ -1415,12 +1500,15 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
}
data = editorPatchXML(data, patches)
if len(annotations) != 0 {
loc, err := mergeImportIndex(base.reader, files, base.document.Annotations, path.Join(m.directory, "Annotations.xml"), "Annotations", annotations)
loc, err := mergeImportIndex(m.ctx, base.reader, files, base.document.Annotations, path.Join(m.directory, "Annotations.xml"), "Annotations", annotations)
if err != nil {
return nil, err
}
if base.document.Annotations == "" {
root, _ = parseEditorXML(data)
root, err = parseEditorXMLContext(m.ctx, data)
if err != nil {
return nil, err
}
data, err = editorXMLSetText(data, root, [][2]string{{"Annotations", loc}})
if err != nil {
return nil, err
@@ -1432,7 +1520,10 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
for _, id := range slices.Sorted(maps.Keys(m.attachments)) {
entries = append(entries, m.attachments[id]...)
}
root, _ = parseEditorXML(data)
root, err = parseEditorXMLContext(m.ctx, data)
if err != nil {
return nil, err
}
node := root.child("Attachments")
if node != nil && len(node.children) != 0 {
entries, err = editorXMLGenerated(entries, node.name.Space)
@@ -1442,7 +1533,7 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
content := append(bytes.Clone(data[node.open:node.close]), entries...)
data = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, node, content)})
} else {
loc, err := mergeImportIndex(base.reader, files, base.document.Attachments.Path, path.Join(m.directory, "Attachments.xml"), "Attachments", entries)
loc, err := mergeImportIndex(m.ctx, base.reader, files, base.document.Attachments.Path, path.Join(m.directory, "Attachments.xml"), "Attachments", entries)
if err != nil {
return nil, err
}
@@ -1459,13 +1550,13 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
if base.reader.OFD.DocBody[base.reader.documentIndex].versioned {
return nil, fmt.Errorf("signature import into a versioned document is not supported")
}
loc, err := mergeImportIndex(base.reader, files, base.document.Signatures, path.Join(m.directory, "Signs", "Signatures.xml"), "Signatures", signatures)
loc, err := mergeImportIndex(m.ctx, base.reader, files, base.document.Signatures, path.Join(m.directory, "Signs", "Signatures.xml"), "Signatures", signatures)
if err != nil {
return nil, err
}
indexName := cleanPackagePath(loc)
indexData := files[indexName]
indexRoot, err := parseEditorXML(indexData)
indexRoot, err := parseEditorXMLContext(m.ctx, indexData)
if err != nil {
return nil, err
}
@@ -1477,11 +1568,11 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
}
files[indexName] = editorPatchXML(indexData, []editorXMLPatch{patch})
if base.document.Signatures == "" {
ofd, err := base.reader.readFile("OFD.xml")
ofd, err := base.reader.readFileViewContext(m.ctx, "OFD.xml")
if err != nil {
return nil, err
}
root, err := parseEditorXML(ofd)
root, err := parseEditorXMLContext(m.ctx, ofd)
if err != nil {
return nil, err
}
@@ -1495,7 +1586,7 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
}
}
}
parts, err := base.reader.versionChanges(context.Background(), files)
parts, err := base.reader.versionChanges(m.ctx, files)
if err != nil {
return nil, err
}
@@ -1528,9 +1619,9 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
}
// mergeImportIndex 向原索引追加条目,不移动原条目的相对路径基准
// 入参: reader 目标输入包, files 新条目, loc 原索引, fallback 新索引路径, rootName 根节点名称, entries 新条目
// 入参: ctx 取消上下文, reader 目标输入包, files 新条目, loc 原索引, fallback 新索引路径, rootName 根节点名称, entries 新条目
// 返回: string 索引绝对路径, error 错误信息
func mergeImportIndex(reader *Reader, files map[string][]byte, loc, fallback, rootName string, entries []byte) (string, error) {
func mergeImportIndex(ctx context.Context, reader *Reader, files map[string][]byte, loc, fallback, rootName string, entries []byte) (string, error) {
name := fallback
var data []byte
var err error
@@ -1545,12 +1636,12 @@ func mergeImportIndex(reader *Reader, files map[string][]byte, loc, fallback, ro
var exists bool
data, exists = files[name]
if !exists {
data, err = reader.readFile(name)
data, err = reader.readFileViewContext(ctx, name)
}
if err != nil {
return "", err
}
root, parseErr := parseEditorXML(data)
root, parseErr := parseEditorXMLContext(ctx, data)
if parseErr != nil {
return "", parseErr
}
+8 -1
View File
@@ -912,7 +912,14 @@ func (e *Editor) editorImage(id string) (image.Point, error) {
return image.Point{}, err
}
defer input.Close()
config, _, err := image.DecodeConfig(input)
config, _, err := decodeImageConfig(input)
if errors.Is(err, errTIFFRandomAccess) {
data, readErr := reader.readFileView(reader.ResPath(name))
if readErr != nil {
return image.Point{}, readErr
}
config, _, err = decodeImageConfigData(data)
}
if err != nil {
return image.Point{}, err
}
+10 -11
View File
@@ -29,14 +29,14 @@ import (
)
// sourceParts 生成编辑快照的XML和新增资源,删页时同步清理标准引用
// 入参: progress 保存进度回调,预览时为nil
// 入参: ctx 取消上下文, progress 保存进度回调,预览时为nil
// 返回: map[string][]byte 替换及新增条目, error 错误信息
func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error) {
func (e *Editor) sourceParts(ctx context.Context, progress editorProgress) (map[string][]byte, error) {
parts, err := e.sourcePartsReferences(progress, nil)
if err != nil {
return nil, err
}
return e.source.reader.versionChanges(context.Background(), parts)
return e.source.reader.versionChanges(ctx, parts)
}
// sourcePartsReferences 生成保存快照,同步收集完全自产页面的实际编码引用
@@ -119,18 +119,17 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
if err := progress.report("pages", len(e.pages), len(e.pages)); err != nil {
return nil, err
}
fonts, images, spaces, resourceFiles := e.usedResources()
fonts, images, spaces, usedFiles := e.usedResources()
for _, resource := range e.resources {
if resource.definition() != "" && slices.Contains(resourceFiles, resource.name) || resource.colorProfileUsed(spaces) {
if resource.definition() != "" && usedFiles[resource.name] || resource.colorProfileUsed(spaces) {
parts[resource.name] = resource.data
}
}
commonData := source.document.CommonData
for _, name := range append(slices.Clone(commonData.DocumentRes), commonData.PublicRes...) {
resourceFiles = slices.DeleteFunc(resourceFiles, func(file string) bool {
return file == reader.ResPath(name)
})
delete(usedFiles, reader.ResPath(name))
}
resourceFiles := slices.Sorted(maps.Keys(usedFiles))
if len(fonts)+len(images)+len(spaces) != 0 {
data, err := e.encodeXML(func(x *ofdXML) { x.resources(fonts, images, spaces) })
if err != nil {
@@ -720,10 +719,10 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress
}
// sourceReader 生成原包与修改条目组成的独立预览快照,不进行ZIP压缩
// 入参: progress 准备进度回调
// 入参: ctx 取消上下文, progress 准备进度回调
// 返回: *Reader 阅读器, error 错误信息
func (e *Editor) sourceReader(progress editorProgress) (*Reader, error) {
parts, err := e.sourceParts(progress)
func (e *Editor) sourceReader(ctx context.Context, progress editorProgress) (*Reader, error) {
parts, err := e.sourceParts(ctx, progress)
if err != nil {
return nil, err
}
+1 -3
View File
@@ -193,8 +193,6 @@ func (s *editorReferenceScan) accept(token xml.Token) bool {
if token.Name.Local != "TextObject" && token.Name.Local != "Text" {
usage.unsafe = true
}
} else if attr.Name.Local == "Substitution" {
s.refs.fontUsage(attr.Value).unsafe = true
}
}
s.stack = append(s.stack, token.Name.Local)
@@ -230,7 +228,7 @@ func (s *editorReferenceScan) accept(token xml.Token) bool {
s.objects[id] = true
}
}
if (token.Name.Local == "Substitution" || token.Name.Local == "Font") && strings.TrimSpace(s.text.String()) != "" {
if token.Name.Local == "Font" && strings.TrimSpace(s.text.String()) != "" {
s.refs.fontUsage(s.text.String()).unsafe = true
}
if usage := s.fonts[len(s.fonts)-1]; usage != nil {
+103
View File
@@ -0,0 +1,103 @@
// 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 (
"bytes"
"context"
"encoding/xml"
"io"
)
// editorResourceElement 返回标准资源集合中的声明元素名称
// 入参: group 资源集合名称
// 返回: string 声明元素名称,未知集合返回空值
func editorResourceElement(group string) string {
switch group {
case "Fonts":
return "Font"
case "MultiMedias":
return "MultiMedia"
case "ColorSpaces":
return "ColorSpace"
case "DrawParams":
return "DrawParam"
case "CompositeGraphicUnits":
return "CompositeGraphicUnit"
}
return ""
}
// editorResourceDefinitions 单次流式读取资源声明依赖,保留历史索引和非图像媒体
// 入参: ctx 取消上下文, data 资源索引内容, name 文件路径, source 扫描结果, preserved 是否保留全部声明
// 返回: *editorResourceRefs 含声明依赖的引用,无法安全分析时为空, error 解析或取消错误
func editorResourceDefinitions(ctx context.Context, data []byte, name string, source *editorResourceRefs, preserved bool) (*editorResourceRefs, error) {
result := *source
result.ids, result.fonts = make(map[string]bool), nil
result.definitions = make(map[string]*editorResourceRefs)
outer := &editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
scan := editorReferenceScan{refs: outer, name: name, safe: true}
decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
for {
if err := ctx.Err(); err != nil {
return nil, err
}
token, err := decoder.Token()
if err == io.EOF {
if !scan.seen || len(scan.stack) != 0 {
return nil, nil
}
return &result, ctx.Err()
}
if err != nil {
return nil, err
}
depth := len(scan.stack)
if node, ok := token.(xml.StartElement); ok {
if depth == 0 && (scan.seen || node.Name.Local != "Res") || depth == 1 && editorResourceElement(node.Name.Local) == "" {
return nil, nil
}
if depth == 2 {
kind := editorResourceElement(scan.stack[1])
var id, mediaType string
for _, attr := range node.Attr {
if attr.Name.Space == "" {
switch attr.Name.Local {
case "ID":
id = editorResourceID(attr.Value)
case "Type":
mediaType = attr.Value
}
}
}
if node.Name.Local != kind || id == "" || result.definitions[id] != nil {
return nil, nil
}
refs := &editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
result.definitions[id] = refs
scan.refs = refs
if preserved || kind == "MultiMedia" && mediaType != "Image" {
result.ids[id] = true
}
}
}
if !scan.accept(token) {
return nil, ctx.Err()
}
if _, end := token.(xml.EndElement); end && depth == 3 {
scan.refs = outer
}
}
}
+24
View File
@@ -35,6 +35,7 @@ func resourceReferenceScopes(ctx context.Context, documents map[string]bool, res
scope := map[string]bool{entry: true}
queue := []string{entry}
refs := &editorResourceRefs{ids: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
definitions := make(map[string]*editorResourceRefs)
for position := 0; position < len(queue); position++ {
if err := ctx.Err(); err != nil {
return nil, err
@@ -47,6 +48,12 @@ func resourceReferenceScopes(ctx context.Context, documents map[string]bool, res
used[name] = true
if selected == "" || selected == entry {
refs.merge(&editorResourceRefs{ids: source.ids, fonts: source.fonts})
for id, definition := range source.definitions {
if definitions[id] != nil {
return nil, nil
}
definitions[id] = definition
}
}
for target, role := range source.roles {
if role&packageReferenceXML != 0 && !scope[target] {
@@ -58,6 +65,23 @@ func resourceReferenceScopes(ctx context.Context, documents map[string]bool, res
if selected != "" && selected != entry {
continue
}
var pending []string
if len(definitions) != 0 {
pending = slices.Collect(maps.Keys(refs.ids))
}
for position := 0; position < len(pending); position++ {
if err := ctx.Err(); err != nil {
return nil, err
}
if definition := definitions[pending[position]]; definition != nil {
for id := range definition.ids {
if !refs.ids[id] {
pending = append(pending, id)
}
}
refs.merge(&editorResourceRefs{ids: definition.ids, fonts: definition.fonts})
}
}
for name := range scope {
if err := ctx.Err(); err != nil {
return nil, err
+110 -39
View File
@@ -35,14 +35,15 @@ const (
editorReferenceFile
)
// editorResourceRefs 保存全包资源标识和直接文件引用,不以页面是否加载判断资源存活
// editorResourceRefs 记录资源标识、文件引用及声明依赖,不以页面是否加载判断资源存活
type editorResourceRefs struct {
ids map[string]bool
files map[string]bool
fonts map[string]*editorFontUsage
versions map[string]bool
documents map[string]bool
roles map[string]int
ids map[string]bool
files map[string]bool
fonts map[string]*editorFontUsage
versions map[string]bool
documents map[string]bool
roles map[string]int
definitions map[string]*editorResourceRefs
}
// compactSourceReferences 合并自产页面引用,版本文档按当前版本范围扫描
@@ -51,7 +52,15 @@ type editorResourceRefs struct {
// 入参: parts 输出条目, progress 保存进度回调, generated 自产页面及引用
// 返回: map[string]bool 可移除条目, error 读取或取消错误
func (e *Editor) compactSourceReferences(parts map[string][]byte, progress editorProgress, generated map[string]editorGeneratedReferences) (map[string]bool, error) {
multiple := e.source.reader.DocumentCount() > 1
return e.compactResourceReferences(parts, e.source.reader, progress, generated, e.source.retiredAttachments)
}
// compactResourceReferences 按文档引用清理输出副本,兼顾新建文档与已有包
// 入参: parts 输出条目, reader 原包,可为空, progress 保存进度回调, generated 自产引用, retired 待移除附件
// 返回: map[string]bool 可移除条目, error 读取或取消错误
func (e *Editor) compactResourceReferences(parts map[string][]byte, reader *Reader, progress editorProgress, generated map[string]editorGeneratedReferences, retired map[string]bool) (map[string]bool, error) {
multiple := reader != nil && reader.DocumentCount() > 1
prune := e.output != nil && e.output.options.Mode != CompressionUnchanged
if e.output != nil && e.output.protected || multiple && (e.output == nil || e.output.options.Mode == CompressionUnchanged) {
return nil, nil
}
@@ -59,7 +68,6 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
return nil, nil
}
generated = e.generatedVectorReferences(parts, generated)
reader := e.source.reader
ctx := context.Background()
if e.output != nil {
ctx = e.output.ctx
@@ -69,11 +77,13 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
return nil, err
}
names := make(map[string]bool)
for name := range reader.fileIndex {
names[name] = true
}
for name := range reader.files {
names[name] = true
if reader != nil {
for name := range reader.fileIndex {
names[name] = true
}
for name := range reader.files {
names[name] = true
}
}
for name := range parts {
names[name] = true
@@ -85,7 +95,7 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
}
}
}
refs, resources, references, safe, err := scanEditorResources(ctx, reader, parts, generated, names, progress, multiple)
refs, resources, references, safe, err := scanEditorResources(ctx, reader, parts, generated, names, progress, multiple || prune)
if err != nil {
return nil, err
}
@@ -93,9 +103,25 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
return nil, nil
}
var scoped map[string]*editorResourceRefs
if multiple {
if prune {
for _, name := range resources {
data, ok := parts[name]
if !ok {
data, err = reader.readFile(name)
if err != nil {
return nil, err
}
}
indexed, err := editorResourceDefinitions(ctx, data, name, references[name], refs.versions[cleanPackagePath(name)])
if err != nil || indexed == nil {
return nil, err
}
references[name] = indexed
}
}
if multiple || prune {
selected := ""
if reader.OFD.DocBody[reader.documentIndex].versioned {
if reader != nil && reader.OFD.DocBody[reader.documentIndex].versioned {
selected, err = reader.DocumentRoot()
if err != nil {
return nil, err
@@ -107,11 +133,36 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
}
}
ordered := slices.Sorted(maps.Keys(names))
removed := maps.Clone(e.source.retiredAttachments)
removed := maps.Clone(retired)
if removed == nil {
removed = make(map[string]bool)
}
usedFiles := maps.Clone(refs.files)
var usedFiles map[string]bool
var protectedProfiles map[string]bool
var profiles map[string][]editorProfileReference
if prune {
usedFiles = make(map[string]bool)
protectedProfiles = make(map[string]bool)
profiles = make(map[string][]editorProfileReference)
for name, source := range references {
if source.definitions == nil {
maps.Copy(usedFiles, source.files)
maps.Copy(protectedProfiles, source.files)
continue
}
usage := scoped[name]
for id, definition := range source.definitions {
if refs.versions[cleanPackagePath(name)] || usage == nil || usage.ids[id] {
maps.Copy(usedFiles, definition.files)
if refs.versions[cleanPackagePath(name)] || usage == nil {
maps.Copy(protectedProfiles, definition.files)
}
}
}
}
} else {
usedFiles = maps.Clone(refs.files)
}
updates := make(map[string][]byte)
fontFiles := make(map[string]*editorFontUsage)
for i, name := range resources {
@@ -131,7 +182,7 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
return nil, nil
}
usageRefs := refs
if multiple {
if multiple || prune {
usageRefs = scoped[name]
}
preserved := refs.versions[cleanPackagePath(name)] || usageRefs == nil
@@ -145,35 +196,52 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
} else if group.name.Local == "MultiMedias" && node.name.Local == "MultiMedia" {
field = "MediaFile"
}
if field == "" {
if field == "" && (!prune || node.name.Local != editorResourceElement(group.name.Local)) {
continue
}
id := editorResourceID(node.attr("ID"))
keep := preserved || id == "" || usageRefs.ids[id] || field == "MediaFile" && node.attr("Type") != "Image"
profile := prune && group.name.Local == "ColorSpaces" && node.name.Local == "ColorSpace"
if prune && keep && !profile {
maps.Copy(protectedProfiles, references[name].definitions[id].files)
}
var value string
if file := node.child(field); file != nil {
var value string
if err := xml.Unmarshal(data[file.start:file.end], &value); err != nil {
return nil, nil
}
if location := editorResourceLocation(name, root.attr("BaseLoc"), value); location != "" {
alternate := cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value))
if field == "FontFile" && !multiple {
usage := usageRefs.fonts[id]
charset := node.attr("Charset")
unsafe := preserved || id == "" || charset != "" && !strings.EqualFold(charset, "unicode") || location != alternate
for _, file := range []string{location, alternate} {
if fontFiles[file] == nil {
fontFiles[file] = newEditorFontUsage()
}
fontFiles[file].merge(usage, unsafe || keep && usage == nil)
} else if profile {
value = node.attr("Profile")
}
if location := editorResourceLocation(name, root.attr("BaseLoc"), value); location != "" {
alternate := cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value))
if field == "FontFile" && !multiple && keep {
var usage *editorFontUsage
if usageRefs != nil {
usage = usageRefs.fonts[id]
}
charset := node.attr("Charset")
unsafe := preserved || id == "" || charset != "" && !strings.EqualFold(charset, "unicode") || location != alternate
for _, file := range []string{location, alternate} {
if fontFiles[file] == nil {
fontFiles[file] = newEditorFontUsage()
}
fontFiles[file].merge(usage, unsafe || keep && usage == nil)
}
}
if keep {
usedFiles[location] = true
usedFiles[alternate] = true
if prune {
if profile && !preserved && location == alternate {
profiles[location] = append(profiles[location], editorProfileReference{index: name, id: id})
} else {
protectedProfiles[location] = true
protectedProfiles[alternate] = true
}
}
if keep {
usedFiles[location] = true
usedFiles[alternate] = true
} else {
removed[location] = true
}
} else {
removed[location] = true
}
}
if !keep {
@@ -226,6 +294,9 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
delete(parts, name)
}
}
if err := compactResourceProfiles(ctx, reader, parts, profiles, protectedProfiles, removed); err != nil {
return nil, err
}
return removed, nil
}
+83 -23
View File
@@ -78,14 +78,25 @@ func (e *Editor) SelectVersion(id string) error {
// 入参: name 版本名称,可为空
// 返回: string 新版本标识, error 创建错误
func (e *Editor) AddVersion(name string) (string, error) {
return e.AddVersionContext(context.Background(), name)
}
// AddVersionContext 以当前内容创建并选中默认版本,取消或失败时不改变文档和撤销历史
// 初始共享文件,后续修改按需隔离,不自动写入创建日期
// 入参: ctx 取消上下文, name 版本名称,可为空
// 返回: string 新版本标识, error 创建或取消错误
func (e *Editor) AddVersionContext(ctx context.Context, name string) (string, error) {
var id string
err := e.editDocuments(func(reader *Reader, data []byte, root *editorXML) (int, error) {
versions, err := reader.documentVersionEntries()
err := e.editDocumentsContext(ctx, func(reader *Reader, data []byte, root *editorXML) (int, error) {
versions, err := reader.documentVersionEntriesContext(ctx)
if err != nil {
return 0, err
}
index := int32(0)
for _, version := range versions {
if err := ctx.Err(); err != nil {
return 0, err
}
index = max(index, version.Index)
}
if index == math.MaxInt32 {
@@ -93,15 +104,26 @@ func (e *Editor) AddVersion(name string) (string, error) {
}
index++
ids := make(map[string]bool)
var collect func(*editorXML)
collect = func(node *editorXML) {
var collect func(*editorXML) error
collect = func(node *editorXML) error {
if err := ctx.Err(); err != nil {
return err
}
ids[node.attr("ID")] = true
for _, child := range node.children {
collect(child)
if err := collect(child); err != nil {
return err
}
}
return nil
}
if err := collect(root); err != nil {
return 0, err
}
collect(root)
for serial := int64(index); ; serial++ {
if err := ctx.Err(); err != nil {
return 0, err
}
id = "v" + strconv.FormatInt(serial, 10)
if !ids[id] {
break
@@ -111,7 +133,7 @@ func (e *Editor) AddVersion(name string) (string, error) {
if err != nil {
return 0, err
}
files, err := reader.versionSnapshotFiles(context.Background())
files, err := reader.versionSnapshotFiles(ctx)
if err != nil {
return 0, err
}
@@ -130,6 +152,10 @@ func (e *Editor) AddVersion(name string) (string, error) {
x.root("DocVersion", attrs)
x.start("FileList", nil)
for serial, file := range slices.Sorted(maps.Keys(files)) {
if err := ctx.Err(); err != nil {
x.err = err
return
}
x.start("File", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: "file" + strconv.Itoa(serial+1)}})
x.token(xml.CharData("/" + file))
x.end("File")
@@ -158,7 +184,7 @@ func (e *Editor) AddVersion(name string) (string, error) {
if err != nil {
return 0, err
}
root, err = parseEditorXML(data)
root, err = parseEditorXMLContext(ctx, data)
if err != nil {
return 0, err
}
@@ -262,7 +288,7 @@ func (e *Editor) RenameVersion(id, name string) error {
}
}
reader.files[location] = updated
if err := reader.retireVersionFiles(map[string]bool{version.Location: true}); err != nil {
if err := reader.retireVersionFiles(context.Background(), map[string]bool{version.Location: true}); err != nil {
return 0, err
}
return reader.documentIndex, nil
@@ -274,8 +300,16 @@ func (e *Editor) RenameVersion(id, name string) error {
// 入参: id 版本标识,不能为空
// 返回: error 删除错误
func (e *Editor) DeleteVersion(id string) error {
return e.editDocuments(func(reader *Reader, data []byte, root *editorXML) (int, error) {
versions, err := reader.DocumentVersions()
return e.DeleteVersionContext(context.Background(), id)
}
// DeleteVersionContext 删除版本及其独占文件,取消或失败时保留当前状态和撤销历史
// 文档主入口及其他版本仍在使用的文件不删除
// 入参: ctx 取消上下文, id 版本标识,不能为空
// 返回: error 删除或取消错误
func (e *Editor) DeleteVersionContext(ctx context.Context, id string) error {
return e.editDocumentsContext(ctx, func(reader *Reader, data []byte, root *editorXML) (int, error) {
versions, err := reader.DocumentVersionsContext(ctx)
if err != nil {
return 0, err
}
@@ -289,6 +323,9 @@ func (e *Editor) DeleteVersion(id string) error {
}
list := root.childAt("DocBody", reader.documentIndex).child("Versions")
for _, node := range list.children {
if err := ctx.Err(); err != nil {
return 0, err
}
if node.matchesOFD("Version") && node.attr("ID") == id {
if len(versions) == 1 {
node = list
@@ -302,9 +339,12 @@ func (e *Editor) DeleteVersion(id string) error {
}
removed := map[string]bool{versions[index].Location: true}
for _, file := range versions[index].Files {
if err := ctx.Err(); err != nil {
return 0, err
}
removed[file.Location] = true
}
if err := reader.retireVersionFiles(removed); err != nil {
if err := reader.retireVersionFiles(ctx, removed); err != nil {
return 0, err
}
return reader.documentIndex, nil
@@ -341,6 +381,9 @@ func editorVersionDefault(data []byte, body *editorXML, id string) ([]byte, erro
// 入参: ctx 取消上下文
// 返回: map[string]bool 快照文件, error 引用或读取错误
func (r *Reader) versionSnapshotFiles(ctx context.Context) (map[string]bool, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
entry, err := r.DocumentRoot()
if err != nil {
return nil, err
@@ -351,15 +394,18 @@ func (r *Reader) versionSnapshotFiles(ctx context.Context) (map[string]bool, err
}
if r.versionInfo != nil {
for _, file := range r.versionInfo.Files {
if err := ctx.Err(); err != nil {
return nil, err
}
files[file.Location] = true
}
}
body := r.OFD.DocBody[r.documentIndex]
data, err := r.readFile("OFD.xml")
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
}
@@ -380,6 +426,9 @@ func (r *Reader) versionSnapshotFiles(ctx context.Context) (map[string]bool, err
maps.Copy(files, signed)
}
for name := range files {
if err := ctx.Err(); err != nil {
return nil, err
}
if !r.fileNames[name] {
return nil, fmt.Errorf("version file not found: %s", name)
}
@@ -388,24 +437,35 @@ func (r *Reader) versionSnapshotFiles(ctx context.Context) (map[string]bool, err
}
// retireVersionFiles 清理修改入口后不再被任何文档或版本引用的候选文件
// 入参: candidates 待清理路径,OFD.xml始终保留
// 返回: error 依赖读取错误
func (r *Reader) retireVersionFiles(candidates map[string]bool) error {
// 入参: ctx 取消上下文, candidates 待清理路径,OFD.xml始终保留
// 返回: error 依赖读取或取消错误
func (r *Reader) retireVersionFiles(ctx context.Context, candidates map[string]bool) error {
if err := ctx.Err(); err != nil {
return err
}
view := *r
if err := view.initRoot(); err != nil {
return err
}
for index := range view.DocumentCount() {
files, err := view.documentFiles(context.Background(), index)
if err != nil {
indexes := make([]int, view.DocumentCount())
for index := range indexes {
indexes[index] = index
}
files, err := view.documentFiles(ctx, indexes...)
if err != nil {
return err
}
for name := range files {
if err := ctx.Err(); err != nil {
return err
}
for name := range files {
delete(candidates, name)
}
delete(candidates, name)
}
delete(candidates, "OFD.xml")
for name := range candidates {
if err := ctx.Err(); err != nil {
return err
}
delete(r.files, name)
}
if r.Zip != nil {
+70 -20
View File
@@ -16,6 +16,7 @@ package ofdgo
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"image"
@@ -27,7 +28,7 @@ import (
_ "github.com/xiaoqidun/jbig2"
_ "golang.org/x/image/bmp"
"golang.org/x/image/draw"
_ "golang.org/x/image/tiff"
"golang.org/x/image/tiff"
)
// transparentEdgeImage 共享只读像素并按需计算透明边缘和采样边距
@@ -147,6 +148,14 @@ func decodeImageData(data []byte) (image.Image, string, error) {
}
return img, img.format, nil
}
if isTIFFData(data) {
layout, err := readTIFFLayout(bytes.NewReader(data))
if err != nil {
return nil, "tiff", err
}
img, err := decodeTIFFImage(nil, data, layout)
return img, "tiff", err
}
img, format, err := image.Decode(bytes.NewReader(data))
if err == nil {
return img, normalizeSealType(format), nil
@@ -175,6 +184,27 @@ func isPNGData(data []byte) bool {
// 入参: data 图片数据
// 返回: image.Config 图片尺寸, string 图片格式, error 错误信息
func decodeImageConfigData(data []byte) (image.Config, string, error) {
if isTIFFData(data) {
layout, err := readTIFFLayout(bytes.NewReader(data))
if err != nil {
return image.Config{}, "tiff", err
}
if layout.planar && layout.samples > 1 || layout.samples == 1 && (layout.depth == 2 || layout.depth == 4) {
img, _, err := tiffPlaneDirectories(nil, data, layout)
if err != nil {
return image.Config{}, "tiff", err
}
size := layout.size()
return image.Config{ColorModel: img.ColorModel(), Width: size.X, Height: size.Y}, "tiff", nil
}
cfg, err := tiff.DecodeConfig(newTIFFDirectoryInput(data, layout.offset))
if err != nil {
return image.Config{}, "tiff", err
}
size := layout.size()
cfg.Width, cfg.Height = size.X, size.Y
return cfg, "tiff", nil
}
cfg, format, err := image.DecodeConfig(bytes.NewReader(data))
if err == nil {
return cfg, normalizeSealType(format), nil
@@ -193,11 +223,11 @@ func decodeBMPConfig(data []byte) (image.Config, error) {
if err != nil {
return image.Config{}, err
}
if offset <= 0 || width <= 0 || height == 0 || compression != 0 {
if offset <= 0 || width <= 0 || height == 0 {
return image.Config{}, fmt.Errorf("unsupported bmp")
}
if bpp != 16 && bpp != 24 && bpp != 32 {
return image.Config{}, fmt.Errorf("unsupported bmp")
if _, err := bmpColorMasks(data, offset, bpp, compression); err != nil {
return image.Config{}, err
}
if height < 0 {
height = -height
@@ -209,15 +239,26 @@ func decodeBMPConfig(data []byte) (image.Config, error) {
// 入参: data 图片数据
// 返回: image.Image 图片对象, error 错误信息
func decodeBMPImage(data []byte) (image.Image, error) {
return decodeBMPImageContext(context.Background(), data)
}
// decodeBMPImageContext 解码BMP真彩色像素,逐行检查取消
// 入参: ctx 取消上下文, data 图片数据
// 返回: image.Image 图片对象, error 格式或取消错误
func decodeBMPImageContext(ctx context.Context, data []byte) (image.Image, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
offset, width, height, bpp, compression, err := parseBMPHeader(data)
if err != nil {
return nil, err
}
if compression != 0 {
return nil, fmt.Errorf("unsupported bmp compression")
if offset > len(data) {
return nil, fmt.Errorf("invalid bmp pixel offset")
}
if bpp != 16 && bpp != 24 && bpp != 32 {
return nil, fmt.Errorf("unsupported bmp depth")
masks, err := bmpColorMasks(data, offset, bpp, compression)
if err != nil {
return nil, err
}
topDown := height < 0
if topDown {
@@ -230,9 +271,15 @@ func decodeBMPImage(data []byte) (image.Image, error) {
if stride > int64(len(data)-offset)/int64(height) {
return nil, fmt.Errorf("truncated bmp")
}
if width > int(^uint(0)>>1)/4/height {
return nil, fmt.Errorf("bmp dimensions exceed image capacity")
}
rowStride := int(stride)
img := image.NewNRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
if err := ctx.Err(); err != nil {
return nil, err
}
srcY := y
if !topDown {
srcY = height - 1 - y
@@ -240,20 +287,20 @@ func decodeBMPImage(data []byte) (image.Image, error) {
row := data[offset+srcY*rowStride:]
for x := 0; x < width; x++ {
switch bpp {
case 16:
v := binary.LittleEndian.Uint16(row[x*2:])
img.SetNRGBA(x, y, color.NRGBA{
R: uint8(((v >> 10) & 0x1F) * 255 / 31),
G: uint8(((v >> 5) & 0x1F) * 255 / 31),
B: uint8((v & 0x1F) * 255 / 31),
A: 255,
})
case 16, 32:
var value uint32
if bpp == 16 {
value = uint32(binary.LittleEndian.Uint16(row[x*2:]))
} else {
value = binary.LittleEndian.Uint32(row[x*4:])
}
p := y*img.Stride + x*4
for i, mask := range masks {
img.Pix[p+i] = mask.sample(value)
}
case 24:
p := x * 3
img.SetNRGBA(x, y, color.NRGBA{R: row[p+2], G: row[p+1], B: row[p], A: 255})
case 32:
p := x * 4
img.SetNRGBA(x, y, color.NRGBA{R: row[p+2], G: row[p+1], B: row[p], A: 255})
}
}
}
@@ -272,7 +319,7 @@ func parseBMPHeader(data []byte) (int, int, int, uint16, uint32, error) {
if dibSize < 40 {
return 0, 0, 0, 0, 0, fmt.Errorf("unsupported bmp")
}
if uint64(dibSize)+14 > uint64(len(data)) || offset < 0 || uint64(offset) < uint64(dibSize)+14 || offset > len(data) {
if uint64(dibSize)+14 > uint64(len(data)) || offset < 0 || uint64(offset) < uint64(dibSize)+14 {
return 0, 0, 0, 0, 0, fmt.Errorf("invalid bmp pixel offset")
}
width := int(int32(binary.LittleEndian.Uint32(data[18:22])))
@@ -450,6 +497,9 @@ func imageNRGBA64At(img image.Image, x, y int) color.NRGBA64 {
switch src := img.(type) {
case interface{ NRGBA64At(int, int) color.NRGBA64 }:
return src.NRGBA64At(x, y)
case *image.NRGBA:
c := src.NRGBAAt(x, y)
return color.NRGBA64{R: uint16(c.R) * 257, G: uint16(c.G) * 257, B: uint16(c.B) * 257, A: uint16(c.A) * 257}
case *image.Gray16:
c := src.Gray16At(x, y)
return color.NRGBA64{R: c.Y, G: c.Y, B: c.Y, A: 0xffff}
+125
View File
@@ -0,0 +1,125 @@
// 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 (
"bufio"
"encoding/binary"
"fmt"
"image"
"io"
"math/bits"
)
// decodeImageConfig 读取图片尺寸,BMP和TIFF只读取布局信息,不解码像素
// 入参: input 图片输入流
// 返回: image.Config 图片配置, string 图片格式, error 头部错误
func decodeImageConfig(input io.Reader) (image.Config, string, error) {
buffer := bufio.NewReader(input)
prefix, _ := buffer.Peek(34)
if isTIFFData(prefix) {
layout, err := readTIFFLayout(buffer)
if err != nil {
return image.Config{}, "tiff", err
}
size := layout.size()
return image.Config{Width: size.X, Height: size.Y}, "tiff", nil
}
if len(prefix) == 34 && string(prefix[:2]) == "BM" {
header := binary.LittleEndian.Uint32(prefix[14:18])
bpp := binary.LittleEndian.Uint16(prefix[28:30])
compression := binary.LittleEndian.Uint32(prefix[30:34])
if (bpp == 16 || bpp == 32 && compression == 3) && (header == 40 || header == 52 || header == 56 || header == 108 || header == 124) {
size := int(header) + 14
if header == 40 && compression == 3 {
size += 12
}
data := make([]byte, size)
if _, err := io.ReadFull(buffer, data); err != nil {
return image.Config{}, "bmp", err
}
config, err := decodeBMPConfig(data)
return config, "bmp", err
}
}
return image.DecodeConfig(buffer)
}
// bmpColorMask 保存一个BMP颜色分量的位域位置和取值范围
type bmpColorMask struct {
mask uint32
shift uint
max uint32
}
// sample 将位域采样值映射到八位通道,缺省透明度为不透明
// 入参: value 编码像素
// 返回: uint8 通道值
func (m bmpColorMask) sample(value uint32) uint8 {
if m.mask == 0 {
return 255
}
return uint8(uint64((value&m.mask)>>m.shift) * 255 / uint64(m.max))
}
// bmpColorMasks 读取BMP真彩色通道布局,校验位域连续性、重叠及像素范围
// 入参: data 已校验头部的BMP数据, offset 像素偏移, bpp 像素位数, compression 编码方式
// 返回: [4]bmpColorMask 红绿蓝和透明度通道, error 格式错误
func bmpColorMasks(data []byte, offset int, bpp uint16, compression uint32) ([4]bmpColorMask, error) {
var result [4]bmpColorMask
if bpp != 16 && bpp != 24 && bpp != 32 {
return result, fmt.Errorf("unsupported bmp depth")
}
masks := [4]uint32{0xff0000, 0xff00, 0xff}
if bpp == 16 {
masks = [4]uint32{0x7c00, 0x3e0, 0x1f}
}
switch compression {
case 0:
case 3:
if bpp == 24 {
return result, fmt.Errorf("invalid bmp bitfield depth")
}
header := binary.LittleEndian.Uint32(data[14:18])
if offset < 66 || len(data) < 66 || header != 40 && header < 52 {
return result, fmt.Errorf("truncated bmp color masks")
}
for i := range 3 {
masks[i] = binary.LittleEndian.Uint32(data[54+i*4:])
}
if header >= 56 {
masks[3] = binary.LittleEndian.Uint32(data[66:70])
}
default:
return result, fmt.Errorf("unsupported bmp compression")
}
var used uint32
for i, mask := range masks {
if mask == 0 {
if i < 3 {
return result, fmt.Errorf("empty bmp color mask")
}
continue
}
shift := uint(bits.TrailingZeros32(mask))
maximum := mask >> shift
if mask&used != 0 || maximum&(maximum+1) != 0 || uint64(mask)>>bpp != 0 {
return result, fmt.Errorf("invalid bmp color mask")
}
result[i] = bmpColorMask{mask: mask, shift: shift, max: maximum}
used |= mask
}
return result, nil
}
+32 -5
View File
@@ -27,12 +27,28 @@ type imagePageInfo struct {
xdpi, ydpi float64
}
// imagePageMetadata 读取PNG物理尺寸与JPEG的JFIF、EXIF信息,EXIF密度优先于JFIF
// imagePageMetadata 读取图片方向与密度,TIFF方向由解码器处理,JPEG的EXIF密度优先于JFIF
// 入参: data 图片数据, format 图像格式, dpi 缺少物理密度时使用的DPI
// 返回: imagePageInfo 方向与横纵DPI, error 元数据错误
func imagePageMetadata(data []byte, format string, dpi float64) (imagePageInfo, error) {
info := imagePageInfo{orientation: 1, xdpi: dpi, ydpi: dpi}
var exif []byte
var directory uint32
if format == "tiff" {
layout, err := readTIFFLayout(bytes.NewReader(data))
if err != nil {
return info, err
}
directory = layout.offset
exif = data
}
if format == "bmp" && len(data) >= 54 && binary.LittleEndian.Uint32(data[14:18]) >= 40 {
x := int32(binary.LittleEndian.Uint32(data[38:42]))
y := int32(binary.LittleEndian.Uint32(data[42:46]))
if x > 0 && y > 0 {
info.xdpi, info.ydpi = float64(x)*0.0254, float64(y)*0.0254
}
}
if format == "png" {
for offset := 8; offset+12 <= len(data); {
n := uint64(binary.BigEndian.Uint32(data[offset:]))
@@ -109,22 +125,28 @@ func imagePageMetadata(data []byte, format string, dpi float64) (imagePageInfo,
}
}
if len(exif) != 0 {
orientation, x, y, err := imageExifMetadata(exif)
orientation, x, y, err := imageExifMetadata(exif, dpi, directory)
if err != nil {
return info, err
}
info.orientation = orientation
if format == "jpeg" && x > 0 && y > 0 {
if (format == "jpeg" || format == "tiff") && x > 0 && y > 0 {
info.xdpi, info.ydpi = x, y
}
if format == "tiff" {
if orientation >= 5 {
info.xdpi, info.ydpi = info.ydpi, info.xdpi
}
info.orientation = 1
}
}
return info, nil
}
// imageExifMetadata 只读取主图IFD中的方向和密度,不跟随缩略图与其他目录
// 入参: data EXIF中的TIFF数据
// 入参: data TIFF或EXIF数据, dpi 无绝对密度单位时的横向DPI,零值不换算, directory 目录偏移,零值使用首目录
// 返回: int EXIF方向值,未声明时为1, float64 横向DPI, float64 纵向DPI, error 元数据错误
func imageExifMetadata(data []byte) (int, float64, float64, error) {
func imageExifMetadata(data []byte, dpi float64, directory uint32) (int, float64, float64, error) {
invalid := fmt.Errorf("invalid EXIF image metadata")
if len(data) < 8 {
return 0, 0, 0, invalid
@@ -142,6 +164,9 @@ func imageExifMetadata(data []byte) (int, float64, float64, error) {
return 0, 0, 0, invalid
}
offset := uint64(order.Uint32(data[4:]))
if directory != 0 {
offset = uint64(directory)
}
if offset < 8 || offset+2 > uint64(len(data)) {
return 0, 0, 0, invalid
}
@@ -185,6 +210,8 @@ func imageExifMetadata(data []byte) (int, float64, float64, error) {
}
if unit == 3 {
x, y = x*2.54, y*2.54
} else if unit == 1 && dpi > 0 && x > 0 && y > 0 {
x, y = dpi, dpi*y/x
} else if unit != 2 {
x, y = 0, 0
}
+354
View File
@@ -0,0 +1,354 @@
// 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 (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"image"
"image/color"
"io"
)
// tiffImageLayout 保存单页TIFF的尺寸、方向、采样及压缩参数
type tiffImageLayout struct {
width, height int
orientation int
offset, next uint32
reduced, mask bool
planar bool
samples uint32
depth uint32
fillOrder uint32
compression uint32
t4, t6 uint32
}
// errTIFFRandomAccess 表示目录链需要回读,仅流式输入需重新打开资源
var errTIFFRandomAccess = errors.New("TIFF directory requires random access")
// tiffDirectoryInput 只替换解码视图中的首目录偏移,不复制或改写原始资源
type tiffDirectoryInput struct {
reader *bytes.Reader
directory [4]byte
ctx context.Context
}
// orientedTIFFImage 按方向映射像素坐标,复用原图数据与颜色精度
type orientedTIFFImage struct {
source image.Image
tiffImageLayout
}
// isTIFFData 识别标准TIFF字节序与版本标识
// 入参: data 文件头或完整编码
// 返回: bool 是否为TIFF 6.0编码
func isTIFFData(data []byte) bool {
return len(data) >= 4 && (string(data[:4]) == "II\x2a\x00" || string(data[:4]) == "MM\x00\x2a")
}
// readTIFFLayout 选择单页TIFF的主图,允许缩略图目录并拒绝循环和多个主图
// 入参: input 从文件头开始的输入流
// 返回: tiffImageLayout 存储尺寸与方向, error 文件结构或方向错误
func readTIFFLayout(input io.Reader) (tiffImageLayout, error) {
var result tiffImageLayout
var header [8]byte
if _, err := io.ReadFull(input, header[:]); err != nil {
return result, err
}
if !isTIFFData(header[:]) {
return result, fmt.Errorf("invalid TIFF header")
}
var order binary.ByteOrder = binary.LittleEndian
if header[0] == 'M' {
order = binary.BigEndian
}
offset := int64(order.Uint32(header[4:]))
position := int64(8)
var seen map[uint32]bool
for offset != 0 {
if seen != nil && seen[uint32(offset)] {
return result, fmt.Errorf("cyclic TIFF directory chain")
}
layout, end, err := readTIFFDirectory(input, order, offset, position)
if err != nil {
return result, err
}
if layout.mask {
return result, fmt.Errorf("TIFF transparency mask directories are not supported")
}
if !layout.reduced || layout.next == 0 && seen == nil {
if result.offset != 0 {
return result, fmt.Errorf("multiple TIFF pages are not supported in OFD resources")
}
result = layout
}
if layout.next != 0 {
if seen == nil {
seen = make(map[uint32]bool)
}
seen[uint32(offset)] = true
}
offset, position = int64(layout.next), end
}
if result.offset == 0 {
return result, fmt.Errorf("missing TIFF main image")
}
return result, nil
}
// readTIFFDirectory 读取目录中的图像布局和子文件类型,不读取像素
// 入参: input 输入流, order 字节序, offset 目录偏移, position 当前流位置
// 返回: tiffImageLayout 目录布局, int64 读取后的流位置, error 结构或读取错误
func readTIFFDirectory(input io.Reader, order binary.ByteOrder, offset, position int64) (tiffImageLayout, int64, error) {
var result tiffImageLayout
var header [4]byte
if offset < 8 {
return result, position, fmt.Errorf("invalid TIFF directory offset")
}
if seeker, ok := input.(io.Seeker); ok {
if _, err := seeker.Seek(offset, io.SeekStart); err != nil {
return result, position, err
}
} else {
if offset < position {
return result, position, errTIFFRandomAccess
}
if _, err := io.CopyN(io.Discard, input, offset-position); err != nil {
return result, position, err
}
}
if _, err := io.ReadFull(input, header[:2]); err != nil {
return result, position, err
}
count := int(order.Uint16(header[:2]))
result.orientation, result.offset, result.samples, result.fillOrder = 1, uint32(offset), 1, 1
result.depth = 1
result.compression = 1
var entry [12]byte
var previous uint16
var subfile, legacy uint32
var hasSubfile bool
for i := range count {
if _, err := io.ReadFull(input, entry[:]); err != nil {
return result, position, err
}
tag, kind, number := order.Uint16(entry[:2]), order.Uint16(entry[2:4]), order.Uint32(entry[4:8])
if i > 0 && tag <= previous {
return result, position, fmt.Errorf("invalid TIFF directory order")
}
previous = tag
if tag == 258 && kind == 3 && number == 1 {
result.depth = uint32(order.Uint16(entry[8:10]))
}
if tag != 254 && tag != 255 && tag != 256 && tag != 257 && tag != 259 && tag != 262 && tag != 266 && tag != 274 && tag != 277 && tag != 284 && tag != 292 && tag != 293 {
continue
}
if number != 1 || kind != 3 && kind != 4 || (tag == 255 || tag == 259 || tag == 262 || tag == 266 || tag == 274 || tag == 277 || tag == 284) && kind != 3 || (tag == 254 || tag == 292 || tag == 293) && kind != 4 {
return result, position, fmt.Errorf("invalid TIFF image layout")
}
value := uint64(order.Uint32(entry[8:12]))
if kind == 3 {
value = uint64(order.Uint16(entry[8:10]))
}
if (tag == 256 || tag == 257) && (value == 0 || value > uint64(int(^uint(0)>>1))) {
return result, position, fmt.Errorf("invalid TIFF dimensions")
}
switch tag {
case 254:
subfile, hasSubfile = uint32(value), true
case 255:
legacy = uint32(value)
case 256:
result.width = int(value)
case 257:
result.height = int(value)
case 259:
result.compression = uint32(value)
case 262:
result.mask = value == 4
case 266:
if value != 1 && value != 2 {
return result, position, fmt.Errorf("invalid TIFF fill order")
}
result.fillOrder = uint32(value)
case 274:
if value < 1 || value > 8 {
return result, position, fmt.Errorf("invalid TIFF orientation")
}
result.orientation = int(value)
case 277:
if value == 0 {
return result, position, fmt.Errorf("invalid TIFF sample count")
}
result.samples = uint32(value)
case 284:
if value != 1 && value != 2 {
return result, position, fmt.Errorf("invalid TIFF planar configuration")
}
result.planar = value == 2
case 292:
result.t4 = uint32(value)
case 293:
result.t6 = uint32(value)
}
}
if _, err := io.ReadFull(input, header[:4]); err != nil {
return result, position, err
}
result.next = order.Uint32(header[:4])
if hasSubfile {
result.reduced, result.mask = subfile&1 != 0, result.mask || subfile&4 != 0
} else {
result.reduced = legacy == 2
}
if result.width == 0 || result.height == 0 {
return result, position, fmt.Errorf("missing TIFF dimensions")
}
return result, offset + 6 + int64(count)*12, nil
}
// newTIFFDirectoryInput 创建以指定目录作为主图的只读解码视图
// 入参: data 已校验的TIFF编码, offset 主图目录偏移
// 返回: *tiffDirectoryInput 随机访问输入
func newTIFFDirectoryInput(data []byte, offset uint32) *tiffDirectoryInput {
input := &tiffDirectoryInput{reader: bytes.NewReader(data)}
var order binary.ByteOrder = binary.LittleEndian
if data[0] == 'M' {
order = binary.BigEndian
}
order.PutUint32(input.directory[:], offset)
return input
}
// patch 替换读取片段中与首目录指针重叠的部分
// 入参: data 读取结果, offset 文件偏移
func (r *tiffDirectoryInput) patch(data []byte, offset int64) {
for i := range r.directory {
at := int64(4+i) - offset
if at >= 0 && at < int64(len(data)) {
data[at] = r.directory[i]
}
}
}
// Read 读取图像编码并覆盖首目录指针
// 入参: data 接收缓冲
// 返回: int 读取字节数, error 读取错误
func (r *tiffDirectoryInput) Read(data []byte) (int, error) {
if r.ctx != nil {
if err := r.ctx.Err(); err != nil {
return 0, err
}
}
offset := r.reader.Size() - int64(r.reader.Len())
n, err := r.reader.Read(data)
r.patch(data[:n], offset)
return n, err
}
// ReadAt 随机读取图像编码并覆盖首目录指针
// 入参: data 接收缓冲, offset 文件偏移
// 返回: int 读取字节数, error 读取错误
func (r *tiffDirectoryInput) ReadAt(data []byte, offset int64) (int, error) {
if r.ctx != nil {
if err := r.ctx.Err(); err != nil {
return 0, err
}
}
n, err := r.reader.ReadAt(data, offset)
r.patch(data[:n], offset)
return n, err
}
// size 返回应用方向后的显示尺寸
// 返回: image.Point 像素宽高
func (l tiffImageLayout) size() image.Point {
if l.orientation >= 5 {
return image.Pt(l.height, l.width)
}
return image.Pt(l.width, l.height)
}
// Bounds 返回从零开始的显示区域
// 返回: image.Rectangle 显示区域
func (s *orientedTIFFImage) Bounds() image.Rectangle {
return image.Rectangle{Max: s.size()}
}
// ColorModel 返回原始颜色模型
// 返回: color.Model 颜色模型
func (s *orientedTIFFImage) ColorModel() color.Model { return s.source.ColorModel() }
// sourcePoint 将显示坐标映射为存储坐标
// 入参: x 横向显示坐标, y 纵向显示坐标
// 返回: image.Point 原始坐标, bool 是否位于显示区域内
func (s *orientedTIFFImage) sourcePoint(x, y int) (image.Point, bool) {
if !image.Pt(x, y).In(s.Bounds()) {
return image.Point{}, false
}
w, h := s.width, s.height
switch s.orientation {
case 2:
x = w - 1 - x
case 3:
x, y = w-1-x, h-1-y
case 4:
y = h - 1 - y
case 5:
x, y = y, x
case 6:
x, y = y, h-1-x
case 7:
x, y = w-1-y, h-1-x
case 8:
x, y = w-1-y, x
}
b := s.source.Bounds()
return image.Pt(b.Min.X+x, b.Min.Y+y), true
}
// At 返回原图颜色,不重新采样或量化
// 入参: x 横向显示坐标, y 纵向显示坐标
// 返回: color.Color 像素颜色,区域外透明
func (s *orientedTIFFImage) At(x, y int) color.Color {
p, ok := s.sourcePoint(x, y)
if !ok {
return color.NRGBA64{}
}
return s.source.At(p.X, p.Y)
}
// NRGBA64At 读取非预乘十六位颜色,保留透明像素中的原始颜色
// 入参: x 横向显示坐标, y 纵向显示坐标
// 返回: color.NRGBA64 像素颜色
func (s *orientedTIFFImage) NRGBA64At(x, y int) color.NRGBA64 {
p, ok := s.sourcePoint(x, y)
if !ok {
return color.NRGBA64{}
}
return imageNRGBA64At(s.source, p.X, p.Y)
}
// RGBA64At 返回预乘十六位像素,保留原始颜色精度
// 入参: x 横向显示坐标, y 纵向显示坐标
// 返回: color.RGBA64 像素颜色
func (s *orientedTIFFImage) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.At(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
+189
View File
@@ -0,0 +1,189 @@
// 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 (
"cmp"
"context"
"encoding/binary"
"fmt"
"image"
"io"
"math/bits"
"slices"
"sort"
"golang.org/x/image/tiff"
)
// tiffBitOrderInput 仅在读取像素块时反转字节内位序,保留目录和原始数据
type tiffBitOrderInput struct {
source io.ReaderAt
ranges [][2]int64
position int64
}
// tiffBitOrderRanges 收集低位优先的未压缩一位图像块,压缩传真位序由解码器处理
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局
// 返回: [][2]int64 按偏移排序且不重叠的字节区间, error 字段或布局错误
func tiffBitOrderRanges(ctx context.Context, data []byte, layout tiffImageLayout) ([][2]int64, error) {
if layout.fillOrder != 2 {
return nil, nil
}
var order binary.ByteOrder = binary.LittleEndian
if data[0] == 'M' {
order = binary.BigEndian
}
offset := int(layout.offset)
count := int(order.Uint16(data[offset:]))
entries := make(map[uint16][]byte, count)
for i := range count {
entry := data[offset+2+i*12 : offset+14+i*12]
entries[order.Uint16(entry)] = entry
}
compression := entries[259]
method := uint16(1)
if compression != nil {
if order.Uint16(compression[2:]) != 3 || order.Uint32(compression[4:]) != 1 {
return nil, fmt.Errorf("invalid TIFF compression field")
}
method = order.Uint16(compression[8:])
}
if method == 3 || method == 4 {
return nil, nil
}
if method != 1 {
return nil, fmt.Errorf("unsupported TIFF fill order for compression %d", method)
}
if entry := entries[258]; entry != nil {
if order.Uint16(entry[2:]) != 3 || order.Uint32(entry[4:]) != layout.samples {
return nil, fmt.Errorf("invalid TIFF sample depths")
}
for i := uint32(0); i < layout.samples; i++ {
depth, err := tiffPlaneValue(data, order, entry, i)
if err != nil || depth != 1 {
return nil, fmt.Errorf("unsupported TIFF fill order for sample depth")
}
}
}
locations, lengths := entries[273], entries[279]
value := func(tag uint16, fallback uint32) (uint32, error) {
entry := entries[tag]
if entry == nil {
return fallback, nil
}
if order.Uint32(entry[4:]) != 1 {
return 0, fmt.Errorf("invalid TIFF scalar field %d", tag)
}
return tiffPlaneValue(data, order, entry, 0)
}
rows, err := value(278, ^uint32(0))
if err != nil || rows == 0 {
return nil, fmt.Errorf("invalid TIFF rows per strip")
}
expected := (uint64(layout.height)-1)/uint64(rows) + 1
if entries[322] != nil {
locations, lengths = entries[324], entries[325]
width, err := value(322, 0)
if err != nil || width == 0 || width%16 != 0 {
return nil, fmt.Errorf("invalid TIFF tile width")
}
height, err := value(323, 0)
if err != nil || height == 0 || height%16 != 0 {
return nil, fmt.Errorf("invalid TIFF tile height")
}
expected = ((uint64(layout.width)-1)/uint64(width) + 1) * ((uint64(layout.height)-1)/uint64(height) + 1)
if locations != nil && order.Uint16(locations[2:]) != 4 {
return nil, fmt.Errorf("invalid TIFF tile offsets")
}
}
if layout.planar {
if expected > uint64(^uint32(0))/uint64(layout.samples) {
return nil, fmt.Errorf("too many TIFF image blocks")
}
expected *= uint64(layout.samples)
}
if locations == nil || lengths == nil {
return nil, fmt.Errorf("missing TIFF block arrays")
}
blocks := order.Uint32(locations[4:])
if blocks == 0 || uint64(blocks) != expected || order.Uint32(lengths[4:]) != blocks {
return nil, fmt.Errorf("invalid TIFF block count")
}
var ranges [][2]int64
for i := uint32(0); i < blocks; i++ {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, err
}
}
at, err := tiffPlaneValue(data, order, locations, i)
if err != nil {
return nil, err
}
size, err := tiffPlaneValue(data, order, lengths, i)
if err != nil || size == 0 || uint64(at)+uint64(size) > uint64(len(data)) {
return nil, fmt.Errorf("truncated TIFF image block")
}
ranges = append(ranges, [2]int64{int64(at), int64(at) + int64(size)})
}
slices.SortFunc(ranges, func(a, b [2]int64) int { return cmp.Compare(a[0], b[0]) })
merged := ranges[:0]
for _, span := range ranges {
if len(merged) != 0 && span[0] <= merged[len(merged)-1][1] {
merged[len(merged)-1][1] = max(merged[len(merged)-1][1], span[1])
} else {
merged = append(merged, span)
}
}
return merged, nil
}
// decodeTIFFBitOrder 使用只读视图校正位序,不分配整幅编码的副本
// 入参: input 原始解码视图, ranges 需要反转位序的像素区间
// 返回: image.Image 解码图像, error 解码错误
func decodeTIFFBitOrder(input interface {
io.Reader
io.ReaderAt
}, ranges [][2]int64) (image.Image, error) {
if len(ranges) == 0 {
return tiff.Decode(input)
}
return tiff.Decode(&tiffBitOrderInput{source: input, ranges: ranges})
}
// Read 顺序读取并校正像素字节的位序
// 入参: data 接收缓冲
// 返回: int 读取字节数, error 读取错误
func (r *tiffBitOrderInput) Read(data []byte) (int, error) {
n, err := r.ReadAt(data, r.position)
r.position += int64(n)
return n, err
}
// ReadAt 按文件偏移读取,只反转实际读取到的像素字节
// 入参: data 接收缓冲, offset 文件偏移
// 返回: int 读取字节数, error 读取错误
func (r *tiffBitOrderInput) ReadAt(data []byte, offset int64) (int, error) {
n, err := r.source.ReadAt(data, offset)
index := sort.Search(len(r.ranges), func(i int) bool { return r.ranges[i][1] > offset })
for ; index < len(r.ranges) && r.ranges[index][0] < offset+int64(n); index++ {
span := r.ranges[index]
for i := max(int64(0), span[0]-offset); i < min(int64(n), span[1]-offset); i++ {
data[i] = bits.Reverse8(data[i])
}
}
return n, err
}
+185
View File
@@ -0,0 +1,185 @@
// 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 (
"context"
"encoding/binary"
"fmt"
"image"
"image/color"
"math/bits"
"github.com/xiaoqidun/pdfgo"
)
// tiffFaxImage 组合独立解码的传真条带或瓦片,保留紧凑的一位像素存储
type tiffFaxImage struct {
blocks []image.Image
bounds image.Rectangle
width int
height int
across int
}
// decodeTIFFFax 按TIFF传真选项解码各图像块,复用PDFGo的T.4与T.6解码器
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局
// 返回: image.Image 图像, error 字段、解码或取消错误
func decodeTIFFFax(ctx context.Context, data []byte, layout tiffImageLayout) (image.Image, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, err
}
if layout.samples != 1 || layout.compression == 3 && layout.t4 & ^uint32(7) != 0 || layout.compression == 4 && layout.t6 & ^uint32(2) != 0 {
return nil, fmt.Errorf("invalid TIFF fax options")
}
var order binary.ByteOrder = binary.LittleEndian
if data[0] == 'M' {
order = binary.BigEndian
}
offset := int(layout.offset)
count := int(order.Uint16(data[offset:]))
entries := make(map[uint16][]byte, count)
for i := range count {
entry := data[offset+2+i*12 : offset+14+i*12]
entries[order.Uint16(entry)] = entry
}
value := func(tag uint16, fallback uint32) (uint32, error) {
entry := entries[tag]
if entry == nil {
return fallback, nil
}
if order.Uint32(entry[4:]) != 1 {
return 0, fmt.Errorf("invalid TIFF scalar field %d", tag)
}
return tiffPlaneValue(data, order, entry, 0)
}
for _, tag := range []uint16{258, 317, 339} {
v, err := value(tag, 1)
if err != nil || v != 1 {
return nil, fmt.Errorf("invalid TIFF fax sample field %d", tag)
}
}
photo, err := value(262, 2)
if err != nil || photo > 1 || entries[338] != nil {
return nil, fmt.Errorf("invalid TIFF fax color model")
}
width := uint32(layout.width)
height, err := value(278, ^uint32(0))
if err != nil || height == 0 {
return nil, fmt.Errorf("invalid TIFF rows per strip")
}
height = min(height, uint32(layout.height))
locations, lengths := entries[273], entries[279]
tiled := entries[322] != nil
if tiled {
width, err = value(322, 0)
if err != nil || width == 0 || width%16 != 0 {
return nil, fmt.Errorf("invalid TIFF tile width")
}
height, err = value(323, 0)
if err != nil || height == 0 || height%16 != 0 {
return nil, fmt.Errorf("invalid TIFF tile height")
}
locations, lengths = entries[324], entries[325]
if locations != nil && order.Uint16(locations[2:]) != 4 {
return nil, fmt.Errorf("invalid TIFF tile offsets")
}
}
if uint64(width) > uint64(int(^uint(0)>>1)) || uint64(height) > uint64(int(^uint(0)>>1)) || locations == nil || lengths == nil {
return nil, fmt.Errorf("invalid TIFF fax blocks")
}
across := (uint64(layout.width)-1)/uint64(width) + 1
down := (uint64(layout.height)-1)/uint64(height) + 1
blocks := across * down
if blocks != uint64(order.Uint32(locations[4:])) || blocks != uint64(order.Uint32(lengths[4:])) {
return nil, fmt.Errorf("invalid TIFF fax block count")
}
result := &tiffFaxImage{bounds: image.Rect(0, 0, layout.width, layout.height), width: int(width), height: int(height), across: int(across)}
for i := uint64(0); i < blocks; i++ {
if err := ctx.Err(); err != nil {
return nil, err
}
at, err := tiffPlaneValue(data, order, locations, uint32(i))
if err != nil {
return nil, err
}
size, err := tiffPlaneValue(data, order, lengths, uint32(i))
if err != nil || size == 0 || uint64(at)+uint64(size) > uint64(len(data)) {
return nil, fmt.Errorf("truncated TIFF fax block")
}
encoded := data[uint64(at) : uint64(at)+uint64(size)]
if layout.fillOrder == 2 {
reversed := make([]byte, len(encoded))
for start := 0; start < len(encoded); {
if err := ctx.Err(); err != nil {
return nil, err
}
end := start + min(32768, len(encoded)-start)
for j := start; j < end; j++ {
reversed[j] = bits.Reverse8(encoded[j])
}
start = end
}
encoded = reversed
}
rows := height
if !tiled {
rows = min(height, uint32(uint64(layout.height)-i*uint64(height)))
}
k := pdfgo.Integer(0)
if layout.compression == 4 {
k = -1
} else if layout.compression == 3 && layout.t4&1 != 0 {
k = 1
}
params := pdfgo.Dictionary{
"K": k, "Columns": pdfgo.Integer(width), "Rows": pdfgo.Integer(rows),
"EndOfBlock": pdfgo.Boolean(false), "EndOfLine": pdfgo.Boolean(layout.compression == 3),
"EncodedByteAlign": pdfgo.Boolean(layout.compression == 2), "BlackIs1": pdfgo.Boolean(photo == 1),
}
object := &pdfgo.Image{Width: int(width), Height: int(rows), BitsPerComponent: 1, ColorSpace: pdfgo.Name("DeviceGray"), Stream: &pdfgo.Stream{Dictionary: pdfgo.Dictionary{"Filter": pdfgo.Name("CCITTFaxDecode"), "DecodeParms": params}, Data: encoded}}
pixels, err := object.DecodeSamplesContext(ctx)
if err != nil {
return nil, fmt.Errorf("TIFF fax block %d: %w", i, err)
}
result.blocks = append(result.blocks, pixels)
}
if err := ctx.Err(); err != nil {
return nil, err
}
return result, nil
}
// Bounds 返回主图显示区域,不包含边缘瓦片的填充像素
// 返回: image.Rectangle 图像区域
func (s *tiffFaxImage) Bounds() image.Rectangle { return s.bounds }
// ColorModel 返回二值图像使用的灰度颜色模型
// 返回: color.Model 灰度模型
func (s *tiffFaxImage) ColorModel() color.Model { return color.GrayModel }
// At 读取所属条带或瓦片中的像素,区域外返回黑色
// 入参: x 横向像素坐标, y 纵向像素坐标
// 返回: color.Color 像素颜色
func (s *tiffFaxImage) At(x, y int) color.Color {
if !image.Pt(x, y).In(s.bounds) {
return color.Gray{}
}
block := s.blocks[(y/s.height)*s.across+x/s.width]
return block.At(x%s.width, y%s.height)
}
+527
View File
@@ -0,0 +1,527 @@
// 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 (
"bytes"
"context"
"encoding/binary"
"fmt"
"image"
"image/color"
"io"
"math"
)
// tiffPlaneInput 将单分量目录附加到只读视图,像素仍从原始编码读取
type tiffPlaneInput struct {
source, directory []byte
position int64
order binary.ByteOrder
ctx context.Context
}
// planarTIFFImage 组合独立分量或映射调色板,不额外分配交错像素缓冲
type planarTIFFImage struct {
planes []image.Image
bounds image.Rectangle
depth uint32
packed [4]uint32
alpha uint32
gray bool
palette []color.NRGBA64
}
// decodeTIFFImage 按存储方式解码主图并统一显示方向
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 主图布局
// 返回: image.Image 图像, error 解码或取消错误
func decodeTIFFImage(ctx context.Context, data []byte, layout tiffImageLayout) (image.Image, error) {
if layout.compression >= 2 && layout.compression <= 4 {
pixels, err := decodeTIFFFax(ctx, data, layout)
if err == nil && layout.orientation != 1 {
pixels = &orientedTIFFImage{source: pixels, tiffImageLayout: layout}
}
return pixels, err
}
ranges, err := tiffBitOrderRanges(ctx, data, layout)
if err != nil {
return nil, err
}
var pixels image.Image
if layout.planar && layout.samples > 1 || layout.samples == 1 && (layout.depth == 2 || layout.depth == 4) {
result, directories, err := tiffPlaneDirectories(ctx, data, layout)
if err != nil {
return nil, err
}
for _, directory := range directories {
input := &tiffPlaneInput{source: data, directory: directory, order: binary.LittleEndian, ctx: ctx}
if data[0] == 'M' {
input.order = binary.BigEndian
}
plane, err := decodeTIFFBitOrder(input, ranges)
if err != nil {
return nil, err
}
result.planes = append(result.planes, plane)
}
pixels = result
} else {
input := newTIFFDirectoryInput(data, layout.offset)
input.ctx = ctx
var err error
pixels, err = decodeTIFFBitOrder(input, ranges)
if err != nil {
return nil, err
}
}
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, err
}
}
if layout.orientation != 1 {
pixels = &orientedTIFFImage{source: pixels, tiffImageLayout: layout}
}
return pixels, nil
}
// tiffPlaneDirectories 为颜色、透明度或调色板索引构建单通道TIFF目录
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局
// 返回: *planarTIFFImage 像素布局, [][]byte 各分量目录, error 字段或布局错误
func tiffPlaneDirectories(ctx context.Context, data []byte, layout tiffImageLayout) (*planarTIFFImage, [][]byte, error) {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
}
if uint64(len(data))+65536*12+8 > math.MaxUint32 {
return nil, nil, fmt.Errorf("TIFF plane directory offset exceeds format limit")
}
var order binary.ByteOrder = binary.LittleEndian
if data[0] == 'M' {
order = binary.BigEndian
}
offset := int(layout.offset)
count := int(order.Uint16(data[offset:]))
entries := make(map[uint16][]byte, count)
for i := range count {
entry := data[offset+2+i*12 : offset+14+i*12]
entries[order.Uint16(entry)] = entry
}
value := func(tag uint16, fallback uint32) (uint32, error) {
entry := entries[tag]
if entry == nil {
return fallback, nil
}
if order.Uint32(entry[4:]) != 1 {
return 0, fmt.Errorf("invalid TIFF scalar field %d", tag)
}
return tiffPlaneValue(data, order, entry, 0)
}
samples, err := value(277, 1)
if err != nil || samples < 1 || samples > 4 {
return nil, nil, fmt.Errorf("invalid TIFF plane count")
}
photo, err := value(262, math.MaxUint32)
if err != nil || photo > 3 || photo == 3 && samples != 1 {
return nil, nil, fmt.Errorf("unsupported TIFF planar color model")
}
compression, err := value(259, 1)
if err != nil {
return nil, nil, err
}
fill, err := value(266, 1)
if err != nil || fill != 1 && (fill != 2 || compression != 1 && compression != 3 && compression != 4) {
return nil, nil, fmt.Errorf("unsupported TIFF plane fill order")
}
predictor, err := value(317, 1)
if err != nil || predictor != 1 && predictor != 2 {
return nil, nil, fmt.Errorf("unsupported TIFF plane predictor")
}
result := &planarTIFFImage{bounds: image.Rect(0, 0, layout.width, layout.height), gray: photo < 2}
colors := uint32(3)
if photo != 2 {
colors = 1
}
if samples != colors {
result.alpha, err = value(338, 0)
if err != nil || samples != colors+1 || result.alpha != 1 && result.alpha != 2 {
return nil, nil, fmt.Errorf("unsupported TIFF extra planes")
}
} else if entries[338] != nil {
return nil, nil, fmt.Errorf("unexpected TIFF extra samples")
}
width, err := value(322, 0)
if err != nil {
return nil, nil, err
}
blocks := uint64(0)
locations, lengths := uint16(273), uint16(279)
if width != 0 {
height, err := value(323, 0)
if err != nil || height == 0 || width%16 != 0 || height%16 != 0 {
return nil, nil, fmt.Errorf("invalid TIFF tile dimensions")
}
blocks = ((uint64(layout.width)-1)/uint64(width) + 1) * ((uint64(layout.height)-1)/uint64(height) + 1)
locations, lengths = 324, 325
} else {
rows, err := value(278, math.MaxUint32)
if err != nil || rows == 0 {
return nil, nil, fmt.Errorf("invalid TIFF rows per strip")
}
blocks = (uint64(layout.height)-1)/uint64(rows) + 1
}
if blocks > math.MaxUint32/uint64(samples) {
return nil, nil, fmt.Errorf("too many TIFF plane blocks")
}
for _, tag := range []uint16{locations, lengths} {
entry := entries[tag]
if entry == nil || uint64(order.Uint32(entry[4:])) != blocks*uint64(samples) || tag == 324 && order.Uint16(entry[2:]) != 4 {
return nil, nil, fmt.Errorf("invalid TIFF plane block count")
}
}
for i := uint32(0); i < uint32(blocks)*samples; i++ {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
}
at, err := tiffPlaneValue(data, order, entries[locations], i)
if err != nil {
return nil, nil, err
}
size, err := tiffPlaneValue(data, order, entries[lengths], i)
if err != nil || size == 0 || uint64(at)+uint64(size) > uint64(len(data)) {
return nil, nil, fmt.Errorf("truncated TIFF plane block")
}
}
for _, tag := range []uint16{258, 339} {
if entry := entries[tag]; entry != nil && (order.Uint16(entry[2:]) != 3 || order.Uint32(entry[4:]) != samples) {
return nil, nil, fmt.Errorf("invalid TIFF sample array")
}
}
var directories [][]byte
for plane := uint32(0); plane < samples; plane++ {
depth := uint32(1)
if entry := entries[258]; entry != nil {
depth, err = tiffPlaneValue(data, order, entry, plane)
}
if err != nil || depth != 1 && depth != 2 && depth != 4 && depth != 8 && depth != 16 {
return nil, nil, fmt.Errorf("unsupported TIFF plane depth")
}
result.depth = max(result.depth, depth)
if depth < 8 && predictor != 1 {
return nil, nil, fmt.Errorf("unsupported TIFF packed plane predictor")
}
if depth == 2 || depth == 4 {
result.packed[plane] = depth
if uint64(layout.width)*uint64(depth) > uint64(math.MaxInt)/uint64(layout.height) || uint64(layout.width)*uint64(depth) > math.MaxUint32 || uint64(width)*uint64(depth) > math.MaxUint32 {
return nil, nil, fmt.Errorf("TIFF plane dimensions exceed format limit")
}
if compression == 2 || compression == 3 || compression == 4 {
return nil, nil, fmt.Errorf("unsupported TIFF packed plane compression")
}
}
if entry := entries[339]; entry != nil {
format, err := tiffPlaneValue(data, order, entry, plane)
if err != nil || format != 1 {
return nil, nil, fmt.Errorf("unsupported TIFF sample format")
}
}
if photo == 3 {
entry := entries[320]
if depth > 8 || entry == nil || order.Uint16(entry[2:]) != 3 || order.Uint32(entry[4:]) != 3<<depth {
return nil, nil, fmt.Errorf("invalid TIFF color map")
}
n := uint32(1) << depth
result.palette = make([]color.NRGBA64, n)
for i := range n {
var components [3]uint16
for c := range components {
v, err := tiffPlaneValue(data, order, entry, uint32(c)*n+i)
if err != nil {
return nil, nil, err
}
components[c] = uint16(v)
if v%257 != 0 {
result.depth = 16
}
}
result.palette[i] = color.NRGBA64{R: components[0], G: components[1], B: components[2], A: 65535}
}
}
directory := make([]byte, 2, 6+count*12)
for i := range count {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
}
entry := bytes.Clone(data[offset+2+i*12 : offset+14+i*12])
tag := order.Uint16(entry)
if tag == 338 || tag == 320 {
continue
}
scalar := uint32(0)
switch tag {
case 258:
scalar = depth
if result.packed[plane] != 0 {
scalar = 1
}
case 256, 322:
if result.packed[plane] != 0 {
original, err := tiffPlaneValue(data, order, entry, 0)
if err != nil {
return nil, nil, err
}
order.PutUint16(entry[2:], 4)
order.PutUint32(entry[4:], 1)
order.PutUint32(entry[8:], original*depth)
}
case 262:
scalar = 1
if result.gray && plane == 0 && photo == 0 {
scalar = 0
}
case 277, 284, 339:
scalar = 1
case locations, lengths:
entry, err = tiffPlaneArray(data, order, entry, uint32(blocks)*plane, uint32(blocks))
if err != nil {
return nil, nil, err
}
}
if tag == 258 || tag == 262 || tag == 277 || tag == 284 || tag == 339 {
order.PutUint16(entry[2:], 3)
order.PutUint32(entry[4:], 1)
clear(entry[8:])
order.PutUint16(entry[8:], uint16(scalar))
}
directory = append(directory, entry...)
}
order.PutUint16(directory, uint16((len(directory)-2)/12))
directories = append(directories, append(directory, 0, 0, 0, 0))
}
return result, directories, nil
}
// tiffPlaneValue 读取SHORT或LONG数组中的一项,校验完整数组范围
// 入参: data 原编码, order 字节序, entry 目录项, index 元素序号
// 返回: uint32 元素值, error 类型或越界错误
func tiffPlaneValue(data []byte, order binary.ByteOrder, entry []byte, index uint32) (uint32, error) {
kind, count := order.Uint16(entry[2:]), order.Uint32(entry[4:])
if kind != 3 && kind != 4 || index >= count {
return 0, fmt.Errorf("invalid TIFF plane array")
}
size := uint64(2)
if kind == 4 {
size = 4
}
values := entry[8:]
if size*uint64(count) > 4 {
at := uint64(order.Uint32(values))
if at+size*uint64(count) > uint64(len(data)) {
return 0, fmt.Errorf("truncated TIFF plane array")
}
values = data[at:]
}
if size == 2 {
return uint32(order.Uint16(values[uint64(index)*size:])), nil
}
return order.Uint32(values[uint64(index)*size:]), nil
}
// tiffPlaneArray 将块索引缩减为指定分量的范围,较长数组继续借用原编码
// 入参: data 原编码, order 字节序, entry 目录项副本, first 起始序号, count 元素数
// 返回: []byte 修改后的目录项, error 类型或越界错误
func tiffPlaneArray(data []byte, order binary.ByteOrder, entry []byte, first, count uint32) ([]byte, error) {
if _, err := tiffPlaneValue(data, order, entry, first+count-1); err != nil {
return nil, err
}
size := uint32(2)
if order.Uint16(entry[2:]) == 4 {
size = 4
}
if uint64(count)*uint64(size) <= 4 {
var values [4]byte
for i := uint32(0); i < count; i++ {
value, _ := tiffPlaneValue(data, order, entry, first+i)
if size == 2 {
order.PutUint16(values[i*size:], uint16(value))
} else {
order.PutUint32(values[i*size:], value)
}
}
copy(entry[8:], values[:])
} else {
order.PutUint32(entry[8:], order.Uint32(entry[8:])+first*size)
}
order.PutUint32(entry[4:], count)
return entry, nil
}
// Read 从组合视图顺序读取
// 入参: data 接收缓冲
// 返回: int 字节数, error 读取或取消错误
func (r *tiffPlaneInput) Read(data []byte) (int, error) {
n, err := r.ReadAt(data, r.position)
r.position += int64(n)
return n, err
}
// ReadAt 从原始编码或附加目录读取并替换首目录指针
// 入参: data 接收缓冲, offset 文件偏移
// 返回: int 字节数, error 读取或取消错误
func (r *tiffPlaneInput) ReadAt(data []byte, offset int64) (int, error) {
if r.ctx != nil {
if err := r.ctx.Err(); err != nil {
return 0, err
}
}
if offset < 0 {
return 0, fmt.Errorf("negative TIFF read offset")
}
start := int64(len(r.source))
directory := start + start%2
written := 0
if offset < start {
written = copy(data, r.source[offset:])
}
if offset+int64(written) == start && start != directory && written < len(data) {
data[written] = 0
written++
}
if at := offset + int64(written) - directory; at >= 0 && at < int64(len(r.directory)) {
written += copy(data[written:], r.directory[at:])
}
var pointer [4]byte
r.order.PutUint32(pointer[:], uint32(directory))
for i, value := range pointer {
if at := int64(4+i) - offset; at >= 0 && at < int64(written) {
data[at] = value
}
}
if written != len(data) {
return written, io.EOF
}
return written, nil
}
// Bounds 返回存储坐标范围
// 返回: image.Rectangle 图像范围
func (p *planarTIFFImage) Bounds() image.Rectangle { return p.bounds }
// ColorModel 按透明度类型和最高分量精度返回颜色模型
// 返回: color.Model 颜色模型
func (p *planarTIFFImage) ColorModel() color.Model {
if p.gray && p.alpha == 0 {
if p.depth > 8 {
return color.Gray16Model
}
return color.GrayModel
}
if p.alpha == 1 {
if p.depth > 8 {
return color.RGBA64Model
}
return color.RGBAModel
}
if p.depth > 8 {
return color.NRGBA64Model
}
return color.NRGBAModel
}
// components 直接读取灰度分量,保留原始透明度语义
// 入参: x 横坐标, y 纵坐标
// 返回: [4]uint16 红、绿、蓝和透明度分量
func (p *planarTIFFImage) components(x, y int) [4]uint16 {
if !image.Pt(x, y).In(p.bounds) {
return [4]uint16{}
}
values := [4]uint16{0, 0, 0, 65535}
for i, plane := range p.planes {
var value uint16
switch plane := plane.(type) {
case *image.Gray:
if bits := p.packed[i]; bits != 0 {
for bit := range int(bits) {
value = value<<1 | uint16(plane.GrayAt(x*int(bits)+bit, y).Y>>7)
}
value *= 65535 / uint16(1<<bits-1)
} else {
value = uint16(plane.GrayAt(x, y).Y) * 257
}
case *image.Gray16:
value = plane.Gray16At(x, y).Y
default:
v, _, _, _ := plane.At(x, y).RGBA()
value = uint16(v)
}
if len(p.palette) != 0 {
c := p.palette[uint32(value)*uint32(len(p.palette)-1)/65535]
return [4]uint16{c.R, c.G, c.B, c.A}
}
if p.gray && i == 0 {
values[0], values[1], values[2] = value, value, value
} else if p.gray {
values[3] = value
} else {
values[i] = value
}
}
return values
}
// At 返回组合后的颜色,关联透明度保留预乘语义
// 入参: x 横坐标, y 纵坐标
// 返回: color.Color 像素颜色
func (p *planarTIFFImage) At(x, y int) color.Color {
values := p.components(x, y)
if p.alpha == 1 {
return color.RGBA64{R: values[0], G: values[1], B: values[2], A: values[3]}
}
return color.NRGBA64{R: values[0], G: values[1], B: values[2], A: values[3]}
}
// NRGBA64At 读取独立透明度颜色,保留非预乘分量中的隐藏颜色
// 入参: x 横坐标, y 纵坐标
// 返回: color.NRGBA64 像素颜色
func (p *planarTIFFImage) NRGBA64At(x, y int) color.NRGBA64 {
values := p.components(x, y)
if p.alpha == 1 && values[3] != 65535 {
if values[3] == 0 {
return color.NRGBA64{}
}
for i := range 3 {
values[i] = uint16(uint32(values[i]) * 65535 / uint32(values[3]))
}
}
return color.NRGBA64{R: values[0], G: values[1], B: values[2], A: values[3]}
}
// RGBA64At 返回预乘十六位颜色,不分配临时颜色对象
// 入参: x 横坐标, y 纵坐标
// 返回: color.RGBA64 像素颜色
func (p *planarTIFFImage) RGBA64At(x, y int) color.RGBA64 {
values := p.components(x, y)
if p.alpha != 1 && values[3] != 65535 {
for i := range 3 {
values[i] = uint16(uint32(values[i]) * uint32(values[3]) / 65535)
}
}
return color.RGBA64{R: values[0], G: values[1], B: values[2], A: values[3]}
}
+7 -4
View File
@@ -190,7 +190,7 @@ func (r *Reader) docStructure() (*Document, error) {
// 入参: page 页面对象
// 返回: *PageContent 页面内容, error 错误信息
func (r *Reader) PageContent(page Page) (*PageContent, error) {
return r.readPageContent(page, false)
return r.readPageContent(page, false, false)
}
// PageCount 获取当前文档总页数
@@ -729,9 +729,9 @@ func resolveResourcePath(resPath, baseLoc, filePath string) string {
}
// readPageContent 读取页面,可跳过文字提取不使用的独立路径和图片
// 入参: page 页面对象, textOnly 是否仅用于文字提取
// 入参: page 页面对象, textOnly 是否仅用于文字提取, template 是否按模板页读取
// 返回: *PageContent 页面内容, error 错误信息
func (r *Reader) readPageContent(page Page, textOnly bool) (*PageContent, error) {
func (r *Reader) readPageContent(page Page, textOnly, template bool) (*PageContent, error) {
fullPath := r.ResPath(page.BaseLoc)
data, err := r.readFile(fullPath)
if err != nil {
@@ -748,6 +748,9 @@ func (r *Reader) readPageContent(page Page, textOnly bool) (*PageContent, error)
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 {
r.loadRes(resolveResourcePath(page.BaseLoc, "", res))
}
@@ -836,7 +839,7 @@ func (r *Reader) resolvePageArea(area PageArea, templates []Template) (PageArea,
}
for _, ref := range templates {
for _, template := range r.doc.CommonData.TemplatePage {
if template.ID != ref.TemplateID {
if !sameResourceID(template.ID, ref.TemplateID) {
continue
}
content, err := r.readPageHeader(Page{BaseLoc: template.BaseLoc})
+44 -20
View File
@@ -127,27 +127,34 @@ func packageReference(root, parent, name, attribute string) int {
return packageReferenceNone
}
// documentFiles 遍历独立文档及版本依赖,只解析结构文件,不读取附件等原始数据
// 入参: ctx 取消上下文, index 文档索引
// documentFiles 汇总指定文档及版本依赖,共享结构只解析一次,不读取附件等原始数据
// 入参: ctx 取消上下文, indexes 文档索引,为空时不扫描任何文档
// 返回: map[string]bool 包内文件路径, error 读取或取消错误
func (r *Reader) documentFiles(ctx context.Context, index int) (map[string]bool, error) {
files, _, err := r.documentDependencies(ctx, index)
func (r *Reader) documentFiles(ctx context.Context, indexes ...int) (map[string]bool, error) {
files, _, err := r.documentDependencies(ctx, indexes...)
return files, err
}
// documentDependencies 区分文档全部依赖与需要解析的结构文件,避免重复读取原始数据
// 入参: ctx 取消上下文, index 文档索引
// documentDependencies 合并指定入口的依赖图,区分结构文件与原始数据
// 入参: ctx 取消上下文, indexes 文档索引,为空时返回空集合
// 返回: map[string]bool 全部依赖, map[string]bool 结构文件, error 读取或取消错误
func (r *Reader) documentDependencies(ctx context.Context, index int) (map[string]bool, map[string]bool, error) {
func (r *Reader) documentDependencies(ctx context.Context, indexes ...int) (map[string]bool, map[string]bool, error) {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
if index < 0 || index >= r.DocumentCount() {
return nil, nil, fmt.Errorf("document index out of range: %d", index)
selected := make(map[int]bool, len(indexes))
for _, index := range indexes {
if index < 0 || index >= r.DocumentCount() {
return nil, nil, fmt.Errorf("document index out of range: %d", index)
}
selected[index] = true
}
var queue []string
seen := make(map[string]bool)
queued := make(map[string]bool)
if len(selected) == 0 {
return seen, queued, ctx.Err()
}
add := func(name string, kind int) {
seen[name] = true
if kind == packageReferenceXML && !queued[name] {
@@ -155,22 +162,21 @@ func (r *Reader) documentDependencies(ctx context.Context, index int) (map[strin
queue = append(queue, name)
}
}
data, err := r.readFile("OFD.xml")
data, err := r.readFileViewContext(ctx, "OFD.xml")
if err != nil {
return nil, nil, err
}
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(ctx, data)
if err != nil {
return nil, nil, err
}
body := root.childAt("DocBody", index)
if body == nil {
return nil, nil, fmt.Errorf("document body not found: %d", index)
}
var collect func(*editorXML)
collect = func(node *editorXML) {
var collect func(*editorXML) error
collect = func(node *editorXML) error {
if err := ctx.Err(); err != nil {
return err
}
if !node.matchesOFD(node.name.Local) {
return
return nil
}
parent := node.parent.name.Local
if kind := packageReference("OFD", parent, node.name.Local, ""); kind != packageReferenceNone {
@@ -187,10 +193,28 @@ func (r *Reader) documentDependencies(ctx context.Context, index int) (map[strin
}
}
for _, child := range node.children {
collect(child)
if err := collect(child); err != nil {
return err
}
}
return nil
}
index := 0
for _, body := range root.children {
if !body.matchesOFD("DocBody") {
continue
}
if selected[index] {
if err := collect(body); err != nil {
return nil, nil, err
}
delete(selected, index)
}
index++
}
if len(selected) != 0 {
return nil, nil, fmt.Errorf("document body not found")
}
collect(body)
for len(queue) > 0 {
if err := ctx.Err(); err != nil {
return nil, nil, err
+3
View File
@@ -75,6 +75,9 @@ func (r *Renderer) AnnotationGeometry(annotation Annotation) (Box, []ObjectConto
// 入参: object 图形对象, state 绘制状态
// 返回: error 度量错误
func (m *annotationMeasurement) DrawObject(object *GraphicObject, state RenderState) error {
if state.Hidden {
return nil
}
result, err := m.renderer.MeasureObject(*object, MeasureOptions{Defaults: state.Defaults, Parent: state.Parent, BoundaryInCTM: state.BoundaryInCTM, Clip: state.Clip, Contours: true})
if err != nil {
return err
+3
View File
@@ -44,6 +44,9 @@ type semanticStyle struct {
// 入参: object 源对象, state 继承状态
// 返回: error 编译错误
func (c *semanticCompiler) DrawObject(object *GraphicObject, state RenderState) error {
if state.Hidden {
return nil
}
switch object.Type {
case "TextObject":
return c.textObject(object.TextObject, state)
+1 -1
View File
@@ -49,7 +49,7 @@ type glyphOutlineKey struct {
func (r *Renderer) PositionText(object TextObject, state RenderState) (*PositionedText, error) {
result := &PositionedText{Run: TextRun{ID: object.ID, Text: object.Text()}}
result.Run.Boxes = make([]Box, len([]rune(result.Run.Text)))
if object.Visible != nil && !*object.Visible {
if state.Hidden || object.Visible != nil && !*object.Visible {
return result, nil
}
geometry, err := r.Geometry()
+75 -24
View File
@@ -17,11 +17,13 @@ package ofdgo
import "fmt"
// RenderState 保存对象继承的样式、父变换和页面坐标裁剪,所有引用只读
// Clip为nil表示未裁剪,空路径表示完全裁去,单位为毫米
// Clip为nil表示未提供裁剪,空路径表示完全裁去,单位为毫米
// Hidden表示当前对象或其上层容器不可见,不改变源对象的Visible属性
type RenderState struct {
Defaults *DrawParam
Parent *Matrix
BoundaryInCTM bool
Hidden bool
Clip *GeometryPath
}
@@ -42,6 +44,19 @@ type PageGroups interface {
SkipObjects(count int)
}
// PageComposites 可选地接收展开前的复合图元及矢量资源,不替代成员遍历
// 状态包含当前图元到页面的完整变换,源对象及状态只读
type PageComposites interface {
VisitComposite(object *CompositeGraphicUnit, state RenderState) error
}
// PageAnalysis 控制WalkPage和WalkObject对隐藏内容及裁剪的处理,不影响已构建场景的重放
// 跳过裁剪时RenderState.Clip为空,源对象的裁剪定义不变,叶对象仍由访问器解释
type PageAnalysis interface {
IncludeHidden() bool
SkipClips() bool
}
// WalkPage 按模板、图层、注解和印章顺序访问页面,不依赖任何绘图库
// 页面背景由调用方处理,文字裁剪可通过几何后端加载字体,图片由访问器按需加载
// 入参: page 页面内容, visitor 页面访问器
@@ -79,7 +94,7 @@ func (r *Renderer) WalkPage(page *PageContent, visitor PageVisitor) error {
}
if r.RenderAnnotations {
for _, annotation := range r.Reader.Annots[page.ID] {
if annotation.Visible != nil && !*annotation.Visible {
if annotation.Visible != nil && !*annotation.Visible && !pageIncludesHidden(visitor) {
continue
}
if err := r.walkAnnotation(annotation, visitor); err != nil {
@@ -116,7 +131,7 @@ func (r *Renderer) walkAnnotation(annotation Annotation, visitor PageVisitor) er
}
box, _ := ParseBox(annotation.Appearance.Boundary)
matrix := TranslationMatrix(box.X, box.Y)
state := RenderState{Parent: &matrix, BoundaryInCTM: true}
state := RenderState{Parent: &matrix, BoundaryInCTM: true, Hidden: annotation.Visible != nil && !*annotation.Visible}
for i := range annotation.Appearance.Objects {
if err := r.WalkObject(&annotation.Appearance.Objects[i], state, visitor); err != nil {
return err
@@ -175,11 +190,21 @@ func (r *Renderer) walkObject(object *GraphicObject, state RenderState, visitor
return err
}
}
if pageSkipsClips(visitor) {
state.Clip = nil
}
if groups, ok := visitor.(PageGroups); ok && groups.BeginObject(object) {
defer groups.EndObject()
}
switch object.Type {
case "TextObject", "PathObject", "ImageObject":
case "TextObject":
state.Hidden = state.Hidden || object.TextObject.Visible != nil && !*object.TextObject.Visible
return visitor.DrawObject(object, state)
case "PathObject":
state.Hidden = state.Hidden || object.PathObject.Visible != nil && !*object.PathObject.Visible
return visitor.DrawObject(object, state)
case "ImageObject":
state.Hidden = state.Hidden || object.ImageObject.Visible != nil && !*object.ImageObject.Visible
return visitor.DrawObject(object, state)
case "CompositeGraphicUnit", "CompositeObject":
return r.walkComposite(object.CompositeGraphicUnit, state, visitor, references)
@@ -192,7 +217,8 @@ func (r *Renderer) walkObject(object *GraphicObject, state RenderState, visitor
// 入参: object 复合图元, state 继承状态, visitor 页面访问器, references 当前资源引用链
// 返回: error 几何或访问器错误
func (r *Renderer) walkComposite(object CompositeGraphicUnit, state RenderState, visitor PageVisitor, references map[*CompositeGraphicUnit]bool) error {
if object.Visible != nil && !*object.Visible {
state.Hidden = state.Hidden || object.Visible != nil && !*object.Visible
if state.Hidden && !pageIncludesHidden(visitor) {
if groups, ok := visitor.(PageGroups); ok {
count, ref, seen := len(object.Objects), object.ResourceID, make(map[*CompositeGraphicUnit]bool)
for ref != "" {
@@ -213,33 +239,42 @@ func (r *Renderer) walkComposite(object CompositeGraphicUnit, state RenderState,
boundary = state.Parent.Multiply(boundary)
}
matrix := boundary.Multiply(NewMatrix(object.CTM))
var err error
if object.Boundary != "" {
state.Clip, err = r.geometryExtentClip(box.W, box.H, boundary, state.Clip)
if pageSkipsClips(visitor) {
state.Clip = nil
} else {
var err error
if object.Boundary != "" {
state.Clip, err = r.geometryExtentClip(box.W, box.H, boundary, state.Clip)
if err != nil {
return err
}
}
state.Clip, err = r.compositeExtentClip(object, matrix, state.Clip)
if err != nil {
return err
}
}
state.Clip, err = r.compositeExtentClip(object, matrix, state.Clip)
if err != nil {
return err
}
clips, clipMatrix := object.Clips, matrix
if !clips.usesObjectMatrix() {
clipMatrix = boundary
}
if clips != nil {
geometry, err := r.Geometry()
if err != nil {
return err
clips, clipMatrix := object.Clips, matrix
if !clips.usesObjectMatrix() {
clipMatrix = boundary
}
state.Clip, err = geometry.Clip(r, clips, clipMatrix, state.Clip)
if err != nil {
return err
if clips != nil {
geometry, err := r.Geometry()
if err != nil {
return err
}
state.Clip, err = geometry.Clip(r, clips, clipMatrix, state.Clip)
if err != nil {
return err
}
}
}
state.Defaults = r.drawParamDefaults(object.DrawParam, state.Defaults)
state.Parent = &matrix
if composites, ok := visitor.(PageComposites); ok {
if err := composites.VisitComposite(&object, state); err != nil {
return err
}
}
if object.ResourceID != "" {
if ref, _ := resourceValue(r.CompositeGraphicUnits, object.ResourceID); ref != nil {
if references[ref] {
@@ -269,6 +304,22 @@ func (r *Renderer) walkComposite(object CompositeGraphicUnit, state RenderState,
return nil
}
// pageIncludesHidden 查询访问器是否需要分析隐藏内容,不改变渲染器配置
// 入参: visitor 页面访问器
// 返回: bool 是否访问隐藏内容
func pageIncludesHidden(visitor PageVisitor) bool {
analysis, ok := visitor.(PageAnalysis)
return ok && analysis.IncludeHidden()
}
// pageSkipsClips 查询访问器是否只需资源和变换,不计算几何裁剪
// 入参: visitor 页面访问器
// 返回: bool 是否跳过裁剪
func pageSkipsClips(visitor PageVisitor) bool {
analysis, ok := visitor.(PageAnalysis)
return ok && analysis.SkipClips()
}
// compositeExtentClip 按矢量资源尺寸裁剪内容,资源裁剪随实例变换
// 入参: object 矢量资源, matrix 资源到页面矩阵, parent 父裁剪
// 返回: *GeometryPath 页面裁剪, error 尺寸或几何错误
+6 -3
View File
@@ -113,18 +113,21 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer {
return renderer
}
// loadTemplate 加载模板页面并复用解析结果
// loadTemplate 按标识数值加载模板页面,等价引用共用解析结果
// 入参: templateID 模板ID
// 返回: *PageContent 模板页面
func (r *Renderer) loadTemplate(templateID string) *PageContent {
if key := editorResourceID(templateID); key != "" {
templateID = key
}
if page := r.templatePageCache[templateID]; page != nil {
return page
}
for _, tpl := range r.Reader.doc.CommonData.TemplatePage {
if tpl.ID != templateID {
if !sameResourceID(tpl.ID, templateID) {
continue
}
page, err := r.Reader.PageContent(Page{BaseLoc: tpl.BaseLoc})
page, err := r.Reader.readPageContent(Page{BaseLoc: tpl.BaseLoc}, false, true)
if err != nil {
return nil
}
+3
View File
@@ -27,6 +27,9 @@ type canvasPageVisitor struct {
// 入参: object 源对象, state 继承状态
// 返回: error 绘制错误
func (v *canvasPageVisitor) DrawObject(object *GraphicObject, state RenderState) error {
if state.Hidden {
return nil
}
clip, err := geometryToCanvasPath(state.Clip)
if err != nil {
return err
+1 -1
View File
@@ -113,7 +113,7 @@ func (r *Renderer) PageTextByIndex(index int) (*PageText, error) {
if index < 0 || index >= len(doc.Pages.Page) {
return nil, fmt.Errorf("page index %d out of range", index)
}
page, err := r.Reader.readPageContent(doc.Pages.Page[index], true)
page, err := r.Reader.readPageContent(doc.Pages.Page[index], true, false)
if err != nil {
return nil, err
}
+41 -6
View File
@@ -15,6 +15,7 @@
package ofdgo
import (
"context"
"fmt"
"strconv"
"strings"
@@ -54,32 +55,53 @@ func (r *Reader) DocumentVersionCount() (int, error) {
// 返回独立副本;没有版本列表时返回nil,不以版本号推测默认版本
// 返回: []DocumentVersionInfo 版本信息, error 读取或描述错误
func (r *Reader) DocumentVersions() ([]DocumentVersionInfo, error) {
entries, err := r.documentVersionEntries()
return r.DocumentVersionsContext(context.Background())
}
// DocumentVersionsContext 按入口顺序读取版本信息,支持取消描述文件和文件清单的解析
// 返回独立副本,不切换当前版本,也不读取文件清单中的内容
// 入参: ctx 取消上下文
// 返回: []DocumentVersionInfo 版本信息, error 读取、描述或取消错误
func (r *Reader) DocumentVersionsContext(ctx context.Context) ([]DocumentVersionInfo, error) {
entries, err := r.documentVersionEntriesContext(ctx)
if err != nil {
return nil, err
}
for i, entry := range entries {
info, err := r.readDocumentVersion(entry.ID, entry.Location)
info, err := r.readDocumentVersionContext(ctx, entry.ID, entry.Location)
if err != nil {
return nil, fmt.Errorf("version %s: %w", entry.ID, err)
}
info.Index, info.Current = entry.Index, entry.Current
entries[i] = info
}
if err := ctx.Err(); err != nil {
return nil, err
}
return entries, nil
}
// documentVersionEntries 读取当前文档的版本入口,不提前加载各版本描述
// 返回: []DocumentVersionInfo 入口属性, error 读取或描述错误
func (r *Reader) documentVersionEntries() ([]DocumentVersionInfo, error) {
return r.documentVersionEntriesContext(context.Background())
}
// documentVersionEntriesContext 读取版本入口,在读取和解析过程中检查取消状态
// 入参: ctx 取消上下文
// 返回: []DocumentVersionInfo 入口属性, error 读取、描述或取消错误
func (r *Reader) documentVersionEntriesContext(ctx context.Context) ([]DocumentVersionInfo, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if r.documentIndex < 0 || r.documentIndex >= r.DocumentCount() {
return nil, fmt.Errorf("no docbody found")
}
data, err := r.readFile("OFD.xml")
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
}
@@ -97,6 +119,9 @@ func (r *Reader) documentVersionEntries() ([]DocumentVersionInfo, error) {
var result []DocumentVersionInfo
ids := make(map[string]bool)
for _, node := range versions.children {
if err := ctx.Err(); err != nil {
return nil, err
}
if !node.matchesOFD("Version") {
continue
}
@@ -135,12 +160,19 @@ func (r *Reader) documentVersionEntries() ([]DocumentVersionInfo, error) {
// 入参: 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 读取、描述或取消错误
func (r *Reader) readDocumentVersionContext(ctx context.Context, id, location string) (DocumentVersionInfo, error) {
var info DocumentVersionInfo
data, err := r.readFile(location)
data, err := r.readFileViewContext(ctx, location)
if err != nil {
return info, err
}
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(ctx, data)
if err != nil {
return info, err
}
@@ -162,6 +194,9 @@ func (r *Reader) readDocumentVersion(id, location string) (DocumentVersionInfo,
ids := map[string]bool{id: true}
found := false
for _, node := range list.children {
if err := ctx.Err(); err != nil {
return DocumentVersionInfo{}, err
}
if !node.matchesOFD("File") {
continue
}
+119 -37
View File
@@ -19,6 +19,7 @@ import (
"context"
"encoding/xml"
"fmt"
"io"
"maps"
"slices"
"strconv"
@@ -88,15 +89,13 @@ func (r *Reader) versionFileDependencies(ctx context.Context, entry string, part
func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[string][]byte, generated map[string]editorGeneratedReferences) (*versionEditGraph, error) {
graph := &versionEditGraph{files: make(map[string]*versionEditFile), used: make(map[string]bool)}
queue := []string{entry}
queued := map[string]bool{entry: true}
for len(queue) != 0 {
if err := ctx.Err(); err != nil {
return nil, err
}
name := queue[0]
queue = queue[1:]
if graph.files[name] != nil {
continue
}
data, ok := parts[name]
if ok && generated[name].stagedLeaf(data) {
graph.files[name], graph.used[name] = &versionEditFile{}, true
@@ -141,7 +140,8 @@ func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[s
target := resolveResourcePath(name, base, value)
file.links = append(file.links, versionEditLink{node: node, attribute: attribute, target: target})
graph.used[target] = true
if kind == packageReferenceXML {
if kind == packageReferenceXML && !queued[target] {
queued[target] = true
queue = append(queue, target)
}
}
@@ -178,11 +178,11 @@ func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[s
// 返回: map[string]bool 受保护路径, error 读取或结构错误
func (r *Reader) versionSharedFiles(ctx context.Context) (map[string]bool, error) {
shared := make(map[string]bool)
data, err := r.readFile("OFD.xml")
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
}
@@ -196,21 +196,25 @@ func (r *Reader) versionSharedFiles(ctx context.Context) (map[string]bool, error
shared[name] = true
}
}
var indexes []int
for index := range r.DocumentCount() {
if index == r.documentIndex {
continue
if index != r.documentIndex {
indexes = append(indexes, index)
}
files, err := r.documentFiles(ctx, index)
if err != nil {
return nil, err
}
maps.Copy(shared, files)
}
versions, err := r.DocumentVersions()
files, err := r.documentFiles(ctx, indexes...)
if err != nil {
return nil, err
}
maps.Copy(shared, files)
versions, err := r.DocumentVersionsContext(ctx)
if err != nil {
return nil, err
}
for _, version := range versions {
if err := ctx.Err(); err != nil {
return nil, err
}
if r.versionInfo != nil && version.ID == r.versionInfo.ID {
continue
}
@@ -253,16 +257,17 @@ func (r *Reader) versionResourceFiles(ctx context.Context, parts map[string][]by
files[file.Location] = true
}
}
var indexes []int
for index := range r.DocumentCount() {
if index == r.documentIndex {
continue
if index != r.documentIndex {
indexes = append(indexes, index)
}
other, err := r.documentFiles(ctx, index)
if err != nil {
return nil, err
}
maps.Copy(files, other)
}
other, err := r.documentFiles(ctx, indexes...)
if err != nil {
return nil, err
}
maps.Copy(files, other)
return files, nil
}
@@ -273,6 +278,54 @@ func (r *Reader) versionChanges(ctx context.Context, changes map[string][]byte)
return r.versionOutputChanges(ctx, changes, nil, nil)
}
// versionFileUnchanged 分块比较原文件与待写入内容,读取失败时保留替换内容
// 入参: ctx 取消上下文, name 包内路径, data 待写入内容, buffer 非空比较缓冲区
// 返回: bool 内容相同, error 取消错误
func (r *Reader) versionFileUnchanged(ctx context.Context, name string, data, buffer []byte) (bool, error) {
if err := ctx.Err(); err != nil {
return false, err
}
name = cleanPackagePath(name)
if original, ok := r.files[name]; ok {
if len(original) != len(data) {
return false, nil
}
for offset := 0; offset < len(data); {
if err := ctx.Err(); err != nil {
return false, err
}
end := offset + min(len(buffer), len(data)-offset)
if !bytes.Equal(original[offset:end], data[offset:end]) {
return false, nil
}
offset = end
}
return true, ctx.Err()
}
file, ok := r.packageFile(name)
if !ok || file.UncompressedSize64 != uint64(len(data)) {
return false, nil
}
input, err := file.Open()
if err != nil {
return false, nil
}
defer input.Close()
reader := imageInput{ReadCloser: input, context: ctx}
for len(data) != 0 {
block := buffer[:min(len(buffer), len(data))]
if _, err := io.ReadFull(&reader, block); err != nil {
return false, ctx.Err()
}
if !bytes.Equal(block, data[:len(block)]) {
return false, nil
}
data = data[len(block):]
}
n, err := reader.Read(buffer[:1])
return n == 0 && err == io.EOF, ctx.Err()
}
// versionOutputChanges 隔离文档或版本改动并同步删除清单,保留其他入口仍需使用的文件
// 入参: ctx 取消上下文, changes 待写入条目, removed 待删除条目,返回前移除受保护路径, generated 本次自产页面及引用,随隔离路径更新
// 返回: map[string][]byte 隔离后的修改集, error 读取、引用或结构错误
@@ -291,11 +344,16 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
if parts == nil {
parts = make(map[string][]byte)
}
buffer := make([]byte, 32*1024)
for name, data := range parts {
if generated[name].stagedLeaf(data) {
continue
}
if original, err := r.readFileView(name); err == nil && bytes.Equal(original, data) {
same, err := r.versionFileUnchanged(ctx, name, data, buffer)
if err != nil {
return nil, err
}
if same {
delete(parts, name)
}
}
@@ -344,17 +402,26 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
pending = append(pending, name)
}
for _, name := range slices.Sorted(maps.Keys(parts)) {
if err := ctx.Err(); err != nil {
return nil, err
}
if name != "OFD.xml" && shared[name] && owned[name] {
allocate(name)
}
}
parents := make(map[string][]string)
for _, name := range slices.Sorted(maps.Keys(graph.files)) {
if err := ctx.Err(); err != nil {
return nil, err
}
for _, link := range graph.files[name].links {
parents[link.target] = append(parents[link.target], name)
}
}
for i := 0; i < len(pending); i++ {
if err := ctx.Err(); err != nil {
return nil, err
}
for _, name := range parents[pending[i]] {
if shared[name] && paths[name] == "" {
allocate(name)
@@ -362,7 +429,7 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
}
}
for name, file := range graph.files {
data, err := file.rewrite(paths)
data, err := file.rewrite(ctx, paths)
if err != nil {
return nil, err
}
@@ -373,7 +440,7 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
for source, target := range paths {
data, ok := parts[source]
if !ok {
data, err = r.readFile(source)
data, err = r.readFileViewContext(ctx, source)
if err != nil {
return nil, err
}
@@ -382,18 +449,18 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
delete(parts, source)
}
if r.versionInfo != nil {
if err := r.versionManifest(parts, paths, graph.used, shared, removed); err != nil {
if err := r.versionManifest(ctx, parts, paths, graph.used, shared, removed); err != nil {
return nil, err
}
} else if target := paths[entry]; target != "" {
data, ok := parts["OFD.xml"]
if !ok {
data, err = r.readFile("OFD.xml")
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
}
@@ -418,12 +485,21 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
}
// rewrite 更新已隔离文件的引用,保留其余XML及相对路径基准
// 入参: paths 原路径到新路径的映射
// 入参: ctx 取消上下文, paths 原路径到新路径的映射
// 返回: []byte 更新后的XML,未改动时借用原文, error 属性编码错误
func (file *versionEditFile) rewrite(paths map[string]string) ([]byte, error) {
func (file *versionEditFile) rewrite(ctx context.Context, paths map[string]string) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if len(paths) == 0 {
return file.data, nil
}
var patches []editorXMLPatch
headers := make(map[*editorXML][]versionEditLink)
for _, link := range file.links {
if err := ctx.Err(); err != nil {
return nil, err
}
if paths[link.target] == "" {
continue
}
@@ -436,7 +512,7 @@ func (file *versionEditFile) rewrite(paths map[string]string) ([]byte, error) {
for node, links := range headers {
data := file.data[node.start:node.end]
for _, link := range links {
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(ctx, data)
if err != nil {
return nil, err
}
@@ -445,7 +521,7 @@ func (file *versionEditFile) rewrite(paths map[string]string) ([]byte, error) {
return nil, err
}
}
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(ctx, data)
if err != nil {
return nil, err
}
@@ -462,15 +538,15 @@ func (file *versionEditFile) rewrite(paths map[string]string) ([]byte, error) {
}
// versionManifest 更新所选版本的入口与文件清单,保留已有文件标识及扩展字段
// 入参: parts 输出条目, paths 隔离路径, used 当前内容依赖, shared 其他入口依赖, removed 当前版本移除的文件
// 入参: ctx 取消上下文, parts 输出条目, paths 隔离路径, used 当前内容依赖, shared 其他入口依赖, removed 当前版本移除的文件
// 返回: error 描述读取或XML编码错误
func (r *Reader) versionManifest(parts map[string][]byte, paths map[string]string, used, shared, removed map[string]bool) error {
func (r *Reader) versionManifest(ctx context.Context, parts map[string][]byte, paths map[string]string, used, shared, removed map[string]bool) error {
version := r.versionInfo
data, err := r.readFile(version.Location)
data, err := r.readFileViewContext(ctx, version.Location)
if err != nil {
return err
}
root, err := parseEditorXML(data)
root, err := parseEditorXMLContext(ctx, data)
if err != nil {
return err
}
@@ -479,6 +555,9 @@ func (r *Reader) versionManifest(parts map[string][]byte, paths map[string]strin
listed := make(map[string]bool)
ids := map[string]bool{version.ID: true}
for _, node := range list.children {
if err := ctx.Err(); err != nil {
return err
}
if !node.matchesOFD("File") {
continue
}
@@ -500,6 +579,9 @@ func (r *Reader) versionManifest(parts map[string][]byte, paths map[string]strin
var added []byte
serial := 0
for _, name := range slices.Sorted(maps.Keys(used)) {
if err := ctx.Err(); err != nil {
return err
}
if target := paths[name]; target != "" {
name = target
}
@@ -533,12 +615,12 @@ func (r *Reader) versionManifest(parts map[string][]byte, paths map[string]strin
name = packageAvailableName(r, parts, name)
entry, ok := parts["OFD.xml"]
if !ok {
entry, err = r.readFile("OFD.xml")
entry, err = r.readFileViewContext(ctx, "OFD.xml")
if err != nil {
return err
}
}
root, err := parseEditorXML(entry)
root, err := parseEditorXMLContext(ctx, entry)
if err != nil {
return err
}
+50 -10
View File
@@ -31,7 +31,7 @@ import (
"time"
)
// editorProgress 保存准备阶段的进度检查点,内存预览使用nil
// editorProgress 编辑与保存阶段的进度回调,nil表示不回报进度
type editorProgress func(stage string, completed, total int) error
// ofdCountingWriter 记录实际输出字节数
@@ -70,18 +70,22 @@ func (e *Editor) WriteTo(writer io.Writer) (int64, error) {
// WritePagesTo 按指定顺序将页面另存为独立OFD,不修改当前文档或撤销记录
// 保留描述信息并生成新文档标识,迁移相关资源、目录、注释和签章外观,移除指向未选页面的跳转
// 快照只准备所选页面,关联引用由页面导入流程统一筛选
// 快照只准备所选页面,关联引用由页面导入流程统一筛选,沿用当前后端及字体来源
// 签名凭据不代表另存文件验签有效;进度与取消复用OnWriteProgress,另含snapshot、ids和commit阶段
// 入参: writer 输出流, indexes 从0开始的页面索引,不可重复或为空
// 返回: int64 已写入字节数, error 错误信息
func (e *Editor) WritePagesTo(writer io.Writer, indexes []int) (int64, error) {
ctx := context.Background()
if e.output != nil {
ctx = e.output.ctx
}
if len(indexes) == 0 {
return 0, fmt.Errorf("no pages selected")
}
if err := e.validatePageIndexes(indexes); err != nil {
return 0, err
}
progress := editorProgress(e.OnWriteProgress)
progress := editorProgress(e.OnWriteProgress).withContext(ctx)
if err := progress.report("snapshot", 0, 0); err != nil {
return 0, err
}
@@ -104,18 +108,24 @@ func (e *Editor) WritePagesTo(writer io.Writer, indexes []int) (int64, error) {
snapshot.pages[i] = e.pages[index]
positions[i] = i
}
if err := snapshot.compressPatternPaints(); err != nil {
return 0, err
}
reader, err := snapshot.reader(progress)
if err != nil {
return 0, err
}
defer reader.Close()
selected := NewEditor()
selected.SetRenderBackends(e.backends)
selected.SetFontDirs(e.fontDirs...)
selected.SetFontFS(e.fontFS...)
selected.encryption = e.encryption
id := selected.Info.DocID
selected.Info = cloneEditorData(e.Info)
selected.Info.DocID = id
selected.OnWriteProgress = e.OnWriteProgress
if _, err := selected.ImportPagesWithOptions(reader, positions, 0, PageImportOptions{Outlines: true, OnProgress: e.OnWriteProgress}); err != nil {
if _, err := selected.ImportPagesContext(ctx, reader, positions, 0, PageImportOptions{Outlines: true, OnProgress: e.OnWriteProgress}); err != nil {
return 0, err
}
if e.output != nil {
@@ -153,6 +163,21 @@ func (progress editorProgress) report(stage string, completed, total int) error
return nil
}
// withContext 在进度回调前后检查取消,保留回调的原始错误
// 入参: ctx 取消上下文
// 返回: editorProgress 可取消的进度回调
func (progress editorProgress) withContext(ctx context.Context) editorProgress {
return func(stage string, completed, total int) error {
if err := ctx.Err(); err != nil {
return err
}
if err := progress.report(stage, completed, total); err != nil {
return err
}
return ctx.Err()
}
}
// writePlaintext 写出未加密包,供显式明文输出与加密封装共用
// 入参: writer 输出流
// 返回: int64 写入字节数, error 错误信息
@@ -220,11 +245,26 @@ func (e *Editor) writePlaintext(writer io.Writer) (int64, error) {
// 入参: progress 准备进度回调
// 返回: *Reader 阅读器, error 错误信息
func (e *Editor) reader(progress editorProgress) (*Reader, error) {
ctx := context.Background()
if e.output != nil {
ctx = e.output.ctx
}
return e.readerContext(ctx, progress)
}
// readerContext 准备可取消的内存快照,版本隔离沿用调用方上下文
// 入参: ctx 取消上下文, progress 准备进度回调
// 返回: *Reader 独立快照, error 准备或取消错误
func (e *Editor) readerContext(ctx context.Context, progress editorProgress) (*Reader, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
progress = progress.withContext(ctx)
if err := e.validate(); err != nil {
return nil, err
}
if e.source != nil {
return e.sourceReader(progress)
return e.sourceReader(ctx, progress)
}
r := &Reader{files: make(map[string][]byte), encryption: e.encryption}
if err := e.writeParts(func(name string, data []byte, _ bool) error {
@@ -373,7 +413,7 @@ func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, pro
if len(fonts)+len(images)+len(spaces) != 0 {
x.text("DocumentRes", "DocumentRes.xml")
}
for _, name := range definitions {
for _, name := range slices.Sorted(maps.Keys(definitions)) {
x.text("DocumentRes", "/"+name)
}
x.end("CommonData")
@@ -432,7 +472,7 @@ func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, pro
}
}
for _, resource := range e.resources {
if resource.definition() != "" && slices.Contains(definitions, resource.name) || resource.colorProfileUsed(spaces) {
if resource.definition() != "" && definitions[resource.name] || resource.colorProfileUsed(spaces) {
if err := write(resource.name, resource.data, false); err != nil {
return err
}
@@ -442,8 +482,8 @@ func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, pro
}
// usedResources 沿图层、对象及资源依赖筛选新增资源和共享文件,不修改资源池
// 返回: []editorResource 新增字体, []editorResource 新增图片, []ColorSpace 新增颜色空间, []string 独立资源文件
func (e *Editor) usedResources() (fonts, images []editorResource, spaces []ColorSpace, sourceFiles []string) {
// 返回: []editorResource 新增字体, []editorResource 新增图片, []ColorSpace 新增颜色空间, map[string]bool 独立资源文件集合
func (e *Editor) usedResources() (fonts, images []editorResource, spaces []ColorSpace, sourceFiles map[string]bool) {
used := make(map[string]bool)
promoted := make(map[string]bool)
files := make(map[string]bool)
@@ -573,7 +613,7 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
files[name] = true
}
}
return fonts, images, spaces, slices.Sorted(maps.Keys(files))
return fonts, images, spaces, files
}
// page 写出页面尺寸、图层和对象