mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 13:56:32 +08:00
This commit is contained in:
1 parent
cfbaa8a177
commit
2c06e5ff69
43 files changed
+3142
-328
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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 检查包中各文档,避免改写其他文档签名保护的共享资源
|
||||
|
||||
+58
-11
@@ -26,12 +26,19 @@ type compressionImages struct {
|
||||
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,15 +61,11 @@ 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)
|
||||
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 }
|
||||
@@ -15,13 +15,10 @@
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"image"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
|
||||
// outputImageBatchLimit 限制并行批次持有的原始编码,超限图片单独处理
|
||||
@@ -38,7 +35,7 @@ type outputImageJob struct {
|
||||
// outputImageSource 保存已读取资源的摘要和格式,不保留原始编码
|
||||
type outputImageSource struct {
|
||||
key outputImageKey
|
||||
jpeg bool
|
||||
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])
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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.format != "" {
|
||||
formats[key] = source.format
|
||||
suffix := ".png"
|
||||
if source.format == "JPEG" {
|
||||
suffix = ".jpg"
|
||||
}
|
||||
if source.jpeg {
|
||||
formats[key] = "JPEG"
|
||||
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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
+56
-18
@@ -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)
|
||||
}
|
||||
kept, err := reader.documentFiles(ctx, others...)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for name := range kept {
|
||||
delete(removed, name)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
+159
-68
@@ -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
|
||||
}
|
||||
reader, err := source.Reader()
|
||||
return e.CopyObjectsContext(ctx, page, objects, options.DX, options.DY)
|
||||
}
|
||||
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")
|
||||
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] != "" && (local == nil || local.child(field[0]) == nil) {
|
||||
if field[1] != "" {
|
||||
fields = append(fields, editorXMLText(field[0], field[1])...)
|
||||
}
|
||||
}
|
||||
if local == nil {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+85
-14
@@ -35,7 +35,7 @@ const (
|
||||
editorReferenceFile
|
||||
)
|
||||
|
||||
// editorResourceRefs 保存全包资源标识和直接文件引用,不以页面是否加载判断资源存活
|
||||
// editorResourceRefs 记录资源标识、文件引用及声明依赖,不以页面是否加载判断资源存活
|
||||
type editorResourceRefs struct {
|
||||
ids map[string]bool
|
||||
files map[string]bool
|
||||
@@ -43,6 +43,7 @@ type editorResourceRefs struct {
|
||||
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,12 +77,14 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
|
||||
return nil, err
|
||||
}
|
||||
names := make(map[string]bool)
|
||||
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,20 +196,30 @@ 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"
|
||||
if file := node.child(field); file != nil {
|
||||
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 {
|
||||
if err := xml.Unmarshal(data[file.start:file.end], &value); err != nil {
|
||||
return nil, 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 {
|
||||
usage := usageRefs.fonts[id]
|
||||
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} {
|
||||
@@ -171,11 +232,18 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
|
||||
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
|
||||
}
|
||||
}
|
||||
} else {
|
||||
removed[location] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !keep {
|
||||
members = append(members, editorXMLPatch{start: node.start, end: node.end})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+80
-20
@@ -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
|
||||
}
|
||||
}
|
||||
collect(root)
|
||||
return nil
|
||||
}
|
||||
if err := collect(root); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
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)
|
||||
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 {
|
||||
delete(candidates, name)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
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
@@ -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}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
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) error
|
||||
collect = func(node *editorXML) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
var collect func(*editorXML)
|
||||
collect = func(node *editorXML) {
|
||||
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
|
||||
}
|
||||
}
|
||||
collect(body)
|
||||
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")
|
||||
}
|
||||
for len(queue) > 0 {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, nil, err
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
+56
-5
@@ -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,6 +239,9 @@ func (r *Renderer) walkComposite(object CompositeGraphicUnit, state RenderState,
|
||||
boundary = state.Parent.Multiply(boundary)
|
||||
}
|
||||
matrix := boundary.Multiply(NewMatrix(object.CTM))
|
||||
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)
|
||||
@@ -238,8 +267,14 @@ func (r *Renderer) walkComposite(object CompositeGraphicUnit, state RenderState,
|
||||
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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
+113
-31
@@ -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)
|
||||
}
|
||||
files, err := r.documentFiles(ctx, indexes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maps.Copy(shared, files)
|
||||
}
|
||||
versions, err := r.DocumentVersions()
|
||||
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)
|
||||
}
|
||||
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
@@ -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 写出页面尺寸、图层和对象
|
||||
|
||||
Reference in new issue
Block a user