mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 17:27:08 +08:00
This commit is contained in:
1 parent
5e0bca66e3
commit
b5a8c9af9c
24 files changed
+935
-223
No files matched your search
@@ -0,0 +1,321 @@
|
||||
// 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"
|
||||
"compress/zlib"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
|
||||
// colorImage 保留源采样及独立的十六位显示像素
|
||||
type colorImage struct {
|
||||
*image.NRGBA64
|
||||
source image.Image
|
||||
}
|
||||
|
||||
// pngColor 保存PNG校准空间及缺省sRGB传递函数
|
||||
type pngColor struct {
|
||||
space *pdfgo.ColorSpace
|
||||
linearSRGB bool
|
||||
}
|
||||
|
||||
// pngColorProfile 读取PNG颜色元数据,ICC优先于sRGB及独立校准参数
|
||||
// 入参: ctx 取消上下文, data 原始编码
|
||||
// 返回: []byte ICC编码, *pngColor 校准参数, error 格式或配置错误
|
||||
func pngColorProfile(ctx context.Context, data []byte) ([]byte, *pngColor, error) {
|
||||
const profileLimit = 16 << 20
|
||||
chunks := make(map[string][]byte)
|
||||
for at := 8; at < len(data); {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(data)-at < 8 {
|
||||
return nil, nil, fmt.Errorf("truncated PNG chunk")
|
||||
}
|
||||
kind := string(data[at+4 : at+8])
|
||||
if kind == "IDAT" || kind == "PLTE" {
|
||||
break
|
||||
}
|
||||
if len(data)-at < 12 {
|
||||
return nil, nil, fmt.Errorf("truncated PNG chunk")
|
||||
}
|
||||
size := uint64(binary.BigEndian.Uint32(data[at:]))
|
||||
if size > uint64(len(data)-at-12) {
|
||||
return nil, nil, fmt.Errorf("invalid PNG chunk length")
|
||||
}
|
||||
end := at + 8 + int(size)
|
||||
switch kind {
|
||||
case "iCCP", "sRGB", "gAMA", "cHRM":
|
||||
if _, ok := chunks[kind]; ok {
|
||||
return nil, nil, fmt.Errorf("invalid PNG %s chunk order", kind)
|
||||
}
|
||||
if crc32.ChecksumIEEE(data[at+4:end]) != binary.BigEndian.Uint32(data[end:]) {
|
||||
return nil, nil, fmt.Errorf("invalid PNG %s checksum", kind)
|
||||
}
|
||||
chunks[kind] = data[at+8 : end]
|
||||
}
|
||||
at = end + 4
|
||||
if kind == "IEND" {
|
||||
break
|
||||
}
|
||||
}
|
||||
if chunk, ok := chunks["iCCP"]; ok {
|
||||
name := bytes.IndexByte(chunk, 0)
|
||||
if name < 1 || name > 79 || len(chunk) < name+3 || chunk[name+1] != 0 {
|
||||
return nil, nil, fmt.Errorf("invalid PNG ICC header")
|
||||
}
|
||||
for i, c := range chunk[:name] {
|
||||
if c < 32 || c > 126 && c < 161 || c == 32 && (i == 0 || i == name-1 || chunk[i-1] == 32) {
|
||||
return nil, nil, fmt.Errorf("invalid PNG ICC name")
|
||||
}
|
||||
}
|
||||
input := bytes.NewReader(chunk[name+2:])
|
||||
reader, err := zlib.NewReader(input)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
profile, err := io.ReadAll(io.LimitReader(contextInput{ReadCloser: reader, context: ctx}, profileLimit+1))
|
||||
closeErr := reader.Close()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if closeErr != nil {
|
||||
return nil, nil, closeErr
|
||||
}
|
||||
if len(profile) > profileLimit {
|
||||
return nil, nil, fmt.Errorf("PNG ICC profile exceeds size limit")
|
||||
}
|
||||
if input.Len() != 0 {
|
||||
return nil, nil, fmt.Errorf("trailing PNG ICC data")
|
||||
}
|
||||
kind := "RGB "
|
||||
if len(data) > 25 && (data[25] == 0 || data[25] == 4) {
|
||||
kind = "GRAY"
|
||||
}
|
||||
if len(profile) < 132 || string(profile[16:20]) != kind {
|
||||
return nil, nil, fmt.Errorf("ICC source model does not match PNG color space %q", kind)
|
||||
}
|
||||
return profile, nil, nil
|
||||
}
|
||||
if chunk, ok := chunks["sRGB"]; ok {
|
||||
if len(chunk) != 1 || chunk[0] > 3 {
|
||||
return nil, nil, fmt.Errorf("invalid PNG sRGB intent")
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
if len(chunks) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
coordinates := [8]float64{0.3127, 0.3290, 0.6400, 0.3300, 0.3000, 0.6000, 0.1500, 0.0600}
|
||||
if chunk, ok := chunks["cHRM"]; ok {
|
||||
if len(chunk) != 32 {
|
||||
return nil, nil, fmt.Errorf("invalid PNG chromaticities")
|
||||
}
|
||||
for i := range coordinates {
|
||||
coordinates[i] = float64(binary.BigEndian.Uint32(chunk[i*4:])) / 100000
|
||||
}
|
||||
}
|
||||
var xyz [4][3]float64
|
||||
for i := range xyz {
|
||||
x, y := coordinates[2*i], coordinates[2*i+1]
|
||||
if y <= 0 || x < 0 || x+y > 1 {
|
||||
return nil, nil, fmt.Errorf("invalid PNG chromaticities")
|
||||
}
|
||||
xyz[i] = [3]float64{x / y, 1, (1 - x - y) / y}
|
||||
}
|
||||
white := xyz[0]
|
||||
det := func(a, b, c [3]float64) float64 {
|
||||
return a[0]*(b[1]*c[2]-b[2]*c[1]) - b[0]*(a[1]*c[2]-a[2]*c[1]) + c[0]*(a[1]*b[2]-a[2]*b[1])
|
||||
}
|
||||
d := det(xyz[1], xyz[2], xyz[3])
|
||||
if math.Abs(d) < 1e-12 {
|
||||
return nil, nil, fmt.Errorf("degenerate PNG chromaticities")
|
||||
}
|
||||
scale := [3]float64{det(white, xyz[2], xyz[3]) / d, det(xyz[1], white, xyz[3]) / d, det(xyz[1], xyz[2], white) / d}
|
||||
var matrix [9]float64
|
||||
for i := range 3 {
|
||||
for j := range 3 {
|
||||
matrix[3*i+j] = xyz[i+1][j] * scale[i]
|
||||
}
|
||||
}
|
||||
gamma := [3]float64{1, 1, 1}
|
||||
linear := true
|
||||
if chunk, ok := chunks["gAMA"]; ok {
|
||||
if len(chunk) != 4 || binary.BigEndian.Uint32(chunk) == 0 {
|
||||
return nil, nil, fmt.Errorf("invalid PNG gamma")
|
||||
}
|
||||
g := 100000 / float64(binary.BigEndian.Uint32(chunk))
|
||||
gamma = [3]float64{g, g, g}
|
||||
linear = false
|
||||
}
|
||||
space, err := pdfgo.NewCalRGBColorSpace(white, [3]float64{}, gamma, matrix)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return nil, &pngColor{space: space, linearSRGB: linear}, nil
|
||||
}
|
||||
|
||||
// jpegColorProfile 读取JPEG分段ICC配置,拒绝缺段、重复及模型不匹配
|
||||
// 入参: ctx 取消上下文, data JPEG编码, model 源颜色模型
|
||||
// 返回: []byte 独立配置编码,未声明时为nil, error 配置或取消错误
|
||||
func jpegColorProfile(ctx context.Context, data []byte, model color.Model) ([]byte, error) {
|
||||
var parts [255][]byte
|
||||
count, found, total := 0, 0, 0
|
||||
for at := 2; at < len(data); {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data[at] != 0xff {
|
||||
return nil, fmt.Errorf("invalid JPEG marker")
|
||||
}
|
||||
for at < len(data) && data[at] == 0xff {
|
||||
at++
|
||||
}
|
||||
if at == len(data) {
|
||||
return nil, fmt.Errorf("truncated JPEG marker")
|
||||
}
|
||||
marker := data[at]
|
||||
at++
|
||||
if marker == 0xda || marker == 0xd9 {
|
||||
break
|
||||
}
|
||||
if marker == 0x01 || marker >= 0xd0 && marker <= 0xd7 {
|
||||
continue
|
||||
}
|
||||
if len(data)-at < 2 {
|
||||
return nil, fmt.Errorf("truncated JPEG segment")
|
||||
}
|
||||
size := int(binary.BigEndian.Uint16(data[at:]))
|
||||
if size < 2 || size > len(data)-at {
|
||||
return nil, fmt.Errorf("invalid JPEG segment length")
|
||||
}
|
||||
segment := data[at+2 : at+size]
|
||||
at += size
|
||||
if marker != 0xe2 || !bytes.HasPrefix(segment, []byte("ICC_PROFILE\x00")) {
|
||||
continue
|
||||
}
|
||||
if len(segment) <= 14 {
|
||||
return nil, fmt.Errorf("truncated JPEG color profile")
|
||||
}
|
||||
index, n := int(segment[12]), int(segment[13])
|
||||
if index == 0 || index > n || count != 0 && count != n || parts[index-1] != nil {
|
||||
return nil, fmt.Errorf("invalid JPEG color profile sequence")
|
||||
}
|
||||
count, found = n, found+1
|
||||
parts[index-1] = segment[14:]
|
||||
total += len(segment) - 14
|
||||
}
|
||||
if found == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if found != count {
|
||||
return nil, fmt.Errorf("incomplete JPEG color profile")
|
||||
}
|
||||
profile := make([]byte, 0, total)
|
||||
for _, part := range parts[:count] {
|
||||
profile = append(profile, part...)
|
||||
}
|
||||
kind := "RGB "
|
||||
if model == color.CMYKModel {
|
||||
kind = "CMYK"
|
||||
} else if model == color.GrayModel {
|
||||
kind = "GRAY"
|
||||
}
|
||||
if len(profile) < 132 || string(profile[16:20]) != kind {
|
||||
return nil, fmt.Errorf("ICC source model does not match JPEG color space %q", kind)
|
||||
}
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// jpegDisplayImage 统一JPEG显示变换,保留源分量并使用文档内嵌ICC
|
||||
// 入参: ctx 取消上下文, source 源像素, profile 颜色配置,可为nil
|
||||
// 返回: image.Image 显示像素, error 变换或取消错误
|
||||
func jpegDisplayImage(ctx context.Context, source image.Image, profile *pdfgo.ColorSpace) (image.Image, error) {
|
||||
cmyk, ok := source.(*image.CMYK)
|
||||
if !ok {
|
||||
if profile != nil {
|
||||
return applyTIFFColorProfile(ctx, source, profile)
|
||||
}
|
||||
return source, nil
|
||||
}
|
||||
if profile == nil {
|
||||
profile = &pdfgo.ColorSpace{Model: "DeviceCMYK"}
|
||||
}
|
||||
bounds := cmyk.Bounds()
|
||||
if bounds.Dx() <= 0 || bounds.Dy() <= 0 || bounds.Dx() > math.MaxInt/8/bounds.Dy() {
|
||||
return nil, fmt.Errorf("JPEG color image dimensions exceed capacity")
|
||||
}
|
||||
pixels := 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
|
||||
}
|
||||
}
|
||||
ink := cmyk.CMYKAt(x, y)
|
||||
values := [4]float64{float64(ink.C) / 255, float64(ink.M) / 255, float64(ink.Y) / 255, float64(ink.K) / 255}
|
||||
rgb, err := profile.SRGB(values[:], profile.DefaultIntent())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pixels.SetNRGBA64(x, y, color.NRGBA64{R: colorWord(rgb[0]), G: colorWord(rgb[1]), B: colorWord(rgb[2]), A: 65535})
|
||||
}
|
||||
}
|
||||
return &colorImage{NRGBA64: pixels, source: source}, nil
|
||||
}
|
||||
|
||||
// colorWord 将归一化颜色舍入为十六位分量
|
||||
// 入参: value 颜色分量
|
||||
// 返回: uint16 十六位分量
|
||||
func colorWord(value float64) uint16 {
|
||||
return uint16(math.Round(max(0, min(1, value)) * 65535))
|
||||
}
|
||||
|
||||
// cmykGrayAt 从原始四色采样计算蒙版灰度,不应用显示配置
|
||||
// 入参: x 横坐标, y 纵坐标
|
||||
// 返回: color.Gray 蒙版灰度
|
||||
func (p *planarTIFFImage) cmykGrayAt(x, y int) color.Gray {
|
||||
if !image.Pt(x, y).In(p.bounds) {
|
||||
return color.Gray{}
|
||||
}
|
||||
ink := p.inksAt(x, y)
|
||||
maximum := uint32(65535)
|
||||
if p.alpha == 1 {
|
||||
maximum = uint32(ink[4])
|
||||
if maximum == 0 {
|
||||
return color.Gray{}
|
||||
}
|
||||
}
|
||||
black := maximum - min(uint32(ink[3]), maximum)
|
||||
var rgb [3]uint32
|
||||
for i := range rgb {
|
||||
rgb[i] = (maximum - min(uint32(ink[i]), maximum)) * black / maximum
|
||||
if p.alpha != 1 {
|
||||
rgb[i] = rgb[i] * uint32(ink[4]) / 65535
|
||||
}
|
||||
}
|
||||
return color.Gray{Y: uint8((19595*rgb[0] + 38470*rgb[1] + 7471*rgb[2] + 1<<15) >> 24)}
|
||||
}
|
||||
@@ -26,17 +26,21 @@ import (
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
|
||||
// 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{}
|
||||
data []byte
|
||||
format string
|
||||
config image.Config
|
||||
profile []byte
|
||||
pngColor *pngColor
|
||||
result atomic.Pointer[encodedImageResult]
|
||||
mu sync.Mutex
|
||||
pending chan struct{}
|
||||
}
|
||||
|
||||
// encodedImageResult 保存已完成的像素解码结果,不保存取消或超时错误
|
||||
@@ -65,9 +69,14 @@ func (img *EncodedImage) Bytes() []byte { return img.data }
|
||||
// 返回: string 媒体类型
|
||||
func (img *EncodedImage) MIME() string { return "image/" + img.format }
|
||||
|
||||
// ColorModel 返回头信息中的颜色模型,不解码像素
|
||||
// ColorModel 返回显示像素的颜色模型,不解码像素
|
||||
// 返回: color.Model 颜色模型
|
||||
func (img *EncodedImage) ColorModel() color.Model { return img.config.ColorModel }
|
||||
func (img *EncodedImage) ColorModel() color.Model {
|
||||
if img.profile != nil || img.pngColor != nil || img.config.ColorModel == color.CMYKModel {
|
||||
return color.NRGBA64Model
|
||||
}
|
||||
return img.config.ColorModel
|
||||
}
|
||||
|
||||
// Bounds 返回图片范围,不解码像素
|
||||
// 返回: image.Rectangle 图片范围
|
||||
@@ -131,6 +140,18 @@ func (img *EncodedImage) decodeContext(ctx context.Context, pending chan struct{
|
||||
img.mu.Unlock()
|
||||
}()
|
||||
pixels, _, err := image.Decode(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(img.data)), context: ctx})
|
||||
if err == nil && (img.format == "jpeg" || img.profile != nil) {
|
||||
var profile *pdfgo.ColorSpace
|
||||
if img.profile != nil {
|
||||
profile, err = pdfgo.NewICCColorSpace(img.profile)
|
||||
}
|
||||
if err == nil {
|
||||
pixels, err = jpegDisplayImage(ctx, pixels, profile)
|
||||
}
|
||||
}
|
||||
if err == nil && img.pngColor != nil {
|
||||
pixels, err = applyImageColorProfile(ctx, pixels, img.pngColor.space, img.pngColor.linearSRGB)
|
||||
}
|
||||
if cancelErr := ctx.Err(); cancelErr != nil {
|
||||
return nil, cancelErr
|
||||
}
|
||||
@@ -173,5 +194,18 @@ func newEncodedImageContext(ctx context.Context, data []byte) (*EncodedImage, er
|
||||
if format != "jpeg" && format != "png" {
|
||||
return nil, fmt.Errorf("unsupported encoded image %q", format)
|
||||
}
|
||||
return &EncodedImage{data: data, format: format, config: config}, nil
|
||||
img := &EncodedImage{data: data, format: format, config: config}
|
||||
if format == "jpeg" {
|
||||
img.profile, err = jpegColorProfile(ctx, data, config.ColorModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if format == "png" {
|
||||
img.profile, img.pngColor, err = pngColorProfile(ctx, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
@@ -441,6 +441,46 @@ func PixelDataContext(ctx context.Context, img image.Image) (image.Image, error)
|
||||
// 入参: img 图片对象, x X坐标, y Y坐标
|
||||
// 返回: color.Gray 灰度像素
|
||||
func GrayAt(img image.Image, x, y int) color.Gray {
|
||||
switch src := img.(type) {
|
||||
case *EncodedImage:
|
||||
pixels, err := src.Image()
|
||||
if err != nil {
|
||||
return color.Gray{}
|
||||
}
|
||||
return GrayAt(pixels, x, y)
|
||||
case *colorImage:
|
||||
return GrayAt(src.source, x, y)
|
||||
case *maskedTIFFImage:
|
||||
if src.NRGBA64At(x, y).A == 0 {
|
||||
return color.Gray{}
|
||||
}
|
||||
bounds, size := src.source.Bounds(), src.bounds.Size()
|
||||
sx := bounds.Min.X + int((uint64(x-src.bounds.Min.X)*uint64(bounds.Dx())+uint64(bounds.Dx()/2))/uint64(size.X))
|
||||
sy := bounds.Min.Y + int((uint64(y-src.bounds.Min.Y)*uint64(bounds.Dy())+uint64(bounds.Dy()/2))/uint64(size.Y))
|
||||
return GrayAt(src.source, sx, sy)
|
||||
case *transparentEdgeImage:
|
||||
if x <= src.rect.Min.X || y <= src.rect.Min.Y || x >= src.rect.Max.X-1 || y >= src.rect.Max.Y-1 {
|
||||
return color.Gray{}
|
||||
}
|
||||
bounds := src.source.Bounds()
|
||||
return GrayAt(src.source, bounds.Min.X+x-1, bounds.Min.Y+y-1)
|
||||
case *transparentEdgeImage64:
|
||||
if x <= src.rect.Min.X || y <= src.rect.Min.Y || x >= src.rect.Max.X-1 || y >= src.rect.Max.Y-1 {
|
||||
return color.Gray{}
|
||||
}
|
||||
bounds := src.source.Bounds()
|
||||
return GrayAt(src.source, bounds.Min.X+x-1, bounds.Min.Y+y-1)
|
||||
case *orientedTIFFImage:
|
||||
point, ok := src.sourcePoint(x, y)
|
||||
if !ok {
|
||||
return color.Gray{}
|
||||
}
|
||||
return GrayAt(src.source, point.X, point.Y)
|
||||
case *planarTIFFImage:
|
||||
if src.cmyk {
|
||||
return src.cmykGrayAt(x, y)
|
||||
}
|
||||
}
|
||||
if src, ok := img.(*image.Gray); ok {
|
||||
return src.GrayAt(x, y)
|
||||
}
|
||||
@@ -662,12 +702,19 @@ func WithTransparentEdge(ctx context.Context, img image.Image) (image.Image, int
|
||||
return img, 0, nil
|
||||
}
|
||||
if src, ok := img.(*EncodedImage); ok && src.MIME() == "image/jpeg" {
|
||||
if src.profile != nil || src.config.ColorModel == color.CMYKModel {
|
||||
pixels, err := src.ImageContext(ctx)
|
||||
return pixels, 0, err
|
||||
}
|
||||
return img, 0, nil
|
||||
}
|
||||
source, err := PixelDataContext(ctx, img)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if encoded, ok := img.(*EncodedImage); ok && (encoded.profile != nil || encoded.pngColor != nil) {
|
||||
img = source
|
||||
}
|
||||
switch source.(type) {
|
||||
case *image.Gray, *image.Gray16, *image.YCbCr, *image.CMYK:
|
||||
return img, 0, nil
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
|
||||
package image
|
||||
|
||||
import "image"
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// StorageCost 预留十六位像素空间,并计入编码、调色板与底层像素缓冲容量
|
||||
// 包装图像可实现BackingImage() image.Image,使底层共享缓冲计入预算
|
||||
@@ -28,6 +31,25 @@ func StorageCost(img image.Image, limit int) int {
|
||||
}
|
||||
pixels := w * h * 8
|
||||
if encoded, ok := img.(*EncodedImage); ok {
|
||||
if encoded.pngColor != nil {
|
||||
if limit-cost < 512 {
|
||||
return limit + 1
|
||||
}
|
||||
cost += 512
|
||||
}
|
||||
if cap(encoded.profile) > limit-cost {
|
||||
return limit + 1
|
||||
}
|
||||
cost += cap(encoded.profile)
|
||||
if encoded.profile != nil || encoded.pngColor != nil || encoded.config.ColorModel == color.CMYKModel {
|
||||
if w > (limit-cost)/12/h {
|
||||
return limit + 1
|
||||
}
|
||||
pixels = w * h * 12
|
||||
}
|
||||
if result := encoded.result.Load(); result != nil && result.image != nil {
|
||||
pixels = max(pixels, StorageCost(result.image, limit-cost))
|
||||
}
|
||||
if cap(encoded.data) > limit-cost-pixels {
|
||||
return limit + 1
|
||||
}
|
||||
@@ -35,6 +57,9 @@ func StorageCost(img image.Image, limit int) int {
|
||||
}
|
||||
var buffers [5]int
|
||||
switch source := img.(type) {
|
||||
case *colorImage:
|
||||
buffers[0] = cap(source.Pix)
|
||||
buffers[1] = StorageCost(source.source, limit-cost)
|
||||
case *image.RGBA:
|
||||
buffers[0] = cap(source.Pix)
|
||||
case *image.RGBA64:
|
||||
|
||||
@@ -23,6 +23,8 @@ import (
|
||||
"image/color"
|
||||
"io"
|
||||
"math"
|
||||
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
|
||||
// tiffPlaneInput 将单分量目录附加到只读视图,像素仍从原始编码读取
|
||||
@@ -60,14 +62,18 @@ func (layout TIFFLayout) needsSampleView() bool {
|
||||
// 返回: uint64 每像素预算字节数
|
||||
func (layout TIFFLayout) DecodedPixelBytes() uint64 {
|
||||
samples := uint64(layout.Samples)
|
||||
profile := uint64(0)
|
||||
if layout.profileSize != 0 {
|
||||
profile = 8
|
||||
}
|
||||
if layout.Planar {
|
||||
if layout.photometric == 5 && samples > 4 {
|
||||
return 20
|
||||
return 20 + profile
|
||||
}
|
||||
return 16
|
||||
return 16 + profile
|
||||
}
|
||||
if !layout.needsSampleView() {
|
||||
return 8
|
||||
return 8 + profile
|
||||
}
|
||||
bytes := uint64(1)
|
||||
if layout.depth == 2 || layout.depth == 4 {
|
||||
@@ -75,7 +81,7 @@ func (layout TIFFLayout) DecodedPixelBytes() uint64 {
|
||||
} else if layout.depth > 8 {
|
||||
bytes = 2
|
||||
}
|
||||
return max(uint64(8), samples*bytes)
|
||||
return max(uint64(8), samples*bytes) + profile
|
||||
}
|
||||
|
||||
// decodeTIFFPixels 按存储方式解码指定目录并统一颜色和显示方向
|
||||
@@ -647,12 +653,12 @@ func (p *planarTIFFImage) ColorModel() color.Model {
|
||||
return color.GrayModel
|
||||
}
|
||||
if p.alpha == 1 {
|
||||
if p.depth > 8 {
|
||||
if p.depth > 8 || p.cmyk {
|
||||
return color.RGBA64Model
|
||||
}
|
||||
return color.RGBAModel
|
||||
}
|
||||
if p.depth > 8 {
|
||||
if p.depth > 8 || p.cmyk {
|
||||
return color.NRGBA64Model
|
||||
}
|
||||
return color.NRGBAModel
|
||||
@@ -674,10 +680,14 @@ func (p *planarTIFFImage) components(x, y int) [4]uint16 {
|
||||
return [4]uint16{}
|
||||
}
|
||||
}
|
||||
black := maximum - min(uint32(ink[3]), maximum)
|
||||
var components [4]float64
|
||||
for i := range components {
|
||||
components[i] = float64(min(uint32(ink[i]), maximum)) / float64(maximum)
|
||||
}
|
||||
rgb, _ := (&pdfgo.ColorSpace{Model: "DeviceCMYK"}).SRGB(components[:], "")
|
||||
var values [4]uint16
|
||||
for i := range 3 {
|
||||
values[i] = uint16((maximum - min(uint32(ink[i]), maximum)) * black / maximum)
|
||||
values[i] = uint16(math.Round(rgb[i] * float64(maximum)))
|
||||
}
|
||||
values[3] = ink[4]
|
||||
return values
|
||||
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
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 nil, fmt.Errorf("ICC source model does not match image color space %q", kind)
|
||||
}
|
||||
return pdfgo.NewICCColorSpace(data)
|
||||
}
|
||||
@@ -75,6 +75,13 @@ func readTIFFColorProfile(ctx context.Context, data []byte, layout TIFFLayout) (
|
||||
// 入参: ctx 取消上下文,可为nil, source 解码图像, profile 已校验的颜色配置
|
||||
// 返回: image.Image 独立透明度图像, error 颜色变换或取消错误
|
||||
func applyTIFFColorProfile(ctx context.Context, source image.Image, profile *pdfgo.ColorSpace) (image.Image, error) {
|
||||
return applyImageColorProfile(ctx, source, profile, false)
|
||||
}
|
||||
|
||||
// applyImageColorProfile 按源配置转换显示像素,缺省传递函数在浮点域解码
|
||||
// 入参: ctx 取消上下文, source 源像素, profile 颜色空间, linearSRGB 是否先解码sRGB传递函数
|
||||
// 返回: image.Image 十六位显示图像, error 变换或取消错误
|
||||
func applyImageColorProfile(ctx context.Context, source image.Image, profile *pdfgo.ColorSpace, linearSRGB bool) (image.Image, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
@@ -144,7 +151,16 @@ func applyTIFFColorProfile(ctx context.Context, source image.Image, profile *pdf
|
||||
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 linearSRGB {
|
||||
for i := range 3 {
|
||||
if values[i] <= 0.04045 {
|
||||
values[i] /= 12.92
|
||||
} else {
|
||||
values[i] = math.Pow((values[i]+0.055)/1.055, 2.4)
|
||||
}
|
||||
}
|
||||
}
|
||||
rgb, err := profile.SRGB(values[:profile.Components()], profile.DefaultIntent())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -161,7 +177,7 @@ func applyTIFFColorProfile(ctx context.Context, source image.Image, profile *pdf
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
return &colorImage{NRGBA64: result, source: source}, nil
|
||||
}
|
||||
|
||||
// newTIFFProfileTable 为离散颜色分配查找表,预乘颜色及小图保持逐像素变换
|
||||
|
||||
Reference in new issue
Block a user