mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
This commit is contained in:
1 parent
21bd592358
commit
bec13d523f
47 files changed
+1409
-493
No files matched your search
@@ -7,9 +7,10 @@ require (
|
|||||||
github.com/emmansun/gmsm v0.45.0
|
github.com/emmansun/gmsm v0.45.0
|
||||||
github.com/go-text/typesetting v0.3.5
|
github.com/go-text/typesetting v0.3.5
|
||||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||||
|
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117
|
||||||
github.com/xiaoqidun/j2kgo v1.0.0
|
github.com/xiaoqidun/j2kgo v1.0.0
|
||||||
github.com/xiaoqidun/jbig2 v1.1.5
|
github.com/xiaoqidun/jbig2 v1.1.6
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20261013134201-f88d759ae672
|
github.com/xiaoqidun/pdfgo v0.0.0-20261014001936-bc05ef67105b
|
||||||
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
|
||||||
@@ -28,7 +29,6 @@ require (
|
|||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||||
github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // indirect
|
github.com/srwiley/scanx v0.0.0-20190309010443-e94503791388 // indirect
|
||||||
github.com/tdewolff/font v0.0.0-20260822205238-d0d2f004b117 // indirect
|
|
||||||
github.com/tdewolff/minify/v2 v2.24.17 // indirect
|
github.com/tdewolff/minify/v2 v2.24.17 // indirect
|
||||||
github.com/tdewolff/parse/v2 v2.8.16 // indirect
|
github.com/tdewolff/parse/v2 v2.8.16 // indirect
|
||||||
github.com/yuin/goldmark v1.8.5 // indirect
|
github.com/yuin/goldmark v1.8.5 // indirect
|
||||||
|
|||||||
@@ -60,10 +60,10 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
|
|||||||
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||||
github.com/xiaoqidun/j2kgo v1.0.0 h1:n7F3s0H8/4x8ET0d65FGScRgTfiqbYuiiiJF/5HFI1I=
|
github.com/xiaoqidun/j2kgo v1.0.0 h1:n7F3s0H8/4x8ET0d65FGScRgTfiqbYuiiiJF/5HFI1I=
|
||||||
github.com/xiaoqidun/j2kgo v1.0.0/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
|
github.com/xiaoqidun/j2kgo v1.0.0/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.5 h1:QcqjDBe/Zp+JlxYtmhsiYG+Ft36jjPB6+INPlmaI/ls=
|
github.com/xiaoqidun/jbig2 v1.1.6 h1:IndAJsRi0YxdOLJDUUnVaiACZcWRZ7niR+FkOI+L1YA=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.5/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA=
|
github.com/xiaoqidun/jbig2 v1.1.6/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20261013134201-f88d759ae672 h1:/hCY94wuMGivF6IX6GeoPkC+sEyblNnZjUxIrb9ZqY8=
|
github.com/xiaoqidun/pdfgo v0.0.0-20261014001936-bc05ef67105b h1:QHzMep5m2n1k3ccMMXW18jPi/XBveTlVGo/n+itNJQk=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20261013134201-f88d759ae672/go.mod h1:UWsVkKkd37a3wEa/tGLUOSwccKGArwmrvEAYlmnEz0U=
|
github.com/xiaoqidun/pdfgo v0.0.0-20261014001936-bc05ef67105b/go.mod h1:UNyTl2gnsYaZlfsIsLSi+7pQ1hOLfGE0YDoAy8P6+Ok=
|
||||||
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=
|
||||||
|
|||||||
@@ -150,17 +150,21 @@ func SubsetSourceSFNT(data []byte, usage *Usage) []byte {
|
|||||||
if usage.Unsafe || len(usage.Characters)+len(usage.Glyphs) == 0 || bytes.HasPrefix(data, []byte("ttcf")) {
|
if usage.Unsafe || len(usage.Characters)+len(usage.Glyphs) == 0 || bytes.HasPrefix(data, []byte("ttcf")) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
sfnt, err := ParseSFNT(data)
|
fontTables, err := FileTables(data, 0)
|
||||||
if err != nil || !sfnt.IsTrueType && !sfnt.IsCFF || fontEmbeddingFlags(sfnt)&0x0100 != 0 {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for tag := range sfnt.Tables {
|
for tag := range fontTables {
|
||||||
switch tag {
|
switch tag {
|
||||||
case "cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "CFF ", "cvt ", "fpgm", "prep", "gasp", "kern", "vhea", "vmtx", "VORG", "hdmx", "LTSH", "VDMX", "GDEF", "GPOS", "GSUB", "meta", "DSIG", "FFTM", "PCLT":
|
case "cmap", "head", "hhea", "hmtx", "maxp", "OS/2", "post", "name", "glyf", "loca", "CFF ", "cvt ", "fpgm", "prep", "gasp", "kern", "vhea", "vmtx", "VORG", "hdmx", "LTSH", "VDMX", "GDEF", "GPOS", "GSUB", "meta", "DSIG", "FFTM", "PCLT":
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sfnt, err := ParseSFNTTables(fontTables)
|
||||||
|
if err != nil || !sfnt.IsTrueType && !sfnt.IsCFF || fontEmbeddingFlags(sfnt)&0x0100 != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
var unicodeTables []sfntCmap
|
var unicodeTables []sfntCmap
|
||||||
for _, record := range sfnt.cmaps {
|
for _, record := range sfnt.cmaps {
|
||||||
if record.format == 14 {
|
if record.format == 14 {
|
||||||
|
|||||||
+52
-62
@@ -22,16 +22,6 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
// otfTableRecord OTF表记录结构
|
|
||||||
// 字段: tag 标签, checksum 校验和, offset 偏移, length 长度, data 数据
|
|
||||||
type otfTableRecord struct {
|
|
||||||
tag string
|
|
||||||
checksum uint32
|
|
||||||
offset int
|
|
||||||
length int
|
|
||||||
data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// finite 判断数值是否有限
|
// finite 判断数值是否有限
|
||||||
// 入参: value 数值
|
// 入参: value 数值
|
||||||
// 返回: bool 是否有限
|
// 返回: bool 是否有限
|
||||||
@@ -50,8 +40,6 @@ func ExtractCollectionFont(data []byte, index int) ([]byte, error) {
|
|||||||
if len(tables["head"]) < 12 {
|
if len(tables["head"]) < 12 {
|
||||||
return nil, fmt.Errorf("invalid head font table")
|
return nil, fmt.Errorf("invalid head font table")
|
||||||
}
|
}
|
||||||
tables["head"] = bytes.Clone(tables["head"])
|
|
||||||
clear(tables["head"][8:12])
|
|
||||||
return SerializeOTF(tables)
|
return SerializeOTF(tables)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,68 +276,70 @@ func buildHmtxTable(widths []uint16) []byte {
|
|||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SerializeOTF 序列化OpenType字体结构
|
// SerializeOTF 序列化OpenType字体结构,一次分配输出,不修改输入表数据
|
||||||
|
// 复用各表校验和计算整份字体的调整值,不重复扫描表数据
|
||||||
// 入参: tables 表数据映射
|
// 入参: tables 表数据映射
|
||||||
// 返回: []byte 完整字体数据, error 错误信息
|
// 返回: []byte 完整字体数据, error 错误信息
|
||||||
func SerializeOTF(tables map[string][]byte) ([]byte, error) {
|
func SerializeOTF(tables map[string][]byte) ([]byte, error) {
|
||||||
numTables := uint16(len(tables))
|
if len(tables) > math.MaxUint16/16 {
|
||||||
|
return nil, fmt.Errorf("too many font tables")
|
||||||
|
}
|
||||||
|
headerSize := 12 + 16*len(tables)
|
||||||
|
size := uint64(headerSize)
|
||||||
|
tags := make([]string, 0, len(tables))
|
||||||
|
for tag, data := range tables {
|
||||||
|
if len(tag) != 4 {
|
||||||
|
return nil, fmt.Errorf("invalid font table tag %q", tag)
|
||||||
|
}
|
||||||
|
if tag == "head" && len(data) < 12 {
|
||||||
|
return nil, fmt.Errorf("invalid head font table")
|
||||||
|
}
|
||||||
|
size += (uint64(len(data)) + 3) &^ 3
|
||||||
|
if size > math.MaxUint32 || size > uint64(math.MaxInt) {
|
||||||
|
return nil, fmt.Errorf("font table data too large")
|
||||||
|
}
|
||||||
|
tags = append(tags, tag)
|
||||||
|
}
|
||||||
|
sort.Strings(tags)
|
||||||
|
data := make([]byte, int(size))
|
||||||
|
numTables := len(tables)
|
||||||
entrySelector := 0
|
entrySelector := 0
|
||||||
for 1<<(entrySelector+1) <= int(numTables) {
|
for 1<<(entrySelector+1) <= numTables {
|
||||||
entrySelector++
|
entrySelector++
|
||||||
}
|
}
|
||||||
searchRange := 1 << (entrySelector + 4)
|
searchRange := 1 << (entrySelector + 4)
|
||||||
rangeShift := int(numTables)*16 - searchRange
|
rangeShift := numTables*16 - searchRange
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
if tables["CFF "] != nil || tables["CFF2"] != nil {
|
if tables["CFF "] != nil || tables["CFF2"] != nil {
|
||||||
buf.WriteString("OTTO")
|
copy(data, "OTTO")
|
||||||
} else {
|
} else {
|
||||||
binary.Write(buf, binary.BigEndian, uint32(0x00010000))
|
binary.BigEndian.PutUint32(data, 0x00010000)
|
||||||
}
|
}
|
||||||
binary.Write(buf, binary.BigEndian, numTables)
|
binary.BigEndian.PutUint16(data[4:], uint16(numTables))
|
||||||
binary.Write(buf, binary.BigEndian, uint16(searchRange))
|
binary.BigEndian.PutUint16(data[6:], uint16(searchRange))
|
||||||
binary.Write(buf, binary.BigEndian, uint16(entrySelector))
|
binary.BigEndian.PutUint16(data[8:], uint16(entrySelector))
|
||||||
binary.Write(buf, binary.BigEndian, uint16(rangeShift))
|
binary.BigEndian.PutUint16(data[10:], uint16(rangeShift))
|
||||||
var tags []string
|
offset, adjustment := headerSize, 0
|
||||||
for t := range tables {
|
var checksum uint32
|
||||||
tags = append(tags, t)
|
for i, tag := range tags {
|
||||||
}
|
table := tables[tag]
|
||||||
sort.Strings(tags)
|
length := (len(table) + 3) &^ 3
|
||||||
headerSize := 12 + 16*int(numTables)
|
padded := data[offset : offset+length]
|
||||||
offset := headerSize
|
copy(padded, table)
|
||||||
var records []otfTableRecord
|
if tag == "head" {
|
||||||
for _, tag := range tags {
|
|
||||||
data := tables[tag]
|
|
||||||
pad := (4 - (len(data) % 4)) % 4
|
|
||||||
padded := make([]byte, len(data)+pad)
|
|
||||||
copy(padded, data)
|
|
||||||
if tag == "head" && len(data) >= 12 {
|
|
||||||
clear(padded[8:12])
|
clear(padded[8:12])
|
||||||
|
adjustment = offset + 8
|
||||||
}
|
}
|
||||||
cs := calcTableChecksum(padded)
|
record := data[12+i*16 : 28+i*16]
|
||||||
records = append(records, otfTableRecord{tag, cs, offset, len(data), padded})
|
copy(record, tag)
|
||||||
offset += len(padded)
|
tableChecksum := calcTableChecksum(padded)
|
||||||
|
checksum += tableChecksum
|
||||||
|
binary.BigEndian.PutUint32(record[4:], tableChecksum)
|
||||||
|
binary.BigEndian.PutUint32(record[8:], uint32(offset))
|
||||||
|
binary.BigEndian.PutUint32(record[12:], uint32(len(table)))
|
||||||
|
offset += length
|
||||||
}
|
}
|
||||||
for _, r := range records {
|
if adjustment != 0 {
|
||||||
buf.WriteString(r.tag)
|
binary.BigEndian.PutUint32(data[adjustment:], 0xB1B0AFBA-checksum-calcTableChecksum(data[:headerSize]))
|
||||||
binary.Write(buf, binary.BigEndian, r.checksum)
|
|
||||||
binary.Write(buf, binary.BigEndian, uint32(r.offset))
|
|
||||||
binary.Write(buf, binary.BigEndian, uint32(r.length))
|
|
||||||
}
|
}
|
||||||
for _, r := range records {
|
return data, nil
|
||||||
buf.Write(r.data)
|
|
||||||
}
|
|
||||||
fullData := buf.Bytes()
|
|
||||||
for _, r := range records {
|
|
||||||
if r.tag == "head" {
|
|
||||||
adjOffset := r.offset + 8
|
|
||||||
if adjOffset+4 <= len(fullData) {
|
|
||||||
binary.BigEndian.PutUint32(fullData[adjOffset:], 0)
|
|
||||||
cs := calcTableChecksum(fullData)
|
|
||||||
checksumAdj := 0xB1B0AFBA - cs
|
|
||||||
binary.BigEndian.PutUint32(fullData[adjOffset:], checksumAdj)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fullData, nil
|
|
||||||
}
|
}
|
||||||
@@ -125,6 +125,7 @@ func compressionImageSize(width, height float64, dpi int) image.Point {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
@@ -152,7 +153,9 @@ func (r *Reader) compressionImageSizes(ctx context.Context, images []ImageInfo,
|
|||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := renderer.WalkPage(page, visitor); err != nil {
|
content := *page
|
||||||
|
content.Template = nil
|
||||||
|
if err := renderer.WalkPage(&content, visitor); err != nil {
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-29
@@ -18,10 +18,12 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"hash/crc32"
|
"hash/crc32"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
||||||
"github.com/xiaoqidun/pdfgo"
|
"github.com/xiaoqidun/pdfgo"
|
||||||
@@ -46,7 +48,7 @@ func outputImageFormat(data []byte) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// optimizeImageResource 将可完整保留的BMP及TIFF转为PNG,再复用图像压缩和降采样策略
|
// optimizeImageResource 优化可完整保留的BMP及TIFF,复用图像压缩和降采样策略
|
||||||
// 原始模式及无法保留的色彩配置不转换,仅采用小于原始编码的结果
|
// 原始模式及无法保留的色彩配置不转换,仅采用小于原始编码的结果
|
||||||
// 入参: ctx 取消上下文, data 图片编码, options 压缩选项, size 像素需求
|
// 入参: ctx 取消上下文, data 图片编码, options 压缩选项, size 像素需求
|
||||||
// 返回: []byte 图片编码,可能引用输入, error 解码、编码或取消错误
|
// 返回: []byte 图片编码,可能引用输入, error 解码、编码或取消错误
|
||||||
@@ -124,13 +126,13 @@ func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// optimizeRasterResource 编码已解码的资源图片,同步降采样后的像素密度,仅采用更小结果
|
// optimizeRasterResource 编码已解码的资源图片,同步降采样后的像素密度,仅采用更小结果
|
||||||
// 绝对密度图片先降采样再编码,密度按最终尺寸换算后量化为PNG整数
|
// 先降采样再编码,绝对密度按最终尺寸量化,相对密度保留精确比例
|
||||||
|
// 有密度时保留PNG,未知密度按最终像素选择编码格式
|
||||||
// 入参: ctx 取消上下文, data 原编码, pixels 像素, options 压缩选项, size 像素需求, density 像素密度
|
// 入参: ctx 取消上下文, data 原编码, pixels 像素, options 压缩选项, size 像素需求, density 像素密度
|
||||||
// 返回: []byte 图片编码, error 编码或取消错误
|
// 返回: []byte 图片编码, error 编码或取消错误
|
||||||
func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image, options CompressionOptions, size image.Point, density rasterDensity) ([]byte, error) {
|
func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image, options CompressionOptions, size image.Point, density rasterDensity) ([]byte, error) {
|
||||||
hasDensity := density.x != 0 || density.y != 0
|
hasDensity := density.x != 0 || density.y != 0
|
||||||
if hasDensity && density.unit == 1 {
|
before := pixels.Bounds().Size()
|
||||||
before := pixels.Bounds()
|
|
||||||
if options.Mode == CompressionLossy {
|
if options.Mode == CompressionLossy {
|
||||||
var err error
|
var err error
|
||||||
pixels, err = pdfgo.ResizeImage(ctx, pixels, size)
|
pixels, err = pdfgo.ResizeImage(ctx, pixels, size)
|
||||||
@@ -138,29 +140,37 @@ func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
density.x *= float64(pixels.Bounds().Dx()) / float64(before.Dx())
|
if after := pixels.Bounds().Size(); hasDensity && before != after {
|
||||||
density.y *= float64(pixels.Bounds().Dy()) / float64(before.Dy())
|
if density.unit == 0 {
|
||||||
size = image.Point{}
|
ratio := new(big.Rat).SetFrac64(int64(density.x)*int64(after.X), int64(density.y)*int64(after.Y))
|
||||||
|
ratio.Mul(ratio, new(big.Rat).SetFrac64(int64(before.Y), int64(before.X)))
|
||||||
|
if ratio.Num().BitLen() > 31 || ratio.Denom().BitLen() > 31 {
|
||||||
|
return data, ctx.Err()
|
||||||
}
|
}
|
||||||
|
density.x, density.y = float64(ratio.Num().Int64()), float64(ratio.Denom().Int64())
|
||||||
|
} else {
|
||||||
|
density.x *= float64(after.X) / float64(before.X)
|
||||||
|
density.y *= float64(after.Y) / float64(before.Y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var candidate []byte
|
||||||
|
var err error
|
||||||
|
if hasDensity {
|
||||||
var encoded bytes.Buffer
|
var encoded bytes.Buffer
|
||||||
limit := &outputCompressionBuffer{buffer: &encoded, limit: outputOptimizationBufferLimit}
|
err = pdfgo.EncodePNGWithOptions(ctx, &encoded, pixels, options)
|
||||||
if err := pdfgo.EncodePNG(ctx, limit, pixels); err != nil {
|
candidate = encoded.Bytes()
|
||||||
if limit.exceeded {
|
} else {
|
||||||
|
candidate, err = pdfgo.EncodeImageResource(ctx, pixels, options)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, io.ErrShortBuffer) {
|
||||||
return data, ctx.Err()
|
return data, ctx.Err()
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
input := encoded.Bytes()
|
|
||||||
if hasDensity && density.unit == 1 {
|
|
||||||
var err error
|
|
||||||
input, err = pdfgo.OptimizeImageSize(ctx, input, options, size)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hasDensity {
|
if hasDensity {
|
||||||
x, y := math.Round(density.x), math.Round(density.y)
|
x, y := math.Round(density.x), math.Round(density.y)
|
||||||
if x > math.MaxInt32 || y > math.MaxInt32 || density.x > 0 && x < 1 || density.y > 0 && y < 1 || len(input) > outputOptimizationBufferLimit-21 {
|
if x > math.MaxInt32 || y > math.MaxInt32 || density.x > 0 && x < 1 || density.y > 0 && y < 1 || len(candidate) > outputOptimizationBufferLimit-21 {
|
||||||
return data, ctx.Err()
|
return data, ctx.Err()
|
||||||
}
|
}
|
||||||
chunk := make([]byte, 21)
|
chunk := make([]byte, 21)
|
||||||
@@ -172,18 +182,10 @@ func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image
|
|||||||
binary.BigEndian.PutUint32(chunk[17:], crc32.ChecksumIEEE(chunk[4:17]))
|
binary.BigEndian.PutUint32(chunk[17:], crc32.ChecksumIEEE(chunk[4:17]))
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
writer := imageMetadataWriter{writer: &output, skip: 33, data: chunk}
|
writer := imageMetadataWriter{writer: &output, skip: 33, data: chunk}
|
||||||
if _, err := writer.Write(input); err != nil {
|
if _, err := writer.Write(candidate); err != nil {
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
input = output.Bytes()
|
|
||||||
}
|
|
||||||
candidate := input
|
|
||||||
if !hasDensity || density.unit == 0 {
|
|
||||||
var err error
|
|
||||||
candidate, err = pdfgo.OptimizeImageResource(ctx, input, options, size)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
candidate = output.Bytes()
|
||||||
}
|
}
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
+46
-11
@@ -471,7 +471,15 @@ func (e *Editor) ResizePages(indexes []int, width, height float64) error {
|
|||||||
// 入参: data 图片数据
|
// 入参: data 图片数据
|
||||||
// 返回: string 图片资源标识, error 错误信息
|
// 返回: string 图片资源标识, error 错误信息
|
||||||
func (e *Editor) AddImage(data []byte) (string, error) {
|
func (e *Editor) AddImage(data []byte) (string, error) {
|
||||||
return e.addDecodedImage(data, nil)
|
return e.AddImageContext(context.Background(), data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddImageContext 注册图片资源,解析、标识扫描及黑白压缩期间响应取消
|
||||||
|
// 图片格式及编码保留规则与AddImage相同,取消时不注册新资源
|
||||||
|
// 入参: ctx 取消上下文, data 图片数据
|
||||||
|
// 返回: string 图片资源标识, error 注册或取消错误
|
||||||
|
func (e *Editor) AddImageContext(ctx context.Context, data []byte) (string, error) {
|
||||||
|
return e.addDecodedImageContext(ctx, data, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImageSize 获取图片资源的像素尺寸,优先复用注册结果,不解码像素
|
// ImageSize 获取图片资源的像素尺寸,优先复用注册结果,不解码像素
|
||||||
@@ -485,32 +493,59 @@ func (e *Editor) ImageSize(id string) (image.Point, error) {
|
|||||||
// 入参: 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 := baseimage.DecodeConfigData(data)
|
return e.addDecodedImageContext(context.Background(), data, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addDecodedImageContext 注册图片并复用原编码摘要及像素,重复资源不再解析
|
||||||
|
// 入参: ctx 取消上下文, data 图片数据, decoded 已解码像素,为nil时按需解码
|
||||||
|
// 返回: string 资源标识, error 注册或取消错误
|
||||||
|
func (e *Editor) addDecodedImageContext(ctx context.Context, data []byte, decoded image.Image) (string, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
checksum := sha256.Sum256(data)
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if id := e.resourceID[editorResourceKey{checksum: checksum, index: -1}]; id != "" {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
config, format, err := baseimage.DecodeConfigDataContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return e.addImage(data, config, format, decoded)
|
return e.addImage(ctx, data, image.Pt(config.Width, config.Height), format, decoded, checksum)
|
||||||
}
|
}
|
||||||
|
|
||||||
// addImage 注册图片资源,复用调用方已完成解码的像素数据
|
// addImage 注册图片资源,复用调用方已完成解码的像素数据
|
||||||
// 入参: data 图片编码数据, config 图片配置, format 图片格式, decoded 已解码图像,可为nil
|
// 入参: ctx 取消上下文, data 图片编码数据, size 像素尺寸, format 图片格式, decoded 已解码图像,可为nil, checksum 原编码摘要
|
||||||
// 返回: string 资源标识, error 格式或资源错误
|
// 返回: string 资源标识, error 格式或资源错误
|
||||||
func (e *Editor) addImage(data []byte, config image.Config, format string, decoded image.Image) (string, error) {
|
func (e *Editor) addImage(ctx context.Context, data []byte, size image.Point, format string, decoded image.Image, checksum [32]byte) (string, error) {
|
||||||
if (format != "png" && format != "jpeg" && format != "bmp" && format != "tiff" && format != "jbig2") || config.Width <= 0 || config.Height <= 0 {
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if (format != "png" && format != "jpeg" && format != "bmp" && format != "tiff" && format != "jbig2") || size.X <= 0 || size.Y <= 0 {
|
||||||
return "", fmt.Errorf("only PNG, JPEG, BMP, single-page TIFF 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: checksum, index: -1}
|
||||||
if id, ok := e.resourceID[key]; ok {
|
if id, ok := e.resourceID[key]; ok {
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
if err := e.prepareSourceIDs(); err != nil {
|
if err := e.prepareSourceIDsContext(ctx); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if format == "png" && uint64(config.Width)*uint64(config.Height) <= 64<<20 {
|
if format == "png" && uint64(size.X)*uint64(size.Y) <= 64<<20 {
|
||||||
if compact := editorBinaryImage(data, decoded); compact != nil {
|
compact, err := editorBinaryImage(ctx, data, decoded)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if compact != nil {
|
||||||
data, format = compact, "jbig2"
|
data, format = compact, "jbig2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
id := e.nextID()
|
id := e.nextID()
|
||||||
extension := format
|
extension := format
|
||||||
if extension == "jpeg" {
|
if extension == "jpeg" {
|
||||||
@@ -524,7 +559,7 @@ func (e *Editor) addImage(data []byte, config image.Config, format string, decod
|
|||||||
data: bytes.Clone(data),
|
data: bytes.Clone(data),
|
||||||
image: &MultiMedia{ID: id, Type: "Image", Format: strings.ToUpper(format), MediaFile: "/" + name},
|
image: &MultiMedia{ID: id, Type: "Image", Format: strings.ToUpper(format), MediaFile: "/" + name},
|
||||||
})
|
})
|
||||||
e.images[id] = image.Pt(config.Width, config.Height)
|
e.images[id] = size
|
||||||
e.resourceID[key] = id
|
e.resourceID[key] = id
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,7 +453,14 @@ func (e *Editor) annotationFiles() []string {
|
|||||||
// 入参: base 原包, parts 修改的文件
|
// 入参: base 原包, parts 修改的文件
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) commitAnnotationParts(base *editorSource, parts map[string][]byte) error {
|
func (e *Editor) commitAnnotationParts(base *editorSource, parts map[string][]byte) error {
|
||||||
parts, err := base.reader.versionChanges(context.Background(), parts)
|
return e.commitAnnotationPartsContext(context.Background(), base, parts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// commitAnnotationPartsContext 准备并提交独立包快照,取消时不更新编辑状态
|
||||||
|
// 入参: ctx 取消上下文, base 原包, parts 修改的文件
|
||||||
|
// 返回: error 结构、读取或取消错误
|
||||||
|
func (e *Editor) commitAnnotationPartsContext(ctx context.Context, base *editorSource, parts map[string][]byte) error {
|
||||||
|
parts, err := base.reader.versionChanges(ctx, parts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -462,25 +469,36 @@ func (e *Editor) commitAnnotationParts(base *editorSource, parts map[string][]by
|
|||||||
files = make(map[string][]byte)
|
files = make(map[string][]byte)
|
||||||
}
|
}
|
||||||
for name, data := range parts {
|
for name, data := range parts {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if file, ok := base.reader.packageFile(name); ok {
|
if file, ok := base.reader.packageFile(name); ok {
|
||||||
name = cleanPackagePath(file.Name)
|
name = cleanPackagePath(file.Name)
|
||||||
}
|
}
|
||||||
files[name] = data
|
files[name] = data
|
||||||
}
|
}
|
||||||
reader := &Reader{Zip: base.reader.Zip, files: files, encryption: base.reader.encryption, documentIndex: base.reader.documentIndex, selectedVersion: base.reader.selectedVersion}
|
reader := &Reader{Zip: base.reader.Zip, files: files, encryption: base.reader.encryption, documentIndex: base.reader.documentIndex, selectedVersion: base.reader.selectedVersion}
|
||||||
if err := reader.initRoot(); err != nil {
|
if err := reader.initRootContext(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
doc, err := reader.docStructure()
|
doc, err := reader.docStructureContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, _ = reader.annotationIndex(doc)
|
_, _ = reader.annotationIndexContext(ctx, doc)
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
before, after := e.source, *base
|
before, after := e.source, *base
|
||||||
after.reader, after.document = reader, doc
|
after.reader, after.document = reader, doc
|
||||||
|
resources := e.resources
|
||||||
if before == nil {
|
if before == nil {
|
||||||
for i := range e.resources {
|
resources = slices.Clone(resources)
|
||||||
resource := &e.resources[i]
|
for i := range resources {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resource := &resources[i]
|
||||||
if resource.font != nil {
|
if resource.font != nil {
|
||||||
resource.font = cloneEditorData(resource.font)
|
resource.font = cloneEditorData(resource.font)
|
||||||
if resource.name != "" {
|
if resource.name != "" {
|
||||||
@@ -493,6 +511,10 @@ func (e *Editor) commitAnnotationParts(base *editorSource, parts map[string][]by
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.resources = resources
|
||||||
e.source = &after
|
e.source = &after
|
||||||
if change := e.recordChange(); change != nil {
|
if change := e.recordChange(); change != nil {
|
||||||
change.undo = func(e *Editor) { e.source = before }
|
change.undo = func(e *Editor) { e.source = before }
|
||||||
|
|||||||
+72
-25
@@ -29,36 +29,59 @@ import (
|
|||||||
// Attachments 获取当前编辑状态的附件列表,返回值与编辑器相互独立
|
// Attachments 获取当前编辑状态的附件列表,返回值与编辑器相互独立
|
||||||
// 返回: []Attachment 附件信息, error 结构错误
|
// 返回: []Attachment 附件信息, error 结构错误
|
||||||
func (e *Editor) Attachments() ([]Attachment, error) {
|
func (e *Editor) Attachments() ([]Attachment, error) {
|
||||||
|
return e.AttachmentsContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttachmentsContext 获取独立的附件列表,读取及解析期间响应取消
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: []Attachment 附件信息, error 结构或取消错误
|
||||||
|
func (e *Editor) AttachmentsContext(ctx context.Context) ([]Attachment, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if e.source == nil {
|
if e.source == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return e.source.reader.Attachments()
|
return e.source.reader.AttachmentsContext(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddAttachment 添加文档附件,保留原始数据,一次操作计入一条撤销记录
|
// AddAttachment 添加文档附件,保留原始数据,一次操作计入一条撤销记录
|
||||||
// 入参: name 显示文件名, data 文件数据
|
// 入参: name 显示文件名, data 文件数据
|
||||||
// 返回: string 附件标识, error 文件名或文档结构错误
|
// 返回: string 附件标识, error 文件名或文档结构错误
|
||||||
func (e *Editor) AddAttachment(name string, data []byte) (string, error) {
|
func (e *Editor) AddAttachment(name string, data []byte) (string, error) {
|
||||||
|
return e.AddAttachmentContext(context.Background(), name, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAttachmentContext 添加附件并响应取消,失败时不修改文档和撤销记录
|
||||||
|
// 入参: ctx 取消上下文, name 显示文件名, data 文件数据
|
||||||
|
// 返回: string 附件标识, error 文件名、结构或取消错误
|
||||||
|
func (e *Editor) AddAttachmentContext(ctx context.Context, name string, data []byte) (string, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
if err := validateAttachmentName(name); err != nil {
|
if err := validateAttachmentName(name); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
maximum, err := e.sourceMaxID(nil)
|
maximum, err := e.sourceMaxID(editorProgress(nil).withContext(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
base := e.source
|
base := e.source
|
||||||
if base == nil {
|
if base == nil {
|
||||||
base, err = e.importBase()
|
base, err = e.importBaseContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
attachments, err := base.reader.Attachments()
|
attachments, err := base.reader.attachmentsContext(ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
used := make(map[string]bool, len(attachments))
|
used := make(map[string]bool, len(attachments))
|
||||||
for _, attachment := range attachments {
|
for _, attachment := range attachments {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
used[attachment.ID] = true
|
used[attachment.ID] = true
|
||||||
}
|
}
|
||||||
id, maximum := nextAttachmentID(used, maximum)
|
id, maximum := nextAttachmentID(used, maximum)
|
||||||
@@ -75,11 +98,11 @@ func (e *Editor) AddAttachment(name string, data []byte) (string, error) {
|
|||||||
}
|
}
|
||||||
entry = bytes.TrimPrefix(entry, []byte(xml.Header))
|
entry = bytes.TrimPrefix(entry, []byte(xml.Header))
|
||||||
docName := base.reader.documentRoot
|
docName := base.reader.documentRoot
|
||||||
docData, err := base.reader.readFile(docName)
|
docData, err := base.reader.readFileViewContext(ctx, docName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
doc, err := parseEditorXML(docData)
|
doc, err := parseEditorXMLContext(ctx, docData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -87,11 +110,11 @@ func (e *Editor) AddAttachment(name string, data []byte) (string, error) {
|
|||||||
parts := map[string][]byte{file: bytes.Clone(data)}
|
parts := map[string][]byte{file: bytes.Clone(data)}
|
||||||
if strings.TrimSpace(base.document.Attachments.Path) != "" {
|
if strings.TrimSpace(base.document.Attachments.Path) != "" {
|
||||||
name := base.reader.ResPath(base.document.Attachments.Path)
|
name := base.reader.ResPath(base.document.Attachments.Path)
|
||||||
existing, err := base.reader.readFile(name)
|
existing, err := base.reader.readFileViewContext(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(existing)
|
root, err := parseEditorXMLContext(ctx, existing)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -127,7 +150,7 @@ func (e *Editor) AddAttachment(name string, data []byte) (string, error) {
|
|||||||
}
|
}
|
||||||
parts[docName] = editorPatchXML(docData, []editorXMLPatch{{position, position, doc.textXML("Attachments", "/"+name)}})
|
parts[docName] = editorPatchXML(docData, []editorXMLPatch{{position, position, doc.textXML("Attachments", "/"+name)}})
|
||||||
}
|
}
|
||||||
if err := e.commitAnnotationParts(base, parts); err != nil {
|
if err := e.commitAnnotationPartsContext(ctx, base, parts); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
e.maxID, e.source.idsReady = maximum, true
|
e.maxID, e.source.idsReady = maximum, true
|
||||||
@@ -152,10 +175,20 @@ func nextAttachmentID(used map[string]bool, maximum int) (string, int) {
|
|||||||
// 入参: id 附件标识, name 显示名称
|
// 入参: id 附件标识, name 显示名称
|
||||||
// 返回: error 名称或附件结构错误
|
// 返回: error 名称或附件结构错误
|
||||||
func (e *Editor) RenameAttachment(id, name string) error {
|
func (e *Editor) RenameAttachment(id, name string) error {
|
||||||
|
return e.RenameAttachmentContext(context.Background(), id, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenameAttachmentContext 修改附件名称并响应取消,保留内容、格式及扩展字段
|
||||||
|
// 入参: ctx 取消上下文, id 附件标识, name 显示名称
|
||||||
|
// 返回: error 名称、结构或取消错误
|
||||||
|
func (e *Editor) RenameAttachmentContext(ctx context.Context, id, name string) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := validateAttachmentName(name); err != nil {
|
if err := validateAttachmentName(name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return e.editAttachment(id, func(data []byte, node *editorXML, parts map[string][]byte) ([]byte, error) {
|
return e.editAttachment(ctx, id, func(data []byte, node *editorXML, parts map[string][]byte) ([]byte, error) {
|
||||||
if node.attr("Name") == name {
|
if node.attr("Name") == name {
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
@@ -168,16 +201,17 @@ func (e *Editor) RenameAttachment(id, name string) error {
|
|||||||
// 入参: id 附件标识, data 文件数据,调用后可复用
|
// 入参: id 附件标识, data 文件数据,调用后可复用
|
||||||
// 返回: error 附件结构或提交错误
|
// 返回: error 附件结构或提交错误
|
||||||
func (e *Editor) ReplaceAttachment(id string, data []byte) error {
|
func (e *Editor) ReplaceAttachment(id string, data []byte) error {
|
||||||
return e.replaceAttachment(context.Background(), id, data)
|
return e.ReplaceAttachmentContext(context.Background(), id, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// replaceAttachment 比较附件内容并提交替换,传递内容读取过程中的取消状态
|
// ReplaceAttachmentContext 替换附件内容,比较、标识扫描及快照提交期间响应取消
|
||||||
|
// 内容相同不产生修改,失败或取消时保留原文档及撤销记录
|
||||||
// 入参: ctx 取消上下文, id 附件标识, data 新文件数据
|
// 入参: ctx 取消上下文, id 附件标识, data 新文件数据
|
||||||
// 返回: error 读取、取消或提交错误
|
// 返回: error 读取、取消或提交错误
|
||||||
func (e *Editor) replaceAttachment(ctx context.Context, id string, data []byte) error {
|
func (e *Editor) ReplaceAttachmentContext(ctx context.Context, id string, data []byte) error {
|
||||||
var maximum int
|
var maximum int
|
||||||
changed := false
|
changed := false
|
||||||
err := e.editAttachment(id, func(fragment []byte, node *editorXML, parts map[string][]byte) ([]byte, error) {
|
err := e.editAttachment(ctx, id, func(fragment []byte, node *editorXML, parts map[string][]byte) ([]byte, error) {
|
||||||
if child := node.child("FileLoc"); child != nil {
|
if child := node.child("FileLoc"); child != nil {
|
||||||
location := strings.TrimSpace(editorImportText(fragment, child))
|
location := strings.TrimSpace(editorImportText(fragment, child))
|
||||||
if location != "" {
|
if location != "" {
|
||||||
@@ -192,7 +226,7 @@ func (e *Editor) replaceAttachment(ctx context.Context, id string, data []byte)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
maximum, err = e.sourceMaxID(nil)
|
maximum, err = e.sourceMaxID(editorProgress(nil).withContext(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -208,7 +242,7 @@ func (e *Editor) replaceAttachment(ctx context.Context, id string, data []byte)
|
|||||||
} else {
|
} else {
|
||||||
fragment = editorPatchXML(fragment, []editorXMLPatch{{node.open, node.open, node.textXML("FileLoc", "/"+file)}})
|
fragment = editorPatchXML(fragment, []editorXMLPatch{{node.open, node.open, node.textXML("FileLoc", "/"+file)}})
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(fragment)
|
root, err := parseEditorXMLContext(ctx, fragment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -233,15 +267,25 @@ func (e *Editor) replaceAttachment(ctx context.Context, id string, data []byte)
|
|||||||
// 入参: id 附件标识
|
// 入参: id 附件标识
|
||||||
// 返回: error 附件结构或提交错误
|
// 返回: error 附件结构或提交错误
|
||||||
func (e *Editor) DeleteAttachment(id string) error {
|
func (e *Editor) DeleteAttachment(id string) error {
|
||||||
return e.editAttachment(id, func([]byte, *editorXML, map[string][]byte) ([]byte, error) {
|
return e.DeleteAttachmentContext(context.Background(), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAttachmentContext 删除附件声明并响应取消,成功后可撤销及重做
|
||||||
|
// 入参: ctx 取消上下文, id 附件标识
|
||||||
|
// 返回: error 结构或取消错误
|
||||||
|
func (e *Editor) DeleteAttachmentContext(ctx context.Context, id string) error {
|
||||||
|
return e.editAttachment(ctx, id, func([]byte, *editorXML, map[string][]byte) ([]byte, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// editAttachment 对单个附件片段执行原子编辑,兼容独立附件文件及内联声明
|
// editAttachment 对单个附件片段执行原子编辑,兼容独立附件文件及内联声明
|
||||||
// 入参: id 附件标识, change 片段及资源修改函数
|
// 入参: ctx 取消上下文, id 附件标识, change 片段及资源修改函数
|
||||||
// 返回: error 结构、标识或提交错误
|
// 返回: error 结构、标识或提交错误
|
||||||
func (e *Editor) editAttachment(id string, change func([]byte, *editorXML, map[string][]byte) ([]byte, error)) error {
|
func (e *Editor) editAttachment(ctx context.Context, id string, change func([]byte, *editorXML, map[string][]byte) ([]byte, error)) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
base := e.source
|
base := e.source
|
||||||
if base == nil {
|
if base == nil {
|
||||||
return fmt.Errorf("attachment not found: %s", id)
|
return fmt.Errorf("attachment not found: %s", id)
|
||||||
@@ -251,11 +295,11 @@ func (e *Editor) editAttachment(id string, change func([]byte, *editorXML, map[s
|
|||||||
if external {
|
if external {
|
||||||
name = base.reader.ResPath(base.document.Attachments.Path)
|
name = base.reader.ResPath(base.document.Attachments.Path)
|
||||||
}
|
}
|
||||||
data, err := base.reader.readFile(name)
|
data, err := base.reader.readFileViewContext(ctx, name)
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -265,6 +309,9 @@ func (e *Editor) editAttachment(id string, change func([]byte, *editorXML, map[s
|
|||||||
var found *editorXML
|
var found *editorXML
|
||||||
if root != nil {
|
if root != nil {
|
||||||
for _, child := range root.children {
|
for _, child := range root.children {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if child.matchesOFD("Attachment") && child.attr("ID") == id {
|
if child.matchesOFD("Attachment") && child.attr("ID") == id {
|
||||||
if found != nil {
|
if found != nil {
|
||||||
return fmt.Errorf("duplicate attachment ID: %s", id)
|
return fmt.Errorf("duplicate attachment ID: %s", id)
|
||||||
@@ -276,11 +323,11 @@ func (e *Editor) editAttachment(id string, change func([]byte, *editorXML, map[s
|
|||||||
if found == nil {
|
if found == nil {
|
||||||
return fmt.Errorf("attachment not found: %s", id)
|
return fmt.Errorf("attachment not found: %s", id)
|
||||||
}
|
}
|
||||||
fragment, err := editorXMLStandalone(data[found.start:found.end], found)
|
fragment, err := editorXMLStandaloneContext(ctx, data[found.start:found.end], found)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
node, err := parseEditorXML(fragment)
|
node, err := parseEditorXMLContext(ctx, fragment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -290,10 +337,10 @@ func (e *Editor) editAttachment(id string, change func([]byte, *editorXML, map[s
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if bytes.Equal(updated, fragment) && len(parts) == 0 {
|
if bytes.Equal(updated, fragment) && len(parts) == 0 {
|
||||||
return nil
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
parts[name] = editorPatchXML(data, []editorXMLPatch{{found.start, found.end, updated}})
|
parts[name] = editorPatchXML(data, []editorXMLPatch{{found.start, found.end, updated}})
|
||||||
if err := e.commitAnnotationParts(base, parts); err != nil {
|
if err := e.commitAnnotationPartsContext(ctx, base, parts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(updated) == 0 {
|
if len(updated) == 0 {
|
||||||
|
|||||||
@@ -179,7 +179,11 @@ func (e *Editor) editBookmark(name string, next *Bookmark, add bool) error {
|
|||||||
}
|
}
|
||||||
parts := make(map[string][]byte)
|
parts := make(map[string][]byte)
|
||||||
if rename || next == nil {
|
if rename || next == nil {
|
||||||
pending, err := e.sourcePartsReferences(nil, nil)
|
ctx := context.Background()
|
||||||
|
if e.output != nil {
|
||||||
|
ctx = e.output.ctx
|
||||||
|
}
|
||||||
|
pending, err := e.sourcePartsReferences(ctx, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -242,29 +246,12 @@ func (e *Editor) editBookmark(name string, next *Bookmark, add bool) error {
|
|||||||
// 返回: error 快照重建或页面加载错误。
|
// 返回: error 快照重建或页面加载错误。
|
||||||
func (e *Editor) commitBookmarkSnapshot(reader *Reader) error {
|
func (e *Editor) commitBookmarkSnapshot(reader *Reader) error {
|
||||||
before := e.transactionSnapshot()
|
before := e.transactionSnapshot()
|
||||||
fonts, images, spaces, files := e.usedResources()
|
|
||||||
used := make(map[string]bool)
|
|
||||||
for _, resource := range fonts {
|
|
||||||
used[resource.font.ID] = true
|
|
||||||
}
|
|
||||||
for _, resource := range images {
|
|
||||||
used[resource.image.ID] = true
|
|
||||||
}
|
|
||||||
for _, space := range spaces {
|
|
||||||
used[space.ID] = true
|
|
||||||
}
|
|
||||||
if err := e.commitResourceSnapshot(context.Background(), reader); err != nil {
|
if err := e.commitResourceSnapshot(context.Background(), reader); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
e.maxID = max(e.maxID, before.maxID)
|
e.maxID = max(e.maxID, before.maxID)
|
||||||
e.fonts, e.images, e.resourceID = before.fonts, before.images, before.resourceID
|
e.fonts, e.images, e.resourceID = before.fonts, before.images, before.resourceID
|
||||||
e.fontRegistration = before.fontRegistration
|
e.fontRegistration = before.fontRegistration
|
||||||
for _, resource := range before.resources {
|
|
||||||
if resource.font != nil && used[resource.font.ID] || resource.image != nil && used[resource.image.ID] || resource.space != nil && used[resource.space.ID] || files[resource.name] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
e.resources = append(e.resources, resource)
|
|
||||||
}
|
|
||||||
for i, page := range before.pages {
|
for i, page := range before.pages {
|
||||||
states := make(map[string]GraphicObject)
|
states := make(map[string]GraphicObject)
|
||||||
for _, layer := range page.Content.Layer {
|
for _, layer := range page.Content.Layer {
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ func (e *Editor) AddColorSpace(space ColorSpace, profile []byte) (string, error)
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if space.Profile == "" {
|
||||||
|
resource := e.registeredResource(id)
|
||||||
|
if resource.space != nil && resource.space.Type == space.Type {
|
||||||
|
space.Profile = "/" + resource.name
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if space.profile == nil {
|
if space.profile == nil {
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1085,13 +1086,25 @@ func (e *Editor) compositeResource(id string, data []byte) (editorResource, erro
|
|||||||
// 入参: data 资源XML
|
// 入参: data 资源XML
|
||||||
// 返回: []string 引用标识, error 错误信息
|
// 返回: []string 引用标识, error 错误信息
|
||||||
func editorResourceReferences(data []byte) ([]string, error) {
|
func editorResourceReferences(data []byte) ([]string, error) {
|
||||||
root, err := parseEditorXML(data)
|
return editorResourceReferencesContext(nil, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorResourceReferencesContext 收集标准资源引用,在解析及遍历期间响应取消
|
||||||
|
// 入参: ctx 取消上下文,可为nil, data 资源XML
|
||||||
|
// 返回: []string 引用标识, error 解析或取消错误
|
||||||
|
func editorResourceReferencesContext(ctx context.Context, data []byte) ([]string, error) {
|
||||||
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
refs := make(map[string]bool)
|
refs := make(map[string]bool)
|
||||||
var collect func(*editorXML)
|
var collect func(*editorXML) error
|
||||||
collect = func(node *editorXML) {
|
collect = func(node *editorXML) error {
|
||||||
|
if ctx != nil {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if classifyOFDNamespace(node.name.Space) != ofdXMLUnknown {
|
if classifyOFDNamespace(node.name.Space) != ofdXMLUnknown {
|
||||||
if node.name.Local == "Thumbnail" || node.name.Local == "Substitution" {
|
if node.name.Local == "Thumbnail" || node.name.Local == "Substitution" {
|
||||||
if id := editorResourceID(editorImportText(data, node)); id != "" {
|
if id := editorResourceID(editorImportText(data, node)); id != "" {
|
||||||
@@ -1105,12 +1118,23 @@ func editorResourceReferences(data []byte) ([]string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, child := range node.children {
|
for _, child := range node.children {
|
||||||
collect(child)
|
if err := collect(child); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
collect(root)
|
return nil
|
||||||
|
}
|
||||||
|
if err := collect(root); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
normalizeResourceReferences(refs)
|
normalizeResourceReferences(refs)
|
||||||
return slices.Sorted(maps.Keys(refs)), nil
|
result := slices.Sorted(maps.Keys(refs))
|
||||||
|
if ctx != nil {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectCompositeReferences 收集内联复合对象引用的新资源
|
// collectCompositeReferences 收集内联复合对象引用的新资源
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
@@ -381,8 +382,25 @@ func (e *Editor) neutralDrawParam() (string, error) {
|
|||||||
// 入参: draw 绘制参数
|
// 入参: draw 绘制参数
|
||||||
// 返回: string 资源标识, error 错误信息
|
// 返回: string 资源标识, error 错误信息
|
||||||
func (e *Editor) addEditorDrawParam(draw DrawParam) (string, error) {
|
func (e *Editor) addEditorDrawParam(draw DrawParam) (string, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
if e.output != nil && e.output.ctx != nil {
|
||||||
|
ctx = e.output.ctx
|
||||||
|
}
|
||||||
|
return e.addEditorDrawParamContext(ctx, draw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addEditorDrawParamContext 注册已校验的参数,编码及引用收集期间响应取消
|
||||||
|
// 入参: ctx 取消上下文, draw 绘制参数
|
||||||
|
// 返回: string 资源标识, error 编码、扫描或取消错误
|
||||||
|
func (e *Editor) addEditorDrawParamContext(ctx context.Context, draw DrawParam) (string, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
draw.ID, draw.ResourceID, draw.BaseLoc, draw.Link = "", "", "", ""
|
draw.ID, draw.ResourceID, draw.BaseLoc, draw.Link = "", "", "", ""
|
||||||
for _, resource := range e.resources {
|
for _, resource := range e.resources {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
if resource.draw != nil {
|
if resource.draw != nil {
|
||||||
previous := *resource.draw
|
previous := *resource.draw
|
||||||
previous.ID = ""
|
previous.ID = ""
|
||||||
@@ -391,11 +409,11 @@ func (e *Editor) addEditorDrawParam(draw DrawParam) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := e.prepareSourceIDs(); err != nil {
|
if err := e.prepareSourceIDsContext(ctx); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
draw.ID = e.nextID()
|
draw.ID = e.nextID()
|
||||||
data, err := e.encodeXML(func(x *ofdXML) {
|
data, err := encodeOFDXMLContext(ctx, func(x *ofdXML) {
|
||||||
x.root("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}})
|
x.root("Res", ofdAttrs{{Name: xml.Name{Local: "BaseLoc"}, Value: "."}})
|
||||||
x.start("DrawParams", nil)
|
x.start("DrawParams", nil)
|
||||||
attrs := ofdAttrs{}
|
attrs := ofdAttrs{}
|
||||||
@@ -431,7 +449,7 @@ func (e *Editor) addEditorDrawParam(draw DrawParam) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
refs, err := editorResourceReferences(data)
|
refs, err := editorResourceReferencesContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-3
@@ -15,6 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
@@ -23,6 +24,16 @@ import (
|
|||||||
// 入参: id 绘制参数标识
|
// 入参: id 绘制参数标识
|
||||||
// 返回: []byte 独立参数片段, error 错误信息
|
// 返回: []byte 独立参数片段, error 错误信息
|
||||||
func (e *Editor) drawParamXML(id string) ([]byte, error) {
|
func (e *Editor) drawParamXML(id string) ([]byte, error) {
|
||||||
|
return e.drawParamXMLContext(context.Background(), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawParamXMLContext 读取并提取绘制参数原文,保留扩展信息并响应取消
|
||||||
|
// 入参: ctx 取消上下文, id 绘制参数标识
|
||||||
|
// 返回: []byte 独立参数片段, error 读取、解析或取消错误
|
||||||
|
func (e *Editor) drawParamXMLContext(ctx context.Context, id string) ([]byte, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
var data []byte
|
var data []byte
|
||||||
for _, resource := range e.resources {
|
for _, resource := range e.resources {
|
||||||
if resource.draw != nil && sameResourceID(resource.draw.ID, id) {
|
if resource.draw != nil && sameResourceID(resource.draw.ID, id) {
|
||||||
@@ -33,17 +44,24 @@ func (e *Editor) drawParamXML(id string) ([]byte, error) {
|
|||||||
if data == nil && e.source != nil {
|
if data == nil && e.source != nil {
|
||||||
var err error
|
var err error
|
||||||
name, _ := resourceValue(e.source.reader.resourceFiles, id)
|
name, _ := resourceValue(e.source.reader.resourceFiles, id)
|
||||||
data, err = e.source.reader.readFile(name)
|
data, err = e.source.reader.readFileViewContext(ctx, name)
|
||||||
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
|
||||||
}
|
}
|
||||||
if node := editorResourceNode(root, "DrawParams", "DrawParam", id); node != nil {
|
if node := editorResourceNode(root, "DrawParams", "DrawParam", id); node != nil {
|
||||||
return editorXMLStandalone(data[node.start:node.end], node)
|
fragment, err := editorXMLStandaloneContext(ctx, data[node.start:node.end], node)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fragment, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("draw parameter %q not found", id)
|
return nil, fmt.Errorf("draw parameter %q not found", id)
|
||||||
}
|
}
|
||||||
|
|||||||
+98
-30
@@ -24,6 +24,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
// editorFontUsage 汇总原文档字体的字符、显式字形及无法确定的引用
|
// editorFontUsage 汇总原文档字体的字符、显式字形及无法确定的引用
|
||||||
@@ -157,7 +158,7 @@ func (e *Editor) addFontResource(parsed *FontResource, checksum [32]byte) (strin
|
|||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddExternalFont 注册不含字体文件的名称引用,由阅读环境匹配
|
// AddExternalFont 注册不含字体文件的名称引用,重复注册复用标识,由阅读环境匹配
|
||||||
// 入参: name 字体名称
|
// 入参: name 字体名称
|
||||||
// 返回: string 字体资源标识, error 错误信息
|
// 返回: string 字体资源标识, error 错误信息
|
||||||
func (e *Editor) AddExternalFont(name string) (string, error) {
|
func (e *Editor) AddExternalFont(name string) (string, error) {
|
||||||
@@ -165,8 +166,13 @@ func (e *Editor) AddExternalFont(name string) (string, error) {
|
|||||||
if name == "" {
|
if name == "" {
|
||||||
return "", fmt.Errorf("empty external font name")
|
return "", fmt.Errorf("empty external font name")
|
||||||
}
|
}
|
||||||
|
key := editorResourceKey{checksum: sha256.Sum256([]byte(name)), kind: "Font/external"}
|
||||||
|
if id := e.resourceID[key]; id != "" {
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
for _, resource := range e.resources {
|
for _, resource := range e.resources {
|
||||||
if resource.font != nil && resource.font.FontFile == "" && resource.font.FontName == name {
|
if resource.font != nil && resource.font.FontFile == "" && resource.font.FontName == name {
|
||||||
|
e.resourceID[key] = resource.font.ID
|
||||||
return resource.font.ID, nil
|
return resource.font.ID, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,6 +181,7 @@ func (e *Editor) AddExternalFont(name string) (string, error) {
|
|||||||
}
|
}
|
||||||
id := e.nextID()
|
id := e.nextID()
|
||||||
e.resources = append(e.resources, editorResource{font: &Font{ID: id, FontName: name, FamilyName: name}})
|
e.resources = append(e.resources, editorResource{font: &Font{ID: id, FontName: name, FamilyName: name}})
|
||||||
|
e.resourceID[key] = id
|
||||||
e.fontRenderer = nil
|
e.fontRenderer = nil
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
@@ -200,23 +207,49 @@ func (r *editorResourceRefs) fontUsage(id string) *editorFontUsage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// text 收集TextCode字符和CGTransform显式字形,异常编号使该字体保持原样
|
// text 收集TextCode字符和CGTransform显式字形,异常编号使该字体保持原样
|
||||||
// 入参: key 元素名称, value 元素文本
|
// 入参: ctx 取消上下文,可为nil, key 元素名称, value 元素文本
|
||||||
func (u *editorFontUsage) text(key, value string) {
|
// 返回: error 取消错误
|
||||||
|
func (u *editorFontUsage) text(ctx context.Context, key, value string) error {
|
||||||
|
if ctx != nil {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
switch key {
|
switch key {
|
||||||
case "TextCode":
|
case "TextCode":
|
||||||
for _, char := range textCodeRunes(value) {
|
chars, err := textCodeRunesContext(ctx, value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for index, char := range chars {
|
||||||
|
if ctx != nil && index&4095 == 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
u.chars[char] = true
|
u.chars[char] = true
|
||||||
}
|
}
|
||||||
case "Glyphs":
|
case "Glyphs":
|
||||||
for _, value := range strings.Fields(strings.ReplaceAll(value, ",", " ")) {
|
count := 0
|
||||||
|
for value := range strings.FieldsFuncSeq(value, func(char rune) bool { return char == ',' || unicode.IsSpace(char) }) {
|
||||||
|
if ctx != nil && count&4095 == 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
id, err := strconv.ParseUint(value, 10, 16)
|
id, err := strconv.ParseUint(value, 10, 16)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
u.unsafe = true
|
u.unsafe = true
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
u.glyphs[uint16(id)] = true
|
u.glyphs[uint16(id)] = true
|
||||||
|
count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ctx != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// merge 合并指向同一字体文件的全部资源用字
|
// merge 合并指向同一字体文件的全部资源用字
|
||||||
@@ -254,10 +287,15 @@ 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) {
|
||||||
|
ctx := context.Background()
|
||||||
|
if e.output != nil {
|
||||||
|
ctx = e.output.ctx
|
||||||
|
}
|
||||||
|
progress = progress.withContext(ctx)
|
||||||
if e.backends.FontResources == nil {
|
if e.backends.FontResources == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -269,9 +307,12 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
}) {
|
}) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
fonts, _, _, files := e.usedResources()
|
fonts, _, _, files, err := e.usedResources(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
total := len(fonts) + len(e.pages) + 2*len(e.resources)
|
total := len(fonts) + len(e.pages) + 2*len(e.resources)
|
||||||
used := make(map[string]map[uint16]bool)
|
used := make(map[string]*editorFontUsage)
|
||||||
mapped := make(map[string]bool)
|
mapped := make(map[string]bool)
|
||||||
for i, resource := range fonts {
|
for i, resource := range fonts {
|
||||||
if err := progress.report("fonts", i, total); err != nil {
|
if err := progress.report("fonts", i, total); err != nil {
|
||||||
@@ -287,10 +328,13 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
e.fonts[resource.font.ID] = resourceFont
|
e.fonts[resource.font.ID] = resourceFont
|
||||||
}
|
}
|
||||||
if resourceFont.CanSubset {
|
if resourceFont.CanSubset {
|
||||||
used[resource.font.ID] = make(map[uint16]bool)
|
used[resource.font.ID] = newEditorFontUsage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(used) == 0 {
|
if len(used) == 0 {
|
||||||
@@ -300,6 +344,10 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
return nil, progress.report("fonts", total, total)
|
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)}
|
||||||
|
scan := func(data []byte, name string) (bool, error) {
|
||||||
|
_, safe := refs.scanContext(ctx, bytes.NewReader(data), name)
|
||||||
|
return safe, ctx.Err()
|
||||||
|
}
|
||||||
nested := false
|
nested := false
|
||||||
for _, resource := range e.resources {
|
for _, resource := range e.resources {
|
||||||
if resource.draw != nil {
|
if resource.draw != nil {
|
||||||
@@ -311,8 +359,8 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
for _, name := range e.annotationFiles() {
|
for _, name := range e.annotationFiles() {
|
||||||
if data, changed := e.source.reader.files[name]; changed {
|
if data, changed := e.source.reader.files[name]; changed {
|
||||||
nested = true
|
nested = true
|
||||||
if _, safe := refs.scan(bytes.NewReader(data), name); !safe {
|
if safe, err := scan(data, name); err != nil || !safe {
|
||||||
return nil, nil
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -323,6 +371,9 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
}
|
}
|
||||||
for _, layer := range page.Content.Layer {
|
for _, layer := range page.Content.Layer {
|
||||||
for _, object := range layer.Objects {
|
for _, object := range layer.Objects {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if editorObjectFontScan(object) {
|
if editorObjectFontScan(object) {
|
||||||
nested = true
|
nested = true
|
||||||
origin := e.objectOrigin(editorObjectID(object))
|
origin := e.objectOrigin(editorObjectID(object))
|
||||||
@@ -339,8 +390,8 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, safe := refs.scan(bytes.NewReader(data), "Page.xml"); !safe {
|
if safe, err := scan(data, "Page.xml"); err != nil || !safe {
|
||||||
return nil, nil
|
return nil, err
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -348,24 +399,31 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
obj := object.TextObject
|
obj := object.TextObject
|
||||||
glyphs := used[obj.Font]
|
usage := used[obj.Font]
|
||||||
if glyphs == nil {
|
if usage == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, code := range obj.TextCode {
|
for _, code := range obj.TextCode {
|
||||||
for _, char := range textCodeRunes(code.Value) {
|
if err := usage.text(ctx, "TextCode", code.Value); err != nil {
|
||||||
glyphs[e.fonts[obj.Font].GlyphIndex(char)] = true
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, transform := range obj.CGTransform {
|
for _, transform := range obj.CGTransform {
|
||||||
mapped[obj.Font] = true
|
mapped[obj.Font] = true
|
||||||
for _, value := range strings.Fields(transform.Glyphs) {
|
count := 0
|
||||||
|
for value := range strings.FieldsSeq(transform.Glyphs) {
|
||||||
|
if count&4095 == 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
id, err := strconv.ParseUint(value, 10, 16)
|
id, err := strconv.ParseUint(value, 10, 16)
|
||||||
if err != nil || id >= uint64(e.fonts[obj.Font].NumGlyphs()) {
|
if err != nil || id >= uint64(e.fonts[obj.Font].NumGlyphs()) {
|
||||||
delete(used, obj.Font)
|
delete(used, obj.Font)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
glyphs[uint16(id)] = true
|
usage.glyphs[uint16(id)] = true
|
||||||
|
count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,18 +439,16 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
}
|
}
|
||||||
if resource.usage.matches(resource.name, resource.data) {
|
if resource.usage.matches(resource.name, resource.data) {
|
||||||
refs.merge(resource.usage.refs)
|
refs.merge(resource.usage.refs)
|
||||||
} else if _, safe := refs.scan(bytes.NewReader(resource.data), resource.name); !safe {
|
} else if safe, err := scan(resource.data, resource.name); err != nil || !safe {
|
||||||
return nil, nil
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for id, usage := range refs.fonts {
|
for id, usage := range refs.fonts {
|
||||||
glyphs := used[id]
|
collected := used[id]
|
||||||
if usage.unsafe {
|
if usage.unsafe {
|
||||||
delete(used, id)
|
delete(used, id)
|
||||||
} else if glyphs != nil {
|
} else if collected != nil {
|
||||||
for char := range usage.chars {
|
collected.merge(usage, false)
|
||||||
glyphs[e.fonts[id].GlyphIndex(char)] = true
|
|
||||||
}
|
|
||||||
if len(usage.glyphs) != 0 {
|
if len(usage.glyphs) != 0 {
|
||||||
mapped[id] = true
|
mapped[id] = true
|
||||||
for glyph := range usage.glyphs {
|
for glyph := range usage.glyphs {
|
||||||
@@ -400,7 +456,6 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
delete(used, id)
|
delete(used, id)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
glyphs[glyph] = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -415,22 +470,35 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error)
|
|||||||
if resource.font == nil {
|
if resource.font == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
glyphs := used[resource.font.ID]
|
usage := used[resource.font.ID]
|
||||||
if len(glyphs) == 0 {
|
if usage == nil || len(usage.chars) == 0 && len(usage.glyphs) == 0 {
|
||||||
resource.subset = nil
|
resource.subset = nil
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
glyphs := usage.glyphs
|
||||||
|
for char := range usage.chars {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
glyphs[e.fonts[resource.font.ID].GlyphIndex(char)] = true
|
||||||
|
}
|
||||||
glyphs[0] = true
|
glyphs[0] = true
|
||||||
ids := make([]uint16, 0, len(glyphs))
|
ids := make([]uint16, 0, len(glyphs))
|
||||||
for id := range glyphs {
|
for id := range glyphs {
|
||||||
ids = append(ids, id)
|
ids = append(ids, id)
|
||||||
}
|
}
|
||||||
slices.Sort(ids)
|
slices.Sort(ids)
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if resource.subset == nil || resource.subset.mapped != mapped[resource.font.ID] || !slices.Equal(resource.subset.glyphs, ids) {
|
if resource.subset == nil || resource.subset.mapped != mapped[resource.font.ID] || !slices.Equal(resource.subset.glyphs, ids) {
|
||||||
data, err := e.backends.FontResources.SubsetFont(resource.data, ids, mapped[resource.font.ID])
|
data, err := e.backends.FontResources.SubsetFont(resource.data, ids, mapped[resource.font.ID])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("subset font %s: %w", resource.font.FontName, err)
|
return nil, fmt.Errorf("subset font %s: %w", resource.font.FontName, err)
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if len(data) >= len(resource.data) {
|
if len(data) >= len(resource.data) {
|
||||||
data = nil
|
data = nil
|
||||||
}
|
}
|
||||||
|
|||||||
+279
-31
@@ -17,11 +17,13 @@ package ofdgo
|
|||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"maps"
|
"maps"
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -100,7 +102,7 @@ func (e *Editor) changeFont(ctx context.Context, id string, mode FontMode, file
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
reader, err := e.Reader()
|
reader, err := e.ReaderContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -112,8 +114,8 @@ func (e *Editor) changeFont(ctx context.Context, id string, mode FontMode, file
|
|||||||
if err != nil || !changed {
|
if err != nil || !changed {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if mode == FontExternal {
|
if location := scan.fonts[editorResourceID(id)].location; location != "" {
|
||||||
if err := pruneUnreferencedFiles(ctx, reader, scan.fonts[editorResourceID(id)].location); err != nil {
|
if err := pruneUnreferencedFiles(ctx, reader, location); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,7 +156,7 @@ func (e *Editor) ProcessFontsWithOptions(ctx context.Context, options FontProces
|
|||||||
if err := progress("prepare", 0, 0); err != nil {
|
if err := progress("prepare", 0, 0); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
reader, err := e.Reader()
|
reader, err := e.ReaderContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -254,7 +256,8 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen
|
|||||||
if declaration == nil {
|
if declaration == nil {
|
||||||
return nil, fmt.Errorf("font not found: %s", id)
|
return nil, fmt.Errorf("font not found: %s", id)
|
||||||
}
|
}
|
||||||
if (mode == FontEmbed) == (declaration.location != "") {
|
explicit := mode == FontEmbed && len(file.Data) != 0
|
||||||
|
if (mode == FontEmbed) == (declaration.location != "") && !explicit {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if scan.unsafe {
|
if scan.unsafe {
|
||||||
@@ -265,7 +268,6 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen
|
|||||||
}
|
}
|
||||||
var data []byte
|
var data []byte
|
||||||
var err error
|
var err error
|
||||||
explicit := mode == FontEmbed && len(file.Data) != 0
|
|
||||||
if mode == FontExternal {
|
if mode == FontExternal {
|
||||||
data, err = scan.fontCache.read(ctx, scan.reader, declaration.location)
|
data, err = scan.fontCache.read(ctx, scan.reader, declaration.location)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -291,6 +293,16 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var original FontFile
|
||||||
|
var originalErr error
|
||||||
|
sameEmbedded := false
|
||||||
|
if explicit && declaration.location != "" {
|
||||||
|
original, originalErr = originalBindingFont(ctx, scan, declaration)
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sameEmbedded = originalErr == nil && sameBindingFont(original.Data, parsed.Data)
|
||||||
|
}
|
||||||
change := &editorFontChange{patches: make(map[string][]editorXMLPatch), files: make(map[string][]byte)}
|
change := &editorFontChange{patches: make(map[string][]editorXMLPatch), files: make(map[string][]byte)}
|
||||||
verifiedGlyphs := false
|
verifiedGlyphs := false
|
||||||
for _, text := range scan.texts[editorResourceID(id)] {
|
for _, text := range scan.texts[editorResourceID(id)] {
|
||||||
@@ -299,14 +311,18 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen
|
|||||||
}
|
}
|
||||||
file, node := text.file, text.node
|
file, node := text.file, text.node
|
||||||
var object TextObject
|
var object TextObject
|
||||||
if err := xml.Unmarshal(file.data[node.start:node.end], &object); err != nil {
|
decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(file.data[node.start:node.end])), context: ctx})
|
||||||
|
if err := decoder.Decode(&object); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := checkFontBinding(object, parsed, mode); err != nil {
|
if err := checkFontBinding(ctx, object, parsed, mode); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if explicit && len(object.CGTransform) != 0 && !verifiedGlyphs {
|
if explicit && len(object.CGTransform) != 0 && !verifiedGlyphs {
|
||||||
source, err := e.resolveBindingFont(ctx, scan, declaration)
|
source, err := original, originalErr
|
||||||
|
if declaration.location == "" {
|
||||||
|
source, err = e.resolveBindingFont(ctx, scan, declaration)
|
||||||
|
}
|
||||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -323,6 +339,9 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if sameEmbedded {
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
data = declaration.file.data
|
data = declaration.file.data
|
||||||
node := declaration.node
|
node := declaration.node
|
||||||
fontFile := node.child("FontFile")
|
fontFile := node.child("FontFile")
|
||||||
@@ -353,7 +372,7 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen
|
|||||||
}
|
}
|
||||||
fragment = editorPatchXML(data[node.start:node.end], []editorXMLPatch{{fontFile.start - node.start, fontFile.end - node.start, nil}})
|
fragment = editorPatchXML(data[node.start:node.end], []editorXMLPatch{{fontFile.start - node.start, fontFile.end - node.start, nil}})
|
||||||
for _, field := range []string{"FontName", "FamilyName"} {
|
for _, field := range []string{"FontName", "FamilyName"} {
|
||||||
root, err := parseEditorXML(fragment)
|
root, err := parseEditorXMLContext(ctx, fragment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -368,7 +387,7 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen
|
|||||||
}
|
}
|
||||||
if explicit {
|
if explicit {
|
||||||
for _, field := range []struct{ name, value string }{{"FontName", parsed.Font.FontName}, {"FamilyName", parsed.Font.FamilyName}, {"Charset", "unicode"}} {
|
for _, field := range []struct{ name, value string }{{"FontName", parsed.Font.FontName}, {"FamilyName", parsed.Font.FamilyName}, {"Charset", "unicode"}} {
|
||||||
root, err := parseEditorXML(fragment)
|
root, err := parseEditorXMLContext(ctx, fragment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -386,6 +405,25 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen
|
|||||||
return change, nil
|
return change, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// originalBindingFont 提取包内原字体供字形映射核对,不使用外部同名字体代替
|
||||||
|
// 入参: ctx 取消上下文, scan 文档引用, declaration 字体声明
|
||||||
|
// 返回: FontFile 独立字体, error 读取、解析或取消错误
|
||||||
|
func originalBindingFont(ctx context.Context, scan *editorFontDocument, declaration *editorFontDeclaration) (FontFile, error) {
|
||||||
|
data, err := scan.fontCache.read(ctx, scan.reader, declaration.location)
|
||||||
|
if err != nil {
|
||||||
|
return FontFile{}, err
|
||||||
|
}
|
||||||
|
index := 0
|
||||||
|
if bytes.HasPrefix(data, []byte("ttcf")) {
|
||||||
|
index = fontCollectionIndex(data, []string{declaration.font.FontName, declaration.font.FamilyName}, declaration.font.Bold, declaration.font.Italic)
|
||||||
|
}
|
||||||
|
data, err = (FontFile{Data: data}).Face(index)
|
||||||
|
if cancelErr := ctx.Err(); cancelErr != nil {
|
||||||
|
return FontFile{}, cancelErr
|
||||||
|
}
|
||||||
|
return FontFile{Data: data}, err
|
||||||
|
}
|
||||||
|
|
||||||
// apply 合并同一文档的XML修改,再统一隔离其他文档及版本使用的文件
|
// apply 合并同一文档的XML修改,再统一隔离其他文档及版本使用的文件
|
||||||
// 入参: ctx 取消上下文, scan 修改前的文档引用
|
// 入参: ctx 取消上下文, scan 修改前的文档引用
|
||||||
// 返回: error 取消、引用或结构错误
|
// 返回: error 取消、引用或结构错误
|
||||||
@@ -560,50 +598,85 @@ func (e *Editor) resolveBindingFont(ctx context.Context, scan *editorFontDocumen
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checkFontBinding 核对字符覆盖及字形编号,外置仅允许等价的一对一字符映射
|
// checkFontBinding 核对字符覆盖及字形编号,外置仅允许等价的一对一字符映射
|
||||||
// 入参: object 文字对象, font 目标字体, mode 保存方式
|
// 入参: ctx 取消上下文, object 文字对象, font 目标字体, mode 保存方式
|
||||||
// 返回: error 字符或字形无法保留的原因
|
// 返回: error 字符、字形或取消错误
|
||||||
func checkFontBinding(object TextObject, font *FontResource, mode FontMode) error {
|
func checkFontBinding(ctx context.Context, object TextObject, font *FontResource, mode FontMode) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
var chars []rune
|
var chars []rune
|
||||||
for _, code := range object.TextCode {
|
for _, code := range object.TextCode {
|
||||||
if code.Index != "" {
|
if code.Index != "" {
|
||||||
return fmt.Errorf("font uses index-encoded text")
|
return fmt.Errorf("font uses index-encoded text")
|
||||||
}
|
}
|
||||||
chars = append(chars, textCodeRunes(code.Value)...)
|
values, err := textCodeRunesContext(ctx, code.Value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
covered := make(map[int]bool)
|
if len(chars) == 0 {
|
||||||
for _, transform := range object.CGTransform {
|
chars = values
|
||||||
values := strings.Fields(transform.Glyphs)
|
} else {
|
||||||
if transform.CodePosition < 0 || transform.CodeCount < 1 || transform.GlyphCount < 1 || transform.CodePosition > len(chars)-transform.CodeCount || transform.GlyphCount != len(values) {
|
chars = append(chars, values...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transforms := slices.Clone(object.CGTransform)
|
||||||
|
slices.SortFunc(transforms, func(a, b CGTransform) int { return cmp.Compare(a.CodePosition, b.CodePosition) })
|
||||||
|
end := 0
|
||||||
|
for _, transform := range transforms {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if transform.CodePosition < 0 || transform.CodeCount < 1 || transform.GlyphCount < 1 || transform.CodePosition > len(chars)-transform.CodeCount {
|
||||||
return fmt.Errorf("invalid font glyph mapping")
|
return fmt.Errorf("invalid font glyph mapping")
|
||||||
}
|
}
|
||||||
|
if transform.CodePosition < end {
|
||||||
|
return fmt.Errorf("overlapping font glyph mapping")
|
||||||
|
}
|
||||||
|
end = transform.CodePosition + transform.CodeCount
|
||||||
if mode == FontExternal && transform.CodeCount != transform.GlyphCount {
|
if mode == FontExternal && transform.CodeCount != transform.GlyphCount {
|
||||||
return fmt.Errorf("font uses non-character glyph mapping")
|
return fmt.Errorf("font uses non-character glyph mapping")
|
||||||
}
|
}
|
||||||
for i, value := range values {
|
count := 0
|
||||||
|
for value := range strings.FieldsSeq(transform.Glyphs) {
|
||||||
|
if count&4095 == 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if count >= transform.GlyphCount {
|
||||||
|
return fmt.Errorf("invalid font glyph mapping")
|
||||||
|
}
|
||||||
glyph, err := strconv.ParseUint(value, 10, 16)
|
glyph, err := strconv.ParseUint(value, 10, 16)
|
||||||
if err != nil || glyph == 0 || glyph >= uint64(font.NumGlyphs()) {
|
if err != nil || glyph == 0 || glyph >= uint64(font.NumGlyphs()) {
|
||||||
return fmt.Errorf("font glyph is unavailable")
|
return fmt.Errorf("font glyph is unavailable")
|
||||||
}
|
}
|
||||||
if mode == FontExternal && uint16(glyph) != font.GlyphIndex(chars[transform.CodePosition+i]) {
|
if mode == FontExternal && uint16(glyph) != font.GlyphIndex(chars[transform.CodePosition+count]) {
|
||||||
return fmt.Errorf("font uses non-character glyph mapping")
|
return fmt.Errorf("font uses non-character glyph mapping")
|
||||||
}
|
}
|
||||||
|
count++
|
||||||
}
|
}
|
||||||
for i := transform.CodePosition; i < transform.CodePosition+transform.CodeCount; i++ {
|
if count != transform.GlyphCount {
|
||||||
if covered[i] {
|
return fmt.Errorf("invalid font glyph mapping")
|
||||||
return fmt.Errorf("overlapping font glyph mapping")
|
|
||||||
}
|
|
||||||
covered[i] = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
covered := 0
|
||||||
for i, char := range chars {
|
for i, char := range chars {
|
||||||
|
if i&4095 == 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if mode == FontExternal && (char >= 0xe000 && char <= 0xf8ff || char >= 0xf0000 && char <= 0x10fffd) {
|
if mode == FontExternal && (char >= 0xe000 && char <= 0xf8ff || char >= 0xf0000 && char <= 0x10fffd) {
|
||||||
return fmt.Errorf("font uses private character mapping")
|
return fmt.Errorf("font uses private character mapping")
|
||||||
}
|
}
|
||||||
if !covered[i] && font.GlyphIndex(char) == 0 {
|
for covered < len(transforms) && i >= transforms[covered].CodePosition+transforms[covered].CodeCount {
|
||||||
|
covered++
|
||||||
|
}
|
||||||
|
if (covered == len(transforms) || i < transforms[covered].CodePosition) && font.GlyphIndex(char) == 0 {
|
||||||
return fmt.Errorf("font has no glyph for U+%04X", char)
|
return fmt.Errorf("font has no glyph for U+%04X", char)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// externalFontName 去除PDF字体子集前缀,不猜测或改写其他名称
|
// externalFontName 去除PDF字体子集前缀,不猜测或改写其他名称
|
||||||
@@ -632,10 +705,13 @@ func (e *Editor) commitResourceSnapshot(ctx context.Context, reader *Reader) err
|
|||||||
if err := clean.initRootContext(ctx); err != nil {
|
if err := clean.initRootContext(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
next, err := clean.Editor()
|
next, err := clean.EditorContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := next.retainPendingResources(ctx, e); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -643,12 +719,184 @@ func (e *Editor) commitResourceSnapshot(ctx context.Context, reader *Reader) err
|
|||||||
e.restoreResourceSnapshot(*next)
|
e.restoreResourceSnapshot(*next)
|
||||||
if change := e.recordChange(); change != nil {
|
if change := e.recordChange(); change != nil {
|
||||||
after := e.transactionSnapshot()
|
after := e.transactionSnapshot()
|
||||||
change.undo = func(e *Editor) { e.restoreResourceSnapshot(before) }
|
before.history, after.history = nil, nil
|
||||||
change.redo = func(e *Editor) { e.restoreResourceSnapshot(after) }
|
change.undo = func(e *Editor) {
|
||||||
|
after.retainDocumentResources(e)
|
||||||
|
e.restoreResourceSnapshot(before)
|
||||||
|
}
|
||||||
|
change.redo = func(e *Editor) {
|
||||||
|
before.retainDocumentResources(e)
|
||||||
|
e.restoreResourceSnapshot(after)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// retainPendingResources 保留未写入包的会话资源及未变化的注册映射,不提前持久化资源
|
||||||
|
// 入参: ctx 取消上下文, source 修改前的编辑状态
|
||||||
|
// 返回: error 取消错误
|
||||||
|
func (e *Editor) retainPendingResources(ctx context.Context, source *Editor) error {
|
||||||
|
e.maxID = max(e.maxID, source.maxID)
|
||||||
|
if len(source.resources) == 0 && len(source.resourceID) == 0 {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
used := make(map[string]bool)
|
||||||
|
var files map[string]bool
|
||||||
|
if len(source.resources) != 0 {
|
||||||
|
var fonts, images []editorResource
|
||||||
|
var spaces []ColorSpace
|
||||||
|
var err error
|
||||||
|
fonts, images, spaces, files, err = source.usedResources(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, resource := range fonts {
|
||||||
|
used[resource.font.ID] = true
|
||||||
|
}
|
||||||
|
for _, resource := range images {
|
||||||
|
used[resource.image.ID] = true
|
||||||
|
}
|
||||||
|
for _, space := range spaces {
|
||||||
|
used[space.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
retained := make(map[string]bool)
|
||||||
|
registered := make(map[string]editorResource)
|
||||||
|
for _, resource := range source.resources {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
id := resource.definition()
|
||||||
|
switch {
|
||||||
|
case resource.font != nil:
|
||||||
|
id = resource.font.ID
|
||||||
|
case resource.image != nil:
|
||||||
|
id = resource.image.ID
|
||||||
|
case resource.space != nil:
|
||||||
|
id = resource.space.ID
|
||||||
|
}
|
||||||
|
registered[id] = resource
|
||||||
|
if id == "" || used[id] || files[resource.name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := resourceValue(e.source.reader.resourceFiles, id); exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e.resources = append(e.resources, resource)
|
||||||
|
retained[id] = true
|
||||||
|
if font := source.fonts[id]; font != nil {
|
||||||
|
e.fonts[id] = font
|
||||||
|
}
|
||||||
|
if size, exists := source.images[id]; exists {
|
||||||
|
e.images[id] = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checked := make(map[string]bool)
|
||||||
|
for key, id := range source.resourceID {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !retained[id] && !checked[id] {
|
||||||
|
checked[id] = true
|
||||||
|
resource, local := registered[id]
|
||||||
|
if !local {
|
||||||
|
resource = source.registeredResource(id)
|
||||||
|
}
|
||||||
|
unchanged, err := e.matchesRegisteredResource(ctx, source, resource, id, local)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
retained[id] = unchanged
|
||||||
|
}
|
||||||
|
if retained[id] {
|
||||||
|
e.resourceID[key] = id
|
||||||
|
if font := source.fonts[id]; font != nil {
|
||||||
|
e.fonts[id] = font
|
||||||
|
}
|
||||||
|
if size, exists := source.images[id]; exists {
|
||||||
|
e.images[id] = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if retained[source.fontRegistration.id] {
|
||||||
|
e.fontRegistration = source.fontRegistration
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// registeredResource 获取已加载的资源定义及文件路径,不扫描页面或其他文档
|
||||||
|
// 入参: id 资源标识
|
||||||
|
// 返回: editorResource 资源定义,未找到时为空
|
||||||
|
func (e *Editor) registeredResource(id string) editorResource {
|
||||||
|
if e.source == nil {
|
||||||
|
return editorResource{}
|
||||||
|
}
|
||||||
|
reader := e.source.reader
|
||||||
|
if font, ok := resourceValue(reader.fontCache, id); ok && font != nil {
|
||||||
|
resource := editorResource{font: font}
|
||||||
|
if font.FontFile != "" {
|
||||||
|
resource.name = reader.ResPath(font.FontFile)
|
||||||
|
}
|
||||||
|
return resource
|
||||||
|
}
|
||||||
|
if media, ok := resourceValue(reader.mediaCache, id); ok {
|
||||||
|
return editorResource{image: &media, name: reader.ResPath(media.MediaFile)}
|
||||||
|
}
|
||||||
|
if space, ok := resourceValue(reader.colorSpaceCache, id); ok && space != nil && space.Profile != "" {
|
||||||
|
return editorResource{space: space, name: reader.ResPath(space.Profile)}
|
||||||
|
}
|
||||||
|
return editorResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchesRegisteredResource 核对同一注册标识的定义与内容,允许文件迁移但不复用已变化的资源
|
||||||
|
// 入参: ctx 取消上下文, source 原状态, before 原资源, id 资源标识, local 是否为会话资源
|
||||||
|
// 返回: bool 是否可保留注册映射, error 取消错误
|
||||||
|
func (e *Editor) matchesRegisteredResource(ctx context.Context, source *Editor, before editorResource, id string, local bool) (bool, error) {
|
||||||
|
after := e.registeredResource(id)
|
||||||
|
switch {
|
||||||
|
case before.font != nil && after.font != nil:
|
||||||
|
x, y := *before.font, *after.font
|
||||||
|
if (x.FontFile == "") != (y.FontFile == "") {
|
||||||
|
return false, ctx.Err()
|
||||||
|
}
|
||||||
|
x.FontFile, y.FontFile = "", ""
|
||||||
|
if x != y {
|
||||||
|
return false, ctx.Err()
|
||||||
|
}
|
||||||
|
if before.font.FontFile == "" {
|
||||||
|
return true, ctx.Err()
|
||||||
|
}
|
||||||
|
case before.image != nil && after.image != nil:
|
||||||
|
x, y := *before.image, *after.image
|
||||||
|
x.MediaFile, y.MediaFile = "", ""
|
||||||
|
if x != y {
|
||||||
|
return false, ctx.Err()
|
||||||
|
}
|
||||||
|
case before.space != nil && after.space != nil:
|
||||||
|
if before.space.Type != after.space.Type {
|
||||||
|
return false, ctx.Err()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false, ctx.Err()
|
||||||
|
}
|
||||||
|
data := before.data
|
||||||
|
if !local {
|
||||||
|
var err error
|
||||||
|
data, err = source.source.reader.readFileViewContext(ctx, before.name)
|
||||||
|
if err != nil {
|
||||||
|
return false, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(data) == 0 || after.name == "" {
|
||||||
|
return false, ctx.Err()
|
||||||
|
}
|
||||||
|
matched, err := packageDataEqual(ctx, e.source.reader, after.name, data)
|
||||||
|
if err != nil {
|
||||||
|
return false, ctx.Err()
|
||||||
|
}
|
||||||
|
return matched, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
// restoreResourceSnapshot 恢复资源修改前后的包状态,保留运行配置与撤销队列
|
// restoreResourceSnapshot 恢复资源修改前后的包状态,保留运行配置与撤销队列
|
||||||
// 入参: state 包快照
|
// 入参: state 包快照
|
||||||
func (e *Editor) restoreResourceSnapshot(state Editor) {
|
func (e *Editor) restoreResourceSnapshot(state Editor) {
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ type editorFontDeclaration struct {
|
|||||||
// 入参: ctx 取消上下文, reader 包快照, index 文档索引
|
// 入参: ctx 取消上下文, reader 包快照, index 文档索引
|
||||||
// 返回: *editorFontDocument 字体引用, error 读取或结构错误
|
// 返回: *editorFontDocument 字体引用, error 读取或结构错误
|
||||||
func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFontDocument, error) {
|
func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFontDocument, error) {
|
||||||
|
return scanEditorFontFiles(ctx, reader, index, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanEditorFontFiles 扫描字体与内容引用,按需复用当前文件的XML树,不额外保留节点树
|
||||||
|
// 入参: ctx 取消上下文, reader 包快照, index 文档索引, inspect 可选的文件节点访问器
|
||||||
|
// 返回: *editorFontDocument 字体引用, error 读取、结构或访问错误
|
||||||
|
func scanEditorFontFiles(ctx context.Context, reader *Reader, index int, inspect func(*editorFontXML, *editorXML) error) (*editorFontDocument, error) {
|
||||||
scan := &editorFontDocument{reader: reader, index: index, files: make(map[string]*editorFontXML), fonts: make(map[string]*editorFontDeclaration), texts: make(map[string][]editorFontText)}
|
scan := &editorFontDocument{reader: reader, index: index, files: make(map[string]*editorFontXML), fonts: make(map[string]*editorFontDeclaration), texts: make(map[string][]editorFontText)}
|
||||||
entry, err := reader.documentEntryContext(ctx, index)
|
entry, err := reader.documentEntryContext(ctx, index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -163,6 +170,14 @@ func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFon
|
|||||||
if err := visit(root); err != nil {
|
if err := visit(root); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if inspect != nil {
|
||||||
|
if err := inspect(file, root); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return scan, nil
|
return scan, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ package ofdgo
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/zlib"
|
"compress/zlib"
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
@@ -29,43 +30,53 @@ const pngBinaryProbeLimit = 4 << 20
|
|||||||
|
|
||||||
// pngBinaryProbe 按需读取连续的IDAT块,不复制压缩数据
|
// pngBinaryProbe 按需读取连续的IDAT块,不复制压缩数据
|
||||||
type pngBinaryProbe struct {
|
type pngBinaryProbe struct {
|
||||||
|
ctx context.Context
|
||||||
data []byte
|
data []byte
|
||||||
payload []byte
|
payload []byte
|
||||||
remaining int
|
remaining int
|
||||||
}
|
}
|
||||||
|
|
||||||
// editorBinaryImage 尝试纯黑白无损编码,不改变透明度,带附加信息或无体积收益时保留原图
|
// editorBinaryImage 尝试纯黑白无损编码,不改变透明度,带附加信息或无体积收益时保留原图
|
||||||
// 入参: data PNG图片数据, img 已解码像素,为nil时读取原图
|
// 入参: ctx 取消上下文, data PNG图片数据, img 已解码像素,为nil时读取原图
|
||||||
// 返回: []byte 更小的JBIG2数据,不适用时为nil
|
// 返回: []byte 更小的JBIG2数据,不适用时为nil, error 取消错误
|
||||||
func editorBinaryImage(data []byte, img image.Image) []byte {
|
func editorBinaryImage(ctx context.Context, data []byte, img image.Image) ([]byte, error) {
|
||||||
if !pngBinarySource(data) {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil
|
return nil, err
|
||||||
|
}
|
||||||
|
if !pngBinarySource(ctx, data) {
|
||||||
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
if img == nil {
|
if img == nil {
|
||||||
if pngNonBinaryImage(data) {
|
if pngNonBinaryImage(ctx, data) {
|
||||||
return nil
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
img, _, err = image.Decode(bytes.NewReader(data))
|
img, _, err = image.Decode(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
if err := jbig2.Encode(&output, img, &jbig2.Options{MaxPageBytes: uint64(len(data))}); err != nil || output.Len() >= len(data) {
|
if err := jbig2.EncodeContext(ctx, &output, img, &jbig2.Options{MaxPageBytes: uint64(len(data))}); err != nil || output.Len() >= len(data) {
|
||||||
return nil
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
return output.Bytes()
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return output.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pngBinarySource 检查PNG是否仅含图像与透明度数据,避免转为JBIG2时丢失附加信息
|
// pngBinarySource 检查PNG是否仅含图像与透明度数据,避免转为JBIG2时丢失附加信息
|
||||||
// 入参: data PNG编码数据
|
// 入参: ctx 取消上下文, data PNG编码数据
|
||||||
// 返回: bool 是否无需保留附加信息
|
// 返回: bool 是否无需保留附加信息
|
||||||
func pngBinarySource(data []byte) bool {
|
func pngBinarySource(ctx context.Context, data []byte) bool {
|
||||||
if len(data) < 33 || string(data[:8]) != "\x89PNG\r\n\x1a\n" {
|
if len(data) < 33 || string(data[:8]) != "\x89PNG\r\n\x1a\n" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for pos := 8; pos < len(data); {
|
for pos := 8; pos < len(data); {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if len(data)-pos < 12 {
|
if len(data)-pos < 12 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -86,9 +97,9 @@ func pngBinarySource(data []byte) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pngNonBinaryImage 检查PNG前部像素是否包含灰阶、彩色或透明度,仅用于排除黑白压缩
|
// pngNonBinaryImage 检查PNG前部像素是否包含灰阶、彩色或透明度,仅用于排除黑白压缩
|
||||||
// 入参: data PNG图片数据
|
// 入参: ctx 取消上下文, data PNG图片数据
|
||||||
// 返回: bool 已发现非不透明黑白像素,无法判定时为false
|
// 返回: bool 已发现非不透明黑白像素,无法判定时为false
|
||||||
func pngNonBinaryImage(data []byte) bool {
|
func pngNonBinaryImage(ctx context.Context, data []byte) bool {
|
||||||
if len(data) < 33 || string(data[:8]) != "\x89PNG\r\n\x1a\n" || binary.BigEndian.Uint32(data[8:]) != 13 || string(data[12:16]) != "IHDR" {
|
if len(data) < 33 || string(data[:8]) != "\x89PNG\r\n\x1a\n" || binary.BigEndian.Uint32(data[8:]) != 13 || string(data[12:16]) != "IHDR" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -117,8 +128,11 @@ func pngNonBinaryImage(data []byte) bool {
|
|||||||
if size > 256<<10 {
|
if size > 256<<10 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
probe := pngBinaryProbe{data: data[33:], remaining: pngBinaryProbeLimit}
|
probe := pngBinaryProbe{ctx: ctx, data: data[33:], remaining: pngBinaryProbeLimit}
|
||||||
for len(probe.data) >= 12 && string(probe.data[4:8]) != "IDAT" {
|
for len(probe.data) >= 12 && string(probe.data[4:8]) != "IDAT" {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
length := uint64(binary.BigEndian.Uint32(probe.data)) + 12
|
length := uint64(binary.BigEndian.Uint32(probe.data)) + 12
|
||||||
if length > uint64(len(probe.data)) || length > uint64(probe.remaining) {
|
if length > uint64(len(probe.data)) || length > uint64(probe.remaining) {
|
||||||
return false
|
return false
|
||||||
@@ -135,6 +149,9 @@ func pngNonBinaryImage(data []byte) bool {
|
|||||||
previous := make([]byte, int(size))
|
previous := make([]byte, int(size))
|
||||||
rows := min(int64(height), 64, (1<<20)/int64(len(row)))
|
rows := min(int64(height), 64, (1<<20)/int64(len(row)))
|
||||||
for index := int64(0); index < rows; index++ {
|
for index := int64(0); index < rows; index++ {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if _, err := io.ReadFull(stream, row); err != nil || !pngBinaryProbeFilter(row[1:], previous, channels, row[0]) {
|
if _, err := io.ReadFull(stream, row); err != nil || !pngBinaryProbeFilter(row[1:], previous, channels, row[0]) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -166,7 +183,13 @@ func (p *pngBinaryProbe) Read(output []byte) (int, error) {
|
|||||||
if len(output) == 0 {
|
if len(output) == 0 {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
for len(p.payload) == 0 {
|
for len(p.payload) == 0 {
|
||||||
|
if err := p.ctx.Err(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
if len(p.data) < 12 || p.remaining < 12 || string(p.data[4:8]) != "IDAT" {
|
if len(p.data) < 12 || p.remaining < 12 || string(p.data[4:8]) != "IDAT" {
|
||||||
return 0, io.EOF
|
return 0, io.EOF
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-11
@@ -42,9 +42,16 @@ type ImagePageOptions struct {
|
|||||||
OnProgress func(stage string, completed, total int) error
|
OnProgress func(stage string, completed, total int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// editorImagePageInfo 保存同批次已校验图片的建页信息,不持有编码、像素或颜色模型
|
||||||
|
type editorImagePageInfo struct {
|
||||||
|
size image.Point
|
||||||
|
format string
|
||||||
|
metadata baseimage.PageInfo
|
||||||
|
}
|
||||||
|
|
||||||
// ImportImages 将PNG、JPEG、BMP、单页TIFF或JBIG2逐张建页并插入指定位置,一次操作计入一条撤销记录
|
// ImportImages 将PNG、JPEG、BMP、单页TIFF或JBIG2逐张建页并插入指定位置,一次操作计入一条撤销记录
|
||||||
// 保留像素及透明度,方向通过对象变换实现,不重新采样;图片压缩沿用AddImage规则
|
// 保留像素及透明度,方向通过对象变换实现,不重新采样;图片压缩沿用AddImage规则
|
||||||
// 同一批次的相同图片复用解码校验结果,不额外保留像素数据
|
// 同一批次的相同图片复用解码校验及元数据,不额外保留像素数据
|
||||||
// 失败或取消时不修改文档,读取期间仍需容纳单张图片的解码数据
|
// 失败或取消时不修改文档,读取期间仍需容纳单张图片的解码数据
|
||||||
// 入参: ctx 取消上下文, sources 按页面顺序排列的图片, at 插入位置, options 建页选项
|
// 入参: ctx 取消上下文, sources 按页面顺序排列的图片, at 插入位置, options 建页选项
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
@@ -82,7 +89,7 @@ func (e *Editor) ImportImages(ctx context.Context, sources []ImagePageSource, at
|
|||||||
}
|
}
|
||||||
return e.Transaction(func(e *Editor) error {
|
return e.Transaction(func(e *Editor) error {
|
||||||
indexes := make([]int, 0, len(sources))
|
indexes := make([]int, 0, len(sources))
|
||||||
verified := make(map[[32]byte]bool)
|
verified := make(map[[32]byte]editorImagePageInfo)
|
||||||
for i, source := range sources {
|
for i, source := range sources {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -111,12 +118,16 @@ func (e *Editor) ImportImages(ctx context.Context, sources []ImagePageSource, at
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addImagePage 保留原图并按方向、物理尺寸与纸张选项建页
|
// addImagePage 保留原图并按方向、物理尺寸与纸张选项建页
|
||||||
// 入参: ctx 取消上下文, data 图片数据, options 建页选项, verified 本批次已成功解码的图片摘要
|
// 入参: ctx 取消上下文, data 图片数据, options 建页选项, verified 本批次已校验图片的建页信息
|
||||||
// 返回: int 从0开始的页面索引, error 错误信息
|
// 返回: int 从0开始的页面索引, error 错误信息
|
||||||
func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePageOptions, verified map[[32]byte]bool) (int, error) {
|
func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePageOptions, verified map[[32]byte]editorImagePageInfo) (int, error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
var decoded image.Image
|
||||||
|
checksum := sha256.Sum256(data)
|
||||||
|
info, known := verified[checksum]
|
||||||
|
if !known {
|
||||||
config, format, err := baseimage.DecodeConfigDataContext(ctx, data)
|
config, format, err := baseimage.DecodeConfigDataContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -124,20 +135,19 @@ func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePag
|
|||||||
if format != "png" && format != "jpeg" && format != "bmp" && format != "tiff" && format != "jbig2" {
|
if format != "png" && format != "jpeg" && format != "bmp" && format != "tiff" && format != "jbig2" {
|
||||||
return 0, fmt.Errorf("only PNG, JPEG, BMP, single-page TIFF and JBIG2 images are supported")
|
return 0, fmt.Errorf("only PNG, JPEG, BMP, single-page TIFF and JBIG2 images are supported")
|
||||||
}
|
}
|
||||||
var decoded image.Image
|
|
||||||
checksum := sha256.Sum256(data)
|
|
||||||
if !verified[checksum] {
|
|
||||||
decoded, _, err = baseimage.DecodePixelsContext(ctx, data)
|
decoded, _, err = baseimage.DecodePixelsContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
verified[checksum] = true
|
|
||||||
}
|
|
||||||
metadata, err := baseimage.PageMetadataContext(ctx, data, format, options.DPI)
|
metadata, err := baseimage.PageMetadataContext(ctx, data, format, options.DPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
w, h := float64(config.Width)*25.4/metadata.XDPI, float64(config.Height)*25.4/metadata.YDPI
|
info = editorImagePageInfo{size: image.Pt(config.Width, config.Height), format: format, metadata: metadata}
|
||||||
|
verified[checksum] = info
|
||||||
|
}
|
||||||
|
metadata := info.metadata
|
||||||
|
w, h := float64(info.size.X)*25.4/metadata.XDPI, float64(info.size.Y)*25.4/metadata.YDPI
|
||||||
if metadata.Orientation >= 5 {
|
if metadata.Orientation >= 5 {
|
||||||
w, h = h, w
|
w, h = h, w
|
||||||
}
|
}
|
||||||
@@ -154,7 +164,7 @@ func (e *Editor) addImagePage(ctx context.Context, data []byte, options ImagePag
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
resource, err := e.addImage(data, config, format, decoded)
|
resource, err := e.addImage(ctx, data, info.size, info.format, decoded, checksum)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-5
@@ -727,29 +727,39 @@ func (e *Editor) appendImportedOutlines(ctx context.Context, imported []byte) ([
|
|||||||
// importBase 为新建文档提供空的保真包基底,不重复保存已创建的页面和资源
|
// importBase 为新建文档提供空的保真包基底,不重复保存已创建的页面和资源
|
||||||
// 返回: *editorSource 包基底, error 错误信息
|
// 返回: *editorSource 包基底, error 错误信息
|
||||||
func (e *Editor) importBase() (*editorSource, error) {
|
func (e *Editor) importBase() (*editorSource, error) {
|
||||||
|
return e.importBaseContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
// importBaseContext 生成新建文档的独立包基底,初始化期间响应取消
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: *editorSource 包基底, error 编码、解析或取消错误
|
||||||
|
func (e *Editor) importBaseContext(ctx context.Context) (*editorSource, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
seed := NewEditor()
|
seed := NewEditor()
|
||||||
seed.Info = e.Info
|
seed.Info = e.Info
|
||||||
seed.pages = []PageContent{{Area: PageArea{PhysicalBox: "0 0 210 297"}}}
|
seed.pages = []PageContent{{Area: PageArea{PhysicalBox: "0 0 210 297"}}}
|
||||||
files := make(map[string][]byte)
|
files := make(map[string][]byte)
|
||||||
if err := seed.writeParts(func(name string, data []byte, _ bool) error {
|
if err := seed.writeParts(ctx, func(name string, data []byte, _ bool) error {
|
||||||
if name == "OFD.xml" || name == "Doc_0/Document.xml" {
|
if name == "OFD.xml" || name == "Doc_0/Document.xml" {
|
||||||
files[name] = data
|
files[name] = data
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}, nil); err != nil {
|
}, editorProgress(nil).withContext(ctx)); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
data := files["Doc_0/Document.xml"]
|
data := files["Doc_0/Document.xml"]
|
||||||
root, err := parseEditorXML(data)
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
files["Doc_0/Document.xml"] = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, root.child("Pages"), nil)})
|
files["Doc_0/Document.xml"] = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, root.child("Pages"), nil)})
|
||||||
reader := &Reader{files: files}
|
reader := &Reader{files: files}
|
||||||
if err := reader.initRoot(); err != nil {
|
if err := reader.initRootContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
editor, err := reader.Editor()
|
editor, err := reader.EditorContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -275,10 +275,23 @@ func cloneEditorMap[T any](values map[string]T) map[string]T {
|
|||||||
// prepareSourceIDs 首次新增内容前流式核对原包标识,不依赖可能缺失或过期的MaxUnitID
|
// prepareSourceIDs 首次新增内容前流式核对原包标识,不依赖可能缺失或过期的MaxUnitID
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) prepareSourceIDs() error {
|
func (e *Editor) prepareSourceIDs() error {
|
||||||
maximum, err := e.sourceMaxID(nil)
|
return e.prepareSourceIDsContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareSourceIDsContext 在可取消的扫描成功后更新最大标识,不保留未完成结果
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: error 扫描或取消错误
|
||||||
|
func (e *Editor) prepareSourceIDsContext(ctx context.Context) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
maximum, err := e.sourceMaxID(editorProgress(nil).withContext(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
e.maxID = maximum
|
e.maxID = maximum
|
||||||
if e.source != nil {
|
if e.source != nil {
|
||||||
e.source.idsReady = true
|
e.source.idsReady = true
|
||||||
@@ -303,6 +316,9 @@ func (e *Editor) sourceMaxID(progress editorProgress) (int, error) {
|
|||||||
}
|
}
|
||||||
maximum := e.maxID
|
maximum := e.maxID
|
||||||
for name := range names {
|
for name := range names {
|
||||||
|
if err := progress.report("ids", 0, 0); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
input, err := reader.openFile(name)
|
input, err := reader.openFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
|
|||||||
+35
-26
@@ -32,7 +32,7 @@ import (
|
|||||||
// 入参: ctx 取消上下文, progress 保存进度回调,预览时为nil
|
// 入参: ctx 取消上下文, progress 保存进度回调,预览时为nil
|
||||||
// 返回: map[string][]byte 替换及新增条目, error 错误信息
|
// 返回: map[string][]byte 替换及新增条目, error 错误信息
|
||||||
func (e *Editor) sourceParts(ctx context.Context, 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(ctx, progress, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -40,16 +40,20 @@ func (e *Editor) sourceParts(ctx context.Context, progress editorProgress) (map[
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sourcePartsReferences 生成保存快照,同步收集完全自产页面的实际编码引用
|
// sourcePartsReferences 生成保存快照,同步收集完全自产页面的实际编码引用
|
||||||
// 入参: progress 保存进度回调, generated 本次自产页面引用,可为nil
|
// 入参: ctx 取消上下文, progress 保存进度回调, generated 本次自产页面引用,可为nil
|
||||||
// 返回: map[string][]byte 输出条目, error 编码错误
|
// 返回: map[string][]byte 输出条目, error 编码错误
|
||||||
func (e *Editor) sourcePartsReferences(progress editorProgress, generated map[string]editorGeneratedReferences) (map[string][]byte, error) {
|
func (e *Editor) sourcePartsReferences(ctx context.Context, progress editorProgress, generated map[string]editorGeneratedReferences) (map[string][]byte, error) {
|
||||||
return e.sourcePartsPrepared(progress, generated, false)
|
return e.sourcePartsPrepared(ctx, progress, generated, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sourcePartsPrepared 准备保存条目,按需压缩暂存自产页面
|
// sourcePartsPrepared 准备保存条目,按需压缩暂存自产页面
|
||||||
// 入参: progress 保存进度回调, generated 本次页面及引用, stage 是否压缩暂存
|
// 入参: ctx 取消上下文, progress 保存进度回调, generated 本次页面及引用, stage 是否压缩暂存
|
||||||
// 返回: map[string][]byte 输出条目, error 编码错误
|
// 返回: map[string][]byte 输出条目, error 编码错误
|
||||||
func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[string]editorGeneratedReferences, stage bool) (map[string][]byte, error) {
|
func (e *Editor) sourcePartsPrepared(ctx context.Context, progress editorProgress, generated map[string]editorGeneratedReferences, stage bool) (map[string][]byte, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
progress = progress.withContext(ctx)
|
||||||
parts := make(map[string][]byte)
|
parts := make(map[string][]byte)
|
||||||
var stager editorPageStager
|
var stager editorPageStager
|
||||||
source := e.source
|
source := e.source
|
||||||
@@ -97,10 +101,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
|
|||||||
}
|
}
|
||||||
generated[name] = known
|
generated[name] = known
|
||||||
if (reader.DocumentCount() > 1 || reader.OFD.DocBody[reader.documentIndex].versioned) && !known.stagedLeaf(nil) || bound && known.objects == nil {
|
if (reader.DocumentCount() > 1 || reader.OFD.DocBody[reader.documentIndex].versioned) && !known.stagedLeaf(nil) || bound && known.objects == nil {
|
||||||
input := data.open()
|
input := imageInput{ReadCloser: data.open(), context: ctx}
|
||||||
if e.output != nil {
|
|
||||||
input = imageInput{ReadCloser: input, context: e.output.ctx}
|
|
||||||
}
|
|
||||||
parts[name], err = io.ReadAll(input)
|
parts[name], err = io.ReadAll(input)
|
||||||
input.Close()
|
input.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -124,8 +125,14 @@ 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, usedFiles := e.usedResources()
|
fonts, images, spaces, usedFiles, err := e.usedResources(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
for _, resource := range e.resources {
|
for _, resource := range e.resources {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if resource.definition() != "" && usedFiles[resource.name] || resource.colorProfileUsed(spaces) {
|
if resource.definition() != "" && usedFiles[resource.name] || resource.colorProfileUsed(spaces) {
|
||||||
parts[resource.name] = resource.data
|
parts[resource.name] = resource.data
|
||||||
}
|
}
|
||||||
@@ -136,14 +143,10 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
|
|||||||
}
|
}
|
||||||
resourceFiles := slices.Sorted(maps.Keys(usedFiles))
|
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 := encodeOFDXMLContext(ctx, func(x *ofdXML) { x.resources(fonts, images, spaces) })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
|
||||||
if e.output != nil {
|
|
||||||
ctx = e.output.ctx
|
|
||||||
}
|
|
||||||
resourcePath, added, err := mergePackageResources(ctx, reader, parts, source.document, source.directory, data)
|
resourcePath, added, err := mergePackageResources(ctx, reader, parts, source.document, source.directory, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -160,11 +163,11 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
|
|||||||
pagesChanged := !slices.Equal(pageRefs, source.document.Pages.Page)
|
pagesChanged := !slices.Equal(pageRefs, source.document.Pages.Page)
|
||||||
if pagesChanged || len(resourceFiles) != 0 || len(parts) != 0 && e.maxID != source.document.CommonData.MaxUnitID {
|
if pagesChanged || len(resourceFiles) != 0 || len(parts) != 0 && e.maxID != source.document.CommonData.MaxUnitID {
|
||||||
name := reader.documentRoot
|
name := reader.documentRoot
|
||||||
data, err := reader.readFile(name)
|
data, err := reader.readFileViewContext(ctx, name)
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -261,7 +264,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
|
|||||||
data, ok := parts[name]
|
data, ok := parts[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
var err error
|
var err error
|
||||||
data, err = reader.readFile(name)
|
data, err = reader.readFileViewContext(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -273,14 +276,17 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
|
|||||||
parts[name] = updated
|
parts[name] = updated
|
||||||
}
|
}
|
||||||
if originalCount != len(source.pages) || len(source.annotationPages) != 0 || len(e.removedPages) != 0 || len(source.removedAttachments) != 0 {
|
if originalCount != len(source.pages) || len(source.annotationPages) != 0 || len(e.removedPages) != 0 || len(source.removedAttachments) != 0 {
|
||||||
if err := e.pruneDocumentReferences(parts, generated); err != nil {
|
if err := e.pruneDocumentReferences(ctx, parts, generated); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := e.pruneStructureReferences(parts, generated); err != nil {
|
if err := e.pruneStructureReferences(ctx, parts, generated); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for name, data := range maps.Clone(parts) {
|
for name, data := range maps.Clone(parts) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if file, ok := reader.packageFile(name); ok {
|
if file, ok := reader.packageFile(name); ok {
|
||||||
actual := cleanPackagePath(file.Name)
|
actual := cleanPackagePath(file.Name)
|
||||||
if actual != name {
|
if actual != name {
|
||||||
@@ -293,6 +299,9 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
return parts, nil
|
return parts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -609,17 +618,17 @@ func (e *Editor) sourcePagePreserved(page PageContent) bool {
|
|||||||
// 入参: writer 输出流, fonts 新增字体子集, progress 保存进度回调
|
// 入参: writer 输出流, fonts 新增字体子集, progress 保存进度回调
|
||||||
// 返回: int64 写入字节数, error 错误信息
|
// 返回: int64 写入字节数, error 错误信息
|
||||||
func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress editorProgress) (int64, error) {
|
func (e *Editor) writeSource(writer io.Writer, fonts map[string][]byte, progress editorProgress) (int64, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
if e.output != nil {
|
||||||
|
ctx = e.output.ctx
|
||||||
|
}
|
||||||
generated := make(map[string]editorGeneratedReferences)
|
generated := make(map[string]editorGeneratedReferences)
|
||||||
parts, err := e.sourcePartsPrepared(progress, generated, true)
|
parts, err := e.sourcePartsPrepared(ctx, progress, generated, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
maps.Copy(parts, fonts)
|
maps.Copy(parts, fonts)
|
||||||
reader := e.source.reader
|
reader := e.source.reader
|
||||||
ctx := context.Background()
|
|
||||||
if e.output != nil {
|
|
||||||
ctx = e.output.ctx
|
|
||||||
}
|
|
||||||
versioned := reader.OFD.DocBody[reader.documentIndex].versioned
|
versioned := reader.OFD.DocBody[reader.documentIndex].versioned
|
||||||
if !versioned && reader.DocumentCount() > 1 {
|
if !versioned && reader.DocumentCount() > 1 {
|
||||||
parts, err = reader.versionOutputChanges(ctx, parts, nil, generated)
|
parts, err = reader.versionOutputChanges(ctx, parts, nil, generated)
|
||||||
|
|||||||
@@ -35,9 +35,12 @@ type pageReferenceNode struct {
|
|||||||
// pruneDocumentReferences 在输出快照中清理已删除页面和附件的引用,不修改历史快照或其他文档
|
// pruneDocumentReferences 在输出快照中清理已删除页面和附件的引用,不修改历史快照或其他文档
|
||||||
// 沿当前文档、页面、模板、资源、注释和签名索引检查,不进入附件、私有扩展和签名原始凭据
|
// 沿当前文档、页面、模板、资源、注释和签名索引检查,不进入附件、私有扩展和签名原始凭据
|
||||||
// 版本包保留原签名及其页面引用,避免改写其他版本共用的签名数据
|
// 版本包保留原签名及其页面引用,避免改写其他版本共用的签名数据
|
||||||
// 入参: parts 输出覆盖条目, generated 本次自产页面及引用,可为nil
|
// 入参: ctx 取消上下文, parts 输出覆盖条目, generated 本次自产页面及引用,可为nil
|
||||||
// 返回: error 读取、解析或取消错误
|
// 返回: error 读取、解析或取消错误
|
||||||
func (e *Editor) pruneDocumentReferences(parts map[string][]byte, generated map[string]editorGeneratedReferences) error {
|
func (e *Editor) pruneDocumentReferences(ctx context.Context, parts map[string][]byte, generated map[string]editorGeneratedReferences) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
removed := maps.Clone(e.removedPages)
|
removed := maps.Clone(e.removedPages)
|
||||||
if removed == nil {
|
if removed == nil {
|
||||||
removed = make(map[string]bool)
|
removed = make(map[string]bool)
|
||||||
@@ -55,10 +58,6 @@ func (e *Editor) pruneDocumentReferences(parts map[string][]byte, generated map[
|
|||||||
if len(removed) == 0 && len(e.source.removedAttachments) == 0 {
|
if len(removed) == 0 && len(e.source.removedAttachments) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
|
||||||
if e.output != nil {
|
|
||||||
ctx = e.output.ctx
|
|
||||||
}
|
|
||||||
reader := e.source.reader
|
reader := e.source.reader
|
||||||
bookmarks := make(map[string]bool)
|
bookmarks := make(map[string]bool)
|
||||||
for _, bookmark := range e.source.document.Bookmarks.Bookmark {
|
for _, bookmark := range e.source.document.Bookmarks.Bookmark {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"maps"
|
"maps"
|
||||||
@@ -43,6 +44,7 @@ type editorVectorUsage struct {
|
|||||||
|
|
||||||
// editorReferenceScan 按XML事件收集资源、字体用字和图元编号,未知内容保持完整扫描
|
// editorReferenceScan 按XML事件收集资源、字体用字和图元编号,未知内容保持完整扫描
|
||||||
type editorReferenceScan struct {
|
type editorReferenceScan struct {
|
||||||
|
ctx context.Context
|
||||||
refs *editorResourceRefs
|
refs *editorResourceRefs
|
||||||
objects map[string]bool
|
objects map[string]bool
|
||||||
graphicIDs map[string]bool
|
graphicIDs map[string]bool
|
||||||
@@ -130,6 +132,9 @@ func (s *editorReferenceScan) vectorUsage(data []byte) *editorVectorUsage {
|
|||||||
// 入参: token 已解析或已编码的XML事件
|
// 入参: token 已解析或已编码的XML事件
|
||||||
// 返回: bool 是否仍可完整判断引用
|
// 返回: bool 是否仍可完整判断引用
|
||||||
func (s *editorReferenceScan) accept(token xml.Token) bool {
|
func (s *editorReferenceScan) accept(token xml.Token) bool {
|
||||||
|
if s.ctx != nil && s.ctx.Err() != nil {
|
||||||
|
s.safe = false
|
||||||
|
}
|
||||||
if !s.safe {
|
if !s.safe {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -253,7 +258,10 @@ func (s *editorReferenceScan) accept(token xml.Token) bool {
|
|||||||
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 {
|
||||||
usage.text(token.Name.Local, s.text.String())
|
if err := usage.text(s.ctx, token.Name.Local, s.text.String()); err != nil {
|
||||||
|
s.safe = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
s.stack = s.stack[:len(s.stack)-1]
|
s.stack = s.stack[:len(s.stack)-1]
|
||||||
s.fonts = s.fonts[:len(s.fonts)-1]
|
s.fonts = s.fonts[:len(s.fonts)-1]
|
||||||
@@ -322,6 +330,7 @@ func (x *ofdXML) referenceToken(token xml.Token) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
s := x.references
|
s := x.references
|
||||||
|
s.ctx = x.ctx
|
||||||
switch value := token.(type) {
|
switch value := token.(type) {
|
||||||
case xml.StartElement:
|
case xml.StartElement:
|
||||||
value.Name = xml.Name{Space: ofdNamespace2016, Local: strings.TrimPrefix(value.Name.Local, "ofd:")}
|
value.Name = xml.Name{Space: ofdNamespace2016, Local: strings.TrimPrefix(value.Name.Local, "ofd:")}
|
||||||
@@ -341,6 +350,7 @@ func (x *ofdXML) referenceToken(token xml.Token) {
|
|||||||
}
|
}
|
||||||
if !s.accept(token) {
|
if !s.accept(token) {
|
||||||
x.references = nil
|
x.references = nil
|
||||||
|
x.checkContext()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+50
-21
@@ -43,6 +43,12 @@ type editorResourcePreview struct {
|
|||||||
patches []editorXMLPatch
|
patches []editorXMLPatch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resourceImageMaskKey 区分包内共享图片文件及尚未提交的替换像素
|
||||||
|
type resourceImageMaskKey struct {
|
||||||
|
file string
|
||||||
|
replacement bool
|
||||||
|
}
|
||||||
|
|
||||||
// Resources 获取当前编辑快照的资源,覆盖正文、模板、注解、图案和复合图元
|
// Resources 获取当前编辑快照的资源,覆盖正文、模板、注解、图案和复合图元
|
||||||
// 入参: ctx 取消上下文
|
// 入参: ctx 取消上下文
|
||||||
// 返回: []ResourceInfo 资源及引用位置, error 快照或结构错误
|
// 返回: []ResourceInfo 资源及引用位置, error 快照或结构错误
|
||||||
@@ -106,16 +112,19 @@ func (e *Editor) ImportMedia(ctx context.Context, kind, format string, data []by
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
working, err := reader.Editor()
|
working, err := reader.EditorContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
if err := working.prepareSourceIDsContext(ctx); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
id, err := working.AddMedia(kind, format, data)
|
id, err := working.AddMedia(kind, format, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
resource := working.resources[len(working.resources)-1]
|
resource := working.resources[len(working.resources)-1]
|
||||||
encoded, err := working.encodeXML(func(x *ofdXML) { x.resources(nil, []editorResource{resource}, nil) })
|
encoded, err := encodeOFDXMLContext(ctx, func(x *ofdXML) { x.resources(nil, []editorResource{resource}, nil) })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -194,11 +203,11 @@ func (e *Editor) ReplaceResource(ctx context.Context, kind, id, format string, d
|
|||||||
}
|
}
|
||||||
name := packageAvailableName(reader, changes, path.Join(e.packageDirectory(), "Res", "Media", "Media_"+item.ID))
|
name := packageAvailableName(reader, changes, path.Join(e.packageDirectory(), "Res", "Media", "Media_"+item.ID))
|
||||||
changes[name] = bytes.Clone(data)
|
changes[name] = bytes.Clone(data)
|
||||||
fragment, err := editorXMLStandalone(target.file.data[target.node.start:target.node.end], target.node)
|
fragment, err := editorXMLStandaloneContext(ctx, target.file.data[target.node.start:target.node.end], target.node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
root, err := parseEditorXML(fragment)
|
root, err := parseEditorXMLContext(ctx, fragment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -247,15 +256,15 @@ func (e *Editor) changeResource(ctx context.Context, kind, id string, replace fu
|
|||||||
return fmt.Errorf("resource is still referenced")
|
return fmt.Errorf("resource is still referenced")
|
||||||
}
|
}
|
||||||
if kind == "Attachment" {
|
if kind == "Attachment" {
|
||||||
working, err := reader.Editor()
|
working, err := reader.EditorContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
revision := working.Revision()
|
revision := working.Revision()
|
||||||
if replace == nil {
|
if replace == nil {
|
||||||
err = working.DeleteAttachment(item.ID)
|
err = working.DeleteAttachmentContext(ctx, item.ID)
|
||||||
} else {
|
} else {
|
||||||
err = working.replaceAttachment(ctx, item.ID, data)
|
err = working.ReplaceAttachmentContext(ctx, item.ID, data)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -267,6 +276,9 @@ func (e *Editor) changeResource(ctx context.Context, kind, id string, replace fu
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := pruneUnreferencedFiles(ctx, reader, item.File); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return e.commitResourceSnapshot(ctx, reader)
|
return e.commitResourceSnapshot(ctx, reader)
|
||||||
}
|
}
|
||||||
target := catalog.nodes[item.Kind+"/"+item.ID]
|
target := catalog.nodes[item.Kind+"/"+item.ID]
|
||||||
@@ -297,18 +309,26 @@ func (e *Editor) changeResource(ctx context.Context, kind, id string, replace fu
|
|||||||
return e.commitResourceSnapshot(ctx, reader)
|
return e.commitResourceSnapshot(ctx, reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateResourceImageMask 校验图片替换涉及的全部蒙版引用,复用尺寸和二值像素校验结果
|
// validateResourceImageMask 校验图片替换涉及的全部蒙版引用,按文件复用尺寸和二值像素校验结果
|
||||||
|
// 待替换资源单独校验,不影响仍使用同一原文件的其他资源声明
|
||||||
// 入参: ctx 取消上下文, catalog 当前文档资源, id 替换图片标识, replacement 新图片
|
// 入参: ctx 取消上下文, catalog 当前文档资源, id 替换图片标识, replacement 新图片
|
||||||
// 返回: error 资源、尺寸、像素或取消错误
|
// 返回: error 资源、尺寸、像素或取消错误
|
||||||
func validateResourceImageMask(ctx context.Context, catalog *resourceCatalog, id string, replacement image.Image) error {
|
func validateResourceImageMask(ctx context.Context, catalog *resourceCatalog, id string, replacement image.Image) error {
|
||||||
sizes := map[string]image.Point{id: replacement.Bounds().Size()}
|
updated := resourceImageMaskKey{replacement: true}
|
||||||
validated := make(map[string]image.Point)
|
keys := map[string]resourceImageMaskKey{id: updated}
|
||||||
read := func(id string) ([]byte, error) {
|
sizes := map[resourceImageMaskKey]image.Point{updated: replacement.Bounds().Size()}
|
||||||
|
validated := make(map[resourceImageMaskKey]image.Point)
|
||||||
|
locate := func(id string) (resourceImageMaskKey, error) {
|
||||||
|
if key, ok := keys[id]; ok {
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
item, err := findResource(catalog.items, "Image", id)
|
item, err := findResource(catalog.items, "Image", id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return resourceImageMaskKey{}, err
|
||||||
}
|
}
|
||||||
return catalog.scan.reader.readFileViewContext(ctx, item.File)
|
key := resourceImageMaskKey{file: item.File}
|
||||||
|
keys[id] = key
|
||||||
|
return key, nil
|
||||||
}
|
}
|
||||||
for _, entry := range catalog.images {
|
for _, entry := range catalog.images {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
@@ -318,9 +338,13 @@ func validateResourceImageMask(ctx context.Context, catalog *resourceCatalog, id
|
|||||||
if mask == "" || source != id && mask != id {
|
if mask == "" || source != id && mask != id {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
size, ok := sizes[source]
|
sourceKey, err := locate(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
size, ok := sizes[sourceKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
data, err := read(source)
|
data, err := catalog.scan.reader.readFileViewContext(ctx, sourceKey.file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -329,17 +353,21 @@ func validateResourceImageMask(ctx context.Context, catalog *resourceCatalog, id
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
size = image.Pt(config.Width, config.Height)
|
size = image.Pt(config.Width, config.Height)
|
||||||
sizes[source] = size
|
sizes[sourceKey] = size
|
||||||
}
|
}
|
||||||
if previous, ok := validated[mask]; ok {
|
maskKey, err := locate(mask)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if previous, ok := validated[maskKey]; ok {
|
||||||
if previous != size {
|
if previous != size {
|
||||||
return fmt.Errorf("image mask %s dimensions differ from source %s", mask, source)
|
return fmt.Errorf("image mask %s dimensions differ from source %s", mask, source)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
img := replacement
|
img := replacement
|
||||||
if mask != id {
|
if !maskKey.replacement {
|
||||||
data, err := read(mask)
|
data, err := catalog.scan.reader.readFileViewContext(ctx, maskKey.file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -351,7 +379,8 @@ func validateResourceImageMask(ctx context.Context, catalog *resourceCatalog, id
|
|||||||
if err := validateImageMask(ctx, img, size); err != nil {
|
if err := validateImageMask(ctx, img, size); err != nil {
|
||||||
return fmt.Errorf("image object %s in %s: %w", entry.node.attr("ID"), entry.file.name, err)
|
return fmt.Errorf("image object %s in %s: %w", entry.node.attr("ID"), entry.file.name, err)
|
||||||
}
|
}
|
||||||
validated[mask] = size
|
sizes[maskKey] = size
|
||||||
|
validated[maskKey] = size
|
||||||
}
|
}
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
@@ -533,7 +562,7 @@ func (e *Editor) BindAttachment(ctx context.Context, id string, page int, object
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
attachments, err := e.Attachments()
|
attachments, err := e.AttachmentsContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func editorResourceDefinitions(ctx context.Context, data []byte, name string, so
|
|||||||
result.ids, result.fonts = make(map[string]bool), nil
|
result.ids, result.fonts = make(map[string]bool), nil
|
||||||
result.definitions = make(map[string]*editorResourceRefs)
|
result.definitions = make(map[string]*editorResourceRefs)
|
||||||
outer := &editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)}
|
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}
|
scan := editorReferenceScan{ctx: ctx, refs: outer, name: name, safe: true}
|
||||||
input := bufio.NewReader(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
input := bufio.NewReader(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
||||||
if found, err := editorXMLPrefix(input); err != nil || !found {
|
if found, err := editorXMLPrefix(input); err != nil || !found {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ func scanEditorResources(ctx context.Context, reader *Reader, parts map[string][
|
|||||||
return nil, nil, nil, false, err
|
return nil, nil, nil, false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resource, safe = local.scan(imageInput{ReadCloser: input, context: ctx}, name)
|
resource, safe = local.scanContext(ctx, input, name)
|
||||||
input.Close()
|
input.Close()
|
||||||
}
|
}
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
|
|||||||
@@ -19,11 +19,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// resourceStyleReference 保存资源依赖及可选的类型要求,不持有XML节点
|
||||||
|
type resourceStyleReference struct {
|
||||||
|
key string
|
||||||
|
kind string
|
||||||
|
}
|
||||||
|
|
||||||
// ColorSpace 获取当前文档及版本的颜色空间定义,不读取或解析ICC文件
|
// ColorSpace 获取当前文档及版本的颜色空间定义,不读取或解析ICC文件
|
||||||
// 入参: ctx 取消上下文, id 资源标识
|
// 入参: ctx 取消上下文, id 资源标识
|
||||||
// 返回: *ColorSpace 独立副本,Profile为包内绝对引用, error 查询或解析错误
|
// 返回: *ColorSpace 独立副本,Profile为包内绝对引用, error 查询或解析错误
|
||||||
@@ -98,11 +105,15 @@ func (c *resourceCatalog) styleDefinition(ctx context.Context, item ResourceInfo
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
entry := c.nodes[item.Kind+"/"+item.ID]
|
entry := c.nodes[item.Kind+"/"+item.ID]
|
||||||
data, err := editorXMLStandalone(entry.file.data[entry.node.start:entry.node.end], entry.node)
|
data, err := editorXMLStandaloneContext(ctx, entry.file.data[entry.node.start:entry.node.end], entry.node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return xml.Unmarshal(data, target)
|
decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
|
||||||
|
if err := decoder.Decode(target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ColorSpace 读取编辑器中的颜色空间,包括尚未绑定图元的新注册定义
|
// ColorSpace 读取编辑器中的颜色空间,包括尚未绑定图元的新注册定义
|
||||||
@@ -132,7 +143,7 @@ func (e *Editor) ImportColorSpace(ctx context.Context, space ColorSpace, profile
|
|||||||
return "", nil, nil, err
|
return "", nil, nil, err
|
||||||
}
|
}
|
||||||
resource := working.resources[len(working.resources)-1]
|
resource := working.resources[len(working.resources)-1]
|
||||||
data, err := working.encodeXML(func(x *ofdXML) { x.resources(nil, nil, []ColorSpace{*resource.space}) })
|
data, err := encodeOFDXMLContext(ctx, func(x *ofdXML) { x.resources(nil, nil, []ColorSpace{*resource.space}) })
|
||||||
parts := make(map[string][]byte)
|
parts := make(map[string][]byte)
|
||||||
if resource.name != "" {
|
if resource.name != "" {
|
||||||
parts[resource.name] = bytes.Clone(resource.data)
|
parts[resource.name] = bytes.Clone(resource.data)
|
||||||
@@ -146,7 +157,7 @@ func (e *Editor) ImportColorSpace(ctx context.Context, space ColorSpace, profile
|
|||||||
// 返回: string 新资源标识, error 定义校验或隔离提交错误
|
// 返回: string 新资源标识, error 定义校验或隔离提交错误
|
||||||
func (e *Editor) ImportDrawParam(ctx context.Context, draw DrawParam) (string, error) {
|
func (e *Editor) ImportDrawParam(ctx context.Context, draw DrawParam) (string, error) {
|
||||||
return e.importResourceStyle(ctx, func(working *Editor) (string, []byte, map[string][]byte, error) {
|
return e.importResourceStyle(ctx, func(working *Editor) (string, []byte, map[string][]byte, error) {
|
||||||
id, err := working.AddDrawParam(draw)
|
id, err := working.addDrawParam(ctx, draw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, nil, err
|
return "", nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -163,10 +174,13 @@ func (e *Editor) importResourceStyle(ctx context.Context, create func(*Editor) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
working, err := reader.Editor()
|
working, err := reader.EditorContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
if err := working.prepareSourceIDsContext(ctx); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
id, data, parts, err := create(working)
|
id, data, parts, err := create(working)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -185,6 +199,7 @@ func (e *Editor) importResourceStyle(ctx context.Context, create func(*Editor) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resourceStyleReader 为样式资源操作补齐待注册声明及附属文件,不改变公开快照的按引用导出语义
|
// resourceStyleReader 为样式资源操作补齐待注册声明及附属文件,不改变公开快照的按引用导出语义
|
||||||
|
// 复用内容扫描收集已有标识,不重复解析结构文件或构建未使用的资源目录
|
||||||
// 入参: ctx 取消上下文
|
// 入参: ctx 取消上下文
|
||||||
// 返回: *Reader 包含待注册字体、颜色空间及参数依赖的独立快照, error 读取或隔离错误
|
// 返回: *Reader 包含待注册字体、颜色空间及参数依赖的独立快照, error 读取或隔离错误
|
||||||
func (e *Editor) resourceStyleReader(ctx context.Context) (*Reader, error) {
|
func (e *Editor) resourceStyleReader(ctx context.Context) (*Reader, error) {
|
||||||
@@ -192,26 +207,25 @@ func (e *Editor) resourceStyleReader(ctx context.Context) (*Reader, error) {
|
|||||||
if err != nil || len(e.resources) == 0 {
|
if err != nil || len(e.resources) == 0 {
|
||||||
return reader, err
|
return reader, err
|
||||||
}
|
}
|
||||||
catalog, err := reader.resourceCatalog(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
known := make(map[string]bool)
|
known := make(map[string]bool)
|
||||||
for _, file := range catalog.scan.files {
|
_, err = scanEditorFontFiles(ctx, reader, reader.DocumentIndex(), func(_ *editorFontXML, root *editorXML) error {
|
||||||
root, err := parseEditorXMLContext(ctx, file.data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !root.matchesOFD("Res") {
|
if !root.matchesOFD("Res") {
|
||||||
continue
|
return nil
|
||||||
}
|
}
|
||||||
for _, group := range root.children {
|
for _, group := range root.children {
|
||||||
for _, node := range group.children {
|
for _, node := range group.children {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if node.matchesOFD(editorResourceElement(group.name.Local)) {
|
if node.matchesOFD(editorResourceElement(group.name.Local)) {
|
||||||
known[editorResourceID(node.attr("ID"))] = true
|
known[editorResourceID(node.attr("ID"))] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
var fonts, images []editorResource
|
var fonts, images []editorResource
|
||||||
var spaces []ColorSpace
|
var spaces []ColorSpace
|
||||||
@@ -252,7 +266,7 @@ func (e *Editor) resourceStyleReader(ctx context.Context) (*Reader, error) {
|
|||||||
}
|
}
|
||||||
var data []byte
|
var data []byte
|
||||||
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 = encodeOFDXMLContext(ctx, func(x *ofdXML) { x.resources(fonts, images, spaces) })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -341,7 +355,7 @@ func (e *Editor) ReplaceColorSpace(ctx context.Context, id string, space ColorSp
|
|||||||
space.Profile = "/" + name
|
space.Profile = "/" + name
|
||||||
changes[name] = bytes.Clone(profile)
|
changes[name] = bytes.Clone(profile)
|
||||||
}
|
}
|
||||||
data, err := working.encodeXML(func(x *ofdXML) { x.resources(nil, nil, []ColorSpace{space}) })
|
data, err := encodeOFDXMLContext(ctx, func(x *ofdXML) { x.resources(nil, nil, []ColorSpace{space}) })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -350,7 +364,7 @@ func (e *Editor) ReplaceColorSpace(ctx context.Context, id string, space ColorSp
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
node := root.child("ColorSpaces").child("ColorSpace")
|
node := root.child("ColorSpaces").child("ColorSpace")
|
||||||
fragment, err := editorXMLStandalone(data[node.start:node.end], node)
|
fragment, err := editorXMLStandaloneContext(ctx, data[node.start:node.end], node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -363,15 +377,18 @@ func (e *Editor) ReplaceColorSpace(ctx context.Context, id string, space ColorSp
|
|||||||
// 返回: error 参数、依赖或循环错误,失败不改变文档,成功隔离其他文档及历史版本并支持撤销
|
// 返回: error 参数、依赖或循环错误,失败不改变文档,成功隔离其他文档及历史版本并支持撤销
|
||||||
func (e *Editor) ReplaceDrawParam(ctx context.Context, id string, draw DrawParam) error {
|
func (e *Editor) ReplaceDrawParam(ctx context.Context, id string, draw DrawParam) error {
|
||||||
return e.changeResourceStyle(ctx, "DrawParam", id, func(catalog *resourceCatalog, item ResourceInfo, changes map[string][]byte) error {
|
return e.changeResourceStyle(ctx, "DrawParam", id, func(catalog *resourceCatalog, item ResourceInfo, changes map[string][]byte) error {
|
||||||
working, err := catalog.scan.reader.Editor()
|
working, err := catalog.scan.reader.EditorContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
added, err := working.AddDrawParam(draw)
|
if err := working.prepareSourceIDsContext(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
added, err := working.addDrawParam(ctx, draw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fragment, err := working.drawParamXML(added)
|
fragment, err := working.drawParamXMLContext(ctx, added)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -471,7 +488,8 @@ func validateResourceColors(ctx context.Context, catalog *resourceCatalog, space
|
|||||||
}
|
}
|
||||||
if resourceColorNode(node) && sameResourceID(id, space.ID) {
|
if resourceColorNode(node) && sameResourceID(id, space.ID) {
|
||||||
var paint FillColor
|
var paint FillColor
|
||||||
if err := xml.Unmarshal(file.data[node.start:node.end], &paint); err != nil {
|
decoder := xml.NewDecoder(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(file.data[node.start:node.end])), context: ctx})
|
||||||
|
if err := decoder.Decode(&paint); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := working.Color(&FillColor{Value: paint.Value, Index: paint.Index, Alpha: paint.Alpha, ColorSpace: space.ID}); err != nil {
|
if _, err := working.Color(&FillColor{Value: paint.Value, Index: paint.Index, Alpha: paint.Alpha, ColorSpace: space.ID}); err != nil {
|
||||||
@@ -493,13 +511,12 @@ func validateResourceColors(ctx context.Context, catalog *resourceCatalog, space
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validateResourceStyleGraph 检查替换定义的传递依赖,拒绝缺失资源、错误类型及跨底纹复合资源的循环
|
// validateResourceStyleGraph 检查替换定义的传递依赖,拒绝缺失资源、错误类型及跨底纹复合资源的循环
|
||||||
|
// 逐文件收集依赖后统一校验,不同时保留所有结构文件的XML树
|
||||||
// 入参: ctx 取消上下文, catalog 当前目录, id 替换标识, changes 待提交修改
|
// 入参: ctx 取消上下文, catalog 当前目录, id 替换标识, changes 待提交修改
|
||||||
// 返回: error 资源引用或循环错误
|
// 返回: error 资源引用或循环错误
|
||||||
func validateResourceStyleGraph(ctx context.Context, catalog *resourceCatalog, id string, changes map[string][]byte) error {
|
func validateResourceStyleGraph(ctx context.Context, catalog *resourceCatalog, id string, changes map[string][]byte) error {
|
||||||
types := make(map[string]string)
|
types := make(map[string]string)
|
||||||
graph := make(map[string][]string)
|
graph := make(map[string][]resourceStyleReference)
|
||||||
roots := make(map[string]*editorXML)
|
|
||||||
contents := make(map[string][]byte)
|
|
||||||
for name, file := range catalog.scan.files {
|
for name, file := range catalog.scan.files {
|
||||||
data, ok := changes[name]
|
data, ok := changes[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -509,10 +526,8 @@ func validateResourceStyleGraph(ctx context.Context, catalog *resourceCatalog, i
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
roots[name], contents[name] = root, data
|
types["file/"+name] = "File"
|
||||||
if !root.matchesOFD("Res") {
|
if root.matchesOFD("Res") {
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, group := range root.children {
|
for _, group := range root.children {
|
||||||
for _, node := range group.children {
|
for _, node := range group.children {
|
||||||
if node.matchesOFD(editorResourceElement(group.name.Local)) {
|
if node.matchesOFD(editorResourceElement(group.name.Local)) {
|
||||||
@@ -529,16 +544,12 @@ func validateResourceStyleGraph(ctx context.Context, catalog *resourceCatalog, i
|
|||||||
}
|
}
|
||||||
if location != "" {
|
if location != "" {
|
||||||
file := "file/" + resolveResourcePath(name, root.attr("BaseLoc"), location)
|
file := "file/" + resolveResourcePath(name, root.attr("BaseLoc"), location)
|
||||||
graph[key] = append(graph[key], file)
|
graph[key] = append(graph[key], resourceStyleReference{key: file})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for name := range roots {
|
|
||||||
types["file/"+name] = "File"
|
|
||||||
}
|
|
||||||
for name, root := range roots {
|
|
||||||
var visit func(*editorXML) error
|
var visit func(*editorXML) error
|
||||||
visit = func(node *editorXML) error {
|
visit = func(node *editorXML) error {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
@@ -552,12 +563,11 @@ func validateResourceStyleGraph(ctx context.Context, catalog *resourceCatalog, i
|
|||||||
owner = "file/" + name
|
owner = "file/" + name
|
||||||
}
|
}
|
||||||
if slices.Contains([]string{"Thumbnail", "Substitution"}, node.name.Local) {
|
if slices.Contains([]string{"Thumbnail", "Substitution"}, node.name.Local) {
|
||||||
graph[owner] = append(graph[owner], editorResourceID(editorImportText(contents[name], node)))
|
graph[owner] = append(graph[owner], resourceStyleReference{key: editorResourceID(editorImportText(data, node))})
|
||||||
}
|
}
|
||||||
for _, attr := range node.attrs {
|
for _, attr := range node.attrs {
|
||||||
if attr.Name.Space == "" && attr.Value != "" && slices.Contains([]string{"Relative", "DrawParam", "ColorSpace", "Font", "ResourceID", "ImageMask", "Substitution", "Thumbnail"}, attr.Name.Local) {
|
if attr.Name.Space == "" && attr.Value != "" && slices.Contains([]string{"Relative", "DrawParam", "ColorSpace", "Font", "ResourceID", "ImageMask", "Substitution", "Thumbnail"}, attr.Name.Local) {
|
||||||
key := editorResourceID(attr.Value)
|
key := editorResourceID(attr.Value)
|
||||||
graph[owner] = append(graph[owner], key)
|
|
||||||
kind := ""
|
kind := ""
|
||||||
switch attr.Name.Local {
|
switch attr.Name.Local {
|
||||||
case "Relative", "DrawParam":
|
case "Relative", "DrawParam":
|
||||||
@@ -565,9 +575,7 @@ func validateResourceStyleGraph(ctx context.Context, catalog *resourceCatalog, i
|
|||||||
case "ColorSpace", "Font":
|
case "ColorSpace", "Font":
|
||||||
kind = attr.Name.Local
|
kind = attr.Name.Local
|
||||||
}
|
}
|
||||||
if kind != "" && types[key] != kind {
|
graph[owner] = append(graph[owner], resourceStyleReference{key: key, kind: kind})
|
||||||
graph[owner] = append(graph[owner], "")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, child := range node.children {
|
for _, child := range node.children {
|
||||||
@@ -582,12 +590,13 @@ func validateResourceStyleGraph(ctx context.Context, catalog *resourceCatalog, i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
state := make(map[string]int)
|
state := make(map[string]int)
|
||||||
var visit func(string) error
|
var visit func(resourceStyleReference) error
|
||||||
visit = func(key string) error {
|
visit = func(reference resourceStyleReference) error {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if types[key] == "" || state[key] == 1 {
|
key := reference.key
|
||||||
|
if types[key] == "" || reference.kind != "" && types[key] != reference.kind || state[key] == 1 {
|
||||||
return fmt.Errorf("missing, mistyped or cyclic style resource %q", key)
|
return fmt.Errorf("missing, mistyped or cyclic style resource %q", key)
|
||||||
}
|
}
|
||||||
if state[key] == 2 {
|
if state[key] == 2 {
|
||||||
@@ -602,5 +611,5 @@ func validateResourceStyleGraph(ctx context.Context, catalog *resourceCatalog, i
|
|||||||
state[key] = 2
|
state[key] = 2
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return visit(id)
|
return visit(resourceStyleReference{key: id})
|
||||||
}
|
}
|
||||||
@@ -324,6 +324,19 @@ func (e *Editor) compactResourceReferences(parts map[string][]byte, reader *Read
|
|||||||
// 入参: input 文件流, name 包内路径
|
// 入参: input 文件流, name 包内路径
|
||||||
// 返回: bool 是否资源索引, bool 是否可完整判断引用
|
// 返回: bool 是否资源索引, bool 是否可完整判断引用
|
||||||
func (r *editorResourceRefs) scan(input io.Reader, name string) (bool, bool) {
|
func (r *editorResourceRefs) scan(input io.Reader, name string) (bool, bool) {
|
||||||
|
return r.scanContext(nil, input, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanContext 流式收集资源引用,在读取和字体用字处理期间检查取消
|
||||||
|
// 入参: ctx 取消上下文,可为nil, input 文件流, name 包内路径
|
||||||
|
// 返回: bool 是否资源索引, bool 是否完整收集引用,取消时为false
|
||||||
|
func (r *editorResourceRefs) scanContext(ctx context.Context, input io.Reader, name string) (bool, bool) {
|
||||||
|
if ctx != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
input = imageInput{ReadCloser: io.NopCloser(input), context: ctx}
|
||||||
|
}
|
||||||
buffer := bufio.NewReader(input)
|
buffer := bufio.NewReader(input)
|
||||||
found, err := editorXMLPrefix(buffer)
|
found, err := editorXMLPrefix(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -333,8 +346,11 @@ func (r *editorResourceRefs) scan(input io.Reader, name string) (bool, bool) {
|
|||||||
return false, !strings.EqualFold(path.Ext(name), ".xml")
|
return false, !strings.EqualFold(path.Ext(name), ".xml")
|
||||||
}
|
}
|
||||||
decoder := xml.NewDecoder(buffer)
|
decoder := xml.NewDecoder(buffer)
|
||||||
scan := editorReferenceScan{refs: r, name: name, safe: true}
|
scan := editorReferenceScan{ctx: ctx, refs: r, name: name, safe: true}
|
||||||
for {
|
for {
|
||||||
|
if ctx != nil && ctx.Err() != nil {
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
token, err := decoder.Token()
|
token, err := decoder.Token()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
return scan.resource, scan.seen && len(scan.stack) == 0
|
return scan.resource, scan.seen && len(scan.stack) == 0
|
||||||
|
|||||||
@@ -27,16 +27,15 @@ import (
|
|||||||
|
|
||||||
// pruneStructureReferences 迁移公文图元编号并清理已删除的引用,保留未知标引和原有悬空引用
|
// pruneStructureReferences 迁移公文图元编号并清理已删除的引用,保留未知标引和原有悬空引用
|
||||||
// 改动进入输出快照,由既有文档及版本隔离流程维护共享文件
|
// 改动进入输出快照,由既有文档及版本隔离流程维护共享文件
|
||||||
// 入参: parts 输出覆盖条目, generated 本次编码结果及扫描记录
|
// 入参: ctx 取消上下文, parts 输出覆盖条目, generated 本次编码结果及扫描记录
|
||||||
// 返回: error 读取、解析或取消错误
|
// 返回: error 读取、解析或取消错误
|
||||||
func (e *Editor) pruneStructureReferences(parts map[string][]byte, generated map[string]editorGeneratedReferences) error {
|
func (e *Editor) pruneStructureReferences(ctx context.Context, parts map[string][]byte, generated map[string]editorGeneratedReferences) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if e.source.document.CustomTags.Path == "" && len(e.source.document.CustomTags.CustomTag) == 0 {
|
if e.source.document.CustomTags.Path == "" && len(e.source.document.CustomTags.CustomTag) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
|
||||||
if e.output != nil {
|
|
||||||
ctx = e.output.ctx
|
|
||||||
}
|
|
||||||
tags, err := e.source.reader.CustomTagsContext(ctx)
|
tags, err := e.source.reader.CustomTagsContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
+19
-1
@@ -15,6 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
@@ -226,6 +227,20 @@ func (e *Editor) SetObjectDrawParam(page int, ids []string, id string) error {
|
|||||||
// 入参: draw 绘制参数,ID由编辑器分配
|
// 入参: draw 绘制参数,ID由编辑器分配
|
||||||
// 返回: string 资源标识, error 错误信息
|
// 返回: string 资源标识, error 错误信息
|
||||||
func (e *Editor) AddDrawParam(draw DrawParam) (string, error) {
|
func (e *Editor) AddDrawParam(draw DrawParam) (string, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
if e.output != nil && e.output.ctx != nil {
|
||||||
|
ctx = e.output.ctx
|
||||||
|
}
|
||||||
|
return e.addDrawParam(ctx, draw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addDrawParam 校验并注册绘制参数,资源操作可传递取消上下文
|
||||||
|
// 入参: ctx 取消上下文, draw 绘制参数
|
||||||
|
// 返回: string 资源标识, error 校验、编码或取消错误
|
||||||
|
func (e *Editor) addDrawParam(ctx context.Context, draw DrawParam) (string, error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
if draw.ResourceID != "" || draw.BaseLoc != "" || draw.Link != "" {
|
if draw.ResourceID != "" || draw.BaseLoc != "" || draw.Link != "" {
|
||||||
return "", fmt.Errorf("external draw parameter references cannot be registered directly")
|
return "", fmt.Errorf("external draw parameter references cannot be registered directly")
|
||||||
}
|
}
|
||||||
@@ -246,11 +261,14 @@ func (e *Editor) AddDrawParam(draw DrawParam) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, paint := range []*FillColor{draw.FillColor, (*FillColor)(draw.StrokeColor)} {
|
for _, paint := range []*FillColor{draw.FillColor, (*FillColor)(draw.StrokeColor)} {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
if err := e.editorColor(paint); err != nil {
|
if err := e.editorColor(paint); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return e.addEditorDrawParam(cloneEditorData(draw))
|
return e.addEditorDrawParamContext(ctx, cloneEditorData(draw))
|
||||||
}
|
}
|
||||||
|
|
||||||
// StyleObjects 原子更新对象透明度及文字、路径的填充和描边样式
|
// StyleObjects 原子更新对象透明度及文字、路径的填充和描边样式
|
||||||
|
|||||||
+20
-2
@@ -839,12 +839,24 @@ func editorXMLContainer(name string, attrs ofdAttrs, content []byte) ([]byte, er
|
|||||||
// 入参: data 对象XML, source 原节点
|
// 入参: data 对象XML, source 原节点
|
||||||
// 返回: []byte 独立XML片段, error 错误信息
|
// 返回: []byte 独立XML片段, error 错误信息
|
||||||
func editorXMLStandalone(data []byte, source *editorXML) ([]byte, error) {
|
func editorXMLStandalone(data []byte, source *editorXML) ([]byte, error) {
|
||||||
root, err := parseEditorXML(data)
|
return editorXMLStandaloneContext(nil, data, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorXMLStandaloneContext 补齐继承的命名空间,解析及生成片段期间响应取消
|
||||||
|
// 入参: ctx 取消上下文,可为nil, data 对象XML, source 原节点
|
||||||
|
// 返回: []byte 独立XML片段, error 解析或取消错误
|
||||||
|
func editorXMLStandaloneContext(ctx context.Context, data []byte, source *editorXML) ([]byte, error) {
|
||||||
|
root, err := parseEditorXMLContext(ctx, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
namespaces := make(map[string]string)
|
namespaces := make(map[string]string)
|
||||||
for node := source.parent; node != nil; node = node.parent {
|
for node := source.parent; node != nil; node = node.parent {
|
||||||
|
if ctx != nil {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, attr := range node.attrs {
|
for _, attr := range node.attrs {
|
||||||
key := ""
|
key := ""
|
||||||
if attr.Name.Space == "xmlns" {
|
if attr.Name.Space == "xmlns" {
|
||||||
@@ -886,5 +898,11 @@ func editorXMLStandalone(data []byte, source *editorXML) ([]byte, error) {
|
|||||||
if data[position-1] == '/' {
|
if data[position-1] == '/' {
|
||||||
position--
|
position--
|
||||||
}
|
}
|
||||||
return editorPatchXML(data, []editorXMLPatch{{position, position, added.Bytes()}}), nil
|
result := editorPatchXML(data, []editorXMLPatch{{position, position, added.Bytes()}})
|
||||||
|
if ctx != nil {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
}
|
}
|
||||||
+19
-5
@@ -78,7 +78,7 @@ func (w imageOutput) Write(data []byte) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Images 列出文档、页面和模板声明的图片资源,不解码图片像素
|
// Images 列出文档、页面和模板声明的图片资源,不解码图片像素
|
||||||
// 包含未使用及重复引用的资源声明,同一资源标识只返回一次
|
// 包含未使用及重复引用的资源声明,同一资源标识只返回一次,不加载注解或签名
|
||||||
// 入参: ctx 取消上下文
|
// 入参: ctx 取消上下文
|
||||||
// 返回: []ImageInfo 图片资源, error 读取或取消错误
|
// 返回: []ImageInfo 图片资源, error 读取或取消错误
|
||||||
func (r *Reader) Images(ctx context.Context) ([]ImageInfo, error) {
|
func (r *Reader) Images(ctx context.Context) ([]ImageInfo, error) {
|
||||||
@@ -88,7 +88,7 @@ func (r *Reader) Images(ctx context.Context) ([]ImageInfo, error) {
|
|||||||
if r.imageCatalog != nil {
|
if r.imageCatalog != nil {
|
||||||
return slices.Clone(r.imageCatalog), nil
|
return slices.Clone(r.imageCatalog), nil
|
||||||
}
|
}
|
||||||
doc, err := r.DocContext(ctx)
|
doc, err := r.docStructureContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ func (r *Reader) Images(ctx context.Context) ([]ImageInfo, error) {
|
|||||||
return slices.Clone(items), nil
|
return slices.Clone(items), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image 查找指定图片,已加载资源直接读取所在资源文件,不扫描其他页面
|
// Image 查找指定图片,复用媒体索引或图片清单,尚未建立索引时读取资源声明
|
||||||
// 入参: ctx 取消上下文, id 图片资源标识
|
// 入参: ctx 取消上下文, id 图片资源标识
|
||||||
// 返回: ImageInfo 图片资源, error 读取或取消错误
|
// 返回: ImageInfo 图片资源, error 读取或取消错误
|
||||||
func (r *Reader) Image(ctx context.Context, id string) (ImageInfo, error) {
|
func (r *Reader) Image(ctx context.Context, id string) (ImageInfo, error) {
|
||||||
@@ -171,6 +171,15 @@ func (r *Reader) Image(ctx context.Context, id string) (ImageInfo, error) {
|
|||||||
}
|
}
|
||||||
name, _ := resourceValue(r.resourceFiles, id)
|
name, _ := resourceValue(r.resourceFiles, id)
|
||||||
if name != "" {
|
if name != "" {
|
||||||
|
if r.resourcesRead[name] {
|
||||||
|
if media, ok := resourceValue(r.mediaCache, id); ok && media.Type == "Image" && media.MediaFile != "" {
|
||||||
|
location := r.ResPath(media.MediaFile)
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return ImageInfo{}, err
|
||||||
|
}
|
||||||
|
return ImageInfo{ID: id, Name: path.Base(location), Format: media.Format, Location: location}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
data, err := r.readFileViewContext(ctx, name)
|
data, err := r.readFileViewContext(ctx, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ImageInfo{}, err
|
return ImageInfo{}, err
|
||||||
@@ -191,15 +200,20 @@ func (r *Reader) Image(ctx context.Context, id string) (ImageInfo, error) {
|
|||||||
}
|
}
|
||||||
return ImageInfo{}, fmt.Errorf("image resource %q not found", id)
|
return ImageInfo{}, fmt.Errorf("image resource %q not found", id)
|
||||||
}
|
}
|
||||||
items, err := r.Images(ctx)
|
items := r.imageCatalog
|
||||||
|
if items == nil {
|
||||||
|
var err error
|
||||||
|
items, err = r.Images(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ImageInfo{}, err
|
return ImageInfo{}, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
key := imageResourceKey(id)
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return ImageInfo{}, err
|
return ImageInfo{}, err
|
||||||
}
|
}
|
||||||
if imageResourceKey(item.ID) == imageResourceKey(id) {
|
if imageResourceKey(item.ID) == key {
|
||||||
return item, nil
|
return item, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -72,10 +72,10 @@ type fontUsageReferences struct {
|
|||||||
patterns map[*Pattern]bool
|
patterns map[*Pattern]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonts 获取OFD声明的字体列表
|
// Fonts 获取OFD声明的字体列表,不加载注解或签名
|
||||||
// 返回: []Font 字体列表, error 错误信息
|
// 返回: []Font 字体列表, error 错误信息
|
||||||
func (r *Reader) Fonts() ([]Font, error) {
|
func (r *Reader) Fonts() ([]Font, error) {
|
||||||
doc, err := r.Doc()
|
doc, err := r.docStructure()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -20,10 +20,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Media 获取已加载的音视频资源,页面资源须先读取所属页面
|
// Media 获取已加载的音视频资源,页面资源须先读取所属页面
|
||||||
|
// 不加载注解或签名
|
||||||
// 入参: id 资源标识
|
// 入参: id 资源标识
|
||||||
// 返回: MultiMedia 资源信息, error 资源缺失或类型错误
|
// 返回: MultiMedia 资源信息, error 资源缺失或类型错误
|
||||||
func (r *Reader) Media(id string) (MultiMedia, error) {
|
func (r *Reader) Media(id string) (MultiMedia, error) {
|
||||||
if _, err := r.Doc(); err != nil {
|
if _, err := r.docStructure(); err != nil {
|
||||||
return MultiMedia{}, err
|
return MultiMedia{}, err
|
||||||
}
|
}
|
||||||
media, ok := resourceValue(r.mediaCache, id)
|
media, ok := resourceValue(r.mediaCache, id)
|
||||||
|
|||||||
@@ -435,6 +435,7 @@ func pdfCompositeInterpolate(target *pdfCompositePixel, source pdfCompositePixel
|
|||||||
}
|
}
|
||||||
|
|
||||||
// drawGroup 按隔离标志保留初始背景,移除背景贡献后应用组透明度
|
// drawGroup 按隔离标志保留初始背景,移除背景贡献后应用组透明度
|
||||||
|
// 非隔离同空间的非挖空组借用父组背景,子组只写入独立结果缓冲
|
||||||
// 入参: node 透明组, pixels 父组像素, parent 父混合空间
|
// 入参: node 透明组, pixels 父组像素, parent 父混合空间
|
||||||
// 返回: error 颜色或合成错误
|
// 返回: error 颜色或合成错误
|
||||||
func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePixel, parent *pdfgo.ColorSpace) error {
|
func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePixel, parent *pdfgo.ColorSpace) error {
|
||||||
@@ -466,15 +467,25 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
|
|||||||
if space == nil {
|
if space == nil {
|
||||||
space = parent
|
space = parent
|
||||||
}
|
}
|
||||||
if !g.Isolated && !g.Knockout && g.Alpha == 1 && g.SoftMask == nil && pdfNormalBlend(g.BlendMode) && space.Equal(parent) {
|
sameSpace := space.Equal(parent)
|
||||||
|
sharedInitial := !g.Isolated && !g.Knockout && sameSpace
|
||||||
|
if sharedInitial && g.Alpha == 1 && g.SoftMask == nil && pdfNormalBlend(g.BlendMode) {
|
||||||
return c.draw(node.children, pixels, space)
|
return c.draw(node.children, pixels, space)
|
||||||
}
|
}
|
||||||
initial := c.acquirePixels(len(pixels))
|
initial := pixels
|
||||||
|
if !sharedInitial {
|
||||||
|
initial = c.acquirePixels(len(pixels))
|
||||||
defer c.releasePixels(initial)
|
defer c.releasePixels(initial)
|
||||||
|
}
|
||||||
|
result := c.acquirePixels(len(initial))
|
||||||
|
defer c.releasePixels(result)
|
||||||
if g.Isolated {
|
if g.Isolated {
|
||||||
clear(initial)
|
clear(initial)
|
||||||
} else {
|
} else {
|
||||||
sameSpace := space.Equal(parent)
|
background := initial
|
||||||
|
if sharedInitial {
|
||||||
|
background = result
|
||||||
|
}
|
||||||
components := parent.Components()
|
components := parent.Components()
|
||||||
for i := range pixels {
|
for i := range pixels {
|
||||||
if i%pdfCompositeTileSize == 0 {
|
if i%pdfCompositeTileSize == 0 {
|
||||||
@@ -491,12 +502,12 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initial[i] = pdfCompositePixel{values: values, alpha: pixel.alpha}
|
background[i] = pdfCompositePixel{values: values, alpha: pixel.alpha}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result := c.acquirePixels(len(initial))
|
if !sharedInitial {
|
||||||
defer c.releasePixels(result)
|
|
||||||
copy(result, initial)
|
copy(result, initial)
|
||||||
|
}
|
||||||
local := *c
|
local := *c
|
||||||
if c.channels != nil {
|
if c.channels != nil {
|
||||||
local.channels = c.acquireChannels(len(c.channels))
|
local.channels = c.acquireChannels(len(c.channels))
|
||||||
@@ -657,6 +668,7 @@ func pdfCompositeGroupRegion(ctx context.Context, pixels, initial, result []pdfC
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
components, parentComponents := space.Components(), parent.Components()
|
components, parentComponents := space.Components(), parent.Components()
|
||||||
|
var target pdfgo.ColorantPixel
|
||||||
for y := region.Min.Y; y < region.Max.Y; y++ {
|
for y := region.Min.Y; y < region.Max.Y; y++ {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -672,7 +684,10 @@ func pdfCompositeGroupRegion(ctx context.Context, pixels, initial, result []pdfC
|
|||||||
alpha *= mask[i]
|
alpha *= mask[i]
|
||||||
}
|
}
|
||||||
pixel := &result[i]
|
pixel := &result[i]
|
||||||
target := pdfgo.ColorantPixel{Values: pixels[i].values[:parentComponents], Alpha: pixels[i].alpha, Shape: pixels[i].shape, Effect: pixels[i].effect}
|
target.Values = pixels[i].values[:parentComponents]
|
||||||
|
target.Alpha = pixels[i].alpha
|
||||||
|
target.Shape = pixels[i].shape
|
||||||
|
target.Effect = pixels[i].effect
|
||||||
if err := compositor.Composite(&target,
|
if err := compositor.Composite(&target,
|
||||||
pdfgo.ColorantPixel{Values: initial[i].values[:components], Alpha: initial[i].alpha, Shape: initial[i].shape, Effect: initial[i].effect},
|
pdfgo.ColorantPixel{Values: initial[i].values[:components], Alpha: initial[i].alpha, Shape: initial[i].shape, Effect: initial[i].effect},
|
||||||
pdfgo.ColorantPixel{Values: pixel.values[:components], Alpha: pixel.alpha, Shape: pixel.shape, Effect: pixel.effect},
|
pdfgo.ColorantPixel{Values: pixel.values[:components], Alpha: pixel.alpha, Shape: pixel.shape, Effect: pixel.effect},
|
||||||
@@ -808,11 +823,11 @@ func (c *pdfCompositor) imageColor(mark *pdfgo.ImageMark, point pdfgo.Point, spa
|
|||||||
}
|
}
|
||||||
return source.ValuesAt(x, y)
|
return source.ValuesAt(x, y)
|
||||||
}
|
}
|
||||||
values, alpha := sample(int(math.Floor(x+.5)), int(math.Floor(y+.5)))
|
var values [4]float64
|
||||||
|
var alpha float64
|
||||||
if mark.Image.Interpolate {
|
if mark.Image.Interpolate {
|
||||||
ix, iy := int(math.Floor(x)), int(math.Floor(y))
|
ix, iy := int(math.Floor(x)), int(math.Floor(y))
|
||||||
fx, fy := x-float64(ix), y-float64(iy)
|
fx, fy := x-float64(ix), y-float64(iy)
|
||||||
values, alpha = [4]float64{}, 0
|
|
||||||
for j := 0; j < 2; j++ {
|
for j := 0; j < 2; j++ {
|
||||||
for i := 0; i < 2; i++ {
|
for i := 0; i < 2; i++ {
|
||||||
weight := (1 - fx) * (1 - fy)
|
weight := (1 - fx) * (1 - fy)
|
||||||
@@ -837,6 +852,8 @@ func (c *pdfCompositor) imageColor(mark *pdfgo.ImageMark, point pdfgo.Point, spa
|
|||||||
values[k] = math.Max(0, math.Min(1, values[k]/alpha))
|
values[k] = math.Max(0, math.Min(1, values[k]/alpha))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
values, alpha = sample(int(math.Floor(x+.5)), int(math.Floor(y+.5)))
|
||||||
}
|
}
|
||||||
if mark.Image.ImageMask {
|
if mark.Image.ImageMask {
|
||||||
if mark.Style.Fill.Tiling != nil || mark.Style.Fill.Shading != nil {
|
if mark.Style.Fill.Tiling != nil || mark.Style.Fill.Shading != nil {
|
||||||
|
|||||||
+2
-3
@@ -19,7 +19,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/png"
|
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
@@ -487,13 +486,13 @@ func (p *pdfImporter) renderGroupSceneInto(source *RasterPage, box Box, target *
|
|||||||
// 返回: error 保存错误
|
// 返回: error 保存错误
|
||||||
func (p *pdfImporter) appendRasterGroup(img image.Image, box Box, opacity float64) error {
|
func (p *pdfImporter) appendRasterGroup(img image.Image, box Box, opacity float64) error {
|
||||||
var data bytes.Buffer
|
var data bytes.Buffer
|
||||||
if err := png.Encode(&data, img); err != nil {
|
if err := pdfgo.EncodePNG(p.ctx, &data, img); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := p.ctx.Err(); err != nil {
|
if err := p.ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
id, err := p.editor.addDecodedImage(data.Bytes(), img)
|
id, err := p.editor.addDecodedImageContext(p.ctx, data.Bytes(), img)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-1
@@ -16,9 +16,11 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -85,6 +87,7 @@ func (s *pdfStencilImage) NRGBA64At(x, y int) color.NRGBA64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// image 转换图像样本,不将整页或其他对象栅格化
|
// image 转换图像样本,不将整页或其他对象栅格化
|
||||||
|
// 有损编码复用已解码像素,候选容量超限时沿用原编码路径
|
||||||
// 入参: mark PDF图像绘制信息
|
// 入参: mark PDF图像绘制信息
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
||||||
@@ -206,6 +209,14 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
|||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
data = jpegOriginal
|
data = jpegOriginal
|
||||||
}
|
}
|
||||||
|
optimized := false
|
||||||
|
if len(data) == 0 && decoded != nil && !source.ImageMask && p.compression.Mode == CompressionLossy {
|
||||||
|
data, err = pdfgo.EncodeImageResource(p.ctx, decoded, p.compression)
|
||||||
|
if err != nil && !errors.Is(err, io.ErrShortBuffer) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
optimized = err == nil
|
||||||
|
}
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
var encoded bytes.Buffer
|
var encoded bytes.Buffer
|
||||||
if decoded != nil {
|
if decoded != nil {
|
||||||
@@ -219,13 +230,15 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
|||||||
data = encoded.Bytes()
|
data = encoded.Bytes()
|
||||||
}
|
}
|
||||||
if !source.ImageMask && p.compression.Mode == CompressionLossy {
|
if !source.ImageMask && p.compression.Mode == CompressionLossy {
|
||||||
|
if !optimized {
|
||||||
data, err = pdfgo.OptimizeImageResource(p.ctx, data, p.compression, key.size)
|
data, err = pdfgo.OptimizeImageResource(p.ctx, data, p.compression, key.size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
decoded = nil
|
decoded = nil
|
||||||
}
|
}
|
||||||
id, err = p.editor.addDecodedImage(data, decoded)
|
id, err = p.editor.addDecodedImageContext(p.ctx, data, decoded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+94
-23
@@ -339,10 +339,10 @@ func (r *Reader) PageContentContext(ctx context.Context, page Page) (*PageConten
|
|||||||
return r.readPageContent(ctx, page, false, false)
|
return r.readPageContent(ctx, page, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageCount 获取当前文档总页数
|
// PageCount 获取当前文档总页数,不加载注解或签名
|
||||||
// 返回: int 页数, error 错误信息
|
// 返回: int 页数, error 错误信息
|
||||||
func (r *Reader) PageCount() (int, error) {
|
func (r *Reader) PageCount() (int, error) {
|
||||||
doc, err := r.Doc()
|
doc, err := r.docStructure()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -443,40 +443,40 @@ func (r *Reader) DocInfo() (*DocInfo, error) {
|
|||||||
return &r.OFD.DocBody[r.documentIndex].DocInfo, nil
|
return &r.OFD.DocBody[r.documentIndex].DocInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permissions 获取文档权限信息
|
// Permissions 获取文档权限信息,不加载注解或签名
|
||||||
// 返回: *Permissions 权限信息, error 错误信息
|
// 返回: *Permissions 权限信息, error 错误信息
|
||||||
func (r *Reader) Permissions() (*Permissions, error) {
|
func (r *Reader) Permissions() (*Permissions, error) {
|
||||||
doc, err := r.Doc()
|
doc, err := r.docStructure()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &doc.Permissions, nil
|
return &doc.Permissions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Outlines 获取文档大纲
|
// Outlines 获取文档大纲,不加载注解或签名
|
||||||
// 返回: []OutlineElem 大纲列表, error 错误信息
|
// 返回: []OutlineElem 大纲列表, error 错误信息
|
||||||
func (r *Reader) Outlines() ([]OutlineElem, error) {
|
func (r *Reader) Outlines() ([]OutlineElem, error) {
|
||||||
doc, err := r.Doc()
|
doc, err := r.docStructure()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return doc.Outlines.OutlineElem, nil
|
return doc.Outlines.OutlineElem, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actions 获取文档动作
|
// Actions 获取文档动作,不加载注解或签名
|
||||||
// 返回: []Action 动作列表, error 错误信息
|
// 返回: []Action 动作列表, error 错误信息
|
||||||
func (r *Reader) Actions() ([]Action, error) {
|
func (r *Reader) Actions() ([]Action, error) {
|
||||||
doc, err := r.Doc()
|
doc, err := r.docStructure()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return doc.Actions, nil
|
return doc.Actions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bookmarks 获取文档书签
|
// Bookmarks 获取文档书签,不加载注解或签名
|
||||||
// 返回: []Bookmark 书签列表, error 错误信息
|
// 返回: []Bookmark 书签列表, error 错误信息
|
||||||
func (r *Reader) Bookmarks() ([]Bookmark, error) {
|
func (r *Reader) Bookmarks() ([]Bookmark, error) {
|
||||||
doc, err := r.Doc()
|
doc, err := r.docStructure()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -486,17 +486,37 @@ func (r *Reader) Bookmarks() ([]Bookmark, error) {
|
|||||||
// Attachments 获取独立的附件列表,修改返回值不影响阅读器缓存
|
// Attachments 获取独立的附件列表,修改返回值不影响阅读器缓存
|
||||||
// 返回: []Attachment 附件列表, error 错误信息
|
// 返回: []Attachment 附件列表, error 错误信息
|
||||||
func (r *Reader) Attachments() ([]Attachment, error) {
|
func (r *Reader) Attachments() ([]Attachment, error) {
|
||||||
doc, err := r.Doc()
|
return r.AttachmentsContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttachmentsContext 获取独立的附件列表,不加载无关注解或签名
|
||||||
|
// 取消或失败时不缓存未完成的附件清单,修改返回值不影响缓存
|
||||||
|
// 入参: ctx 取消上下文
|
||||||
|
// 返回: []Attachment 附件列表, error 读取、解析或取消错误
|
||||||
|
func (r *Reader) AttachmentsContext(ctx context.Context) ([]Attachment, error) {
|
||||||
|
return r.attachmentsContext(ctx, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// attachmentsContext 按调用方所有权读取附件清单,库内只读访问不复制完整清单
|
||||||
|
// 入参: ctx 取消上下文, readonly 是否允许借用只读清单
|
||||||
|
// 返回: []Attachment 附件列表, error 读取、解析或取消错误
|
||||||
|
func (r *Reader) attachmentsContext(ctx context.Context, readonly bool) ([]Attachment, error) {
|
||||||
|
doc, err := r.docStructureContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(doc.Attachments.Path) != "" && doc.Attachments.Attachment == nil {
|
items := doc.Attachments.Attachment
|
||||||
|
pending := strings.TrimSpace(doc.Attachments.Path) != "" && items == nil
|
||||||
|
if pending {
|
||||||
var attachments Attachments
|
var attachments Attachments
|
||||||
partPath, err := r.readDocumentPart(doc.Attachments.Path, &attachments)
|
partPath, err := r.readDocumentPartContext(ctx, doc.Attachments.Path, &attachments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for i := range attachments.Attachment {
|
for i := range attachments.Attachment {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
attachment := &attachments.Attachment[i]
|
attachment := &attachments.Attachment[i]
|
||||||
if fileLoc := resolveResourcePath(partPath, "", attachment.FileLoc); fileLoc != "" {
|
if fileLoc := resolveResourcePath(partPath, "", attachment.FileLoc); fileLoc != "" {
|
||||||
attachment.FileLoc = "/" + fileLoc
|
attachment.FileLoc = "/" + fileLoc
|
||||||
@@ -505,15 +525,30 @@ func (r *Reader) Attachments() ([]Attachment, error) {
|
|||||||
if attachments.Attachment == nil {
|
if attachments.Attachment == nil {
|
||||||
attachments.Attachment = []Attachment{}
|
attachments.Attachment = []Attachment{}
|
||||||
}
|
}
|
||||||
doc.Attachments.Attachment = attachments.Attachment
|
items = attachments.Attachment
|
||||||
}
|
}
|
||||||
attachments := slices.Clone(doc.Attachments.Attachment)
|
attachments := items
|
||||||
for i := range attachments {
|
if !readonly {
|
||||||
|
if items != nil {
|
||||||
|
attachments = make([]Attachment, len(items))
|
||||||
|
}
|
||||||
|
for i := range items {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
attachments[i] = items[i]
|
||||||
if attachments[i].Size != nil {
|
if attachments[i].Size != nil {
|
||||||
size := *attachments[i].Size
|
size := *attachments[i].Size
|
||||||
attachments[i].Size = &size
|
attachments[i].Size = &size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if pending {
|
||||||
|
doc.Attachments.Attachment = items
|
||||||
|
}
|
||||||
return attachments, nil
|
return attachments, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,14 +556,36 @@ func (r *Reader) Attachments() ([]Attachment, error) {
|
|||||||
// 入参: id 附件标识
|
// 入参: id 附件标识
|
||||||
// 返回: io.ReadCloser 附件文件流, error 错误信息
|
// 返回: io.ReadCloser 附件文件流, error 错误信息
|
||||||
func (r *Reader) OpenAttachment(id string) (io.ReadCloser, error) {
|
func (r *Reader) OpenAttachment(id string) (io.ReadCloser, error) {
|
||||||
attachments, err := r.Attachments()
|
return r.OpenAttachmentContext(context.Background(), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenAttachmentContext 打开可取消读取的附件文件流,调用方负责关闭
|
||||||
|
// 完整读取后校验ZIP数据,不加载无关注解或签名
|
||||||
|
// 入参: ctx 取消上下文, id 附件标识
|
||||||
|
// 返回: io.ReadCloser 附件文件流, error 读取或取消错误
|
||||||
|
func (r *Reader) OpenAttachmentContext(ctx context.Context, id string) (io.ReadCloser, error) {
|
||||||
|
attachments, err := r.attachmentsContext(ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, attachment := range attachments {
|
for _, attachment := range attachments {
|
||||||
if attachment.ID == id {
|
if err := ctx.Err(); err != nil {
|
||||||
return r.openFile(r.ResPath(attachment.FileLoc))
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if attachment.ID == id {
|
||||||
|
stream, err := r.openFile(r.ResPath(attachment.FileLoc))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
stream.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return imageInput{ReadCloser: stream, context: ctx}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("attachment not found: %s", id)
|
return nil, fmt.Errorf("attachment not found: %s", id)
|
||||||
}
|
}
|
||||||
@@ -537,12 +594,26 @@ func (r *Reader) OpenAttachment(id string) (io.ReadCloser, error) {
|
|||||||
// 入参: id 附件标识
|
// 入参: id 附件标识
|
||||||
// 返回: []byte 附件数据, error 错误信息
|
// 返回: []byte 附件数据, error 错误信息
|
||||||
func (r *Reader) AttachmentData(id string) ([]byte, error) {
|
func (r *Reader) AttachmentData(id string) ([]byte, error) {
|
||||||
stream, err := r.OpenAttachment(id)
|
return r.AttachmentDataContext(context.Background(), id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttachmentDataContext 获取独立的附件数据,读取失败或取消时不返回部分内容
|
||||||
|
// 入参: ctx 取消上下文, id 附件标识
|
||||||
|
// 返回: []byte 附件数据, error 读取或取消错误
|
||||||
|
func (r *Reader) AttachmentDataContext(ctx context.Context, id string) ([]byte, error) {
|
||||||
|
stream, err := r.OpenAttachmentContext(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer stream.Close()
|
defer stream.Close()
|
||||||
return io.ReadAll(stream)
|
data, err := io.ReadAll(stream)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CustomTags 获取自定义标引
|
// CustomTags 获取自定义标引
|
||||||
@@ -599,11 +670,11 @@ func (r *Reader) CustomDatas() ([]CustomData, error) {
|
|||||||
return info.CustomDatas.CustomData, nil
|
return info.CustomDatas.CustomData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extensions 获取扩展项
|
// Extensions 获取扩展项,不加载注解或签名
|
||||||
// 返回的文件路径可直接传入ResData读取
|
// 返回的文件路径可直接传入ResData读取
|
||||||
// 返回: []Extension 扩展项列表, error 错误信息
|
// 返回: []Extension 扩展项列表, error 错误信息
|
||||||
func (r *Reader) Extensions() ([]Extension, error) {
|
func (r *Reader) Extensions() ([]Extension, error) {
|
||||||
doc, err := r.Doc()
|
doc, err := r.docStructure()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ func (r *Renderer) GetPageBox(page *PageContent) (Box, error) {
|
|||||||
// 入参: page 页面内容
|
// 入参: page 页面内容
|
||||||
// 返回: []PageLink 页面链接, error 错误信息
|
// 返回: []PageLink 页面链接, error 错误信息
|
||||||
func (r *Renderer) PageLinks(page *PageContent) ([]PageLink, error) {
|
func (r *Renderer) PageLinks(page *PageContent) ([]PageLink, error) {
|
||||||
|
if r.RenderAnnotations && r.Reader.relatedPending {
|
||||||
|
if _, err := r.Reader.DocContext(r.outputContext()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
sources := r.pageActionSources(page)
|
sources := r.pageActionSources(page)
|
||||||
if r.RenderAnnotations {
|
if r.RenderAnnotations {
|
||||||
sources = append(sources, r.annotationActionSources(r.Reader.Annots[page.ID])...)
|
sources = append(sources, r.annotationActionSources(r.Reader.Annots[page.ID])...)
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ type PageAnalysis interface {
|
|||||||
|
|
||||||
// WalkPage 按模板、图层、注解和印章顺序访问页面,不依赖任何绘图库
|
// WalkPage 按模板、图层、注解和印章顺序访问页面,不依赖任何绘图库
|
||||||
// 页面背景由调用方处理,文字裁剪可通过几何后端加载字体,图片由访问器按需加载
|
// 页面背景由调用方处理,文字裁剪可通过几何后端加载字体,图片由访问器按需加载
|
||||||
|
// 已读取文档结构时,先完成延迟的注解与签名加载
|
||||||
// 入参: page 页面内容, visitor 页面访问器
|
// 入参: page 页面内容, visitor 页面访问器
|
||||||
// 返回: error 遍历或访问器错误
|
// 返回: error 遍历或访问器错误
|
||||||
func (r *Renderer) WalkPage(page *PageContent, visitor PageVisitor) error {
|
func (r *Renderer) WalkPage(page *PageContent, visitor PageVisitor) error {
|
||||||
@@ -70,6 +71,11 @@ func (r *Renderer) WalkPage(page *PageContent, visitor PageVisitor) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if r.Reader.relatedPending {
|
||||||
|
if _, err := r.Reader.DocContext(r.outputContext()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
for order := range 3 {
|
for order := range 3 {
|
||||||
if r.Reader.doc != nil {
|
if r.Reader.doc != nil {
|
||||||
for _, ref := range page.Template {
|
for _, ref := range page.Template {
|
||||||
|
|||||||
@@ -393,12 +393,24 @@ func reduceRepeatedNonZeroPath(path *canvas.Path) *canvas.Path {
|
|||||||
// 入参: path 非零填充描边, clip 裁剪路径
|
// 入参: path 非零填充描边, clip 裁剪路径
|
||||||
// 返回: *canvas.Path 绘制轮廓
|
// 返回: *canvas.Path 绘制轮廓
|
||||||
func applyFillClipPath(path, clip *canvas.Path) *canvas.Path {
|
func applyFillClipPath(path, clip *canvas.Path) *canvas.Path {
|
||||||
if path != nil && clip != nil && len(path.Data()) >= 64 {
|
if path == nil || clip == nil {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if rect, ok := rectangularPath(clip); ok {
|
||||||
|
bounds := path.Bounds()
|
||||||
|
if rect.Contains(bounds) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
if !rect.Overlaps(bounds) {
|
||||||
|
return &canvas.Path{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(path.Data()) >= 64 {
|
||||||
if result, ok := intersectLinearCanvasContours(path, clip); ok {
|
if result, ok := intersectLinearCanvasContours(path, clip); ok {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if path != nil && clip != nil && path.HasSubpaths() && len(path.Split()) >= 8 {
|
if path.HasSubpaths() && len(path.Split()) >= 8 {
|
||||||
if result, ok := intersectCanvasContours(path, clip); ok {
|
if result, ok := intersectCanvasContours(path, clip); ok {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,6 +231,11 @@ func (n *pdfNavigation) addPage(renderer *Renderer, index int, page *PageContent
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if renderer.RenderAnnotations && renderer.Reader.relatedPending {
|
||||||
|
if _, err := renderer.Reader.DocContext(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, source := range renderer.pageOpenActionSources(page) {
|
for _, source := range renderer.pageOpenActionSources(page) {
|
||||||
if err := n.addLifecycleActions(ctx, source, index, n.bookmarks, n.pageIndex, pages); err != nil {
|
if err := n.addLifecycleActions(ctx, source, index, n.bookmarks, n.pageIndex, pages); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ package ofdgo
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"image"
|
"image"
|
||||||
"image/png"
|
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -69,6 +69,16 @@ type svgObjectGroup struct {
|
|||||||
// RenderImage 按输出策略编码图片,分离资源时保持尺寸和变换
|
// RenderImage 按输出策略编码图片,分离资源时保持尺寸和变换
|
||||||
// 入参: img 图片对象, m 变换矩阵
|
// 入参: img 图片对象, m 变换矩阵
|
||||||
func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) {
|
func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) {
|
||||||
|
if s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
if s.renderer != nil {
|
||||||
|
ctx = s.renderer.outputContext()
|
||||||
|
}
|
||||||
|
if s.err = ctx.Err(); s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
var target image.Point
|
var target image.Point
|
||||||
if s.renderer != nil && s.renderer.Compression.ImageDPI() > 0 {
|
if s.renderer != nil && s.renderer.Compression.ImageDPI() > 0 {
|
||||||
bounds := img.Bounds()
|
bounds := img.Bounds()
|
||||||
@@ -78,30 +88,10 @@ func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) {
|
|||||||
name, ok := s.imageNames[key]
|
name, ok := s.imageNames[key]
|
||||||
var resource SVGImage
|
var resource SVGImage
|
||||||
if !ok {
|
if !ok {
|
||||||
resource = SVGImage{MIME: "image/png"}
|
resource, s.err = s.imageResource(ctx, img, target)
|
||||||
if encoded, ok := img.(*canvasimage.Image); ok {
|
if s.err != nil {
|
||||||
resource.MIME, resource.Data = encoded.Mimetype, encoded.Bytes
|
|
||||||
} else {
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
if err := png.Encode(&buffer, img); err != nil {
|
|
||||||
s.err = err
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resource.Data = buffer.Bytes()
|
|
||||||
}
|
|
||||||
if s.renderer != nil && s.renderer.Compression.Mode != CompressionUnchanged {
|
|
||||||
data, err := pdfgo.OptimizeImageResource(s.renderer.outputContext(), resource.Data, s.renderer.Compression, target)
|
|
||||||
if err != nil {
|
|
||||||
if s.err = s.renderer.outputContext().Err(); s.err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
resource.Data = data
|
|
||||||
if bytes.HasPrefix(data, []byte{255, 216}) {
|
|
||||||
resource.MIME = "image/jpeg"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if s.imageNames != nil {
|
if s.imageNames != nil {
|
||||||
name = fmt.Sprintf("ofdgo-image-%x", sha256.Sum256(resource.Data))
|
name = fmt.Sprintf("ofdgo-image-%x", sha256.Sum256(resource.Data))
|
||||||
resource.Name = name
|
resource.Name = name
|
||||||
@@ -116,12 +106,19 @@ func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) {
|
|||||||
size := img.Bounds().Size()
|
size := img.Bounds().Size()
|
||||||
m = m.Translate(0, float64(size.Y))
|
m = m.Translate(0, float64(size.Y))
|
||||||
matrix := Matrix{a: m[0][0], b: -m[1][0], c: -m[0][1], d: m[1][1], e: m[0][2], f: height - m[1][2]}
|
matrix := Matrix{a: m[0][0], b: -m[1][0], c: -m[0][1], d: m[1][1], e: m[0][2], f: height - m[1][2]}
|
||||||
fmt.Fprintf(s.writer, `<image transform="matrix(%s)" width="%d" height="%d" xlink:href="`, matrix.String(), size.X, size.Y)
|
writer := imageOutput{context: ctx, writer: s.writer}
|
||||||
|
if _, s.err = fmt.Fprintf(writer, `<image transform="matrix(%s)" width="%d" height="%d" xlink:href="`, matrix.String(), size.X, size.Y); s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if s.imageNames != nil {
|
if s.imageNames != nil {
|
||||||
io.WriteString(s.writer, name)
|
if _, s.err = io.WriteString(writer, name); s.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(s.writer, "data:%s;base64,", resource.MIME)
|
if _, s.err = fmt.Fprintf(writer, "data:%s;base64,", resource.MIME); s.err != nil {
|
||||||
encoder := base64.NewEncoder(base64.StdEncoding, s.writer)
|
return
|
||||||
|
}
|
||||||
|
encoder := base64.NewEncoder(base64.StdEncoding, writer)
|
||||||
if _, err := encoder.Write(resource.Data); err != nil {
|
if _, err := encoder.Write(resource.Data); err != nil {
|
||||||
s.err = err
|
s.err = err
|
||||||
return
|
return
|
||||||
@@ -131,7 +128,66 @@ func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
io.WriteString(s.writer, `"/>`)
|
_, s.err = io.WriteString(writer, `"/>`)
|
||||||
|
if s.err == nil {
|
||||||
|
s.err = ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// imageResource 保留原编码或压缩已解码像素,先降采样再编码并响应取消
|
||||||
|
// 优化失败时回退原图PNG,取消时终止,不改变SVG中的图片尺寸和变换
|
||||||
|
// 入参: ctx 取消上下文, img 原图, target 压缩像素需求
|
||||||
|
// 返回: SVGImage 图片资源, error 编码或取消错误
|
||||||
|
func (s *svgResourceRenderer) imageResource(ctx context.Context, img image.Image, target image.Point) (SVGImage, error) {
|
||||||
|
options := CompressionOptions{}
|
||||||
|
if s.renderer != nil {
|
||||||
|
options = s.renderer.Compression
|
||||||
|
}
|
||||||
|
resource := SVGImage{MIME: "image/png"}
|
||||||
|
if encoded, ok := img.(*canvasimage.Image); ok {
|
||||||
|
resource.MIME, resource.Data = encoded.Mimetype, encoded.Bytes
|
||||||
|
if options.Mode != CompressionUnchanged {
|
||||||
|
data, err := pdfgo.OptimizeImageResource(ctx, resource.Data, options, target)
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return SVGImage{}, ctx.Err()
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
resource.Data = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if options.Mode != CompressionUnchanged {
|
||||||
|
pixels := img
|
||||||
|
var err error
|
||||||
|
if options.Mode == CompressionLossy {
|
||||||
|
pixels, err = pdfgo.ResizeImage(ctx, img, target)
|
||||||
|
if err != nil {
|
||||||
|
return SVGImage{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resource.Data, err = pdfgo.EncodeImageResource(ctx, pixels, options)
|
||||||
|
if err != nil {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return SVGImage{}, ctx.Err()
|
||||||
|
}
|
||||||
|
resource.Data = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if resource.Data == nil {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
if err := pdfgo.EncodePNG(ctx, &buffer, img); err != nil {
|
||||||
|
return SVGImage{}, err
|
||||||
|
}
|
||||||
|
resource.Data = buffer.Bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bytes.HasPrefix(resource.Data, []byte{255, 216}) {
|
||||||
|
resource.MIME = "image/jpeg"
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return SVGImage{}, err
|
||||||
|
}
|
||||||
|
return resource, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderText 绘制文字并记录实际使用的字体资源
|
// RenderText 绘制文字并记录实际使用的字体资源
|
||||||
|
|||||||
+24
-19
@@ -15,10 +15,10 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"maps"
|
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -121,15 +121,12 @@ func (r *Reader) resourceList(ctx context.Context) ([]ResourceInfo, error) {
|
|||||||
return r.resourceInfoCache, nil
|
return r.resourceInfoCache, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resourceCatalog 按标准内容路径收集资源声明及引用,不把任意数字属性作为资源引用
|
// resourceCatalog 单次解析标准内容路径,复用字体扫描的XML树收集资源声明及引用
|
||||||
|
// 结果按文件名及原节点顺序归集,不把任意数字属性作为资源引用
|
||||||
// 入参: ctx 取消上下文
|
// 入参: ctx 取消上下文
|
||||||
// 返回: *resourceCatalog 资源索引, error 结构或读取错误
|
// 返回: *resourceCatalog 资源索引, error 结构或读取错误
|
||||||
func (r *Reader) resourceCatalog(ctx context.Context) (*resourceCatalog, error) {
|
func (r *Reader) resourceCatalog(ctx context.Context) (*resourceCatalog, error) {
|
||||||
scan, err := scanEditorFonts(ctx, r, r.DocumentIndex())
|
catalog := &resourceCatalog{nodes: make(map[string]editorFontText), items: []ResourceInfo{}}
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
catalog := &resourceCatalog{scan: scan, nodes: make(map[string]editorFontText), items: []ResourceInfo{}}
|
|
||||||
pages := make(map[string]int)
|
pages := make(map[string]int)
|
||||||
doc, err := r.DocContext(ctx)
|
doc, err := r.DocContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -147,16 +144,12 @@ func (r *Reader) resourceCatalog(ctx context.Context) (*resourceCatalog, error)
|
|||||||
pages[cleanPackagePath(r.ResPath(page.BaseLoc))] = -1
|
pages[cleanPackagePath(r.ResPath(page.BaseLoc))] = -1
|
||||||
}
|
}
|
||||||
refs := make(map[string][]ResourceReference)
|
refs := make(map[string][]ResourceReference)
|
||||||
for _, name := range slices.Sorted(maps.Keys(scan.files)) {
|
scan, err := scanEditorFontFiles(ctx, r, r.DocumentIndex(), func(file *editorFontXML, root *editorXML) error {
|
||||||
file := scan.files[name]
|
name := file.name
|
||||||
root, err := parseEditorXMLContext(ctx, file.data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if root.matchesOFD("Res") {
|
if root.matchesOFD("Res") {
|
||||||
for _, group := range root.children {
|
for _, group := range root.children {
|
||||||
for _, node := range group.children {
|
for _, node := range group.children {
|
||||||
item := ResourceInfo{ID: editorResourceID(node.attr("ID")), Complete: !scan.unsafe}
|
item := ResourceInfo{ID: editorResourceID(node.attr("ID"))}
|
||||||
field := ""
|
field := ""
|
||||||
if group.matchesOFD("Fonts") && node.matchesOFD("Font") {
|
if group.matchesOFD("Fonts") && node.matchesOFD("Font") {
|
||||||
item.Kind, item.Name, field = "Font", node.attr("FontName"), "FontFile"
|
item.Kind, item.Name, field = "Font", node.attr("FontName"), "FontFile"
|
||||||
@@ -174,7 +167,7 @@ func (r *Reader) resourceCatalog(ctx context.Context) (*resourceCatalog, error)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if item.ID == "" {
|
if item.ID == "" {
|
||||||
return nil, fmt.Errorf("invalid resource ID")
|
return fmt.Errorf("invalid resource ID")
|
||||||
}
|
}
|
||||||
if target := node.child(field); target != nil {
|
if target := node.child(field); target != nil {
|
||||||
location := strings.TrimSpace(editorImportText(file.data, target))
|
location := strings.TrimSpace(editorImportText(file.data, target))
|
||||||
@@ -186,7 +179,7 @@ func (r *Reader) resourceCatalog(ctx context.Context) (*resourceCatalog, error)
|
|||||||
item.Name = path.Base(item.File)
|
item.Name = path.Base(item.File)
|
||||||
}
|
}
|
||||||
if err := catalog.add(item, editorFontText{file: file, node: node}); err != nil {
|
if err := catalog.add(item, editorFontText{file: file, node: node}); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,11 +226,19 @@ func (r *Reader) resourceCatalog(ctx context.Context) (*resourceCatalog, error)
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := visit(root, ""); err != nil {
|
return visit(root, "")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
catalog.scan = scan
|
||||||
attachments, err := r.Attachments()
|
slices.SortStableFunc(catalog.items, func(a, b ResourceInfo) int {
|
||||||
|
return cmp.Compare(catalog.nodes[a.Kind+"/"+a.ID].file.name, catalog.nodes[b.Kind+"/"+b.ID].file.name)
|
||||||
|
})
|
||||||
|
slices.SortStableFunc(catalog.images, func(a, b editorFontText) int {
|
||||||
|
return cmp.Compare(a.file.name, b.file.name)
|
||||||
|
})
|
||||||
|
attachments, err := r.attachmentsContext(ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -249,11 +250,15 @@ func (r *Reader) resourceCatalog(ctx context.Context) (*resourceCatalog, error)
|
|||||||
}
|
}
|
||||||
for i := range catalog.items {
|
for i := range catalog.items {
|
||||||
item := &catalog.items[i]
|
item := &catalog.items[i]
|
||||||
|
item.Complete = !scan.unsafe
|
||||||
key := item.ID
|
key := item.ID
|
||||||
if item.Kind == "Attachment" {
|
if item.Kind == "Attachment" {
|
||||||
key = "Attachment/" + item.ID
|
key = "Attachment/" + item.ID
|
||||||
}
|
}
|
||||||
item.References = refs[key]
|
item.References = refs[key]
|
||||||
|
slices.SortStableFunc(item.References, func(a, b ResourceReference) int {
|
||||||
|
return cmp.Compare(a.File, b.File)
|
||||||
|
})
|
||||||
if item.File != "" {
|
if item.File != "" {
|
||||||
if data, ok := r.files[cleanPackagePath(item.File)]; ok {
|
if data, ok := r.files[cleanPackagePath(item.File)]; ok {
|
||||||
item.Size = uint64(len(data))
|
item.Size = uint64(len(data))
|
||||||
|
|||||||
+31
-2
@@ -15,6 +15,7 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -295,15 +296,38 @@ func textCodeXMLValue(value string) string {
|
|||||||
// 入参: value 文本编码内容
|
// 入参: value 文本编码内容
|
||||||
// 返回: []rune 文本字符
|
// 返回: []rune 文本字符
|
||||||
func textCodeRunes(value string) []rune {
|
func textCodeRunes(value string) []rune {
|
||||||
|
runes, _ := textCodeRunesContext(nil, value)
|
||||||
|
return runes
|
||||||
|
}
|
||||||
|
|
||||||
|
// textCodeRunesContext 解码文本字符及转义序列,长文本分段检查取消
|
||||||
|
// 入参: ctx 取消上下文,可为nil, value 文本编码内容
|
||||||
|
// 返回: []rune 文本字符, error 取消错误
|
||||||
|
func textCodeRunesContext(ctx context.Context, value string) ([]rune, error) {
|
||||||
|
if ctx != nil {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if strings.ContainsAny(value, "\r\n") {
|
if strings.ContainsAny(value, "\r\n") {
|
||||||
value = strings.TrimSpace(value)
|
value = strings.TrimSpace(value)
|
||||||
}
|
}
|
||||||
runes := []rune(value)
|
runes := []rune(value)
|
||||||
|
if ctx != nil {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if !strings.Contains(value, "\\") {
|
if !strings.Contains(value, "\\") {
|
||||||
return runes
|
return runes, nil
|
||||||
}
|
}
|
||||||
written := 0
|
written := 0
|
||||||
for i := 0; i < len(runes); i++ {
|
for i := 0; i < len(runes); i++ {
|
||||||
|
if ctx != nil && written&4095 == 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
char := runes[i]
|
char := runes[i]
|
||||||
if char == '\\' && i+4 < len(runes) {
|
if char == '\\' && i+4 < len(runes) {
|
||||||
if code, err := strconv.ParseUint(string(runes[i+1:i+5]), 16, 16); err == nil {
|
if code, err := strconv.ParseUint(string(runes[i+1:i+5]), 16, 16); err == nil {
|
||||||
@@ -322,5 +346,10 @@ func textCodeRunes(value string) []rune {
|
|||||||
runes[written] = char
|
runes[written] = char
|
||||||
written++
|
written++
|
||||||
}
|
}
|
||||||
return runes[:written]
|
if ctx != nil {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runes[:written], nil
|
||||||
}
|
}
|
||||||
+50
-20
@@ -190,6 +190,10 @@ func (progress editorProgress) withContext(ctx context.Context) editorProgress {
|
|||||||
// 入参: writer 输出流
|
// 入参: writer 输出流
|
||||||
// 返回: int64 写入字节数, error 错误信息
|
// 返回: int64 写入字节数, error 错误信息
|
||||||
func (e *Editor) writePlaintext(writer io.Writer) (int64, error) {
|
func (e *Editor) writePlaintext(writer io.Writer) (int64, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
if e.output != nil {
|
||||||
|
ctx = e.output.ctx
|
||||||
|
}
|
||||||
if err := e.validate(); err != nil {
|
if err := e.validate(); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -208,7 +212,7 @@ func (e *Editor) writePlaintext(writer io.Writer) (int64, error) {
|
|||||||
archive := zip.NewWriter(output)
|
archive := zip.NewWriter(output)
|
||||||
if e.output != nil && e.output.options.Mode != CompressionUnchanged {
|
if e.output != nil && e.output.options.Mode != CompressionUnchanged {
|
||||||
parts := make(map[string][]byte)
|
parts := make(map[string][]byte)
|
||||||
err := e.writeParts(func(name string, data []byte, _ bool) error {
|
err := e.writeParts(ctx, func(name string, data []byte, _ bool) error {
|
||||||
if subset, ok := fonts[name]; ok {
|
if subset, ok := fonts[name]; ok {
|
||||||
data = subset
|
data = subset
|
||||||
}
|
}
|
||||||
@@ -233,7 +237,7 @@ func (e *Editor) writePlaintext(writer io.Writer) (int64, error) {
|
|||||||
err = archive.Close()
|
err = archive.Close()
|
||||||
return output.count, err
|
return output.count, err
|
||||||
}
|
}
|
||||||
err = e.writeParts(func(name string, data []byte, compressed bool) error {
|
err = e.writeParts(ctx, func(name string, data []byte, compressed bool) error {
|
||||||
if subset, ok := fonts[name]; ok {
|
if subset, ok := fonts[name]; ok {
|
||||||
data = subset
|
data = subset
|
||||||
}
|
}
|
||||||
@@ -275,7 +279,7 @@ func (e *Editor) readerContext(ctx context.Context, progress editorProgress) (*R
|
|||||||
return e.sourceReader(ctx, 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(ctx, func(name string, data []byte, _ bool) error {
|
||||||
r.files[name] = data
|
r.files[name] = data
|
||||||
return nil
|
return nil
|
||||||
}, progress); err != nil {
|
}, progress); err != nil {
|
||||||
@@ -296,6 +300,10 @@ func (e *Editor) readerContext(ctx context.Context, progress editorProgress) (*R
|
|||||||
// resourceReader 为临时图元编译准备资源索引,不序列化页面或作为公共快照
|
// resourceReader 为临时图元编译准备资源索引,不序列化页面或作为公共快照
|
||||||
// 返回: *Reader 资源阅读器, error 资源错误
|
// 返回: *Reader 资源阅读器, error 资源错误
|
||||||
func (e *Editor) resourceReader() (*Reader, error) {
|
func (e *Editor) resourceReader() (*Reader, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
if e.output != nil {
|
||||||
|
ctx = e.output.ctx
|
||||||
|
}
|
||||||
if e.source != nil || len(e.origins) != 0 {
|
if e.source != nil || len(e.origins) != 0 {
|
||||||
return nil, fmt.Errorf("resource reader requires created objects")
|
return nil, fmt.Errorf("resource reader requires created objects")
|
||||||
}
|
}
|
||||||
@@ -303,7 +311,7 @@ func (e *Editor) resourceReader() (*Reader, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
r := &Reader{files: make(map[string][]byte)}
|
r := &Reader{files: make(map[string][]byte)}
|
||||||
if err := e.writePartsWithPages(func(name string, data []byte, _ bool) error {
|
if err := e.writePartsWithPages(ctx, func(name string, data []byte, _ bool) error {
|
||||||
r.files[name] = data
|
r.files[name] = data
|
||||||
return nil
|
return nil
|
||||||
}, nil, false); err != nil {
|
}, nil, false); err != nil {
|
||||||
@@ -343,21 +351,18 @@ func (e *Editor) validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// writeParts 生成根节点、文档、资源索引、页面及二进制资源
|
// writeParts 生成根节点、文档、资源索引、页面及二进制资源
|
||||||
// 入参: write 条目写入方法, progress 保存进度回调,预览时为nil
|
// 入参: ctx 取消上下文, write 条目写入方法, progress 保存进度回调,预览时为nil
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) writeParts(write func(string, []byte, bool) error, progress editorProgress) error {
|
func (e *Editor) writeParts(ctx context.Context, write func(string, []byte, bool) error, progress editorProgress) error {
|
||||||
return e.writePartsWithPages(write, progress, true)
|
return e.writePartsWithPages(ctx, write, progress, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writePartsWithPages 统一生成包条目,临时编译可省去页面XML
|
// writePartsWithPages 统一生成包条目,临时编译可省去页面XML
|
||||||
// 入参: write 条目写入方法, progress 保存进度回调, includePages 是否写出页面
|
// 入参: ctx 取消上下文, write 条目写入方法, progress 保存进度回调, includePages 是否写出页面
|
||||||
// 返回: error 写入错误
|
// 返回: error 写入错误
|
||||||
func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, progress editorProgress, includePages bool) error {
|
func (e *Editor) writePartsWithPages(ctx context.Context, write func(string, []byte, bool) error, progress editorProgress, includePages bool) error {
|
||||||
|
progress = progress.withContext(ctx)
|
||||||
if len(e.removedPages) != 0 {
|
if len(e.removedPages) != 0 {
|
||||||
ctx := context.Background()
|
|
||||||
if e.output != nil {
|
|
||||||
ctx = e.output.ctx
|
|
||||||
}
|
|
||||||
output := write
|
output := write
|
||||||
write = func(name string, data []byte, binary bool) error {
|
write = func(name string, data []byte, binary bool) error {
|
||||||
if !binary && strings.EqualFold(path.Ext(name), ".xml") {
|
if !binary && strings.EqualFold(path.Ext(name), ".xml") {
|
||||||
@@ -370,9 +375,12 @@ func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, pro
|
|||||||
return output(name, data, binary)
|
return output(name, data, binary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fonts, images, spaces, definitions := e.usedResources()
|
fonts, images, spaces, definitions, err := e.usedResources(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
writeXML := func(name string, encode func(*ofdXML)) error {
|
writeXML := func(name string, encode func(*ofdXML)) error {
|
||||||
data, err := e.encodeXML(encode)
|
data, err := encodeOFDXMLContext(ctx, encode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -492,31 +500,41 @@ func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
// usedResources 沿图层、对象及资源依赖筛选新增资源和共享文件,不修改资源池
|
// usedResources 沿图层、对象及资源依赖筛选新增资源和共享文件,不修改资源池
|
||||||
// 返回: []editorResource 新增字体, []editorResource 新增图片, []ColorSpace 新增颜色空间, map[string]bool 独立资源文件集合
|
// 入参: ctx 取消上下文
|
||||||
func (e *Editor) usedResources() (fonts, images []editorResource, spaces []ColorSpace, sourceFiles map[string]bool) {
|
// 返回: []editorResource 新增字体, []editorResource 新增图片, []ColorSpace 新增颜色空间, map[string]bool 独立资源文件集合, error 取消错误
|
||||||
|
func (e *Editor) usedResources(ctx context.Context) (fonts, images []editorResource, spaces []ColorSpace, sourceFiles map[string]bool, err error) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
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)
|
||||||
retainAll := false
|
retainAll := false
|
||||||
if len(e.outlines) != 0 {
|
if len(e.outlines) != 0 {
|
||||||
refs := editorResourceRefs{ids: used, files: make(map[string]bool)}
|
refs := editorResourceRefs{ids: used, files: make(map[string]bool)}
|
||||||
if _, safe := refs.scan(bytes.NewReader(e.outlines), e.packageName("Document.xml")); !safe {
|
if _, safe := refs.scanContext(ctx, bytes.NewReader(e.outlines), e.packageName("Document.xml")); !safe {
|
||||||
retainAll = true
|
retainAll = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if e.source != nil {
|
if e.source != nil {
|
||||||
collectActionReferences(e.source.document.Actions, used)
|
collectActionReferences(e.source.document.Actions, used)
|
||||||
for _, name := range e.annotationFiles() {
|
for _, name := range e.annotationFiles() {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
data, changed := e.source.reader.files[name]
|
data, changed := e.source.reader.files[name]
|
||||||
if !changed {
|
if !changed {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
refs := editorResourceRefs{ids: used, files: make(map[string]bool)}
|
refs := editorResourceRefs{ids: used, files: make(map[string]bool)}
|
||||||
if _, safe := refs.scan(bytes.NewReader(data), name); !safe {
|
if _, safe := refs.scanContext(ctx, bytes.NewReader(data), name); !safe {
|
||||||
retainAll = true
|
retainAll = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
if retainAll {
|
if retainAll {
|
||||||
for _, resource := range e.resources {
|
for _, resource := range e.resources {
|
||||||
if resource.font != nil {
|
if resource.font != nil {
|
||||||
@@ -535,6 +553,9 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
|
|||||||
maps.Copy(promoted, used)
|
maps.Copy(promoted, used)
|
||||||
}
|
}
|
||||||
for _, page := range e.pages {
|
for _, page := range e.pages {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
collectActionReferences(page.Actions, used)
|
collectActionReferences(page.Actions, used)
|
||||||
var original map[string]GraphicObject
|
var original map[string]GraphicObject
|
||||||
var originalLayers map[string]string
|
var originalLayers map[string]string
|
||||||
@@ -558,6 +579,9 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, object := range layer.Objects {
|
for _, object := range layer.Objects {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
before, exists := original[editorObjectID(object)]
|
before, exists := original[editorObjectID(object)]
|
||||||
collectObjectReferences(object, used)
|
collectObjectReferences(object, used)
|
||||||
if e.source != nil && (!exists || !reflect.DeepEqual(before, object)) {
|
if e.source != nil && (!exists || !reflect.DeepEqual(before, object)) {
|
||||||
@@ -584,6 +608,9 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for len(pending) != 0 {
|
for len(pending) != 0 {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
id := pending[len(pending)-1]
|
id := pending[len(pending)-1]
|
||||||
pending = pending[:len(pending)-1]
|
pending = pending[:len(pending)-1]
|
||||||
index, ok := definitions[id]
|
index, ok := definitions[id]
|
||||||
@@ -623,7 +650,10 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color
|
|||||||
files[name] = true
|
files[name] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fonts, images, spaces, files
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, nil, nil, nil, err
|
||||||
|
}
|
||||||
|
return fonts, images, spaces, files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// page 按标准顺序写出页面定义、图层、对象及动作
|
// page 按标准顺序写出页面定义、图层、对象及动作
|
||||||
|
|||||||
Reference in new issue
Block a user