mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:53:44 +08:00
This commit is contained in:
1 parent
5d1fd593cd
commit
f8263f798f
21 files changed
+1002
-309
No files matched your search
@@ -9,7 +9,7 @@ require (
|
|||||||
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
|
||||||
github.com/xiaoqidun/j2kgo v1.0.0
|
github.com/xiaoqidun/j2kgo v1.0.0
|
||||||
github.com/xiaoqidun/jbig2 v1.1.5
|
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/crypto v0.58.0
|
||||||
golang.org/x/image v0.47.0
|
golang.org/x/image v0.47.0
|
||||||
golang.org/x/text v0.43.0
|
golang.org/x/text v0.43.0
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ github.com/xiaoqidun/j2kgo v1.0.0 h1:n7F3s0H8/4x8ET0d65FGScRgTfiqbYuiiiJF/5HFI1I
|
|||||||
github.com/xiaoqidun/j2kgo v1.0.0/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
|
github.com/xiaoqidun/j2kgo v1.0.0/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.5 h1:QcqjDBe/Zp+JlxYtmhsiYG+Ft36jjPB6+INPlmaI/ls=
|
github.com/xiaoqidun/jbig2 v1.1.5 h1:QcqjDBe/Zp+JlxYtmhsiYG+Ft36jjPB6+INPlmaI/ls=
|
||||||
github.com/xiaoqidun/jbig2 v1.1.5/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA=
|
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-20261012150156-647733ab0dae h1:8iQdSdXA4sVofF+wIcmFJB63P5Uy4uQeltj2PpP/hzE=
|
||||||
github.com/xiaoqidun/pdfgo v0.0.0-20261012061033-90df32d37c4d/go.mod h1:UWsVkKkd37a3wEa/tGLUOSwccKGArwmrvEAYlmnEz0U=
|
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 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||||
|
|||||||
@@ -15,8 +15,11 @@
|
|||||||
package ofdgo
|
package ofdgo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"image"
|
"image"
|
||||||
|
"io"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@@ -24,6 +27,9 @@ import (
|
|||||||
// outputImageBatchLimit 限制并行批次持有的原始编码,超限图片单独处理
|
// outputImageBatchLimit 限制并行批次持有的原始编码,超限图片单独处理
|
||||||
const outputImageBatchLimit = 16 << 20
|
const outputImageBatchLimit = 16 << 20
|
||||||
|
|
||||||
|
// outputImageWorkingSetLimit 限制并行批次的像素工作量估算,超限图片单独处理
|
||||||
|
const outputImageWorkingSetLimit = 64 << 20
|
||||||
|
|
||||||
// outputImageJob 保存独立图片任务,不访问编辑文档或调用进度回调
|
// outputImageJob 保存独立图片任务,不访问编辑文档或调用进度回调
|
||||||
type outputImageJob struct {
|
type outputImageJob struct {
|
||||||
key outputImageKey
|
key outputImageKey
|
||||||
@@ -63,13 +69,13 @@ func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
batch := make([]outputImageJob, 0, workers)
|
batch := make([]outputImageJob, 0, workers)
|
||||||
size := 0
|
size, workingSet := 0, 0
|
||||||
run := func() error {
|
run := func() error {
|
||||||
if err := e.runOutputImages(batch, results); err != nil {
|
if err := e.runOutputImages(batch, results); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
clear(batch)
|
clear(batch)
|
||||||
batch, size = batch[:0], 0
|
batch, size, workingSet = batch[:0], 0, 0
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
@@ -101,14 +107,16 @@ func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, e
|
|||||||
if duplicate {
|
if duplicate {
|
||||||
continue
|
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 {
|
if err := run(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
batch = append(batch, outputImageJob{key: cacheKey, data: data})
|
batch = append(batch, outputImageJob{key: cacheKey, data: data})
|
||||||
size += len(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 {
|
if err := run(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -117,6 +125,26 @@ func (e *Editor) prepareOutputImages(keys []string, load func(string) ([]byte, e
|
|||||||
return run()
|
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 等待整批结束后记录更小候选,失败图片保留原始编码
|
// runOutputImages 等待整批结束后记录更小候选,失败图片保留原始编码
|
||||||
// 入参: jobs 独占图片任务, results 本次写出的候选缓存
|
// 入参: jobs 独占图片任务, results 本次写出的候选缓存
|
||||||
// 返回: error 取消错误
|
// 返回: error 取消错误
|
||||||
|
|||||||
+12
-2
@@ -678,6 +678,15 @@ func (e *Editor) ReplaceImage(page int, id string, data []byte, fit string) erro
|
|||||||
if object.Type != "ImageObject" {
|
if object.Type != "ImageObject" {
|
||||||
return fmt.Errorf("object %q is not an image", id)
|
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)
|
object.ImageObject.ResourceID, err = e.AddImage(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -863,7 +872,8 @@ func (v *editorValidation) prepareObject(id string, object GraphicObject) (Graph
|
|||||||
fill, stroke = obj.FillColor, (*FillColor)(obj.StrokeColor)
|
fill, stroke = obj.FillColor, (*FillColor)(obj.StrokeColor)
|
||||||
case "ImageObject":
|
case "ImageObject":
|
||||||
obj := &object.ImageObject
|
obj := &object.ImageObject
|
||||||
if _, err := e.editorImage(obj.ResourceID); err != nil {
|
size, err := e.editorImage(obj.ResourceID)
|
||||||
|
if err != nil {
|
||||||
return GraphicObject{}, err
|
return GraphicObject{}, err
|
||||||
}
|
}
|
||||||
if obj.Substitution != "" {
|
if obj.Substitution != "" {
|
||||||
@@ -872,7 +882,7 @@ func (v *editorValidation) prepareObject(id string, object GraphicObject) (Graph
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if obj.ImageMask != "" {
|
if obj.ImageMask != "" {
|
||||||
if _, err := e.editorImage(obj.ImageMask); err != nil {
|
if err := e.validateImageMask(obj.ImageMask, size); err != nil {
|
||||||
return GraphicObject{}, err
|
return GraphicObject{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
"maps"
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
)
|
)
|
||||||
@@ -113,12 +115,25 @@ func (e *Editor) OrderCompositeObjects(page int, path ObjectPath, indexes []int,
|
|||||||
// 入参: page 页面索引, path 父复合路径, index 图片序号, data PNG或JPEG数据
|
// 入参: page 页面索引, path 父复合路径, index 图片序号, data PNG或JPEG数据
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (e *Editor) ReplaceCompositeImage(page int, path ObjectPath, index int, data []byte) 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]
|
node := nodes[0]
|
||||||
if node.object.Type != "ImageObject" {
|
if node.object.Type != "ImageObject" {
|
||||||
return fmt.Errorf("composite member is not an image")
|
return fmt.Errorf("composite member is not an image")
|
||||||
}
|
}
|
||||||
object := node.object
|
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)
|
id, err := e.AddImage(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -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)
|
index = fontCollectionIndex(data, []string{declaration.font.FontName, declaration.font.FamilyName}, declaration.font.Bold, declaration.font.Italic)
|
||||||
}
|
}
|
||||||
} else if len(file.Data) == 0 {
|
} else if len(file.Data) == 0 {
|
||||||
file, err = e.resolveBindingFont(scan, declaration)
|
file, err = e.resolveBindingFont(ctx, scan, declaration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -302,7 +302,10 @@ func (e *Editor) prepareFontBinding(ctx context.Context, scan *editorFontDocumen
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if explicit && len(object.CGTransform) != 0 && !verifiedGlyphs {
|
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) {
|
if err != nil || !sameBindingFont(source.Data, parsed.Data) {
|
||||||
return nil, fmt.Errorf("font glyph mapping requires the original font")
|
return nil, fmt.Errorf("font glyph mapping requires the original font")
|
||||||
}
|
}
|
||||||
@@ -503,23 +506,33 @@ func sameBindingFont(source, target []byte) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolveBindingFont 精确匹配当前字体并提取独立字体数据,集合索引不传递到提取结果
|
// resolveBindingFont 精确匹配当前字体并提取独立字体数据,集合索引不传递到提取结果
|
||||||
// 入参: scan 文档引用, declaration 字体声明
|
// 同一文档复用入口和资源定义,字体解析结果不跨调用保留
|
||||||
|
// 入参: ctx 取消上下文, scan 文档引用, declaration 字体声明
|
||||||
// 返回: FontFile 独立字体, error 匹配或解析错误
|
// 返回: FontFile 独立字体, error 匹配或解析错误
|
||||||
func (e *Editor) resolveBindingFont(scan *editorFontDocument, declaration *editorFontDeclaration) (FontFile, error) {
|
func (e *Editor) resolveBindingFont(ctx context.Context, scan *editorFontDocument, declaration *editorFontDeclaration) (FontFile, error) {
|
||||||
reader := &Reader{Zip: scan.reader.Zip, files: maps.Clone(scan.reader.files), documentIndex: scan.index}
|
if err := ctx.Err(); 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 {
|
if scan.index == scan.reader.documentIndex {
|
||||||
reader.selectedVersion = scan.reader.selectedVersion
|
reader.selectedVersion = scan.reader.selectedVersion
|
||||||
}
|
}
|
||||||
if err := reader.initRoot(); err != nil {
|
if err := reader.initRootContext(ctx); err != nil {
|
||||||
return FontFile{}, err
|
return FontFile{}, err
|
||||||
}
|
}
|
||||||
if _, err := reader.Doc(); err != nil {
|
if _, err := reader.DocContext(ctx); err != nil {
|
||||||
return FontFile{}, err
|
return FontFile{}, err
|
||||||
}
|
}
|
||||||
|
scan.fontReader = reader
|
||||||
|
}
|
||||||
definition := declaration.font
|
definition := declaration.font
|
||||||
definition.FontFile = ""
|
definition.FontFile = ""
|
||||||
reader.fontCache[definition.ID] = &definition
|
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 {
|
if err != nil {
|
||||||
return FontFile{}, err
|
return FontFile{}, err
|
||||||
}
|
}
|
||||||
@@ -532,6 +545,9 @@ func (e *Editor) resolveBindingFont(scan *editorFontDocument, declaration *edito
|
|||||||
index = resolved.Face.Index
|
index = resolved.Face.Index
|
||||||
}
|
}
|
||||||
data, err := file.Face(index)
|
data, err := file.Face(index)
|
||||||
|
if cancelErr := ctx.Err(); cancelErr != nil {
|
||||||
|
return FontFile{}, cancelErr
|
||||||
|
}
|
||||||
return FontFile{Data: data}, err
|
return FontFile{Data: data}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ type editorFontDocument struct {
|
|||||||
fonts map[string]*editorFontDeclaration
|
fonts map[string]*editorFontDeclaration
|
||||||
texts map[string][]editorFontText
|
texts map[string][]editorFontText
|
||||||
fontCache editorBindingFontCache
|
fontCache editorBindingFontCache
|
||||||
|
fontReader *Reader
|
||||||
unsafe bool
|
unsafe bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -957,6 +957,36 @@ func (e *Editor) editorImage(id string) (image.Point, error) {
|
|||||||
return size, nil
|
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 编码可编辑对象,包含独立命名空间声明
|
// editorObjectXML 编码可编辑对象,包含独立命名空间声明
|
||||||
// 入参: object 对象
|
// 入参: object 对象
|
||||||
// 返回: []byte XML片段, error 错误信息
|
// 返回: []byte XML片段, error 错误信息
|
||||||
|
|||||||
@@ -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...)
|
||||||
|
}
|
||||||
@@ -232,7 +232,7 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
|
|||||||
var unicodeTables []sfntCmap
|
var unicodeTables []sfntCmap
|
||||||
for _, record := range sfnt.cmaps {
|
for _, record := range sfnt.cmaps {
|
||||||
if record.format == 14 {
|
if record.format == 14 {
|
||||||
return nil
|
continue
|
||||||
}
|
}
|
||||||
if record.platform == 0 || record.platform == 3 && (record.encoding == 1 || record.encoding == 10) {
|
if record.platform == 0 || record.platform == 3 && (record.encoding == 1 || record.encoding == 10) {
|
||||||
unicodeTables = append(unicodeTables, record)
|
unicodeTables = append(unicodeTables, record)
|
||||||
@@ -256,7 +256,8 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
|
|||||||
mapping[char] = id
|
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
|
return nil
|
||||||
}
|
}
|
||||||
if sfnt.IsCFF {
|
if sfnt.IsCFF {
|
||||||
@@ -270,7 +271,7 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
|
|||||||
}
|
}
|
||||||
tables := maps.Clone(sfnt.Tables)
|
tables := maps.Clone(sfnt.Tables)
|
||||||
tables["CFF "] = cff
|
tables["CFF "] = cff
|
||||||
tables["cmap"] = buildCmapTable(sfnt.NumGlyphs(), mapping)
|
tables["cmap"] = buildCmapWithVariations(sfnt.NumGlyphs(), mapping, variations)
|
||||||
delete(tables, "DSIG")
|
delete(tables, "DSIG")
|
||||||
result, err := serializeOTF(tables)
|
result, err := serializeOTF(tables)
|
||||||
if err != nil || len(result) >= len(data) {
|
if err != nil || len(result) >= len(data) {
|
||||||
@@ -307,7 +308,7 @@ func subsetSourceSFNTFont(data []byte, usage *editorFontUsage) []byte {
|
|||||||
}
|
}
|
||||||
tables := maps.Clone(sfnt.Tables)
|
tables := maps.Clone(sfnt.Tables)
|
||||||
tables["glyf"], tables["loca"] = glyf, loca
|
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"])
|
tables["head"] = bytes.Clone(tables["head"])
|
||||||
clear(tables["head"][8:12])
|
clear(tables["head"][8:12])
|
||||||
delete(tables, "DSIG")
|
delete(tables, "DSIG")
|
||||||
|
|||||||
+221
-164
@@ -23,18 +23,16 @@ import (
|
|||||||
"image/color"
|
"image/color"
|
||||||
_ "image/gif"
|
_ "image/gif"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
|
||||||
|
|
||||||
_ "github.com/xiaoqidun/j2kgo"
|
_ "github.com/xiaoqidun/j2kgo"
|
||||||
_ "github.com/xiaoqidun/jbig2"
|
_ "github.com/xiaoqidun/jbig2"
|
||||||
_ "golang.org/x/image/bmp"
|
_ "golang.org/x/image/bmp"
|
||||||
"golang.org/x/image/draw"
|
|
||||||
"golang.org/x/image/tiff"
|
"golang.org/x/image/tiff"
|
||||||
)
|
)
|
||||||
|
|
||||||
// transparentEdgeImage 共享只读像素并按需计算透明边缘和采样边距
|
// transparentEdgeImage 共享只读像素并按需计算透明边缘和采样边距
|
||||||
type transparentEdgeImage struct {
|
type transparentEdgeImage struct {
|
||||||
source *image.NRGBA
|
source image.Image
|
||||||
rect image.Rectangle
|
rect image.Rectangle
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +59,7 @@ func (s *transparentEdgeImage) NRGBAAt(x, y int) color.NRGBA {
|
|||||||
}
|
}
|
||||||
b := s.source.Bounds()
|
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)
|
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 c.A == 0 {
|
||||||
if edge, ok := transparentEdgeColor(s.source, sx, sy); ok {
|
if edge, ok := transparentEdgeColor(s.source, sx, sy); ok {
|
||||||
c = edge
|
c = edge
|
||||||
@@ -352,86 +350,136 @@ func parseBMPHeader(data []byte) (int, int, int, uint16, uint32, error) {
|
|||||||
return offset, width, height, bpp, compression, nil
|
return offset, width, height, bpp, compression, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// imageWithMask 应用图片蒙版
|
// imageWithEffects 合并同尺寸二值蒙版和对象透明度,保留原图采样精度及只读像素
|
||||||
// 入参: img 图片对象, mask 蒙版图片
|
// 入参: ctx 取消上下文,nil使用后台上下文, img 原图, mask 二值蒙版,可为空, alpha 对象透明度,可为空
|
||||||
// 返回: image.Image 蒙版处理后的图片对象
|
// 返回: image.Image 处理后的图片, error 蒙版、解码或取消错误
|
||||||
func imageWithMask(img, mask image.Image) image.Image {
|
func imageWithEffects(ctx context.Context, img, mask image.Image, alpha *int) (image.Image, error) {
|
||||||
if img == nil || mask == nil {
|
if ctx == nil {
|
||||||
return img
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
bounds := img.Bounds()
|
if err := ctx.Err(); err != nil {
|
||||||
if bounds.Empty() || mask.Bounds().Empty() {
|
return nil, err
|
||||||
return img
|
|
||||||
}
|
}
|
||||||
source := imagePixelSource(img)
|
a := 255
|
||||||
maskSource := imagePixelSource(mask)
|
if alpha != nil {
|
||||||
maskBounds := maskSource.Bounds()
|
a = clampColor(*alpha)
|
||||||
var opacity image.Image = maskSource
|
}
|
||||||
if bounds.Size() != maskBounds.Size() {
|
if img == nil || mask == nil && a == 255 {
|
||||||
resized := image.NewGray(bounds)
|
return img, nil
|
||||||
draw.CatmullRom.Scale(resized, bounds, maskSource, maskBounds, draw.Src, nil)
|
}
|
||||||
opacity = resized
|
source, err := imagePixelDataContext(ctx, img)
|
||||||
maskBounds = bounds
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
out := image.NewNRGBA(bounds)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||||
|
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++ {
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||||
mx := maskBounds.Min.X + x - bounds.Min.X
|
out.SetNRGBA(x, y, imageNRGBAAt(source, x, y))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// imageWithAlpha 合并图片透明度
|
// imageMaskAlpha 读取二值蒙版,拒绝灰阶、彩色及带透明度的像素
|
||||||
// 入参: img 图片对象, alpha 对象透明度
|
// 入参: mask 蒙版图像, x 横坐标, y 纵坐标
|
||||||
// 返回: image.Image 合并后的图片对象
|
// 返回: uint8 透明度,黑色为0、白色为255, error 非二值像素错误
|
||||||
func imageWithAlpha(img image.Image, alpha *int) image.Image {
|
func imageMaskAlpha(mask image.Image, x, y int) (uint8, error) {
|
||||||
if img == nil || alpha == nil {
|
var r, g, b, a uint32
|
||||||
return img
|
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 != 65535 || r != g || r != b || r != 0 && r != 65535 {
|
||||||
if a == 255 {
|
return 0, fmt.Errorf("image mask contains non-binary pixel at (%d, %d)", x, y)
|
||||||
return img
|
|
||||||
}
|
}
|
||||||
bounds := img.Bounds()
|
return uint8(r >> 8), nil
|
||||||
out := image.NewNRGBA(bounds)
|
}
|
||||||
source := imagePixelSource(img)
|
|
||||||
if src, ok := source.(*image.NRGBA); ok {
|
// validateImageMask 校验二值蒙版尺寸及像素,不分配合成图像
|
||||||
for y := 0; y < bounds.Dy(); y++ {
|
// 入参: ctx 取消上下文, mask 蒙版图像, size 原图尺寸
|
||||||
offset := src.PixOffset(bounds.Min.X, bounds.Min.Y+y)
|
// 返回: error 解码、尺寸、像素或取消错误
|
||||||
row := out.Pix[y*out.Stride : (y+1)*out.Stride]
|
func validateImageMask(ctx context.Context, mask image.Image, size image.Point) error {
|
||||||
copy(row, src.Pix[offset:offset+len(row)])
|
if mask.Bounds().Size() != size || size.X <= 0 || size.Y <= 0 {
|
||||||
for x := 3; x < len(row); x += 4 {
|
return fmt.Errorf("image mask dimensions differ from source")
|
||||||
row[x] = uint8(int(row[x]) * a / 255)
|
|
||||||
}
|
}
|
||||||
|
mask, err := imagePixelDataContext(ctx, mask)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return out
|
bounds := mask.Bounds()
|
||||||
}
|
|
||||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
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++ {
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||||
c := imageNRGBAAt(source, x, y)
|
if _, err := imageMaskAlpha(mask, x, y); err != nil {
|
||||||
c.A = uint8(int(c.A) * a / 255)
|
return err
|
||||||
out.SetNRGBA(x, y, c)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
}
|
||||||
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// imagePixelSource 获取图片像素源
|
// imagePixelSource 获取图片像素源
|
||||||
@@ -454,6 +502,28 @@ func imagePixelData(img image.Image) (image.Image, error) {
|
|||||||
return img, nil
|
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 获取图片灰度像素
|
// imageGrayAt 获取图片灰度像素
|
||||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
// 入参: img 图片对象, x X坐标, y Y坐标
|
||||||
// 返回: color.Gray 灰度像素
|
// 返回: color.Gray 灰度像素
|
||||||
@@ -509,6 +579,14 @@ func imageAlphaAt(img image.Image, x, y int) uint32 {
|
|||||||
return alpha
|
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 读取十六位非预乘像素,原生图像避免颜色装箱
|
// imageNRGBA64At 读取十六位非预乘像素,原生图像避免颜色装箱
|
||||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
// 入参: img 图片对象, x X坐标, y Y坐标
|
||||||
// 返回: color.NRGBA64 NRGBA64像素
|
// 返回: color.NRGBA64 NRGBA64像素
|
||||||
@@ -545,9 +623,34 @@ func imageNRGBA64At(img image.Image, x, y int) color.NRGBA64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// transparentEdgeColor 获取透明像素相邻的可见颜色
|
// transparentEdgeColor 获取透明像素相邻的可见颜色
|
||||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
// 入参: source 图片对象, x X坐标, y Y坐标
|
||||||
// 返回: color.NRGBA 颜色, bool 是否存在
|
// 返回: 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()
|
bounds := img.Bounds()
|
||||||
best, alpha := 0, uint8(0)
|
best, alpha := 0, uint8(0)
|
||||||
left, right := max(x-1, bounds.Min.X), min(x+2, bounds.Max.X)
|
left, right := max(x-1, bounds.Min.X), min(x+2, bounds.Max.X)
|
||||||
@@ -578,123 +681,70 @@ func transparentPaddingColor(c color.NRGBA) color.NRGBA {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// clipStampImage 裁剪印章图像
|
// imageWithTransparentEdge 扫描透明边缘并共享只读像素,保留原图采样精度
|
||||||
// 入参: img 印章图像, box 印章区域, clip 裁剪区域
|
// 入参: ctx 取消上下文,nil使用后台上下文, img 图片对象
|
||||||
// 返回: image.Image 裁剪后的印章图像
|
// 返回: image.Image 补边图像, int 补边像素数, error 解码或取消错误
|
||||||
func clipStampImage(img image.Image, box, clip Box) image.Image {
|
func imageWithTransparentEdge(ctx context.Context, img image.Image) (image.Image, int, error) {
|
||||||
bounds := img.Bounds()
|
if ctx == nil {
|
||||||
x0 := int(math.Floor(clip.X / box.W * float64(bounds.Dx())))
|
ctx = context.Background()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
bounds := img.Bounds()
|
if err := ctx.Err(); err != nil {
|
||||||
out := image.NewNRGBA(bounds)
|
return nil, 0, err
|
||||||
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 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 {
|
if img == nil {
|
||||||
return img, 0
|
return img, 0, nil
|
||||||
}
|
}
|
||||||
bounds := img.Bounds()
|
bounds := img.Bounds()
|
||||||
w, h := bounds.Dx(), bounds.Dy()
|
w, h := bounds.Dx(), bounds.Dy()
|
||||||
if w == 0 || h == 0 {
|
if w == 0 || h == 0 {
|
||||||
return img, 0
|
return img, 0, nil
|
||||||
}
|
}
|
||||||
if src, ok := img.(*EncodedImage); ok && src.MIME() == "image/jpeg" {
|
if src, ok := img.(*EncodedImage); ok && src.MIME() == "image/jpeg" {
|
||||||
return img, 0
|
return img, 0, nil
|
||||||
}
|
}
|
||||||
source := imagePixelSource(img)
|
source, err := imagePixelDataContext(ctx, img)
|
||||||
if opaque, ok := source.(interface{ Opaque() bool }); ok && opaque.Opaque() {
|
if err != nil {
|
||||||
return img, 0
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
if model := source.ColorModel(); model == color.NRGBA64Model || model == color.RGBA64Model || model == color.Alpha16Model || model == color.Gray16Model {
|
switch source.(type) {
|
||||||
return imageWithTransparentEdge64(img, source)
|
case *image.Gray, *image.Gray16, *image.YCbCr, *image.CMYK:
|
||||||
|
return img, 0, nil
|
||||||
}
|
}
|
||||||
hasZero, hasVisible := false, false
|
hasZero, hasVisible := false, false
|
||||||
src, ok := source.(*image.NRGBA)
|
var pixels []byte
|
||||||
if ok {
|
var stride, step int
|
||||||
srcBounds := src.Bounds()
|
switch src := source.(type) {
|
||||||
hasZero = src.Pix[3] == 0
|
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
|
hasVisible = !hasZero
|
||||||
scan:
|
scan:
|
||||||
for y := srcBounds.Min.Y; y < srcBounds.Max.Y; y++ {
|
for y := 0; y < h; y++ {
|
||||||
offset := src.PixOffset(srcBounds.Min.X, y) + 3
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
offset := y*stride + step - 1
|
||||||
for x := 0; x < w; x++ {
|
for x := 0; x < w; x++ {
|
||||||
if (src.Pix[offset] == 0) != hasZero {
|
if (pixels[offset] == 0) != hasZero {
|
||||||
hasZero, hasVisible = true, true
|
hasZero, hasVisible = true, true
|
||||||
break scan
|
break scan
|
||||||
}
|
}
|
||||||
offset += 4
|
offset += step
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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 {
|
|
||||||
hasZero = true
|
|
||||||
} else {
|
|
||||||
hasVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 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++ {
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||||
_, _, _, a := source.At(x, y).RGBA()
|
if imageAlphaAt(source, x, y) == 0 {
|
||||||
if a == 0 {
|
|
||||||
hasZero = true
|
hasZero = true
|
||||||
} else {
|
} else {
|
||||||
hasVisible = true
|
hasVisible = true
|
||||||
@@ -704,8 +754,15 @@ func imageWithTransparentEdge64(original, source image.Image) (image.Image, int)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !hasZero || !hasVisible {
|
|
||||||
return original, 0
|
|
||||||
}
|
}
|
||||||
return &transparentEdgeImage64{source: source, rect: image.Rect(0, 0, bounds.Dx()+2, bounds.Dy()+2)}, 1
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if !hasZero || !hasVisible {
|
||||||
|
return img, 0, nil
|
||||||
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
+183
-13
@@ -17,12 +17,15 @@ package ofdgo
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
// imageCacheLimit 限制图片缓存的编码数据及预计解码内存
|
// imageCacheLimit 限制图片缓存的编码数据及预计解码内存
|
||||||
@@ -34,8 +37,14 @@ type EncodedImage struct {
|
|||||||
data []byte
|
data []byte
|
||||||
format string
|
format string
|
||||||
config image.Config
|
config image.Config
|
||||||
once sync.Once
|
result atomic.Pointer[encodedImageResult]
|
||||||
decoded image.Image
|
mu sync.Mutex
|
||||||
|
pending chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodedImageResult 保存已完成的像素解码结果,不保存取消或超时错误
|
||||||
|
type encodedImageResult struct {
|
||||||
|
image image.Image
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,8 +76,67 @@ func (img *EncodedImage) Bounds() image.Rectangle {
|
|||||||
// Image 首次解码图片并缓存结果或错误,支持并发读取
|
// Image 首次解码图片并缓存结果或错误,支持并发读取
|
||||||
// 返回: image.Image 只读像素, error 解码错误
|
// 返回: image.Image 只读像素, error 解码错误
|
||||||
func (img *EncodedImage) 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)) })
|
if result := img.result.Load(); result != nil {
|
||||||
return img.decoded, img.err
|
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检查错误
|
// At 返回像素颜色,解码失败时返回透明色,绘制前应通过Image检查错误
|
||||||
@@ -101,7 +169,7 @@ func (r *Renderer) imageObjectResource(object ImageObject) (image.Image, error)
|
|||||||
read := func(id string) (image.Image, error) {
|
read := func(id string) (image.Image, error) {
|
||||||
img, err := r.ImageResource(id)
|
img, err := r.ImageResource(id)
|
||||||
if err == nil && (r.decodeImages || object.Substitution != "") {
|
if err == nil && (r.decodeImages || object.Substitution != "") {
|
||||||
pixels, decodeErr := imagePixelData(img)
|
pixels, decodeErr := imagePixelDataContext(r.pageContext, img)
|
||||||
err = decodeErr
|
err = decodeErr
|
||||||
if r.decodeImages {
|
if r.decodeImages {
|
||||||
img = pixels
|
img = pixels
|
||||||
@@ -126,6 +194,67 @@ func (r *Renderer) imageObjectResource(object ImageObject) (image.Image, error)
|
|||||||
return img, nil
|
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 接管编码数据并读取头信息
|
// newEncodedImage 接管编码数据并读取头信息
|
||||||
// 入参: data 原始编码
|
// 入参: data 原始编码
|
||||||
// 返回: *EncodedImage 惰性图片, error 头信息或格式错误
|
// 返回: *EncodedImage 惰性图片, error 头信息或格式错误
|
||||||
@@ -162,7 +291,7 @@ func (r *Renderer) cachedImageResource(resPath string) (image.Image, error) {
|
|||||||
return img, nil
|
return img, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// imageResourceCost 预留十六位像素展开空间,超预算图片不进入缓存
|
// imageResourceCost 预留十六位像素空间,并计入编码、像素缓冲及补边源图的可见容量
|
||||||
// 入参: img 图片资源, limit 缓存预算
|
// 入参: img 图片资源, limit 缓存预算
|
||||||
// 返回: int 估算字节数或超预算标记
|
// 返回: int 估算字节数或超预算标记
|
||||||
func imageResourceCost(img image.Image, limit int) 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 {
|
if limit < cost || w <= 0 || h <= 0 || w > (limit-cost)/8/h {
|
||||||
return limit + 1
|
return limit + 1
|
||||||
}
|
}
|
||||||
cost += w * h * 8
|
pixels := w * h * 8
|
||||||
if encoded, ok := img.(*EncodedImage); ok {
|
if encoded, ok := img.(*EncodedImage); ok {
|
||||||
if len(encoded.data) > limit-cost {
|
if cap(encoded.data) > limit-cost-pixels {
|
||||||
return limit + 1
|
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 读取或复用图片资源,按输出需要解码像素
|
// decodeImageResource 读取或复用图片资源,按输出需要解码像素
|
||||||
@@ -190,9 +362,7 @@ func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if r.decodeImages {
|
if r.decodeImages {
|
||||||
if source, ok := img.(*EncodedImage); ok {
|
return imagePixelDataContext(r.pageContext, img)
|
||||||
return source.Image()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return img, nil
|
return img, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,14 +206,7 @@ func (c *pdfCompositor) geometry(node pdfCompositeNode, stroke bool) (pdfComposi
|
|||||||
c.cache.bounds[key] = geometry.box
|
c.cache.bounds[key] = geometry.box
|
||||||
cost := 256
|
cost := 256
|
||||||
if geometry.scene != nil {
|
if geometry.scene != nil {
|
||||||
for _, command := range geometry.scene.Commands {
|
cost += rasterCommandsCost(geometry.scene.Commands, cache.limit-cost)
|
||||||
part := rasterCommandCost(command)
|
|
||||||
if part <= 0 || part > cache.limit-cost {
|
|
||||||
cost = cache.limit + 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
cost += part
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
cache.put(sceneKey, geometry, cost)
|
cache.put(sceneKey, geometry, cost)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"container/list"
|
"container/list"
|
||||||
|
"image"
|
||||||
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
// renderCache 按估算字节数限制派生渲染数据,淘汰不修改源资源
|
// renderCache 按估算字节数限制派生渲染数据,淘汰不修改源资源
|
||||||
@@ -85,3 +87,37 @@ func rasterCommandCost(command RasterCommand) int {
|
|||||||
}
|
}
|
||||||
return cost
|
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
|
||||||
|
}
|
||||||
@@ -490,7 +490,7 @@ func (c *semanticCompiler) fillPattern(clip GeometryPath, pattern *PatternPaint,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := c.appendPatternCell(commands, clip, segments); err != nil {
|
if err := c.appendClippedCommands(commands, clip, segments); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,23 +64,14 @@ func (c *semanticCompiler) imageObject(object ImageObject, state RenderState) er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if object.ImageMask != "" || object.Alpha != nil && *object.Alpha < 255 {
|
mask, err := c.renderer.imageObjectMask(object, img)
|
||||||
img, err = imagePixelData(img)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
img, err = imageWithEffects(c.renderer.pageContext, img, mask, object.Alpha)
|
||||||
if object.ImageMask != "" {
|
|
||||||
mask, err := c.renderer.ImageResource(object.ImageMask)
|
|
||||||
if err == nil {
|
|
||||||
mask, err = imagePixelData(mask)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
img = imageWithMask(img, mask)
|
|
||||||
}
|
|
||||||
img = imageWithAlpha(img, object.Alpha)
|
|
||||||
bounds := img.Bounds()
|
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)))
|
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 {
|
if err := c.addImage(img, matrix, clip); err != nil {
|
||||||
@@ -187,18 +178,20 @@ func (c *semanticCompiler) drawStampContent(stamp Stamp) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 stamp.Clip != nil {
|
||||||
if renderer.backends.Raster == nil {
|
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})
|
||||||
return fmt.Errorf("stamp raster: %w", ErrBackendUnavailable)
|
segments, err = c.segments(clip)
|
||||||
}
|
|
||||||
img, err := renderer.backends.Raster.Render(page)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
for _, command := range page.Commands {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
command.Transform = RasterMatrix(matrix.Multiply(MatrixFromValues([6]float64(command.Transform))).Values())
|
command.Transform = RasterMatrix(matrix.Multiply(MatrixFromValues([6]float64(command.Transform))).Values())
|
||||||
if command.Stroke != nil {
|
if command.Stroke != nil {
|
||||||
stroke := *command.Stroke
|
stroke := *command.Stroke
|
||||||
@@ -221,31 +214,42 @@ func (c *semanticCompiler) drawStampContent(stamp Stamp) error {
|
|||||||
command.Clip[i] = segment
|
command.Clip[i] = segment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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)
|
c.page.Commands = append(c.page.Commands, command)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
img, _, err := decodeImageDataContext(ctx, stamp.Data)
|
img, _, err := decodeImageDataContext(ctx, stamp.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return c.stampImage(stampImageWithTransparentWhite(img), stamp)
|
return c.stampImage(img, stamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// stampImage 按印章裁剪区域放置图像
|
// stampImage 按印章裁剪区域放置图像
|
||||||
// 入参: img 印章图像, stamp 印章
|
// 入参: img 印章图像, stamp 印章
|
||||||
// 返回: error 编译错误
|
// 返回: error 编译错误
|
||||||
func (c *semanticCompiler) stampImage(img image.Image, stamp 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
|
box := stamp.Box
|
||||||
|
var clip *GeometryPath
|
||||||
if stamp.Clip != nil {
|
if stamp.Clip != nil {
|
||||||
img = clipStampImage(img, box, *stamp.Clip)
|
path := geometryRectangle(Box{X: box.X + stamp.Clip.X, Y: box.Y + stamp.Clip.Y, W: stamp.Clip.W, H: stamp.Clip.H})
|
||||||
box.X += stamp.Clip.X
|
clip = &path
|
||||||
box.Y += stamp.Clip.Y
|
|
||||||
box.W = stamp.Clip.W
|
|
||||||
box.H = stamp.Clip.H
|
|
||||||
}
|
}
|
||||||
bounds := img.Bounds()
|
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)))
|
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
@@ -96,21 +96,7 @@ func (c *semanticCompiler) sharedStamp(stamp Stamp, compile func(*semanticCompil
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
commands = page.Commands
|
commands = page.Commands
|
||||||
cost := 0
|
cost := rasterCommandsCost(commands, state.cache.limit)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -158,10 +144,7 @@ func (c *semanticCompiler) patternCell(pattern *PatternPaint, matrix Matrix) ([]
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cost := 0
|
cost := rasterCommandsCost(child.page.Commands, c.patterns.limit)
|
||||||
for _, cmd := range child.page.Commands {
|
|
||||||
cost += rasterCommandCost(cmd)
|
|
||||||
}
|
|
||||||
c.patterns.put(key, child.page.Commands, cost)
|
c.patterns.put(key, child.page.Commands, cost)
|
||||||
return translatePatternCommands(child.page.Commands, x, y), nil
|
return translatePatternCommands(child.page.Commands, x, y), nil
|
||||||
}
|
}
|
||||||
@@ -226,10 +209,10 @@ func translatePatternCommands(commands []RasterCommand, x, y float64) []RasterCo
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendPatternCell 将缓存指令放入当前裁剪,不修改单元数据
|
// appendClippedCommands 合并页面裁剪后追加指令,不修改原始指令
|
||||||
// 入参: commands 单元指令, clip 页面裁剪, segments 已转换裁剪
|
// 入参: commands 绘制指令, clip 页面裁剪, segments 已转换裁剪
|
||||||
// 返回: error 裁剪合并错误
|
// 返回: 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 {
|
for _, command := range commands {
|
||||||
commandClip := segments
|
commandClip := segments
|
||||||
if command.Clip != nil {
|
if command.Clip != nil {
|
||||||
|
|||||||
@@ -104,14 +104,22 @@ func (c *canvasPageCompiler) RenderImage(img image.Image, m canvas.Matrix) {
|
|||||||
if c.err != nil {
|
if c.err != nil {
|
||||||
return
|
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) {
|
if (m[0][1] != 0 || m[1][0] != 0) && (m[0][0] != 0 || m[1][1] == 0) {
|
||||||
bounds := img.Bounds()
|
bounds := img.Bounds()
|
||||||
img = &rasterPaddedImage{source: img, rect: image.Rect(0, 0, bounds.Dx()+8, bounds.Dy()+8)}
|
img = &rasterPaddedImage{source: img, rect: image.Rect(0, 0, bounds.Dx()+8, bounds.Dy()+8)}
|
||||||
m = m.Translate(-4, -4)
|
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 := 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})
|
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 图像边界
|
// 返回: image.Rectangle 图像边界
|
||||||
func (s *rasterPaddedImage) Bounds() image.Rectangle { return s.rect }
|
func (s *rasterPaddedImage) Bounds() image.Rectangle { return s.rect }
|
||||||
|
|
||||||
// ColorModel 返回预乘八位颜色模型,与实体补边图像保持一致
|
// ColorModel 按原图采样精度返回支持透明补边的预乘颜色模型
|
||||||
// 返回: color.Model 颜色模型
|
// 返回: 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 读取补边图像的预乘像素,边距及图像外透明
|
// At 读取补边图像的预乘像素,边距及图像外透明
|
||||||
// 入参: x 横向坐标, y 纵向坐标
|
// 入参: x 横向坐标, y 纵向坐标
|
||||||
// 返回: color.Color 像素颜色
|
// 返回: color.Color 像素颜色
|
||||||
func (s *rasterPaddedImage) At(x, y int) color.Color {
|
func (s *rasterPaddedImage) At(x, y int) color.Color { return s.RGBA64At(x, y) }
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
// RGBA64At 返回预乘十六位像素供采样器读取
|
// RGBA64At 保留原始预乘精度,支持采样接口的图像不进行颜色装箱
|
||||||
// 入参: x 横向坐标, y 纵向坐标
|
// 入参: x 横向坐标, y 纵向坐标
|
||||||
// 返回: color.RGBA64 像素颜色
|
// 返回: color.RGBA64 像素颜色
|
||||||
func (s *rasterPaddedImage) RGBA64At(x, y int) 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)}
|
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+111
-20
@@ -16,6 +16,7 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"math"
|
"math"
|
||||||
@@ -44,25 +45,16 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
box, _ := ParseBox(obj.Boundary)
|
box, _ := ParseBox(obj.Boundary)
|
||||||
if obj.ImageMask != "" || obj.Alpha != nil && *obj.Alpha < 255 {
|
mask, err := r.imageObjectMask(obj, img)
|
||||||
img, err = imagePixelData(img)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.renderError = err
|
r.renderError = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
img, err = imageWithEffects(r.pageContext, img, mask, obj.Alpha)
|
||||||
if obj.ImageMask != "" {
|
|
||||||
mask, err := r.ImageResource(obj.ImageMask)
|
|
||||||
if err == nil {
|
|
||||||
mask, err = imagePixelData(mask)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.renderError = err
|
r.renderError = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
img = imageWithMask(img, mask)
|
|
||||||
}
|
|
||||||
img = imageWithAlpha(img, obj.Alpha)
|
|
||||||
imgBounds := img.Bounds()
|
imgBounds := img.Bounds()
|
||||||
imgW, imgH := float64(imgBounds.Dx()), float64(imgBounds.Dy())
|
imgW, imgH := float64(imgBounds.Dx()), float64(imgBounds.Dy())
|
||||||
if imgW <= 0 || imgH <= 0 {
|
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))
|
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
|
imageClip := clipPath
|
||||||
compiler, compiled := ctx.Renderer.(*canvasPageCompiler)
|
compiler, compiled := ctx.Renderer.(*canvasPageCompiler)
|
||||||
if compiled && imageClip != nil && imageClipContains(img, imageClip, m) {
|
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()))
|
m = m.Scale(imgW/float64(img.Bounds().Dx()), imgH/float64(img.Bounds().Dy()))
|
||||||
if _, pdf := ctx.Renderer.(*pdfRenderer); !pdf {
|
if _, pdf := ctx.Renderer.(*pdfRenderer); !pdf {
|
||||||
var pad int
|
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 {
|
if pad > 0 {
|
||||||
p := float64(pad)
|
p := float64(pad)
|
||||||
m[0][2] -= m[0][0]*p + m[0][1]*p
|
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
|
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输出边界保留原始编码,不重新压缩图片
|
// canvasEncodedImage 在Canvas输出边界保留原始编码,不重新压缩图片
|
||||||
@@ -175,11 +182,19 @@ func imageClipContains(img image.Image, clipPath *canvas.Path, m canvas.Matrix)
|
|||||||
return clipPath.Contains(outline)
|
return clipPath.Contains(outline)
|
||||||
}
|
}
|
||||||
|
|
||||||
// imageWithClip 应用图片裁剪区域
|
// imageWithClip 应用图片裁剪区域,保留原图采样精度
|
||||||
// 低分辨率图片按最低精度细化蒙版,细化后的像素上限16Mi,不降低原图分辨率
|
// 低分辨率图片按最低精度细化蒙版,细化后的像素上限16Mi,不降低原图分辨率
|
||||||
// 入参: img 图片对象, clipPath 裁剪路径, m 图片变换矩阵, dpi 最低蒙版分辨率,0保留原图精度
|
// 入参: img 图片对象, clipPath 裁剪路径, m 图片变换矩阵, dpi 最低蒙版分辨率,0保留原图精度
|
||||||
// 返回: image.Image 裁剪后的图片对象
|
// 返回: image.Image 裁剪后的图片对象
|
||||||
func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canvas.Matrix, dpi float64) 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 {
|
if img == nil || clipPath == nil || m.Det() == 0 {
|
||||||
return img
|
return img
|
||||||
}
|
}
|
||||||
@@ -192,7 +207,7 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva
|
|||||||
return img
|
return img
|
||||||
}
|
}
|
||||||
original := img
|
original := img
|
||||||
source, err := imagePixelData(img)
|
source, err := imagePixelDataContext(r.pageContext, img)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.renderError = err
|
r.renderError = err
|
||||||
return img
|
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))))
|
scale = math.Min(scale, math.Sqrt((16<<20)/(float64(w)*float64(h))))
|
||||||
if scale > 1 {
|
if scale > 1 {
|
||||||
nw, nh := int(float64(w)*scale), int(float64(h)*scale)
|
nw, nh := int(float64(w)*scale), int(float64(h)*scale)
|
||||||
resized := image.NewNRGBA(image.Rect(0, 0, nw, nh))
|
var resized draw.Image
|
||||||
draw.CatmullRom.Scale(resized, resized.Bounds(), imagePixelSource(img), bounds, draw.Src, nil)
|
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
|
img = resized
|
||||||
m = m.Scale(float64(w)/float64(nw), float64(h)/float64(nh))
|
m = m.Scale(float64(w)/float64(nw), float64(h)/float64(nh))
|
||||||
bounds, w, h = resized.Bounds(), nw, 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())
|
clip := clipPath.Copy().Transform(m.Inv())
|
||||||
compiler := &canvasPageCompiler{page: &RasterPage{Width: float64(w), Height: float64(h), DPI: 25.4}, geometry: r.backends.Geometry}
|
compiler := &canvasPageCompiler{page: &RasterPage{Width: float64(w), Height: float64(h), DPI: 25.4}, geometry: r.backends.Geometry}
|
||||||
ctx := canvas.NewContext(compiler)
|
ctx := canvas.NewContext(compiler)
|
||||||
@@ -223,16 +247,67 @@ func (r *Renderer) imageWithClip(img image.Image, clipPath *canvas.Path, m canva
|
|||||||
return img
|
return img
|
||||||
}
|
}
|
||||||
result, err := r.backends.Raster.Render(compiler.page)
|
result, err := r.backends.Raster.Render(compiler.page)
|
||||||
|
if cancelErr := work.Err(); cancelErr != nil {
|
||||||
|
err = cancelErr
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.renderError = err
|
r.renderError = err
|
||||||
return img
|
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)
|
mask, ok := result.(*image.RGBA)
|
||||||
if !ok {
|
if !ok {
|
||||||
mask = image.NewRGBA(image.Rect(0, 0, w, h))
|
mask = image.NewRGBA(image.Rect(0, 0, w, h))
|
||||||
draw.Draw(mask, mask.Bounds(), result, result.Bounds().Min, draw.Src)
|
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 {
|
if scale <= 1 {
|
||||||
return original
|
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}
|
out := &image.NRGBA{Pix: mask.Pix, Stride: mask.Stride, Rect: bounds}
|
||||||
if src, ok := source.(*image.NRGBA); ok {
|
if src, ok := source.(*image.NRGBA); ok {
|
||||||
for y := 0; y < h; y++ {
|
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)
|
offset := src.PixOffset(bounds.Min.X, bounds.Min.Y+y)
|
||||||
row := out.Pix[y*out.Stride : y*out.Stride+w*4]
|
row := out.Pix[y*out.Stride : y*out.Stride+w*4]
|
||||||
pixels := src.Pix[offset : offset+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)
|
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
|
return out
|
||||||
}
|
}
|
||||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
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++ {
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||||
c := imageNRGBAAt(source, x, y)
|
c := imageNRGBAAt(source, x, y)
|
||||||
a := mask.RGBAAt(x-bounds.Min.X, y-bounds.Min.Y).A
|
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)
|
out.SetNRGBA(x, y, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := work.Err(); err != nil {
|
||||||
|
r.renderError = err
|
||||||
|
return img
|
||||||
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
@@ -164,7 +164,7 @@ func (r *pdfRenderer) writePDF(ctx context.Context, data []byte, writer io.Write
|
|||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
pixels, err := imagePixelData(original)
|
pixels, err := imagePixelDataContext(ctx, original)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.Type == "ofd" && len(s.Data) > 0 {
|
||||||
if s.Clip != nil {
|
if s.Clip != nil {
|
||||||
if img := r.renderOFDStampImage(s.Data); img != nil {
|
if img := r.renderOFDStampImage(s.Data); img != nil {
|
||||||
r.renderStampImage(ctx, stampImageWithTransparentWhite(img), s, pageH)
|
r.renderStampImage(ctx, img, s, pageH)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -60,11 +60,13 @@ func (r *Renderer) renderStamp(ctx *canvas.Context, s Stamp, pageH float64) {
|
|||||||
if pageContext.Err() != nil {
|
if pageContext.Err() != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
continue
|
r.renderError = err
|
||||||
|
return
|
||||||
}
|
}
|
||||||
sealBox, err := renderer.GetPageBox(content)
|
sealBox, err := renderer.GetPageBox(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
r.renderError = err
|
||||||
|
return
|
||||||
}
|
}
|
||||||
ctx.Push()
|
ctx.Push()
|
||||||
ctx.Translate(s.Box.X, pageH-(s.Box.Y+s.Box.H))
|
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
|
return
|
||||||
}
|
}
|
||||||
|
r.renderError = err
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
r.renderError = err
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if len(s.Data) > 0 {
|
if len(s.Data) > 0 {
|
||||||
img, _, err := decodeImageDataContext(pageContext, s.Data)
|
img, _, err := decodeImageDataContext(pageContext, s.Data)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
if r.decodeImages {
|
r.renderError = err
|
||||||
img = imagePixelSource(img)
|
|
||||||
}
|
|
||||||
r.renderStampImage(ctx, stampImageWithTransparentWhite(img), s, pageH)
|
|
||||||
return
|
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)))
|
reader, err := NewReader(bytes.NewReader(data), int64(len(data)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
r.renderError = err
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
doc, err := reader.DocContext(ctx)
|
doc, err := reader.DocContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
r.renderError = err
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
renderer := r.childRenderer(reader)
|
renderer := r.childRenderer(reader)
|
||||||
@@ -127,7 +133,8 @@ func (r *Renderer) renderOFDStampImage(data []byte) image.Image {
|
|||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
continue
|
r.renderError = err
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
img, err := renderer.RenderToImage(content)
|
img, err := renderer.RenderToImage(content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -142,17 +149,19 @@ func (r *Renderer) renderOFDStampImage(data []byte) image.Image {
|
|||||||
// renderStampImage 渲染印章图像
|
// renderStampImage 渲染印章图像
|
||||||
// 入参: ctx 画布上下文, img 印章图像, s 印章对象, pageH 页面高度
|
// 入参: ctx 画布上下文, img 印章图像, s 印章对象, pageH 页面高度
|
||||||
func (r *Renderer) renderStampImage(ctx *canvas.Context, img image.Image, s Stamp, pageH float64) {
|
func (r *Renderer) renderStampImage(ctx *canvas.Context, img image.Image, s Stamp, pageH float64) {
|
||||||
box := s.Box
|
pixels, err := imagePixelDataContext(r.pageContext, img)
|
||||||
if s.Clip != nil {
|
if err != nil {
|
||||||
img = clipStampImage(img, box, *s.Clip)
|
r.renderError = err
|
||||||
box.X += s.Clip.X
|
return
|
||||||
box.Y += s.Clip.Y
|
|
||||||
box.W = s.Clip.W
|
|
||||||
box.H = s.Clip.H
|
|
||||||
}
|
}
|
||||||
ctx.Push()
|
if r.decodeImages {
|
||||||
ctx.Translate(box.X, pageH-(box.Y+box.H))
|
img = pixels
|
||||||
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))
|
box := s.Box
|
||||||
ctx.Pop()
|
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)
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user