mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 15:06:46 +08:00
1 parent
ee3fd02a0a
commit
64f9ec4241
87 files changed
+2693
-2289
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)}
|
||||
}
|
||||
@@ -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]}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user