From bec13d523f918cd760c41d9279ba03104330d87a Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 14 Oct 2026 08:22:31 +0800 Subject: [PATCH] =?UTF-8?q?perf(=E6=80=A7=E8=83=BD=E4=BC=98=E5=8C=96):=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=B5=84=E6=BA=90=E8=AF=BB=E5=86=99=E4=B8=8E?= =?UTF-8?q?=E5=AD=97=E4=BD=93=E5=9B=BE=E5=83=8F=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 6 +- go.sum | 8 +- internal/base/font/subset.go | 10 +- internal/base/font/util.go | 114 ++++++------ ofdgo_compression_images.go | 5 +- ofdgo_compression_raster.go | 76 ++++---- ofdgo_editor.go | 57 ++++-- ofdgo_editor_annot_content.go | 34 +++- ofdgo_editor_attachment.go | 97 +++++++--- ofdgo_editor_bookmark.go | 23 +-- ofdgo_editor_color.go | 6 + ofdgo_editor_composite.go | 38 +++- ofdgo_editor_composite_insert.go | 24 ++- ofdgo_editor_draw.go | 24 ++- ofdgo_editor_font.go | 128 ++++++++++--- ofdgo_editor_font_binding.go | 310 +++++++++++++++++++++++++++---- ofdgo_editor_font_scan.go | 15 ++ ofdgo_editor_image_binary.go | 57 ++++-- ofdgo_editor_images.go | 48 +++-- ofdgo_editor_import.go | 20 +- ofdgo_editor_open.go | 18 +- ofdgo_editor_package.go | 61 +++--- ofdgo_editor_pages.go | 11 +- ofdgo_editor_reference.go | 12 +- ofdgo_editor_resource.go | 71 ++++--- ofdgo_editor_resource_graph.go | 2 +- ofdgo_editor_resource_scan.go | 2 +- ofdgo_editor_resource_style.go | 129 +++++++------ ofdgo_editor_resources.go | 18 +- ofdgo_editor_structure.go | 11 +- ofdgo_editor_style.go | 20 +- ofdgo_editor_xml.go | 22 ++- ofdgo_extract_image.go | 28 ++- ofdgo_font_info.go | 4 +- ofdgo_media.go | 3 +- ofdgo_pdf_composite_draw.go | 39 ++-- ofdgo_pdf_group.go | 5 +- ofdgo_pdf_object.go | 21 ++- ofdgo_reader.go | 123 +++++++++--- ofdgo_render.go | 5 + ofdgo_render_walk.go | 6 + ofdgo_renderer_clip_canvas.go | 16 +- ofdgo_renderer_pdf_canvas.go | 5 + ofdgo_renderer_svg_canvas.go | 114 +++++++++--- ofdgo_resource.go | 45 +++-- ofdgo_text.go | 33 +++- ofdgo_writer.go | 70 +++++-- 47 files changed, 1455 insertions(+), 539 deletions(-) diff --git a/go.mod b/go.mod index 306c5e0..2845bcf 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,10 @@ require ( github.com/emmansun/gmsm v0.45.0 github.com/go-text/typesetting v0.3.5 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/jbig2 v1.1.5 - github.com/xiaoqidun/pdfgo v0.0.0-20261013134201-f88d759ae672 + github.com/xiaoqidun/jbig2 v1.1.6 + github.com/xiaoqidun/pdfgo v0.0.0-20261014001936-bc05ef67105b golang.org/x/crypto v0.58.0 golang.org/x/image v0.47.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/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // 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/parse/v2 v2.8.16 // indirect github.com/yuin/goldmark v1.8.5 // indirect diff --git a/go.sum b/go.sum index 6a399aa..3ba682f 100644 --- a/go.sum +++ b/go.sum @@ -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/xiaoqidun/j2kgo v1.0.0 h1:n7F3s0H8/4x8ET0d65FGScRgTfiqbYuiiiJF/5HFI1I= 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.5/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA= -github.com/xiaoqidun/pdfgo v0.0.0-20261013134201-f88d759ae672 h1:/hCY94wuMGivF6IX6GeoPkC+sEyblNnZjUxIrb9ZqY8= -github.com/xiaoqidun/pdfgo v0.0.0-20261013134201-f88d759ae672/go.mod h1:UWsVkKkd37a3wEa/tGLUOSwccKGArwmrvEAYlmnEz0U= +github.com/xiaoqidun/jbig2 v1.1.6 h1:IndAJsRi0YxdOLJDUUnVaiACZcWRZ7niR+FkOI+L1YA= +github.com/xiaoqidun/jbig2 v1.1.6/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA= +github.com/xiaoqidun/pdfgo v0.0.0-20261014001936-bc05ef67105b h1:QHzMep5m2n1k3ccMMXW18jPi/XBveTlVGo/n+itNJQk= +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/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= diff --git a/internal/base/font/subset.go b/internal/base/font/subset.go index 2b458fe..5f5ee91 100644 --- a/internal/base/font/subset.go +++ b/internal/base/font/subset.go @@ -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")) { return nil } - sfnt, err := ParseSFNT(data) - if err != nil || !sfnt.IsTrueType && !sfnt.IsCFF || fontEmbeddingFlags(sfnt)&0x0100 != 0 { + fontTables, err := FileTables(data, 0) + if err != nil { return nil } - for tag := range sfnt.Tables { + for tag := range fontTables { 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": default: return nil } } + sfnt, err := ParseSFNTTables(fontTables) + if err != nil || !sfnt.IsTrueType && !sfnt.IsCFF || fontEmbeddingFlags(sfnt)&0x0100 != 0 { + return nil + } var unicodeTables []sfntCmap for _, record := range sfnt.cmaps { if record.format == 14 { diff --git a/internal/base/font/util.go b/internal/base/font/util.go index e77d62c..dc6efa4 100644 --- a/internal/base/font/util.go +++ b/internal/base/font/util.go @@ -22,16 +22,6 @@ import ( "sort" ) -// otfTableRecord OTF表记录结构 -// 字段: tag 标签, checksum 校验和, offset 偏移, length 长度, data 数据 -type otfTableRecord struct { - tag string - checksum uint32 - offset int - length int - data []byte -} - // finite 判断数值是否有限 // 入参: value 数值 // 返回: bool 是否有限 @@ -50,8 +40,6 @@ func ExtractCollectionFont(data []byte, index int) ([]byte, error) { if len(tables["head"]) < 12 { return nil, fmt.Errorf("invalid head font table") } - tables["head"] = bytes.Clone(tables["head"]) - clear(tables["head"][8:12]) return SerializeOTF(tables) } @@ -288,68 +276,70 @@ func buildHmtxTable(widths []uint16) []byte { return buf.Bytes() } -// SerializeOTF 序列化OpenType字体结构 +// SerializeOTF 序列化OpenType字体结构,一次分配输出,不修改输入表数据 +// 复用各表校验和计算整份字体的调整值,不重复扫描表数据 // 入参: tables 表数据映射 // 返回: []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 - for 1<<(entrySelector+1) <= int(numTables) { + for 1<<(entrySelector+1) <= numTables { entrySelector++ } searchRange := 1 << (entrySelector + 4) - rangeShift := int(numTables)*16 - searchRange - buf := new(bytes.Buffer) + rangeShift := numTables*16 - searchRange if tables["CFF "] != nil || tables["CFF2"] != nil { - buf.WriteString("OTTO") + copy(data, "OTTO") } else { - binary.Write(buf, binary.BigEndian, uint32(0x00010000)) + binary.BigEndian.PutUint32(data, 0x00010000) } - binary.Write(buf, binary.BigEndian, numTables) - binary.Write(buf, binary.BigEndian, uint16(searchRange)) - binary.Write(buf, binary.BigEndian, uint16(entrySelector)) - binary.Write(buf, binary.BigEndian, uint16(rangeShift)) - var tags []string - for t := range tables { - tags = append(tags, t) - } - sort.Strings(tags) - headerSize := 12 + 16*int(numTables) - offset := headerSize - var records []otfTableRecord - 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 { + binary.BigEndian.PutUint16(data[4:], uint16(numTables)) + binary.BigEndian.PutUint16(data[6:], uint16(searchRange)) + binary.BigEndian.PutUint16(data[8:], uint16(entrySelector)) + binary.BigEndian.PutUint16(data[10:], uint16(rangeShift)) + offset, adjustment := headerSize, 0 + var checksum uint32 + for i, tag := range tags { + table := tables[tag] + length := (len(table) + 3) &^ 3 + padded := data[offset : offset+length] + copy(padded, table) + if tag == "head" { clear(padded[8:12]) + adjustment = offset + 8 } - cs := calcTableChecksum(padded) - records = append(records, otfTableRecord{tag, cs, offset, len(data), padded}) - offset += len(padded) + record := data[12+i*16 : 28+i*16] + copy(record, tag) + 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 { - buf.WriteString(r.tag) - binary.Write(buf, binary.BigEndian, r.checksum) - binary.Write(buf, binary.BigEndian, uint32(r.offset)) - binary.Write(buf, binary.BigEndian, uint32(r.length)) + if adjustment != 0 { + binary.BigEndian.PutUint32(data[adjustment:], 0xB1B0AFBA-checksum-calcTableChecksum(data[:headerSize])) } - for _, r := range records { - 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 + return data, nil } diff --git a/ofdgo_compression_images.go b/ofdgo_compression_images.go index e027837..885d6b3 100644 --- a/ofdgo_compression_images.go +++ b/ofdgo_compression_images.go @@ -125,6 +125,7 @@ func compressionImageSize(width, height float64, dpi int) image.Point { } // compressionImageSizes 按正文、模板、注解及底纹的实际变换汇总图片尺寸,包括隐藏内容 +// 模板使用独立页面坐标,统一分析一次,避免随引用页数重复展开 // 入参: ctx 取消上下文, images 图片资源, dpi 分辨率上限 // 返回: map[string]image.Point 资源像素需求, error 取消错误 func (r *Reader) compressionImageSizes(ctx context.Context, images []ImageInfo, dpi int) (map[string]image.Point, error) { @@ -152,7 +153,9 @@ func (r *Reader) compressionImageSizes(ctx context.Context, images []ImageInfo, 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() } } diff --git a/ofdgo_compression_raster.go b/ofdgo_compression_raster.go index a54d5d8..d7bd8a9 100644 --- a/ofdgo_compression_raster.go +++ b/ofdgo_compression_raster.go @@ -18,10 +18,12 @@ import ( "bytes" "context" "encoding/binary" + "errors" "hash/crc32" "image" "io" "math" + "math/big" baseimage "github.com/xiaoqidun/ofdgo/internal/base/image" "github.com/xiaoqidun/pdfgo" @@ -46,7 +48,7 @@ func outputImageFormat(data []byte) string { return "" } -// optimizeImageResource 将可完整保留的BMP及TIFF转为PNG,再复用图像压缩和降采样策略 +// optimizeImageResource 优化可完整保留的BMP及TIFF,复用图像压缩和降采样策略 // 原始模式及无法保留的色彩配置不转换,仅采用小于原始编码的结果 // 入参: ctx 取消上下文, data 图片编码, options 压缩选项, size 像素需求 // 返回: []byte 图片编码,可能引用输入, error 解码、编码或取消错误 @@ -124,43 +126,51 @@ func optimizeBMPResource(ctx context.Context, data []byte, options CompressionOp } // optimizeRasterResource 编码已解码的资源图片,同步降采样后的像素密度,仅采用更小结果 -// 绝对密度图片先降采样再编码,密度按最终尺寸换算后量化为PNG整数 +// 先降采样再编码,绝对密度按最终尺寸量化,相对密度保留精确比例 +// 有密度时保留PNG,未知密度按最终像素选择编码格式 // 入参: ctx 取消上下文, data 原编码, pixels 像素, options 压缩选项, size 像素需求, density 像素密度 // 返回: []byte 图片编码, error 编码或取消错误 func optimizeRasterResource(ctx context.Context, data []byte, pixels image.Image, options CompressionOptions, size image.Point, density rasterDensity) ([]byte, error) { hasDensity := density.x != 0 || density.y != 0 - if hasDensity && density.unit == 1 { - before := pixels.Bounds() - if options.Mode == CompressionLossy { - var err error - pixels, err = pdfgo.ResizeImage(ctx, pixels, size) - if err != nil { - return nil, err - } - } - density.x *= float64(pixels.Bounds().Dx()) / float64(before.Dx()) - density.y *= float64(pixels.Bounds().Dy()) / float64(before.Dy()) - size = image.Point{} - } - var encoded bytes.Buffer - limit := &outputCompressionBuffer{buffer: &encoded, limit: outputOptimizationBufferLimit} - if err := pdfgo.EncodePNG(ctx, limit, pixels); err != nil { - if limit.exceeded { - return data, ctx.Err() - } - return nil, err - } - input := encoded.Bytes() - if hasDensity && density.unit == 1 { + before := pixels.Bounds().Size() + if options.Mode == CompressionLossy { var err error - input, err = pdfgo.OptimizeImageSize(ctx, input, options, size) + pixels, err = pdfgo.ResizeImage(ctx, pixels, size) if err != nil { return nil, err } } + if after := pixels.Bounds().Size(); hasDensity && before != after { + if density.unit == 0 { + 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 + err = pdfgo.EncodePNGWithOptions(ctx, &encoded, pixels, options) + candidate = encoded.Bytes() + } else { + candidate, err = pdfgo.EncodeImageResource(ctx, pixels, options) + } + if err != nil { + if errors.Is(err, io.ErrShortBuffer) { + return data, ctx.Err() + } + return nil, err + } if hasDensity { 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() } 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])) var output bytes.Buffer writer := imageMetadataWriter{writer: &output, skip: 33, data: chunk} - if _, err := writer.Write(input); 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 { + if _, err := writer.Write(candidate); err != nil { return nil, err } + candidate = output.Bytes() } if err := ctx.Err(); err != nil { return nil, err diff --git a/ofdgo_editor.go b/ofdgo_editor.go index 20e25ea..44b0c29 100644 --- a/ofdgo_editor.go +++ b/ofdgo_editor.go @@ -471,7 +471,15 @@ func (e *Editor) ResizePages(indexes []int, width, height float64) error { // 入参: data 图片数据 // 返回: 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 获取图片资源的像素尺寸,优先复用注册结果,不解码像素 @@ -485,32 +493,59 @@ func (e *Editor) ImageSize(id string) (image.Point, error) { // 入参: data 图片数据, decoded 已解码像素,为nil时按需解码 // 返回: 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 { return "", err } - return e.addImage(data, config, format, decoded) + return e.addImage(ctx, data, image.Pt(config.Width, config.Height), format, decoded, checksum) } // addImage 注册图片资源,复用调用方已完成解码的像素数据 -// 入参: data 图片编码数据, config 图片配置, format 图片格式, decoded 已解码图像,可为nil +// 入参: ctx 取消上下文, data 图片编码数据, size 像素尺寸, format 图片格式, decoded 已解码图像,可为nil, checksum 原编码摘要 // 返回: string 资源标识, error 格式或资源错误 -func (e *Editor) addImage(data []byte, config image.Config, format string, decoded image.Image) (string, error) { - if (format != "png" && format != "jpeg" && format != "bmp" && format != "tiff" && format != "jbig2") || config.Width <= 0 || config.Height <= 0 { +func (e *Editor) addImage(ctx context.Context, data []byte, size image.Point, format string, decoded image.Image, checksum [32]byte) (string, error) { + 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") } - key := editorResourceKey{checksum: sha256.Sum256(data), index: -1} + key := editorResourceKey{checksum: checksum, index: -1} if id, ok := e.resourceID[key]; ok { return id, nil } - if err := e.prepareSourceIDs(); err != nil { + if err := e.prepareSourceIDsContext(ctx); err != nil { return "", err } - if format == "png" && uint64(config.Width)*uint64(config.Height) <= 64<<20 { - if compact := editorBinaryImage(data, decoded); compact != nil { + if format == "png" && uint64(size.X)*uint64(size.Y) <= 64<<20 { + compact, err := editorBinaryImage(ctx, data, decoded) + if err != nil { + return "", err + } + if compact != nil { data, format = compact, "jbig2" } } + if err := ctx.Err(); err != nil { + return "", err + } id := e.nextID() extension := format if extension == "jpeg" { @@ -524,7 +559,7 @@ func (e *Editor) addImage(data []byte, config image.Config, format string, decod data: bytes.Clone(data), 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 return id, nil } diff --git a/ofdgo_editor_annot_content.go b/ofdgo_editor_annot_content.go index eb75810..8763a5e 100644 --- a/ofdgo_editor_annot_content.go +++ b/ofdgo_editor_annot_content.go @@ -453,7 +453,14 @@ func (e *Editor) annotationFiles() []string { // 入参: base 原包, parts 修改的文件 // 返回: 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 { return err } @@ -462,25 +469,36 @@ func (e *Editor) commitAnnotationParts(base *editorSource, parts map[string][]by files = make(map[string][]byte) } for name, data := range parts { + if err := ctx.Err(); err != nil { + return err + } if file, ok := base.reader.packageFile(name); ok { name = cleanPackagePath(file.Name) } files[name] = data } 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 } - doc, err := reader.docStructure() + doc, err := reader.docStructureContext(ctx) if err != nil { return err } - _, _ = reader.annotationIndex(doc) + _, _ = reader.annotationIndexContext(ctx, doc) + if err := ctx.Err(); err != nil { + return err + } before, after := e.source, *base after.reader, after.document = reader, doc + resources := e.resources if before == nil { - for i := range e.resources { - resource := &e.resources[i] + resources = slices.Clone(resources) + for i := range resources { + if err := ctx.Err(); err != nil { + return err + } + resource := &resources[i] if resource.font != nil { resource.font = cloneEditorData(resource.font) 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 if change := e.recordChange(); change != nil { change.undo = func(e *Editor) { e.source = before } diff --git a/ofdgo_editor_attachment.go b/ofdgo_editor_attachment.go index b680477..89c2c8f 100644 --- a/ofdgo_editor_attachment.go +++ b/ofdgo_editor_attachment.go @@ -29,36 +29,59 @@ import ( // 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 { return nil, nil } - return e.source.reader.Attachments() + return e.source.reader.AttachmentsContext(ctx) } // AddAttachment 添加文档附件,保留原始数据,一次操作计入一条撤销记录 // 入参: name 显示文件名, data 文件数据 // 返回: 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 { return "", err } - maximum, err := e.sourceMaxID(nil) + maximum, err := e.sourceMaxID(editorProgress(nil).withContext(ctx)) if err != nil { return "", err } base := e.source if base == nil { - base, err = e.importBase() + base, err = e.importBaseContext(ctx) if err != nil { return "", err } } - attachments, err := base.reader.Attachments() + attachments, err := base.reader.attachmentsContext(ctx, true) if err != nil { return "", err } used := make(map[string]bool, len(attachments)) for _, attachment := range attachments { + if err := ctx.Err(); err != nil { + return "", err + } used[attachment.ID] = true } 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)) docName := base.reader.documentRoot - docData, err := base.reader.readFile(docName) + docData, err := base.reader.readFileViewContext(ctx, docName) if err != nil { return "", err } - doc, err := parseEditorXML(docData) + doc, err := parseEditorXMLContext(ctx, docData) if err != nil { return "", err } @@ -87,11 +110,11 @@ func (e *Editor) AddAttachment(name string, data []byte) (string, error) { parts := map[string][]byte{file: bytes.Clone(data)} if strings.TrimSpace(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 { return "", err } - root, err := parseEditorXML(existing) + root, err := parseEditorXMLContext(ctx, existing) if err != nil { 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)}}) } - if err := e.commitAnnotationParts(base, parts); err != nil { + if err := e.commitAnnotationPartsContext(ctx, base, parts); err != nil { return "", err } e.maxID, e.source.idsReady = maximum, true @@ -152,10 +175,20 @@ func nextAttachmentID(used map[string]bool, maximum int) (string, int) { // 入参: id 附件标识, name 显示名称 // 返回: 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 { 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 { return data, nil } @@ -168,16 +201,17 @@ func (e *Editor) RenameAttachment(id, name string) error { // 入参: id 附件标识, data 文件数据,调用后可复用 // 返回: 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 新文件数据 // 返回: 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 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 { location := strings.TrimSpace(editorImportText(fragment, child)) if location != "" { @@ -192,7 +226,7 @@ func (e *Editor) replaceAttachment(ctx context.Context, id string, data []byte) } } var err error - maximum, err = e.sourceMaxID(nil) + maximum, err = e.sourceMaxID(editorProgress(nil).withContext(ctx)) if err != nil { return nil, err } @@ -208,7 +242,7 @@ func (e *Editor) replaceAttachment(ctx context.Context, id string, data []byte) } else { fragment = editorPatchXML(fragment, []editorXMLPatch{{node.open, node.open, node.textXML("FileLoc", "/"+file)}}) } - root, err := parseEditorXML(fragment) + root, err := parseEditorXMLContext(ctx, fragment) if err != nil { return nil, err } @@ -233,15 +267,25 @@ func (e *Editor) replaceAttachment(ctx context.Context, id string, data []byte) // 入参: id 附件标识 // 返回: 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 }) } // editAttachment 对单个附件片段执行原子编辑,兼容独立附件文件及内联声明 -// 入参: id 附件标识, change 片段及资源修改函数 +// 入参: ctx 取消上下文, id 附件标识, change 片段及资源修改函数 // 返回: 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 if base == nil { 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 { name = base.reader.ResPath(base.document.Attachments.Path) } - data, err := base.reader.readFile(name) + data, err := base.reader.readFileViewContext(ctx, name) if err != nil { return err } - root, err := parseEditorXML(data) + root, err := parseEditorXMLContext(ctx, data) if err != nil { return err } @@ -265,6 +309,9 @@ func (e *Editor) editAttachment(id string, change func([]byte, *editorXML, map[s var found *editorXML if root != nil { for _, child := range root.children { + if err := ctx.Err(); err != nil { + return err + } if child.matchesOFD("Attachment") && child.attr("ID") == id { if found != nil { 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 { 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 { return err } - node, err := parseEditorXML(fragment) + node, err := parseEditorXMLContext(ctx, fragment) if err != nil { return err } @@ -290,10 +337,10 @@ func (e *Editor) editAttachment(id string, change func([]byte, *editorXML, map[s return err } if bytes.Equal(updated, fragment) && len(parts) == 0 { - return nil + return ctx.Err() } 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 } if len(updated) == 0 { diff --git a/ofdgo_editor_bookmark.go b/ofdgo_editor_bookmark.go index cb63129..93770ee 100644 --- a/ofdgo_editor_bookmark.go +++ b/ofdgo_editor_bookmark.go @@ -179,7 +179,11 @@ func (e *Editor) editBookmark(name string, next *Bookmark, add bool) error { } parts := make(map[string][]byte) 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 { return err } @@ -242,29 +246,12 @@ func (e *Editor) editBookmark(name string, next *Bookmark, add bool) error { // 返回: error 快照重建或页面加载错误。 func (e *Editor) commitBookmarkSnapshot(reader *Reader) error { 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 { return err } e.maxID = max(e.maxID, before.maxID) e.fonts, e.images, e.resourceID = before.fonts, before.images, before.resourceID 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 { states := make(map[string]GraphicObject) for _, layer := range page.Content.Layer { diff --git a/ofdgo_editor_color.go b/ofdgo_editor_color.go index 9769052..e1d149d 100644 --- a/ofdgo_editor_color.go +++ b/ofdgo_editor_color.go @@ -82,6 +82,12 @@ func (e *Editor) AddColorSpace(space ColorSpace, profile []byte) (string, error) 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 { var err error diff --git a/ofdgo_editor_composite.go b/ofdgo_editor_composite.go index 76e5e6e..b522d8f 100644 --- a/ofdgo_editor_composite.go +++ b/ofdgo_editor_composite.go @@ -16,6 +16,7 @@ package ofdgo import ( "bytes" + "context" "encoding/xml" "errors" "fmt" @@ -1085,13 +1086,25 @@ func (e *Editor) compositeResource(id string, data []byte) (editorResource, erro // 入参: data 资源XML // 返回: []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 { return nil, err } refs := make(map[string]bool) - var collect func(*editorXML) - collect = func(node *editorXML) { + var collect func(*editorXML) error + collect = func(node *editorXML) error { + if ctx != nil { + if err := ctx.Err(); err != nil { + return err + } + } if classifyOFDNamespace(node.name.Space) != ofdXMLUnknown { if node.name.Local == "Thumbnail" || node.name.Local == "Substitution" { if id := editorResourceID(editorImportText(data, node)); id != "" { @@ -1105,12 +1118,23 @@ func editorResourceReferences(data []byte) ([]string, error) { } } for _, child := range node.children { - collect(child) + if err := collect(child); err != nil { + return err + } + } + return nil + } + if err := collect(root); err != nil { + return nil, err + } + normalizeResourceReferences(refs) + result := slices.Sorted(maps.Keys(refs)) + if ctx != nil { + if err := ctx.Err(); err != nil { + return nil, err } } - collect(root) - normalizeResourceReferences(refs) - return slices.Sorted(maps.Keys(refs)), nil + return result, nil } // collectCompositeReferences 收集内联复合对象引用的新资源 diff --git a/ofdgo_editor_composite_insert.go b/ofdgo_editor_composite_insert.go index e56547f..f77fa45 100644 --- a/ofdgo_editor_composite_insert.go +++ b/ofdgo_editor_composite_insert.go @@ -16,6 +16,7 @@ package ofdgo import ( "bytes" + "context" "encoding/xml" "fmt" "image/color" @@ -381,8 +382,25 @@ func (e *Editor) neutralDrawParam() (string, error) { // 入参: draw 绘制参数 // 返回: 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 = "", "", "", "" for _, resource := range e.resources { + if err := ctx.Err(); err != nil { + return "", err + } if resource.draw != nil { previous := *resource.draw 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 } 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.start("DrawParams", nil) attrs := ofdAttrs{} @@ -431,7 +449,7 @@ func (e *Editor) addEditorDrawParam(draw DrawParam) (string, error) { if err != nil { return "", err } - refs, err := editorResourceReferences(data) + refs, err := editorResourceReferencesContext(ctx, data) if err != nil { return "", err } diff --git a/ofdgo_editor_draw.go b/ofdgo_editor_draw.go index 87e2a20..163f143 100644 --- a/ofdgo_editor_draw.go +++ b/ofdgo_editor_draw.go @@ -15,6 +15,7 @@ package ofdgo import ( + "context" "encoding/xml" "fmt" ) @@ -23,6 +24,16 @@ import ( // 入参: id 绘制参数标识 // 返回: []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 for _, resource := range e.resources { 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 { var err error 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 { return nil, err } } - root, err := parseEditorXML(data) + root, err := parseEditorXMLContext(ctx, data) if err != nil { return nil, err } 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) } diff --git a/ofdgo_editor_font.go b/ofdgo_editor_font.go index 80d1dc3..2762975 100644 --- a/ofdgo_editor_font.go +++ b/ofdgo_editor_font.go @@ -24,6 +24,7 @@ import ( "slices" "strconv" "strings" + "unicode" ) // editorFontUsage 汇总原文档字体的字符、显式字形及无法确定的引用 @@ -157,7 +158,7 @@ func (e *Editor) addFontResource(parsed *FontResource, checksum [32]byte) (strin return id, nil } -// AddExternalFont 注册不含字体文件的名称引用,由阅读环境匹配 +// AddExternalFont 注册不含字体文件的名称引用,重复注册复用标识,由阅读环境匹配 // 入参: name 字体名称 // 返回: string 字体资源标识, error 错误信息 func (e *Editor) AddExternalFont(name string) (string, error) { @@ -165,8 +166,13 @@ func (e *Editor) AddExternalFont(name string) (string, error) { if 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 { if resource.font != nil && resource.font.FontFile == "" && resource.font.FontName == name { + e.resourceID[key] = resource.font.ID return resource.font.ID, nil } } @@ -175,6 +181,7 @@ func (e *Editor) AddExternalFont(name string) (string, error) { } id := e.nextID() e.resources = append(e.resources, editorResource{font: &Font{ID: id, FontName: name, FamilyName: name}}) + e.resourceID[key] = id e.fontRenderer = nil return id, nil } @@ -200,23 +207,49 @@ func (r *editorResourceRefs) fontUsage(id string) *editorFontUsage { } // text 收集TextCode字符和CGTransform显式字形,异常编号使该字体保持原样 -// 入参: key 元素名称, value 元素文本 -func (u *editorFontUsage) text(key, value string) { +// 入参: ctx 取消上下文,可为nil, key 元素名称, value 元素文本 +// 返回: 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 { 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 } 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) if err != nil { u.unsafe = true - return + return nil } u.glyphs[uint16(id)] = true + count++ } } + if ctx != nil { + return ctx.Err() + } + return nil } // merge 合并指向同一字体文件的全部资源用字 @@ -254,10 +287,15 @@ func editorObjectFontScan(object GraphicObject) bool { return clips != nil || fill != nil && fill.Pattern != nil || stroke != nil && stroke.Pattern != nil } -// subsetFonts 仅解析实际引用的新增字体,按用字裁剪保存结果,保留完整编辑字体 +// subsetFonts 仅解析实际引用的新增字体,合并用字后裁剪保存结果,保留完整编辑字体 // 入参: progress 保存进度回调 // 返回: map[string][]byte 包内字体子集, error 错误信息 func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) { + ctx := context.Background() + if e.output != nil { + ctx = e.output.ctx + } + progress = progress.withContext(ctx) if e.backends.FontResources == nil { return nil, nil } @@ -269,9 +307,12 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) }) { 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) - used := make(map[string]map[uint16]bool) + used := make(map[string]*editorFontUsage) mapped := make(map[string]bool) for i, resource := range fonts { 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 { return nil, err } + if err := ctx.Err(); err != nil { + return nil, err + } e.fonts[resource.font.ID] = resourceFont } if resourceFont.CanSubset { - used[resource.font.ID] = make(map[uint16]bool) + used[resource.font.ID] = newEditorFontUsage() } } 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) } 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 for _, resource := range e.resources { if resource.draw != nil { @@ -311,8 +359,8 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) for _, name := range e.annotationFiles() { if data, changed := e.source.reader.files[name]; changed { nested = true - if _, safe := refs.scan(bytes.NewReader(data), name); !safe { - return nil, nil + if safe, err := scan(data, name); err != nil || !safe { + return nil, err } } } @@ -323,6 +371,9 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) } for _, layer := range page.Content.Layer { for _, object := range layer.Objects { + if err := ctx.Err(); err != nil { + return nil, err + } if editorObjectFontScan(object) { nested = true origin := e.objectOrigin(editorObjectID(object)) @@ -339,8 +390,8 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) if err != nil { return nil, err } - if _, safe := refs.scan(bytes.NewReader(data), "Page.xml"); !safe { - return nil, nil + if safe, err := scan(data, "Page.xml"); err != nil || !safe { + return nil, err } continue } @@ -348,24 +399,31 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) continue } obj := object.TextObject - glyphs := used[obj.Font] - if glyphs == nil { + usage := used[obj.Font] + if usage == nil { continue } for _, code := range obj.TextCode { - for _, char := range textCodeRunes(code.Value) { - glyphs[e.fonts[obj.Font].GlyphIndex(char)] = true + if err := usage.text(ctx, "TextCode", code.Value); err != nil { + return nil, err } } for _, transform := range obj.CGTransform { 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) if err != nil || id >= uint64(e.fonts[obj.Font].NumGlyphs()) { delete(used, obj.Font) 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) { refs.merge(resource.usage.refs) - } else if _, safe := refs.scan(bytes.NewReader(resource.data), resource.name); !safe { - return nil, nil + } else if safe, err := scan(resource.data, resource.name); err != nil || !safe { + return nil, err } } for id, usage := range refs.fonts { - glyphs := used[id] + collected := used[id] if usage.unsafe { delete(used, id) - } else if glyphs != nil { - for char := range usage.chars { - glyphs[e.fonts[id].GlyphIndex(char)] = true - } + } else if collected != nil { + collected.merge(usage, false) if len(usage.glyphs) != 0 { mapped[id] = true for glyph := range usage.glyphs { @@ -400,7 +456,6 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) delete(used, id) break } - glyphs[glyph] = true } } } @@ -415,22 +470,35 @@ func (e *Editor) subsetFonts(progress editorProgress) (map[string][]byte, error) if resource.font == nil { continue } - glyphs := used[resource.font.ID] - if len(glyphs) == 0 { + usage := used[resource.font.ID] + if usage == nil || len(usage.chars) == 0 && len(usage.glyphs) == 0 { resource.subset = nil 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 ids := make([]uint16, 0, len(glyphs)) for id := range glyphs { ids = append(ids, id) } 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) { data, err := e.backends.FontResources.SubsetFont(resource.data, ids, mapped[resource.font.ID]) if err != nil { 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) { data = nil } diff --git a/ofdgo_editor_font_binding.go b/ofdgo_editor_font_binding.go index 2d5c639..0e1ee02 100644 --- a/ofdgo_editor_font_binding.go +++ b/ofdgo_editor_font_binding.go @@ -17,11 +17,13 @@ package ofdgo import ( "archive/zip" "bytes" + "cmp" "context" "crypto/sha256" "encoding/xml" "errors" "fmt" + "io" "maps" "path" "slices" @@ -100,7 +102,7 @@ func (e *Editor) changeFont(ctx context.Context, id string, mode FontMode, file if err := ctx.Err(); err != nil { return err } - reader, err := e.Reader() + reader, err := e.ReaderContext(ctx) if err != nil { return err } @@ -112,8 +114,8 @@ func (e *Editor) changeFont(ctx context.Context, id string, mode FontMode, file if err != nil || !changed { return err } - if mode == FontExternal { - if err := pruneUnreferencedFiles(ctx, reader, scan.fonts[editorResourceID(id)].location); err != nil { + if location := scan.fonts[editorResourceID(id)].location; location != "" { + if err := pruneUnreferencedFiles(ctx, reader, location); err != nil { return err } } @@ -154,7 +156,7 @@ func (e *Editor) ProcessFontsWithOptions(ctx context.Context, options FontProces if err := progress("prepare", 0, 0); err != nil { return nil, err } - reader, err := e.Reader() + reader, err := e.ReaderContext(ctx) if err != nil { return nil, err } @@ -254,7 +256,8 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen if declaration == nil { 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 } if scan.unsafe { @@ -265,7 +268,6 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen } var data []byte var err error - explicit := mode == FontEmbed && len(file.Data) != 0 if mode == FontExternal { data, err = scan.fontCache.read(ctx, scan.reader, declaration.location) if err != nil { @@ -291,6 +293,16 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen 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)} verifiedGlyphs := false 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 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 } - if err := checkFontBinding(object, parsed, mode); err != nil { + if err := checkFontBinding(ctx, object, parsed, mode); err != nil { return nil, err } 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) { 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 node := declaration.node 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}}) for _, field := range []string{"FontName", "FamilyName"} { - root, err := parseEditorXML(fragment) + root, err := parseEditorXMLContext(ctx, fragment) if err != nil { return nil, err } @@ -368,7 +387,7 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen } if explicit { 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 { return nil, err } @@ -386,6 +405,25 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen 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修改,再统一隔离其他文档及版本使用的文件 // 入参: ctx 取消上下文, scan 修改前的文档引用 // 返回: error 取消、引用或结构错误 @@ -560,50 +598,85 @@ func (e *Editor) resolveBindingFont(ctx context.Context, scan *editorFontDocumen } // checkFontBinding 核对字符覆盖及字形编号,外置仅允许等价的一对一字符映射 -// 入参: object 文字对象, font 目标字体, mode 保存方式 -// 返回: error 字符或字形无法保留的原因 -func checkFontBinding(object TextObject, font *FontResource, mode FontMode) error { +// 入参: ctx 取消上下文, object 文字对象, font 目标字体, mode 保存方式 +// 返回: error 字符、字形或取消错误 +func checkFontBinding(ctx context.Context, object TextObject, font *FontResource, mode FontMode) error { + if err := ctx.Err(); err != nil { + return err + } var chars []rune for _, code := range object.TextCode { if code.Index != "" { 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 + } + if len(chars) == 0 { + chars = values + } else { + chars = append(chars, values...) + } } - covered := make(map[int]bool) - for _, transform := range object.CGTransform { - values := strings.Fields(transform.Glyphs) - if transform.CodePosition < 0 || transform.CodeCount < 1 || transform.GlyphCount < 1 || transform.CodePosition > len(chars)-transform.CodeCount || transform.GlyphCount != len(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") } + if transform.CodePosition < end { + return fmt.Errorf("overlapping font glyph mapping") + } + end = transform.CodePosition + transform.CodeCount if mode == FontExternal && transform.CodeCount != transform.GlyphCount { 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) if err != nil || glyph == 0 || glyph >= uint64(font.NumGlyphs()) { 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") } + count++ } - for i := transform.CodePosition; i < transform.CodePosition+transform.CodeCount; i++ { - if covered[i] { - return fmt.Errorf("overlapping font glyph mapping") - } - covered[i] = true + if count != transform.GlyphCount { + return fmt.Errorf("invalid font glyph mapping") } } + covered := 0 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) { 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 nil + return ctx.Err() } // externalFontName 去除PDF字体子集前缀,不猜测或改写其他名称 @@ -632,10 +705,13 @@ func (e *Editor) commitResourceSnapshot(ctx context.Context, reader *Reader) err if err := clean.initRootContext(ctx); err != nil { return err } - next, err := clean.Editor() + next, err := clean.EditorContext(ctx) if err != nil { return err } + if err := next.retainPendingResources(ctx, e); err != nil { + return err + } if err := ctx.Err(); err != nil { return err } @@ -643,12 +719,184 @@ func (e *Editor) commitResourceSnapshot(ctx context.Context, reader *Reader) err e.restoreResourceSnapshot(*next) if change := e.recordChange(); change != nil { after := e.transactionSnapshot() - change.undo = func(e *Editor) { e.restoreResourceSnapshot(before) } - change.redo = func(e *Editor) { e.restoreResourceSnapshot(after) } + before.history, after.history = nil, nil + change.undo = func(e *Editor) { + after.retainDocumentResources(e) + e.restoreResourceSnapshot(before) + } + change.redo = func(e *Editor) { + before.retainDocumentResources(e) + e.restoreResourceSnapshot(after) + } } 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 恢复资源修改前后的包状态,保留运行配置与撤销队列 // 入参: state 包快照 func (e *Editor) restoreResourceSnapshot(state Editor) { diff --git a/ofdgo_editor_font_scan.go b/ofdgo_editor_font_scan.go index 5392b8a..ff85b5f 100644 --- a/ofdgo_editor_font_scan.go +++ b/ofdgo_editor_font_scan.go @@ -60,6 +60,13 @@ type editorFontDeclaration struct { // 入参: ctx 取消上下文, reader 包快照, index 文档索引 // 返回: *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)} entry, err := reader.documentEntryContext(ctx, index) if err != nil { @@ -163,6 +170,14 @@ func scanEditorFonts(ctx context.Context, reader *Reader, index int) (*editorFon if err := visit(root); err != nil { 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 } diff --git a/ofdgo_editor_image_binary.go b/ofdgo_editor_image_binary.go index 70a6c34..eb4d2e8 100644 --- a/ofdgo_editor_image_binary.go +++ b/ofdgo_editor_image_binary.go @@ -17,6 +17,7 @@ package ofdgo import ( "bytes" "compress/zlib" + "context" "encoding/binary" "image" "io" @@ -29,43 +30,53 @@ const pngBinaryProbeLimit = 4 << 20 // pngBinaryProbe 按需读取连续的IDAT块,不复制压缩数据 type pngBinaryProbe struct { + ctx context.Context data []byte payload []byte remaining int } // editorBinaryImage 尝试纯黑白无损编码,不改变透明度,带附加信息或无体积收益时保留原图 -// 入参: data PNG图片数据, img 已解码像素,为nil时读取原图 -// 返回: []byte 更小的JBIG2数据,不适用时为nil -func editorBinaryImage(data []byte, img image.Image) []byte { - if !pngBinarySource(data) { - return nil +// 入参: ctx 取消上下文, data PNG图片数据, img 已解码像素,为nil时读取原图 +// 返回: []byte 更小的JBIG2数据,不适用时为nil, error 取消错误 +func editorBinaryImage(ctx context.Context, data []byte, img image.Image) ([]byte, error) { + if err := ctx.Err(); err != nil { + return nil, err + } + if !pngBinarySource(ctx, data) { + return nil, ctx.Err() } if img == nil { - if pngNonBinaryImage(data) { - return nil + if pngNonBinaryImage(ctx, data) { + return nil, ctx.Err() } 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 { - return nil + return nil, ctx.Err() } } var output bytes.Buffer - if err := jbig2.Encode(&output, img, &jbig2.Options{MaxPageBytes: uint64(len(data))}); err != nil || output.Len() >= len(data) { - return nil + if err := jbig2.EncodeContext(ctx, &output, img, &jbig2.Options{MaxPageBytes: uint64(len(data))}); err != nil || output.Len() >= len(data) { + return nil, ctx.Err() } - return output.Bytes() + if err := ctx.Err(); err != nil { + return nil, err + } + return output.Bytes(), nil } // pngBinarySource 检查PNG是否仅含图像与透明度数据,避免转为JBIG2时丢失附加信息 -// 入参: data PNG编码数据 +// 入参: ctx 取消上下文, data PNG编码数据 // 返回: 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" { return false } for pos := 8; pos < len(data); { + if ctx.Err() != nil { + return false + } if len(data)-pos < 12 { return false } @@ -86,9 +97,9 @@ func pngBinarySource(data []byte) bool { } // pngNonBinaryImage 检查PNG前部像素是否包含灰阶、彩色或透明度,仅用于排除黑白压缩 -// 入参: data PNG图片数据 +// 入参: ctx 取消上下文, data PNG图片数据 // 返回: 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" { return false } @@ -117,8 +128,11 @@ func pngNonBinaryImage(data []byte) bool { if size > 256<<10 { 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" { + if ctx.Err() != nil { + return false + } length := uint64(binary.BigEndian.Uint32(probe.data)) + 12 if length > uint64(len(probe.data)) || length > uint64(probe.remaining) { return false @@ -135,6 +149,9 @@ func pngNonBinaryImage(data []byte) bool { previous := make([]byte, int(size)) rows := min(int64(height), 64, (1<<20)/int64(len(row))) 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]) { return false } @@ -166,7 +183,13 @@ func (p *pngBinaryProbe) Read(output []byte) (int, error) { if len(output) == 0 { return 0, nil } + if err := p.ctx.Err(); err != nil { + return 0, err + } 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" { return 0, io.EOF } diff --git a/ofdgo_editor_images.go b/ofdgo_editor_images.go index a800e4a..ea13016 100644 --- a/ofdgo_editor_images.go +++ b/ofdgo_editor_images.go @@ -42,9 +42,16 @@ type ImagePageOptions struct { 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逐张建页并插入指定位置,一次操作计入一条撤销记录 // 保留像素及透明度,方向通过对象变换实现,不重新采样;图片压缩沿用AddImage规则 -// 同一批次的相同图片复用解码校验结果,不额外保留像素数据 +// 同一批次的相同图片复用解码校验及元数据,不额外保留像素数据 // 失败或取消时不修改文档,读取期间仍需容纳单张图片的解码数据 // 入参: ctx 取消上下文, sources 按页面顺序排列的图片, at 插入位置, options 建页选项 // 返回: error 错误信息 @@ -82,7 +89,7 @@ func (e *Editor) ImportImages(ctx context.Context, sources []ImagePageSource, at } return e.Transaction(func(e *Editor) error { indexes := make([]int, 0, len(sources)) - verified := make(map[[32]byte]bool) + verified := make(map[[32]byte]editorImagePageInfo) for i, source := range sources { if err := ctx.Err(); err != nil { return err @@ -111,33 +118,36 @@ func (e *Editor) ImportImages(ctx context.Context, sources []ImagePageSource, at } // addImagePage 保留原图并按方向、物理尺寸与纸张选项建页 -// 入参: ctx 取消上下文, data 图片数据, options 建页选项, verified 本批次已成功解码的图片摘要 +// 入参: ctx 取消上下文, data 图片数据, options 建页选项, verified 本批次已校验图片的建页信息 // 返回: 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 { return 0, err } - config, format, err := baseimage.DecodeConfigDataContext(ctx, data) - if err != nil { - return 0, err - } - if format != "png" && format != "jpeg" && format != "bmp" && format != "tiff" && format != "jbig2" { - return 0, fmt.Errorf("only PNG, JPEG, BMP, single-page TIFF and JBIG2 images are supported") - } var decoded image.Image checksum := sha256.Sum256(data) - if !verified[checksum] { + info, known := verified[checksum] + if !known { + config, format, err := baseimage.DecodeConfigDataContext(ctx, data) + if err != nil { + return 0, err + } + 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") + } decoded, _, err = baseimage.DecodePixelsContext(ctx, data) if err != nil { return 0, err } - verified[checksum] = true + metadata, err := baseimage.PageMetadataContext(ctx, data, format, options.DPI) + if err != nil { + return 0, err + } + info = editorImagePageInfo{size: image.Pt(config.Width, config.Height), format: format, metadata: metadata} + verified[checksum] = info } - metadata, err := baseimage.PageMetadataContext(ctx, data, format, options.DPI) - if err != nil { - return 0, err - } - w, h := float64(config.Width)*25.4/metadata.XDPI, float64(config.Height)*25.4/metadata.YDPI + 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 { 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 { 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 { return 0, err } diff --git a/ofdgo_editor_import.go b/ofdgo_editor_import.go index e792377..71c6da1 100644 --- a/ofdgo_editor_import.go +++ b/ofdgo_editor_import.go @@ -727,29 +727,39 @@ func (e *Editor) appendImportedOutlines(ctx context.Context, imported []byte) ([ // 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.Info = e.Info seed.pages = []PageContent{{Area: PageArea{PhysicalBox: "0 0 210 297"}}} 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" { files[name] = data } return nil - }, nil); err != nil { + }, editorProgress(nil).withContext(ctx)); err != nil { return nil, err } data := files["Doc_0/Document.xml"] - root, err := parseEditorXML(data) + root, err := parseEditorXMLContext(ctx, data) if err != nil { return nil, err } files["Doc_0/Document.xml"] = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, root.child("Pages"), nil)}) reader := &Reader{files: files} - if err := reader.initRoot(); err != nil { + if err := reader.initRootContext(ctx); err != nil { return nil, err } - editor, err := reader.Editor() + editor, err := reader.EditorContext(ctx) if err != nil { return nil, err } diff --git a/ofdgo_editor_open.go b/ofdgo_editor_open.go index afd74f6..974c301 100644 --- a/ofdgo_editor_open.go +++ b/ofdgo_editor_open.go @@ -275,10 +275,23 @@ func cloneEditorMap[T any](values map[string]T) map[string]T { // prepareSourceIDs 首次新增内容前流式核对原包标识,不依赖可能缺失或过期的MaxUnitID // 返回: 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 { return err } + if err := ctx.Err(); err != nil { + return err + } e.maxID = maximum if e.source != nil { e.source.idsReady = true @@ -303,6 +316,9 @@ func (e *Editor) sourceMaxID(progress editorProgress) (int, error) { } maximum := e.maxID for name := range names { + if err := progress.report("ids", 0, 0); err != nil { + return 0, err + } input, err := reader.openFile(name) if err != nil { return 0, err diff --git a/ofdgo_editor_package.go b/ofdgo_editor_package.go index a4db64a..10e5a23 100644 --- a/ofdgo_editor_package.go +++ b/ofdgo_editor_package.go @@ -32,7 +32,7 @@ import ( // 入参: ctx 取消上下文, progress 保存进度回调,预览时为nil // 返回: 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 { return nil, err } @@ -40,16 +40,20 @@ func (e *Editor) sourceParts(ctx context.Context, progress editorProgress) (map[ } // sourcePartsReferences 生成保存快照,同步收集完全自产页面的实际编码引用 -// 入参: progress 保存进度回调, generated 本次自产页面引用,可为nil +// 入参: ctx 取消上下文, progress 保存进度回调, generated 本次自产页面引用,可为nil // 返回: map[string][]byte 输出条目, error 编码错误 -func (e *Editor) sourcePartsReferences(progress editorProgress, generated map[string]editorGeneratedReferences) (map[string][]byte, error) { - return e.sourcePartsPrepared(progress, generated, false) +func (e *Editor) sourcePartsReferences(ctx context.Context, progress editorProgress, generated map[string]editorGeneratedReferences) (map[string][]byte, error) { + return e.sourcePartsPrepared(ctx, progress, generated, false) } // sourcePartsPrepared 准备保存条目,按需压缩暂存自产页面 -// 入参: progress 保存进度回调, generated 本次页面及引用, stage 是否压缩暂存 +// 入参: ctx 取消上下文, progress 保存进度回调, generated 本次页面及引用, stage 是否压缩暂存 // 返回: 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) var stager editorPageStager source := e.source @@ -97,10 +101,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri } generated[name] = known if (reader.DocumentCount() > 1 || reader.OFD.DocBody[reader.documentIndex].versioned) && !known.stagedLeaf(nil) || bound && known.objects == nil { - input := data.open() - if e.output != nil { - input = imageInput{ReadCloser: input, context: e.output.ctx} - } + input := imageInput{ReadCloser: data.open(), context: ctx} parts[name], err = io.ReadAll(input) input.Close() 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 { 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 { + if err := ctx.Err(); err != nil { + return nil, err + } if resource.definition() != "" && usedFiles[resource.name] || resource.colorProfileUsed(spaces) { parts[resource.name] = resource.data } @@ -136,14 +143,10 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri } resourceFiles := slices.Sorted(maps.Keys(usedFiles)) if len(fonts)+len(images)+len(spaces) != 0 { - data, err := e.encodeXML(func(x *ofdXML) { x.resources(fonts, images, spaces) }) + data, err := encodeOFDXMLContext(ctx, func(x *ofdXML) { x.resources(fonts, images, spaces) }) if err != nil { 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) if err != nil { return nil, err @@ -160,11 +163,11 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri pagesChanged := !slices.Equal(pageRefs, source.document.Pages.Page) if pagesChanged || len(resourceFiles) != 0 || len(parts) != 0 && e.maxID != source.document.CommonData.MaxUnitID { name := reader.documentRoot - data, err := reader.readFile(name) + data, err := reader.readFileViewContext(ctx, name) if err != nil { return nil, err } - root, err := parseEditorXML(data) + root, err := parseEditorXMLContext(ctx, data) if err != nil { return nil, err } @@ -261,7 +264,7 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri data, ok := parts[name] if !ok { var err error - data, err = reader.readFile(name) + data, err = reader.readFileViewContext(ctx, name) if err != nil { return nil, err } @@ -273,14 +276,17 @@ func (e *Editor) sourcePartsPrepared(progress editorProgress, generated map[stri parts[name] = updated } 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 } } - if err := e.pruneStructureReferences(parts, generated); err != nil { + if err := e.pruneStructureReferences(ctx, parts, generated); err != nil { return nil, err } for name, data := range maps.Clone(parts) { + if err := ctx.Err(); err != nil { + return nil, err + } if file, ok := reader.packageFile(name); ok { actual := cleanPackagePath(file.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 } @@ -609,17 +618,17 @@ func (e *Editor) sourcePagePreserved(page PageContent) bool { // 入参: writer 输出流, fonts 新增字体子集, progress 保存进度回调 // 返回: 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) - parts, err := e.sourcePartsPrepared(progress, generated, true) + parts, err := e.sourcePartsPrepared(ctx, progress, generated, true) if err != nil { return 0, err } maps.Copy(parts, fonts) reader := e.source.reader - ctx := context.Background() - if e.output != nil { - ctx = e.output.ctx - } versioned := reader.OFD.DocBody[reader.documentIndex].versioned if !versioned && reader.DocumentCount() > 1 { parts, err = reader.versionOutputChanges(ctx, parts, nil, generated) diff --git a/ofdgo_editor_pages.go b/ofdgo_editor_pages.go index 122d164..f0cad25 100644 --- a/ofdgo_editor_pages.go +++ b/ofdgo_editor_pages.go @@ -35,9 +35,12 @@ type pageReferenceNode struct { // pruneDocumentReferences 在输出快照中清理已删除页面和附件的引用,不修改历史快照或其他文档 // 沿当前文档、页面、模板、资源、注释和签名索引检查,不进入附件、私有扩展和签名原始凭据 // 版本包保留原签名及其页面引用,避免改写其他版本共用的签名数据 -// 入参: parts 输出覆盖条目, generated 本次自产页面及引用,可为nil +// 入参: ctx 取消上下文, parts 输出覆盖条目, generated 本次自产页面及引用,可为nil // 返回: 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) if removed == nil { 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 { return nil } - ctx := context.Background() - if e.output != nil { - ctx = e.output.ctx - } reader := e.source.reader bookmarks := make(map[string]bool) for _, bookmark := range e.source.document.Bookmarks.Bookmark { diff --git a/ofdgo_editor_reference.go b/ofdgo_editor_reference.go index c34d80d..d899d30 100644 --- a/ofdgo_editor_reference.go +++ b/ofdgo_editor_reference.go @@ -16,6 +16,7 @@ package ofdgo import ( "bytes" + "context" "crypto/sha256" "encoding/xml" "maps" @@ -43,6 +44,7 @@ type editorVectorUsage struct { // editorReferenceScan 按XML事件收集资源、字体用字和图元编号,未知内容保持完整扫描 type editorReferenceScan struct { + ctx context.Context refs *editorResourceRefs objects map[string]bool graphicIDs map[string]bool @@ -130,6 +132,9 @@ func (s *editorReferenceScan) vectorUsage(data []byte) *editorVectorUsage { // 入参: token 已解析或已编码的XML事件 // 返回: bool 是否仍可完整判断引用 func (s *editorReferenceScan) accept(token xml.Token) bool { + if s.ctx != nil && s.ctx.Err() != nil { + s.safe = false + } if !s.safe { return false } @@ -253,7 +258,10 @@ func (s *editorReferenceScan) accept(token xml.Token) bool { s.refs.fontUsage(s.text.String()).unsafe = true } 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.fonts = s.fonts[:len(s.fonts)-1] @@ -322,6 +330,7 @@ func (x *ofdXML) referenceToken(token xml.Token) { return } s := x.references + s.ctx = x.ctx switch value := token.(type) { case xml.StartElement: 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) { x.references = nil + x.checkContext() } } diff --git a/ofdgo_editor_resource.go b/ofdgo_editor_resource.go index 2247d7e..e03b6e2 100644 --- a/ofdgo_editor_resource.go +++ b/ofdgo_editor_resource.go @@ -43,6 +43,12 @@ type editorResourcePreview struct { patches []editorXMLPatch } +// resourceImageMaskKey 区分包内共享图片文件及尚未提交的替换像素 +type resourceImageMaskKey struct { + file string + replacement bool +} + // Resources 获取当前编辑快照的资源,覆盖正文、模板、注解、图案和复合图元 // 入参: ctx 取消上下文 // 返回: []ResourceInfo 资源及引用位置, error 快照或结构错误 @@ -106,16 +112,19 @@ func (e *Editor) ImportMedia(ctx context.Context, kind, format string, data []by if err != nil { return "", err } - working, err := reader.Editor() + working, err := reader.EditorContext(ctx) if err != nil { return "", err } + if err := working.prepareSourceIDsContext(ctx); err != nil { + return "", err + } id, err := working.AddMedia(kind, format, data) if err != nil { return "", err } 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 { 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)) 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 { return err } - root, err := parseEditorXML(fragment) + root, err := parseEditorXMLContext(ctx, fragment) if err != nil { 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") } if kind == "Attachment" { - working, err := reader.Editor() + working, err := reader.EditorContext(ctx) if err != nil { return err } revision := working.Revision() if replace == nil { - err = working.DeleteAttachment(item.ID) + err = working.DeleteAttachmentContext(ctx, item.ID) } else { - err = working.replaceAttachment(ctx, item.ID, data) + err = working.ReplaceAttachmentContext(ctx, item.ID, data) } if err != nil { return err @@ -267,6 +276,9 @@ func (e *Editor) changeResource(ctx context.Context, kind, id string, replace fu if err != nil { return err } + if err := pruneUnreferencedFiles(ctx, reader, item.File); err != nil { + return err + } return e.commitResourceSnapshot(ctx, reader) } 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) } -// validateResourceImageMask 校验图片替换涉及的全部蒙版引用,复用尺寸和二值像素校验结果 +// validateResourceImageMask 校验图片替换涉及的全部蒙版引用,按文件复用尺寸和二值像素校验结果 +// 待替换资源单独校验,不影响仍使用同一原文件的其他资源声明 // 入参: ctx 取消上下文, catalog 当前文档资源, id 替换图片标识, replacement 新图片 // 返回: error 资源、尺寸、像素或取消错误 func validateResourceImageMask(ctx context.Context, catalog *resourceCatalog, id string, replacement image.Image) error { - sizes := map[string]image.Point{id: replacement.Bounds().Size()} - validated := make(map[string]image.Point) - read := func(id string) ([]byte, error) { + updated := resourceImageMaskKey{replacement: true} + keys := map[string]resourceImageMaskKey{id: updated} + 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) 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 { if err := ctx.Err(); err != nil { @@ -318,9 +338,13 @@ func validateResourceImageMask(ctx context.Context, catalog *resourceCatalog, id if mask == "" || source != id && mask != id { continue } - size, ok := sizes[source] + sourceKey, err := locate(source) + if err != nil { + return err + } + size, ok := sizes[sourceKey] if !ok { - data, err := read(source) + data, err := catalog.scan.reader.readFileViewContext(ctx, sourceKey.file) if err != nil { return err } @@ -329,17 +353,21 @@ func validateResourceImageMask(ctx context.Context, catalog *resourceCatalog, id return err } 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 { return fmt.Errorf("image mask %s dimensions differ from source %s", mask, source) } continue } img := replacement - if mask != id { - data, err := read(mask) + if !maskKey.replacement { + data, err := catalog.scan.reader.readFileViewContext(ctx, maskKey.file) if err != nil { return err } @@ -351,7 +379,8 @@ func validateResourceImageMask(ctx context.Context, catalog *resourceCatalog, id 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) } - validated[mask] = size + sizes[maskKey] = size + validated[maskKey] = size } 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 { return err } - attachments, err := e.Attachments() + attachments, err := e.AttachmentsContext(ctx) if err != nil { return err } diff --git a/ofdgo_editor_resource_graph.go b/ofdgo_editor_resource_graph.go index 05dfd1e..1602300 100644 --- a/ofdgo_editor_resource_graph.go +++ b/ofdgo_editor_resource_graph.go @@ -49,7 +49,7 @@ func editorResourceDefinitions(ctx context.Context, data []byte, name string, so result.ids, result.fonts = make(map[string]bool), nil result.definitions = make(map[string]*editorResourceRefs) outer := &editorResourceRefs{ids: make(map[string]bool), files: make(map[string]bool), fonts: make(map[string]*editorFontUsage)} - scan := editorReferenceScan{refs: outer, name: name, safe: true} + scan := editorReferenceScan{ctx: ctx, refs: outer, name: name, safe: true} input := bufio.NewReader(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx}) if found, err := editorXMLPrefix(input); err != nil || !found { return nil, err diff --git a/ofdgo_editor_resource_scan.go b/ofdgo_editor_resource_scan.go index 4ea602d..d8afc3d 100644 --- a/ofdgo_editor_resource_scan.go +++ b/ofdgo_editor_resource_scan.go @@ -108,7 +108,7 @@ func scanEditorResources(ctx context.Context, reader *Reader, parts map[string][ 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() } if err := ctx.Err(); err != nil { diff --git a/ofdgo_editor_resource_style.go b/ofdgo_editor_resource_style.go index 29bdc0b..e2b8815 100644 --- a/ofdgo_editor_resource_style.go +++ b/ofdgo_editor_resource_style.go @@ -19,11 +19,18 @@ import ( "context" "encoding/xml" "fmt" + "io" "path" "slices" "strconv" ) +// resourceStyleReference 保存资源依赖及可选的类型要求,不持有XML节点 +type resourceStyleReference struct { + key string + kind string +} + // ColorSpace 获取当前文档及版本的颜色空间定义,不读取或解析ICC文件 // 入参: ctx 取消上下文, id 资源标识 // 返回: *ColorSpace 独立副本,Profile为包内绝对引用, error 查询或解析错误 @@ -98,11 +105,15 @@ func (c *resourceCatalog) styleDefinition(ctx context.Context, item ResourceInfo return err } 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 { 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 读取编辑器中的颜色空间,包括尚未绑定图元的新注册定义 @@ -132,7 +143,7 @@ func (e *Editor) ImportColorSpace(ctx context.Context, space ColorSpace, profile return "", nil, nil, err } 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) if resource.name != "" { parts[resource.name] = bytes.Clone(resource.data) @@ -146,7 +157,7 @@ func (e *Editor) ImportColorSpace(ctx context.Context, space ColorSpace, profile // 返回: 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) { - id, err := working.AddDrawParam(draw) + id, err := working.addDrawParam(ctx, draw) if err != nil { return "", nil, nil, err } @@ -163,10 +174,13 @@ func (e *Editor) importResourceStyle(ctx context.Context, create func(*Editor) ( if err != nil { return "", err } - working, err := reader.Editor() + working, err := reader.EditorContext(ctx) if err != nil { return "", err } + if err := working.prepareSourceIDsContext(ctx); err != nil { + return "", err + } id, data, parts, err := create(working) if err != nil { return "", err @@ -185,6 +199,7 @@ func (e *Editor) importResourceStyle(ctx context.Context, create func(*Editor) ( } // resourceStyleReader 为样式资源操作补齐待注册声明及附属文件,不改变公开快照的按引用导出语义 +// 复用内容扫描收集已有标识,不重复解析结构文件或构建未使用的资源目录 // 入参: ctx 取消上下文 // 返回: *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 { return reader, err } - catalog, err := reader.resourceCatalog(ctx) - if err != nil { - return nil, err - } known := make(map[string]bool) - for _, file := range catalog.scan.files { - root, err := parseEditorXMLContext(ctx, file.data) - if err != nil { - return nil, err - } + _, err = scanEditorFontFiles(ctx, reader, reader.DocumentIndex(), func(_ *editorFontXML, root *editorXML) error { if !root.matchesOFD("Res") { - continue + return nil } for _, group := range root.children { for _, node := range group.children { + if err := ctx.Err(); err != nil { + return err + } if node.matchesOFD(editorResourceElement(group.name.Local)) { known[editorResourceID(node.attr("ID"))] = true } } } + return nil + }) + if err != nil { + return nil, err } var fonts, images []editorResource var spaces []ColorSpace @@ -252,7 +266,7 @@ func (e *Editor) resourceStyleReader(ctx context.Context) (*Reader, error) { } var data []byte 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 { return nil, err } @@ -341,7 +355,7 @@ func (e *Editor) ReplaceColorSpace(ctx context.Context, id string, space ColorSp space.Profile = "/" + name 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 { return err } @@ -350,7 +364,7 @@ func (e *Editor) ReplaceColorSpace(ctx context.Context, id string, space ColorSp return err } 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 { return err } @@ -363,15 +377,18 @@ func (e *Editor) ReplaceColorSpace(ctx context.Context, id string, space ColorSp // 返回: 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 { - working, err := catalog.scan.reader.Editor() + working, err := catalog.scan.reader.EditorContext(ctx) if err != nil { 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 { return err } - fragment, err := working.drawParamXML(added) + fragment, err := working.drawParamXMLContext(ctx, added) if err != nil { return err } @@ -471,7 +488,8 @@ func validateResourceColors(ctx context.Context, catalog *resourceCatalog, space } if resourceColorNode(node) && sameResourceID(id, space.ID) { 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 } 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 检查替换定义的传递依赖,拒绝缺失资源、错误类型及跨底纹复合资源的循环 +// 逐文件收集依赖后统一校验,不同时保留所有结构文件的XML树 // 入参: ctx 取消上下文, catalog 当前目录, id 替换标识, changes 待提交修改 // 返回: error 资源引用或循环错误 func validateResourceStyleGraph(ctx context.Context, catalog *resourceCatalog, id string, changes map[string][]byte) error { types := make(map[string]string) - graph := make(map[string][]string) - roots := make(map[string]*editorXML) - contents := make(map[string][]byte) + graph := make(map[string][]resourceStyleReference) for name, file := range catalog.scan.files { data, ok := changes[name] if !ok { @@ -509,36 +526,30 @@ func validateResourceStyleGraph(ctx context.Context, catalog *resourceCatalog, i if err != nil { return err } - roots[name], contents[name] = root, data - if !root.matchesOFD("Res") { - continue - } - for _, group := range root.children { - for _, node := range group.children { - if node.matchesOFD(editorResourceElement(group.name.Local)) { - key := editorResourceID(node.attr("ID")) - if key == "" || types[key] != "" { - return fmt.Errorf("invalid or duplicate resource ID %q", key) - } - types[key] = node.name.Local - location := "" - if node.matchesOFD("CompositeGraphicUnit") { - location = node.attr("BaseLoc") - } else if node.matchesOFD("DrawParam") { - location = node.attr("Link") - } - if location != "" { - file := "file/" + resolveResourcePath(name, root.attr("BaseLoc"), location) - graph[key] = append(graph[key], file) + types["file/"+name] = "File" + if root.matchesOFD("Res") { + for _, group := range root.children { + for _, node := range group.children { + if node.matchesOFD(editorResourceElement(group.name.Local)) { + key := editorResourceID(node.attr("ID")) + if key == "" || types[key] != "" { + return fmt.Errorf("invalid or duplicate resource ID %q", key) + } + types[key] = node.name.Local + location := "" + if node.matchesOFD("CompositeGraphicUnit") { + location = node.attr("BaseLoc") + } else if node.matchesOFD("DrawParam") { + location = node.attr("Link") + } + if location != "" { + file := "file/" + resolveResourcePath(name, root.attr("BaseLoc"), location) + 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 visit = func(node *editorXML) error { if err := ctx.Err(); err != nil { @@ -552,12 +563,11 @@ func validateResourceStyleGraph(ctx context.Context, catalog *resourceCatalog, i owner = "file/" + name } 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 { if attr.Name.Space == "" && attr.Value != "" && slices.Contains([]string{"Relative", "DrawParam", "ColorSpace", "Font", "ResourceID", "ImageMask", "Substitution", "Thumbnail"}, attr.Name.Local) { key := editorResourceID(attr.Value) - graph[owner] = append(graph[owner], key) kind := "" switch attr.Name.Local { case "Relative", "DrawParam": @@ -565,9 +575,7 @@ func validateResourceStyleGraph(ctx context.Context, catalog *resourceCatalog, i case "ColorSpace", "Font": kind = attr.Name.Local } - if kind != "" && types[key] != kind { - graph[owner] = append(graph[owner], "") - } + graph[owner] = append(graph[owner], resourceStyleReference{key: key, kind: kind}) } } for _, child := range node.children { @@ -582,12 +590,13 @@ func validateResourceStyleGraph(ctx context.Context, catalog *resourceCatalog, i } } state := make(map[string]int) - var visit func(string) error - visit = func(key string) error { + var visit func(resourceStyleReference) error + visit = func(reference resourceStyleReference) error { if err := ctx.Err(); err != nil { 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) } if state[key] == 2 { @@ -602,5 +611,5 @@ func validateResourceStyleGraph(ctx context.Context, catalog *resourceCatalog, i state[key] = 2 return nil } - return visit(id) + return visit(resourceStyleReference{key: id}) } diff --git a/ofdgo_editor_resources.go b/ofdgo_editor_resources.go index b2482a8..a3442fd 100644 --- a/ofdgo_editor_resources.go +++ b/ofdgo_editor_resources.go @@ -324,6 +324,19 @@ func (e *Editor) compactResourceReferences(parts map[string][]byte, reader *Read // 入参: input 文件流, name 包内路径 // 返回: 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) found, err := editorXMLPrefix(buffer) 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") } decoder := xml.NewDecoder(buffer) - scan := editorReferenceScan{refs: r, name: name, safe: true} + scan := editorReferenceScan{ctx: ctx, refs: r, name: name, safe: true} for { + if ctx != nil && ctx.Err() != nil { + return false, false + } token, err := decoder.Token() if err == io.EOF { return scan.resource, scan.seen && len(scan.stack) == 0 diff --git a/ofdgo_editor_structure.go b/ofdgo_editor_structure.go index c814c1c..96ab4d0 100644 --- a/ofdgo_editor_structure.go +++ b/ofdgo_editor_structure.go @@ -27,16 +27,15 @@ import ( // pruneStructureReferences 迁移公文图元编号并清理已删除的引用,保留未知标引和原有悬空引用 // 改动进入输出快照,由既有文档及版本隔离流程维护共享文件 -// 入参: parts 输出覆盖条目, generated 本次编码结果及扫描记录 +// 入参: ctx 取消上下文, parts 输出覆盖条目, generated 本次编码结果及扫描记录 // 返回: 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 { return nil } - ctx := context.Background() - if e.output != nil { - ctx = e.output.ctx - } tags, err := e.source.reader.CustomTagsContext(ctx) if err != nil { return err diff --git a/ofdgo_editor_style.go b/ofdgo_editor_style.go index de936b9..b5e83c6 100644 --- a/ofdgo_editor_style.go +++ b/ofdgo_editor_style.go @@ -15,6 +15,7 @@ package ofdgo import ( + "context" "encoding/xml" "fmt" "image/color" @@ -226,6 +227,20 @@ func (e *Editor) SetObjectDrawParam(page int, ids []string, id string) error { // 入参: draw 绘制参数,ID由编辑器分配 // 返回: 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 != "" { 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)} { + if err := ctx.Err(); err != nil { + return "", err + } if err := e.editorColor(paint); err != nil { return "", err } } - return e.addEditorDrawParam(cloneEditorData(draw)) + return e.addEditorDrawParamContext(ctx, cloneEditorData(draw)) } // StyleObjects 原子更新对象透明度及文字、路径的填充和描边样式 diff --git a/ofdgo_editor_xml.go b/ofdgo_editor_xml.go index 251ff3a..d7927c7 100644 --- a/ofdgo_editor_xml.go +++ b/ofdgo_editor_xml.go @@ -839,12 +839,24 @@ func editorXMLContainer(name string, attrs ofdAttrs, content []byte) ([]byte, er // 入参: data 对象XML, source 原节点 // 返回: []byte 独立XML片段, 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 { return nil, err } namespaces := make(map[string]string) 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 { key := "" if attr.Name.Space == "xmlns" { @@ -886,5 +898,11 @@ func editorXMLStandalone(data []byte, source *editorXML) ([]byte, error) { if data[position-1] == '/' { 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 } diff --git a/ofdgo_extract_image.go b/ofdgo_extract_image.go index 87f8a5d..08cd403 100644 --- a/ofdgo_extract_image.go +++ b/ofdgo_extract_image.go @@ -78,7 +78,7 @@ func (w imageOutput) Write(data []byte) (int, error) { } // Images 列出文档、页面和模板声明的图片资源,不解码图片像素 -// 包含未使用及重复引用的资源声明,同一资源标识只返回一次 +// 包含未使用及重复引用的资源声明,同一资源标识只返回一次,不加载注解或签名 // 入参: ctx 取消上下文 // 返回: []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 { return slices.Clone(r.imageCatalog), nil } - doc, err := r.DocContext(ctx) + doc, err := r.docStructureContext(ctx) if err != nil { return nil, err } @@ -162,7 +162,7 @@ func (r *Reader) Images(ctx context.Context) ([]ImageInfo, error) { return slices.Clone(items), nil } -// Image 查找指定图片,已加载资源直接读取所在资源文件,不扫描其他页面 +// Image 查找指定图片,复用媒体索引或图片清单,尚未建立索引时读取资源声明 // 入参: ctx 取消上下文, id 图片资源标识 // 返回: 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) if name != "" { + if r.resourcesRead[name] { + if media, ok := resourceValue(r.mediaCache, id); ok && media.Type == "Image" && media.MediaFile != "" { + location := r.ResPath(media.MediaFile) + if err := ctx.Err(); err != nil { + return ImageInfo{}, err + } + return ImageInfo{ID: id, Name: path.Base(location), Format: media.Format, Location: location}, nil + } + } data, err := r.readFileViewContext(ctx, name) if err != nil { return ImageInfo{}, err @@ -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) } - items, err := r.Images(ctx) - if err != nil { - return ImageInfo{}, err + items := r.imageCatalog + if items == nil { + var err error + items, err = r.Images(ctx) + if err != nil { + return ImageInfo{}, err + } } + key := imageResourceKey(id) for _, item := range items { if err := ctx.Err(); err != nil { return ImageInfo{}, err } - if imageResourceKey(item.ID) == imageResourceKey(id) { + if imageResourceKey(item.ID) == key { return item, nil } } diff --git a/ofdgo_font_info.go b/ofdgo_font_info.go index 527b79e..d74415c 100644 --- a/ofdgo_font_info.go +++ b/ofdgo_font_info.go @@ -72,10 +72,10 @@ type fontUsageReferences struct { patterns map[*Pattern]bool } -// Fonts 获取OFD声明的字体列表 +// Fonts 获取OFD声明的字体列表,不加载注解或签名 // 返回: []Font 字体列表, error 错误信息 func (r *Reader) Fonts() ([]Font, error) { - doc, err := r.Doc() + doc, err := r.docStructure() if err != nil { return nil, err } diff --git a/ofdgo_media.go b/ofdgo_media.go index 3bf49bd..8d00d39 100644 --- a/ofdgo_media.go +++ b/ofdgo_media.go @@ -20,10 +20,11 @@ import ( ) // Media 获取已加载的音视频资源,页面资源须先读取所属页面 +// 不加载注解或签名 // 入参: id 资源标识 // 返回: 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 } media, ok := resourceValue(r.mediaCache, id) diff --git a/ofdgo_pdf_composite_draw.go b/ofdgo_pdf_composite_draw.go index d0065df..6c0e9af 100644 --- a/ofdgo_pdf_composite_draw.go +++ b/ofdgo_pdf_composite_draw.go @@ -435,6 +435,7 @@ func pdfCompositeInterpolate(target *pdfCompositePixel, source pdfCompositePixel } // drawGroup 按隔离标志保留初始背景,移除背景贡献后应用组透明度 +// 非隔离同空间的非挖空组借用父组背景,子组只写入独立结果缓冲 // 入参: node 透明组, pixels 父组像素, parent 父混合空间 // 返回: 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 { 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) } - initial := c.acquirePixels(len(pixels)) - defer c.releasePixels(initial) + initial := pixels + if !sharedInitial { + initial = c.acquirePixels(len(pixels)) + defer c.releasePixels(initial) + } + result := c.acquirePixels(len(initial)) + defer c.releasePixels(result) if g.Isolated { clear(initial) } else { - sameSpace := space.Equal(parent) + background := initial + if sharedInitial { + background = result + } components := parent.Components() for i := range pixels { if i%pdfCompositeTileSize == 0 { @@ -491,12 +502,12 @@ func (c *pdfCompositor) drawGroup(node pdfCompositeNode, pixels []pdfCompositePi return err } } - initial[i] = pdfCompositePixel{values: values, alpha: pixel.alpha} + background[i] = pdfCompositePixel{values: values, alpha: pixel.alpha} } } - result := c.acquirePixels(len(initial)) - defer c.releasePixels(result) - copy(result, initial) + if !sharedInitial { + copy(result, initial) + } local := *c if c.channels != nil { local.channels = c.acquireChannels(len(c.channels)) @@ -657,6 +668,7 @@ func pdfCompositeGroupRegion(ctx context.Context, pixels, initial, result []pdfC return nil } components, parentComponents := space.Components(), parent.Components() + var target pdfgo.ColorantPixel for y := region.Min.Y; y < region.Max.Y; y++ { if err := ctx.Err(); err != nil { return err @@ -672,7 +684,10 @@ func pdfCompositeGroupRegion(ctx context.Context, pixels, initial, result []pdfC alpha *= mask[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, 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}, @@ -808,11 +823,11 @@ func (c *pdfCompositor) imageColor(mark *pdfgo.ImageMark, point pdfgo.Point, spa } 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 { ix, iy := int(math.Floor(x)), int(math.Floor(y)) fx, fy := x-float64(ix), y-float64(iy) - values, alpha = [4]float64{}, 0 for j := 0; j < 2; j++ { for i := 0; i < 2; i++ { 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)) } } + } else { + values, alpha = sample(int(math.Floor(x+.5)), int(math.Floor(y+.5))) } if mark.Image.ImageMask { if mark.Style.Fill.Tiling != nil || mark.Style.Fill.Shading != nil { diff --git a/ofdgo_pdf_group.go b/ofdgo_pdf_group.go index b016c1e..69a596b 100644 --- a/ofdgo_pdf_group.go +++ b/ofdgo_pdf_group.go @@ -19,7 +19,6 @@ import ( "errors" "fmt" "image" - "image/png" "math" "reflect" @@ -487,13 +486,13 @@ func (p *pdfImporter) renderGroupSceneInto(source *RasterPage, box Box, target * // 返回: error 保存错误 func (p *pdfImporter) appendRasterGroup(img image.Image, box Box, opacity float64) error { var data bytes.Buffer - if err := png.Encode(&data, img); err != nil { + if err := pdfgo.EncodePNG(p.ctx, &data, img); err != nil { return err } if err := p.ctx.Err(); err != nil { return err } - id, err := p.editor.addDecodedImage(data.Bytes(), img) + id, err := p.editor.addDecodedImageContext(p.ctx, data.Bytes(), img) if err != nil { return err } diff --git a/ofdgo_pdf_object.go b/ofdgo_pdf_object.go index e17fed4..59fae44 100644 --- a/ofdgo_pdf_object.go +++ b/ofdgo_pdf_object.go @@ -16,9 +16,11 @@ package ofdgo import ( "bytes" + "errors" "fmt" "image" "image/color" + "io" "math" "reflect" "slices" @@ -85,6 +87,7 @@ func (s *pdfStencilImage) NRGBA64At(x, y int) color.NRGBA64 { } // image 转换图像样本,不将整页或其他对象栅格化 +// 有损编码复用已解码像素,候选容量超限时沿用原编码路径 // 入参: mark PDF图像绘制信息 // 返回: 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 { 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 { var encoded bytes.Buffer if decoded != nil { @@ -219,13 +230,15 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error { data = encoded.Bytes() } if !source.ImageMask && p.compression.Mode == CompressionLossy { - data, err = pdfgo.OptimizeImageResource(p.ctx, data, p.compression, key.size) - if err != nil { - return err + if !optimized { + data, err = pdfgo.OptimizeImageResource(p.ctx, data, p.compression, key.size) + if err != nil { + return err + } } decoded = nil } - id, err = p.editor.addDecodedImage(data, decoded) + id, err = p.editor.addDecodedImageContext(p.ctx, data, decoded) if err != nil { return err } diff --git a/ofdgo_reader.go b/ofdgo_reader.go index d3308be..c33a167 100644 --- a/ofdgo_reader.go +++ b/ofdgo_reader.go @@ -339,10 +339,10 @@ func (r *Reader) PageContentContext(ctx context.Context, page Page) (*PageConten return r.readPageContent(ctx, page, false, false) } -// PageCount 获取当前文档总页数 +// PageCount 获取当前文档总页数,不加载注解或签名 // 返回: int 页数, error 错误信息 func (r *Reader) PageCount() (int, error) { - doc, err := r.Doc() + doc, err := r.docStructure() if err != nil { return 0, err } @@ -443,40 +443,40 @@ func (r *Reader) DocInfo() (*DocInfo, error) { return &r.OFD.DocBody[r.documentIndex].DocInfo, nil } -// Permissions 获取文档权限信息 +// Permissions 获取文档权限信息,不加载注解或签名 // 返回: *Permissions 权限信息, error 错误信息 func (r *Reader) Permissions() (*Permissions, error) { - doc, err := r.Doc() + doc, err := r.docStructure() if err != nil { return nil, err } return &doc.Permissions, nil } -// Outlines 获取文档大纲 +// Outlines 获取文档大纲,不加载注解或签名 // 返回: []OutlineElem 大纲列表, error 错误信息 func (r *Reader) Outlines() ([]OutlineElem, error) { - doc, err := r.Doc() + doc, err := r.docStructure() if err != nil { return nil, err } return doc.Outlines.OutlineElem, nil } -// Actions 获取文档动作 +// Actions 获取文档动作,不加载注解或签名 // 返回: []Action 动作列表, error 错误信息 func (r *Reader) Actions() ([]Action, error) { - doc, err := r.Doc() + doc, err := r.docStructure() if err != nil { return nil, err } return doc.Actions, nil } -// Bookmarks 获取文档书签 +// Bookmarks 获取文档书签,不加载注解或签名 // 返回: []Bookmark 书签列表, error 错误信息 func (r *Reader) Bookmarks() ([]Bookmark, error) { - doc, err := r.Doc() + doc, err := r.docStructure() if err != nil { return nil, err } @@ -486,17 +486,37 @@ func (r *Reader) Bookmarks() ([]Bookmark, error) { // 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 { 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 - partPath, err := r.readDocumentPart(doc.Attachments.Path, &attachments) + partPath, err := r.readDocumentPartContext(ctx, doc.Attachments.Path, &attachments) if err != nil { return nil, err } for i := range attachments.Attachment { + if err := ctx.Err(); err != nil { + return nil, err + } attachment := &attachments.Attachment[i] if fileLoc := resolveResourcePath(partPath, "", attachment.FileLoc); fileLoc != "" { attachment.FileLoc = "/" + fileLoc @@ -505,14 +525,29 @@ func (r *Reader) Attachments() ([]Attachment, error) { if attachments.Attachment == nil { attachments.Attachment = []Attachment{} } - doc.Attachments.Attachment = attachments.Attachment + items = attachments.Attachment } - attachments := slices.Clone(doc.Attachments.Attachment) - for i := range attachments { - if attachments[i].Size != nil { - size := *attachments[i].Size - attachments[i].Size = &size + attachments := items + 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 { + size := *attachments[i].Size + attachments[i].Size = &size + } + } + } + if err := ctx.Err(); err != nil { + return nil, err + } + if pending { + doc.Attachments.Attachment = items } return attachments, nil } @@ -521,14 +556,36 @@ func (r *Reader) Attachments() ([]Attachment, error) { // 入参: id 附件标识 // 返回: 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 { return nil, err } for _, attachment := range attachments { - if attachment.ID == id { - return r.openFile(r.ResPath(attachment.FileLoc)) + if err := ctx.Err(); err != nil { + 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) } @@ -537,12 +594,26 @@ func (r *Reader) OpenAttachment(id string) (io.ReadCloser, error) { // 入参: id 附件标识 // 返回: []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 { return nil, err } 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 获取自定义标引 @@ -599,11 +670,11 @@ func (r *Reader) CustomDatas() ([]CustomData, error) { return info.CustomDatas.CustomData, nil } -// Extensions 获取扩展项 +// Extensions 获取扩展项,不加载注解或签名 // 返回的文件路径可直接传入ResData读取 // 返回: []Extension 扩展项列表, error 错误信息 func (r *Reader) Extensions() ([]Extension, error) { - doc, err := r.Doc() + doc, err := r.docStructure() if err != nil { return nil, err } diff --git a/ofdgo_render.go b/ofdgo_render.go index b7ad68a..9b9ae41 100644 --- a/ofdgo_render.go +++ b/ofdgo_render.go @@ -48,6 +48,11 @@ func (r *Renderer) GetPageBox(page *PageContent) (Box, error) { // 入参: page 页面内容 // 返回: []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) if r.RenderAnnotations { sources = append(sources, r.annotationActionSources(r.Reader.Annots[page.ID])...) diff --git a/ofdgo_render_walk.go b/ofdgo_render_walk.go index 0023dac..c8a1090 100644 --- a/ofdgo_render_walk.go +++ b/ofdgo_render_walk.go @@ -62,6 +62,7 @@ type PageAnalysis interface { // WalkPage 按模板、图层、注解和印章顺序访问页面,不依赖任何绘图库 // 页面背景由调用方处理,文字裁剪可通过几何后端加载字体,图片由访问器按需加载 +// 已读取文档结构时,先完成延迟的注解与签名加载 // 入参: page 页面内容, visitor 页面访问器 // 返回: 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 } } + if r.Reader.relatedPending { + if _, err := r.Reader.DocContext(r.outputContext()); err != nil { + return err + } + } for order := range 3 { if r.Reader.doc != nil { for _, ref := range page.Template { diff --git a/ofdgo_renderer_clip_canvas.go b/ofdgo_renderer_clip_canvas.go index aa95977..1b5a280 100644 --- a/ofdgo_renderer_clip_canvas.go +++ b/ofdgo_renderer_clip_canvas.go @@ -393,12 +393,24 @@ func reduceRepeatedNonZeroPath(path *canvas.Path) *canvas.Path { // 入参: path 非零填充描边, clip 裁剪路径 // 返回: *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 { 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 { return result } diff --git a/ofdgo_renderer_pdf_canvas.go b/ofdgo_renderer_pdf_canvas.go index 28fe386..ed01e37 100644 --- a/ofdgo_renderer_pdf_canvas.go +++ b/ofdgo_renderer_pdf_canvas.go @@ -231,6 +231,11 @@ func (n *pdfNavigation) addPage(renderer *Renderer, index int, page *PageContent if err := ctx.Err(); err != nil { return err } + if renderer.RenderAnnotations && renderer.Reader.relatedPending { + if _, err := renderer.Reader.DocContext(ctx); err != nil { + return err + } + } for _, source := range renderer.pageOpenActionSources(page) { if err := n.addLifecycleActions(ctx, source, index, n.bookmarks, n.pageIndex, pages); err != nil { return err diff --git a/ofdgo_renderer_svg_canvas.go b/ofdgo_renderer_svg_canvas.go index fe686a0..60bf304 100644 --- a/ofdgo_renderer_svg_canvas.go +++ b/ofdgo_renderer_svg_canvas.go @@ -17,12 +17,12 @@ package ofdgo import ( "bufio" "bytes" + "context" "crypto/sha256" "encoding/base64" "fmt" "html" "image" - "image/png" "io" "math" "strings" @@ -69,6 +69,16 @@ type svgObjectGroup struct { // RenderImage 按输出策略编码图片,分离资源时保持尺寸和变换 // 入参: img 图片对象, m 变换矩阵 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 if s.renderer != nil && s.renderer.Compression.ImageDPI() > 0 { bounds := img.Bounds() @@ -78,29 +88,9 @@ func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) { name, ok := s.imageNames[key] var resource SVGImage if !ok { - resource = SVGImage{MIME: "image/png"} - if encoded, ok := img.(*canvasimage.Image); ok { - 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 - } - 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" - } - } + resource, s.err = s.imageResource(ctx, img, target) + if s.err != nil { + return } if s.imageNames != nil { name = fmt.Sprintf("ofdgo-image-%x", sha256.Sum256(resource.Data)) @@ -116,12 +106,19 @@ func (s *svgResourceRenderer) RenderImage(img image.Image, m canvas.Matrix) { size := img.Bounds().Size() 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]} - fmt.Fprintf(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 绘制文字并记录实际使用的字体资源 diff --git a/ofdgo_resource.go b/ofdgo_resource.go index cd56fee..c3d08ee 100644 --- a/ofdgo_resource.go +++ b/ofdgo_resource.go @@ -15,10 +15,10 @@ package ofdgo import ( + "cmp" "context" "fmt" "io" - "maps" "path" "slices" "strings" @@ -121,15 +121,12 @@ func (r *Reader) resourceList(ctx context.Context) ([]ResourceInfo, error) { return r.resourceInfoCache, nil } -// resourceCatalog 按标准内容路径收集资源声明及引用,不把任意数字属性作为资源引用 +// resourceCatalog 单次解析标准内容路径,复用字体扫描的XML树收集资源声明及引用 +// 结果按文件名及原节点顺序归集,不把任意数字属性作为资源引用 // 入参: ctx 取消上下文 // 返回: *resourceCatalog 资源索引, error 结构或读取错误 func (r *Reader) resourceCatalog(ctx context.Context) (*resourceCatalog, error) { - scan, err := scanEditorFonts(ctx, r, r.DocumentIndex()) - if err != nil { - return nil, err - } - catalog := &resourceCatalog{scan: scan, nodes: make(map[string]editorFontText), items: []ResourceInfo{}} + catalog := &resourceCatalog{nodes: make(map[string]editorFontText), items: []ResourceInfo{}} pages := make(map[string]int) doc, err := r.DocContext(ctx) if err != nil { @@ -147,16 +144,12 @@ func (r *Reader) resourceCatalog(ctx context.Context) (*resourceCatalog, error) pages[cleanPackagePath(r.ResPath(page.BaseLoc))] = -1 } refs := make(map[string][]ResourceReference) - for _, name := range slices.Sorted(maps.Keys(scan.files)) { - file := scan.files[name] - root, err := parseEditorXMLContext(ctx, file.data) - if err != nil { - return nil, err - } + scan, err := scanEditorFontFiles(ctx, r, r.DocumentIndex(), func(file *editorFontXML, root *editorXML) error { + name := file.name if root.matchesOFD("Res") { for _, group := range root.children { for _, node := range group.children { - item := ResourceInfo{ID: editorResourceID(node.attr("ID")), Complete: !scan.unsafe} + item := ResourceInfo{ID: editorResourceID(node.attr("ID"))} field := "" if group.matchesOFD("Fonts") && node.matchesOFD("Font") { item.Kind, item.Name, field = "Font", node.attr("FontName"), "FontFile" @@ -174,7 +167,7 @@ func (r *Reader) resourceCatalog(ctx context.Context) (*resourceCatalog, error) continue } if item.ID == "" { - return nil, fmt.Errorf("invalid resource ID") + return fmt.Errorf("invalid resource ID") } if target := node.child(field); target != nil { 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) } 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 } - if err := visit(root, ""); err != nil { - return nil, err - } + return visit(root, "") + }) + if err != nil { + return nil, err } - attachments, err := r.Attachments() + catalog.scan = scan + 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 { return nil, err } @@ -249,11 +250,15 @@ func (r *Reader) resourceCatalog(ctx context.Context) (*resourceCatalog, error) } for i := range catalog.items { item := &catalog.items[i] + item.Complete = !scan.unsafe key := item.ID if item.Kind == "Attachment" { key = "Attachment/" + item.ID } item.References = refs[key] + slices.SortStableFunc(item.References, func(a, b ResourceReference) int { + return cmp.Compare(a.File, b.File) + }) if item.File != "" { if data, ok := r.files[cleanPackagePath(item.File)]; ok { item.Size = uint64(len(data)) diff --git a/ofdgo_text.go b/ofdgo_text.go index aee14cc..307396c 100644 --- a/ofdgo_text.go +++ b/ofdgo_text.go @@ -15,6 +15,7 @@ package ofdgo import ( + "context" "math" "strconv" "strings" @@ -295,15 +296,38 @@ func textCodeXMLValue(value string) string { // 入参: value 文本编码内容 // 返回: []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") { value = strings.TrimSpace(value) } runes := []rune(value) + if ctx != nil { + if err := ctx.Err(); err != nil { + return nil, err + } + } if !strings.Contains(value, "\\") { - return runes + return runes, nil } written := 0 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] if char == '\\' && i+4 < len(runes) { 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 written++ } - return runes[:written] + if ctx != nil { + if err := ctx.Err(); err != nil { + return nil, err + } + } + return runes[:written], nil } diff --git a/ofdgo_writer.go b/ofdgo_writer.go index f07f286..a7fceee 100644 --- a/ofdgo_writer.go +++ b/ofdgo_writer.go @@ -190,6 +190,10 @@ func (progress editorProgress) withContext(ctx context.Context) editorProgress { // 入参: 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 { return 0, err } @@ -208,7 +212,7 @@ func (e *Editor) writePlaintext(writer io.Writer) (int64, error) { archive := zip.NewWriter(output) if e.output != nil && e.output.options.Mode != CompressionUnchanged { 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 { data = subset } @@ -233,7 +237,7 @@ func (e *Editor) writePlaintext(writer io.Writer) (int64, error) { err = archive.Close() 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 { data = subset } @@ -275,7 +279,7 @@ func (e *Editor) readerContext(ctx context.Context, progress editorProgress) (*R return e.sourceReader(ctx, progress) } r := &Reader{files: make(map[string][]byte), encryption: e.encryption} - if err := e.writeParts(func(name string, data []byte, _ bool) error { + if err := e.writeParts(ctx, func(name string, data []byte, _ bool) error { r.files[name] = data return nil }, progress); err != nil { @@ -296,6 +300,10 @@ func (e *Editor) readerContext(ctx context.Context, progress editorProgress) (*R // 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 { return nil, fmt.Errorf("resource reader requires created objects") } @@ -303,7 +311,7 @@ func (e *Editor) resourceReader() (*Reader, error) { return nil, err } 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 return nil }, nil, false); err != nil { @@ -343,21 +351,18 @@ func (e *Editor) validate() error { } // writeParts 生成根节点、文档、资源索引、页面及二进制资源 -// 入参: write 条目写入方法, progress 保存进度回调,预览时为nil +// 入参: ctx 取消上下文, write 条目写入方法, progress 保存进度回调,预览时为nil // 返回: error 错误信息 -func (e *Editor) writeParts(write func(string, []byte, bool) error, progress editorProgress) error { - return e.writePartsWithPages(write, progress, true) +func (e *Editor) writeParts(ctx context.Context, write func(string, []byte, bool) error, progress editorProgress) error { + return e.writePartsWithPages(ctx, write, progress, true) } // writePartsWithPages 统一生成包条目,临时编译可省去页面XML -// 入参: write 条目写入方法, progress 保存进度回调, includePages 是否写出页面 +// 入参: ctx 取消上下文, write 条目写入方法, progress 保存进度回调, includePages 是否写出页面 // 返回: 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 { - ctx := context.Background() - if e.output != nil { - ctx = e.output.ctx - } output := write write = func(name string, data []byte, binary bool) error { 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) } } - 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 { - data, err := e.encodeXML(encode) + data, err := encodeOFDXMLContext(ctx, encode) if err != nil { return err } @@ -492,31 +500,41 @@ func (e *Editor) writePartsWithPages(write func(string, []byte, bool) error, pro } // usedResources 沿图层、对象及资源依赖筛选新增资源和共享文件,不修改资源池 -// 返回: []editorResource 新增字体, []editorResource 新增图片, []ColorSpace 新增颜色空间, map[string]bool 独立资源文件集合 -func (e *Editor) usedResources() (fonts, images []editorResource, spaces []ColorSpace, sourceFiles map[string]bool) { +// 入参: ctx 取消上下文 +// 返回: []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) promoted := make(map[string]bool) files := make(map[string]bool) retainAll := false if len(e.outlines) != 0 { 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 } } if e.source != nil { collectActionReferences(e.source.document.Actions, used) for _, name := range e.annotationFiles() { + if err := ctx.Err(); err != nil { + return nil, nil, nil, nil, err + } data, changed := e.source.reader.files[name] if !changed { continue } 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 } } } + if err := ctx.Err(); err != nil { + return nil, nil, nil, nil, err + } if retainAll { for _, resource := range e.resources { if resource.font != nil { @@ -535,6 +553,9 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color maps.Copy(promoted, used) } for _, page := range e.pages { + if err := ctx.Err(); err != nil { + return nil, nil, nil, nil, err + } collectActionReferences(page.Actions, used) var original map[string]GraphicObject var originalLayers map[string]string @@ -558,6 +579,9 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color } } for _, object := range layer.Objects { + if err := ctx.Err(); err != nil { + return nil, nil, nil, nil, err + } before, exists := original[editorObjectID(object)] collectObjectReferences(object, used) 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 { + if err := ctx.Err(); err != nil { + return nil, nil, nil, nil, err + } id := pending[len(pending)-1] pending = pending[:len(pending)-1] index, ok := definitions[id] @@ -623,7 +650,10 @@ func (e *Editor) usedResources() (fonts, images []editorResource, spaces []Color 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 按标准顺序写出页面定义、图层、对象及动作