// 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 ( "bytes" "context" "fmt" "image" "image/color" _ "image/jpeg" _ "image/png" "sync" ) // imageCacheLimit 限制图片缓存的编码数据及预计解码内存 const imageCacheLimit = 64 << 20 // EncodedImage 保留PNG或JPEG原始编码,首次读取像素时解码 // Bytes返回只读数据,Image返回共享只读像素,解码错误由Image返回 type EncodedImage struct { data []byte format string config image.Config once sync.Once decoded image.Image err error } // NewEncodedImage 读取PNG或JPEG头信息,不解码像素,复制输入数据 // 入参: data 原始编码 // 返回: *EncodedImage 惰性图片, error 头信息或格式错误 func NewEncodedImage(data []byte) (*EncodedImage, error) { return newEncodedImage(bytes.Clone(data)) } // Bytes 返回只读原始编码,调用方不得修改 // 返回: []byte 原始编码 func (img *EncodedImage) Bytes() []byte { return img.data } // MIME 返回图片媒体类型 // 返回: string 媒体类型 func (img *EncodedImage) MIME() string { return "image/" + img.format } // ColorModel 返回头信息中的颜色模型,不解码像素 // 返回: color.Model 颜色模型 func (img *EncodedImage) ColorModel() color.Model { return img.config.ColorModel } // Bounds 返回图片范围,不解码像素 // 返回: image.Rectangle 图片范围 func (img *EncodedImage) Bounds() image.Rectangle { return image.Rect(0, 0, img.config.Width, img.config.Height) } // 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 } // At 返回像素颜色,解码失败时返回透明色,绘制前应通过Image检查错误 // 入参: x 横坐标, y 纵坐标 // 返回: color.Color 像素颜色 func (img *EncodedImage) At(x, y int) color.Color { decoded, err := img.Image() if err != nil { return color.Transparent } return decoded.At(x, y) } // ImageResource 按资源标识读取只读图片,PNG和JPEG保留原始编码并惰性解码 // 应先读取源页面以加载页面资源,返回EncodedImage时通过Image检查像素解码错误 // 入参: id 图片资源标识 // 返回: image.Image 图片资源, error 读取错误 func (r *Renderer) ImageResource(id string) (image.Image, error) { path, ok := resourceValue(r.Reader.ResMap, id) if !ok { return nil, fmt.Errorf("image resource %q not found", id) } return r.cachedImageResource(path) } // imageObjectResource 读取主图,无法读取或解码时使用对象声明的替换图 // 入参: object 图像对象 // 返回: image.Image 图片资源, error 读取或解码错误 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) err = decodeErr if r.decodeImages { img = pixels } } return img, err } img, err := read(object.ResourceID) if err == nil || object.Substitution == "" || sameResourceID(object.Substitution, object.ResourceID) { return img, err } if r.pageContext != nil && r.pageContext.Err() != nil { return nil, r.pageContext.Err() } if r.OutputContext != nil && r.OutputContext.Err() != nil { return nil, r.OutputContext.Err() } img, substituteErr := read(object.Substitution) if substituteErr != nil { return nil, fmt.Errorf("image resource %q: %w; substitution %q: %w", object.ResourceID, err, object.Substitution, substituteErr) } return img, nil } // newEncodedImage 接管编码数据并读取头信息 // 入参: data 原始编码 // 返回: *EncodedImage 惰性图片, error 头信息或格式错误 func newEncodedImage(data []byte) (*EncodedImage, error) { config, format, err := image.DecodeConfig(bytes.NewReader(data)) if err != nil { return nil, err } if format != "jpeg" && format != "png" { return nil, fmt.Errorf("unsupported encoded image %q", format) } return &EncodedImage{data: data, format: format, config: config}, nil } // cachedImageResource 复用原始图片资源,不依赖输出后端 // 入参: resPath 图片资源路径 // 返回: image.Image 图片对象, error 读取错误 func (r *Renderer) cachedImageResource(resPath string) (image.Image, error) { if r.pageContext != nil { if err := r.pageContext.Err(); err != nil { return nil, err } } resPath = cleanPackagePath(r.Reader.ResPath(resPath)) img, ok := r.imageCache.get(resPath) if !ok { var err error img, err = r.readImageResource(resPath) if err != nil { return nil, err } r.imageCache.put(resPath, img, imageResourceCost(img, r.imageCache.limit)) } return img, nil } // imageResourceCost 预留十六位像素展开空间,超预算图片不进入缓存 // 入参: img 图片资源, limit 缓存预算 // 返回: int 估算字节数或超预算标记 func imageResourceCost(img image.Image, limit int) int { cost := 256 w, h := img.Bounds().Dx(), img.Bounds().Dy() if limit < cost || w <= 0 || h <= 0 || w > (limit-cost)/8/h { return limit + 1 } cost += w * h * 8 if encoded, ok := img.(*EncodedImage); ok { if len(encoded.data) > limit-cost { return limit + 1 } cost += len(encoded.data) } return cost } // decodeImageResource 读取或复用图片资源,按输出需要解码像素 // 入参: resPath 图片资源路径 // 返回: image.Image 图片对象, error 读取或解码错误 func (r *Renderer) decodeImageResource(resPath string) (image.Image, error) { img, err := r.cachedImageResource(resPath) if err != nil { return nil, err } if r.decodeImages { if source, ok := img.(*EncodedImage); ok { return source.Image() } } return img, nil } // readImageResource 按页面上下文读取图片,借用已加载的只读编码 // 入参: resPath 图片资源路径 // 返回: image.Image 图片对象, error 读取或解码错误 func (r *Renderer) readImageResource(resPath string) (image.Image, error) { ctx := r.pageContext if ctx == nil { ctx = context.Background() } data, err := r.Reader.readFileViewContext(ctx, resPath) if err != nil { return nil, err } img, _, err := decodeImageDataContext(ctx, data) return img, err }