Files
ofdgo/ofdgo_image_tiff.go
T
2026-10-12 11:20:30 +08:00

355 lines
11 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 ofdgo
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"image"
"image/color"
"io"
)
// errTIFFRandomAccess 表示目录链需要回读,仅流式输入需重新打开资源
var errTIFFRandomAccess = errors.New("TIFF directory requires random access")
// tiffImageLayout 保存单页TIFF的尺寸、方向、采样及压缩参数
type tiffImageLayout struct {
width, height int
orientation int
offset, next uint32
reduced, mask bool
planar bool
samples uint32
depth uint32
fillOrder uint32
compression uint32
t4, t6 uint32
}
// tiffDirectoryInput 只替换解码视图中的首目录偏移,不复制或改写原始资源
type tiffDirectoryInput struct {
reader *bytes.Reader
directory [4]byte
ctx context.Context
}
// orientedTIFFImage 按方向映射像素坐标,复用原图数据与颜色精度
type orientedTIFFImage struct {
source image.Image
tiffImageLayout
}
// isTIFFData 识别标准TIFF字节序与版本标识
// 入参: data 文件头或完整编码
// 返回: bool 是否为TIFF 6.0编码
func isTIFFData(data []byte) bool {
return len(data) >= 4 && (string(data[:4]) == "II\x2a\x00" || string(data[:4]) == "MM\x00\x2a")
}
// readTIFFLayout 选择单页TIFF的主图,允许缩略图目录并拒绝循环和多个主图
// 入参: input 从文件头开始的输入流
// 返回: tiffImageLayout 存储尺寸与方向, error 文件结构或方向错误
func readTIFFLayout(input io.Reader) (tiffImageLayout, error) {
var result tiffImageLayout
var header [8]byte
if _, err := io.ReadFull(input, header[:]); err != nil {
return result, err
}
if !isTIFFData(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
for offset != 0 {
if seen != nil && seen[uint32(offset)] {
return result, fmt.Errorf("cyclic TIFF directory chain")
}
layout, end, err := readTIFFDirectory(input, order, offset, position)
if err != nil {
return result, err
}
if layout.mask {
return result, fmt.Errorf("TIFF transparency mask directories are not supported")
}
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 result.offset == 0 {
return result, fmt.Errorf("missing TIFF main image")
}
return result, nil
}
// readTIFFDirectory 读取目录中的图像布局和子文件类型,不读取像素
// 入参: input 输入流, order 字节序, offset 目录偏移, position 当前流位置
// 返回: tiffImageLayout 目录布局, int64 读取后的流位置, error 结构或读取错误
func readTIFFDirectory(input io.Reader, order binary.ByteOrder, offset, position int64) (tiffImageLayout, int64, error) {
var result tiffImageLayout
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
}
if _, err := io.CopyN(io.Discard, input, 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
var entry [12]byte
var previous uint16
var subfile, legacy uint32
var hasSubfile bool
for i := range count {
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 == 258 && kind == 3 && number == 1 {
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 {
continue
}
if number != 1 || kind != 3 && kind != 4 || (tag == 255 || tag == 259 || tag == 262 || tag == 266 || tag == 274 || tag == 277 || tag == 284) && 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) && (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.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)
}
}
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")
}
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 tiffImageLayout) size() image.Point {
if l.orientation >= 5 {
return image.Pt(l.height, l.width)
}
return image.Pt(l.width, l.height)
}
// 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 imageNRGBA64At(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)}
}