Files
ofdgo/internal/base/image/tiff.go
T
2026-10-19 08:43:50 +08:00

434 lines
13 KiB
Go

// 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), ctx: 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)}
}