Files
ofdgo/internal/base/image/tiff_fax.go
T
2026-10-14 00:12:00 +08:00

186 lines
6.4 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 (
"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)
}