mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:31:12 +08:00
355 lines
11 KiB
Go
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"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// errTIFFRandomAccess 表示目录链需要回读,仅流式输入需重新打开资源
|
|
var errTIFFRandomAccess = errors.New("TIFF directory requires random access")
|
|
|
|
// 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)}
|
|
}
|