feat(字体图像): 优化字体图像并修正签章裁剪
build.yaml / build (push) Successful in 3m41s

This commit is contained in:
xiaoqidun committed 2026-10-12 23:04:11 +08:00
1 parent 5d1fd593cd
commit f8263f798f
21 files changed
+1029 -336

No files matched your search

+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+32 -4
View File
@@ -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 取消错误
+12 -2
View File
@@ -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
}
}
+16 -1
View File
@@ -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
+28 -12
View File
@@ -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
}
+8 -7
View File
@@ -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
+30
View File
@@ -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 错误信息
+234
View File
@@ -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...)
}
+5 -4
View File
@@ -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")
+225 -168
View File
@@ -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
}
+184 -14
View File
@@ -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
}
+1 -8
View File
@@ -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)
}
+36
View File
@@ -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
}
+1 -1
View File
@@ -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
}
}
+33 -29
View File
@@ -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)
}
+5 -22
View File
@@ -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 {
+28 -13
View File
@@ -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)}
}
+117 -26
View File
@@ -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
}
+1 -1
View File
@@ -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
}
+30 -21
View File
@@ -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)
}