mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-30 01:27:26 +08:00
This commit is contained in:
1 parent
cfbaa8a177
commit
2c06e5ff69
43 files changed
+3211
-397
No files matched your search
@@ -292,7 +292,7 @@
|
|||||||
<label for="createName">名称</label>
|
<label for="createName">名称</label>
|
||||||
<input id="createName" type="text" value="未命名" required autofocus>
|
<input id="createName" type="text" value="未命名" required autofocus>
|
||||||
</div>
|
</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="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 id="createDimensions" class="form-pair">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
@@ -466,7 +466,7 @@
|
|||||||
<form id="insertForm">
|
<form id="insertForm">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<label for="insertImage">图片</label>
|
<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>
|
||||||
<div id="insertFitRow" class="form-row" hidden>
|
<div id="insertFitRow" class="form-row" hidden>
|
||||||
<label for="insertFit">缩放</label>
|
<label for="insertFit">缩放</label>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ require (
|
|||||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||||
github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e
|
github.com/xiaoqidun/j2kgo v0.0.0-20261011021706-cedb3037ff6e
|
||||||
github.com/xiaoqidun/jbig2 v1.1.4
|
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/crypto v0.58.0
|
||||||
golang.org/x/image v0.47.0
|
golang.org/x/image v0.47.0
|
||||||
golang.org/x/text v0.43.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/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 h1:LF6J1si4JY+TszTh9+m4KAg9/ba0JjfXoGtQTQC2S8c=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.4/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA=
|
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-20261011103616-6cafccc7f8f6 h1:TW7d8qguwR6+WAxXw4ncAXdHTSrAgz6zjj7NmKbipyo=
|
||||||
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/go.mod h1:hkZn8gHOI94K0fE2ynbXaE8J5T0/jCvoTS/yroKgb+E=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||||
|
|||||||
+25
-11
@@ -25,7 +25,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
|
||||||
"image/color"
|
"image/color"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@@ -1570,11 +1569,11 @@ func writeAnnotation(args []js.Value) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
config, _, err := image.DecodeConfig(bytes.NewReader(data))
|
size, err := currentEditor.ImageSize(resource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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" {
|
if options.Kind == "replace" {
|
||||||
style := ofdgo.TextStyle{Font: font}
|
style := ofdgo.TextStyle{Font: font}
|
||||||
@@ -3297,15 +3296,16 @@ func importPages(args []js.Value) (any, error) {
|
|||||||
}
|
}
|
||||||
at := args[1].Int()
|
at := args[1].Int()
|
||||||
options := ofdgo.PageImportOptions{Outlines: len(args) > 2 && args[2].Bool()}
|
options := ofdgo.PageImportOptions{Outlines: len(args) > 2 && args[2].Bool()}
|
||||||
|
ctx := context.Background()
|
||||||
progressIndex := 3
|
progressIndex := 3
|
||||||
if len(args) > 3 && args[3].Type() == js.TypeBoolean {
|
if len(args) > 3 && args[3].Type() == js.TypeBoolean {
|
||||||
options.AllowDecrypted = args[3].Bool()
|
options.AllowDecrypted = args[3].Bool()
|
||||||
progressIndex = 4
|
progressIndex = 4
|
||||||
}
|
}
|
||||||
if len(args) > progressIndex {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
clearImport()
|
clearImport()
|
||||||
@@ -4103,12 +4103,12 @@ func insertImage(args []js.Value) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
config, _, err := image.DecodeConfig(bytes.NewReader(data))
|
size, err := currentEditor.ImageSize(resourceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
width := args[4].Float()
|
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())
|
content, err := currentSession.pageContent(args[0].Int())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -4593,11 +4593,11 @@ func compressionFromJS(value js.Value, options *ofdgo.CompressionOptions) error
|
|||||||
return options.Validate()
|
return options.Validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// outputContext 定期让出浏览器线程,将取消请求传入库层压缩循环
|
// outputContext 定期让出浏览器线程,将取消请求传入库层长操作
|
||||||
type outputContext struct {
|
type outputContext struct {
|
||||||
context.Context
|
context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
checkpoint js.Value
|
checkpoint func() error
|
||||||
checked time.Time
|
checked time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4606,7 +4606,21 @@ type outputContext struct {
|
|||||||
// 返回: context.Context 导出上下文
|
// 返回: context.Context 导出上下文
|
||||||
func newOutputContext(checkpoint js.Value) context.Context {
|
func newOutputContext(checkpoint js.Value) context.Context {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
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 在取消检查间隙处理浏览器消息,不为每个像素触发跨语言回调
|
// Err 在取消检查间隙处理浏览器消息,不为每个像素触发跨语言回调
|
||||||
@@ -4617,7 +4631,7 @@ func (c *outputContext) Err() error {
|
|||||||
}
|
}
|
||||||
if time.Since(c.checked) >= 50*time.Millisecond {
|
if time.Since(c.checked) >= 50*time.Millisecond {
|
||||||
c.checked = time.Now()
|
c.checked = time.Now()
|
||||||
if err := awaitExport(c.checkpoint); err != nil {
|
if err := c.checkpoint(); err != nil {
|
||||||
c.cancel()
|
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})
|
return snapshot.WriteTo(convertWriter{context: ctx, writer: writer, count: &count})
|
||||||
}
|
}
|
||||||
|
|
||||||
// WritePagesToWithOptions 按指定顺序另存页面并应用输出压缩
|
// WritePagesToWithOptions 按指定顺序另存页面,仅为所选页面准备资源和压缩计划
|
||||||
// 入参: ctx 取消上下文, writer 输出流, indexes 零基页码, options 输出配置
|
// 入参: ctx 取消上下文, writer 输出流, indexes 零基页码, options 输出配置
|
||||||
// 返回: int64 已写入字节数, error 错误信息
|
// 返回: int64 已写入字节数, error 错误信息
|
||||||
func (e *Editor) WritePagesToWithOptions(ctx context.Context, writer io.Writer, indexes []int, options WriteOptions) (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 := ctx.Err(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
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
|
var count int64
|
||||||
return snapshot.WritePagesTo(convertWriter{context: ctx, writer: writer, count: &count}, indexes)
|
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)
|
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 {
|
if options.Compression.Mode == CompressionUnchanged {
|
||||||
return &snapshot, nil
|
return &snapshot, nil
|
||||||
}
|
}
|
||||||
for _, resource := range e.resources {
|
|
||||||
if resource.image != nil {
|
|
||||||
snapshot.output.images[cleanPackagePath(resource.name)] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if e.source != nil {
|
if e.source != nil {
|
||||||
protected, err := e.source.reader.hasOutputSignatures()
|
protected, err := e.source.reader.hasOutputSignatures()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -146,19 +146,11 @@ func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Edi
|
|||||||
}
|
}
|
||||||
snapshot.output.protected = protected
|
snapshot.output.protected = protected
|
||||||
if protected {
|
if protected {
|
||||||
snapshot.output.images = nil
|
|
||||||
return &snapshot, 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 {
|
if options.Compression.Mode == CompressionLossy {
|
||||||
reader, err := e.reader(editorProgress(snapshot.OnWriteProgress))
|
reader, err := e.readerContext(ctx, editorProgress(snapshot.OnWriteProgress))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -174,7 +166,7 @@ func (e *Editor) outputSnapshot(ctx context.Context, options WriteOptions) (*Edi
|
|||||||
return &snapshot, nil
|
return &snapshot, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// compressionImageSafety 保护蒙版及其原图和替换图像,识别不能可靠降采样的图案及隐藏内容
|
// compressionImageSafety 保护蒙版及其原图和替换图像,不完整的文档结构不参与降采样
|
||||||
// 入参: ctx 取消上下文, images 图片资源
|
// 入参: ctx 取消上下文, images 图片资源
|
||||||
// 返回: map[string]bool 只能无损处理的资源路径, bool 是否可分析尺寸, error 读取或取消错误
|
// 返回: map[string]bool 只能无损处理的资源路径, bool 是否可分析尺寸, error 读取或取消错误
|
||||||
func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo) (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)
|
masks := make(map[string]bool)
|
||||||
geometry := true
|
|
||||||
for _, name := range slices.Sorted(maps.Keys(names)) {
|
for _, name := range slices.Sorted(maps.Keys(names)) {
|
||||||
if !safe {
|
if !safe {
|
||||||
break
|
break
|
||||||
@@ -248,17 +239,11 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo)
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
stack = append(stack, element.Name)
|
stack = append(stack, element.Name)
|
||||||
if element.Name.Local == "Pattern" {
|
|
||||||
geometry = false
|
|
||||||
}
|
|
||||||
resource, substitution, masked := "", "", false
|
resource, substitution, masked := "", "", false
|
||||||
for _, attr := range element.Attr {
|
for _, attr := range element.Attr {
|
||||||
if attr.Name.Space != "" {
|
if attr.Name.Space != "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if attr.Name.Local == "Visible" && (attr.Value == "false" || attr.Value == "0") {
|
|
||||||
geometry = false
|
|
||||||
}
|
|
||||||
if attr.Name.Local == "ResourceID" {
|
if attr.Name.Local == "ResourceID" {
|
||||||
resource = editorResourceID(attr.Value)
|
resource = editorResourceID(attr.Value)
|
||||||
}
|
}
|
||||||
@@ -278,6 +263,8 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo)
|
|||||||
}
|
}
|
||||||
case xml.EndElement:
|
case xml.EndElement:
|
||||||
stack = stack[:len(stack)-1]
|
stack = stack[:len(stack)-1]
|
||||||
|
case xml.Directive:
|
||||||
|
safe = false
|
||||||
}
|
}
|
||||||
if !safe {
|
if !safe {
|
||||||
break
|
break
|
||||||
@@ -295,7 +282,7 @@ func (r *Reader) compressionImageSafety(ctx context.Context, images []ImageInfo)
|
|||||||
paths[cleanPackagePath(img.Location)] = true
|
paths[cleanPackagePath(img.Location)] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return paths, safe && geometry, ctx.Err()
|
return paths, safe, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasOutputSignatures 检查包中各文档,避免改写其他文档签名保护的共享资源
|
// hasOutputSignatures 检查包中各文档,避免改写其他文档签名保护的共享资源
|
||||||
|
|||||||
+63
-16
@@ -23,15 +23,22 @@ import (
|
|||||||
|
|
||||||
// compressionImages 记录图片各处使用的最大像素需求
|
// compressionImages 记录图片各处使用的最大像素需求
|
||||||
type compressionImages struct {
|
type compressionImages struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
dpi int
|
dpi int
|
||||||
sizes map[string]image.Point
|
sizes map[string]image.Point
|
||||||
|
renderer *Renderer
|
||||||
|
patternPage *Matrix
|
||||||
|
patterns map[*Pattern]bool
|
||||||
|
patternVisits map[compressionPatternKey]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// compressionImagePlan 分文档分析标识,按共享路径合并最大尺寸和保护条件
|
// compressionImagePlan 为有损输出分文档分析图片,按共享路径合并最大尺寸和保护条件
|
||||||
// 入参: ctx 取消上下文, options 压缩配置
|
// 入参: ctx 取消上下文, options 压缩配置
|
||||||
// 返回: map[string]bool 可有损处理的路径, map[string]image.Point 像素需求, error 读取或取消错误
|
// 返回: 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) {
|
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)
|
images := make(map[string]bool)
|
||||||
sizes := make(map[string]image.Point)
|
sizes := make(map[string]image.Point)
|
||||||
blocked := make(map[string]bool)
|
blocked := make(map[string]bool)
|
||||||
@@ -54,14 +61,10 @@ func (r *Reader) compressionImagePlan(ctx context.Context, options CompressionOp
|
|||||||
}
|
}
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
var masks map[string]bool
|
|
||||||
var demands map[string]image.Point
|
var demands map[string]image.Point
|
||||||
if options.Mode == CompressionLossy {
|
masks, geometry, err := document.compressionImageSafety(ctx, catalog)
|
||||||
var geometry bool
|
if err == nil && geometry {
|
||||||
masks, geometry, err = document.compressionImageSafety(ctx, catalog)
|
demands, err = document.compressionImageSizes(ctx, catalog, options.ImageDPI())
|
||||||
if err == nil && geometry {
|
|
||||||
demands, err = document.compressionImageSizes(ctx, catalog, options.ImageDPI())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if document != r {
|
if document != r {
|
||||||
document.Close()
|
document.Close()
|
||||||
@@ -102,7 +105,7 @@ func compressionImageSize(width, height float64, dpi int) image.Point {
|
|||||||
return image.Pt(int(w), int(h))
|
return image.Pt(int(w), int(h))
|
||||||
}
|
}
|
||||||
|
|
||||||
// compressionImageSizes 按正文、模板及注解的实际变换汇总共享图片尺寸
|
// compressionImageSizes 按正文、模板、注解及底纹的实际变换汇总图片尺寸,包括隐藏内容
|
||||||
// 入参: ctx 取消上下文, images 图片资源, dpi 分辨率上限
|
// 入参: ctx 取消上下文, images 图片资源, dpi 分辨率上限
|
||||||
// 返回: map[string]image.Point 资源像素需求, error 取消错误
|
// 返回: map[string]image.Point 资源像素需求, error 取消错误
|
||||||
func (r *Reader) compressionImageSizes(ctx context.Context, images []ImageInfo, dpi int) (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 {
|
if err != nil {
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
renderer := NewRenderer(r)
|
renderer := NewRenderer(r, WithRenderBackends(RenderBackends{}))
|
||||||
defer renderer.ClearCache()
|
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++ {
|
for index := 0; index < count; index++ {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -157,14 +160,21 @@ func (r *Reader) compressionImageSizes(ctx context.Context, images []ImageInfo,
|
|||||||
return result, ctx.Err()
|
return result, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// DrawObject 汇总原图和替换图像的完整显示尺寸,不使用裁剪后的局部面积
|
// DrawObject 汇总图片、替换图及文字和路径底纹的尺寸,不使用裁剪后的局部面积
|
||||||
// 入参: object 图形对象, state 继承状态
|
// 入参: object 图形对象, state 继承状态
|
||||||
// 返回: error 取消或几何错误
|
// 返回: error 取消或几何错误
|
||||||
func (v *compressionImages) DrawObject(object *GraphicObject, state RenderState) error {
|
func (v *compressionImages) DrawObject(object *GraphicObject, state RenderState) error {
|
||||||
if err := v.ctx.Err(); err != nil {
|
if err := v.ctx.Err(); err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
img := object.ImageObject
|
img := object.ImageObject
|
||||||
@@ -182,6 +192,35 @@ func (v *compressionImages) DrawObject(object *GraphicObject, state RenderState)
|
|||||||
}
|
}
|
||||||
matrix, _ := renderObjectMatrix(img.Boundary, local, state)
|
matrix, _ := renderObjectMatrix(img.Boundary, local, state)
|
||||||
size := compressionImageSize(math.Hypot(matrix.a, matrix.b), math.Hypot(matrix.c, matrix.d), v.dpi)
|
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 {
|
if size.X == 0 || size.Y == 0 {
|
||||||
return fmt.Errorf("invalid image placement")
|
return fmt.Errorf("invalid image placement")
|
||||||
}
|
}
|
||||||
@@ -199,3 +238,11 @@ func (v *compressionImages) DrawObject(object *GraphicObject, state RenderState)
|
|||||||
// 入参: stamp 印章
|
// 入参: stamp 印章
|
||||||
// 返回: error 始终为空
|
// 返回: error 始终为空
|
||||||
func (v *compressionImages) DrawStamp(stamp Stamp) error { return nil }
|
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
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"image"
|
"image"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// outputImageBatchLimit 限制并行批次持有的原始编码,超限图片单独处理
|
// outputImageBatchLimit 限制并行批次持有的原始编码,超限图片单独处理
|
||||||
@@ -37,8 +34,8 @@ type outputImageJob struct {
|
|||||||
|
|
||||||
// outputImageSource 保存已读取资源的摘要和格式,不保留原始编码
|
// outputImageSource 保存已读取资源的摘要和格式,不保留原始编码
|
||||||
type outputImageSource struct {
|
type outputImageSource struct {
|
||||||
key outputImageKey
|
key outputImageKey
|
||||||
jpeg bool
|
format string
|
||||||
}
|
}
|
||||||
|
|
||||||
// outputImageOptions 统一资源压缩及像素需求,受保护图片只进行无损编码
|
// outputImageOptions 统一资源压缩及像素需求,受保护图片只进行无损编码
|
||||||
@@ -92,7 +89,7 @@ func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, e
|
|||||||
options, demand := e.outputImageOptions(key)
|
options, demand := e.outputImageOptions(key)
|
||||||
cacheKey := outputImageKey{hash: sha256.Sum256(data), options: options, size: demand}
|
cacheKey := outputImageKey{hash: sha256.Sum256(data), options: options, size: demand}
|
||||||
if sources != nil {
|
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 {
|
if _, cached := results[cacheKey]; cached {
|
||||||
continue
|
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 {
|
func (e *Editor) runOutputImages(jobs []outputImageJob, results map[outputImageKey][]byte) error {
|
||||||
ctx := e.output.ctx
|
ctx := e.output.ctx
|
||||||
optimize := func(job *outputImageJob) {
|
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 {
|
if len(jobs) == 1 {
|
||||||
optimize(&jobs[0])
|
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"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/xiaoqidun/pdfgo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// outputImageKey 区分相同图像的压缩配置和像素需求,不跨写出保留状态
|
// outputImageKey 区分相同图像的压缩配置和像素需求,不跨写出保留状态
|
||||||
@@ -39,6 +37,13 @@ type outputImageKey struct {
|
|||||||
// 入参: parts 输出改动, reader 原包,可为空, removed 已移除条目
|
// 入参: parts 输出改动, reader 原包,可为空, removed 已移除条目
|
||||||
// 返回: error 读取或取消错误
|
// 返回: error 读取或取消错误
|
||||||
func (e *Editor) compressResourceParts(parts map[string][]byte, reader *Reader, removed map[string]bool) 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)
|
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]
|
candidate, cached := imageResults[cacheKey]
|
||||||
var err error
|
var err error
|
||||||
if !cached {
|
if !cached {
|
||||||
candidate, err = pdfgo.OptimizeImageResource(e.output.ctx, data, options, demand)
|
candidate, err = optimizeImageResource(e.output.ctx, data, options, demand)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
imageResults[cacheKey] = nil
|
imageResults[cacheKey] = nil
|
||||||
if len(candidate) < len(data) {
|
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)
|
source.key.hash = sha256.Sum256(data)
|
||||||
}
|
}
|
||||||
if data != nil {
|
if data != nil {
|
||||||
source.jpeg = bytes.HasPrefix(data, []byte{255, 216})
|
source.format = outputImageFormat(data)
|
||||||
}
|
}
|
||||||
if source.jpeg {
|
if source.format != "" {
|
||||||
formats[key] = "JPEG"
|
formats[key] = source.format
|
||||||
|
suffix := ".png"
|
||||||
|
if source.format == "JPEG" {
|
||||||
|
suffix = ".jpg"
|
||||||
|
}
|
||||||
ext := path.Ext(actual[key])
|
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 {
|
if data == nil {
|
||||||
data, err = read(actual[key])
|
data, err = read(actual[key])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -258,9 +267,9 @@ func (e *Editor) compressResourceReferences(parts map[string][]byte, reader *Rea
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
stem := strings.TrimSuffix(actual[key], ext)
|
stem := strings.TrimSuffix(actual[key], ext)
|
||||||
name := stem + ".jpg"
|
name := stem + suffix
|
||||||
for n := 1; reserved[name]; n++ {
|
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
|
reserved[name] = true
|
||||||
locations[key] = name
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@@ -256,6 +257,16 @@ func (e *Editor) CopyPage(index int) (int, error) {
|
|||||||
// 入参: indexes 不重复的原页面索引
|
// 入参: indexes 不重复的原页面索引
|
||||||
// 返回: []int 副本页面索引, error 错误信息
|
// 返回: []int 副本页面索引, error 错误信息
|
||||||
func (e *Editor) CopyPages(indexes []int) ([]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 {
|
if err := e.validatePageIndexes(indexes); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -263,11 +274,12 @@ func (e *Editor) CopyPages(indexes []int) ([]int, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
at := len(e.pages)
|
at := len(e.pages)
|
||||||
source, err := e.Reader()
|
source, err := e.readerContext(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
result := make([]int, len(indexes))
|
result := make([]int, len(indexes))
|
||||||
@@ -456,7 +468,7 @@ func (e *Editor) ResizePages(indexes []int, width, height float64) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddImage 注册PNG、JPEG或JBIG2图片,重复资源复用标识,引用后写入文档
|
// AddImage 注册PNG、JPEG、BMP、单页TIFF或JBIG2图片,重复资源复用标识,引用后写入文档
|
||||||
// 不透明纯黑白PNG仅在无损JBIG2编码更小时转换,其他图片保留原始编码
|
// 不透明纯黑白PNG仅在无损JBIG2编码更小时转换,其他图片保留原始编码
|
||||||
// 入参: data 图片数据
|
// 入参: data 图片数据
|
||||||
// 返回: string 图片资源标识, error 错误信息
|
// 返回: string 图片资源标识, error 错误信息
|
||||||
@@ -464,11 +476,18 @@ func (e *Editor) AddImage(data []byte) (string, error) {
|
|||||||
return e.addDecodedImage(data, nil)
|
return e.addDecodedImage(data, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ImageSize 获取图片资源的像素尺寸,优先复用注册结果,不解码像素
|
||||||
|
// 入参: id 图片资源标识
|
||||||
|
// 返回: image.Point 像素宽高, error 资源或格式错误
|
||||||
|
func (e *Editor) ImageSize(id string) (image.Point, error) {
|
||||||
|
return e.editorImage(id)
|
||||||
|
}
|
||||||
|
|
||||||
// addDecodedImage 注册图片并复用与编码数据一致的像素
|
// addDecodedImage 注册图片并复用与编码数据一致的像素
|
||||||
// 入参: data 图片数据, decoded 已解码像素,为nil时按需解码
|
// 入参: data 图片数据, decoded 已解码像素,为nil时按需解码
|
||||||
// 返回: string 资源标识, error 注册错误
|
// 返回: string 资源标识, error 注册错误
|
||||||
func (e *Editor) addDecodedImage(data []byte, decoded image.Image) (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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -479,8 +498,8 @@ func (e *Editor) addDecodedImage(data []byte, decoded image.Image) (string, erro
|
|||||||
// 入参: data 图片编码数据, config 图片配置, format 图片格式, decoded 已解码图像,可为nil
|
// 入参: data 图片编码数据, config 图片配置, format 图片格式, decoded 已解码图像,可为nil
|
||||||
// 返回: string 资源标识, error 格式或资源错误
|
// 返回: string 资源标识, error 格式或资源错误
|
||||||
func (e *Editor) addImage(data []byte, config image.Config, format string, decoded image.Image) (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 {
|
if (format != "png" && format != "jpeg" && format != "bmp" && format != "tiff" && format != "jbig2") || config.Width <= 0 || config.Height <= 0 {
|
||||||
return "", fmt.Errorf("only PNG, JPEG and JBIG2 images are supported")
|
return "", fmt.Errorf("only PNG, JPEG, BMP, single-page TIFF and JBIG2 images are supported")
|
||||||
}
|
}
|
||||||
key := editorResourceKey{checksum: sha256.Sum256(data), index: -1}
|
key := editorResourceKey{checksum: sha256.Sum256(data), index: -1}
|
||||||
if id, ok := e.resourceID[key]; ok {
|
if id, ok := e.resourceID[key]; ok {
|
||||||
|
|||||||
+59
-21
@@ -113,17 +113,15 @@ func (e *Editor) AddDocument(title string, width, height float64, at int) (int,
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
err := e.editDocuments(func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
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 := ""
|
directory := ""
|
||||||
for number := 0; ; number++ {
|
for number := 0; ; number++ {
|
||||||
candidate := fmt.Sprintf("Doc_%d", number)
|
candidate := fmt.Sprintf("Doc_%d", number)
|
||||||
occupied := false
|
if !occupied[candidate] {
|
||||||
for name := range reader.fileNames {
|
|
||||||
if name == candidate || strings.HasPrefix(name, candidate+"/") {
|
|
||||||
occupied = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !occupied {
|
|
||||||
directory = candidate
|
directory = candidate
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -219,14 +217,25 @@ func (e *Editor) MoveDocument(from, to int) error {
|
|||||||
// 入参: index 文档索引,从0开始
|
// 入参: index 文档索引,从0开始
|
||||||
// 返回: error 删除错误
|
// 返回: error 删除错误
|
||||||
func (e *Editor) DeleteDocument(index int) 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() {
|
if index < 0 || index >= e.DocumentCount() {
|
||||||
return fmt.Errorf("document index %d out of range", index)
|
return fmt.Errorf("document index %d out of range", index)
|
||||||
}
|
}
|
||||||
if e.DocumentCount() == 1 {
|
if e.DocumentCount() == 1 {
|
||||||
return fmt.Errorf("package must contain at least one document")
|
return fmt.Errorf("package must contain at least one document")
|
||||||
}
|
}
|
||||||
return e.editDocuments(func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
return e.editDocumentsContext(ctx, func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
||||||
removed, err := reader.documentFiles(context.Background(), index)
|
removed, err := reader.documentFiles(ctx, index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
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+"/")
|
return name != cleanPackagePath(reader.OFD.DocBody[index].DocRoot) && strings.HasPrefix(name, directory+"/")
|
||||||
}) {
|
}) {
|
||||||
for name := range reader.fileNames {
|
for name := range reader.fileNames {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
if strings.HasPrefix(name, directory+"/") {
|
if strings.HasPrefix(name, directory+"/") {
|
||||||
removed[name] = true
|
removed[name] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var others []int
|
||||||
for other := range reader.OFD.DocBody {
|
for other := range reader.OFD.DocBody {
|
||||||
if other == index {
|
if other != index {
|
||||||
continue
|
others = append(others, other)
|
||||||
}
|
}
|
||||||
kept, err := reader.documentFiles(context.Background(), other)
|
}
|
||||||
if err != nil {
|
kept, err := reader.documentFiles(ctx, others...)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
for name := range kept {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
for name := range kept {
|
delete(removed, name)
|
||||||
delete(removed, name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
delete(removed, "OFD.xml")
|
delete(removed, "OFD.xml")
|
||||||
for name := range reader.files {
|
for name := range reader.files {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
if removed[name] {
|
if removed[name] {
|
||||||
delete(reader.files, name)
|
delete(reader.files, name)
|
||||||
}
|
}
|
||||||
@@ -280,15 +299,26 @@ func (e *Editor) DeleteDocument(index int) error {
|
|||||||
// 入参: edit 修改包快照并返回新的当前文档索引
|
// 入参: edit 修改包快照并返回新的当前文档索引
|
||||||
// 返回: error 修改错误
|
// 返回: error 修改错误
|
||||||
func (e *Editor) editDocuments(edit func(*Reader, []byte, *editorXML) (int, error)) 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
data, err := reader.readFile("OFD.xml")
|
data, err := reader.readFileViewContext(ctx, "OFD.xml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -298,6 +328,9 @@ func (e *Editor) editDocuments(edit func(*Reader, []byte, *editorXML) (int, erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
before := e.transactionSnapshot()
|
before := e.transactionSnapshot()
|
||||||
reader.documentIndex, reader.doc = index, nil
|
reader.documentIndex, reader.doc = index, nil
|
||||||
if err := reader.initRoot(); err != 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 {
|
if _, err := reader.Doc(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := e.adoptDocumentReader(reader); err != nil {
|
next := *e
|
||||||
|
if err := next.adoptDocumentReader(reader); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*e = next
|
||||||
e.recordTransaction(before)
|
e.recordTransaction(before)
|
||||||
return nil
|
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
|
return clips != nil || fill != nil && fill.Pattern != nil || stroke != nil && stroke.Pattern != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// subsetFonts 收集新增字体实际使用的字形,仅裁剪保存结果,保留完整编辑字体
|
// subsetFonts 仅解析实际引用的新增字体,按用字裁剪保存结果,保留完整编辑字体
|
||||||
// 入参: progress 保存进度回调
|
// 入参: progress 保存进度回调
|
||||||
// 返回: map[string][]byte 包内字体子集, error 错误信息
|
// 返回: map[string][]byte 包内字体子集, error 错误信息
|
||||||
func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) {
|
func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) {
|
||||||
if e.backends.FontResources == nil {
|
if e.backends.FontResources == nil {
|
||||||
return nil, 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)
|
used := make(map[string]map[uint16]bool)
|
||||||
mapped := make(map[string]bool)
|
mapped := make(map[string]bool)
|
||||||
for _, resource := range e.resources {
|
for i, resource := range fonts {
|
||||||
if resource.font == nil || resource.font.FontFile == "" {
|
if err := progress.report("fonts", i, total); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resource.font.FontFile == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
resourceFont := e.fonts[resource.font.ID]
|
resourceFont := e.fonts[resource.font.ID]
|
||||||
@@ -202,7 +215,10 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(used) == 0 {
|
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)}
|
refs := editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool)}
|
||||||
nested := false
|
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 {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, layer := range page.Content.Layer {
|
for _, layer := range page.Content.Layer {
|
||||||
@@ -278,12 +293,11 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if nested {
|
if nested {
|
||||||
_, _, _, files := e.usedResources()
|
|
||||||
for i, resource := range e.resources {
|
for i, resource := range e.resources {
|
||||||
if resource.definition() == "" || !slices.Contains(files, resource.name) {
|
if resource.definition() == "" || !files[resource.name] {
|
||||||
continue
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if resource.usage.matches(resource.name, resource.data) {
|
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)
|
result := make(map[string][]byte)
|
||||||
for i := range e.resources {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
resource := &e.resources[i]
|
resource := &e.resources[i]
|
||||||
|
|||||||
+10
-5
@@ -40,7 +40,7 @@ type ImagePageOptions struct {
|
|||||||
OnProgress func(stage string, completed, total int) error
|
OnProgress func(stage string, completed, total int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportImages 将PNG、JPEG或JBIG2逐张建页并插入指定位置,一次操作计入一条撤销记录
|
// ImportImages 将PNG、JPEG、BMP、单页TIFF或JBIG2逐张建页并插入指定位置,一次操作计入一条撤销记录
|
||||||
// 保留像素及透明度,方向通过对象变换实现,不重新采样;图片压缩沿用AddImage规则
|
// 保留像素及透明度,方向通过对象变换实现,不重新采样;图片压缩沿用AddImage规则
|
||||||
// 失败或取消时不修改文档,读取期间仍需容纳单张图片的解码数据
|
// 失败或取消时不修改文档,读取期间仍需容纳单张图片的解码数据
|
||||||
// 入参: ctx 取消上下文, sources 按页面顺序排列的图片, at 插入位置, options 建页选项
|
// 入参: ctx 取消上下文, sources 按页面顺序排列的图片, at 插入位置, options 建页选项
|
||||||
@@ -110,14 +110,19 @@ func (e *Editor) ImportImages(ctx context.Context, sources []ImagePageSource, at
|
|||||||
// 入参: data 图片数据, options 建页选项
|
// 入参: data 图片数据, options 建页选项
|
||||||
// 返回: int 从0开始的页面索引, error 错误信息
|
// 返回: int 从0开始的页面索引, error 错误信息
|
||||||
func (e *Editor) addImagePage(data []byte, options ImagePageOptions) (int, 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 {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
if format != "png" && format != "jpeg" && format != "jbig2" {
|
if format != "png" && format != "jpeg" && format != "bmp" && format != "tiff" && format != "jbig2" {
|
||||||
return 0, fmt.Errorf("only PNG, JPEG and JBIG2 images are supported")
|
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 {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
+163
-72
@@ -32,15 +32,19 @@ type editorImportEntry struct {
|
|||||||
name, base, group string
|
name, base, group string
|
||||||
data []byte
|
data []byte
|
||||||
node *editorXML
|
node *editorXML
|
||||||
|
template bool
|
||||||
|
ids map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorPageImport 一次插页操作的独立资源图,提交前不修改目标文档
|
// editorPageImport 一次插页操作的独立资源图,提交前不修改目标文档
|
||||||
type editorPageImport struct {
|
type editorPageImport struct {
|
||||||
|
ctx context.Context
|
||||||
reader *Reader
|
reader *Reader
|
||||||
doc *Document
|
doc *Document
|
||||||
directory string
|
directory string
|
||||||
target *Reader
|
target *Reader
|
||||||
paths map[string]string
|
paths map[string]string
|
||||||
|
templatePaths map[string]string
|
||||||
evidence map[string]string
|
evidence map[string]string
|
||||||
resourceXML []byte
|
resourceXML []byte
|
||||||
pageParts []byte
|
pageParts []byte
|
||||||
@@ -95,6 +99,17 @@ type editorProgressReader struct {
|
|||||||
// 入参: source 来源编辑器, sourcePage 来源页, ids 对象标识, page 目标页, options 迁移选项
|
// 入参: source 来源编辑器, sourcePage 来源页, ids 对象标识, page 目标页, options 迁移选项
|
||||||
// 返回: []string 新对象标识, error 错误信息
|
// 返回: []string 新对象标识, error 错误信息
|
||||||
func (e *Editor) ImportObjects(source *Editor, sourcePage int, ids []string, page int, options ObjectImportOptions) ([]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) {
|
if !finite(options.DX) || !finite(options.DY) {
|
||||||
return nil, fmt.Errorf("import requires finite offsets")
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if source == e {
|
if source == e {
|
||||||
return e.CopyObjects(page, objects, options.DX, options.DY)
|
if err := progress.report("commit", 0, 0); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return e.CopyObjectsContext(ctx, page, objects, options.DX, options.DY)
|
||||||
}
|
}
|
||||||
reader, err := source.Reader()
|
reader, err := source.readerContext(ctx, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer reader.Close()
|
||||||
doc, err := reader.Doc()
|
doc, err := reader.Doc()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ref := doc.Pages.Page[sourcePage]
|
ref := doc.Pages.Page[sourcePage]
|
||||||
progress := editorProgress(options.OnProgress)
|
base, migration, err := e.prepareImport(ctx, reader, doc, map[string]bool{ref.ID: true}, false, progress)
|
||||||
base, migration, err := e.prepareImport(reader, doc, map[string]bool{ref.ID: true}, false, progress)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -169,7 +187,7 @@ func (e *Editor) ImportObjects(source *Editor, sourcePage int, ids []string, pag
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -207,7 +225,7 @@ func (e *Editor) ImportObjects(source *Editor, sourcePage int, ids []string, pag
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
layer := content.Layer[0]
|
layer := content.Layer[0]
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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].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}
|
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 err
|
||||||
})
|
})
|
||||||
return copied, err
|
return copied, err
|
||||||
@@ -244,10 +262,21 @@ func (e *Editor) ImportPages(source *Reader, indexes []int, at int) ([]string, e
|
|||||||
// 入参: source 来源阅读器, indexes 来源页面索引, at 目标插入位置, options 导入选项
|
// 入参: source 来源阅读器, indexes 来源页面索引, at 目标插入位置, options 导入选项
|
||||||
// 返回: []string 新页面标识, error 错误信息
|
// 返回: []string 新页面标识, error 错误信息
|
||||||
func (e *Editor) ImportPagesWithOptions(source *Reader, indexes []int, at int, options PageImportOptions) ([]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 {
|
if source.encryption != nil && e.encryption == nil && !options.AllowDecrypted {
|
||||||
return nil, ErrEncryptionPolicyRequired
|
return nil, ErrEncryptionPolicyRequired
|
||||||
}
|
}
|
||||||
return e.importPages(source, indexes, at, false, options)
|
return e.importPages(ctx, source, indexes, at, false, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read 读取数据并在下一块开始前检查取消
|
// Read 读取数据并在下一块开始前检查取消
|
||||||
@@ -263,9 +292,12 @@ func (r *editorProgressReader) Read(data []byte) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// importPages 迁移页面,同文档复制时复用原资源并保留其他页面的跳转
|
// importPages 迁移页面,同文档复制时复用原资源并保留其他页面的跳转
|
||||||
// 入参: source 来源阅读器, indexes 来源页索引, at 插入位置, copyPage 是否同文档复制, options 导入选项
|
// 入参: ctx 取消上下文, source 来源阅读器, indexes 来源页索引, at 插入位置, copyPage 是否同文档复制, options 导入选项
|
||||||
// 返回: []string 新页标识, error 错误信息
|
// 返回: []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) {
|
if at < 0 || at > len(e.pages) {
|
||||||
return nil, fmt.Errorf("page index %d out of range", at)
|
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 {
|
if len(indexes) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
progress := editorProgress(options.OnProgress)
|
progress := editorProgress(options.OnProgress).withContext(ctx)
|
||||||
base, m, err := e.prepareImport(source, doc, selected, copyPage, progress)
|
base, m, err := e.prepareImport(ctx, source, doc, selected, copyPage, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -300,7 +332,7 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
|||||||
}
|
}
|
||||||
page := doc.Pages.Page[index]
|
page := doc.Pages.Page[index]
|
||||||
ids[i] = m.id(page.ID)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -328,7 +360,7 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
outlines, err = e.appendImportedOutlines(imported)
|
outlines, err = e.appendImportedOutlines(ctx, imported)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -353,6 +385,9 @@ func (e *Editor) importPages(source *Reader, indexes []int, at int, copyPage boo
|
|||||||
preview := *e
|
preview := *e
|
||||||
preview.source, preview.pages = next, added
|
preview.source, preview.pages = next, added
|
||||||
for i, index := range indexes {
|
for i, index := range indexes {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
states := make(map[string]editorCompositeState)
|
states := make(map[string]editorCompositeState)
|
||||||
groups := make(map[string]map[string]editorCompositeState)
|
groups := make(map[string]map[string]editorCompositeState)
|
||||||
for _, layer := range e.pages[index].Content.Layer {
|
for _, layer := range e.pages[index].Content.Layer {
|
||||||
@@ -433,9 +468,9 @@ func (e *Editor) bindImportResources() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// prepareImport 建立页面和对象迁移共用的资源图,不修改目标编辑器
|
// prepareImport 建立页面和对象迁移共用的资源图,不修改目标编辑器
|
||||||
// 入参: source 来源, doc 来源文档, selected 页面映射范围, copyPage 是否同文档, progress 进度
|
// 入参: ctx 取消上下文, source 来源, doc 来源文档, selected 页面映射范围, copyPage 是否同文档, progress 进度
|
||||||
// 返回: *editorSource 目标基础, *editorPageImport 资源图, error 错误信息
|
// 返回: *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)
|
maximum, err := e.sourceMaxID(progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@@ -447,7 +482,7 @@ func (e *Editor) prepareImport(source *Reader, doc *Document, selected map[strin
|
|||||||
return nil, nil, err
|
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)),
|
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),
|
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)}
|
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 {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
m.documentRoot, err = parseEditorXML(m.documentXML)
|
m.documentRoot, err = parseEditorXMLContext(ctx, m.documentXML)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -501,6 +536,9 @@ func (m *editorPageImport) encodeResources() error {
|
|||||||
for _, group := range []string{"ColorSpaces", "DrawParams", "Fonts", "MultiMedias", "CompositeGraphicUnits"} {
|
for _, group := range []string{"ColorSpaces", "DrawParams", "Fonts", "MultiMedias", "CompositeGraphicUnits"} {
|
||||||
var entries []byte
|
var entries []byte
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
|
if err := m.ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if m.resources[id].group == group {
|
if m.resources[id].group == group {
|
||||||
entries = append(entries, m.used[id]...)
|
entries = append(entries, m.used[id]...)
|
||||||
}
|
}
|
||||||
@@ -542,7 +580,7 @@ func (m *editorPageImport) outlines() ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(m.ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -591,7 +629,7 @@ func (m *editorPageImport) outlines() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
data = editorPatchXML(data, patches)
|
data = editorPatchXML(data, patches)
|
||||||
root, err = parseEditorXML(data)
|
root, err = parseEditorXMLContext(m.ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -624,13 +662,13 @@ func (m *editorPageImport) outlines() ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// appendImportedOutlines 将已迁移目录追加到目标根目录,规范计数且不修改当前编辑器
|
// appendImportedOutlines 将已迁移目录追加到目标根目录,规范计数且不修改当前编辑器
|
||||||
// 入参: imported 已迁移目录片段
|
// 入参: ctx 取消上下文, imported 已迁移目录片段
|
||||||
// 返回: []byte 合并后的目录, error 错误信息
|
// 返回: []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 {
|
if len(imported) == 0 {
|
||||||
return e.outlines, nil
|
return e.outlines, nil
|
||||||
}
|
}
|
||||||
added, err := parseEditorXML(imported)
|
added, err := parseEditorXMLContext(ctx, imported)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -638,7 +676,7 @@ func (e *Editor) appendImportedOutlines(imported []byte) ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -647,6 +685,9 @@ func (e *Editor) appendImportedOutlines(imported []byte) ([]byte, error) {
|
|||||||
content = bytes.Clone(data[root.open:root.close])
|
content = bytes.Clone(data[root.open:root.close])
|
||||||
}
|
}
|
||||||
for _, child := range editorOutlineChildren(added) {
|
for _, child := range editorOutlineChildren(added) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
value, err := editorXMLStandalone(imported[child.start:child.end], child)
|
value, err := editorXMLStandalone(imported[child.start:child.end], child)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -732,7 +773,7 @@ func (m *editorPageImport) resourceIndex(name string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(m.ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -773,7 +814,7 @@ func (m *editorPageImport) reference(value string) (string, error) {
|
|||||||
if template, ok := m.templates[key]; ok {
|
if template, ok := m.templates[key]; ok {
|
||||||
if _, seen := m.templateParts[key]; !seen {
|
if _, seen := m.templateParts[key]; !seen {
|
||||||
m.templateParts[key] = nil
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -801,7 +842,7 @@ func (m *editorPageImport) documentReference(group, kind, source, id, loc string
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, attr := range [][2]string{{"ID", id}, {"BaseLoc", loc}} {
|
for _, attr := range [][2]string{{"ID", id}, {"BaseLoc", loc}} {
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(m.ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -857,11 +898,18 @@ func (m *editorPageImport) copyData(name, relative string, evidence bool) (strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// copyXML 复制页面或标准关联XML并重映射引用
|
// copyXML 复制页面或标准关联XML并重映射引用
|
||||||
// 入参: name 来源路径, candidate 目标候选路径
|
// 入参: name 来源路径, candidate 目标候选路径, template 是否按模板页复制
|
||||||
// 返回: string 新路径, error 错误信息
|
// 返回: 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)
|
key := cleanPackagePath(name)
|
||||||
if target := m.paths[key]; target != "" {
|
if target := paths[key]; target != "" {
|
||||||
return "/" + target, nil
|
return "/" + target, nil
|
||||||
}
|
}
|
||||||
target := packageAvailableName(m.target, m.files, candidate)
|
target := packageAvailableName(m.target, m.files, candidate)
|
||||||
@@ -869,11 +917,11 @@ func (m *editorPageImport) copyXML(name, candidate string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(m.ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
m.paths[key], m.files[target] = target, nil
|
paths[key], m.files[target] = target, nil
|
||||||
if root.name.Local == "Page" {
|
if root.name.Local == "Page" {
|
||||||
for _, child := range root.children {
|
for _, child := range root.children {
|
||||||
if child.name.Local == "PageRes" {
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -895,6 +950,21 @@ func (m *editorPageImport) copyXML(name, candidate string) (string, error) {
|
|||||||
return "/" + target, nil
|
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实体
|
// editorImportText 获取节点直接文本,解码XML实体
|
||||||
// 入参: data XML, node 节点
|
// 入参: data XML, node 节点
|
||||||
// 返回: string 文本
|
// 返回: string 文本
|
||||||
@@ -910,6 +980,11 @@ func editorImportText(data []byte, node *editorXML) string {
|
|||||||
// 入参: entry 来源上下文, node 当前节点
|
// 入参: entry 来源上下文, node 当前节点
|
||||||
// 返回: []byte 独立XML片段, error 错误信息
|
// 返回: []byte 独立XML片段, error 错误信息
|
||||||
func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]byte, 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 {
|
if classifyOFDNamespace(node.name.Space) == ofdXMLUnknown {
|
||||||
return nil, fmt.Errorf("cannot remap extension namespace %q", node.name.Space)
|
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
|
var err error
|
||||||
switch key {
|
switch key {
|
||||||
case "ID":
|
case "ID":
|
||||||
if name == "Attachment" {
|
if id := entry.ids[editorImportKey(value)]; id != "" {
|
||||||
|
value = id
|
||||||
|
} else if name == "Attachment" {
|
||||||
value, err = m.attachmentID(value)
|
value, err = m.attachmentID(value)
|
||||||
} else if name == "Signature" || name == "StampAnnot" {
|
} else if name == "Signature" || name == "StampAnnot" {
|
||||||
value, err = m.signatureID(entry.name, name, value)
|
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)
|
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":
|
case "AttachID":
|
||||||
value, err = m.attachment(value)
|
value, err = m.attachment(value)
|
||||||
@@ -1032,15 +1109,24 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
|
|||||||
var content []byte
|
var content []byte
|
||||||
pageContent := name == "Page" && node.parent == nil
|
pageContent := name == "Page" && node.parent == nil
|
||||||
if pageContent {
|
if pageContent {
|
||||||
area := m.doc.CommonData.PageArea
|
|
||||||
local := node.child("Area")
|
local := node.child("Area")
|
||||||
var fields []byte
|
|
||||||
for _, field := range [][2]string{{"PhysicalBox", area.PhysicalBox}, {"ApplicationBox", area.ApplicationBox}, {"ContentBox", area.ContentBox}, {"BleedBox", area.BleedBox}} {
|
|
||||||
if field[1] != "" && (local == nil || local.child(field[0]) == nil) {
|
|
||||||
fields = append(fields, editorXMLText(field[0], field[1])...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if local == nil {
|
if local == nil {
|
||||||
|
var templates []Template
|
||||||
|
for _, child := range node.children {
|
||||||
|
if !entry.template && child.matchesOFD("Template") {
|
||||||
|
templates = append(templates, Template{TemplateID: child.attr("TemplateID")})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
area, err := m.reader.resolvePageArea(PageArea{}, templates)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var fields []byte
|
||||||
|
for _, field := range [][2]string{{"PhysicalBox", area.PhysicalBox}, {"ApplicationBox", area.ApplicationBox}, {"ContentBox", area.ContentBox}, {"BleedBox", area.BleedBox}} {
|
||||||
|
if field[1] != "" {
|
||||||
|
fields = append(fields, editorXMLText(field[0], field[1])...)
|
||||||
|
}
|
||||||
|
}
|
||||||
encoded, err := editorXMLContainer("Area", nil, fields)
|
encoded, err := editorXMLContainer("Area", nil, fields)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1051,17 +1137,13 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(encoded)
|
content = 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)})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, child := range node.children {
|
for _, child := range node.children {
|
||||||
|
if pageContent && entry.template && child.matchesOFD("Template") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if pageContent && child.name.Local == "Area" {
|
if pageContent && child.name.Local == "Area" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1075,7 +1157,7 @@ func (m *editorPageImport) encode(entry editorImportEntry, node *editorXML) ([]b
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(encoded)
|
root, err := parseEditorXMLContext(m.ctx, encoded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
value, err = m.copyData(loc, path.Join("Attachments", path.Base(loc)), false)
|
||||||
} else {
|
} else {
|
||||||
candidate := path.Join(m.directory, "Annotations", "Page_"+m.id(node.parent.attr("PageID"))+".xml")
|
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":
|
case "Font", "Substitution", "Thumbnail":
|
||||||
value, err = m.reference(strings.TrimSpace(value))
|
value, err = m.reference(strings.TrimSpace(value))
|
||||||
@@ -1162,7 +1244,7 @@ func (m *editorPageImport) annotations() ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(m.ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1198,7 +1280,7 @@ func (m *editorPageImport) attachment(id string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(m.ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -1261,7 +1343,7 @@ func (m *editorPageImport) signatureID(source, kind, id string) (string, error)
|
|||||||
}
|
}
|
||||||
if doc.Signatures != "" {
|
if doc.Signatures != "" {
|
||||||
name := m.target.ResPath(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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -1272,7 +1354,7 @@ func (m *editorPageImport) signatureID(source, kind, id string) (string, error)
|
|||||||
sequence.observe(list.MaxSignID)
|
sequence.observe(list.MaxSignID)
|
||||||
for _, ref := range list.List {
|
for _, ref := range list.List {
|
||||||
sequence.observe(ref.ID)
|
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 {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -1306,7 +1388,7 @@ func (m *editorPageImport) signatures() ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(m.ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1347,6 +1429,9 @@ func (m *editorPageImport) signatures() ([]byte, error) {
|
|||||||
// 入参: base 目标基底, refs 插入页面, annotations 注释索引, signatures 签名索引
|
// 入参: base 目标基底, refs 插入页面, annotations 注释索引, signatures 签名索引
|
||||||
// 返回: *editorSource 新基底, error 错误信息
|
// 返回: *editorSource 新基底, error 错误信息
|
||||||
func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, signatures []byte) (*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)
|
files := maps.Clone(base.reader.files)
|
||||||
if files == nil {
|
if files == nil {
|
||||||
files = make(map[string][]byte)
|
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 {
|
if file, ok := base.reader.packageFile(name); ok {
|
||||||
name = cleanPackagePath(file.Name)
|
name = cleanPackagePath(file.Name)
|
||||||
}
|
}
|
||||||
data, err := base.reader.readFile(name)
|
data, err := base.reader.readFileViewContext(m.ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(m.ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1415,12 +1500,15 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
|
|||||||
}
|
}
|
||||||
data = editorPatchXML(data, patches)
|
data = editorPatchXML(data, patches)
|
||||||
if len(annotations) != 0 {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if base.document.Annotations == "" {
|
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}})
|
data, err = editorXMLSetText(data, root, [][2]string{{"Annotations", loc}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)) {
|
for _, id := range slices.Sorted(maps.Keys(m.attachments)) {
|
||||||
entries = append(entries, m.attachments[id]...)
|
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")
|
node := root.child("Attachments")
|
||||||
if node != nil && len(node.children) != 0 {
|
if node != nil && len(node.children) != 0 {
|
||||||
entries, err = editorXMLGenerated(entries, node.name.Space)
|
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...)
|
content := append(bytes.Clone(data[node.open:node.close]), entries...)
|
||||||
data = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, node, content)})
|
data = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, node, content)})
|
||||||
} else {
|
} 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 {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if base.reader.OFD.DocBody[base.reader.documentIndex].versioned {
|
||||||
return nil, fmt.Errorf("signature import into a versioned document is not supported")
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
indexName := cleanPackagePath(loc)
|
indexName := cleanPackagePath(loc)
|
||||||
indexData := files[indexName]
|
indexData := files[indexName]
|
||||||
indexRoot, err := parseEditorXML(indexData)
|
indexRoot, err := parseEditorXMLContext(m.ctx, indexData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1477,11 +1568,11 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
|
|||||||
}
|
}
|
||||||
files[indexName] = editorPatchXML(indexData, []editorXMLPatch{patch})
|
files[indexName] = editorPatchXML(indexData, []editorXMLPatch{patch})
|
||||||
if base.document.Signatures == "" {
|
if base.document.Signatures == "" {
|
||||||
ofd, err := base.reader.readFile("OFD.xml")
|
ofd, err := base.reader.readFileViewContext(m.ctx, "OFD.xml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(ofd)
|
root, err := parseEditorXMLContext(m.ctx, ofd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1528,9 +1619,9 @@ func (m *editorPageImport) merge(base *editorSource, refs []Page, annotations, s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mergeImportIndex 向原索引追加条目,不移动原条目的相对路径基准
|
// mergeImportIndex 向原索引追加条目,不移动原条目的相对路径基准
|
||||||
// 入参: reader 目标输入包, files 新条目, loc 原索引, fallback 新索引路径, rootName 根节点名称, entries 新条目
|
// 入参: ctx 取消上下文, reader 目标输入包, files 新条目, loc 原索引, fallback 新索引路径, rootName 根节点名称, entries 新条目
|
||||||
// 返回: string 索引绝对路径, error 错误信息
|
// 返回: 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
|
name := fallback
|
||||||
var data []byte
|
var data []byte
|
||||||
var err error
|
var err error
|
||||||
@@ -1545,12 +1636,12 @@ func mergeImportIndex(reader *Reader, files map[string][]byte, loc, fallback, ro
|
|||||||
var exists bool
|
var exists bool
|
||||||
data, exists = files[name]
|
data, exists = files[name]
|
||||||
if !exists {
|
if !exists {
|
||||||
data, err = reader.readFile(name)
|
data, err = reader.readFileViewContext(ctx, name)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
root, parseErr := parseEditorXML(data)
|
root, parseErr := parseEditorXMLContext(ctx, data)
|
||||||
if parseErr != nil {
|
if parseErr != nil {
|
||||||
return "", parseErr
|
return "", parseErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -912,7 +912,14 @@ func (e *Editor) editorImage(id string) (image.Point, error) {
|
|||||||
return image.Point{}, err
|
return image.Point{}, err
|
||||||
}
|
}
|
||||||
defer input.Close()
|
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 {
|
if err != nil {
|
||||||
return image.Point{}, err
|
return image.Point{}, err
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-11
@@ -29,14 +29,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// sourceParts 生成编辑快照的XML和新增资源,删页时同步清理标准引用
|
// sourceParts 生成编辑快照的XML和新增资源,删页时同步清理标准引用
|
||||||
// 入参: progress 保存进度回调,预览时为nil
|
// 入参: ctx 取消上下文, progress 保存进度回调,预览时为nil
|
||||||
// 返回: map[string][]byte 替换及新增条目, error 错误信息
|
// 返回: 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)
|
parts, err := e.sourcePartsReferences(progress, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return e.source.reader.versionChanges(context.Background(), parts)
|
return e.source.reader.versionChanges(ctx, parts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sourcePartsReferences 生成保存快照,同步收集完全自产页面的实际编码引用
|
// 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 {
|
if err := progress.report("pages", len(e.pages), len(e.pages)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fonts, images, spaces, resourceFiles := e.usedResources()
|
fonts, images, spaces, usedFiles := e.usedResources()
|
||||||
for _, resource := range e.resources {
|
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
|
parts[resource.name] = resource.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
commonData := source.document.CommonData
|
commonData := source.document.CommonData
|
||||||
for _, name := range append(slices.Clone(commonData.DocumentRes), commonData.PublicRes...) {
|
for _, name := range append(slices.Clone(commonData.DocumentRes), commonData.PublicRes...) {
|
||||||
resourceFiles = slices.DeleteFunc(resourceFiles, func(file string) bool {
|
delete(usedFiles, reader.ResPath(name))
|
||||||
return file == reader.ResPath(name)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
resourceFiles := slices.Sorted(maps.Keys(usedFiles))
|
||||||
if len(fonts)+len(images)+len(spaces) != 0 {
|
if len(fonts)+len(images)+len(spaces) != 0 {
|
||||||
data, err := e.encodeXML(func(x *ofdXML) { x.resources(fonts, images, spaces) })
|
data, err := e.encodeXML(func(x *ofdXML) { x.resources(fonts, images, spaces) })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -720,10 +719,10 @@ func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sourceReader 生成原包与修改条目组成的独立预览快照,不进行ZIP压缩
|
// sourceReader 生成原包与修改条目组成的独立预览快照,不进行ZIP压缩
|
||||||
// 入参: progress 准备进度回调
|
// 入参: ctx 取消上下文, progress 准备进度回调
|
||||||
// 返回: *Reader 阅读器, error 错误信息
|
// 返回: *Reader 阅读器, error 错误信息
|
||||||
func (e *Editor) sourceReader(progress editorProgress) (*Reader, error) {
|
func (e *Editor) sourceReader(ctx context.Context, progress editorProgress) (*Reader, error) {
|
||||||
parts, err := e.sourceParts(progress)
|
parts, err := e.sourceParts(ctx, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,8 +193,6 @@ func (s *editorReferenceScan) accept(token xml.Token) bool {
|
|||||||
if token.Name.Local != "TextObject" && token.Name.Local != "Text" {
|
if token.Name.Local != "TextObject" && token.Name.Local != "Text" {
|
||||||
usage.unsafe = true
|
usage.unsafe = true
|
||||||
}
|
}
|
||||||
} else if attr.Name.Local == "Substitution" {
|
|
||||||
s.refs.fontUsage(attr.Value).unsafe = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.stack = append(s.stack, token.Name.Local)
|
s.stack = append(s.stack, token.Name.Local)
|
||||||
@@ -230,7 +228,7 @@ func (s *editorReferenceScan) accept(token xml.Token) bool {
|
|||||||
s.objects[id] = true
|
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
|
s.refs.fontUsage(s.text.String()).unsafe = true
|
||||||
}
|
}
|
||||||
if usage := s.fonts[len(s.fonts)-1]; usage != nil {
|
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}
|
scope := map[string]bool{entry: true}
|
||||||
queue := []string{entry}
|
queue := []string{entry}
|
||||||
refs := &editorResourceRefs{ids: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
|
refs := &editorResourceRefs{ids: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
|
||||||
|
definitions := make(map[string]*editorResourceRefs)
|
||||||
for position := 0; position < len(queue); position++ {
|
for position := 0; position < len(queue); position++ {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -47,6 +48,12 @@ func resourceReferenceScopes(ctx context.Context, documents map[string]bool, res
|
|||||||
used[name] = true
|
used[name] = true
|
||||||
if selected == "" || selected == entry {
|
if selected == "" || selected == entry {
|
||||||
refs.merge(&editorResourceRefs{ids: source.ids, fonts: source.fonts})
|
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 {
|
for target, role := range source.roles {
|
||||||
if role&packageReferenceXML != 0 && !scope[target] {
|
if role&packageReferenceXML != 0 && !scope[target] {
|
||||||
@@ -58,6 +65,23 @@ func resourceReferenceScopes(ctx context.Context, documents map[string]bool, res
|
|||||||
if selected != "" && selected != entry {
|
if selected != "" && selected != entry {
|
||||||
continue
|
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 {
|
for name := range scope {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
+110
-39
@@ -35,14 +35,15 @@ const (
|
|||||||
editorReferenceFile
|
editorReferenceFile
|
||||||
)
|
)
|
||||||
|
|
||||||
// editorResourceRefs 保存全包资源标识和直接文件引用,不以页面是否加载判断资源存活
|
// editorResourceRefs 记录资源标识、文件引用及声明依赖,不以页面是否加载判断资源存活
|
||||||
type editorResourceRefs struct {
|
type editorResourceRefs struct {
|
||||||
ids map[string]bool
|
ids map[string]bool
|
||||||
files map[string]bool
|
files map[string]bool
|
||||||
fonts map[string]*editorFontUsage
|
fonts map[string]*editorFontUsage
|
||||||
versions map[string]bool
|
versions map[string]bool
|
||||||
documents map[string]bool
|
documents map[string]bool
|
||||||
roles map[string]int
|
roles map[string]int
|
||||||
|
definitions map[string]*editorResourceRefs
|
||||||
}
|
}
|
||||||
|
|
||||||
// compactSourceReferences 合并自产页面引用,版本文档按当前版本范围扫描
|
// compactSourceReferences 合并自产页面引用,版本文档按当前版本范围扫描
|
||||||
@@ -51,7 +52,15 @@ type editorResourceRefs struct {
|
|||||||
// 入参: parts 输出条目, progress 保存进度回调, generated 自产页面及引用
|
// 入参: parts 输出条目, progress 保存进度回调, generated 自产页面及引用
|
||||||
// 返回: map[string]bool 可移除条目, error 读取或取消错误
|
// 返回: map[string]bool 可移除条目, error 读取或取消错误
|
||||||
func (e *Editor) compactSourceReferences(parts map[string][]byte, progress editorProgress, generated map[string]editorGeneratedReferences) (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) {
|
if e.output != nil && e.output.protected || multiple && (e.output == nil || e.output.options.Mode == CompressionUnchanged) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -59,7 +68,6 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
generated = e.generatedVectorReferences(parts, generated)
|
generated = e.generatedVectorReferences(parts, generated)
|
||||||
reader := e.source.reader
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if e.output != nil {
|
if e.output != nil {
|
||||||
ctx = e.output.ctx
|
ctx = e.output.ctx
|
||||||
@@ -69,11 +77,13 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
names := make(map[string]bool)
|
names := make(map[string]bool)
|
||||||
for name := range reader.fileIndex {
|
if reader != nil {
|
||||||
names[name] = true
|
for name := range reader.fileIndex {
|
||||||
}
|
names[name] = true
|
||||||
for name := range reader.files {
|
}
|
||||||
names[name] = true
|
for name := range reader.files {
|
||||||
|
names[name] = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for name := range parts {
|
for name := range parts {
|
||||||
names[name] = true
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -93,9 +103,25 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
var scoped map[string]*editorResourceRefs
|
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 := ""
|
selected := ""
|
||||||
if reader.OFD.DocBody[reader.documentIndex].versioned {
|
if reader != nil && reader.OFD.DocBody[reader.documentIndex].versioned {
|
||||||
selected, err = reader.DocumentRoot()
|
selected, err = reader.DocumentRoot()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -107,11 +133,36 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ordered := slices.Sorted(maps.Keys(names))
|
ordered := slices.Sorted(maps.Keys(names))
|
||||||
removed := maps.Clone(e.source.retiredAttachments)
|
removed := maps.Clone(retired)
|
||||||
if removed == nil {
|
if removed == nil {
|
||||||
removed = make(map[string]bool)
|
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)
|
updates := make(map[string][]byte)
|
||||||
fontFiles := make(map[string]*editorFontUsage)
|
fontFiles := make(map[string]*editorFontUsage)
|
||||||
for i, name := range resources {
|
for i, name := range resources {
|
||||||
@@ -131,7 +182,7 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
usageRefs := refs
|
usageRefs := refs
|
||||||
if multiple {
|
if multiple || prune {
|
||||||
usageRefs = scoped[name]
|
usageRefs = scoped[name]
|
||||||
}
|
}
|
||||||
preserved := refs.versions[cleanPackagePath(name)] || usageRefs == nil
|
preserved := refs.versions[cleanPackagePath(name)] || usageRefs == nil
|
||||||
@@ -145,35 +196,52 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
|
|||||||
} else if group.name.Local == "MultiMedias" && node.name.Local == "MultiMedia" {
|
} else if group.name.Local == "MultiMedias" && node.name.Local == "MultiMedia" {
|
||||||
field = "MediaFile"
|
field = "MediaFile"
|
||||||
}
|
}
|
||||||
if field == "" {
|
if field == "" && (!prune || node.name.Local != editorResourceElement(group.name.Local)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
id := editorResourceID(node.attr("ID"))
|
id := editorResourceID(node.attr("ID"))
|
||||||
keep := preserved || id == "" || usageRefs.ids[id] || field == "MediaFile" && node.attr("Type") != "Image"
|
keep := preserved || id == "" || usageRefs.ids[id] || field == "MediaFile" && node.attr("Type") != "Image"
|
||||||
|
profile := prune && group.name.Local == "ColorSpaces" && node.name.Local == "ColorSpace"
|
||||||
|
if prune && keep && !profile {
|
||||||
|
maps.Copy(protectedProfiles, references[name].definitions[id].files)
|
||||||
|
}
|
||||||
|
var value string
|
||||||
if file := node.child(field); file != nil {
|
if file := node.child(field); file != nil {
|
||||||
var value string
|
|
||||||
if err := xml.Unmarshal(data[file.start:file.end], &value); err != nil {
|
if err := xml.Unmarshal(data[file.start:file.end], &value); err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if location := editorResourceLocation(name, root.attr("BaseLoc"), value); location != "" {
|
} else if profile {
|
||||||
alternate := cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value))
|
value = node.attr("Profile")
|
||||||
if field == "FontFile" && !multiple {
|
}
|
||||||
usage := usageRefs.fonts[id]
|
if location := editorResourceLocation(name, root.attr("BaseLoc"), value); location != "" {
|
||||||
charset := node.attr("Charset")
|
alternate := cleanPackagePath(resolveResourcePath(name, root.attr("BaseLoc"), value))
|
||||||
unsafe := preserved || id == "" || charset != "" && !strings.EqualFold(charset, "unicode") || location != alternate
|
if field == "FontFile" && !multiple && keep {
|
||||||
for _, file := range []string{location, alternate} {
|
var usage *editorFontUsage
|
||||||
if fontFiles[file] == nil {
|
if usageRefs != nil {
|
||||||
fontFiles[file] = newEditorFontUsage()
|
usage = usageRefs.fonts[id]
|
||||||
}
|
}
|
||||||
fontFiles[file].merge(usage, unsafe || keep && usage == nil)
|
charset := node.attr("Charset")
|
||||||
|
unsafe := preserved || id == "" || charset != "" && !strings.EqualFold(charset, "unicode") || location != alternate
|
||||||
|
for _, file := range []string{location, alternate} {
|
||||||
|
if fontFiles[file] == nil {
|
||||||
|
fontFiles[file] = newEditorFontUsage()
|
||||||
|
}
|
||||||
|
fontFiles[file].merge(usage, unsafe || keep && usage == nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if keep {
|
||||||
|
usedFiles[location] = true
|
||||||
|
usedFiles[alternate] = true
|
||||||
|
if prune {
|
||||||
|
if profile && !preserved && location == alternate {
|
||||||
|
profiles[location] = append(profiles[location], editorProfileReference{index: name, id: id})
|
||||||
|
} else {
|
||||||
|
protectedProfiles[location] = true
|
||||||
|
protectedProfiles[alternate] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if keep {
|
} else {
|
||||||
usedFiles[location] = true
|
removed[location] = true
|
||||||
usedFiles[alternate] = true
|
|
||||||
} else {
|
|
||||||
removed[location] = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !keep {
|
if !keep {
|
||||||
@@ -226,6 +294,9 @@ func (e *Editor) compactSourceReferences(parts map[string][]byte, progress edito
|
|||||||
delete(parts, name)
|
delete(parts, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := compactResourceProfiles(ctx, reader, parts, profiles, protectedProfiles, removed); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return removed, nil
|
return removed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+83
-23
@@ -78,14 +78,25 @@ func (e *Editor) SelectVersion(id string) error {
|
|||||||
// 入参: name 版本名称,可为空
|
// 入参: name 版本名称,可为空
|
||||||
// 返回: string 新版本标识, error 创建错误
|
// 返回: string 新版本标识, error 创建错误
|
||||||
func (e *Editor) AddVersion(name string) (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
|
var id string
|
||||||
err := e.editDocuments(func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
err := e.editDocumentsContext(ctx, func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
||||||
versions, err := reader.documentVersionEntries()
|
versions, err := reader.documentVersionEntriesContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
index := int32(0)
|
index := int32(0)
|
||||||
for _, version := range versions {
|
for _, version := range versions {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
index = max(index, version.Index)
|
index = max(index, version.Index)
|
||||||
}
|
}
|
||||||
if index == math.MaxInt32 {
|
if index == math.MaxInt32 {
|
||||||
@@ -93,15 +104,26 @@ func (e *Editor) AddVersion(name string) (string, error) {
|
|||||||
}
|
}
|
||||||
index++
|
index++
|
||||||
ids := make(map[string]bool)
|
ids := make(map[string]bool)
|
||||||
var collect func(*editorXML)
|
var collect func(*editorXML) error
|
||||||
collect = func(node *editorXML) {
|
collect = func(node *editorXML) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
ids[node.attr("ID")] = true
|
ids[node.attr("ID")] = true
|
||||||
for _, child := range node.children {
|
for _, child := range node.children {
|
||||||
collect(child)
|
if err := collect(child); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := collect(root); err != nil {
|
||||||
|
return 0, err
|
||||||
}
|
}
|
||||||
collect(root)
|
|
||||||
for serial := int64(index); ; serial++ {
|
for serial := int64(index); ; serial++ {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
id = "v" + strconv.FormatInt(serial, 10)
|
id = "v" + strconv.FormatInt(serial, 10)
|
||||||
if !ids[id] {
|
if !ids[id] {
|
||||||
break
|
break
|
||||||
@@ -111,7 +133,7 @@ func (e *Editor) AddVersion(name string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
files, err := reader.versionSnapshotFiles(context.Background())
|
files, err := reader.versionSnapshotFiles(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -130,6 +152,10 @@ func (e *Editor) AddVersion(name string) (string, error) {
|
|||||||
x.root("DocVersion", attrs)
|
x.root("DocVersion", attrs)
|
||||||
x.start("FileList", nil)
|
x.start("FileList", nil)
|
||||||
for serial, file := range slices.Sorted(maps.Keys(files)) {
|
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.start("File", ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: "file" + strconv.Itoa(serial+1)}})
|
||||||
x.token(xml.CharData("/" + file))
|
x.token(xml.CharData("/" + file))
|
||||||
x.end("File")
|
x.end("File")
|
||||||
@@ -158,7 +184,7 @@ func (e *Editor) AddVersion(name string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
root, err = parseEditorXML(data)
|
root, err = parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -262,7 +288,7 @@ func (e *Editor) RenameVersion(id, name string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
reader.files[location] = updated
|
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 0, err
|
||||||
}
|
}
|
||||||
return reader.documentIndex, nil
|
return reader.documentIndex, nil
|
||||||
@@ -274,8 +300,16 @@ func (e *Editor) RenameVersion(id, name string) error {
|
|||||||
// 入参: id 版本标识,不能为空
|
// 入参: id 版本标识,不能为空
|
||||||
// 返回: error 删除错误
|
// 返回: error 删除错误
|
||||||
func (e *Editor) DeleteVersion(id string) error {
|
func (e *Editor) DeleteVersion(id string) error {
|
||||||
return e.editDocuments(func(reader *Reader, data []byte, root *editorXML) (int, error) {
|
return e.DeleteVersionContext(context.Background(), id)
|
||||||
versions, err := reader.DocumentVersions()
|
}
|
||||||
|
|
||||||
|
// 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 {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -289,6 +323,9 @@ func (e *Editor) DeleteVersion(id string) error {
|
|||||||
}
|
}
|
||||||
list := root.childAt("DocBody", reader.documentIndex).child("Versions")
|
list := root.childAt("DocBody", reader.documentIndex).child("Versions")
|
||||||
for _, node := range list.children {
|
for _, node := range list.children {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
if node.matchesOFD("Version") && node.attr("ID") == id {
|
if node.matchesOFD("Version") && node.attr("ID") == id {
|
||||||
if len(versions) == 1 {
|
if len(versions) == 1 {
|
||||||
node = list
|
node = list
|
||||||
@@ -302,9 +339,12 @@ func (e *Editor) DeleteVersion(id string) error {
|
|||||||
}
|
}
|
||||||
removed := map[string]bool{versions[index].Location: true}
|
removed := map[string]bool{versions[index].Location: true}
|
||||||
for _, file := range versions[index].Files {
|
for _, file := range versions[index].Files {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
removed[file.Location] = true
|
removed[file.Location] = true
|
||||||
}
|
}
|
||||||
if err := reader.retireVersionFiles(removed); err != nil {
|
if err := reader.retireVersionFiles(ctx, removed); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
return reader.documentIndex, nil
|
return reader.documentIndex, nil
|
||||||
@@ -341,6 +381,9 @@ func editorVersionDefault(data []byte, body *editorXML, id string) ([]byte, erro
|
|||||||
// 入参: ctx 取消上下文
|
// 入参: ctx 取消上下文
|
||||||
// 返回: map[string]bool 快照文件, error 引用或读取错误
|
// 返回: map[string]bool 快照文件, error 引用或读取错误
|
||||||
func (r *Reader) versionSnapshotFiles(ctx context.Context) (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()
|
entry, err := r.DocumentRoot()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -351,15 +394,18 @@ func (r *Reader) versionSnapshotFiles(ctx context.Context) (map[string]bool, err
|
|||||||
}
|
}
|
||||||
if r.versionInfo != nil {
|
if r.versionInfo != nil {
|
||||||
for _, file := range r.versionInfo.Files {
|
for _, file := range r.versionInfo.Files {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
files[file.Location] = true
|
files[file.Location] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body := r.OFD.DocBody[r.documentIndex]
|
body := r.OFD.DocBody[r.documentIndex]
|
||||||
data, err := r.readFile("OFD.xml")
|
data, err := r.readFileViewContext(ctx, "OFD.xml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -380,6 +426,9 @@ func (r *Reader) versionSnapshotFiles(ctx context.Context) (map[string]bool, err
|
|||||||
maps.Copy(files, signed)
|
maps.Copy(files, signed)
|
||||||
}
|
}
|
||||||
for name := range files {
|
for name := range files {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if !r.fileNames[name] {
|
if !r.fileNames[name] {
|
||||||
return nil, fmt.Errorf("version file not found: %s", 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 清理修改入口后不再被任何文档或版本引用的候选文件
|
// retireVersionFiles 清理修改入口后不再被任何文档或版本引用的候选文件
|
||||||
// 入参: candidates 待清理路径,OFD.xml始终保留
|
// 入参: ctx 取消上下文, candidates 待清理路径,OFD.xml始终保留
|
||||||
// 返回: error 依赖读取错误
|
// 返回: error 依赖读取或取消错误
|
||||||
func (r *Reader) retireVersionFiles(candidates map[string]bool) error {
|
func (r *Reader) retireVersionFiles(ctx context.Context, candidates map[string]bool) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
view := *r
|
view := *r
|
||||||
if err := view.initRoot(); err != nil {
|
if err := view.initRoot(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for index := range view.DocumentCount() {
|
indexes := make([]int, view.DocumentCount())
|
||||||
files, err := view.documentFiles(context.Background(), index)
|
for index := range indexes {
|
||||||
if err != nil {
|
indexes[index] = index
|
||||||
|
}
|
||||||
|
files, err := view.documentFiles(ctx, indexes...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for name := range files {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for name := range files {
|
delete(candidates, name)
|
||||||
delete(candidates, name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
delete(candidates, "OFD.xml")
|
delete(candidates, "OFD.xml")
|
||||||
for name := range candidates {
|
for name := range candidates {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
delete(r.files, name)
|
delete(r.files, name)
|
||||||
}
|
}
|
||||||
if r.Zip != nil {
|
if r.Zip != nil {
|
||||||
|
|||||||
+70
-20
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
@@ -27,7 +28,7 @@ import (
|
|||||||
_ "github.com/xiaoqidun/jbig2"
|
_ "github.com/xiaoqidun/jbig2"
|
||||||
_ "golang.org/x/image/bmp"
|
_ "golang.org/x/image/bmp"
|
||||||
"golang.org/x/image/draw"
|
"golang.org/x/image/draw"
|
||||||
_ "golang.org/x/image/tiff"
|
"golang.org/x/image/tiff"
|
||||||
)
|
)
|
||||||
|
|
||||||
// transparentEdgeImage 共享只读像素并按需计算透明边缘和采样边距
|
// transparentEdgeImage 共享只读像素并按需计算透明边缘和采样边距
|
||||||
@@ -147,6 +148,14 @@ func decodeImageData(data []byte) (image.Image, string, error) {
|
|||||||
}
|
}
|
||||||
return img, img.format, nil
|
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))
|
img, format, err := image.Decode(bytes.NewReader(data))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return img, normalizeSealType(format), nil
|
return img, normalizeSealType(format), nil
|
||||||
@@ -175,6 +184,27 @@ func isPNGData(data []byte) bool {
|
|||||||
// 入参: data 图片数据
|
// 入参: data 图片数据
|
||||||
// 返回: image.Config 图片尺寸, string 图片格式, error 错误信息
|
// 返回: image.Config 图片尺寸, string 图片格式, error 错误信息
|
||||||
func decodeImageConfigData(data []byte) (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))
|
cfg, format, err := image.DecodeConfig(bytes.NewReader(data))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return cfg, normalizeSealType(format), nil
|
return cfg, normalizeSealType(format), nil
|
||||||
@@ -193,11 +223,11 @@ func decodeBMPConfig(data []byte) (image.Config, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return image.Config{}, err
|
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")
|
return image.Config{}, fmt.Errorf("unsupported bmp")
|
||||||
}
|
}
|
||||||
if bpp != 16 && bpp != 24 && bpp != 32 {
|
if _, err := bmpColorMasks(data, offset, bpp, compression); err != nil {
|
||||||
return image.Config{}, fmt.Errorf("unsupported bmp")
|
return image.Config{}, err
|
||||||
}
|
}
|
||||||
if height < 0 {
|
if height < 0 {
|
||||||
height = -height
|
height = -height
|
||||||
@@ -209,15 +239,26 @@ func decodeBMPConfig(data []byte) (image.Config, error) {
|
|||||||
// 入参: data 图片数据
|
// 入参: data 图片数据
|
||||||
// 返回: image.Image 图片对象, error 错误信息
|
// 返回: image.Image 图片对象, error 错误信息
|
||||||
func decodeBMPImage(data []byte) (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)
|
offset, width, height, bpp, compression, err := parseBMPHeader(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if compression != 0 {
|
if offset > len(data) {
|
||||||
return nil, fmt.Errorf("unsupported bmp compression")
|
return nil, fmt.Errorf("invalid bmp pixel offset")
|
||||||
}
|
}
|
||||||
if bpp != 16 && bpp != 24 && bpp != 32 {
|
masks, err := bmpColorMasks(data, offset, bpp, compression)
|
||||||
return nil, fmt.Errorf("unsupported bmp depth")
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
topDown := height < 0
|
topDown := height < 0
|
||||||
if topDown {
|
if topDown {
|
||||||
@@ -230,9 +271,15 @@ func decodeBMPImage(data []byte) (image.Image, error) {
|
|||||||
if stride > int64(len(data)-offset)/int64(height) {
|
if stride > int64(len(data)-offset)/int64(height) {
|
||||||
return nil, fmt.Errorf("truncated bmp")
|
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)
|
rowStride := int(stride)
|
||||||
img := image.NewNRGBA(image.Rect(0, 0, width, height))
|
img := image.NewNRGBA(image.Rect(0, 0, width, height))
|
||||||
for y := 0; y < height; y++ {
|
for y := 0; y < height; y++ {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
srcY := y
|
srcY := y
|
||||||
if !topDown {
|
if !topDown {
|
||||||
srcY = height - 1 - y
|
srcY = height - 1 - y
|
||||||
@@ -240,20 +287,20 @@ func decodeBMPImage(data []byte) (image.Image, error) {
|
|||||||
row := data[offset+srcY*rowStride:]
|
row := data[offset+srcY*rowStride:]
|
||||||
for x := 0; x < width; x++ {
|
for x := 0; x < width; x++ {
|
||||||
switch bpp {
|
switch bpp {
|
||||||
case 16:
|
case 16, 32:
|
||||||
v := binary.LittleEndian.Uint16(row[x*2:])
|
var value uint32
|
||||||
img.SetNRGBA(x, y, color.NRGBA{
|
if bpp == 16 {
|
||||||
R: uint8(((v >> 10) & 0x1F) * 255 / 31),
|
value = uint32(binary.LittleEndian.Uint16(row[x*2:]))
|
||||||
G: uint8(((v >> 5) & 0x1F) * 255 / 31),
|
} else {
|
||||||
B: uint8((v & 0x1F) * 255 / 31),
|
value = binary.LittleEndian.Uint32(row[x*4:])
|
||||||
A: 255,
|
}
|
||||||
})
|
p := y*img.Stride + x*4
|
||||||
|
for i, mask := range masks {
|
||||||
|
img.Pix[p+i] = mask.sample(value)
|
||||||
|
}
|
||||||
case 24:
|
case 24:
|
||||||
p := x * 3
|
p := x * 3
|
||||||
img.SetNRGBA(x, y, color.NRGBA{R: row[p+2], G: row[p+1], B: row[p], A: 255})
|
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 {
|
if dibSize < 40 {
|
||||||
return 0, 0, 0, 0, 0, fmt.Errorf("unsupported bmp")
|
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")
|
return 0, 0, 0, 0, 0, fmt.Errorf("invalid bmp pixel offset")
|
||||||
}
|
}
|
||||||
width := int(int32(binary.LittleEndian.Uint32(data[18:22])))
|
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) {
|
switch src := img.(type) {
|
||||||
case interface{ NRGBA64At(int, int) color.NRGBA64 }:
|
case interface{ NRGBA64At(int, int) color.NRGBA64 }:
|
||||||
return src.NRGBA64At(x, y)
|
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:
|
case *image.Gray16:
|
||||||
c := src.Gray16At(x, y)
|
c := src.Gray16At(x, y)
|
||||||
return color.NRGBA64{R: c.Y, G: c.Y, B: c.Y, A: 0xffff}
|
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
|
xdpi, ydpi float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// imagePageMetadata 读取PNG物理尺寸与JPEG的JFIF、EXIF信息,EXIF密度优先于JFIF
|
// imagePageMetadata 读取图片方向与密度,TIFF方向由解码器处理,JPEG的EXIF密度优先于JFIF
|
||||||
// 入参: data 图片数据, format 图像格式, dpi 缺少物理密度时使用的DPI
|
// 入参: data 图片数据, format 图像格式, dpi 缺少物理密度时使用的DPI
|
||||||
// 返回: imagePageInfo 方向与横纵DPI, error 元数据错误
|
// 返回: imagePageInfo 方向与横纵DPI, error 元数据错误
|
||||||
func imagePageMetadata(data []byte, format string, dpi float64) (imagePageInfo, error) {
|
func imagePageMetadata(data []byte, format string, dpi float64) (imagePageInfo, error) {
|
||||||
info := imagePageInfo{orientation: 1, xdpi: dpi, ydpi: dpi}
|
info := imagePageInfo{orientation: 1, xdpi: dpi, ydpi: dpi}
|
||||||
var exif []byte
|
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" {
|
if format == "png" {
|
||||||
for offset := 8; offset+12 <= len(data); {
|
for offset := 8; offset+12 <= len(data); {
|
||||||
n := uint64(binary.BigEndian.Uint32(data[offset:]))
|
n := uint64(binary.BigEndian.Uint32(data[offset:]))
|
||||||
@@ -109,22 +125,28 @@ func imagePageMetadata(data []byte, format string, dpi float64) (imagePageInfo,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(exif) != 0 {
|
if len(exif) != 0 {
|
||||||
orientation, x, y, err := imageExifMetadata(exif)
|
orientation, x, y, err := imageExifMetadata(exif, dpi, directory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return info, err
|
return info, err
|
||||||
}
|
}
|
||||||
info.orientation = orientation
|
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
|
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
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// imageExifMetadata 只读取主图IFD中的方向和密度,不跟随缩略图与其他目录
|
// imageExifMetadata 只读取主图IFD中的方向和密度,不跟随缩略图与其他目录
|
||||||
// 入参: data EXIF中的TIFF数据
|
// 入参: data TIFF或EXIF数据, dpi 无绝对密度单位时的横向DPI,零值不换算, directory 目录偏移,零值使用首目录
|
||||||
// 返回: int EXIF方向值,未声明时为1, float64 横向DPI, float64 纵向DPI, error 元数据错误
|
// 返回: 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")
|
invalid := fmt.Errorf("invalid EXIF image metadata")
|
||||||
if len(data) < 8 {
|
if len(data) < 8 {
|
||||||
return 0, 0, 0, invalid
|
return 0, 0, 0, invalid
|
||||||
@@ -142,6 +164,9 @@ func imageExifMetadata(data []byte) (int, float64, float64, error) {
|
|||||||
return 0, 0, 0, invalid
|
return 0, 0, 0, invalid
|
||||||
}
|
}
|
||||||
offset := uint64(order.Uint32(data[4:]))
|
offset := uint64(order.Uint32(data[4:]))
|
||||||
|
if directory != 0 {
|
||||||
|
offset = uint64(directory)
|
||||||
|
}
|
||||||
if offset < 8 || offset+2 > uint64(len(data)) {
|
if offset < 8 || offset+2 > uint64(len(data)) {
|
||||||
return 0, 0, 0, invalid
|
return 0, 0, 0, invalid
|
||||||
}
|
}
|
||||||
@@ -185,6 +210,8 @@ func imageExifMetadata(data []byte) (int, float64, float64, error) {
|
|||||||
}
|
}
|
||||||
if unit == 3 {
|
if unit == 3 {
|
||||||
x, y = x*2.54, y*2.54
|
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 {
|
} else if unit != 2 {
|
||||||
x, y = 0, 0
|
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 页面对象
|
// 入参: page 页面对象
|
||||||
// 返回: *PageContent 页面内容, error 错误信息
|
// 返回: *PageContent 页面内容, error 错误信息
|
||||||
func (r *Reader) PageContent(page Page) (*PageContent, error) {
|
func (r *Reader) PageContent(page Page) (*PageContent, error) {
|
||||||
return r.readPageContent(page, false)
|
return r.readPageContent(page, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageCount 获取当前文档总页数
|
// PageCount 获取当前文档总页数
|
||||||
@@ -729,9 +729,9 @@ func resolveResourcePath(resPath, baseLoc, filePath string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// readPageContent 读取页面,可跳过文字提取不使用的独立路径和图片
|
// readPageContent 读取页面,可跳过文字提取不使用的独立路径和图片
|
||||||
// 入参: page 页面对象, textOnly 是否仅用于文字提取
|
// 入参: page 页面对象, textOnly 是否仅用于文字提取, template 是否按模板页读取
|
||||||
// 返回: *PageContent 页面内容, error 错误信息
|
// 返回: *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)
|
fullPath := r.ResPath(page.BaseLoc)
|
||||||
data, err := r.readFile(fullPath)
|
data, err := r.readFile(fullPath)
|
||||||
if err != nil {
|
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)
|
return nil, fmt.Errorf("failed to unmarshal page content: %w", err)
|
||||||
}
|
}
|
||||||
content.ID = page.ID
|
content.ID = page.ID
|
||||||
|
if template {
|
||||||
|
content.Template = nil
|
||||||
|
}
|
||||||
for _, res := range content.PageRes {
|
for _, res := range content.PageRes {
|
||||||
r.loadRes(resolveResourcePath(page.BaseLoc, "", res))
|
r.loadRes(resolveResourcePath(page.BaseLoc, "", res))
|
||||||
}
|
}
|
||||||
@@ -836,7 +839,7 @@ func (r *Reader) resolvePageArea(area PageArea, templates []Template) (PageArea,
|
|||||||
}
|
}
|
||||||
for _, ref := range templates {
|
for _, ref := range templates {
|
||||||
for _, template := range r.doc.CommonData.TemplatePage {
|
for _, template := range r.doc.CommonData.TemplatePage {
|
||||||
if template.ID != ref.TemplateID {
|
if !sameResourceID(template.ID, ref.TemplateID) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
content, err := r.readPageHeader(Page{BaseLoc: template.BaseLoc})
|
content, err := r.readPageHeader(Page{BaseLoc: template.BaseLoc})
|
||||||
|
|||||||
@@ -127,27 +127,34 @@ func packageReference(root, parent, name, attribute string) int {
|
|||||||
return packageReferenceNone
|
return packageReferenceNone
|
||||||
}
|
}
|
||||||
|
|
||||||
// documentFiles 遍历独立文档及版本依赖,只解析结构文件,不读取附件等原始数据
|
// documentFiles 汇总指定文档及版本依赖,共享结构只解析一次,不读取附件等原始数据
|
||||||
// 入参: ctx 取消上下文, index 文档索引
|
// 入参: ctx 取消上下文, indexes 文档索引,为空时不扫描任何文档
|
||||||
// 返回: map[string]bool 包内文件路径, error 读取或取消错误
|
// 返回: map[string]bool 包内文件路径, error 读取或取消错误
|
||||||
func (r *Reader) documentFiles(ctx context.Context, index int) (map[string]bool, error) {
|
func (r *Reader) documentFiles(ctx context.Context, indexes ...int) (map[string]bool, error) {
|
||||||
files, _, err := r.documentDependencies(ctx, index)
|
files, _, err := r.documentDependencies(ctx, indexes...)
|
||||||
return files, err
|
return files, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// documentDependencies 区分文档全部依赖与需要解析的结构文件,避免重复读取原始数据
|
// documentDependencies 合并指定入口的依赖图,区分结构文件与原始数据
|
||||||
// 入参: ctx 取消上下文, index 文档索引
|
// 入参: ctx 取消上下文, indexes 文档索引,为空时返回空集合
|
||||||
// 返回: map[string]bool 全部依赖, map[string]bool 结构文件, error 读取或取消错误
|
// 返回: 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 {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
if index < 0 || index >= r.DocumentCount() {
|
selected := make(map[int]bool, len(indexes))
|
||||||
return nil, nil, fmt.Errorf("document index out of range: %d", index)
|
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
|
var queue []string
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
queued := make(map[string]bool)
|
queued := make(map[string]bool)
|
||||||
|
if len(selected) == 0 {
|
||||||
|
return seen, queued, ctx.Err()
|
||||||
|
}
|
||||||
add := func(name string, kind int) {
|
add := func(name string, kind int) {
|
||||||
seen[name] = true
|
seen[name] = true
|
||||||
if kind == packageReferenceXML && !queued[name] {
|
if kind == packageReferenceXML && !queued[name] {
|
||||||
@@ -155,22 +162,21 @@ func (r *Reader) documentDependencies(ctx context.Context, index int) (map[strin
|
|||||||
queue = append(queue, name)
|
queue = append(queue, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data, err := r.readFile("OFD.xml")
|
data, err := r.readFileViewContext(ctx, "OFD.xml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
body := root.childAt("DocBody", index)
|
var collect func(*editorXML) error
|
||||||
if body == nil {
|
collect = func(node *editorXML) error {
|
||||||
return nil, nil, fmt.Errorf("document body not found: %d", index)
|
if err := ctx.Err(); err != nil {
|
||||||
}
|
return err
|
||||||
var collect func(*editorXML)
|
}
|
||||||
collect = func(node *editorXML) {
|
|
||||||
if !node.matchesOFD(node.name.Local) {
|
if !node.matchesOFD(node.name.Local) {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
parent := node.parent.name.Local
|
parent := node.parent.name.Local
|
||||||
if kind := packageReference("OFD", parent, node.name.Local, ""); kind != packageReferenceNone {
|
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 {
|
for _, child := range node.children {
|
||||||
collect(child)
|
if err := collect(child); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
index := 0
|
||||||
|
for _, body := range root.children {
|
||||||
|
if !body.matchesOFD("DocBody") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if selected[index] {
|
||||||
|
if err := collect(body); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
delete(selected, index)
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
if len(selected) != 0 {
|
||||||
|
return nil, nil, fmt.Errorf("document body not found")
|
||||||
}
|
}
|
||||||
collect(body)
|
|
||||||
for len(queue) > 0 {
|
for len(queue) > 0 {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ func (r *Renderer) AnnotationGeometry(annotation Annotation) (Box, []ObjectConto
|
|||||||
// 入参: object 图形对象, state 绘制状态
|
// 入参: object 图形对象, state 绘制状态
|
||||||
// 返回: error 度量错误
|
// 返回: error 度量错误
|
||||||
func (m *annotationMeasurement) DrawObject(object *GraphicObject, state RenderState) 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})
|
result, err := m.renderer.MeasureObject(*object, MeasureOptions{Defaults: state.Defaults, Parent: state.Parent, BoundaryInCTM: state.BoundaryInCTM, Clip: state.Clip, Contours: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ type semanticStyle struct {
|
|||||||
// 入参: object 源对象, state 继承状态
|
// 入参: object 源对象, state 继承状态
|
||||||
// 返回: error 编译错误
|
// 返回: error 编译错误
|
||||||
func (c *semanticCompiler) DrawObject(object *GraphicObject, state RenderState) error {
|
func (c *semanticCompiler) DrawObject(object *GraphicObject, state RenderState) error {
|
||||||
|
if state.Hidden {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
switch object.Type {
|
switch object.Type {
|
||||||
case "TextObject":
|
case "TextObject":
|
||||||
return c.textObject(object.TextObject, state)
|
return c.textObject(object.TextObject, state)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ type glyphOutlineKey struct {
|
|||||||
func (r *Renderer) PositionText(object TextObject, state RenderState) (*PositionedText, error) {
|
func (r *Renderer) PositionText(object TextObject, state RenderState) (*PositionedText, error) {
|
||||||
result := &PositionedText{Run: TextRun{ID: object.ID, Text: object.Text()}}
|
result := &PositionedText{Run: TextRun{ID: object.ID, Text: object.Text()}}
|
||||||
result.Run.Boxes = make([]Box, len([]rune(result.Run.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
|
return result, nil
|
||||||
}
|
}
|
||||||
geometry, err := r.Geometry()
|
geometry, err := r.Geometry()
|
||||||
|
|||||||
+75
-24
@@ -17,11 +17,13 @@ package ofdgo
|
|||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
// RenderState 保存对象继承的样式、父变换和页面坐标裁剪,所有引用只读
|
// RenderState 保存对象继承的样式、父变换和页面坐标裁剪,所有引用只读
|
||||||
// Clip为nil表示未裁剪,空路径表示完全裁去,单位为毫米
|
// Clip为nil表示未提供裁剪,空路径表示完全裁去,单位为毫米
|
||||||
|
// Hidden表示当前对象或其上层容器不可见,不改变源对象的Visible属性
|
||||||
type RenderState struct {
|
type RenderState struct {
|
||||||
Defaults *DrawParam
|
Defaults *DrawParam
|
||||||
Parent *Matrix
|
Parent *Matrix
|
||||||
BoundaryInCTM bool
|
BoundaryInCTM bool
|
||||||
|
Hidden bool
|
||||||
Clip *GeometryPath
|
Clip *GeometryPath
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +44,19 @@ type PageGroups interface {
|
|||||||
SkipObjects(count int)
|
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 按模板、图层、注解和印章顺序访问页面,不依赖任何绘图库
|
// WalkPage 按模板、图层、注解和印章顺序访问页面,不依赖任何绘图库
|
||||||
// 页面背景由调用方处理,文字裁剪可通过几何后端加载字体,图片由访问器按需加载
|
// 页面背景由调用方处理,文字裁剪可通过几何后端加载字体,图片由访问器按需加载
|
||||||
// 入参: page 页面内容, visitor 页面访问器
|
// 入参: page 页面内容, visitor 页面访问器
|
||||||
@@ -79,7 +94,7 @@ func (r *Renderer) WalkPage(page *PageContent, visitor PageVisitor) error {
|
|||||||
}
|
}
|
||||||
if r.RenderAnnotations {
|
if r.RenderAnnotations {
|
||||||
for _, annotation := range r.Reader.Annots[page.ID] {
|
for _, annotation := range r.Reader.Annots[page.ID] {
|
||||||
if annotation.Visible != nil && !*annotation.Visible {
|
if annotation.Visible != nil && !*annotation.Visible && !pageIncludesHidden(visitor) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := r.walkAnnotation(annotation, visitor); err != nil {
|
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)
|
box, _ := ParseBox(annotation.Appearance.Boundary)
|
||||||
matrix := TranslationMatrix(box.X, box.Y)
|
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 {
|
for i := range annotation.Appearance.Objects {
|
||||||
if err := r.WalkObject(&annotation.Appearance.Objects[i], state, visitor); err != nil {
|
if err := r.WalkObject(&annotation.Appearance.Objects[i], state, visitor); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -175,11 +190,21 @@ func (r *Renderer) walkObject(object *GraphicObject, state RenderState, visitor
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if pageSkipsClips(visitor) {
|
||||||
|
state.Clip = nil
|
||||||
|
}
|
||||||
if groups, ok := visitor.(PageGroups); ok && groups.BeginObject(object) {
|
if groups, ok := visitor.(PageGroups); ok && groups.BeginObject(object) {
|
||||||
defer groups.EndObject()
|
defer groups.EndObject()
|
||||||
}
|
}
|
||||||
switch object.Type {
|
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)
|
return visitor.DrawObject(object, state)
|
||||||
case "CompositeGraphicUnit", "CompositeObject":
|
case "CompositeGraphicUnit", "CompositeObject":
|
||||||
return r.walkComposite(object.CompositeGraphicUnit, state, visitor, references)
|
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 当前资源引用链
|
// 入参: object 复合图元, state 继承状态, visitor 页面访问器, references 当前资源引用链
|
||||||
// 返回: error 几何或访问器错误
|
// 返回: error 几何或访问器错误
|
||||||
func (r *Renderer) walkComposite(object CompositeGraphicUnit, state RenderState, visitor PageVisitor, references map[*CompositeGraphicUnit]bool) 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 {
|
if groups, ok := visitor.(PageGroups); ok {
|
||||||
count, ref, seen := len(object.Objects), object.ResourceID, make(map[*CompositeGraphicUnit]bool)
|
count, ref, seen := len(object.Objects), object.ResourceID, make(map[*CompositeGraphicUnit]bool)
|
||||||
for ref != "" {
|
for ref != "" {
|
||||||
@@ -213,33 +239,42 @@ func (r *Renderer) walkComposite(object CompositeGraphicUnit, state RenderState,
|
|||||||
boundary = state.Parent.Multiply(boundary)
|
boundary = state.Parent.Multiply(boundary)
|
||||||
}
|
}
|
||||||
matrix := boundary.Multiply(NewMatrix(object.CTM))
|
matrix := boundary.Multiply(NewMatrix(object.CTM))
|
||||||
var err error
|
if pageSkipsClips(visitor) {
|
||||||
if object.Boundary != "" {
|
state.Clip = nil
|
||||||
state.Clip, err = r.geometryExtentClip(box.W, box.H, boundary, state.Clip)
|
} else {
|
||||||
|
var err error
|
||||||
|
if object.Boundary != "" {
|
||||||
|
state.Clip, err = r.geometryExtentClip(box.W, box.H, boundary, state.Clip)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.Clip, err = r.compositeExtentClip(object, matrix, state.Clip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
clips, clipMatrix := object.Clips, matrix
|
||||||
state.Clip, err = r.compositeExtentClip(object, matrix, state.Clip)
|
if !clips.usesObjectMatrix() {
|
||||||
if err != nil {
|
clipMatrix = boundary
|
||||||
return err
|
|
||||||
}
|
|
||||||
clips, clipMatrix := object.Clips, matrix
|
|
||||||
if !clips.usesObjectMatrix() {
|
|
||||||
clipMatrix = boundary
|
|
||||||
}
|
|
||||||
if clips != nil {
|
|
||||||
geometry, err := r.Geometry()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
state.Clip, err = geometry.Clip(r, clips, clipMatrix, state.Clip)
|
if clips != nil {
|
||||||
if err != nil {
|
geometry, err := r.Geometry()
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state.Clip, err = geometry.Clip(r, clips, clipMatrix, state.Clip)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.Defaults = r.drawParamDefaults(object.DrawParam, state.Defaults)
|
state.Defaults = r.drawParamDefaults(object.DrawParam, state.Defaults)
|
||||||
state.Parent = &matrix
|
state.Parent = &matrix
|
||||||
|
if composites, ok := visitor.(PageComposites); ok {
|
||||||
|
if err := composites.VisitComposite(&object, state); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if object.ResourceID != "" {
|
if object.ResourceID != "" {
|
||||||
if ref, _ := resourceValue(r.CompositeGraphicUnits, object.ResourceID); ref != nil {
|
if ref, _ := resourceValue(r.CompositeGraphicUnits, object.ResourceID); ref != nil {
|
||||||
if references[ref] {
|
if references[ref] {
|
||||||
@@ -269,6 +304,22 @@ func (r *Renderer) walkComposite(object CompositeGraphicUnit, state RenderState,
|
|||||||
return nil
|
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 按矢量资源尺寸裁剪内容,资源裁剪随实例变换
|
// compositeExtentClip 按矢量资源尺寸裁剪内容,资源裁剪随实例变换
|
||||||
// 入参: object 矢量资源, matrix 资源到页面矩阵, parent 父裁剪
|
// 入参: object 矢量资源, matrix 资源到页面矩阵, parent 父裁剪
|
||||||
// 返回: *GeometryPath 页面裁剪, error 尺寸或几何错误
|
// 返回: *GeometryPath 页面裁剪, error 尺寸或几何错误
|
||||||
|
|||||||
+6
-3
@@ -113,18 +113,21 @@ func (r *Renderer) childRenderer(reader *Reader) *Renderer {
|
|||||||
return renderer
|
return renderer
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadTemplate 加载模板页面并复用解析结果
|
// loadTemplate 按标识数值加载模板页面,等价引用共用解析结果
|
||||||
// 入参: templateID 模板ID
|
// 入参: templateID 模板ID
|
||||||
// 返回: *PageContent 模板页面
|
// 返回: *PageContent 模板页面
|
||||||
func (r *Renderer) loadTemplate(templateID string) *PageContent {
|
func (r *Renderer) loadTemplate(templateID string) *PageContent {
|
||||||
|
if key := editorResourceID(templateID); key != "" {
|
||||||
|
templateID = key
|
||||||
|
}
|
||||||
if page := r.templatePageCache[templateID]; page != nil {
|
if page := r.templatePageCache[templateID]; page != nil {
|
||||||
return page
|
return page
|
||||||
}
|
}
|
||||||
for _, tpl := range r.Reader.doc.CommonData.TemplatePage {
|
for _, tpl := range r.Reader.doc.CommonData.TemplatePage {
|
||||||
if tpl.ID != templateID {
|
if !sameResourceID(tpl.ID, templateID) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
page, err := r.Reader.PageContent(Page{BaseLoc: tpl.BaseLoc})
|
page, err := r.Reader.readPageContent(Page{BaseLoc: tpl.BaseLoc}, false, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ type canvasPageVisitor struct {
|
|||||||
// 入参: object 源对象, state 继承状态
|
// 入参: object 源对象, state 继承状态
|
||||||
// 返回: error 绘制错误
|
// 返回: error 绘制错误
|
||||||
func (v *canvasPageVisitor) DrawObject(object *GraphicObject, state RenderState) error {
|
func (v *canvasPageVisitor) DrawObject(object *GraphicObject, state RenderState) error {
|
||||||
|
if state.Hidden {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
clip, err := geometryToCanvasPath(state.Clip)
|
clip, err := geometryToCanvasPath(state.Clip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
+1
-1
@@ -113,7 +113,7 @@ func (r *Renderer) PageTextByIndex(index int) (*PageText, error) {
|
|||||||
if index < 0 || index >= len(doc.Pages.Page) {
|
if index < 0 || index >= len(doc.Pages.Page) {
|
||||||
return nil, fmt.Errorf("page index %d out of range", index)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-6
@@ -15,6 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -54,32 +55,53 @@ func (r *Reader) DocumentVersionCount() (int, error) {
|
|||||||
// 返回独立副本;没有版本列表时返回nil,不以版本号推测默认版本
|
// 返回独立副本;没有版本列表时返回nil,不以版本号推测默认版本
|
||||||
// 返回: []DocumentVersionInfo 版本信息, error 读取或描述错误
|
// 返回: []DocumentVersionInfo 版本信息, error 读取或描述错误
|
||||||
func (r *Reader) DocumentVersions() ([]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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for i, entry := range entries {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("version %s: %w", entry.ID, err)
|
return nil, fmt.Errorf("version %s: %w", entry.ID, err)
|
||||||
}
|
}
|
||||||
info.Index, info.Current = entry.Index, entry.Current
|
info.Index, info.Current = entry.Index, entry.Current
|
||||||
entries[i] = info
|
entries[i] = info
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return entries, nil
|
return entries, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// documentVersionEntries 读取当前文档的版本入口,不提前加载各版本描述
|
// documentVersionEntries 读取当前文档的版本入口,不提前加载各版本描述
|
||||||
// 返回: []DocumentVersionInfo 入口属性, error 读取或描述错误
|
// 返回: []DocumentVersionInfo 入口属性, error 读取或描述错误
|
||||||
func (r *Reader) 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() {
|
if r.documentIndex < 0 || r.documentIndex >= r.DocumentCount() {
|
||||||
return nil, fmt.Errorf("no docbody found")
|
return nil, fmt.Errorf("no docbody found")
|
||||||
}
|
}
|
||||||
data, err := r.readFile("OFD.xml")
|
data, err := r.readFileViewContext(ctx, "OFD.xml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -97,6 +119,9 @@ func (r *Reader) documentVersionEntries() ([]DocumentVersionInfo, error) {
|
|||||||
var result []DocumentVersionInfo
|
var result []DocumentVersionInfo
|
||||||
ids := make(map[string]bool)
|
ids := make(map[string]bool)
|
||||||
for _, node := range versions.children {
|
for _, node := range versions.children {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if !node.matchesOFD("Version") {
|
if !node.matchesOFD("Version") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -135,12 +160,19 @@ func (r *Reader) documentVersionEntries() ([]DocumentVersionInfo, error) {
|
|||||||
// 入参: id 入口中的版本标识, location 描述文件路径
|
// 入参: id 入口中的版本标识, location 描述文件路径
|
||||||
// 返回: DocumentVersionInfo 版本信息, error 读取或描述错误
|
// 返回: DocumentVersionInfo 版本信息, error 读取或描述错误
|
||||||
func (r *Reader) readDocumentVersion(id, location string) (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
|
var info DocumentVersionInfo
|
||||||
data, err := r.readFile(location)
|
data, err := r.readFileViewContext(ctx, location)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return info, err
|
return info, err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return info, err
|
return info, err
|
||||||
}
|
}
|
||||||
@@ -162,6 +194,9 @@ func (r *Reader) readDocumentVersion(id, location string) (DocumentVersionInfo,
|
|||||||
ids := map[string]bool{id: true}
|
ids := map[string]bool{id: true}
|
||||||
found := false
|
found := false
|
||||||
for _, node := range list.children {
|
for _, node := range list.children {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return DocumentVersionInfo{}, err
|
||||||
|
}
|
||||||
if !node.matchesOFD("File") {
|
if !node.matchesOFD("File") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
+119
-37
@@ -19,6 +19,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"maps"
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"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) {
|
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)}
|
graph := &versionEditGraph{files: make(map[string]*versionEditFile), used: make(map[string]bool)}
|
||||||
queue := []string{entry}
|
queue := []string{entry}
|
||||||
|
queued := map[string]bool{entry: true}
|
||||||
for len(queue) != 0 {
|
for len(queue) != 0 {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
name := queue[0]
|
name := queue[0]
|
||||||
queue = queue[1:]
|
queue = queue[1:]
|
||||||
if graph.files[name] != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
data, ok := parts[name]
|
data, ok := parts[name]
|
||||||
if ok && generated[name].stagedLeaf(data) {
|
if ok && generated[name].stagedLeaf(data) {
|
||||||
graph.files[name], graph.used[name] = &versionEditFile{}, true
|
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)
|
target := resolveResourcePath(name, base, value)
|
||||||
file.links = append(file.links, versionEditLink{node: node, attribute: attribute, target: target})
|
file.links = append(file.links, versionEditLink{node: node, attribute: attribute, target: target})
|
||||||
graph.used[target] = true
|
graph.used[target] = true
|
||||||
if kind == packageReferenceXML {
|
if kind == packageReferenceXML && !queued[target] {
|
||||||
|
queued[target] = true
|
||||||
queue = append(queue, target)
|
queue = append(queue, target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,11 +178,11 @@ func (r *Reader) versionEditGraph(ctx context.Context, entry string, parts map[s
|
|||||||
// 返回: map[string]bool 受保护路径, error 读取或结构错误
|
// 返回: map[string]bool 受保护路径, error 读取或结构错误
|
||||||
func (r *Reader) versionSharedFiles(ctx context.Context) (map[string]bool, error) {
|
func (r *Reader) versionSharedFiles(ctx context.Context) (map[string]bool, error) {
|
||||||
shared := make(map[string]bool)
|
shared := make(map[string]bool)
|
||||||
data, err := r.readFile("OFD.xml")
|
data, err := r.readFileViewContext(ctx, "OFD.xml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -196,21 +196,25 @@ func (r *Reader) versionSharedFiles(ctx context.Context) (map[string]bool, error
|
|||||||
shared[name] = true
|
shared[name] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var indexes []int
|
||||||
for index := range r.DocumentCount() {
|
for index := range r.DocumentCount() {
|
||||||
if index == r.documentIndex {
|
if index != r.documentIndex {
|
||||||
continue
|
indexes = append(indexes, index)
|
||||||
}
|
}
|
||||||
files, err := r.documentFiles(ctx, index)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
maps.Copy(shared, files)
|
|
||||||
}
|
}
|
||||||
versions, err := r.DocumentVersions()
|
files, err := r.documentFiles(ctx, indexes...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
maps.Copy(shared, files)
|
||||||
|
versions, err := r.DocumentVersionsContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, version := range versions {
|
for _, version := range versions {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if r.versionInfo != nil && version.ID == r.versionInfo.ID {
|
if r.versionInfo != nil && version.ID == r.versionInfo.ID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -253,16 +257,17 @@ func (r *Reader) versionResourceFiles(ctx context.Context, parts map[string][]by
|
|||||||
files[file.Location] = true
|
files[file.Location] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var indexes []int
|
||||||
for index := range r.DocumentCount() {
|
for index := range r.DocumentCount() {
|
||||||
if index == r.documentIndex {
|
if index != r.documentIndex {
|
||||||
continue
|
indexes = append(indexes, index)
|
||||||
}
|
}
|
||||||
other, err := r.documentFiles(ctx, index)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
maps.Copy(files, other)
|
|
||||||
}
|
}
|
||||||
|
other, err := r.documentFiles(ctx, indexes...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
maps.Copy(files, other)
|
||||||
return files, nil
|
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)
|
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 隔离文档或版本改动并同步删除清单,保留其他入口仍需使用的文件
|
// versionOutputChanges 隔离文档或版本改动并同步删除清单,保留其他入口仍需使用的文件
|
||||||
// 入参: ctx 取消上下文, changes 待写入条目, removed 待删除条目,返回前移除受保护路径, generated 本次自产页面及引用,随隔离路径更新
|
// 入参: ctx 取消上下文, changes 待写入条目, removed 待删除条目,返回前移除受保护路径, generated 本次自产页面及引用,随隔离路径更新
|
||||||
// 返回: map[string][]byte 隔离后的修改集, error 读取、引用或结构错误
|
// 返回: map[string][]byte 隔离后的修改集, error 读取、引用或结构错误
|
||||||
@@ -291,11 +344,16 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
|
|||||||
if parts == nil {
|
if parts == nil {
|
||||||
parts = make(map[string][]byte)
|
parts = make(map[string][]byte)
|
||||||
}
|
}
|
||||||
|
buffer := make([]byte, 32*1024)
|
||||||
for name, data := range parts {
|
for name, data := range parts {
|
||||||
if generated[name].stagedLeaf(data) {
|
if generated[name].stagedLeaf(data) {
|
||||||
continue
|
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)
|
delete(parts, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -344,17 +402,26 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
|
|||||||
pending = append(pending, name)
|
pending = append(pending, name)
|
||||||
}
|
}
|
||||||
for _, name := range slices.Sorted(maps.Keys(parts)) {
|
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] {
|
if name != "OFD.xml" && shared[name] && owned[name] {
|
||||||
allocate(name)
|
allocate(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
parents := make(map[string][]string)
|
parents := make(map[string][]string)
|
||||||
for _, name := range slices.Sorted(maps.Keys(graph.files)) {
|
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 {
|
for _, link := range graph.files[name].links {
|
||||||
parents[link.target] = append(parents[link.target], name)
|
parents[link.target] = append(parents[link.target], name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i := 0; i < len(pending); i++ {
|
for i := 0; i < len(pending); i++ {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
for _, name := range parents[pending[i]] {
|
for _, name := range parents[pending[i]] {
|
||||||
if shared[name] && paths[name] == "" {
|
if shared[name] && paths[name] == "" {
|
||||||
allocate(name)
|
allocate(name)
|
||||||
@@ -362,7 +429,7 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for name, file := range graph.files {
|
for name, file := range graph.files {
|
||||||
data, err := file.rewrite(paths)
|
data, err := file.rewrite(ctx, paths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -373,7 +440,7 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
|
|||||||
for source, target := range paths {
|
for source, target := range paths {
|
||||||
data, ok := parts[source]
|
data, ok := parts[source]
|
||||||
if !ok {
|
if !ok {
|
||||||
data, err = r.readFile(source)
|
data, err = r.readFileViewContext(ctx, source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -382,18 +449,18 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
|
|||||||
delete(parts, source)
|
delete(parts, source)
|
||||||
}
|
}
|
||||||
if r.versionInfo != nil {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
} else if target := paths[entry]; target != "" {
|
} else if target := paths[entry]; target != "" {
|
||||||
data, ok := parts["OFD.xml"]
|
data, ok := parts["OFD.xml"]
|
||||||
if !ok {
|
if !ok {
|
||||||
data, err = r.readFile("OFD.xml")
|
data, err = r.readFileViewContext(ctx, "OFD.xml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -418,12 +485,21 @@ func (r *Reader) versionOutputChanges(ctx context.Context, changes map[string][]
|
|||||||
}
|
}
|
||||||
|
|
||||||
// rewrite 更新已隔离文件的引用,保留其余XML及相对路径基准
|
// rewrite 更新已隔离文件的引用,保留其余XML及相对路径基准
|
||||||
// 入参: paths 原路径到新路径的映射
|
// 入参: ctx 取消上下文, paths 原路径到新路径的映射
|
||||||
// 返回: []byte 更新后的XML,未改动时借用原文, error 属性编码错误
|
// 返回: []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
|
var patches []editorXMLPatch
|
||||||
headers := make(map[*editorXML][]versionEditLink)
|
headers := make(map[*editorXML][]versionEditLink)
|
||||||
for _, link := range file.links {
|
for _, link := range file.links {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if paths[link.target] == "" {
|
if paths[link.target] == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -436,7 +512,7 @@ func (file *versionEditFile) rewrite(paths map[string]string) ([]byte, error) {
|
|||||||
for node, links := range headers {
|
for node, links := range headers {
|
||||||
data := file.data[node.start:node.end]
|
data := file.data[node.start:node.end]
|
||||||
for _, link := range links {
|
for _, link := range links {
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -445,7 +521,7 @@ func (file *versionEditFile) rewrite(paths map[string]string) ([]byte, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -462,15 +538,15 @@ func (file *versionEditFile) rewrite(paths map[string]string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// versionManifest 更新所选版本的入口与文件清单,保留已有文件标识及扩展字段
|
// versionManifest 更新所选版本的入口与文件清单,保留已有文件标识及扩展字段
|
||||||
// 入参: parts 输出条目, paths 隔离路径, used 当前内容依赖, shared 其他入口依赖, removed 当前版本移除的文件
|
// 入参: ctx 取消上下文, parts 输出条目, paths 隔离路径, used 当前内容依赖, shared 其他入口依赖, removed 当前版本移除的文件
|
||||||
// 返回: error 描述读取或XML编码错误
|
// 返回: 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
|
version := r.versionInfo
|
||||||
data, err := r.readFile(version.Location)
|
data, err := r.readFileViewContext(ctx, version.Location)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -479,6 +555,9 @@ func (r *Reader) versionManifest(parts map[string][]byte, paths map[string]strin
|
|||||||
listed := make(map[string]bool)
|
listed := make(map[string]bool)
|
||||||
ids := map[string]bool{version.ID: true}
|
ids := map[string]bool{version.ID: true}
|
||||||
for _, node := range list.children {
|
for _, node := range list.children {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if !node.matchesOFD("File") {
|
if !node.matchesOFD("File") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -500,6 +579,9 @@ func (r *Reader) versionManifest(parts map[string][]byte, paths map[string]strin
|
|||||||
var added []byte
|
var added []byte
|
||||||
serial := 0
|
serial := 0
|
||||||
for _, name := range slices.Sorted(maps.Keys(used)) {
|
for _, name := range slices.Sorted(maps.Keys(used)) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if target := paths[name]; target != "" {
|
if target := paths[name]; target != "" {
|
||||||
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)
|
name = packageAvailableName(r, parts, name)
|
||||||
entry, ok := parts["OFD.xml"]
|
entry, ok := parts["OFD.xml"]
|
||||||
if !ok {
|
if !ok {
|
||||||
entry, err = r.readFile("OFD.xml")
|
entry, err = r.readFileViewContext(ctx, "OFD.xml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(entry)
|
root, err := parseEditorXMLContext(ctx, entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-10
@@ -31,7 +31,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// editorProgress 保存准备阶段的进度检查点,内存预览使用nil
|
// editorProgress 编辑与保存阶段的进度回调,nil表示不回报进度
|
||||||
type editorProgress func(stage string, completed, total int) error
|
type editorProgress func(stage string, completed, total int) error
|
||||||
|
|
||||||
// ofdCountingWriter 记录实际输出字节数
|
// ofdCountingWriter 记录实际输出字节数
|
||||||
@@ -70,18 +70,22 @@ func (e *Editor) WriteTo(writer io.Writer) (int64, error) {
|
|||||||
|
|
||||||
// WritePagesTo 按指定顺序将页面另存为独立OFD,不修改当前文档或撤销记录
|
// WritePagesTo 按指定顺序将页面另存为独立OFD,不修改当前文档或撤销记录
|
||||||
// 保留描述信息并生成新文档标识,迁移相关资源、目录、注释和签章外观,移除指向未选页面的跳转
|
// 保留描述信息并生成新文档标识,迁移相关资源、目录、注释和签章外观,移除指向未选页面的跳转
|
||||||
// 快照只准备所选页面,关联引用由页面导入流程统一筛选
|
// 快照只准备所选页面,关联引用由页面导入流程统一筛选,沿用当前后端及字体来源
|
||||||
// 签名凭据不代表另存文件验签有效;进度与取消复用OnWriteProgress,另含snapshot、ids和commit阶段
|
// 签名凭据不代表另存文件验签有效;进度与取消复用OnWriteProgress,另含snapshot、ids和commit阶段
|
||||||
// 入参: writer 输出流, indexes 从0开始的页面索引,不可重复或为空
|
// 入参: writer 输出流, indexes 从0开始的页面索引,不可重复或为空
|
||||||
// 返回: int64 已写入字节数, error 错误信息
|
// 返回: int64 已写入字节数, error 错误信息
|
||||||
func (e *Editor) WritePagesTo(writer io.Writer, indexes []int) (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 {
|
if len(indexes) == 0 {
|
||||||
return 0, fmt.Errorf("no pages selected")
|
return 0, fmt.Errorf("no pages selected")
|
||||||
}
|
}
|
||||||
if err := e.validatePageIndexes(indexes); err != nil {
|
if err := e.validatePageIndexes(indexes); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
progress := editorProgress(e.OnWriteProgress)
|
progress := editorProgress(e.OnWriteProgress).withContext(ctx)
|
||||||
if err := progress.report("snapshot", 0, 0); err != nil {
|
if err := progress.report("snapshot", 0, 0); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -104,18 +108,24 @@ func (e *Editor) WritePagesTo(writer io.Writer, indexes []int) (int64, error) {
|
|||||||
snapshot.pages[i] = e.pages[index]
|
snapshot.pages[i] = e.pages[index]
|
||||||
positions[i] = i
|
positions[i] = i
|
||||||
}
|
}
|
||||||
|
if err := snapshot.compressPatternPaints(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
reader, err := snapshot.reader(progress)
|
reader, err := snapshot.reader(progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
selected := NewEditor()
|
selected := NewEditor()
|
||||||
|
selected.SetRenderBackends(e.backends)
|
||||||
|
selected.SetFontDirs(e.fontDirs...)
|
||||||
|
selected.SetFontFS(e.fontFS...)
|
||||||
selected.encryption = e.encryption
|
selected.encryption = e.encryption
|
||||||
id := selected.Info.DocID
|
id := selected.Info.DocID
|
||||||
selected.Info = cloneEditorData(e.Info)
|
selected.Info = cloneEditorData(e.Info)
|
||||||
selected.Info.DocID = id
|
selected.Info.DocID = id
|
||||||
selected.OnWriteProgress = e.OnWriteProgress
|
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
|
return 0, err
|
||||||
}
|
}
|
||||||
if e.output != nil {
|
if e.output != nil {
|
||||||
@@ -153,6 +163,21 @@ func (progress editorProgress) report(stage string, completed, total int) error
|
|||||||
return nil
|
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 写出未加密包,供显式明文输出与加密封装共用
|
// writePlaintext 写出未加密包,供显式明文输出与加密封装共用
|
||||||
// 入参: writer 输出流
|
// 入参: writer 输出流
|
||||||
// 返回: int64 写入字节数, error 错误信息
|
// 返回: int64 写入字节数, error 错误信息
|
||||||
@@ -220,11 +245,26 @@ func (e *Editor) writePlaintext(writer io.Writer) (int64, error) {
|
|||||||
// 入参: progress 准备进度回调
|
// 入参: progress 准备进度回调
|
||||||
// 返回: *Reader 阅读器, error 错误信息
|
// 返回: *Reader 阅读器, error 错误信息
|
||||||
func (e *Editor) reader(progress editorProgress) (*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 {
|
if err := e.validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if e.source != nil {
|
if e.source != nil {
|
||||||
return e.sourceReader(progress)
|
return e.sourceReader(ctx, progress)
|
||||||
}
|
}
|
||||||
r := &Reader{files: make(map[string][]byte), encryption: e.encryption}
|
r := &Reader{files: make(map[string][]byte), encryption: e.encryption}
|
||||||
if err := e.writeParts(func(name string, data []byte, _ bool) error {
|
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 {
|
if len(fonts)+len(images)+len(spaces) != 0 {
|
||||||
x.text("DocumentRes", "DocumentRes.xml")
|
x.text("DocumentRes", "DocumentRes.xml")
|
||||||
}
|
}
|
||||||
for _, name := range definitions {
|
for _, name := range slices.Sorted(maps.Keys(definitions)) {
|
||||||
x.text("DocumentRes", "/"+name)
|
x.text("DocumentRes", "/"+name)
|
||||||
}
|
}
|
||||||
x.end("CommonData")
|
x.end("CommonData")
|
||||||
@@ -432,7 +472,7 @@ func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, pro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, resource := range e.resources {
|
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 {
|
if err := write(resource.name, resource.data, false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -442,8 +482,8 @@ func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// usedResources 沿图层、对象及资源依赖筛选新增资源和共享文件,不修改资源池
|
// usedResources 沿图层、对象及资源依赖筛选新增资源和共享文件,不修改资源池
|
||||||
// 返回: []editorResource 新增字体, []editorResource 新增图片, []ColorSpace 新增颜色空间, []string 独立资源文件
|
// 返回: []editorResource 新增字体, []editorResource 新增图片, []ColorSpace 新增颜色空间, map[string]bool 独立资源文件集合
|
||||||
func (e *Editor) usedResources() (fonts, images []editorResource, spaces []ColorSpace, sourceFiles []string) {
|
func (e *Editor) usedResources() (fonts, images []editorResource, spaces []ColorSpace, sourceFiles map[string]bool) {
|
||||||
used := make(map[string]bool)
|
used := make(map[string]bool)
|
||||||
promoted := make(map[string]bool)
|
promoted := make(map[string]bool)
|
||||||
files := 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
|
files[name] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fonts, images, spaces, slices.Sorted(maps.Keys(files))
|
return fonts, images, spaces, files
|
||||||
}
|
}
|
||||||
|
|
||||||
// page 写出页面尺寸、图层和对象
|
// page 写出页面尺寸、图层和对象
|
||||||
|
|||||||
Reference in new issue
Block a user