feat(文件拆分): 拆分字体图像基础能力
build.yaml / build (push) Successful in 49s

This commit is contained in:
xiaoqidun committed 2026-10-14 00:12:00 +08:00
1 parent ee3fd02a0a
commit 64f9ec4241
87 files changed
+2693 -2289

No files matched your search

+125
View File
@@ -0,0 +1,125 @@
// 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 image
import (
"bufio"
"encoding/binary"
"fmt"
"image"
"io"
"math/bits"
)
// bmpColorMask 保存一个BMP颜色分量的位域位置和取值范围
type bmpColorMask struct {
mask uint32
shift uint
Max uint32
}
// DecodeConfig 读取图片尺寸,BMP和TIFF只读取布局信息,不解码像素
// 入参: input 图片输入流
// 返回: image.Config 图片配置, string 图片格式, error 头部错误
func DecodeConfig(input io.Reader) (image.Config, string, error) {
buffer := bufio.NewReader(input)
prefix, _ := buffer.Peek(34)
if IsTIFF(prefix) {
layout, err := readTIFFLayout(buffer)
if err != nil {
return image.Config{}, "tiff", err
}
size := layout.DisplaySize()
return image.Config{Width: size.X, Height: size.Y}, "tiff", nil
}
if len(prefix) == 34 && string(prefix[:2]) == "BM" {
header := binary.LittleEndian.Uint32(prefix[14:18])
bpp := binary.LittleEndian.Uint16(prefix[28:30])
compression := binary.LittleEndian.Uint32(prefix[30:34])
if (bpp == 16 || bpp == 32 && compression == 3) && (header == 40 || header == 52 || header == 56 || header == 108 || header == 124) {
size := int(header) + 14
if header == 40 && compression == 3 {
size += 12
}
data := make([]byte, size)
if _, err := io.ReadFull(buffer, data); err != nil {
return image.Config{}, "bmp", err
}
config, err := decodeBMPConfig(data)
return config, "bmp", err
}
}
return image.DecodeConfig(buffer)
}
// sample 将位域采样值映射到八位通道,缺省透明度为不透明
// 入参: value 编码像素
// 返回: uint8 通道值
func (m bmpColorMask) sample(value uint32) uint8 {
if m.mask == 0 {
return 255
}
return uint8(uint64((value&m.mask)>>m.shift) * 255 / uint64(m.Max))
}
// BMPColorMasks 读取BMP真彩色通道布局,校验位域连续性、重叠及像素范围
// 入参: data 已校验头部的BMP数据, offset 像素偏移, bpp 像素位数, compression 编码方式
// 返回: [4]bmpColorMask 红绿蓝和透明度通道, error 格式错误
func BMPColorMasks(data []byte, offset int, bpp uint16, compression uint32) ([4]bmpColorMask, error) {
var result [4]bmpColorMask
if bpp != 16 && bpp != 24 && bpp != 32 {
return result, fmt.Errorf("unsupported bmp depth")
}
masks := [4]uint32{0xff0000, 0xff00, 0xff}
if bpp == 16 {
masks = [4]uint32{0x7c00, 0x3e0, 0x1f}
}
switch compression {
case 0:
case 3:
if bpp == 24 {
return result, fmt.Errorf("invalid bmp bitfield depth")
}
header := binary.LittleEndian.Uint32(data[14:18])
if offset < 66 || len(data) < 66 || header != 40 && header < 52 {
return result, fmt.Errorf("truncated bmp color masks")
}
for i := range 3 {
masks[i] = binary.LittleEndian.Uint32(data[54+i*4:])
}
if header >= 56 {
masks[3] = binary.LittleEndian.Uint32(data[66:70])
}
default:
return result, fmt.Errorf("unsupported bmp compression")
}
var used uint32
for i, mask := range masks {
if mask == 0 {
if i < 3 {
return result, fmt.Errorf("empty bmp color mask")
}
continue
}
shift := uint(bits.TrailingZeros32(mask))
maximum := mask >> shift
if mask&used != 0 || maximum&(maximum+1) != 0 || uint64(mask)>>bpp != 0 {
return result, fmt.Errorf("invalid bmp color mask")
}
result[i] = bmpColorMask{mask: mask, shift: shift, Max: maximum}
used |= mask
}
return result, nil
}
+172
View File
@@ -0,0 +1,172 @@
// 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 image
import (
"bytes"
"context"
"errors"
"fmt"
"image"
"image/color"
_ "image/jpeg"
_ "image/png"
"io"
"sync"
"sync/atomic"
)
// EncodedImage 保留PNG或JPEG原始编码,首次读取像素时解码
// Bytes返回只读数据,Image返回共享只读像素,解码错误由Image返回
type EncodedImage struct {
data []byte
format string
config image.Config
result atomic.Pointer[encodedImageResult]
mu sync.Mutex
pending chan struct{}
}
// encodedImageResult 保存已完成的像素解码结果,不保存取消或超时错误
type encodedImageResult struct {
image 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) {
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(contextInput{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检查错误
// 入参: 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)
}
// newEncodedImage 接管编码数据并读取头信息
// 入参: data 原始编码
// 返回: *EncodedImage 惰性图片, error 头信息或格式错误
func newEncodedImage(data []byte) (*EncodedImage, error) {
return newEncodedImageContext(context.Background(), data)
}
// newEncodedImageContext 读取PNG或JPEG头信息,元数据扫描期间响应取消,不复制输入或解码像素
// 入参: ctx 取消上下文, data 只读图片编码
// 返回: *EncodedImage 惰性图片, error 头信息、格式或取消错误
func newEncodedImageContext(ctx context.Context, data []byte) (*EncodedImage, error) {
config, format, err := image.DecodeConfig(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
if cancelErr := ctx.Err(); cancelErr != nil {
return nil, cancelErr
}
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
}
+31
View File
@@ -0,0 +1,31 @@
// 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 image
// NormalizeFormat 标准化印章媒体类型
// 入参: s 原始媒体类型
// 返回: string 标准媒体类型
func NormalizeFormat(s string) string {
switch s {
case "jpg":
return "jpeg"
case "bmp":
return "bmp"
case "jb2", "gbig2":
return "jbig2"
default:
return s
}
}
+714
View File
@@ -0,0 +1,714 @@
// 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 image
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"image"
"image/color"
_ "image/gif"
"io"
_ "github.com/xiaoqidun/j2kgo"
_ "github.com/xiaoqidun/jbig2"
_ "golang.org/x/image/bmp"
"golang.org/x/image/tiff"
)
// transparentEdgeImage 共享只读像素并按需计算透明边缘和采样边距
type transparentEdgeImage struct {
source image.Image
rect image.Rectangle
}
// transparentEdgeImage64 按需补齐十六位像素边缘,保留原始只读图像
type transparentEdgeImage64 struct {
source image.Image
rect image.Rectangle
}
// Bounds 返回含采样边距的图像边界
// 返回: image.Rectangle 图像边界
func (s *transparentEdgeImage) Bounds() image.Rectangle { return s.rect }
// ColorModel 返回非预乘颜色模型
// 返回: color.Model 颜色模型
func (s *transparentEdgeImage) ColorModel() color.Model { return color.NRGBAModel }
// NRGBAAt 读取原始像素或按邻域补齐透明边缘,边界外透明
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.NRGBA 非预乘颜色
func (s *transparentEdgeImage) NRGBAAt(x, y int) color.NRGBA {
if !image.Pt(x, y).In(s.rect) {
return color.NRGBA{}
}
b := s.source.Bounds()
sx, sy := b.Min.X+min(max(x-1, 0), b.Dx()-1), b.Min.Y+min(max(y-1, 0), b.Dy()-1)
c := NRGBAAt(s.source, sx, sy)
if c.A == 0 {
if edge, ok := transparentEdgeColor(s.source, sx, sy); ok {
c = edge
}
}
if x == 0 || y == 0 || x == s.rect.Max.X-1 || y == s.rect.Max.Y-1 {
c = transparentPaddingColor(c)
}
return c
}
// At 返回非预乘像素颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.Color 像素颜色
func (s *transparentEdgeImage) At(x, y int) color.Color { return s.NRGBAAt(x, y) }
// RGBA64At 返回预乘十六位像素,供采样器无装箱读取
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.RGBA64 预乘颜色
func (s *transparentEdgeImage) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.NRGBAAt(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
// Bounds 返回含采样边距的图像边界
// 返回: image.Rectangle 图像边界
func (s *transparentEdgeImage64) Bounds() image.Rectangle { return s.rect }
// ColorModel 返回十六位非预乘颜色模型
// 返回: color.Model 颜色模型
func (s *transparentEdgeImage64) ColorModel() color.Model { return color.NRGBA64Model }
// NRGBA64At 读取十六位原始颜色,透明邻域使用最小非零透明度保留边缘颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.NRGBA64 非预乘颜色
func (s *transparentEdgeImage64) NRGBA64At(x, y int) color.NRGBA64 {
if !image.Pt(x, y).In(s.rect) {
return color.NRGBA64{}
}
b := s.source.Bounds()
sx, sy := b.Min.X+min(max(x-1, 0), b.Dx()-1), b.Min.Y+min(max(y-1, 0), b.Dy()-1)
c := NRGBA64At(s.source, sx, sy)
if c.A == 0 {
var best color.NRGBA64
for ny := max(sy-1, b.Min.Y); ny < min(sy+2, b.Max.Y); ny++ {
for nx := max(sx-1, b.Min.X); nx < min(sx+2, b.Max.X); nx++ {
v := NRGBA64At(s.source, nx, ny)
if v.A > best.A {
best = v
}
}
}
if best.A != 0 {
best.A = 1
c = best
}
}
if x == 0 || y == 0 || x == s.rect.Max.X-1 || y == s.rect.Max.Y-1 {
c.A = 1
}
return c
}
// At 返回非预乘十六位像素颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.Color 像素颜色
func (s *transparentEdgeImage64) At(x, y int) color.Color { return s.NRGBA64At(x, y) }
// RGBA64At 返回保留原采样精度的预乘颜色
// 入参: x 横向坐标, y 纵向坐标
// 返回: color.RGBA64 预乘颜色
func (s *transparentEdgeImage64) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.NRGBA64At(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
// Decode 解码图片数据
// 入参: data 图片数据
// 返回: image.Image 图片对象, string 图片格式, error 错误信息
func Decode(data []byte) (image.Image, string, error) {
return DecodeContext(context.Background(), data)
}
// DecodeContext 解码图片并传递取消状态,PNG和JPEG仍保留惰性像素解码
// 入参: ctx 取消上下文, data 只读图片编码
// 返回: image.Image 图片对象, string 图片格式, error 解码或取消错误
func DecodeContext(ctx context.Context, data []byte) (img image.Image, format string, err error) {
if err := ctx.Err(); err != nil {
return nil, "", err
}
defer func() {
if cancelErr := ctx.Err(); cancelErr != nil {
img, err = nil, cancelErr
}
}()
if IsJPEG(data) || IsPNG(data) {
img, err := newEncodedImageContext(ctx, data)
if err != nil {
return nil, "", err
}
return img, img.format, nil
}
if IsTIFF(data) {
layout, err := ReadTIFFLayoutContext(ctx, bytes.NewReader(data))
if err != nil {
return nil, "tiff", err
}
img, err := DecodeTIFF(ctx, data, layout)
return img, "tiff", err
}
img, format, err = image.Decode(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
if err == nil {
return img, NormalizeFormat(format), nil
}
if ctx.Err() != nil {
return nil, "", ctx.Err()
}
if img, err := DecodeBMPContext(ctx, data); err == nil {
return img, "bmp", nil
}
return nil, "", err
}
// DecodePixelsContext 完整解码图片,确保像素可读取并传递取消状态
// 入参: ctx 取消上下文, data 图片编码数据
// 返回: image.Image 解码像素, string 图片格式, error 解码或取消错误
func DecodePixelsContext(ctx context.Context, data []byte) (image.Image, string, error) {
img, format, err := DecodeContext(ctx, data)
if err != nil {
return nil, format, err
}
pixels, err := PixelDataContext(ctx, img)
return pixels, format, err
}
// IsJPEG 判断是否为JPEG图片数据
// 入参: data 图片数据
// 返回: bool 是否为JPEG图片数据
func IsJPEG(data []byte) bool {
return len(data) >= 2 && data[0] == 0xFF && data[1] == 0xD8
}
// IsPNG 判断是否为PNG图片数据
// 入参: data 图片数据
// 返回: bool 是否为PNG图片数据
func IsPNG(data []byte) bool {
return bytes.HasPrefix(data, []byte("\x89PNG\r\n\x1a\n"))
}
// DecodeConfigData 解码图片尺寸
// 入参: data 图片数据
// 返回: image.Config 图片尺寸, string 图片格式, error 错误信息
func DecodeConfigData(data []byte) (image.Config, string, error) {
return DecodeConfigDataContext(context.Background(), data)
}
// DecodeConfigDataContext 读取图片尺寸和颜色模型,传递布局解析中的取消状态
// 入参: ctx 取消上下文, data 图片数据
// 返回: image.Config 图片配置, string 图片格式, error 读取或取消错误
func DecodeConfigDataContext(ctx context.Context, data []byte) (config image.Config, format string, err error) {
if err := ctx.Err(); err != nil {
return image.Config{}, "", err
}
defer func() {
if cancelErr := ctx.Err(); cancelErr != nil {
config, err = image.Config{}, cancelErr
}
}()
if IsTIFF(data) {
layout, err := ReadTIFFLayoutContext(ctx, bytes.NewReader(data))
if err != nil {
return image.Config{}, "tiff", err
}
profile, err := readTIFFColorProfile(ctx, data, layout)
if err != nil {
return image.Config{}, "tiff", err
}
var cfg image.Config
if layout.needsSampleView() {
img, _, err := tiffPlaneDirectories(ctx, data, layout)
if err != nil {
return image.Config{}, "tiff", err
}
cfg.ColorModel = img.ColorModel()
} else {
input := newTIFFDirectoryInput(data, layout.Offset)
input.ctx = ctx
cfg, err = tiff.DecodeConfig(input)
if err != nil {
return image.Config{}, "tiff", err
}
}
if profile != nil || layout.Transparency != nil {
cfg.ColorModel = color.NRGBA64Model
}
if layout.Transparency != nil {
if _, err := readTIFFColorProfile(ctx, data, *layout.Transparency); err != nil {
return image.Config{}, "tiff", err
}
if _, _, err := tiffPlaneDirectories(ctx, data, *layout.Transparency); err != nil {
return image.Config{}, "tiff", err
}
}
size := layout.DisplaySize()
cfg.Width, cfg.Height = size.X, size.Y
return cfg, "tiff", nil
}
cfg, format, err := image.DecodeConfig(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(data)), context: ctx})
if err == nil {
return cfg, NormalizeFormat(format), nil
}
if cfg, err := decodeBMPConfig(data); err == nil {
return cfg, "bmp", nil
}
return image.Config{}, "", err
}
// decodeBMPConfig 解码BMP尺寸
// 入参: data 图片数据
// 返回: image.Config 图片尺寸, error 错误信息
func decodeBMPConfig(data []byte) (image.Config, error) {
offset, width, height, bpp, compression, err := ParseBMPHeader(data)
if err != nil {
return image.Config{}, err
}
if offset <= 0 || width <= 0 || height == 0 {
return image.Config{}, fmt.Errorf("unsupported bmp")
}
if _, err := BMPColorMasks(data, offset, bpp, compression); err != nil {
return image.Config{}, err
}
if height < 0 {
height = -height
}
return image.Config{ColorModel: color.NRGBAModel, Width: width, Height: height}, nil
}
// decodeBMPImage 解码BMP图片
// 入参: data 图片数据
// 返回: image.Image 图片对象, error 错误信息
func decodeBMPImage(data []byte) (image.Image, error) {
return DecodeBMPContext(context.Background(), data)
}
// DecodeBMPContext 解码BMP真彩色像素,逐行检查取消
// 入参: ctx 取消上下文, data 图片数据
// 返回: image.Image 图片对象, error 格式或取消错误
func DecodeBMPContext(ctx context.Context, data []byte) (image.Image, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
offset, width, height, bpp, compression, err := ParseBMPHeader(data)
if err != nil {
return nil, err
}
if offset > len(data) {
return nil, fmt.Errorf("invalid bmp pixel offset")
}
masks, err := BMPColorMasks(data, offset, bpp, compression)
if err != nil {
return nil, err
}
topDown := height < 0
if topDown {
height = -height
}
if width <= 0 || height <= 0 {
return nil, fmt.Errorf("invalid bmp size")
}
stride := ((int64(width)*int64(bpp) + 31) / 32) * 4
if stride > int64(len(data)-offset)/int64(height) {
return nil, fmt.Errorf("truncated bmp")
}
if width > int(^uint(0)>>1)/4/height {
return nil, fmt.Errorf("bmp dimensions exceed image capacity")
}
rowStride := int(stride)
img := image.NewNRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
if err := ctx.Err(); err != nil {
return nil, err
}
srcY := y
if !topDown {
srcY = height - 1 - y
}
row := data[offset+srcY*rowStride:]
for x := 0; x < width; x++ {
switch bpp {
case 16, 32:
var value uint32
if bpp == 16 {
value = uint32(binary.LittleEndian.Uint16(row[x*2:]))
} else {
value = binary.LittleEndian.Uint32(row[x*4:])
}
p := y*img.Stride + x*4
for i, mask := range masks {
img.Pix[p+i] = mask.sample(value)
}
case 24:
p := x * 3
img.SetNRGBA(x, y, color.NRGBA{R: row[p+2], G: row[p+1], B: row[p], A: 255})
}
}
}
return img, nil
}
// ParseBMPHeader 解析BMP文件头
// 入参: data 图片数据
// 返回: int 像素偏移, int 宽度, int 高度, uint16 位数, uint32 压缩方式, error 错误信息
func ParseBMPHeader(data []byte) (int, int, int, uint16, uint32, error) {
if len(data) < 54 || string(data[:2]) != "BM" {
return 0, 0, 0, 0, 0, fmt.Errorf("not bmp")
}
offset := int(binary.LittleEndian.Uint32(data[10:14]))
dibSize := binary.LittleEndian.Uint32(data[14:18])
if dibSize < 40 {
return 0, 0, 0, 0, 0, fmt.Errorf("unsupported bmp")
}
if uint64(dibSize)+14 > uint64(len(data)) || offset < 0 || uint64(offset) < uint64(dibSize)+14 {
return 0, 0, 0, 0, 0, fmt.Errorf("invalid bmp pixel offset")
}
width := int(int32(binary.LittleEndian.Uint32(data[18:22])))
height := int(int32(binary.LittleEndian.Uint32(data[22:26])))
planes := binary.LittleEndian.Uint16(data[26:28])
bpp := binary.LittleEndian.Uint16(data[28:30])
compression := binary.LittleEndian.Uint32(data[30:34])
if planes != 1 || width <= 0 || height == 0 || int64(height) == -int64(^uint(0)>>1)-1 {
return 0, 0, 0, 0, 0, fmt.Errorf("invalid bmp")
}
return offset, width, height, bpp, compression, nil
}
// PixelSource 获取图片像素源
// 入参: img 图片对象
// 返回: image.Image 图片像素源
func PixelSource(img image.Image) image.Image {
if decoded, err := PixelData(img); err == nil {
return decoded
}
return img
}
// PixelData 获取像素并保留惰性解码错误,供绘制和像素处理入口校验
// 入参: img 图片对象
// 返回: image.Image 解码像素, error 解码错误
func PixelData(img image.Image) (image.Image, error) {
if src, ok := img.(interface{ Image() (image.Image, error) }); ok {
return src.Image()
}
return img, nil
}
// PixelDataContext 获取像素并传递取消状态,兼容不接收上下文的图像实现
// 入参: ctx 取消上下文,nil使用后台上下文, img 图片对象
// 返回: image.Image 解码像素, error 解码或取消错误
func PixelDataContext(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 := PixelData(img)
if cancelErr := ctx.Err(); cancelErr != nil {
return nil, cancelErr
}
return pixels, err
}
// GrayAt 获取图片灰度像素
// 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.Gray 灰度像素
func GrayAt(img image.Image, x, y int) color.Gray {
if src, ok := img.(*image.Gray); ok {
return src.GrayAt(x, y)
}
if src, ok := img.(*image.Paletted); ok {
return color.GrayModel.Convert(src.At(x, y)).(color.Gray)
}
if src, ok := img.(image.RGBA64Image); ok {
r, g, b, _ := src.RGBA64At(x, y).RGBA()
return color.Gray{Y: uint8((19595*r + 38470*g + 7471*b + 1<<15) >> 24)}
}
return color.GrayModel.Convert(img.At(x, y)).(color.Gray)
}
// NRGBAAt 读取八位非预乘像素,缩减位深时保留原始颜色分量
// 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.NRGBA NRGBA像素
func NRGBAAt(img image.Image, x, y int) color.NRGBA {
switch src := img.(type) {
case interface{ NRGBAAt(int, int) color.NRGBA }:
return src.NRGBAAt(x, y)
case interface{ NRGBA64At(int, int) color.NRGBA64 }:
c := src.NRGBA64At(x, y)
return color.NRGBA{R: uint8(c.R >> 8), G: uint8(c.G >> 8), B: uint8(c.B >> 8), A: uint8(c.A >> 8)}
case *image.RGBA, *image.RGBA64, *image.Gray, *image.Gray16, *image.Alpha, *image.Alpha16, *image.CMYK, *image.YCbCr, *image.NYCbCrA:
c := src.(image.RGBA64Image).RGBA64At(x, y)
if c.A == 0 {
return color.NRGBA{}
}
r, g, b, a := uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A)
if a != 0xffff {
r = r * 0xffff / a
g = g * 0xffff / a
b = b * 0xffff / a
}
return color.NRGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: uint8(a >> 8)}
}
value := img.At(x, y)
if c, ok := value.(color.NRGBA64); ok {
return color.NRGBA{R: uint8(c.R >> 8), G: uint8(c.G >> 8), B: uint8(c.B >> 8), A: uint8(c.A >> 8)}
}
return color.NRGBAModel.Convert(value).(color.NRGBA)
}
// AlphaAt 读取十六位透明度,原生图像避免颜色装箱
// 入参: img 图像, x 横坐标, y 纵坐标
// 返回: uint32 十六位透明度
func AlphaAt(img image.Image, x, y int) uint32 {
if source, ok := img.(image.RGBA64Image); ok {
return uint32(source.RGBA64At(x, y).A)
}
_, _, _, alpha := img.At(x, y).RGBA()
return alpha
}
// HighPrecision 判断颜色模型及调色板是否需要保留十六位采样
// 入参: img 图片对象
// 返回: bool 是否使用十六位颜色或透明度
func HighPrecision(img image.Image) bool {
model := img.ColorModel()
if palette, ok := model.(color.Palette); ok {
for _, value := range palette {
switch value.(type) {
case color.RGBA, color.NRGBA, color.Gray, color.Alpha, color.CMYK, color.YCbCr, color.NYCbCrA:
default:
pixel := color.NRGBA64Model.Convert(value).(color.NRGBA64)
if pixel.R%257 != 0 || pixel.G%257 != 0 || pixel.B%257 != 0 || pixel.A%257 != 0 {
return true
}
}
}
return false
}
return model == color.NRGBA64Model || model == color.RGBA64Model || model == color.Gray16Model || model == color.Alpha16Model
}
// NRGBA64At 读取十六位非预乘像素,保留透明像素颜色,原生图像避免颜色装箱
// 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.NRGBA64 NRGBA64像素
func NRGBA64At(img image.Image, x, y int) color.NRGBA64 {
switch src := img.(type) {
case interface{ NRGBA64At(int, int) color.NRGBA64 }:
return src.NRGBA64At(x, y)
case *image.NRGBA:
c := src.NRGBAAt(x, y)
return color.NRGBA64{R: uint16(c.R) * 257, G: uint16(c.G) * 257, B: uint16(c.B) * 257, A: uint16(c.A) * 257}
case *image.Gray:
v := uint16(src.GrayAt(x, y).Y) * 257
return color.NRGBA64{R: v, G: v, B: v, A: 0xffff}
case *image.Gray16:
c := src.Gray16At(x, y)
return color.NRGBA64{R: c.Y, G: c.Y, B: c.Y, A: 0xffff}
case *image.Alpha, *image.Alpha16:
c := src.(image.RGBA64Image).RGBA64At(x, y)
if c.A == 0 {
return color.NRGBA64{}
}
return color.NRGBA64{R: 0xffff, G: 0xffff, B: 0xffff, A: c.A}
case *image.RGBA, *image.RGBA64, *image.CMYK, *image.YCbCr, *image.NYCbCrA:
c := src.(image.RGBA64Image).RGBA64At(x, y)
if c.A == 0 {
return color.NRGBA64{}
}
r, g, b, a := uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A)
if a != 0xffff {
r = r * 0xffff / a
g = g * 0xffff / a
b = b * 0xffff / a
}
return color.NRGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: c.A}
}
value := img.At(x, y)
if c, ok := value.(color.NRGBA); ok {
return color.NRGBA64{R: uint16(c.R) * 257, G: uint16(c.G) * 257, B: uint16(c.B) * 257, A: uint16(c.A) * 257}
}
return color.NRGBA64Model.Convert(value).(color.NRGBA64)
}
// transparentEdgeColor 获取透明像素相邻的可见颜色
// 入参: source 图片对象, x X坐标, y Y坐标
// 返回: 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 := NRGBAAt(source, nx, ny)
if pixel.A > best.A {
best = pixel
if best.A == 255 {
best.A = 1
return best, true
}
}
}
}
if best.A == 0 {
return color.NRGBA{}, false
}
best.A = 1
return best, true
}
bounds := img.Bounds()
best, alpha := 0, uint8(0)
left, right := max(x-1, bounds.Min.X), min(x+2, bounds.Max.X)
scan:
for ny := max(y-1, bounds.Min.Y); ny < min(y+2, bounds.Max.Y); ny++ {
offset := img.PixOffset(left, ny)
for nx := left; nx < right; nx++ {
if (nx != x || ny != y) && img.Pix[offset+3] > alpha {
best, alpha = offset, img.Pix[offset+3]
if alpha == 255 {
break scan
}
}
offset += 4
}
}
if alpha == 0 {
return color.NRGBA{}, false
}
return color.NRGBA{R: img.Pix[best], G: img.Pix[best+1], B: img.Pix[best+2], A: 1}, true
}
// transparentPaddingColor 获取透明补齐颜色
// 入参: c 边缘颜色
// 返回: color.NRGBA 补齐颜色
func transparentPaddingColor(c color.NRGBA) color.NRGBA {
c.A = 1
return c
}
// WithTransparentEdge 扫描透明边缘并共享只读像素,保留原图采样精度
// 入参: ctx 取消上下文,nil使用后台上下文, img 图片对象
// 返回: image.Image 补边图像, int 补边像素数, error 解码或取消错误
func WithTransparentEdge(ctx context.Context, img image.Image) (image.Image, int, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, 0, err
}
if img == nil {
return img, 0, nil
}
bounds := img.Bounds()
w, h := bounds.Dx(), bounds.Dy()
if w == 0 || h == 0 {
return img, 0, nil
}
if src, ok := img.(*EncodedImage); ok && src.MIME() == "image/jpeg" {
return img, 0, nil
}
source, err := PixelDataContext(ctx, img)
if err != nil {
return nil, 0, err
}
switch source.(type) {
case *image.Gray, *image.Gray16, *image.YCbCr, *image.CMYK:
return img, 0, nil
}
hasZero, hasVisible := false, false
var pixels []byte
var stride, step int
switch src := source.(type) {
case *image.NRGBA:
pixels, stride, step = src.Pix, src.Stride, 4
case *image.RGBA:
pixels, stride, step = src.Pix, src.Stride, 4
case *image.Alpha:
pixels, stride, step = src.Pix, src.Stride, 1
}
if pixels != nil {
hasZero = pixels[step-1] == 0
hasVisible = !hasZero
scan:
for y := 0; y < h; y++ {
if err := ctx.Err(); err != nil {
return nil, 0, err
}
offset := y*stride + step - 1
for x := 0; x < w; x++ {
if x&255 == 0 {
if err := ctx.Err(); err != nil {
return nil, 0, err
}
}
if (pixels[offset] == 0) != hasZero {
hasZero, hasVisible = true, true
break scan
}
offset += step
}
}
} else {
for y := bounds.Min.Y; y < bounds.Max.Y && !(hasZero && hasVisible); y++ {
if err := ctx.Err(); err != nil {
return nil, 0, err
}
for x := bounds.Min.X; x < bounds.Max.X; x++ {
if (x-bounds.Min.X)&255 == 0 {
if err := ctx.Err(); err != nil {
return nil, 0, err
}
}
if AlphaAt(source, x, y) == 0 {
hasZero = true
} else {
hasVisible = true
}
if hasZero && hasVisible {
break
}
}
}
}
if err := ctx.Err(); err != nil {
return nil, 0, err
}
if !hasZero || !hasVisible {
return img, 0, nil
}
if HighPrecision(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
}
+36
View File
@@ -0,0 +1,36 @@
// 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 image
import (
"context"
"io"
)
// contextInput 在读取图像编码时检查调用方取消状态
type contextInput struct {
io.ReadCloser
context context.Context
}
// Read 检查取消状态后读取原始数据
// 入参: data 数据缓冲区
// 返回: int 读取字节数, error 读取或取消错误
func (r contextInput) Read(data []byte) (int, error) {
if err := r.context.Err(); err != nil {
return 0, err
}
return r.ReadCloser.Read(data)
}
+234
View File
@@ -0,0 +1,234 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package image
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"hash/crc32"
)
// PageInfo 图片方向与像素密度,不改变原始编码
type PageInfo struct {
Orientation int
XDPI, YDPI float64
}
// PageMetadataContext 读取图片方向与密度,扫描目录或数据段时响应取消
// TIFF方向由解码器处理,JPEG的EXIF密度优先于JFIF
// 入参: ctx 取消上下文, data 图片数据, format 图像格式, dpi 缺少物理密度时使用的DPI
// 返回: PageInfo 方向与横纵DPI, error 元数据或取消错误
func PageMetadataContext(ctx context.Context, data []byte, format string, dpi float64) (PageInfo, error) {
info := PageInfo{Orientation: 1, XDPI: dpi, YDPI: dpi}
if err := ctx.Err(); err != nil {
return info, err
}
var exif []byte
var directory uint32
scaleX, scaleY := 1.0, 1.0
if format == "tiff" {
layout, err := ReadTIFFLayoutContext(ctx, bytes.NewReader(data))
if err != nil {
return info, err
}
directory = layout.Offset
exif = data
original, display := layout.size(), layout.DisplaySize()
scaleX, scaleY = float64(display.X)/float64(original.X), float64(display.Y)/float64(original.Y)
}
if format == "bmp" && len(data) >= 54 && binary.LittleEndian.Uint32(data[14:18]) >= 40 {
x := int32(binary.LittleEndian.Uint32(data[38:42]))
y := int32(binary.LittleEndian.Uint32(data[42:46]))
if x > 0 && y > 0 {
info.XDPI, info.YDPI = float64(x)*0.0254, float64(y)*0.0254
}
}
if format == "png" {
for offset := 8; offset+12 <= len(data); {
if err := ctx.Err(); err != nil {
return info, err
}
n := uint64(binary.BigEndian.Uint32(data[offset:]))
if n > uint64(len(data)-offset-12) {
return info, fmt.Errorf("truncated PNG chunk")
}
end := offset + 8 + int(n)
kind, payload := string(data[offset+4:offset+8]), data[offset+8:end]
if kind == "pHYs" || kind == "eXIf" {
if crc32.ChecksumIEEE(data[offset+4:end]) != binary.BigEndian.Uint32(data[end:]) {
return info, fmt.Errorf("invalid PNG %s checksum", kind)
}
}
if kind == "pHYs" && len(payload) == 9 {
x, y := float64(binary.BigEndian.Uint32(payload)), float64(binary.BigEndian.Uint32(payload[4:]))
if x > 0 && y > 0 {
if payload[8] == 1 {
info.XDPI, info.YDPI = x*0.0254, y*0.0254
} else if payload[8] == 0 {
info.YDPI = dpi * y / x
}
}
} else if kind == "eXIf" {
exif = payload
}
offset = end + 4
if kind == "IEND" {
break
}
}
} else if format == "jpeg" {
for offset := 2; offset < len(data); {
if err := ctx.Err(); err != nil {
return info, err
}
if data[offset] != 0xff {
return info, fmt.Errorf("invalid JPEG marker")
}
for offset < len(data) && data[offset] == 0xff {
offset++
}
if offset >= len(data) {
break
}
marker := data[offset]
offset++
if marker == 0xda || marker == 0xd9 {
break
}
if marker == 0x01 || marker >= 0xd0 && marker <= 0xd7 {
continue
}
if offset+2 > len(data) {
return info, fmt.Errorf("truncated JPEG marker")
}
n := int(binary.BigEndian.Uint16(data[offset:]))
if n < 2 || n > len(data)-offset {
return info, fmt.Errorf("truncated JPEG segment")
}
payload := data[offset+2 : offset+n]
if marker == 0xe0 && len(payload) >= 14 && bytes.Equal(payload[:5], []byte("JFIF\x00")) {
x, y := float64(binary.BigEndian.Uint16(payload[8:])), float64(binary.BigEndian.Uint16(payload[10:]))
if x > 0 && y > 0 {
switch payload[7] {
case 0:
info.YDPI = dpi * y / x
case 1:
info.XDPI, info.YDPI = x, y
case 2:
info.XDPI, info.YDPI = x*2.54, y*2.54
}
}
} else if marker == 0xe1 && bytes.HasPrefix(payload, []byte("Exif\x00\x00")) {
exif = payload[6:]
}
offset += n
}
}
if len(exif) != 0 {
orientation, x, y, err := imageExifMetadata(exif, dpi, directory)
if err != nil {
return info, err
}
info.Orientation = orientation
if (format == "jpeg" || format == "tiff") && x > 0 && y > 0 {
info.XDPI, info.YDPI = x, y
}
if format == "tiff" {
if orientation >= 5 {
info.XDPI, info.YDPI = info.YDPI, info.XDPI
}
info.Orientation = 1
}
}
info.XDPI, info.YDPI = info.XDPI*scaleX, info.YDPI*scaleY
return info, ctx.Err()
}
// imageExifMetadata 只读取主图IFD中的方向和密度,不跟随缩略图与其他目录
// 入参: data TIFF或EXIF数据, dpi 无绝对密度单位时的横向DPI,零值不换算, directory 目录偏移,零值使用首目录
// 返回: int EXIF方向值,未声明时为1, float64 横向DPI, float64 纵向DPI, error 元数据错误
func imageExifMetadata(data []byte, dpi float64, directory uint32) (int, float64, float64, error) {
invalid := fmt.Errorf("invalid EXIF image metadata")
if len(data) < 8 {
return 0, 0, 0, invalid
}
var order binary.ByteOrder
switch string(data[:2]) {
case "II":
order = binary.LittleEndian
case "MM":
order = binary.BigEndian
default:
return 0, 0, 0, invalid
}
if order.Uint16(data[2:]) != 42 {
return 0, 0, 0, invalid
}
offset := uint64(order.Uint32(data[4:]))
if directory != 0 {
offset = uint64(directory)
}
if offset < 8 || offset+2 > uint64(len(data)) {
return 0, 0, 0, invalid
}
count := uint64(order.Uint16(data[offset:]))
if count*12+offset+6 > uint64(len(data)) {
return 0, 0, 0, invalid
}
orientation, unit := 1, 2
var x, y float64
for i := uint64(0); i < count; i++ {
entry := data[offset+2+i*12 : offset+14+i*12]
tag, kind, count := order.Uint16(entry), order.Uint16(entry[2:]), order.Uint32(entry[4:])
switch tag {
case 0x112, 0x128:
if kind != 3 || count != 1 {
return 0, 0, 0, invalid
}
value := int(order.Uint16(entry[8:]))
if tag == 0x112 {
if value < 1 || value > 8 {
return 0, 0, 0, invalid
}
orientation = value
} else {
unit = value
}
case 0x11a, 0x11b:
at := uint64(order.Uint32(entry[8:]))
if kind != 5 || count != 1 || at+8 > uint64(len(data)) {
return 0, 0, 0, invalid
}
n, d := order.Uint32(data[at:]), order.Uint32(data[at+4:])
if n > 0 && d > 0 {
if tag == 0x11a {
x = float64(n) / float64(d)
} else {
y = float64(n) / float64(d)
}
}
}
}
if unit == 3 {
x, y = x*2.54, y*2.54
} else if unit == 1 && dpi > 0 && x > 0 && y > 0 {
x, y = dpi, dpi*y/x
} else if unit != 2 {
x, y = 0, 0
}
return orientation, x, y, nil
}
+119
View File
@@ -0,0 +1,119 @@
// 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 image
import "image"
// StorageCost 预留十六位像素空间,并计入编码、调色板与底层像素缓冲容量
// 包装图像可实现BackingImage() image.Image,使底层共享缓冲计入预算
// 入参: img 图片资源, limit 缓存预算
// 返回: int 估算字节数或超预算标记
func StorageCost(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
}
pixels := w * h * 8
if encoded, ok := img.(*EncodedImage); ok {
if cap(encoded.data) > limit-cost-pixels {
return limit + 1
}
return cost + pixels + cap(encoded.data)
}
var buffers [5]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 = [5]int{cap(source.Y), cap(source.Cb), cap(source.Cr)}
case *image.NYCbCrA:
buffers = [5]int{cap(source.Y), cap(source.Cb), cap(source.Cr), cap(source.A)}
case *planarTIFFImage:
if cap(source.palette) > (limit-cost)/8 {
return limit + 1
}
cost += cap(source.palette) * 8
if cap(source.planes) > (limit-cost)/16 || len(source.planes) > len(buffers) {
return limit + 1
}
cost += cap(source.planes) * 16
for i, plane := range source.planes {
switch plane := plane.(type) {
case *image.Gray:
buffers[i] = cap(plane.Pix)
case *image.Gray16:
buffers[i] = cap(plane.Pix)
default:
buffers[i] = StorageCost(plane, limit-cost)
}
}
case *tiffFaxImage:
if cap(source.blocks) > (limit-cost)/16 {
return limit + 1
}
cost += cap(source.blocks) * 16
for _, block := range source.blocks {
size := StorageCost(block, limit-cost)
if size > limit-cost {
return limit + 1
}
cost += size
}
case *orientedTIFFImage:
buffers[0] = StorageCost(source.source, limit-cost)
case *maskedTIFFImage:
buffers[0] = StorageCost(source.source, limit-cost)
buffers[1] = StorageCost(source.mask, limit-cost)
case *transparentEdgeImage:
buffers[0] = StorageCost(source.source, limit-cost)
case *transparentEdgeImage64:
buffers[0] = StorageCost(source.source, limit-cost)
case interface{ BackingImage() image.Image }:
buffers[0] = StorageCost(source.BackingImage(), limit-cost)
}
storage := 0
for _, size := range buffers {
if size > limit-cost-storage {
return limit + 1
}
storage += size
}
if pixels > limit-cost {
return limit + 1
}
return cost + max(pixels, storage)
}
+433
View File
@@ -0,0 +1,433 @@
// 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 image
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"image"
"image/color"
"io"
)
// ErrTIFFRandomAccess 表示目录链需要回读,仅流式输入需重新打开资源
var ErrTIFFRandomAccess = errors.New("TIFF directory requires random access")
// TIFFLayout 保存TIFF目录的图像布局及关联透明蒙版
type TIFFLayout struct {
Width, Height int
TileWidth int
TileHeight int
Orientation int
Offset, Next uint32
Reduced, mask bool
Planar bool
auxiliary bool
Samples uint32
depth uint32
fillOrder uint32
compression uint32
predictor uint32
photometric uint32
profileOffset uint32
profileSize uint32
t4, t6 uint32
Transparency *TIFFLayout
}
// tiffDirectoryInput 只替换解码视图中的首目录偏移,不复制或改写原始资源
type tiffDirectoryInput struct {
reader *bytes.Reader
directory [4]byte
ctx context.Context
}
// orientedTIFFImage 按方向映射像素坐标,复用原图数据与颜色精度
type orientedTIFFImage struct {
source image.Image
TIFFLayout
}
// IsTIFF 识别标准TIFF字节序与版本标识
// 入参: data 文件头或完整编码
// 返回: bool 是否为TIFF 6.0编码
func IsTIFF(data []byte) bool {
return len(data) >= 4 && (string(data[:4]) == "II\x2a\x00" || string(data[:4]) == "MM\x00\x2a")
}
// readTIFFLayout 选择主图及其蒙版,允许缩略图并拒绝循环和多个主图
// 入参: input 从文件头开始的输入流
// 返回: TIFFLayout 存储尺寸与方向, error 文件结构或方向错误
func readTIFFLayout(input io.Reader) (TIFFLayout, error) {
return ReadTIFFLayoutContext(context.Background(), input)
}
// ReadTIFFLayoutContext 扫描目录链并选择主图,读取期间响应取消
// 入参: ctx 取消上下文, input 从文件头开始的输入流
// 返回: TIFFLayout 存储尺寸与方向, error 结构、读取或取消错误
func ReadTIFFLayoutContext(ctx context.Context, input io.Reader) (TIFFLayout, error) {
var result TIFFLayout
if err := ctx.Err(); err != nil {
return result, err
}
var header [8]byte
if _, err := io.ReadFull(input, header[:]); err != nil {
return result, err
}
if !IsTIFF(header[:]) {
return result, fmt.Errorf("invalid TIFF header")
}
var order binary.ByteOrder = binary.LittleEndian
if header[0] == 'M' {
order = binary.BigEndian
}
offset := int64(order.Uint32(header[4:]))
position := int64(8)
var seen map[uint32]bool
var transparency *TIFFLayout
for offset != 0 {
if seen != nil && seen[uint32(offset)] {
return result, fmt.Errorf("cyclic TIFF directory chain")
}
layout, end, err := readTIFFDirectory(ctx, input, order, offset, position)
if err != nil {
return result, err
}
if layout.mask {
if !layout.Reduced {
if transparency != nil {
return result, fmt.Errorf("multiple TIFF transparency masks")
}
transparency = &layout
}
} else if !layout.Reduced || layout.Next == 0 && seen == nil {
if result.Offset != 0 {
return result, fmt.Errorf("multiple TIFF pages are not supported in OFD resources")
}
result = layout
}
if layout.Next != 0 {
if seen == nil {
seen = make(map[uint32]bool)
}
seen[uint32(offset)] = true
}
offset, position = int64(layout.Next), end
}
if err := ctx.Err(); err != nil {
return TIFFLayout{}, err
}
if result.Offset == 0 {
return result, fmt.Errorf("missing TIFF main image")
}
result.Transparency = transparency
if transparency != nil {
size := result.DisplaySize()
if size.X > int(^uint(0)>>1)/8/size.Y {
return TIFFLayout{}, fmt.Errorf("TIFF mask image dimensions exceed capacity")
}
}
return result, nil
}
// readTIFFDirectory 读取目录中的图像布局和子文件类型,不读取像素
// 入参: ctx 取消上下文, input 输入流, order 字节序, offset 目录偏移, position 当前流位置
// 返回: TIFFLayout 目录布局, int64 读取后的流位置, error 结构、读取或取消错误
func readTIFFDirectory(ctx context.Context, input io.Reader, order binary.ByteOrder, offset, position int64) (TIFFLayout, int64, error) {
var result TIFFLayout
if err := ctx.Err(); err != nil {
return result, position, err
}
var header [4]byte
if offset < 8 {
return result, position, fmt.Errorf("invalid TIFF directory offset")
}
if seeker, ok := input.(io.Seeker); ok {
if _, err := seeker.Seek(offset, io.SeekStart); err != nil {
return result, position, err
}
} else {
if offset < position {
return result, position, ErrTIFFRandomAccess
}
reader := contextInput{ReadCloser: io.NopCloser(input), context: ctx}
if _, err := io.CopyN(io.Discard, reader, offset-position); err != nil {
return result, position, err
}
}
if _, err := io.ReadFull(input, header[:2]); err != nil {
return result, position, err
}
count := int(order.Uint16(header[:2]))
result.Orientation, result.Offset, result.Samples, result.fillOrder = 1, uint32(offset), 1, 1
result.depth = 1
result.compression = 1
result.predictor = 1
var entry [12]byte
var previous uint16
var subfile, legacy uint32
var hasSubfile bool
var invalidMaskDepth bool
for i := range count {
if err := ctx.Err(); err != nil {
return result, position, err
}
if _, err := io.ReadFull(input, entry[:]); err != nil {
return result, position, err
}
tag, kind, number := order.Uint16(entry[:2]), order.Uint16(entry[2:4]), order.Uint32(entry[4:8])
if i > 0 && tag <= previous {
return result, position, fmt.Errorf("invalid TIFF directory order")
}
previous = tag
if tag == 338 {
result.auxiliary = kind != 3 || number != 1 || order.Uint16(entry[8:]) == 0
}
if tag == 34675 {
if kind != 7 || number < 132 {
return result, position, fmt.Errorf("invalid TIFF color profile")
}
result.profileOffset, result.profileSize = order.Uint32(entry[8:]), number
continue
}
if tag == 258 {
invalidMaskDepth = kind != 3 || number != 1
if !invalidMaskDepth {
result.depth = uint32(order.Uint16(entry[8:10]))
}
}
if tag != 254 && tag != 255 && tag != 256 && tag != 257 && tag != 259 && tag != 262 && tag != 266 && tag != 274 && tag != 277 && tag != 284 && tag != 292 && tag != 293 && tag != 317 && tag != 322 && tag != 323 {
continue
}
if number != 1 || kind != 3 && kind != 4 || (tag == 255 || tag == 259 || tag == 262 || tag == 266 || tag == 274 || tag == 277 || tag == 284 || tag == 317) && kind != 3 || (tag == 254 || tag == 292 || tag == 293) && kind != 4 {
return result, position, fmt.Errorf("invalid TIFF image layout")
}
value := uint64(order.Uint32(entry[8:12]))
if kind == 3 {
value = uint64(order.Uint16(entry[8:10]))
}
if (tag == 256 || tag == 257 || tag == 322 || tag == 323) && (value == 0 || value > uint64(int(^uint(0)>>1))) {
return result, position, fmt.Errorf("invalid TIFF dimensions")
}
switch tag {
case 254:
subfile, hasSubfile = uint32(value), true
case 255:
legacy = uint32(value)
case 256:
result.Width = int(value)
case 257:
result.Height = int(value)
case 259:
result.compression = uint32(value)
case 262:
result.photometric = uint32(value)
result.mask = value == 4
case 266:
if value != 1 && value != 2 {
return result, position, fmt.Errorf("invalid TIFF fill order")
}
result.fillOrder = uint32(value)
case 274:
if value < 1 || value > 8 {
return result, position, fmt.Errorf("invalid TIFF orientation")
}
result.Orientation = int(value)
case 277:
if value == 0 {
return result, position, fmt.Errorf("invalid TIFF sample count")
}
result.Samples = uint32(value)
case 284:
if value != 1 && value != 2 {
return result, position, fmt.Errorf("invalid TIFF planar configuration")
}
result.Planar = value == 2
case 292:
result.t4 = uint32(value)
case 293:
result.t6 = uint32(value)
case 317:
result.predictor = uint32(value)
case 322:
result.TileWidth = int(value)
case 323:
result.TileHeight = int(value)
}
}
if _, err := io.ReadFull(input, header[:4]); err != nil {
return result, position, err
}
result.Next = order.Uint32(header[:4])
if hasSubfile {
result.Reduced, result.mask = subfile&1 != 0, result.mask || subfile&4 != 0
} else {
result.Reduced = legacy == 2
}
if result.Width == 0 || result.Height == 0 {
return result, position, fmt.Errorf("missing TIFF dimensions")
}
if (result.TileWidth == 0) != (result.TileHeight == 0) || result.TileWidth%16 != 0 || result.TileHeight%16 != 0 {
return result, position, fmt.Errorf("invalid TIFF tile dimensions")
}
if result.mask && (invalidMaskDepth || result.photometric != 4 || result.Samples != 1 || result.depth != 1) {
return result, position, fmt.Errorf("invalid TIFF transparency mask")
}
return result, offset + 6 + int64(count)*12, nil
}
// newTIFFDirectoryInput 创建以指定目录作为主图的只读解码视图
// 入参: data 已校验的TIFF编码, offset 主图目录偏移
// 返回: *tiffDirectoryInput 随机访问输入
func newTIFFDirectoryInput(data []byte, offset uint32) *tiffDirectoryInput {
input := &tiffDirectoryInput{reader: bytes.NewReader(data)}
var order binary.ByteOrder = binary.LittleEndian
if data[0] == 'M' {
order = binary.BigEndian
}
order.PutUint32(input.directory[:], offset)
return input
}
// patch 替换读取片段中与首目录指针重叠的部分
// 入参: data 读取结果, offset 文件偏移
func (r *tiffDirectoryInput) patch(data []byte, offset int64) {
for i := range r.directory {
at := int64(4+i) - offset
if at >= 0 && at < int64(len(data)) {
data[at] = r.directory[i]
}
}
}
// Read 读取图像编码并覆盖首目录指针
// 入参: data 接收缓冲
// 返回: int 读取字节数, error 读取错误
func (r *tiffDirectoryInput) Read(data []byte) (int, error) {
if r.ctx != nil {
if err := r.ctx.Err(); err != nil {
return 0, err
}
}
offset := r.reader.Size() - int64(r.reader.Len())
n, err := r.reader.Read(data)
r.patch(data[:n], offset)
return n, err
}
// ReadAt 随机读取图像编码并覆盖首目录指针
// 入参: data 接收缓冲, offset 文件偏移
// 返回: int 读取字节数, error 读取错误
func (r *tiffDirectoryInput) ReadAt(data []byte, offset int64) (int, error) {
if r.ctx != nil {
if err := r.ctx.Err(); err != nil {
return 0, err
}
}
n, err := r.reader.ReadAt(data, offset)
r.patch(data[:n], offset)
return n, err
}
// size 返回应用方向后的显示尺寸
// 返回: image.Point 像素宽高
func (l TIFFLayout) size() image.Point {
if l.Orientation >= 5 {
return image.Pt(l.Height, l.Width)
}
return image.Pt(l.Width, l.Height)
}
// DisplaySize 返回同时保留主图和透明蒙版分辨率的显示尺寸
// 返回: image.Point 保留高分辨率蒙版边缘的像素宽高
func (l TIFFLayout) DisplaySize() image.Point {
size := l.size()
if l.Transparency != nil {
mask := l.Transparency.size()
size.X, size.Y = max(size.X, mask.X), max(size.Y, mask.Y)
}
return size
}
// Bounds 返回从零开始的显示区域
// 返回: image.Rectangle 显示区域
func (s *orientedTIFFImage) Bounds() image.Rectangle {
return image.Rectangle{Max: s.size()}
}
// ColorModel 返回原始颜色模型
// 返回: color.Model 颜色模型
func (s *orientedTIFFImage) ColorModel() color.Model { return s.source.ColorModel() }
// sourcePoint 将显示坐标映射为存储坐标
// 入参: x 横向显示坐标, y 纵向显示坐标
// 返回: image.Point 原始坐标, bool 是否位于显示区域内
func (s *orientedTIFFImage) sourcePoint(x, y int) (image.Point, bool) {
if !image.Pt(x, y).In(s.Bounds()) {
return image.Point{}, false
}
w, h := s.Width, s.Height
switch s.Orientation {
case 2:
x = w - 1 - x
case 3:
x, y = w-1-x, h-1-y
case 4:
y = h - 1 - y
case 5:
x, y = y, x
case 6:
x, y = y, h-1-x
case 7:
x, y = w-1-y, h-1-x
case 8:
x, y = w-1-y, x
}
b := s.source.Bounds()
return image.Pt(b.Min.X+x, b.Min.Y+y), true
}
// At 返回原图颜色,不重新采样或量化
// 入参: x 横向显示坐标, y 纵向显示坐标
// 返回: color.Color 像素颜色,区域外透明
func (s *orientedTIFFImage) At(x, y int) color.Color {
p, ok := s.sourcePoint(x, y)
if !ok {
return color.NRGBA64{}
}
return s.source.At(p.X, p.Y)
}
// NRGBA64At 读取非预乘十六位颜色,保留透明像素中的原始颜色
// 入参: x 横向显示坐标, y 纵向显示坐标
// 返回: color.NRGBA64 像素颜色
func (s *orientedTIFFImage) NRGBA64At(x, y int) color.NRGBA64 {
p, ok := s.sourcePoint(x, y)
if !ok {
return color.NRGBA64{}
}
return NRGBA64At(s.source, p.X, p.Y)
}
// RGBA64At 返回预乘十六位像素,保留原始颜色精度
// 入参: x 横向显示坐标, y 纵向显示坐标
// 返回: color.RGBA64 像素颜色
func (s *orientedTIFFImage) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.At(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
+189
View File
@@ -0,0 +1,189 @@
// 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 image
import (
"cmp"
"context"
"encoding/binary"
"fmt"
"image"
"io"
"math/bits"
"slices"
"sort"
"golang.org/x/image/tiff"
)
// tiffBitOrderInput 仅在读取像素块时反转字节内位序,保留目录和原始数据
type tiffBitOrderInput struct {
source io.ReaderAt
ranges [][2]int64
position int64
}
// tiffBitOrderRanges 收集低位优先的未压缩一位图像块,压缩传真位序由解码器处理
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局
// 返回: [][2]int64 按偏移排序且不重叠的字节区间, error 字段或布局错误
func tiffBitOrderRanges(ctx context.Context, data []byte, layout TIFFLayout) ([][2]int64, error) {
if layout.fillOrder != 2 {
return nil, nil
}
var order binary.ByteOrder = binary.LittleEndian
if data[0] == 'M' {
order = binary.BigEndian
}
offset := int(layout.Offset)
count := int(order.Uint16(data[offset:]))
entries := make(map[uint16][]byte, count)
for i := range count {
entry := data[offset+2+i*12 : offset+14+i*12]
entries[order.Uint16(entry)] = entry
}
compression := entries[259]
method := uint16(1)
if compression != nil {
if order.Uint16(compression[2:]) != 3 || order.Uint32(compression[4:]) != 1 {
return nil, fmt.Errorf("invalid TIFF compression field")
}
method = order.Uint16(compression[8:])
}
if method == 3 || method == 4 {
return nil, nil
}
if method != 1 {
return nil, fmt.Errorf("unsupported TIFF fill order for compression %d", method)
}
if entry := entries[258]; entry != nil {
if order.Uint16(entry[2:]) != 3 || order.Uint32(entry[4:]) != layout.Samples {
return nil, fmt.Errorf("invalid TIFF sample depths")
}
for i := uint32(0); i < layout.Samples; i++ {
depth, err := tiffPlaneValue(data, order, entry, i)
if err != nil || depth != 1 {
return nil, fmt.Errorf("unsupported TIFF fill order for sample depth")
}
}
}
locations, lengths := entries[273], entries[279]
value := func(tag uint16, fallback uint32) (uint32, error) {
entry := entries[tag]
if entry == nil {
return fallback, nil
}
if order.Uint32(entry[4:]) != 1 {
return 0, fmt.Errorf("invalid TIFF scalar field %d", tag)
}
return tiffPlaneValue(data, order, entry, 0)
}
rows, err := value(278, ^uint32(0))
if err != nil || rows == 0 {
return nil, fmt.Errorf("invalid TIFF rows per strip")
}
expected := (uint64(layout.Height)-1)/uint64(rows) + 1
if entries[322] != nil {
locations, lengths = entries[324], entries[325]
width, err := value(322, 0)
if err != nil || width == 0 || width%16 != 0 {
return nil, fmt.Errorf("invalid TIFF tile width")
}
height, err := value(323, 0)
if err != nil || height == 0 || height%16 != 0 {
return nil, fmt.Errorf("invalid TIFF tile height")
}
expected = ((uint64(layout.Width)-1)/uint64(width) + 1) * ((uint64(layout.Height)-1)/uint64(height) + 1)
if locations != nil && order.Uint16(locations[2:]) != 4 {
return nil, fmt.Errorf("invalid TIFF tile offsets")
}
}
if layout.Planar {
if expected > uint64(^uint32(0))/uint64(layout.Samples) {
return nil, fmt.Errorf("too many TIFF image blocks")
}
expected *= uint64(layout.Samples)
}
if locations == nil || lengths == nil {
return nil, fmt.Errorf("missing TIFF block arrays")
}
blocks := order.Uint32(locations[4:])
if blocks == 0 || uint64(blocks) != expected || order.Uint32(lengths[4:]) != blocks {
return nil, fmt.Errorf("invalid TIFF block count")
}
var ranges [][2]int64
for i := uint32(0); i < blocks; i++ {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, err
}
}
at, err := tiffPlaneValue(data, order, locations, i)
if err != nil {
return nil, err
}
size, err := tiffPlaneValue(data, order, lengths, i)
if err != nil || size == 0 || uint64(at)+uint64(size) > uint64(len(data)) {
return nil, fmt.Errorf("truncated TIFF image block")
}
ranges = append(ranges, [2]int64{int64(at), int64(at) + int64(size)})
}
slices.SortFunc(ranges, func(a, b [2]int64) int { return cmp.Compare(a[0], b[0]) })
merged := ranges[:0]
for _, span := range ranges {
if len(merged) != 0 && span[0] <= merged[len(merged)-1][1] {
merged[len(merged)-1][1] = max(merged[len(merged)-1][1], span[1])
} else {
merged = append(merged, span)
}
}
return merged, nil
}
// decodeTIFFBitOrder 使用只读视图校正位序,不分配整幅编码的副本
// 入参: input 原始解码视图, ranges 需要反转位序的像素区间
// 返回: image.Image 解码图像, error 解码错误
func decodeTIFFBitOrder(input interface {
io.Reader
io.ReaderAt
}, ranges [][2]int64) (image.Image, error) {
if len(ranges) == 0 {
return tiff.Decode(input)
}
return tiff.Decode(&tiffBitOrderInput{source: input, ranges: ranges})
}
// Read 顺序读取并校正像素字节的位序
// 入参: data 接收缓冲
// 返回: int 读取字节数, error 读取错误
func (r *tiffBitOrderInput) Read(data []byte) (int, error) {
n, err := r.ReadAt(data, r.position)
r.position += int64(n)
return n, err
}
// ReadAt 按文件偏移读取,只反转实际读取到的像素字节
// 入参: data 接收缓冲, offset 文件偏移
// 返回: int 读取字节数, error 读取错误
func (r *tiffBitOrderInput) ReadAt(data []byte, offset int64) (int, error) {
n, err := r.source.ReadAt(data, offset)
index := sort.Search(len(r.ranges), func(i int) bool { return r.ranges[i][1] > offset })
for ; index < len(r.ranges) && r.ranges[index][0] < offset+int64(n); index++ {
span := r.ranges[index]
for i := max(int64(0), span[0]-offset); i < min(int64(n), span[1]-offset); i++ {
data[i] = bits.Reverse8(data[i])
}
}
return n, err
}
+185
View File
@@ -0,0 +1,185 @@
// 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 image
import (
"context"
"encoding/binary"
"fmt"
"image"
"image/color"
"math/bits"
"github.com/xiaoqidun/pdfgo"
)
// tiffFaxImage 组合独立解码的传真条带或瓦片,保留紧凑的一位像素存储
type tiffFaxImage struct {
blocks []image.Image
bounds image.Rectangle
width int
height int
across int
}
// decodeTIFFFax 按TIFF传真选项解码各图像块,复用PDFGo的T.4与T.6解码器
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局
// 返回: image.Image 图像, error 字段、解码或取消错误
func decodeTIFFFax(ctx context.Context, data []byte, layout TIFFLayout) (image.Image, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, err
}
if layout.Samples != 1 || layout.compression == 3 && layout.t4 & ^uint32(7) != 0 || layout.compression == 4 && layout.t6 & ^uint32(2) != 0 {
return nil, fmt.Errorf("invalid TIFF fax options")
}
var order binary.ByteOrder = binary.LittleEndian
if data[0] == 'M' {
order = binary.BigEndian
}
offset := int(layout.Offset)
count := int(order.Uint16(data[offset:]))
entries := make(map[uint16][]byte, count)
for i := range count {
entry := data[offset+2+i*12 : offset+14+i*12]
entries[order.Uint16(entry)] = entry
}
value := func(tag uint16, fallback uint32) (uint32, error) {
entry := entries[tag]
if entry == nil {
return fallback, nil
}
if order.Uint32(entry[4:]) != 1 {
return 0, fmt.Errorf("invalid TIFF scalar field %d", tag)
}
return tiffPlaneValue(data, order, entry, 0)
}
for _, tag := range []uint16{258, 317, 339} {
v, err := value(tag, 1)
if err != nil || v != 1 || entries[tag] != nil && order.Uint16(entries[tag][2:]) != 3 {
return nil, fmt.Errorf("invalid TIFF fax sample field %d", tag)
}
}
photo, err := value(262, 2)
if err != nil || photo > 1 && photo != 4 || entries[338] != nil {
return nil, fmt.Errorf("invalid TIFF fax color model")
}
width := uint32(layout.Width)
height, err := value(278, ^uint32(0))
if err != nil || height == 0 {
return nil, fmt.Errorf("invalid TIFF rows per strip")
}
height = min(height, uint32(layout.Height))
locations, lengths := entries[273], entries[279]
tiled := entries[322] != nil
if tiled {
width, err = value(322, 0)
if err != nil || width == 0 || width%16 != 0 {
return nil, fmt.Errorf("invalid TIFF tile width")
}
height, err = value(323, 0)
if err != nil || height == 0 || height%16 != 0 {
return nil, fmt.Errorf("invalid TIFF tile height")
}
locations, lengths = entries[324], entries[325]
if locations != nil && order.Uint16(locations[2:]) != 4 {
return nil, fmt.Errorf("invalid TIFF tile offsets")
}
}
if uint64(width) > uint64(int(^uint(0)>>1)) || uint64(height) > uint64(int(^uint(0)>>1)) || locations == nil || lengths == nil {
return nil, fmt.Errorf("invalid TIFF fax blocks")
}
across := (uint64(layout.Width)-1)/uint64(width) + 1
down := (uint64(layout.Height)-1)/uint64(height) + 1
blocks := across * down
if blocks != uint64(order.Uint32(locations[4:])) || blocks != uint64(order.Uint32(lengths[4:])) {
return nil, fmt.Errorf("invalid TIFF fax block count")
}
result := &tiffFaxImage{bounds: image.Rect(0, 0, layout.Width, layout.Height), width: int(width), height: int(height), across: int(across)}
for i := uint64(0); i < blocks; i++ {
if err := ctx.Err(); err != nil {
return nil, err
}
at, err := tiffPlaneValue(data, order, locations, uint32(i))
if err != nil {
return nil, err
}
size, err := tiffPlaneValue(data, order, lengths, uint32(i))
if err != nil || size == 0 || uint64(at)+uint64(size) > uint64(len(data)) {
return nil, fmt.Errorf("truncated TIFF fax block")
}
encoded := data[uint64(at) : uint64(at)+uint64(size)]
if layout.fillOrder == 2 {
reversed := make([]byte, len(encoded))
for start := 0; start < len(encoded); {
if err := ctx.Err(); err != nil {
return nil, err
}
end := start + min(32768, len(encoded)-start)
for j := start; j < end; j++ {
reversed[j] = bits.Reverse8(encoded[j])
}
start = end
}
encoded = reversed
}
rows := height
if !tiled {
rows = min(height, uint32(uint64(layout.Height)-i*uint64(height)))
}
k := pdfgo.Integer(0)
if layout.compression == 4 {
k = -1
} else if layout.compression == 3 && layout.t4&1 != 0 {
k = 1
}
params := pdfgo.Dictionary{
"K": k, "Columns": pdfgo.Integer(width), "Rows": pdfgo.Integer(rows),
"EndOfBlock": pdfgo.Boolean(false), "EndOfLine": pdfgo.Boolean(layout.compression == 3),
"EncodedByteAlign": pdfgo.Boolean(layout.compression == 2), "BlackIs1": pdfgo.Boolean(photo == 1 || photo == 4),
}
object := &pdfgo.Image{Width: int(width), Height: int(rows), BitsPerComponent: 1, ColorSpace: pdfgo.Name("DeviceGray"), Stream: &pdfgo.Stream{Dictionary: pdfgo.Dictionary{"Filter": pdfgo.Name("CCITTFaxDecode"), "DecodeParms": params}, Data: encoded}}
pixels, err := object.DecodeSamplesContext(ctx)
if err != nil {
return nil, fmt.Errorf("TIFF fax block %d: %w", i, err)
}
result.blocks = append(result.blocks, pixels)
}
if err := ctx.Err(); err != nil {
return nil, err
}
return result, nil
}
// Bounds 返回主图显示区域,不包含边缘瓦片的填充像素
// 返回: image.Rectangle 图像区域
func (s *tiffFaxImage) Bounds() image.Rectangle { return s.bounds }
// ColorModel 返回二值图像使用的灰度颜色模型
// 返回: color.Model 灰度模型
func (s *tiffFaxImage) ColorModel() color.Model { return color.GrayModel }
// At 读取所属条带或瓦片中的像素,区域外返回黑色
// 入参: x 横向像素坐标, y 纵向像素坐标
// 返回: color.Color 像素颜色
func (s *tiffFaxImage) At(x, y int) color.Color {
if !image.Pt(x, y).In(s.bounds) {
return color.Gray{}
}
block := s.blocks[(y/s.height)*s.across+x/s.width]
return block.At(x%s.width, y%s.height)
}
+85
View File
@@ -0,0 +1,85 @@
// 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 image
import (
"context"
"image"
"image/color"
)
// maskedTIFFImage 将独立透明蒙版叠加到主图,复用两个目录的只读像素
type maskedTIFFImage struct {
source, mask image.Image
bounds image.Rectangle
}
// DecodeTIFF 解码主图及透明蒙版,保留颜色精度与高分辨率蒙版
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 主图布局
// 返回: image.Image 图像, error 解码或取消错误
func DecodeTIFF(ctx context.Context, data []byte, layout TIFFLayout) (image.Image, error) {
source, err := decodeTIFFPixels(ctx, data, layout)
if err != nil || layout.Transparency == nil {
return source, err
}
mask, err := decodeTIFFPixels(ctx, data, *layout.Transparency)
if err != nil {
return nil, err
}
return &maskedTIFFImage{source: source, mask: mask, bounds: image.Rectangle{Max: layout.DisplaySize()}}, nil
}
// Bounds 返回主图和蒙版共同覆盖的像素区域
// 返回: image.Rectangle 显示区域
func (s *maskedTIFFImage) Bounds() image.Rectangle { return s.bounds }
// ColorModel 返回保留原图采样精度的非预乘颜色模型
// 返回: color.Model 颜色模型
func (s *maskedTIFFImage) ColorModel() color.Model { return color.NRGBA64Model }
// NRGBA64At 按像素中心读取主图及蒙版,零位隐藏原图并保留隐藏颜色
// 入参: x 横坐标, y 纵坐标
// 返回: color.NRGBA64 非预乘十六位颜色
func (s *maskedTIFFImage) NRGBA64At(x, y int) color.NRGBA64 {
if !image.Pt(x, y).In(s.bounds) {
return color.NRGBA64{}
}
size := s.bounds.Size()
x, y = x-s.bounds.Min.X, y-s.bounds.Min.Y
b := s.source.Bounds()
sx := b.Min.X + int((uint64(x)*uint64(b.Dx())+uint64(b.Dx()/2))/uint64(size.X))
sy := b.Min.Y + int((uint64(y)*uint64(b.Dy())+uint64(b.Dy()/2))/uint64(size.Y))
pixel := NRGBA64At(s.source, sx, sy)
b = s.mask.Bounds()
mx := b.Min.X + int((uint64(x)*uint64(b.Dx())+uint64(b.Dx()/2))/uint64(size.X))
my := b.Min.Y + int((uint64(y)*uint64(b.Dy())+uint64(b.Dy()/2))/uint64(size.Y))
if NRGBA64At(s.mask, mx, my).R == 0 {
pixel.A = 0
}
return pixel
}
// At 返回非预乘颜色,区域外透明
// 入参: x 横坐标, y 纵坐标
// 返回: color.Color 像素颜色
func (s *maskedTIFFImage) At(x, y int) color.Color { return s.NRGBA64At(x, y) }
// RGBA64At 返回预乘十六位颜色,供采样器直接读取
// 入参: x 横坐标, y 纵坐标
// 返回: color.RGBA64 预乘颜色
func (s *maskedTIFFImage) RGBA64At(x, y int) color.RGBA64 {
r, g, b, a := s.NRGBA64At(x, y).RGBA()
return color.RGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: uint16(a)}
}
+797
View File
@@ -0,0 +1,797 @@
// 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 image
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"image"
"image/color"
"io"
"math"
)
// tiffPlaneInput 将单分量目录附加到只读视图,像素仍从原始编码读取
type tiffPlaneInput struct {
source, directory []byte
position int64
order binary.ByteOrder
ctx context.Context
}
// planarTIFFImage 组合颜色与Alpha分量,保留交错步长并按需映射调色板
type planarTIFFImage struct {
planes []image.Image
bounds image.Rectangle
depth uint32
packed [5]uint32
offsets [5]int
alpha uint32
gray bool
cmyk bool
dotRange [4][2]uint16
palette []color.NRGBA64
stride int
invert bool
predictorWidth int
}
// needsSampleView 判断是否需通过分量视图处理存储布局,供尺寸读取和像素解码共用
// 返回: bool 是否需要分量视图
func (layout TIFFLayout) needsSampleView() bool {
return layout.mask || layout.auxiliary || layout.photometric == 5 || layout.Planar && layout.Samples > 1 || layout.Samples == 2 || layout.depth < 8 && (layout.Samples > 1 || layout.depth > 1 || layout.predictor == 2)
}
// DecodedPixelBytes 估算每像素解码缓冲,低位深分量按逐位展开后的字节数计量
// 返回: uint64 每像素预算字节数
func (layout TIFFLayout) DecodedPixelBytes() uint64 {
samples := uint64(layout.Samples)
if layout.Planar {
if layout.photometric == 5 && samples > 4 {
return 20
}
return 16
}
if !layout.needsSampleView() {
return 8
}
bytes := uint64(1)
if layout.depth == 2 || layout.depth == 4 {
bytes = uint64(layout.depth)
} else if layout.depth > 8 {
bytes = 2
}
return max(uint64(8), samples*bytes)
}
// decodeTIFFPixels 按存储方式解码指定目录并统一颜色和显示方向
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 目录布局
// 返回: image.Image 图像, error 解码或取消错误
func decodeTIFFPixels(ctx context.Context, data []byte, layout TIFFLayout) (image.Image, error) {
profile, err := readTIFFColorProfile(ctx, data, layout)
if err != nil {
return nil, err
}
var ranges [][2]int64
if layout.compression < 2 || layout.compression > 4 {
ranges, err = tiffBitOrderRanges(ctx, data, layout)
if err != nil {
return nil, err
}
}
var pixels image.Image
if layout.compression >= 2 && layout.compression <= 4 {
pixels, err = decodeTIFFFax(ctx, data, layout)
if err != nil {
return nil, err
}
} else if layout.needsSampleView() {
result, directories, err := tiffPlaneDirectories(ctx, data, layout)
if err != nil {
return nil, err
}
for _, directory := range directories {
input := &tiffPlaneInput{source: data, directory: directory, order: binary.LittleEndian, ctx: ctx}
if data[0] == 'M' {
input.order = binary.BigEndian
}
plane, err := decodeTIFFBitOrder(input, ranges)
if err != nil {
return nil, err
}
if result.predictorWidth != 0 && (result.stride > 1 || result.packed[len(result.planes)] != 0) {
if err := restoreTIFFPrediction(ctx, plane, max(result.stride, 1), result.predictorWidth, result.packed[len(result.planes)]); err != nil {
return nil, err
}
}
result.planes = append(result.planes, plane)
}
pixels = result
} else {
input := newTIFFDirectoryInput(data, layout.Offset)
input.ctx = ctx
var err error
pixels, err = decodeTIFFBitOrder(input, ranges)
if err != nil {
return nil, err
}
}
if profile != nil {
pixels, err = applyTIFFColorProfile(ctx, pixels, profile)
if err != nil {
return nil, err
}
}
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, err
}
}
if layout.Orientation != 1 {
pixels = &orientedTIFFImage{source: pixels, TIFFLayout: layout}
}
return pixels, nil
}
// tiffPlaneDirectories 为颜色、Alpha或调色板索引构建单通道目录,跳过无显示用途的独立平面
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 已校验的主图布局
// 返回: *planarTIFFImage 像素布局, [][]byte 各分量目录, error 字段或布局错误
func tiffPlaneDirectories(ctx context.Context, data []byte, layout TIFFLayout) (*planarTIFFImage, [][]byte, error) {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
}
if uint64(len(data))+65536*12+8 > math.MaxUint32 {
return nil, nil, fmt.Errorf("TIFF plane directory offset exceeds format limit")
}
var order binary.ByteOrder = binary.LittleEndian
if data[0] == 'M' {
order = binary.BigEndian
}
offset := int(layout.Offset)
count := int(order.Uint16(data[offset:]))
entries := make(map[uint16][]byte, count)
for i := range count {
entry := data[offset+2+i*12 : offset+14+i*12]
entries[order.Uint16(entry)] = entry
}
value := func(tag uint16, fallback uint32) (uint32, error) {
entry := entries[tag]
if entry == nil {
return fallback, nil
}
if order.Uint32(entry[4:]) != 1 {
return 0, fmt.Errorf("invalid TIFF scalar field %d", tag)
}
return tiffPlaneValue(data, order, entry, 0)
}
samples, err := value(277, 1)
if err != nil || samples < 1 || samples > math.MaxUint16 {
return nil, nil, fmt.Errorf("invalid TIFF plane count")
}
photo, err := value(262, math.MaxUint32)
if err != nil || photo > 5 {
return nil, nil, fmt.Errorf("unsupported TIFF planar color model")
}
compression, err := value(259, 1)
if err != nil {
return nil, nil, err
}
fill, err := value(266, 1)
if err != nil || fill != 1 && (fill != 2 || compression != 1 && (compression < 2 || compression > 4)) {
return nil, nil, fmt.Errorf("unsupported TIFF plane fill order")
}
predictor, err := value(317, 1)
if err != nil || predictor != 1 && predictor != 2 {
return nil, nil, fmt.Errorf("unsupported TIFF plane predictor")
}
result := &planarTIFFImage{bounds: image.Rect(0, 0, layout.Width, layout.Height), gray: photo < 2 || photo == 4, invert: photo == 0, cmyk: photo == 5}
if result.cmyk {
for _, tag := range []uint16{332, 334} {
want := uint32(1)
if tag == 334 {
want = 4
}
got, err := value(tag, want)
if err != nil || got != want || entries[tag] != nil && order.Uint16(entries[tag][2:]) != 3 {
return nil, nil, fmt.Errorf("unsupported TIFF ink configuration")
}
}
}
planes := samples
if !layout.Planar && samples > 1 {
result.stride = int(samples)
planes = 1
}
colors := uint32(3)
if result.cmyk {
colors = 4
} else if photo != 2 {
colors = 1
}
if samples != colors {
entry := entries[338]
if samples < colors || entry == nil || order.Uint16(entry[2:]) != 3 || order.Uint32(entry[4:]) != samples-colors {
return nil, nil, fmt.Errorf("invalid TIFF extra samples")
}
for i := uint32(0); i < samples-colors; i++ {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
}
alpha, err := tiffPlaneValue(data, order, entry, i)
if err != nil || alpha > 2 || alpha != 0 && result.alpha != 0 {
return nil, nil, fmt.Errorf("unsupported TIFF extra samples")
}
if alpha != 0 {
result.alpha, result.offsets[colors] = alpha, int(i)
}
}
} else if entries[338] != nil {
return nil, nil, fmt.Errorf("unexpected TIFF extra samples")
}
width, err := value(322, 0)
if err != nil {
return nil, nil, err
}
blocks := uint64(0)
locations, lengths := uint16(273), uint16(279)
if width != 0 {
height, err := value(323, 0)
if err != nil || height == 0 || width%16 != 0 || height%16 != 0 {
return nil, nil, fmt.Errorf("invalid TIFF tile dimensions")
}
blocks = ((uint64(layout.Width)-1)/uint64(width) + 1) * ((uint64(layout.Height)-1)/uint64(height) + 1)
locations, lengths = 324, 325
} else {
rows, err := value(278, math.MaxUint32)
if err != nil || rows == 0 {
return nil, nil, fmt.Errorf("invalid TIFF rows per strip")
}
blocks = (uint64(layout.Height)-1)/uint64(rows) + 1
}
if blocks > math.MaxUint32/uint64(planes) {
return nil, nil, fmt.Errorf("too many TIFF plane blocks")
}
for _, tag := range []uint16{locations, lengths} {
entry := entries[tag]
if entry == nil || uint64(order.Uint32(entry[4:])) != blocks*uint64(planes) || tag == 324 && order.Uint16(entry[2:]) != 4 {
return nil, nil, fmt.Errorf("invalid TIFF plane block count")
}
}
for i := uint32(0); i < uint32(blocks)*planes; i++ {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
}
at, err := tiffPlaneValue(data, order, entries[locations], i)
if err != nil {
return nil, nil, err
}
size, err := tiffPlaneValue(data, order, entries[lengths], i)
if err != nil || size == 0 || uint64(at)+uint64(size) > uint64(len(data)) {
return nil, nil, fmt.Errorf("truncated TIFF plane block")
}
}
for _, tag := range []uint16{258, 339} {
if entry := entries[tag]; entry != nil && (order.Uint16(entry[2:]) != 3 || order.Uint32(entry[4:]) != samples) {
return nil, nil, fmt.Errorf("invalid TIFF sample array")
}
}
var directories [][]byte
var firstDepth uint32
for plane := uint32(0); plane < samples; plane++ {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
}
slot := plane
visible := plane < colors || result.alpha != 0 && plane == colors+uint32(result.offsets[colors])
if !visible && result.stride == 0 {
continue
}
if plane >= colors {
slot = colors
}
depth := uint32(1)
if entry := entries[258]; entry != nil {
depth, err = tiffPlaneValue(data, order, entry, plane)
}
if err != nil || depth != 1 && depth != 2 && depth != 4 && depth != 8 && depth != 16 {
return nil, nil, fmt.Errorf("unsupported TIFF plane depth")
}
if plane == 0 {
firstDepth = depth
} else if result.stride > 1 && depth != firstDepth {
return nil, nil, fmt.Errorf("unsupported TIFF mixed interleaved sample depths")
}
if !visible {
continue
}
result.depth = max(result.depth, depth)
if result.cmyk && plane < 4 {
low, high := uint32(0), uint32(1<<depth-1)
if entry := entries[336]; entry != nil {
n := order.Uint32(entry[4:])
if kind := order.Uint16(entry[2:]); kind != 1 && kind != 3 || n != 2 && n != 2*samples {
return nil, nil, fmt.Errorf("invalid TIFF dot range")
}
i := plane * 2
if n == 2 {
i = 0
}
if order.Uint16(entry[2:]) == 1 {
values := entry[8:]
if n > 4 {
at := uint64(order.Uint32(values))
if at+uint64(n) > uint64(len(data)) {
return nil, nil, fmt.Errorf("truncated TIFF dot range")
}
values = data[at : at+uint64(n)]
}
low, high = uint32(values[i]), uint32(values[i+1])
} else {
low, err = tiffPlaneValue(data, order, entry, i)
if err != nil {
return nil, nil, err
}
high, err = tiffPlaneValue(data, order, entry, i+1)
}
if err != nil || low == high || low > 1<<depth-1 || high > 1<<depth-1 {
return nil, nil, fmt.Errorf("invalid TIFF dot range")
}
}
result.dotRange[plane] = [2]uint16{uint16(low * 65535 / (1<<depth - 1)), uint16(high * 65535 / (1<<depth - 1))}
}
factor := uint64(max(result.stride, 1))
if depth == 1 && predictor == 2 {
result.packed[slot] = 1
}
if depth == 2 || depth == 4 {
result.packed[slot] = depth
factor *= uint64(depth)
if compression == 2 || compression == 3 || compression == 4 {
return nil, nil, fmt.Errorf("unsupported TIFF packed plane compression")
}
}
pixelBytes := factor
if depth > 8 {
pixelBytes *= 2
}
if uint64(layout.Width)*pixelBytes > uint64(math.MaxInt)/uint64(layout.Height) || uint64(layout.Width)*factor > math.MaxUint32 || uint64(width)*factor > math.MaxUint32 {
return nil, nil, fmt.Errorf("TIFF plane dimensions exceed format limit")
}
if predictor == 2 && (result.stride > 1 || depth < 8) {
result.predictorWidth = layout.Width
if width != 0 {
result.predictorWidth = int(width)
}
}
if entry := entries[339]; entry != nil {
format, err := tiffPlaneValue(data, order, entry, plane)
if err != nil || format != 1 {
return nil, nil, fmt.Errorf("unsupported TIFF sample format")
}
}
if photo == 3 && plane == 0 {
entry := entries[320]
if depth > 8 || entry == nil || order.Uint16(entry[2:]) != 3 || order.Uint32(entry[4:]) != 3<<depth {
return nil, nil, fmt.Errorf("invalid TIFF color map")
}
n := uint32(1) << depth
result.palette = make([]color.NRGBA64, n)
for i := range n {
var components [3]uint16
for c := range components {
v, err := tiffPlaneValue(data, order, entry, uint32(c)*n+i)
if err != nil {
return nil, nil, err
}
components[c] = uint16(v)
if v%257 != 0 {
result.depth = 16
}
}
result.palette[i] = color.NRGBA64{R: components[0], G: components[1], B: components[2], A: 65535}
}
}
if result.stride > 1 && plane != 0 {
continue
}
directory := make([]byte, 2, 6+count*12)
for i := range count {
if ctx != nil {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
}
entry := bytes.Clone(data[offset+2+i*12 : offset+14+i*12])
tag := order.Uint16(entry)
if tag == 338 || tag == 320 || tag >= 332 && tag <= 337 || tag == 34675 {
continue
}
scalar := uint32(0)
switch tag {
case 258:
scalar = depth
if result.packed[slot] != 0 {
scalar = 1
}
case 256, 322:
if factor != 1 {
original, err := tiffPlaneValue(data, order, entry, 0)
if err != nil {
return nil, nil, err
}
order.PutUint16(entry[2:], 4)
order.PutUint32(entry[4:], 1)
order.PutUint32(entry[8:], original*uint32(factor))
}
case 262:
scalar = 1
case 277, 284, 339:
scalar = 1
case 317:
if predictor == 2 && (result.stride > 1 || depth < 8) {
order.PutUint16(entry[8:], 1)
}
case locations, lengths:
entry, err = tiffPlaneArray(data, order, entry, uint32(blocks)*plane, uint32(blocks))
if err != nil {
return nil, nil, err
}
}
if tag == 258 || tag == 262 || tag == 277 || tag == 284 || tag == 339 {
order.PutUint16(entry[2:], 3)
order.PutUint32(entry[4:], 1)
clear(entry[8:])
order.PutUint16(entry[8:], uint16(scalar))
}
directory = append(directory, entry...)
}
order.PutUint16(directory, uint16((len(directory)-2)/12))
directories = append(directories, append(directory, 0, 0, 0, 0))
}
return result, directories, nil
}
// restoreTIFFPrediction 在解码缓冲内恢复水平差分,各行及瓦片独立计算
// 入参: ctx 取消上下文,可为nil, pixels 单通道解码视图, stride 每像素分量数, width 原始条带或瓦片宽度, packed 展开为一位采样的原位深,零表示未展开
// 返回: error 图像类型或取消错误
func restoreTIFFPrediction(ctx context.Context, pixels image.Image, stride, width int, packed uint32) error {
if ctx == nil {
ctx = context.Background()
}
var data []byte
var pitch, step int
switch pixels := pixels.(type) {
case *image.Gray:
data, pitch, step = pixels.Pix, pixels.Stride, max(int(packed), 1)
case *image.Gray16:
data, pitch, step = pixels.Pix, pixels.Stride, 2
default:
return fmt.Errorf("unsupported TIFF predictor image")
}
bounds := pixels.Bounds()
columns := bounds.Dx() / max(int(packed), 1)
block := width * stride
for y := range bounds.Dy() {
if err := ctx.Err(); err != nil {
return err
}
row := data[y*pitch : y*pitch+columns*step]
for start := 0; start < columns; {
end := start + min(block, columns-start)
for x := start + stride; x < end; {
if err := ctx.Err(); err != nil {
return err
}
limit := x + min(4096, end-x)
for ; x < limit; x++ {
at, previous := x*step, (x-stride)*step
if packed != 0 {
var current, prior uint16
for bit := range step {
current = current<<1 | uint16(row[at+bit]>>7)
prior = prior<<1 | uint16(row[previous+bit]>>7)
}
value := current + prior
for bit := range step {
row[at+bit] = byte(value>>uint(step-1-bit)&1) * 255
}
} else if step == 2 {
value := binary.BigEndian.Uint16(row[at:]) + binary.BigEndian.Uint16(row[previous:])
binary.BigEndian.PutUint16(row[at:], value)
} else {
row[at] += row[previous]
}
}
}
start = end
}
}
return ctx.Err()
}
// tiffPlaneValue 读取SHORT或LONG数组中的一项,校验完整数组范围
// 入参: data 原编码, order 字节序, entry 目录项, index 元素序号
// 返回: uint32 元素值, error 类型或越界错误
func tiffPlaneValue(data []byte, order binary.ByteOrder, entry []byte, index uint32) (uint32, error) {
kind, count := order.Uint16(entry[2:]), order.Uint32(entry[4:])
if kind != 3 && kind != 4 || index >= count {
return 0, fmt.Errorf("invalid TIFF plane array")
}
size := uint64(2)
if kind == 4 {
size = 4
}
values := entry[8:]
if size*uint64(count) > 4 {
at := uint64(order.Uint32(values))
if at+size*uint64(count) > uint64(len(data)) {
return 0, fmt.Errorf("truncated TIFF plane array")
}
values = data[at:]
}
if size == 2 {
return uint32(order.Uint16(values[uint64(index)*size:])), nil
}
return order.Uint32(values[uint64(index)*size:]), nil
}
// tiffPlaneArray 将块索引缩减为指定分量的范围,较长数组继续借用原编码
// 入参: data 原编码, order 字节序, entry 目录项副本, first 起始序号, count 元素数
// 返回: []byte 修改后的目录项, error 类型或越界错误
func tiffPlaneArray(data []byte, order binary.ByteOrder, entry []byte, first, count uint32) ([]byte, error) {
if _, err := tiffPlaneValue(data, order, entry, first+count-1); err != nil {
return nil, err
}
size := uint32(2)
if order.Uint16(entry[2:]) == 4 {
size = 4
}
if uint64(count)*uint64(size) <= 4 {
var values [4]byte
for i := uint32(0); i < count; i++ {
value, _ := tiffPlaneValue(data, order, entry, first+i)
if size == 2 {
order.PutUint16(values[i*size:], uint16(value))
} else {
order.PutUint32(values[i*size:], value)
}
}
copy(entry[8:], values[:])
} else {
order.PutUint32(entry[8:], order.Uint32(entry[8:])+first*size)
}
order.PutUint32(entry[4:], count)
return entry, nil
}
// Read 从组合视图顺序读取
// 入参: data 接收缓冲
// 返回: int 字节数, error 读取或取消错误
func (r *tiffPlaneInput) Read(data []byte) (int, error) {
n, err := r.ReadAt(data, r.position)
r.position += int64(n)
return n, err
}
// ReadAt 从原始编码或附加目录读取并替换首目录指针
// 入参: data 接收缓冲, offset 文件偏移
// 返回: int 字节数, error 读取或取消错误
func (r *tiffPlaneInput) ReadAt(data []byte, offset int64) (int, error) {
if r.ctx != nil {
if err := r.ctx.Err(); err != nil {
return 0, err
}
}
if offset < 0 {
return 0, fmt.Errorf("negative TIFF read offset")
}
start := int64(len(r.source))
directory := start + start%2
written := 0
if offset < start {
written = copy(data, r.source[offset:])
}
if offset+int64(written) == start && start != directory && written < len(data) {
data[written] = 0
written++
}
if at := offset + int64(written) - directory; at >= 0 && at < int64(len(r.directory)) {
written += copy(data[written:], r.directory[at:])
}
var pointer [4]byte
r.order.PutUint32(pointer[:], uint32(directory))
for i, value := range pointer {
if at := int64(4+i) - offset; at >= 0 && at < int64(written) {
data[at] = value
}
}
if written != len(data) {
return written, io.EOF
}
return written, nil
}
// Bounds 返回存储坐标范围
// 返回: image.Rectangle 图像范围
func (p *planarTIFFImage) Bounds() image.Rectangle { return p.bounds }
// ColorModel 按透明度类型和最高分量精度返回颜色模型
// 返回: color.Model 颜色模型
func (p *planarTIFFImage) ColorModel() color.Model {
if p.gray && p.alpha == 0 {
if p.depth > 8 {
return color.Gray16Model
}
return color.GrayModel
}
if p.alpha == 1 {
if p.depth > 8 {
return color.RGBA64Model
}
return color.RGBAModel
}
if p.depth > 8 {
return color.NRGBA64Model
}
return color.NRGBAModel
}
// components 直接读取灰度分量,保留原始透明度语义
// 入参: x 横坐标, y 纵坐标
// 返回: [4]uint16 红、绿、蓝和透明度分量
func (p *planarTIFFImage) components(x, y int) [4]uint16 {
if !image.Pt(x, y).In(p.bounds) {
return [4]uint16{}
}
if p.cmyk {
ink := p.inksAt(x, y)
maximum := uint32(65535)
if p.alpha == 1 {
maximum = uint32(ink[4])
if maximum == 0 {
return [4]uint16{}
}
}
black := maximum - min(uint32(ink[3]), maximum)
var values [4]uint16
for i := range 3 {
values[i] = uint16((maximum - min(uint32(ink[i]), maximum)) * black / maximum)
}
values[3] = ink[4]
return values
}
values := [4]uint16{0, 0, 0, 65535}
count := 3
if p.gray || len(p.palette) != 0 {
count = 1
}
if p.alpha != 0 {
count++
}
for i := range count {
value := p.sampleAt(i, x, y)
if i == 0 && p.invert {
value = 65535 - value
}
if len(p.palette) != 0 && i == 0 {
c := p.palette[uint32(value)*uint32(len(p.palette)-1)/65535]
values = [4]uint16{c.R, c.G, c.B, c.A}
} else if p.gray && i == 0 {
values[0], values[1], values[2] = value, value, value
} else if p.gray || len(p.palette) != 0 {
values[3] = value
} else {
values[i] = value
}
}
if p.alpha == 1 && values[3] == 0 {
return [4]uint16{}
}
return values
}
// sampleAt 读取一个分量并扩展为十六位,交错布局复用同一像素缓冲
// 入参: i 分量序号, x 横坐标, y 纵坐标
// 返回: uint16 归一化采样值
func (p *planarTIFFImage) sampleAt(i, x, y int) uint16 {
plane, sx := p.planes[0], x
if p.stride > 1 {
sx = x*p.stride + i + p.offsets[i]
} else {
plane = p.planes[i]
}
switch plane := plane.(type) {
case *image.Gray:
if bits := p.packed[i]; bits != 0 {
var value uint16
for bit := range int(bits) {
value = value<<1 | uint16(plane.GrayAt(sx*int(bits)+bit, y).Y>>7)
}
return value * (65535 / uint16(1<<bits-1))
}
return uint16(plane.GrayAt(sx, y).Y) * 257
case *image.Gray16:
return plane.Gray16At(sx, y).Y
default:
v, _, _, _ := plane.At(sx, y).RGBA()
return uint16(v)
}
}
// inksAt 按网点范围读取CMYK覆盖量,范围外采样按端点截断,透明度独立归一化
// 入参: x 横坐标, y 纵坐标
// 返回: [5]uint16 青、品红、黄、黑及透明度
func (p *planarTIFFImage) inksAt(x, y int) [5]uint16 {
values := [5]uint16{0, 0, 0, 0, 65535}
for i := range 4 {
value := int64(p.sampleAt(i, x, y))
low, high := int64(p.dotRange[i][0]), int64(p.dotRange[i][1])
values[i] = uint16(max(0, min(65535, (value-low)*65535/(high-low))))
}
if p.alpha != 0 {
values[4] = p.sampleAt(4, x, y)
}
return values
}
// At 返回组合后的颜色,关联透明度保留预乘语义
// 入参: x 横坐标, y 纵坐标
// 返回: color.Color 像素颜色
func (p *planarTIFFImage) At(x, y int) color.Color {
values := p.components(x, y)
if p.alpha == 1 {
return color.RGBA64{R: values[0], G: values[1], B: values[2], A: values[3]}
}
return color.NRGBA64{R: values[0], G: values[1], B: values[2], A: values[3]}
}
// NRGBA64At 读取独立透明度颜色,保留非预乘分量中的隐藏颜色
// 入参: x 横坐标, y 纵坐标
// 返回: color.NRGBA64 像素颜色
func (p *planarTIFFImage) NRGBA64At(x, y int) color.NRGBA64 {
values := p.components(x, y)
if p.alpha == 1 && values[3] != 65535 {
if values[3] == 0 {
return color.NRGBA64{}
}
for i := range 3 {
values[i] = uint16(uint32(values[i]) * 65535 / uint32(values[3]))
}
}
return color.NRGBA64{R: values[0], G: values[1], B: values[2], A: values[3]}
}
// RGBA64At 返回预乘十六位颜色,不分配临时颜色对象
// 入参: x 横坐标, y 纵坐标
// 返回: color.RGBA64 像素颜色
func (p *planarTIFFImage) RGBA64At(x, y int) color.RGBA64 {
values := p.components(x, y)
if p.alpha != 1 && values[3] != 65535 {
for i := range 3 {
values[i] = uint16(uint32(values[i]) * uint32(values[3]) / 65535)
}
}
return color.RGBA64{R: values[0], G: values[1], B: values[2], A: values[3]}
}
+221
View File
@@ -0,0 +1,221 @@
// 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 image
import (
"context"
"fmt"
"image"
"image/color"
"math"
"strings"
"github.com/xiaoqidun/pdfgo"
)
// parseTIFFColorProfile 校验ICC源模型,保留原有模型错误并复用浮点颜色变换
// 入参: kind 源颜色模型, data ICC编码
// 返回: *pdfgo.ColorSpace 颜色变换, error 模型或配置错误
func parseTIFFColorProfile(kind string, data []byte) (*pdfgo.ColorSpace, error) {
model := map[string]string{"GRAY": "GRAY", "RGB": "RGB ", "CMYK": "CMYK"}[strings.ToUpper(kind)]
if len(data) < 132 || model == "" || string(data[16:20]) != model {
return nil, fmt.Errorf("ICC source model does not match OFD color space %q", kind)
}
return pdfgo.NewICCColorSpace(data)
}
// readTIFFColorProfile 读取主图的ICC配置,校验颜色模型和数据范围
// 入参: ctx 取消上下文,可为nil, data TIFF编码, layout 主图布局
// 返回: *pdfgo.ColorSpace 颜色配置,未声明时为nil, error 配置或取消错误
func readTIFFColorProfile(ctx context.Context, data []byte, layout TIFFLayout) (*pdfgo.ColorSpace, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, err
}
if layout.profileSize == 0 {
return nil, nil
}
at, size := uint64(layout.profileOffset), uint64(layout.profileSize)
if at+size > uint64(len(data)) {
return nil, fmt.Errorf("truncated TIFF color profile")
}
kind := ""
switch layout.photometric {
case 0, 1:
kind = "GRAY"
case 2, 3:
kind = "RGB"
case 5:
kind = "CMYK"
default:
return nil, fmt.Errorf("unsupported TIFF profile color model")
}
profile, err := parseTIFFColorProfile(kind, data[at:at+size])
if cancelErr := ctx.Err(); cancelErr != nil {
return nil, cancelErr
}
return profile, err
}
// applyTIFFColorProfile 将源颜色转换为十六位sRGB,保留独立透明度和隐藏颜色
// 入参: ctx 取消上下文,可为nil, source 解码图像, profile 已校验的颜色配置
// 返回: image.Image 独立透明度图像, error 颜色变换或取消错误
func applyTIFFColorProfile(ctx context.Context, source image.Image, profile *pdfgo.ColorSpace) (image.Image, error) {
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, err
}
bounds := source.Bounds()
if bounds.Dx() <= 0 || bounds.Dy() <= 0 || bounds.Dx() > math.MaxInt/8/bounds.Dy() {
return nil, fmt.Errorf("TIFF profile image dimensions exceed capacity")
}
cmyk, _ := source.(*planarTIFFImage)
if cmyk != nil && !cmyk.cmyk {
cmyk = nil
}
model := source.ColorModel()
premultiplied := model == color.RGBAModel || model == color.RGBA64Model
table := newTIFFProfileTable(source)
result := image.NewNRGBA64(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
if (x-bounds.Min.X)&255 == 0 {
if err := ctx.Err(); err != nil {
return nil, err
}
}
slot := -1
if len(table) != 0 {
index, alpha := tiffProfileSample(source, x, y, len(table))
slot = index
if pixel := table[index]; pixel.A != 0 {
pixel.A = alpha
result.SetNRGBA64(x, y, pixel)
continue
}
}
var values [4]float64
var alpha uint16
if cmyk != nil {
ink := cmyk.inksAt(x, y)
alpha = ink[4]
if cmyk.alpha == 1 && alpha == 0 {
continue
}
maximum := float64(65535)
if cmyk.alpha == 1 {
maximum = float64(alpha)
}
for i := range values {
values[i] = min(1, float64(ink[i])/maximum)
}
} else if premultiplied {
var pixel color.RGBA64
if native, ok := source.(image.RGBA64Image); ok {
pixel = native.RGBA64At(x, y)
} else {
pixel = color.RGBA64Model.Convert(source.At(x, y)).(color.RGBA64)
}
alpha = pixel.A
if alpha == 0 {
continue
}
for i, value := range [3]uint16{pixel.R, pixel.G, pixel.B} {
values[i] = min(1, float64(value)/float64(alpha))
}
} else {
pixel := NRGBA64At(source, x, y)
alpha = pixel.A
values = [4]float64{float64(pixel.R) / 65535, float64(pixel.G) / 65535, float64(pixel.B) / 65535}
}
rgb, err := profile.RGB(values[:profile.Components()], profile.DefaultIntent())
if err != nil {
return nil, err
}
var channels [3]uint16
for i, value := range rgb {
channels[i] = uint16(math.Round(max(0, min(1, value)) * 65535))
}
if slot >= 0 {
table[slot] = color.NRGBA64{R: channels[0], G: channels[1], B: channels[2], A: 65535}
}
result.SetNRGBA64(x, y, color.NRGBA64{R: channels[0], G: channels[1], B: channels[2], A: alpha})
}
}
if err := ctx.Err(); err != nil {
return nil, err
}
return result, nil
}
// newTIFFProfileTable 为离散颜色分配查找表,预乘颜色及小图保持逐像素变换
// 入参: source 已解码且尺寸有效的图像
// 返回: []color.NRGBA64 颜色表,Alpha非零表示已有结果,不适用时为nil
func newTIFFProfileTable(source image.Image) []color.NRGBA64 {
count := 0
switch source := source.(type) {
case *image.Gray:
count = 256
case *image.Gray16:
count = 65536
case *image.Paletted:
count = len(source.Palette)
case *planarTIFFImage:
if source.alpha != 1 {
if len(source.palette) != 0 {
count = len(source.palette)
} else if source.gray {
count = 256
if source.packed[0] != 0 {
count = 1 << source.packed[0]
} else if _, ok := source.planes[0].(*image.Gray16); ok {
count = 65536
}
}
}
}
if count == 0 || count >= source.Bounds().Dx()*source.Bounds().Dy() {
return nil
}
return make([]color.NRGBA64, count)
}
// tiffProfileSample 读取查找表索引与独立透明度,颜色键不包含透明度
// 入参: source 适用查找表的图像, x 横坐标, y 纵坐标, count 颜色表长度
// 返回: int 颜色索引, uint16 透明度
func tiffProfileSample(source image.Image, x, y, count int) (int, uint16) {
switch source := source.(type) {
case *image.Gray:
return int(source.GrayAt(x, y).Y), 65535
case *image.Gray16:
return int(source.Gray16At(x, y).Y), 65535
case *image.Paletted:
index := source.ColorIndexAt(x, y)
_, _, _, alpha := source.Palette[index].RGBA()
return int(index), uint16(alpha)
case *planarTIFFImage:
index := int(uint32(source.sampleAt(0, x, y)) * uint32(count-1) / 65535)
alpha := uint16(65535)
if source.alpha != 0 {
alpha = source.sampleAt(1, x, y)
}
return index, alpha
}
return 0, 0
}