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

+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
}