diff --git a/go.mod b/go.mod index 593353b..1a5c969 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/xiaoqidun/j2kgo v1.0.0 github.com/xiaoqidun/jbig2 v1.1.5 - github.com/xiaoqidun/pdfgo v0.0.0-20261012061033-90df32d37c4d + github.com/xiaoqidun/pdfgo v0.0.0-20261012150156-647733ab0dae golang.org/x/crypto v0.58.0 golang.org/x/image v0.47.0 golang.org/x/text v0.43.0 diff --git a/go.sum b/go.sum index 5e03c6d..5557116 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ 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-20261012061033-90df32d37c4d h1:asgIx1MO7h1zcN4weRAxrBv6ZHsz/85JRImvIW9qbeM= -github.com/xiaoqidun/pdfgo v0.0.0-20261012061033-90df32d37c4d/go.mod h1:UWsVkKkd37a3wEa/tGLUOSwccKGArwmrvEAYlmnEz0U= +github.com/xiaoqidun/pdfgo v0.0.0-20261012150156-647733ab0dae h1:8iQdSdXA4sVofF+wIcmFJB63P5Uy4uQeltj2PpP/hzE= +github.com/xiaoqidun/pdfgo v0.0.0-20261012150156-647733ab0dae/go.mod h1:UWsVkKkd37a3wEa/tGLUOSwccKGArwmrvEAYlmnEz0U= 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/ofdgo_compression_parallel.go b/ofdgo_compression_parallel.go index e898d55..91024d0 100644 --- a/ofdgo_compression_parallel.go +++ b/ofdgo_compression_parallel.go @@ -15,8 +15,11 @@ package ofdgo import ( + "bytes" + "context" "crypto/sha256" "image" + "io" "runtime" "sync" ) @@ -24,6 +27,9 @@ import ( // outputImageBatchLimit 限制并行批次持有的原始编码,超限图片单独处理 const outputImageBatchLimit = 16 << 20 +// outputImageWorkingSetLimit 限制并行批次的像素工作量估算,超限图片单独处理 +const outputImageWorkingSetLimit = 64 << 20 + // outputImageJob 保存独立图片任务,不访问编辑文档或调用进度回调 type outputImageJob struct { key outputImageKey @@ -63,13 +69,13 @@ func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, e return nil } batch := make([]outputImageJob, 0, workers) - size := 0 + size, workingSet := 0, 0 run := func() error { if err := e.runOutputImages(batch, results); err != nil { return err } clear(batch) - batch, size = batch[:0], 0 + batch, size, workingSet = batch[:0], 0, 0 return nil } for _, key := range keys { @@ -101,14 +107,16 @@ func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, e if duplicate { continue } - if len(batch) != 0 && len(data) > outputImageBatchLimit-size { + cost := outputImageWorkingSet(e.output.ctx, data) + if len(batch) != 0 && (len(data) > outputImageBatchLimit-size || cost > outputImageWorkingSetLimit-workingSet) { if err := run(); err != nil { return err } } batch = append(batch, outputImageJob{key: cacheKey, data: data}) size += len(data) - if len(batch) == workers || size >= outputImageBatchLimit { + workingSet += cost + if len(batch) == workers || size >= outputImageBatchLimit || workingSet >= outputImageWorkingSetLimit { if err := run(); err != nil { return err } @@ -117,6 +125,26 @@ func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, e return run() } +// outputImageWorkingSet 按原图尺寸估算解码和重采样缓冲,不以降采样目标代替原图 +// 按两份16位RGBA像素缓冲分批,不代表实际内存上限;格式或尺寸不明时单独处理 +// 入参: ctx 取消上下文, data 原始图像编码 +// 返回: int 编码及像素缓冲的估算字节数,上限为单批预算 +func outputImageWorkingSet(ctx context.Context, data []byte) int { + if len(data) >= outputImageWorkingSetLimit { + return outputImageWorkingSetLimit + } + input := imageInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx} + config, _, err := decodeImageConfig(input) + if err != nil || config.Width <= 0 || config.Height <= 0 { + return outputImageWorkingSetLimit + } + available := outputImageWorkingSetLimit - len(data) + if config.Width > available/16/config.Height { + return outputImageWorkingSetLimit + } + return len(data) + config.Width*config.Height*16 +} + // runOutputImages 等待整批结束后记录更小候选,失败图片保留原始编码 // 入参: jobs 独占图片任务, results 本次写出的候选缓存 // 返回: error 取消错误 diff --git a/ofdgo_editor.go b/ofdgo_editor.go index ab7b55f..3950476 100644 --- a/ofdgo_editor.go +++ b/ofdgo_editor.go @@ -678,6 +678,15 @@ func (e *Editor) ReplaceImage(page int, id string, data []byte, fit string) erro if object.Type != "ImageObject" { return fmt.Errorf("object %q is not an image", id) } + if object.ImageObject.ImageMask != "" { + config, _, err := decodeImageConfigData(data) + if err != nil { + return err + } + if err := e.validateImageMask(object.ImageObject.ImageMask, image.Pt(config.Width, config.Height)); err != nil { + return err + } + } object.ImageObject.ResourceID, err = e.AddImage(data) if err != nil { return err @@ -863,7 +872,8 @@ func (v *editorValidation) prepareObject(id string, object GraphicObject) (Graph fill, stroke = obj.FillColor, (*FillColor)(obj.StrokeColor) case "ImageObject": obj := &object.ImageObject - if _, err := e.editorImage(obj.ResourceID); err != nil { + size, err := e.editorImage(obj.ResourceID) + if err != nil { return GraphicObject{}, err } if obj.Substitution != "" { @@ -872,7 +882,7 @@ func (v *editorValidation) prepareObject(id string, object GraphicObject) (Graph } } if obj.ImageMask != "" { - if _, err := e.editorImage(obj.ImageMask); err != nil { + if err := e.validateImageMask(obj.ImageMask, size); err != nil { return GraphicObject{}, err } } diff --git a/ofdgo_editor_composite_ops.go b/ofdgo_editor_composite_ops.go index 68cf8ea..19d2eff 100644 --- a/ofdgo_editor_composite_ops.go +++ b/ofdgo_editor_composite_ops.go @@ -16,7 +16,9 @@ package ofdgo import ( "bytes" + "context" "fmt" + "image" "maps" "slices" ) @@ -113,12 +115,25 @@ func (e *Editor) OrderCompositeObjects(page int, path ObjectPath, indexes []int, // 入参: page 页面索引, path 父复合路径, index 图片序号, data PNG或JPEG数据 // 返回: error 错误信息 func (e *Editor) ReplaceCompositeImage(page int, path ObjectPath, index int, data []byte) error { - return e.editCompositeObjects(page, path, []int{index}, func(_ *Renderer, nodes []*editorCompositeNode, _ []CompositeMember) error { + return e.editCompositeObjects(page, path, []int{index}, func(renderer *Renderer, nodes []*editorCompositeNode, _ []CompositeMember) error { node := nodes[0] if node.object.Type != "ImageObject" { return fmt.Errorf("composite member is not an image") } object := node.object + if object.ImageObject.ImageMask != "" { + config, _, err := decodeImageConfigData(data) + if err != nil { + return err + } + mask, err := renderer.ImageResource(object.ImageObject.ImageMask) + if err != nil { + return err + } + if err := validateImageMask(context.Background(), mask, image.Pt(config.Width, config.Height)); err != nil { + return err + } + } id, err := e.AddImage(data) if err != nil { return err diff --git a/ofdgo_editor_font_binding.go b/ofdgo_editor_font_binding.go index 308aa85..7ec848e 100644 --- a/ofdgo_editor_font_binding.go +++ b/ofdgo_editor_font_binding.go @@ -272,7 +272,7 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen index = fontCollectionIndex(data, []string{declaration.font.FontName, declaration.font.FamilyName}, declaration.font.Bold, declaration.font.Italic) } } else if len(file.Data) == 0 { - file, err = e.resolveBindingFont(scan, declaration) + file, err = e.resolveBindingFont(ctx, scan, declaration) if err != nil { return nil, err } @@ -302,7 +302,10 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen return nil, err } if explicit && len(object.CGTransform) != 0 && !verifiedGlyphs { - source, err := e.resolveBindingFont(scan, declaration) + source, err := e.resolveBindingFont(ctx, scan, declaration) + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return nil, err + } if err != nil || !sameBindingFont(source.Data, parsed.Data) { return nil, fmt.Errorf("font glyph mapping requires the original font") } @@ -503,23 +506,33 @@ func sameBindingFont(source, target []byte) bool { } // resolveBindingFont 精确匹配当前字体并提取独立字体数据,集合索引不传递到提取结果 -// 入参: scan 文档引用, declaration 字体声明 +// 同一文档复用入口和资源定义,字体解析结果不跨调用保留 +// 入参: ctx 取消上下文, scan 文档引用, declaration 字体声明 // 返回: FontFile 独立字体, error 匹配或解析错误 -func (e *Editor) resolveBindingFont(scan *editorFontDocument, declaration *editorFontDeclaration) (FontFile, error) { - reader := &Reader{Zip: scan.reader.Zip, files: maps.Clone(scan.reader.files), documentIndex: scan.index} - if scan.index == scan.reader.documentIndex { - reader.selectedVersion = scan.reader.selectedVersion - } - if err := reader.initRoot(); err != nil { +func (e *Editor) resolveBindingFont(ctx context.Context, scan *editorFontDocument, declaration *editorFontDeclaration) (FontFile, error) { + if err := ctx.Err(); err != nil { return FontFile{}, err } - if _, err := reader.Doc(); err != nil { - return FontFile{}, err + reader := scan.fontReader + if reader == nil { + reader = &Reader{Zip: scan.reader.Zip, files: maps.Clone(scan.reader.files), documentIndex: scan.index} + if scan.index == scan.reader.documentIndex { + reader.selectedVersion = scan.reader.selectedVersion + } + if err := reader.initRootContext(ctx); err != nil { + return FontFile{}, err + } + if _, err := reader.DocContext(ctx); err != nil { + return FontFile{}, err + } + scan.fontReader = reader } definition := declaration.font definition.FontFile = "" reader.fontCache[definition.ID] = &definition - resolved, err := e.newRenderer(reader).ResolveFont(definition.ID, true) + renderer := e.newRenderer(reader) + renderer.pageContext = ctx + resolved, err := renderer.ResolveFont(definition.ID, true) if err != nil { return FontFile{}, err } @@ -532,6 +545,9 @@ func (e *Editor) resolveBindingFont(scan *editorFontDocument, declaration *edito index = resolved.Face.Index } data, err := file.Face(index) + if cancelErr := ctx.Err(); cancelErr != nil { + return FontFile{}, cancelErr + } return FontFile{Data: data}, err } diff --git a/ofdgo_editor_font_scan.go b/ofdgo_editor_font_scan.go index 0060b61..5392b8a 100644 --- a/ofdgo_editor_font_scan.go +++ b/ofdgo_editor_font_scan.go @@ -26,13 +26,14 @@ import ( // editorFontDocument 保存一个文档的字体声明与完整内容引用,不进入附件和签名封装 type editorFontDocument struct { - reader *Reader - index int - files map[string]*editorFontXML - fonts map[string]*editorFontDeclaration - texts map[string][]editorFontText - fontCache editorBindingFontCache - unsafe bool + reader *Reader + index int + files map[string]*editorFontXML + fonts map[string]*editorFontDeclaration + texts map[string][]editorFontText + fontCache editorBindingFontCache + fontReader *Reader + unsafe bool } // editorFontXML 保存字体声明及文字引用所在的XML diff --git a/ofdgo_editor_open.go b/ofdgo_editor_open.go index 4fede67..3990eef 100644 --- a/ofdgo_editor_open.go +++ b/ofdgo_editor_open.go @@ -957,6 +957,36 @@ func (e *Editor) editorImage(id string) (image.Point, error) { return size, nil } +// validateImageMask 校验蒙版与原图尺寸一致且仅包含不透明黑白像素 +// 入参: id 蒙版资源标识, size 原图尺寸 +// 返回: error 资源、尺寸或像素错误 +func (e *Editor) validateImageMask(id string, size image.Point) error { + var data []byte + for _, resource := range e.resources { + if resource.image != nil && sameResourceID(resource.image.ID, id) { + data = resource.data + break + } + } + if data == nil && e.source != nil { + reader := e.source.reader + name, ok := resourceValue(reader.ResMap, id) + if !ok { + return fmt.Errorf("image mask resource %q not found", id) + } + var err error + data, err = reader.readFileView(reader.ResPath(name)) + if err != nil { + return err + } + } + mask, _, err := decodeImageData(data) + if err != nil { + return err + } + return validateImageMask(context.Background(), mask, size) +} + // editorObjectXML 编码可编辑对象,包含独立命名空间声明 // 入参: object 对象 // 返回: []byte XML片段, error 错误信息 diff --git a/ofdgo_font_cmap_variation.go b/ofdgo_font_cmap_variation.go new file mode 100644 index 0000000..e917b36 --- /dev/null +++ b/ofdgo_font_cmap_variation.go @@ -0,0 +1,234 @@ +// Copyright 2025-2026 肖其顿 (XIAO QI DUN) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ofdgo + +import ( + "encoding/binary" + "maps" + "slices" + "unicode/utf8" + + "github.com/go-text/typesetting/font/opentype/tables" +) + +// subsetCmapVariations 裁剪Unicode变体映射,将非默认变体的字形加入保留集合 +// 入参: data 字符映射表, chars 使用字符, glyphs 保留字形, count 字形总数 +// 返回: []byte 格式14子表,无变体时为空, bool 映射是否有效 +func subsetCmapVariations(data []byte, chars map[rune]bool, glyphs map[uint16]bool, count uint16) ([]byte, bool) { + if len(data) < 4 { + return nil, false + } + n := int(binary.BigEndian.Uint16(data[2:])) + if n > (len(data)-4)/8 { + return nil, false + } + var source []byte + for i := 0; i < n; i++ { + record := data[4+i*8:] + offset := uint64(binary.BigEndian.Uint32(record[4:])) + if offset < uint64(4+n*8) || offset+2 > uint64(len(data)) { + return nil, false + } + if binary.BigEndian.Uint16(data[offset:]) != 14 { + continue + } + if source != nil || binary.BigEndian.Uint16(record) != 0 || binary.BigEndian.Uint16(record[2:]) != 5 || offset+10 > uint64(len(data)) { + return nil, false + } + length := uint64(binary.BigEndian.Uint32(data[offset+2:])) + if length < 10 || offset+length > uint64(len(data)) { + return nil, false + } + source = data[offset : offset+length] + } + if source == nil { + return nil, true + } + return subsetCmapVariationTable(source, chars, glyphs, count) +} + +// subsetCmapVariationTable 按使用字符筛选默认及非默认变体,保持选择符和映射有序 +// 入参: data 格式14子表, chars 使用字符, glyphs 保留字形, count 字形总数 +// 返回: []byte 裁剪后的子表, bool 子表是否有效 +func subsetCmapVariationTable(data []byte, chars map[rune]bool, glyphs map[uint16]bool, count uint16) ([]byte, bool) { + n := uint64(binary.BigEndian.Uint32(data[6:])) + header := 10 + n*11 + if header > uint64(len(data)) { + return nil, false + } + used := slices.Sorted(maps.Keys(chars)) + var records, payload []byte + previous := rune(-1) + for i := uint64(0); i < n; i++ { + raw := data[10+i*11:] + for j, size := range []uint64{4, 5} { + offset := uint64(binary.BigEndian.Uint32(raw[3+j*4:])) + if offset == 0 { + continue + } + if offset < header || offset+4 > uint64(len(data)) { + return nil, false + } + entries := uint64(binary.BigEndian.Uint32(data[offset:])) + if offset+4+entries*size > uint64(len(data)) { + return nil, false + } + } + record, _, err := tables.ParseVariationSelector(raw, data) + selector := cmapVariationRune(record.VarSelector) + if err != nil || !utf8.ValidRune(selector) || selector <= previous || !validCmapVariation(record, count) { + return nil, false + } + previous = selector + if !chars[selector] { + continue + } + defaults := subsetCmapDefaultVariations(record.DefaultUVS.Ranges, used) + var alternatives []byte + for _, entry := range record.NonDefaultUVS.Ranges { + if chars[cmapVariationRune(entry.UnicodeValue)] { + alternatives = append(alternatives, entry.UnicodeValue[:]...) + alternatives = binary.BigEndian.AppendUint16(alternatives, uint16(entry.GlyphID)) + glyphs[uint16(entry.GlyphID)] = true + } + } + if len(defaults)+len(alternatives) == 0 { + continue + } + start := len(records) + records = append(records, record.VarSelector[:]...) + records = append(records, make([]byte, 8)...) + for j, entries := range [][]byte{defaults, alternatives} { + if len(entries) == 0 { + continue + } + binary.BigEndian.PutUint32(records[start+3+j*4:], uint32(len(payload)+1)) + payload = binary.BigEndian.AppendUint32(payload, uint32(len(entries)/(4+j))) + payload = append(payload, entries...) + } + } + if len(records) == 0 { + return nil, true + } + if uint64(len(records))+uint64(len(payload))+10 > 1<<32-1 { + return nil, false + } + for i := 0; i < len(records); i += 11 { + for _, j := range []int{3, 7} { + offset := binary.BigEndian.Uint32(records[i+j:]) + if offset != 0 { + binary.BigEndian.PutUint32(records[i+j:], offset-1+uint32(10+len(records))) + } + } + } + out := make([]byte, 10, 10+len(records)+len(payload)) + binary.BigEndian.PutUint16(out, 14) + binary.BigEndian.PutUint32(out[2:], uint32(cap(out))) + binary.BigEndian.PutUint32(out[6:], uint32(len(records)/11)) + out = append(out, records...) + return append(out, payload...), true +} + +// validCmapVariation 检查变体映射的字符范围、排序、互斥关系及字形编号 +// 入参: record 选择符记录, count 字形总数 +// 返回: bool 映射是否有效 +func validCmapVariation(record tables.VariationSelector, count uint16) bool { + defaults := record.DefaultUVS.Ranges + previous := rune(-1) + for _, entry := range defaults { + first := cmapVariationRune(entry.StartUnicodeValue) + last := first + rune(entry.AdditionalCount) + if first <= previous || !utf8.ValidRune(first) || !utf8.ValidRune(last) || first < 0xd800 && last >= 0xd800 { + return false + } + previous = last + } + previous = -1 + index := 0 + for _, entry := range record.NonDefaultUVS.Ranges { + char := cmapVariationRune(entry.UnicodeValue) + if char <= previous || !utf8.ValidRune(char) || uint16(entry.GlyphID) >= count { + return false + } + previous = char + for index < len(defaults) && cmapVariationRune(defaults[index].StartUnicodeValue)+rune(defaults[index].AdditionalCount) < char { + index++ + } + if index < len(defaults) && cmapVariationRune(defaults[index].StartUnicodeValue) <= char { + return false + } + } + return true +} + +// subsetCmapDefaultVariations 筛选默认变体的使用字符,将连续字符合并为格式14范围 +// 入参: ranges 默认变体范围, chars 已排序的使用字符 +// 返回: []byte 范围记录,不含数量字段 +func subsetCmapDefaultVariations(ranges []tables.UnicodeRange, chars []rune) []byte { + var out []byte + index := 0 + last := rune(-2) + for _, char := range chars { + for index < len(ranges) && cmapVariationRune(ranges[index].StartUnicodeValue)+rune(ranges[index].AdditionalCount) < char { + index++ + } + if index == len(ranges) { + break + } + if cmapVariationRune(ranges[index].StartUnicodeValue) > char { + continue + } + if char == last+1 && out[len(out)-1] < 255 { + out[len(out)-1]++ + } else { + out = append(out, byte(char>>16), byte(char>>8), byte(char), 0) + } + last = char + } + return out +} + +// cmapVariationRune 读取变体映射中的24位Unicode码点 +// 入参: value 三字节码点 +// 返回: rune Unicode码点 +func cmapVariationRune(value [3]byte) rune { + return rune(value[0])<<16 | rune(value[1])<<8 | rune(value[2]) +} + +// buildCmapWithVariations 在生成的Unicode映射中插入格式14记录,保持编码记录有序 +// 入参: count 字形总数, mapping 字符映射, variations 格式14子表 +// 返回: []byte 字符映射表 +func buildCmapWithVariations(count uint16, mapping map[rune]uint16, variations []byte) []byte { + base := buildCmapTable(count, mapping) + if len(variations) == 0 { + return base + } + payload := base[binary.BigEndian.Uint32(base[8:]):] + out := make([]byte, 28, 28+len(payload)+len(variations)) + binary.BigEndian.PutUint16(out[2:], 3) + encoding := uint16(3) + if binary.BigEndian.Uint16(payload) == 12 { + encoding = 4 + } + binary.BigEndian.PutUint16(out[6:], encoding) + binary.BigEndian.PutUint32(out[8:], 28) + binary.BigEndian.PutUint16(out[14:], 5) + binary.BigEndian.PutUint32(out[16:], uint32(28+len(payload))) + last := 4 + 8*(int(binary.BigEndian.Uint16(base[2:]))-1) + copy(out[20:28], base[last:last+8]) + binary.BigEndian.PutUint32(out[24:], 28) + out = append(out, payload...) + return append(out, variations...) +} diff --git a/ofdgo_font_resource_sfnt.go b/ofdgo_font_resource_sfnt.go index 6fad1fa..b66f5fa 100644 --- a/ofdgo_font_resource_sfnt.go +++ b/ofdgo_font_resource_sfnt.go @@ -232,7 +232,7 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte { var unicodeTables []sfntCmap for _, record := range sfnt.cmaps { if record.format == 14 { - return nil + continue } if record.platform == 0 || record.platform == 3 && (record.encoding == 1 || record.encoding == 10) { unicodeTables = append(unicodeTables, record) @@ -256,7 +256,8 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte { mapping[char] = id } } - if !retainSFNTSubstitutions(sfnt.Tables["GSUB"], glyphs, sfnt.NumGlyphs()) { + variations, ok := subsetCmapVariations(sfnt.Tables["cmap"], usage.chars, glyphs, sfnt.NumGlyphs()) + if !ok || !retainSFNTSubstitutions(sfnt.Tables["GSUB"], glyphs, sfnt.NumGlyphs()) { return nil } if sfnt.IsCFF { @@ -270,7 +271,7 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte { } tables := maps.Clone(sfnt.Tables) tables["CFF "] = cff - tables["cmap"] = buildCmapTable(sfnt.NumGlyphs(), mapping) + tables["cmap"] = buildCmapWithVariations(sfnt.NumGlyphs(), mapping, variations) delete(tables, "DSIG") result, err := serializeOTF(tables) if err != nil || len(result) >= len(data) { @@ -307,7 +308,7 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte { } tables := maps.Clone(sfnt.Tables) tables["glyf"], tables["loca"] = glyf, loca - tables["cmap"] = buildCmapTable(uint16(sfnt.NumGlyphs()), mapping) + tables["cmap"] = buildCmapWithVariations(sfnt.NumGlyphs(), mapping, variations) tables["head"] = bytes.Clone(tables["head"]) clear(tables["head"][8:12]) delete(tables, "DSIG") diff --git a/ofdgo_image.go b/ofdgo_image.go index 8aff37a..82b14d9 100644 --- a/ofdgo_image.go +++ b/ofdgo_image.go @@ -23,18 +23,16 @@ import ( "image/color" _ "image/gif" "io" - "math" _ "github.com/xiaoqidun/j2kgo" _ "github.com/xiaoqidun/jbig2" _ "golang.org/x/image/bmp" - "golang.org/x/image/draw" "golang.org/x/image/tiff" ) // transparentEdgeImage 共享只读像素并按需计算透明边缘和采样边距 type transparentEdgeImage struct { - source *image.NRGBA + source image.Image rect image.Rectangle } @@ -61,7 +59,7 @@ func (s *transparentEdgeImage) NRGBAAt(x, y int) color.NRGBA { } b := s.source.Bounds() sx, sy := b.Min.X+min(max(x-1, 0), b.Dx()-1), b.Min.Y+min(max(y-1, 0), b.Dy()-1) - c := s.source.NRGBAAt(sx, sy) + c := imageNRGBAAt(s.source, sx, sy) if c.A == 0 { if edge, ok := transparentEdgeColor(s.source, sx, sy); ok { c = edge @@ -352,86 +350,136 @@ func parseBMPHeader(data []byte) (int, int, int, uint16, uint32, error) { return offset, width, height, bpp, compression, nil } -// imageWithMask 应用图片蒙版 -// 入参: img 图片对象, mask 蒙版图片 -// 返回: image.Image 蒙版处理后的图片对象 -func imageWithMask(img, mask image.Image) image.Image { - if img == nil || mask == nil { - return img +// imageWithEffects 合并同尺寸二值蒙版和对象透明度,保留原图采样精度及只读像素 +// 入参: ctx 取消上下文,nil使用后台上下文, img 原图, mask 二值蒙版,可为空, alpha 对象透明度,可为空 +// 返回: image.Image 处理后的图片, error 蒙版、解码或取消错误 +func imageWithEffects(ctx context.Context, img, mask image.Image, alpha *int) (image.Image, error) { + if ctx == nil { + ctx = context.Background() } - bounds := img.Bounds() - if bounds.Empty() || mask.Bounds().Empty() { - return img + if err := ctx.Err(); err != nil { + return nil, err } - source := imagePixelSource(img) - maskSource := imagePixelSource(mask) - maskBounds := maskSource.Bounds() - var opacity image.Image = maskSource - if bounds.Size() != maskBounds.Size() { - resized := image.NewGray(bounds) - draw.CatmullRom.Scale(resized, bounds, maskSource, maskBounds, draw.Src, nil) - opacity = resized - maskBounds = bounds + a := 255 + if alpha != nil { + a = clampColor(*alpha) } - out := image.NewNRGBA(bounds) - if src, ok := source.(*image.NRGBA); ok { - for y := 0; y < bounds.Dy(); y++ { - offset := src.PixOffset(bounds.Min.X, bounds.Min.Y+y) - row := out.Pix[y*out.Stride : (y+1)*out.Stride] - copy(row, src.Pix[offset:offset+len(row)]) - for x := 3; x < len(row); x += 4 { - a := imageGrayAt(opacity, maskBounds.Min.X+x/4, maskBounds.Min.Y+y).Y - row[x] = uint8(int(row[x]) * int(a) / 255) + if img == nil || mask == nil && a == 255 { + return img, nil + } + source, err := imagePixelDataContext(ctx, img) + if err != nil { + return nil, err + } + bounds := source.Bounds() + var maskBounds image.Rectangle + if mask != nil { + mask, err = imagePixelDataContext(ctx, mask) + if err != nil { + return nil, err + } + maskBounds = mask.Bounds() + if bounds.Empty() || bounds.Size() != maskBounds.Size() { + return nil, fmt.Errorf("image mask dimensions differ from source") + } + } + if imageHighPrecision(source) { + out := image.NewNRGBA64(bounds) + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + if err := ctx.Err(); err != nil { + return nil, err + } + for x := bounds.Min.X; x < bounds.Max.X; x++ { + opacity := a + if mask != nil { + value, err := imageMaskAlpha(mask, maskBounds.Min.X+x-bounds.Min.X, maskBounds.Min.Y+y-bounds.Min.Y) + if err != nil { + return nil, err + } + opacity *= int(value) / 255 + } + pixel := imageNRGBA64At(source, x, y) + pixel.A = uint16(uint32(pixel.A) * uint32(opacity) / 255) + out.SetNRGBA64(x, y, pixel) } } - return out + if err := ctx.Err(); err != nil { + return nil, err + } + return out, nil } + out := image.NewNRGBA(bounds) for y := bounds.Min.Y; y < bounds.Max.Y; y++ { - for x := bounds.Min.X; x < bounds.Max.X; x++ { - mx := maskBounds.Min.X + x - bounds.Min.X - my := maskBounds.Min.Y + y - bounds.Min.Y - a := imageGrayAt(opacity, mx, my).Y - c := imageNRGBAAt(source, x, y) - c.A = uint8(int(c.A) * int(a) / 255) - out.SetNRGBA(x, y, c) + if err := ctx.Err(); err != nil { + return nil, err + } + row := out.Pix[(y-bounds.Min.Y)*out.Stride : (y-bounds.Min.Y+1)*out.Stride] + if src, ok := source.(*image.NRGBA); ok { + offset := src.PixOffset(bounds.Min.X, y) + copy(row, src.Pix[offset:offset+len(row)]) + } else { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + out.SetNRGBA(x, y, imageNRGBAAt(source, x, y)) + } + } + for x := 0; x < bounds.Dx(); x++ { + opacity := a + if mask != nil { + value, err := imageMaskAlpha(mask, maskBounds.Min.X+x, maskBounds.Min.Y+y-bounds.Min.Y) + if err != nil { + return nil, err + } + opacity *= int(value) / 255 + } + row[x*4+3] = uint8(int(row[x*4+3]) * opacity / 255) } } - return out + if err := ctx.Err(); err != nil { + return nil, err + } + return out, nil } -// imageWithAlpha 合并图片透明度 -// 入参: img 图片对象, alpha 对象透明度 -// 返回: image.Image 合并后的图片对象 -func imageWithAlpha(img image.Image, alpha *int) image.Image { - if img == nil || alpha == nil { - return img +// imageMaskAlpha 读取二值蒙版,拒绝灰阶、彩色及带透明度的像素 +// 入参: mask 蒙版图像, x 横坐标, y 纵坐标 +// 返回: uint8 透明度,黑色为0、白色为255, error 非二值像素错误 +func imageMaskAlpha(mask image.Image, x, y int) (uint8, error) { + var r, g, b, a uint32 + if source, ok := mask.(image.RGBA64Image); ok { + pixel := source.RGBA64At(x, y) + r, g, b, a = uint32(pixel.R), uint32(pixel.G), uint32(pixel.B), uint32(pixel.A) + } else { + r, g, b, a = mask.At(x, y).RGBA() } - a := clampColor(*alpha) - if a == 255 { - return img + if a != 65535 || r != g || r != b || r != 0 && r != 65535 { + return 0, fmt.Errorf("image mask contains non-binary pixel at (%d, %d)", x, y) } - bounds := img.Bounds() - out := image.NewNRGBA(bounds) - source := imagePixelSource(img) - if src, ok := source.(*image.NRGBA); ok { - for y := 0; y < bounds.Dy(); y++ { - offset := src.PixOffset(bounds.Min.X, bounds.Min.Y+y) - row := out.Pix[y*out.Stride : (y+1)*out.Stride] - copy(row, src.Pix[offset:offset+len(row)]) - for x := 3; x < len(row); x += 4 { - row[x] = uint8(int(row[x]) * a / 255) + return uint8(r >> 8), nil +} + +// validateImageMask 校验二值蒙版尺寸及像素,不分配合成图像 +// 入参: ctx 取消上下文, mask 蒙版图像, size 原图尺寸 +// 返回: error 解码、尺寸、像素或取消错误 +func validateImageMask(ctx context.Context, mask image.Image, size image.Point) error { + if mask.Bounds().Size() != size || size.X <= 0 || size.Y <= 0 { + return fmt.Errorf("image mask dimensions differ from source") + } + mask, err := imagePixelDataContext(ctx, mask) + if err != nil { + return err + } + bounds := mask.Bounds() + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + if err := ctx.Err(); err != nil { + return err + } + for x := bounds.Min.X; x < bounds.Max.X; x++ { + if _, err := imageMaskAlpha(mask, x, y); err != nil { + return err } } - return out } - for y := bounds.Min.Y; y < bounds.Max.Y; y++ { - for x := bounds.Min.X; x < bounds.Max.X; x++ { - c := imageNRGBAAt(source, x, y) - c.A = uint8(int(c.A) * a / 255) - out.SetNRGBA(x, y, c) - } - } - return out + return ctx.Err() } // imagePixelSource 获取图片像素源 @@ -454,6 +502,28 @@ func imagePixelData(img image.Image) (image.Image, error) { return img, nil } +// imagePixelDataContext 获取像素并传递取消状态,兼容不接收上下文的图像实现 +// 入参: ctx 取消上下文,nil使用后台上下文, img 图片对象 +// 返回: image.Image 解码像素, error 解码或取消错误 +func imagePixelDataContext(ctx context.Context, img image.Image) (image.Image, error) { + if ctx == nil { + ctx = context.Background() + } + if err := ctx.Err(); err != nil { + return nil, err + } + if src, ok := img.(interface { + ImageContext(context.Context) (image.Image, error) + }); ok { + return src.ImageContext(ctx) + } + pixels, err := imagePixelData(img) + if cancelErr := ctx.Err(); cancelErr != nil { + return nil, cancelErr + } + return pixels, err +} + // imageGrayAt 获取图片灰度像素 // 入参: img 图片对象, x X坐标, y Y坐标 // 返回: color.Gray 灰度像素 @@ -509,6 +579,14 @@ func imageAlphaAt(img image.Image, x, y int) uint32 { return alpha } +// imageHighPrecision 判断颜色模型是否保留十六位采样 +// 入参: img 图片对象 +// 返回: bool 是否使用十六位颜色或透明度 +func imageHighPrecision(img image.Image) bool { + model := img.ColorModel() + return model == color.NRGBA64Model || model == color.RGBA64Model || model == color.Gray16Model || model == color.Alpha16Model +} + // imageNRGBA64At 读取十六位非预乘像素,原生图像避免颜色装箱 // 入参: img 图片对象, x X坐标, y Y坐标 // 返回: color.NRGBA64 NRGBA64像素 @@ -545,9 +623,34 @@ func imageNRGBA64At(img image.Image, x, y int) color.NRGBA64 { } // transparentEdgeColor 获取透明像素相邻的可见颜色 -// 入参: img 图片对象, x X坐标, y Y坐标 +// 入参: source 图片对象, x X坐标, y Y坐标 // 返回: color.NRGBA 颜色, bool 是否存在 -func transparentEdgeColor(img *image.NRGBA, x, y int) (color.NRGBA, bool) { +func transparentEdgeColor(source image.Image, x, y int) (color.NRGBA, bool) { + img, ok := source.(*image.NRGBA) + if !ok { + bounds := source.Bounds() + var best color.NRGBA + for ny := max(y-1, bounds.Min.Y); ny < min(y+2, bounds.Max.Y); ny++ { + for nx := max(x-1, bounds.Min.X); nx < min(x+2, bounds.Max.X); nx++ { + if nx == x && ny == y { + continue + } + pixel := imageNRGBAAt(source, nx, ny) + if pixel.A > best.A { + best = pixel + if best.A == 255 { + best.A = 1 + return best, true + } + } + } + } + if best.A == 0 { + return color.NRGBA{}, false + } + best.A = 1 + return best, true + } bounds := img.Bounds() best, alpha := 0, uint8(0) left, right := max(x-1, bounds.Min.X), min(x+2, bounds.Max.X) @@ -578,134 +681,88 @@ func transparentPaddingColor(c color.NRGBA) color.NRGBA { return c } -// clipStampImage 裁剪印章图像 -// 入参: img 印章图像, box 印章区域, clip 裁剪区域 -// 返回: image.Image 裁剪后的印章图像 -func clipStampImage(img image.Image, box, clip Box) image.Image { - bounds := img.Bounds() - x0 := int(math.Floor(clip.X / box.W * float64(bounds.Dx()))) - y0 := int(math.Floor(clip.Y / box.H * float64(bounds.Dy()))) - x1 := int(math.Ceil((clip.X + clip.W) / box.W * float64(bounds.Dx()))) - y1 := int(math.Ceil((clip.Y + clip.H) / box.H * float64(bounds.Dy()))) - out := image.NewNRGBA(image.Rect(0, 0, x1-x0, y1-y0)) - draw.Draw(out, out.Bounds(), img, image.Pt(bounds.Min.X+x0, bounds.Min.Y+y0), draw.Src) - return out -} - -// stampImageWithTransparentWhite 处理印章图片白色底色 -// 入参: img 印章图片对象 -// 返回: image.Image 处理后的印章图片对象 -func stampImageWithTransparentWhite(img image.Image) image.Image { - if opaque, ok := img.(interface{ Opaque() bool }); ok && !opaque.Opaque() { - return img +// imageWithTransparentEdge 扫描透明边缘并共享只读像素,保留原图采样精度 +// 入参: ctx 取消上下文,nil使用后台上下文, img 图片对象 +// 返回: image.Image 补边图像, int 补边像素数, error 解码或取消错误 +func imageWithTransparentEdge(ctx context.Context, img image.Image) (image.Image, int, error) { + if ctx == nil { + ctx = context.Background() } - bounds := img.Bounds() - out := image.NewNRGBA(bounds) - hasAlpha := false - for y := bounds.Min.Y; y < bounds.Max.Y; y++ { - for x := bounds.Min.X; x < bounds.Max.X; x++ { - c := color.NRGBAModel.Convert(img.At(x, y)).(color.NRGBA) - if c.A < 255 { - hasAlpha = true - } - out.SetNRGBA(x, y, c) - } + if err := ctx.Err(); err != nil { + return nil, 0, err } - if hasAlpha { - return img - } - for y := bounds.Min.Y; y < bounds.Max.Y; y++ { - for x := bounds.Min.X; x < bounds.Max.X; x++ { - c := out.NRGBAAt(x, y) - if c.R >= 250 && c.G >= 250 && c.B >= 250 { - c.A = 0 - out.SetNRGBA(x, y, c) - } - } - } - return out -} - -// imageWithTransparentEdge 补齐透明图片边缘颜色 -// 入参: img 图片对象 -// 返回: image.Image 补齐后的图片对象, int 补齐像素数 -func imageWithTransparentEdge(img image.Image) (image.Image, int) { if img == nil { - return img, 0 + return img, 0, nil } bounds := img.Bounds() w, h := bounds.Dx(), bounds.Dy() if w == 0 || h == 0 { - return img, 0 + return img, 0, nil } if src, ok := img.(*EncodedImage); ok && src.MIME() == "image/jpeg" { - return img, 0 + return img, 0, nil } - source := imagePixelSource(img) - if opaque, ok := source.(interface{ Opaque() bool }); ok && opaque.Opaque() { - return img, 0 + source, err := imagePixelDataContext(ctx, img) + if err != nil { + return nil, 0, err } - if model := source.ColorModel(); model == color.NRGBA64Model || model == color.RGBA64Model || model == color.Alpha16Model || model == color.Gray16Model { - return imageWithTransparentEdge64(img, source) + switch source.(type) { + case *image.Gray, *image.Gray16, *image.YCbCr, *image.CMYK: + return img, 0, nil } hasZero, hasVisible := false, false - src, ok := source.(*image.NRGBA) - if ok { - srcBounds := src.Bounds() - hasZero = src.Pix[3] == 0 + var pixels []byte + var stride, step int + switch src := source.(type) { + case *image.NRGBA: + pixels, stride, step = src.Pix, src.Stride, 4 + case *image.RGBA: + pixels, stride, step = src.Pix, src.Stride, 4 + case *image.Alpha: + pixels, stride, step = src.Pix, src.Stride, 1 + } + if pixels != nil { + hasZero = pixels[step-1] == 0 hasVisible = !hasZero scan: - for y := srcBounds.Min.Y; y < srcBounds.Max.Y; y++ { - offset := src.PixOffset(srcBounds.Min.X, y) + 3 + for y := 0; y < h; y++ { + if err := ctx.Err(); err != nil { + return nil, 0, err + } + offset := y*stride + step - 1 for x := 0; x < w; x++ { - if (src.Pix[offset] == 0) != hasZero { + if (pixels[offset] == 0) != hasZero { hasZero, hasVisible = true, true break scan } - offset += 4 + offset += step } } } else { - src = image.NewNRGBA(image.Rect(0, 0, w, h)) - for y := 0; y < h; y++ { - for x := 0; x < w; x++ { - c := imageNRGBAAt(source, bounds.Min.X+x, bounds.Min.Y+y) - src.SetNRGBA(x, y, c) - if c.A == 0 { + for y := bounds.Min.Y; y < bounds.Max.Y && !(hasZero && hasVisible); y++ { + if err := ctx.Err(); err != nil { + return nil, 0, err + } + for x := bounds.Min.X; x < bounds.Max.X; x++ { + if imageAlphaAt(source, x, y) == 0 { hasZero = true } else { hasVisible = true } + if hasZero && hasVisible { + break + } } } } - if !hasZero || !hasVisible { - return img, 0 - } - return &transparentEdgeImage{source: src, rect: image.Rect(0, 0, w+2, h+2)}, 1 -} - -// imageWithTransparentEdge64 保留十六位颜色和透明度,不经八位图像中转 -// 入参: original 原始图像, source 已解码的像素源 -// 返回: image.Image 边缘处理结果, int 补齐像素数 -func imageWithTransparentEdge64(original, source image.Image) (image.Image, int) { - bounds := source.Bounds() - hasZero, hasVisible := false, false - for y := bounds.Min.Y; y < bounds.Max.Y && !(hasZero && hasVisible); y++ { - for x := bounds.Min.X; x < bounds.Max.X; x++ { - _, _, _, a := source.At(x, y).RGBA() - if a == 0 { - hasZero = true - } else { - hasVisible = true - } - if hasZero && hasVisible { - break - } - } + if err := ctx.Err(); err != nil { + return nil, 0, err } if !hasZero || !hasVisible { - return original, 0 + return img, 0, nil } - return &transparentEdgeImage64{source: source, rect: image.Rect(0, 0, bounds.Dx()+2, bounds.Dy()+2)}, 1 + if imageHighPrecision(source) { + return &transparentEdgeImage64{source: source, rect: image.Rect(0, 0, w+2, h+2)}, 1, nil + } + return &transparentEdgeImage{source: source, rect: image.Rect(0, 0, w+2, h+2)}, 1, nil } diff --git a/ofdgo_image_resource.go b/ofdgo_image_resource.go index 1a4f5d5..9e1695a 100644 --- a/ofdgo_image_resource.go +++ b/ofdgo_image_resource.go @@ -17,12 +17,15 @@ package ofdgo import ( "bytes" "context" + "errors" "fmt" "image" "image/color" _ "image/jpeg" _ "image/png" + "io" "sync" + "sync/atomic" ) // imageCacheLimit 限制图片缓存的编码数据及预计解码内存 @@ -34,9 +37,15 @@ type EncodedImage struct { data []byte format string config image.Config - once sync.Once - decoded image.Image - err error + result atomic.Pointer[encodedImageResult] + mu sync.Mutex + pending chan struct{} +} + +// encodedImageResult 保存已完成的像素解码结果,不保存取消或超时错误 +type encodedImageResult struct { + image image.Image + err error } // NewEncodedImage 读取PNG或JPEG头信息,不解码像素,复制输入数据 @@ -67,8 +76,67 @@ func (img *EncodedImage) Bounds() image.Rectangle { // Image 首次解码图片并缓存结果或错误,支持并发读取 // 返回: image.Image 只读像素, error 解码错误 func (img *EncodedImage) Image() (image.Image, error) { - img.once.Do(func() { img.decoded, _, img.err = image.Decode(bytes.NewReader(img.data)) }) - return img.decoded, img.err + if result := img.result.Load(); result != nil { + return result.image, result.err + } + return img.ImageContext(context.Background()) +} + +// ImageContext 解码并共享只读像素,取消或超时后可重试,nil使用后台上下文 +// 并发调用等待正在进行的解码,等待方取消不影响其他调用 +// 入参: ctx 取消上下文 +// 返回: image.Image 只读像素, error 解码或取消错误 +func (img *EncodedImage) ImageContext(ctx context.Context) (image.Image, error) { + if ctx == nil { + ctx = context.Background() + } + for { + if err := ctx.Err(); err != nil { + return nil, err + } + if result := img.result.Load(); result != nil { + return result.image, result.err + } + img.mu.Lock() + if img.result.Load() != nil { + img.mu.Unlock() + continue + } + pending := img.pending + if pending == nil { + pending = make(chan struct{}) + img.pending = pending + img.mu.Unlock() + return img.decodeContext(ctx, pending) + } + img.mu.Unlock() + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-pending: + } + } +} + +// decodeContext 执行本次解码,退出时唤醒等待方,只缓存完整结果及稳定错误 +// 入参: ctx 取消上下文, pending 本次解码的完成通知 +// 返回: image.Image 只读像素, error 解码或取消错误 +func (img *EncodedImage) decodeContext(ctx context.Context, pending chan struct{}) (image.Image, error) { + defer func() { + img.mu.Lock() + img.pending = nil + close(pending) + img.mu.Unlock() + }() + pixels, _, err := image.Decode(imageInput{ReadCloser: io.NopCloser(bytes.NewReader(img.data)), context: ctx}) + if cancelErr := ctx.Err(); cancelErr != nil { + return nil, cancelErr + } + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return nil, err + } + img.result.Store(&encodedImageResult{image: pixels, err: err}) + return pixels, err } // At 返回像素颜色,解码失败时返回透明色,绘制前应通过Image检查错误 @@ -101,7 +169,7 @@ func (r *Renderer) imageObjectResource(object ImageObject) (image.Image, error) read := func(id string) (image.Image, error) { img, err := r.ImageResource(id) if err == nil && (r.decodeImages || object.Substitution != "") { - pixels, decodeErr := imagePixelData(img) + pixels, decodeErr := imagePixelDataContext(r.pageContext, img) err = decodeErr if r.decodeImages { img = pixels @@ -126,6 +194,67 @@ func (r *Renderer) imageObjectResource(object ImageObject) (image.Image, error) return img, nil } +// imageObjectMask 校验原图蒙版,替换图分辨率不同时按像素中心进行最近邻采样 +// 入参: object 图片对象, target 实际绘制图像 +// 返回: image.Image 二值蒙版,无蒙版时为nil, error 资源或蒙版错误 +func (r *Renderer) imageObjectMask(object ImageObject, target image.Image) (image.Image, error) { + if object.ImageMask == "" { + return nil, nil + } + mask, err := r.ImageResource(object.ImageMask) + if err != nil { + return nil, err + } + size := target.Bounds().Size() + if object.Substitution == "" { + if mask.Bounds().Size() == size { + return mask, nil + } + return nil, fmt.Errorf("image mask dimensions differ from source") + } + original, err := r.ImageResource(object.ResourceID) + if err != nil { + if mask.Bounds().Size() == size { + return mask, nil + } + return nil, fmt.Errorf("cannot verify image mask dimensions: %w", err) + } + if mask.Bounds().Size() != original.Bounds().Size() { + return nil, fmt.Errorf("image mask dimensions differ from source") + } + if mask.Bounds().Size() == size { + return mask, nil + } + ctx := r.pageContext + if ctx == nil { + ctx = context.Background() + } + if err := validateImageMask(ctx, mask, original.Bounds().Size()); err != nil { + return nil, err + } + mask, err = imagePixelDataContext(ctx, mask) + if err != nil { + return nil, err + } + bounds := mask.Bounds() + result := image.NewGray(image.Rectangle{Max: size}) + for y := range size.Y { + if err := ctx.Err(); err != nil { + return nil, err + } + sy := bounds.Min.Y + int((int64(y)*2+1)*int64(bounds.Dy())/(2*int64(size.Y))) + for x := range size.X { + sx := bounds.Min.X + int((int64(x)*2+1)*int64(bounds.Dx())/(2*int64(size.X))) + value, err := imageMaskAlpha(mask, sx, sy) + if err != nil { + return nil, err + } + result.SetGray(x, y, color.Gray{Y: value}) + } + } + return result, ctx.Err() +} + // newEncodedImage 接管编码数据并读取头信息 // 入参: data 原始编码 // 返回: *EncodedImage 惰性图片, error 头信息或格式错误 @@ -162,7 +291,7 @@ func (r *Renderer) cachedImageResource(resPath string) (image.Image, error) { return img, nil } -// imageResourceCost 预留十六位像素展开空间,超预算图片不进入缓存 +// imageResourceCost 预留十六位像素空间,并计入编码、像素缓冲及补边源图的可见容量 // 入参: img 图片资源, limit 缓存预算 // 返回: int 估算字节数或超预算标记 func imageResourceCost(img image.Image, limit int) int { @@ -171,14 +300,57 @@ func imageResourceCost(img image.Image, limit int) int { if limit < cost || w <= 0 || h <= 0 || w > (limit-cost)/8/h { return limit + 1 } - cost += w * h * 8 + pixels := w * h * 8 if encoded, ok := img.(*EncodedImage); ok { - if len(encoded.data) > limit-cost { + if cap(encoded.data) > limit-cost-pixels { return limit + 1 } - cost += len(encoded.data) + return cost + pixels + cap(encoded.data) } - return cost + var buffers [4]int + switch source := img.(type) { + case *image.RGBA: + buffers[0] = cap(source.Pix) + case *image.RGBA64: + buffers[0] = cap(source.Pix) + case *image.NRGBA: + buffers[0] = cap(source.Pix) + case *image.NRGBA64: + buffers[0] = cap(source.Pix) + case *image.Gray: + buffers[0] = cap(source.Pix) + case *image.Gray16: + buffers[0] = cap(source.Pix) + case *image.Alpha: + buffers[0] = cap(source.Pix) + case *image.Alpha16: + buffers[0] = cap(source.Pix) + case *image.CMYK: + buffers[0] = cap(source.Pix) + case *image.Paletted: + if cap(source.Palette) > (limit-cost)/16 { + return limit + 1 + } + buffers[0], buffers[1] = cap(source.Pix), cap(source.Palette)*16 + case *image.YCbCr: + buffers = [4]int{cap(source.Y), cap(source.Cb), cap(source.Cr)} + case *image.NYCbCrA: + buffers = [4]int{cap(source.Y), cap(source.Cb), cap(source.Cr), cap(source.A)} + case *transparentEdgeImage: + buffers[0] = imageResourceCost(source.source, limit-cost) + case *transparentEdgeImage64: + buffers[0] = imageResourceCost(source.source, limit-cost) + case *rasterPaddedImage: + buffers[0] = imageResourceCost(source.source, limit-cost) + } + storage := 0 + for _, size := range buffers { + if size > limit-cost-storage { + return limit + 1 + } + storage += size + } + return cost + max(pixels, storage) } // decodeImageResource 读取或复用图片资源,按输出需要解码像素 @@ -190,9 +362,7 @@ func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) { return nil, err } if r.decodeImages { - if source, ok := img.(*EncodedImage); ok { - return source.Image() - } + return imagePixelDataContext(r.pageContext, img) } return img, nil } diff --git a/ofdgo_pdf_composite.go b/ofdgo_pdf_composite.go index a459609..80870a5 100644 --- a/ofdgo_pdf_composite.go +++ b/ofdgo_pdf_composite.go @@ -206,14 +206,7 @@ func (c *pdfCompositor) geometry(node pdfCompositeNode, stroke bool) (pdfComposi c.cache.bounds[key] = geometry.box cost := 256 if geometry.scene != nil { - for _, command := range geometry.scene.Commands { - part := rasterCommandCost(command) - if part <= 0 || part > cache.limit-cost { - cost = cache.limit + 1 - break - } - cost += part - } + cost += rasterCommandsCost(geometry.scene.Commands, cache.limit-cost) } cache.put(sceneKey, geometry, cost) } diff --git a/ofdgo_render_cache.go b/ofdgo_render_cache.go index daf6d43..d20f80e 100644 --- a/ofdgo_render_cache.go +++ b/ofdgo_render_cache.go @@ -16,6 +16,8 @@ package ofdgo import ( "container/list" + "image" + "reflect" ) // renderCache 按估算字节数限制派生渲染数据,淘汰不修改源资源 @@ -85,3 +87,37 @@ func rasterCommandCost(command RasterCommand) int { } return cost } + +// rasterCommandsCost 估算整组指令保留的内存,同一图片实例只计一次 +// 入参: commands 绘制指令, limit 缓存预算 +// 返回: int 估算字节数,超预算时返回预算加一 +func rasterCommandsCost(commands []RasterCommand, limit int) int { + limit = max(0, limit) + cost := 0 + var images map[image.Image]bool + for _, command := range commands { + part := rasterCommandCost(command) + if part < 0 || part > limit-cost { + return limit + 1 + } + cost += part + if command.Image == nil { + continue + } + if reflect.ValueOf(command.Image).Kind() == reflect.Pointer { + if images[command.Image] { + continue + } + if images == nil { + images = make(map[image.Image]bool) + } + images[command.Image] = true + } + part = imageResourceCost(command.Image, limit-cost) + if part > limit-cost { + return limit + 1 + } + cost += part + } + return cost +} diff --git a/ofdgo_render_compile.go b/ofdgo_render_compile.go index dda8797..28a0a2e 100644 --- a/ofdgo_render_compile.go +++ b/ofdgo_render_compile.go @@ -490,7 +490,7 @@ func (c *semanticCompiler) fillPattern(clip GeometryPath, pattern *PatternPaint, if err != nil { return err } - if err := c.appendPatternCell(commands, clip, segments); err != nil { + if err := c.appendClippedCommands(commands, clip, segments); err != nil { return err } } diff --git a/ofdgo_render_compile_image.go b/ofdgo_render_compile_image.go index 3cc987f..5a74b50 100644 --- a/ofdgo_render_compile_image.go +++ b/ofdgo_render_compile_image.go @@ -64,23 +64,14 @@ func (c *semanticCompiler) imageObject(object ImageObject, state RenderState) er if err != nil { return err } - if object.ImageMask != "" || object.Alpha != nil && *object.Alpha < 255 { - img, err = imagePixelData(img) - if err != nil { - return err - } + mask, err := c.renderer.imageObjectMask(object, img) + if err != nil { + return err } - if object.ImageMask != "" { - mask, err := c.renderer.ImageResource(object.ImageMask) - if err == nil { - mask, err = imagePixelData(mask) - } - if err != nil { - return err - } - img = imageWithMask(img, mask) + img, err = imageWithEffects(c.renderer.pageContext, img, mask, object.Alpha) + if err != nil { + return err } - img = imageWithAlpha(img, object.Alpha) bounds := img.Bounds() matrix = matrix.Multiply(Matrix{a: 1 / float64(bounds.Dx()), d: 1 / float64(bounds.Dy())}).Multiply(TranslationMatrix(-float64(bounds.Min.X), -float64(bounds.Min.Y))) if err := c.addImage(img, matrix, clip); err != nil { @@ -187,18 +178,20 @@ func (c *semanticCompiler) drawStampContent(stamp Stamp) error { if err != nil { return err } + matrix := TranslationMatrix(stamp.Box.X, stamp.Box.Y).Multiply(Matrix{a: stamp.Box.W / page.Width, d: stamp.Box.H / page.Height}) + var clip GeometryPath + var segments []RasterSegment if stamp.Clip != nil { - if renderer.backends.Raster == nil { - return fmt.Errorf("stamp raster: %w", ErrBackendUnavailable) - } - img, err := renderer.backends.Raster.Render(page) + clip = geometryRectangle(Box{X: stamp.Box.X + stamp.Clip.X, Y: stamp.Box.Y + stamp.Clip.Y, W: stamp.Clip.W, H: stamp.Clip.H}) + segments, err = c.segments(clip) if err != nil { return err } - return c.stampImage(stampImageWithTransparentWhite(img), stamp) } - matrix := TranslationMatrix(stamp.Box.X, stamp.Box.Y).Multiply(Matrix{a: stamp.Box.W / page.Width, d: stamp.Box.H / page.Height}) for _, command := range page.Commands { + if err := ctx.Err(); err != nil { + return err + } command.Transform = RasterMatrix(matrix.Multiply(MatrixFromValues([6]float64(command.Transform))).Values()) if command.Stroke != nil { stroke := *command.Stroke @@ -221,7 +214,13 @@ func (c *semanticCompiler) drawStampContent(stamp Stamp) error { command.Clip[i] = segment } } - c.page.Commands = append(c.page.Commands, command) + if stamp.Clip != nil { + if err := c.appendClippedCommands([]RasterCommand{command}, clip, segments); err != nil { + return err + } + } else { + c.page.Commands = append(c.page.Commands, command) + } } } return nil @@ -230,22 +229,27 @@ func (c *semanticCompiler) drawStampContent(stamp Stamp) error { if err != nil { return err } - return c.stampImage(stampImageWithTransparentWhite(img), stamp) + return c.stampImage(img, stamp) } // stampImage 按印章裁剪区域放置图像 // 入参: img 印章图像, stamp 印章 // 返回: error 编译错误 func (c *semanticCompiler) stampImage(img image.Image, stamp Stamp) error { + pixels, err := imagePixelDataContext(c.renderer.pageContext, img) + if err != nil { + return err + } + if c.renderer.decodeImages { + img = pixels + } box := stamp.Box + var clip *GeometryPath if stamp.Clip != nil { - img = clipStampImage(img, box, *stamp.Clip) - box.X += stamp.Clip.X - box.Y += stamp.Clip.Y - box.W = stamp.Clip.W - box.H = stamp.Clip.H + path := geometryRectangle(Box{X: box.X + stamp.Clip.X, Y: box.Y + stamp.Clip.Y, W: stamp.Clip.W, H: stamp.Clip.H}) + clip = &path } bounds := img.Bounds() matrix := TranslationMatrix(box.X, box.Y).Multiply(Matrix{a: box.W / float64(bounds.Dx()), d: box.H / float64(bounds.Dy())}).Multiply(TranslationMatrix(-float64(bounds.Min.X), -float64(bounds.Min.Y))) - return c.addImage(img, matrix, nil) + return c.addImage(img, matrix, clip) } diff --git a/ofdgo_render_pattern.go b/ofdgo_render_pattern.go index 12d96a7..f82d98f 100644 --- a/ofdgo_render_pattern.go +++ b/ofdgo_render_pattern.go @@ -96,21 +96,7 @@ func (c *semanticCompiler) sharedStamp(stamp Stamp, compile func(*semanticCompil return err } commands = page.Commands - cost := 0 - for _, command := range commands { - part := rasterCommandCost(command) - if part < 0 || part > state.cache.limit-cost { - cost = state.cache.limit + 1 - break - } - cost += part - if command.Image != nil { - cost += imageResourceCost(command.Image, state.cache.limit-cost) - } - if cost > state.cache.limit { - break - } - } + cost := rasterCommandsCost(commands, state.cache.limit) if err := ctx.Err(); err != nil { return err } @@ -158,10 +144,7 @@ func (c *semanticCompiler) patternCell(pattern *PatternPaint, matrix Matrix) ([] return nil, err } } - cost := 0 - for _, cmd := range child.page.Commands { - cost += rasterCommandCost(cmd) - } + cost := rasterCommandsCost(child.page.Commands, c.patterns.limit) c.patterns.put(key, child.page.Commands, cost) return translatePatternCommands(child.page.Commands, x, y), nil } @@ -226,10 +209,10 @@ func translatePatternCommands(commands []RasterCommand, x, y float64) []RasterCo return result } -// appendPatternCell 将缓存指令放入当前裁剪,不修改单元数据 -// 入参: commands 单元指令, clip 页面裁剪, segments 已转换裁剪 +// appendClippedCommands 合并页面裁剪后追加指令,不修改原始指令 +// 入参: commands 绘制指令, clip 页面裁剪, segments 已转换裁剪 // 返回: error 裁剪合并错误 -func (c *semanticCompiler) appendPatternCell(commands []RasterCommand, clip GeometryPath, segments []RasterSegment) error { +func (c *semanticCompiler) appendClippedCommands(commands []RasterCommand, clip GeometryPath, segments []RasterSegment) error { for _, command := range commands { commandClip := segments if command.Clip != nil { diff --git a/ofdgo_renderer_compile_canvas.go b/ofdgo_renderer_compile_canvas.go index bb3deb1..2e6058c 100644 --- a/ofdgo_renderer_compile_canvas.go +++ b/ofdgo_renderer_compile_canvas.go @@ -104,14 +104,22 @@ func (c *canvasPageCompiler) RenderImage(img image.Image, m canvas.Matrix) { if c.err != nil { return } - img = imagePixelSource(img) + var err error + img, err = imagePixelData(img) + if err != nil { + c.err = err + return + } if (m[0][1] != 0 || m[1][0] != 0) && (m[0][0] != 0 || m[1][1] == 0) { bounds := img.Bounds() img = &rasterPaddedImage{source: img, rect: image.Rect(0, 0, bounds.Dx()+8, bounds.Dy()+8)} m = m.Translate(-4, -4) } - h := float64(img.Bounds().Dy()) + bounds := img.Bounds() + h := float64(bounds.Dy()) t := RasterMatrix{m[0][0], -m[1][0], -m[0][1], m[1][1], m[0][2] + m[0][1]*h, c.page.Height - m[1][2] - m[1][1]*h} + t[4] -= t[0]*float64(bounds.Min.X) + t[2]*float64(bounds.Min.Y) + t[5] -= t[1]*float64(bounds.Min.X) + t[3]*float64(bounds.Min.Y) c.page.Commands = append(c.page.Commands, RasterCommand{Image: img, Transform: t}) } @@ -119,26 +127,33 @@ func (c *canvasPageCompiler) RenderImage(img image.Image, m canvas.Matrix) { // 返回: image.Rectangle 图像边界 func (s *rasterPaddedImage) Bounds() image.Rectangle { return s.rect } -// ColorModel 返回预乘八位颜色模型,与实体补边图像保持一致 +// ColorModel 按原图采样精度返回支持透明补边的预乘颜色模型 // 返回: color.Model 颜色模型 -func (s *rasterPaddedImage) ColorModel() color.Model { return color.RGBAModel } +func (s *rasterPaddedImage) ColorModel() color.Model { + if imageHighPrecision(s.source) { + return color.RGBA64Model + } + return color.RGBAModel +} // At 读取补边图像的预乘像素,边距及图像外透明 // 入参: x 横向坐标, y 纵向坐标 // 返回: color.Color 像素颜色 -func (s *rasterPaddedImage) At(x, y int) color.Color { - if !image.Pt(x, y).In(s.rect.Inset(4)) { - return color.RGBA{} - } - b := s.source.Bounds() - return color.RGBAModel.Convert(s.source.At(x-4+b.Min.X, y-4+b.Min.Y)) -} +func (s *rasterPaddedImage) At(x, y int) color.Color { return s.RGBA64At(x, y) } -// RGBA64At 返回预乘十六位像素供采样器读取 +// RGBA64At 保留原始预乘精度,支持采样接口的图像不进行颜色装箱 // 入参: x 横向坐标, y 纵向坐标 // 返回: color.RGBA64 像素颜色 func (s *rasterPaddedImage) RGBA64At(x, y int) color.RGBA64 { - r, g, b, a := s.At(x, y).RGBA() + if !image.Pt(x, y).In(s.rect.Inset(4)) { + return color.RGBA64{} + } + bounds := s.source.Bounds() + x, y = x-4+bounds.Min.X, y-4+bounds.Min.Y + if source, ok := s.source.(image.RGBA64Image); ok { + return source.RGBA64At(x, y) + } + r, g, b, a := s.source.At(x, y).RGBA() return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)} } diff --git a/ofdgo_renderer_image_canvas.go b/ofdgo_renderer_image_canvas.go index 5f6acde..a3a5180 100644 --- a/ofdgo_renderer_image_canvas.go +++ b/ofdgo_renderer_image_canvas.go @@ -16,6 +16,7 @@ package ofdgo import ( "bytes" + "context" "fmt" "image" "math" @@ -44,25 +45,16 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float return } box, _ := ParseBox(obj.Boundary) - if obj.ImageMask != "" || obj.Alpha != nil && *obj.Alpha < 255 { - img, err = imagePixelData(img) - if err != nil { - r.renderError = err - return - } + mask, err := r.imageObjectMask(obj, img) + if err != nil { + r.renderError = err + return } - if obj.ImageMask != "" { - mask, err := r.ImageResource(obj.ImageMask) - if err == nil { - mask, err = imagePixelData(mask) - } - if err != nil { - r.renderError = err - return - } - img = imageWithMask(img, mask) + img, err = imageWithEffects(r.pageContext, img, mask, obj.Alpha) + if err != nil { + r.renderError = err + return } - img = imageWithAlpha(img, obj.Alpha) imgBounds := img.Bounds() imgW, imgH := float64(imgBounds.Dx()), float64(imgBounds.Dy()) if imgW <= 0 || imgH <= 0 { @@ -91,6 +83,19 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float } } clipPath := r.intersectCachedClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, obj.Boundary, localCTM, parentCTM, boundaryInCTM)) + r.drawClippedImage(ctx, img, clipPath, m) + if r.renderError != nil { + return + } + if obj.Border != nil { + r.renderImageBorder(ctx, obj, box, pageH, parentCTM, boundaryInCTM, clipPath) + } +} + +// drawClippedImage 共用图像及签章的几何裁剪,编译输出保留原图和独立裁剪路径 +// 入参: ctx 画布上下文, img 原图, clipPath 页面裁剪,可为空, m 像素到画布的变换 +func (r *Renderer) drawClippedImage(ctx *canvas.Context, img image.Image, clipPath *canvas.Path, m canvas.Matrix) { + imgW, imgH := float64(img.Bounds().Dx()), float64(img.Bounds().Dy()) imageClip := clipPath compiler, compiled := ctx.Renderer.(*canvasPageCompiler) if compiled && imageClip != nil && imageClipContains(img, imageClip, m) { @@ -105,7 +110,12 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float m = m.Scale(imgW/float64(img.Bounds().Dx()), imgH/float64(img.Bounds().Dy())) if _, pdf := ctx.Renderer.(*pdfRenderer); !pdf { var pad int - img, pad = imageWithTransparentEdge(img) + var err error + img, pad, err = imageWithTransparentEdge(r.pageContext, img) + if err != nil { + r.renderError = err + return + } if pad > 0 { p := float64(pad) m[0][2] -= m[0][0]*p + m[0][1]*p @@ -122,9 +132,6 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float } compiler.page.Commands[len(compiler.page.Commands)-1].Clip = segments } - if obj.Border != nil { - r.renderImageBorder(ctx, obj, box, pageH, parentCTM, boundaryInCTM, clipPath) - } } // canvasEncodedImage 在Canvas输出边界保留原始编码,不重新压缩图片 @@ -175,11 +182,19 @@ func imageClipContains(img image.Image, clipPath *canvas.Path, m canvas.Matrix) return clipPath.Contains(outline) } -// imageWithClip 应用图片裁剪区域 +// imageWithClip 应用图片裁剪区域,保留原图采样精度 // 低分辨率图片按最低精度细化蒙版,细化后的像素上限16Mi,不降低原图分辨率 // 入参: img 图片对象, clipPath 裁剪路径, m 图片变换矩阵, dpi 最低蒙版分辨率,0保留原图精度 // 返回: image.Image 裁剪后的图片对象 func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canvas.Matrix, dpi float64) image.Image { + work := r.pageContext + if work == nil { + work = context.Background() + } + if err := work.Err(); err != nil { + r.renderError = err + return img + } if img == nil || clipPath == nil || m.Det() == 0 { return img } @@ -192,7 +207,7 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva return img } original := img - source, err := imagePixelData(img) + source, err := imagePixelDataContext(r.pageContext, img) if err != nil { r.renderError = err return img @@ -202,12 +217,21 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva scale = math.Min(scale, math.Sqrt((16<<20)/(float64(w)*float64(h)))) if scale > 1 { nw, nh := int(float64(w)*scale), int(float64(h)*scale) - resized := image.NewNRGBA(image.Rect(0, 0, nw, nh)) - draw.CatmullRom.Scale(resized, resized.Bounds(), imagePixelSource(img), bounds, draw.Src, nil) + var resized draw.Image + if imageHighPrecision(img) { + resized = image.NewNRGBA64(image.Rect(0, 0, nw, nh)) + } else { + resized = image.NewNRGBA(image.Rect(0, 0, nw, nh)) + } + draw.CatmullRom.Scale(resized, resized.Bounds(), img, bounds, draw.Src, nil) img = resized m = m.Scale(float64(w)/float64(nw), float64(h)/float64(nh)) bounds, w, h = resized.Bounds(), nw, nh } + if err := work.Err(); err != nil { + r.renderError = err + return img + } clip := clipPath.Copy().Transform(m.Inv()) compiler := &canvasPageCompiler{page: &RasterPage{Width: float64(w), Height: float64(h), DPI: 25.4}, geometry: r.backends.Geometry} ctx := canvas.NewContext(compiler) @@ -223,16 +247,67 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva return img } result, err := r.backends.Raster.Render(compiler.page) + if cancelErr := work.Err(); cancelErr != nil { + err = cancelErr + } if err != nil { r.renderError = err return img } + if result == nil || result.Bounds().Size() != image.Pt(w, h) { + r.renderError = fmt.Errorf("image clip raster dimensions differ from source") + return img + } + result, err = imagePixelDataContext(work, result) + if err != nil { + r.renderError = err + return img + } + if imageHighPrecision(img) || imageHighPrecision(result) { + out := image.NewNRGBA64(bounds) + maskBounds := result.Bounds() + opaque := true + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + if err := work.Err(); err != nil { + r.renderError = err + return img + } + for x := bounds.Min.X; x < bounds.Max.X; x++ { + pixel := imageNRGBA64At(img, x, y) + alpha := imageAlphaAt(result, maskBounds.Min.X+x-bounds.Min.X, maskBounds.Min.Y+y-bounds.Min.Y) + opaque = opaque && alpha == 65535 + pixel.A = uint16(uint32(pixel.A) * alpha / 65535) + out.SetNRGBA64(x, y, pixel) + } + } + if err := work.Err(); err != nil { + r.renderError = err + return img + } + if opaque { + if scale <= 1 { + return original + } + return img + } + return out + } mask, ok := result.(*image.RGBA) if !ok { mask = image.NewRGBA(image.Rect(0, 0, w, h)) draw.Draw(mask, mask.Bounds(), result, result.Bounds().Min, draw.Src) } - if mask.Opaque() { + if mask.Rect.Min != (image.Point{}) { + normalized := *mask + normalized.Rect = mask.Rect.Sub(mask.Rect.Min) + mask = &normalized + } + opaque := mask.Opaque() + if err := work.Err(); err != nil { + r.renderError = err + return img + } + if opaque { if scale <= 1 { return original } @@ -242,6 +317,10 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva out := &image.NRGBA{Pix: mask.Pix, Stride: mask.Stride, Rect: bounds} if src, ok := source.(*image.NRGBA); ok { for y := 0; y < h; y++ { + if err := work.Err(); err != nil { + r.renderError = err + return img + } offset := src.PixOffset(bounds.Min.X, bounds.Min.Y+y) row := out.Pix[y*out.Stride : y*out.Stride+w*4] pixels := src.Pix[offset : offset+w*4] @@ -250,9 +329,17 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva row[x+3] = uint8(int(pixels[x+3]) * int(row[x+3]) / 255) } } + if err := work.Err(); err != nil { + r.renderError = err + return img + } return out } for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + if err := work.Err(); err != nil { + r.renderError = err + return img + } for x := bounds.Min.X; x < bounds.Max.X; x++ { c := imageNRGBAAt(source, x, y) a := mask.RGBAAt(x-bounds.Min.X, y-bounds.Min.Y).A @@ -260,5 +347,9 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva out.SetNRGBA(x, y, c) } } + if err := work.Err(); err != nil { + r.renderError = err + return img + } return out } diff --git a/ofdgo_renderer_pdf_write.go b/ofdgo_renderer_pdf_write.go index eec3fae..a5f4897 100644 --- a/ofdgo_renderer_pdf_write.go +++ b/ofdgo_renderer_pdf_write.go @@ -164,7 +164,7 @@ func (r *pdfRenderer) writePDF(ctx context.Context, data []byte, writer io.Write if err := ctx.Err(); err != nil { return err } - pixels, err := imagePixelData(original) + pixels, err := imagePixelDataContext(ctx, original) if err != nil { return err } diff --git a/ofdgo_renderer_stamp_canvas.go b/ofdgo_renderer_stamp_canvas.go index 3446894..59d859d 100644 --- a/ofdgo_renderer_stamp_canvas.go +++ b/ofdgo_renderer_stamp_canvas.go @@ -44,7 +44,7 @@ func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) { if s.Type == "ofd" && len(s.Data) > 0 { if s.Clip != nil { if img := r.renderOFDStampImage(s.Data); img != nil { - r.renderStampImage(ctx, stampImageWithTransparentWhite(img), s, pageH) + r.renderStampImage(ctx, img, s, pageH) } return } @@ -60,11 +60,13 @@ func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) { if pageContext.Err() != nil { return } - continue + r.renderError = err + return } sealBox, err := renderer.GetPageBox(content) if err != nil { - continue + r.renderError = err + return } ctx.Push() ctx.Translate(s.Box.X, pageH-(s.Box.Y+s.Box.H)) @@ -78,17 +80,19 @@ func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) { } return } + r.renderError = err + return } + r.renderError = err + return } if len(s.Data) > 0 { img, _, err := decodeImageDataContext(pageContext, s.Data) - if err == nil { - if r.decodeImages { - img = imagePixelSource(img) - } - r.renderStampImage(ctx, stampImageWithTransparentWhite(img), s, pageH) + if err != nil { + r.renderError = err return } + r.renderStampImage(ctx, img, s, pageH) } } @@ -111,11 +115,13 @@ func (r *Renderer) renderOFDStampImage(data []byte) image.Image { } reader, err := NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { + r.renderError = err return nil } defer reader.Close() doc, err := reader.DocContext(ctx) if err != nil { + r.renderError = err return nil } renderer := r.childRenderer(reader) @@ -127,7 +133,8 @@ func (r *Renderer) renderOFDStampImage(data []byte) image.Image { if ctx.Err() != nil { return nil } - continue + r.renderError = err + return nil } img, err := renderer.RenderToImage(content) if err != nil { @@ -142,17 +149,19 @@ func (r *Renderer) renderOFDStampImage(data []byte) image.Image { // renderStampImage 渲染印章图像 // 入参: ctx 画布上下文, img 印章图像, s 印章对象, pageH 页面高度 func (r *Renderer) renderStampImage(ctx *canvas.Context, img image.Image, s Stamp, pageH float64) { - box := s.Box - if s.Clip != nil { - img = clipStampImage(img, box, *s.Clip) - box.X += s.Clip.X - box.Y += s.Clip.Y - box.W = s.Clip.W - box.H = s.Clip.H + pixels, err := imagePixelDataContext(r.pageContext, img) + if err != nil { + r.renderError = err + return } - ctx.Push() - ctx.Translate(box.X, pageH-(box.Y+box.H)) - ctx.Scale(box.W/float64(img.Bounds().Dx()), box.H/float64(img.Bounds().Dy())) - ctx.DrawImage(0, 0, r.canvasEncodedImage(img), canvas.DPMM(1.0)) - ctx.Pop() + if r.decodeImages { + img = pixels + } + box := s.Box + var clip *canvas.Path + if s.Clip != nil { + clip = canvas.Rectangle(s.Clip.W, s.Clip.H).Translate(box.X+s.Clip.X, pageH-box.Y-s.Clip.Y-s.Clip.H) + } + matrix := canvas.Identity.Translate(box.X, pageH-box.Y-box.H).Scale(box.W/float64(img.Bounds().Dx()), box.H/float64(img.Bounds().Dy())) + r.drawClippedImage(ctx, img, clip, matrix) }