refactor(色彩管理): 统一色彩与四色保真
build.yaml / build (push) Successful in 10m5s

This commit is contained in:
xiaoqidun committed 2026-10-16 00:05:46 +08:00
1 parent 5e0bca66e3
commit b5a8c9af9c
24 files changed
+935 -223

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/tdewolff/font v0.0.0-20261010204837-88a7f6b56918 github.com/tdewolff/font v0.0.0-20261010204837-88a7f6b56918
github.com/xiaoqidun/j2kgo v1.0.1 github.com/xiaoqidun/j2kgo v1.0.1
github.com/xiaoqidun/jbig2 v1.1.7 github.com/xiaoqidun/jbig2 v1.1.7
github.com/xiaoqidun/pdfgo v0.0.0-20261015084227-8b2c590c8095 github.com/xiaoqidun/pdfgo v0.0.0-20261015160218-fa5114475eb5
golang.org/x/crypto v0.58.0 golang.org/x/crypto v0.58.0
golang.org/x/image v0.47.0 golang.org/x/image v0.47.0
golang.org/x/text v0.43.0 golang.org/x/text v0.43.0
+2 -2
View File
@@ -62,8 +62,8 @@ github.com/xiaoqidun/j2kgo v1.0.1 h1:W0SytzjvgiOhXwq+JNRSVKLB847fjGOvGWTNKqmwvo0
github.com/xiaoqidun/j2kgo v1.0.1/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA= github.com/xiaoqidun/j2kgo v1.0.1/go.mod h1:DCwAfuDzCDYqUiAlZ7BDwigTjaXw7o9L1FtEma/D1aA=
github.com/xiaoqidun/jbig2 v1.1.7 h1:118d2lQn2ZsupmXazK+2p23Jp5xQ09btBmPUoYoa/+Y= github.com/xiaoqidun/jbig2 v1.1.7 h1:118d2lQn2ZsupmXazK+2p23Jp5xQ09btBmPUoYoa/+Y=
github.com/xiaoqidun/jbig2 v1.1.7/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA= github.com/xiaoqidun/jbig2 v1.1.7/go.mod h1:9jromqVwMqlptUDEt6Yip8vqYA5I1YjhG7FgMl/Z2dA=
github.com/xiaoqidun/pdfgo v0.0.0-20261015084227-8b2c590c8095 h1:lkK0xbGXJIKvH9zgKtf94HiGt35krCmy8EESPUCxRFE= github.com/xiaoqidun/pdfgo v0.0.0-20261015160218-fa5114475eb5 h1:eJuai2A2q9R8Mo/oKXiIpNc41uzolkKplPWXUmYiHcU=
github.com/xiaoqidun/pdfgo v0.0.0-20261015084227-8b2c590c8095/go.mod h1:FUQ8+TfNoec6tZCdsckx+rrD25JACm7EzuZyFH37QHg= github.com/xiaoqidun/pdfgo v0.0.0-20261015160218-fa5114475eb5/go.mod h1:FUQ8+TfNoec6tZCdsckx+rrD25JACm7EzuZyFH37QHg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+321
View File
@@ -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)}
}
+43 -9
View File
@@ -26,17 +26,21 @@ import (
"io" "io"
"sync" "sync"
"sync/atomic" "sync/atomic"
"github.com/xiaoqidun/pdfgo"
) )
// EncodedImage 保留PNG或JPEG原始编码,首次读取像素时解码 // EncodedImage 保留PNG或JPEG原始编码,首次读取像素时解码
// Bytes返回只读数据,Image返回共享只读像素,解码错误由Image返回 // Bytes返回只读数据,Image返回共享只读像素,解码错误由Image返回
type EncodedImage struct { type EncodedImage struct {
data []byte data []byte
format string format string
config image.Config config image.Config
result atomic.Pointer[encodedImageResult] profile []byte
mu sync.Mutex pngColor *pngColor
pending chan struct{} result atomic.Pointer[encodedImageResult]
mu sync.Mutex
pending chan struct{}
} }
// encodedImageResult 保存已完成的像素解码结果,不保存取消或超时错误 // encodedImageResult 保存已完成的像素解码结果,不保存取消或超时错误
@@ -65,9 +69,14 @@ func (img *EncodedImage) Bytes() []byte { return img.data }
// 返回: string 媒体类型 // 返回: string 媒体类型
func (img *EncodedImage) MIME() string { return "image/" + img.format } func (img *EncodedImage) MIME() string { return "image/" + img.format }
// ColorModel 返回头信息中的颜色模型,不解码像素 // ColorModel 返回显示像素的颜色模型,不解码像素
// 返回: color.Model 颜色模型 // 返回: 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 返回图片范围,不解码像素 // Bounds 返回图片范围,不解码像素
// 返回: image.Rectangle 图片范围 // 返回: image.Rectangle 图片范围
@@ -131,6 +140,18 @@ func (img *EncodedImage) decodeContext(ctx context.Context, pending chan struct{
img.mu.Unlock() img.mu.Unlock()
}() }()
pixels, _, err := image.Decode(contextInput{ReadCloser: io.NopCloser(bytes.NewReader(img.data)), context: ctx}) 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 { if cancelErr := ctx.Err(); cancelErr != nil {
return nil, cancelErr return nil, cancelErr
} }
@@ -173,5 +194,18 @@ func newEncodedImageContext(ctx context.Context, data []byte) (*EncodedImage, er
if format != "jpeg" && format != "png" { if format != "jpeg" && format != "png" {
return nil, fmt.Errorf("unsupported encoded image %q", format) 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
} }
+47
View File
@@ -441,6 +441,46 @@ func PixelDataContext(ctx context.Context, img image.Image) (image.Image, error)
// 入参: img 图片对象, x X坐标, y Y坐标 // 入参: img 图片对象, x X坐标, y Y坐标
// 返回: color.Gray 灰度像素 // 返回: color.Gray 灰度像素
func GrayAt(img image.Image, x, y int) 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 { if src, ok := img.(*image.Gray); ok {
return src.GrayAt(x, y) return src.GrayAt(x, y)
} }
@@ -662,12 +702,19 @@ func WithTransparentEdge(ctx context.Context, img image.Image) (image.Image, int
return img, 0, nil return img, 0, nil
} }
if src, ok := img.(*EncodedImage); ok && src.MIME() == "image/jpeg" { 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 return img, 0, nil
} }
source, err := PixelDataContext(ctx, img) source, err := PixelDataContext(ctx, img)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
if encoded, ok := img.(*EncodedImage); ok && (encoded.profile != nil || encoded.pngColor != nil) {
img = source
}
switch source.(type) { switch source.(type) {
case *image.Gray, *image.Gray16, *image.YCbCr, *image.CMYK: case *image.Gray, *image.Gray16, *image.YCbCr, *image.CMYK:
return img, 0, nil return img, 0, nil
+26 -1
View File
@@ -14,7 +14,10 @@
package image package image
import "image" import (
"image"
"image/color"
)
// StorageCost 预留十六位像素空间,并计入编码、调色板与底层像素缓冲容量 // StorageCost 预留十六位像素空间,并计入编码、调色板与底层像素缓冲容量
// 包装图像可实现BackingImage() image.Image,使底层共享缓冲计入预算 // 包装图像可实现BackingImage() image.Image,使底层共享缓冲计入预算
@@ -28,6 +31,25 @@ func StorageCost(img image.Image, limit int) int {
} }
pixels := w * h * 8 pixels := w * h * 8
if encoded, ok := img.(*EncodedImage); ok { 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 { if cap(encoded.data) > limit-cost-pixels {
return limit + 1 return limit + 1
} }
@@ -35,6 +57,9 @@ func StorageCost(img image.Image, limit int) int {
} }
var buffers [5]int var buffers [5]int
switch source := img.(type) { switch source := img.(type) {
case *colorImage:
buffers[0] = cap(source.Pix)
buffers[1] = StorageCost(source.source, limit-cost)
case *image.RGBA: case *image.RGBA:
buffers[0] = cap(source.Pix) buffers[0] = cap(source.Pix)
case *image.RGBA64: case *image.RGBA64:
+18 -8
View File
@@ -23,6 +23,8 @@ import (
"image/color" "image/color"
"io" "io"
"math" "math"
"github.com/xiaoqidun/pdfgo"
) )
// tiffPlaneInput 将单分量目录附加到只读视图,像素仍从原始编码读取 // tiffPlaneInput 将单分量目录附加到只读视图,像素仍从原始编码读取
@@ -60,14 +62,18 @@ func (layout TIFFLayout) needsSampleView() bool {
// 返回: uint64 每像素预算字节数 // 返回: uint64 每像素预算字节数
func (layout TIFFLayout) DecodedPixelBytes() uint64 { func (layout TIFFLayout) DecodedPixelBytes() uint64 {
samples := uint64(layout.Samples) samples := uint64(layout.Samples)
profile := uint64(0)
if layout.profileSize != 0 {
profile = 8
}
if layout.Planar { if layout.Planar {
if layout.photometric == 5 && samples > 4 { if layout.photometric == 5 && samples > 4 {
return 20 return 20 + profile
} }
return 16 return 16 + profile
} }
if !layout.needsSampleView() { if !layout.needsSampleView() {
return 8 return 8 + profile
} }
bytes := uint64(1) bytes := uint64(1)
if layout.depth == 2 || layout.depth == 4 { if layout.depth == 2 || layout.depth == 4 {
@@ -75,7 +81,7 @@ func (layout TIFFLayout) DecodedPixelBytes() uint64 {
} else if layout.depth > 8 { } else if layout.depth > 8 {
bytes = 2 bytes = 2
} }
return max(uint64(8), samples*bytes) return max(uint64(8), samples*bytes) + profile
} }
// decodeTIFFPixels 按存储方式解码指定目录并统一颜色和显示方向 // decodeTIFFPixels 按存储方式解码指定目录并统一颜色和显示方向
@@ -647,12 +653,12 @@ func (p *planarTIFFImage) ColorModel() color.Model {
return color.GrayModel return color.GrayModel
} }
if p.alpha == 1 { if p.alpha == 1 {
if p.depth > 8 { if p.depth > 8 || p.cmyk {
return color.RGBA64Model return color.RGBA64Model
} }
return color.RGBAModel return color.RGBAModel
} }
if p.depth > 8 { if p.depth > 8 || p.cmyk {
return color.NRGBA64Model return color.NRGBA64Model
} }
return color.NRGBAModel return color.NRGBAModel
@@ -674,10 +680,14 @@ func (p *planarTIFFImage) components(x, y int) [4]uint16 {
return [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 var values [4]uint16
for i := range 3 { 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] values[3] = ink[4]
return values return values
+19 -3
View File
@@ -31,7 +31,7 @@ import (
func parseTIFFColorProfile(kind string, data []byte) (*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)] model := map[string]string{"GRAY": "GRAY", "RGB": "RGB ", "CMYK": "CMYK"}[strings.ToUpper(kind)]
if len(data) < 132 || model == "" || string(data[16:20]) != model { 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) return pdfgo.NewICCColorSpace(data)
} }
@@ -75,6 +75,13 @@ func readTIFFColorProfile(ctx context.Context, data []byte, layout TIFFLayout) (
// 入参: ctx 取消上下文,可为nil, source 解码图像, profile 已校验的颜色配置 // 入参: ctx 取消上下文,可为nil, source 解码图像, profile 已校验的颜色配置
// 返回: image.Image 独立透明度图像, error 颜色变换或取消错误 // 返回: image.Image 独立透明度图像, error 颜色变换或取消错误
func applyTIFFColorProfile(ctx context.Context, source image.Image, profile *pdfgo.ColorSpace) (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 { if ctx == nil {
ctx = context.Background() ctx = context.Background()
} }
@@ -144,7 +151,16 @@ func applyTIFFColorProfile(ctx context.Context, source image.Image, profile *pdf
alpha = pixel.A alpha = pixel.A
values = [4]float64{float64(pixel.R) / 65535, float64(pixel.G) / 65535, float64(pixel.B) / 65535} 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 { if err != nil {
return nil, err return nil, err
} }
@@ -161,7 +177,7 @@ func applyTIFFColorProfile(ctx context.Context, source image.Image, profile *pdf
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
} }
return result, nil return &colorImage{NRGBA64: result, source: source}, nil
} }
// newTIFFProfileTable 为离散颜色分配查找表,预乘颜色及小图保持逐像素变换 // newTIFFProfileTable 为离散颜色分配查找表,预乘颜色及小图保持逐像素变换
+1 -5
View File
@@ -76,11 +76,7 @@ func (c *TransferCache) Resolve(reader *pdfgo.Reader, style pdfgo.Style, model p
// 入参: space 合成空间, values 非预乘分量 // 入参: space 合成空间, values 非预乘分量
// 返回: [3]float64 显示颜色, error 校准颜色转换错误 // 返回: [3]float64 显示颜色, error 校准颜色转换错误
func OutputColor(space *pdfgo.ColorSpace, values [4]float64) ([3]float64, error) { func OutputColor(space *pdfgo.ColorSpace, values [4]float64) ([3]float64, error) {
if space.Model == "DeviceCMYK" && !space.Calibrated() { return space.SRGB(values[:space.Components()], "RelativeColorimetric")
black := 1 - values[3]
return [3]float64{(1 - values[0]) * black, (1 - values[1]) * black, (1 - values[2]) * black}, nil
}
return space.RGB(values[:space.Components()], "RelativeColorimetric")
} }
// NormalBlend 判断混合模式是否为普通覆盖 // NormalBlend 判断混合模式是否为普通覆盖
+120
View File
@@ -15,6 +15,11 @@
package pdf package pdf
import ( import (
"bytes"
"compress/zlib"
"context"
"encoding/binary"
"fmt"
"image" "image"
"image/color" "image/color"
"math" "math"
@@ -23,6 +28,121 @@ import (
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
// EncodeCMYKTIFF 将直接四色图像保存为十六位无损TIFF,不经过显示颜色转换
// 入参: ctx 取消上下文, source PDF源图像
// 返回: []byte 含ICC和非预乘透明度的TIFF,非四色图像为nil, error 解码或编码错误
func EncodeCMYKTIFF(ctx context.Context, source *pdfgo.Image) ([]byte, error) {
if ctx == nil || source == nil {
return nil, fmt.Errorf("missing CMYK image context or source")
}
if err := ctx.Err(); err != nil {
return nil, err
}
if source.ImageMask {
return nil, nil
}
family, err := source.ColorSpaceFamily()
if err != nil || family != "DeviceCMYK" && family != "ICCBased" {
return nil, err
}
if family == "ICCBased" {
count, err := source.ColorSpaceComponents()
if err != nil || count != 4 {
return nil, err
}
}
components, err := source.DecodeComponentsViewContext(ctx)
if err != nil {
return nil, err
}
if components.Space == nil || components.Space.Model != "DeviceCMYK" || components.Space.Components() != 4 || components.ColorantSpace != nil {
return nil, nil
}
bounds := components.Rect
width, height := bounds.Dx(), bounds.Dy()
if width <= 0 || height <= 0 || width > int(^uint(0)>>1)/10 || uint64(width) > uint64(^uint32(0))/10 || uint64(height) > uint64(^uint32(0)) {
return nil, fmt.Errorf("invalid CMYK TIFF dimensions")
}
profile := components.Space.ICCProfile(source.Intent)
count := 12
if len(profile) != 0 {
count++
}
bitsOffset := uint64(8 + 2 + count*12 + 4)
profileOffset := bitsOffset + 10
stripOffset := (profileOffset + uint64(len(profile)) + 1) &^ uint64(1)
if stripOffset > uint64(^uint32(0)) {
return nil, fmt.Errorf("CMYK TIFF profile exceeds size limit")
}
var compressed bytes.Buffer
writer := zlib.NewWriter(&compressed)
row := make([]byte, width*10)
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)%1024 == 0 {
if err := ctx.Err(); err != nil {
writer.Close()
return nil, err
}
}
values, alpha := components.ValuesAt(x, y)
pixel := row[(x-bounds.Min.X)*10:]
for channel, value := range [5]float64{values[0], values[1], values[2], values[3], alpha} {
if math.IsNaN(value) || math.IsInf(value, 0) || value < 0 || value > 1 {
writer.Close()
return nil, fmt.Errorf("invalid CMYK TIFF component")
}
binary.LittleEndian.PutUint16(pixel[channel*2:], uint16(math.Round(value*65535)))
}
}
if _, err := writer.Write(row); err != nil {
writer.Close()
return nil, err
}
if stripOffset+uint64(compressed.Len()) > uint64(^uint32(0)) {
writer.Close()
return nil, fmt.Errorf("CMYK TIFF exceeds size limit")
}
}
if err := writer.Close(); err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
total := stripOffset + uint64(compressed.Len())
if total > uint64(^uint32(0)) || total > uint64(int(^uint(0)>>1)) {
return nil, fmt.Errorf("CMYK TIFF exceeds size limit")
}
data := make([]byte, int(total))
copy(data, "II")
binary.LittleEndian.PutUint16(data[2:], 42)
binary.LittleEndian.PutUint32(data[4:], 8)
binary.LittleEndian.PutUint16(data[8:], uint16(count))
entries := [][4]uint32{
{256, 4, 1, uint32(width)}, {257, 4, 1, uint32(height)},
{258, 3, 5, uint32(bitsOffset)}, {259, 3, 1, 8}, {262, 3, 1, 5},
{273, 4, 1, uint32(stripOffset)}, {277, 3, 1, 5}, {278, 4, 1, uint32(height)},
{279, 4, 1, uint32(compressed.Len())}, {284, 3, 1, 1}, {332, 3, 1, 1}, {338, 3, 1, 2},
}
if len(profile) != 0 {
entries = append(entries, [4]uint32{34675, 7, uint32(len(profile)), uint32(profileOffset)})
}
for i, entry := range entries {
p := data[10+i*12:]
binary.LittleEndian.PutUint16(p, uint16(entry[0]))
binary.LittleEndian.PutUint16(p[2:], uint16(entry[1]))
binary.LittleEndian.PutUint32(p[4:], entry[2])
binary.LittleEndian.PutUint32(p[8:], entry[3])
}
for i := range 5 {
binary.LittleEndian.PutUint16(data[int(bitsOffset)+2*i:], 16)
}
copy(data[int(profileOffset):], profile)
copy(data[int(stripOffset):], compressed.Bytes())
return data, nil
}
// StencilImage 按当前填充色读取模板图像,不分配整幅彩色样本 // StencilImage 按当前填充色读取模板图像,不分配整幅彩色样本
type StencilImage struct { type StencilImage struct {
Source image.Image Source image.Image
+25 -3
View File
@@ -97,11 +97,33 @@ func (r *Renderer) colorDefinition(id string) *ColorSpace {
return space return space
} }
// rgb 转换归一化源分量,不在ICC变换前量化为8位 // srgb 转换归一化源分量,不在ICC变换前量化为8位
// 入参: values 源分量 // 入参: values 源分量
// 返回: [3]float64 sRGB分量, error 变换错误 // 返回: [3]float64 sRGB分量, error 变换错误
func (p *colorProfile) rgb(values [4]float64) ([3]float64, error) { func (p *colorProfile) srgb(values [4]float64) ([3]float64, error) {
return p.space.RGB(values[:p.space.Components()], p.space.DefaultIntent()) return p.space.SRGB(values[:p.space.Components()], p.space.DefaultIntent())
}
// displayColor 按源模型及配置转换显示颜色,保持源分量精度
// 入参: kind 源模型, values 归一化分量, profile 可选ICC配置
// 返回: [3]float64 sRGB分量, error 模型或转换错误
func displayColor(kind string, values [4]float64, profile *colorProfile) ([3]float64, error) {
if profile != nil {
return profile.srgb(values)
}
var model pdfgo.Name
switch kind {
case "GRAY":
model = "DeviceGray"
case "RGB":
model = "DeviceRGB"
case "CMYK":
model = "DeviceCMYK"
default:
return [3]float64{}, fmt.Errorf("unsupported color space %q", kind)
}
space := pdfgo.ColorSpace{Model: model}
return space.SRGB(values[:space.Components()], "RelativeColorimetric")
} }
// profileColor 将显示分量一次量化并按透明度预乘 // profileColor 将显示分量一次量化并按透明度预乘
+4 -15
View File
@@ -278,7 +278,6 @@ func (e *Editor) Color(value *FillColor) (color.NRGBA, error) {
if len(fields) != 0 && len(fields) != count { if len(fields) != 0 && len(fields) != count {
return color.NRGBA{}, fmt.Errorf("expected %d color components", count) return color.NRGBA{}, fmt.Errorf("expected %d color components", count)
} }
var components [4]uint8
var normalized [4]float64 var normalized [4]float64
limit := 1<<bits - 1 limit := 1<<bits - 1
for i, field := range fields { for i, field := range fields {
@@ -286,15 +285,8 @@ func (e *Editor) Color(value *FillColor) (color.NRGBA, error) {
if err != nil || v < 0 || v > limit { if err != nil || v < 0 || v > limit {
return color.NRGBA{}, fmt.Errorf("invalid color component %q", field) return color.NRGBA{}, fmt.Errorf("invalid color component %q", field)
} }
components[i] = uint8((v*255 + limit/2) / limit)
normalized[i] = float64(v) / float64(limit) normalized[i] = float64(v) / float64(limit)
} }
r, g, b := components[0], components[1], components[2]
if kind == "GRAY" {
g, b = r, r
} else if kind == "CMYK" {
r, g, b = color.CMYKToRGB(components[0], components[1], components[2], components[3])
}
var reader *Reader var reader *Reader
if e.source != nil { if e.source != nil {
reader = e.source.reader reader = e.source.reader
@@ -303,12 +295,9 @@ func (e *Editor) Color(value *FillColor) (color.NRGBA, error) {
if err != nil { if err != nil {
return color.NRGBA{}, err return color.NRGBA{}, err
} }
if profile != nil { rgb, err := displayColor(kind, normalized, profile)
rgb, err := profile.rgb(normalized) if err != nil {
if err != nil { return color.NRGBA{}, err
return color.NRGBA{}, err
}
r, g, b = meshByte(rgb[0]), meshByte(rgb[1]), meshByte(rgb[2])
} }
a := 255 a := 255
if value.Alpha != nil { if value.Alpha != nil {
@@ -317,7 +306,7 @@ func (e *Editor) Color(value *FillColor) (color.NRGBA, error) {
return color.NRGBA{}, fmt.Errorf("color alpha must be between 0 and 255") return color.NRGBA{}, fmt.Errorf("color alpha must be between 0 and 255")
} }
} }
return color.NRGBA{R: r, G: g, B: b, A: uint8(a)}, nil return color.NRGBA{R: meshByte(rgb[0]), G: meshByte(rgb[1]), B: meshByte(rgb[2]), A: uint8(a)}, nil
} }
// RGBColor 创建浏览器等调用方使用的RGB纯色,非RGB文档按需注册独立颜色空间 // RGBColor 创建浏览器等调用方使用的RGB纯色,非RGB文档按需注册独立颜色空间
+126 -80
View File
@@ -15,8 +15,10 @@
package ofdgo package ofdgo
import ( import (
"fmt"
"image/color" "image/color"
"math" "math"
"slices"
"strconv" "strconv"
"strings" "strings"
) )
@@ -30,100 +32,109 @@ type PatternPaint struct {
// ResolveColor 按文档颜色空间和调色板解析颜色,返回预乘透明度的RGBA // ResolveColor 按文档颜色空间和调色板解析颜色,返回预乘透明度的RGBA
// 入参: value 颜色分量, index 调色板索引, space 颜色空间标识, alpha 透明度 // 入参: value 颜色分量, index 调色板索引, space 颜色空间标识, alpha 透明度
// 返回: color.RGBA 标准颜色 // 返回: color.RGBA 标准颜色, error 颜色定义或配置错误
func (r *Renderer) ResolveColor(value string, index *int, space string, alpha *int) color.RGBA { func (r *Renderer) ResolveColor(value string, index *int, space string, alpha *int) (color.RGBA, error) {
return colorToRGBA(r.parseColorWithAlpha(value, index, space, alpha)) kind, values, err := r.colorComponents(value, index, space)
if err != nil {
return color.RGBA{}, err
}
if alpha != nil && (*alpha < 0 || *alpha > 255) {
return color.RGBA{}, fmt.Errorf("color alpha must be between 0 and 255")
}
profile, err := r.Reader.colorProfile(r.colorDefinition(space))
if err != nil {
return color.RGBA{}, err
}
rgb, err := displayColor(kind, values, profile)
if err != nil {
return color.RGBA{}, err
}
a := 1.0
if alpha != nil {
a = float64(*alpha) / 255
}
return profileColor(rgb, a), nil
} }
// GradientStops 解析OFD渐变分段的位置与透明度,返回独立数据并保留原分段顺序 // GradientStops 解析OFD渐变分段的位置与透明度,返回独立数据并保留原分段顺序
// 入参: segments 渐变分段, alpha 透明度 // 入参: segments 渐变分段, alpha 透明度
// 返回: []ColorStop 后端无关的渐变色标 // 返回: []ColorStop 后端无关的渐变色标, error 颜色定义或配置错误
func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []ColorStop { func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) ([]ColorStop, error) {
var gradient []ColorStop var gradient []ColorStop
positions := gradientPositions(segments) positions := gradientPositions(segments)
for i, segment := range segments { for i, segment := range segments {
segmentAlpha := mergeAlpha(segment.Color.Alpha, alpha) segmentAlpha := mergeAlpha(segment.Color.Alpha, alpha)
gradient = append(gradient, ColorStop{Offset: positions[i], Color: r.ResolveColor(segment.Color.Value, segment.Color.Index, segment.Color.ColorSpace, segmentAlpha)}) value, err := r.ResolveColor(segment.Color.Value, segment.Color.Index, segment.Color.ColorSpace, segmentAlpha)
if err != nil {
return nil, err
}
gradient = append(gradient, ColorStop{Offset: positions[i], Color: value})
} }
return gradient return gradient, nil
} }
// parseColorWithAlpha 解析带透明度的颜色 // parseColorWithAlpha 解析带透明度的颜色
// 入参: value 色值, index 调色板索引, space 颜色空间标识, alpha 透明度 // 入参: value 色值, index 调色板索引, space 颜色空间标识, alpha 透明度
// 返回: color.Color 颜色对象 // 返回: color.Color 颜色对象
func (r *Renderer) parseColorWithAlpha(value string, index *int, space string, alpha *int) color.Color { func (r *Renderer) parseColorWithAlpha(value string, index *int, space string, alpha *int) color.Color {
kind, values := r.colorComponents(value, index, space) result, err := r.ResolveColor(value, index, space, alpha)
profile, err := r.Reader.colorProfile(r.colorDefinition(space))
if err != nil { if err != nil {
r.renderError = err r.renderError = err
return color.RGBA{}
} }
if profile != nil { return result
rgb, err := profile.rgb(values)
if err != nil {
r.renderError = err
return color.RGBA{}
}
a := 1.0
if alpha != nil {
a = float64(clampColor(*alpha)) / 255
}
return profileColor(rgb, a)
}
var components [4]uint8
for i, value := range values {
components[i] = uint8(math.Round(value * 255))
}
red, green, blue := components[0], components[1], components[2]
switch kind {
case "GRAY":
green, blue = red, red
case "CMYK":
red, green, blue = color.CMYKToRGB(components[0], components[1], components[2], components[3])
}
a := 255
if alpha != nil {
a = clampColor(*alpha)
}
return color.RGBA{R: uint8(int(red) * a / 255), G: uint8(int(green) * a / 255), B: uint8(int(blue) * a / 255), A: uint8(a)}
} }
// colorComponents 读取颜色空间及归一化分量,保留源位深供渐变插值 // colorComponents 读取颜色空间及归一化分量,保留源位深供渐变插值
// 入参: value 色值, index 调色板索引, space 颜色空间标识 // 入参: value 色值, index 调色板索引, space 颜色空间标识
// 返回: string 颜色模型, [4]float64 单位分量 // 返回: string 颜色模型, [4]float64 单位分量, error 颜色定义错误
func (r *Renderer) colorComponents(value string, index *int, space string) (string, [4]float64) { func (r *Renderer) colorComponents(value string, index *int, space string) (string, [4]float64, error) {
cs := r.colorDefinition(space) cs := r.colorDefinition(space)
if cs == nil && (space != "" || r.Reader != nil && r.Reader.doc != nil && r.Reader.doc.CommonData.DefaultCS != 0) {
if space == "" {
space = strconv.Itoa(r.Reader.doc.CommonData.DefaultCS)
}
return "", [4]float64{}, fmt.Errorf("color space %q not found", space)
}
kind, bits, count := "RGB", 8, 3 kind, bits, count := "RGB", 8, 3
if cs != nil { if cs != nil {
kind = strings.ToUpper(cs.Type) kind = strings.ToUpper(cs.Type)
switch cs.BitsPerComponent { switch cs.BitsPerComponent {
case 0:
case 1, 2, 4, 8, 16: case 1, 2, 4, 8, 16:
bits = cs.BitsPerComponent bits = cs.BitsPerComponent
} default:
if strings.TrimSpace(value) == "" && index != nil && 0 <= *index && *index < len(cs.Palette) { return "", [4]float64{}, fmt.Errorf("unsupported color depth %d", cs.BitsPerComponent)
value = cs.Palette[*index]
} }
} }
if strings.TrimSpace(value) == "" && index != nil {
if cs == nil || *index < 0 || *index >= len(cs.Palette) {
return "", [4]float64{}, fmt.Errorf("color palette index out of range")
}
value = cs.Palette[*index]
}
switch kind { switch kind {
case "RGB":
case "GRAY": case "GRAY":
count = 1 count = 1
case "CMYK": case "CMYK":
count = 4 count = 4
default:
return "", [4]float64{}, fmt.Errorf("unsupported color space %q", kind)
} }
parts := strings.Fields(value) parts := strings.Fields(value)
var components [4]float64 var components [4]float64
limit := 1<<bits - 1 limit := 1<<bits - 1
if len(parts) >= count { if len(parts) != 0 && len(parts) != count {
for i, part := range parts[:count] { return "", components, fmt.Errorf("expected %d color components", count)
v, err := parseColorComponent(part)
if err != nil || v < 0 || limit < v {
components = [4]float64{}
break
}
components[i] = float64(v) / float64(limit)
}
} }
return kind, components for i, part := range parts {
v, err := parseColorComponent(part)
if err != nil || v < 0 || limit < v {
return "", [4]float64{}, fmt.Errorf("invalid color component %q", part)
}
components[i] = float64(v) / float64(limit)
}
return kind, components, nil
} }
// parseColorComponent 解析颜色分量 // parseColorComponent 解析颜色分量
@@ -157,6 +168,12 @@ func mergeAlpha(colorAlpha, objectAlpha *int) *int {
if colorAlpha == nil && objectAlpha == nil { if colorAlpha == nil && objectAlpha == nil {
return nil return nil
} }
for _, value := range []*int{colorAlpha, objectAlpha} {
if value != nil && (*value < 0 || *value > 255) {
alpha := *value
return &alpha
}
}
alpha := 255 alpha := 255
if colorAlpha != nil { if colorAlpha != nil {
alpha = *colorAlpha alpha = *colorAlpha
@@ -164,7 +181,6 @@ func mergeAlpha(colorAlpha, objectAlpha *int) *int {
if objectAlpha != nil { if objectAlpha != nil {
alpha = alpha * *objectAlpha / 255 alpha = alpha * *objectAlpha / 255
} }
alpha = clampColor(alpha)
return &alpha return &alpha
} }
@@ -259,11 +275,15 @@ func (r *Renderer) parseShdColor(segments []ShdSegment, alpha *int) color.Color
return r.parseColorWithAlpha(c.Value, c.Index, c.ColorSpace, mergeAlpha(c.Alpha, alpha)) return r.parseColorWithAlpha(c.Value, c.Index, c.ColorSpace, mergeAlpha(c.Alpha, alpha))
} }
// renderGradientStops 在CMYK分量空间插值,将RGB分段误差控制在半个8位灰阶内 // renderGradientStops 在源CMYK空间插值,保留截断边界及透明度变化
// 入参: segments 原始颜色分段, alpha 对象透明度 // 入参: segments 原始颜色分段, alpha 对象透明度
// 返回: []ColorStop 仅供绘制的颜色节点,不改变原始分段 // 返回: []ColorStop 仅供绘制的颜色节点,不改变原始分段
func (r *Renderer) renderGradientStops(segments []ShdSegment, alpha *int) []ColorStop { func (r *Renderer) renderGradientStops(segments []ShdSegment, alpha *int) []ColorStop {
stops := r.GradientStops(segments, alpha) stops, err := r.GradientStops(segments, alpha)
if err != nil {
r.renderError = err
return nil
}
if len(stops) < 2 { if len(stops) < 2 {
return stops return stops
} }
@@ -271,31 +291,14 @@ func (r *Renderer) renderGradientStops(segments []ShdSegment, alpha *int) []Colo
for i, stop := range stops { for i, stop := range stops {
if i > 0 && stops[i-1].Offset < stop.Offset { if i > 0 && stops[i-1].Offset < stop.Offset {
a, b := segments[i-1].Color, segments[i].Color a, b := segments[i-1].Color, segments[i].Color
leftKind, left := r.colorComponents(a.Value, a.Index, a.ColorSpace) leftKind, left, _ := r.colorComponents(a.Value, a.Index, a.ColorSpace)
rightKind, right := r.colorComponents(b.Value, b.Index, b.ColorSpace) rightKind, right, _ := r.colorComponents(b.Value, b.Index, b.ColorSpace)
if leftKind == "CMYK" && rightKind == "CMYK" { leftSpace, rightSpace := r.colorDefinition(a.ColorSpace), r.colorDefinition(b.ColorSpace)
a0, a1 := float64(stops[i-1].Color.A)/255, float64(stop.Color.A)/255 if leftKind == "CMYK" && rightKind == "CMYK" && (leftSpace == nil || leftSpace.Profile == "") && (rightSpace == nil || rightSpace.Profile == "") {
dk, da := right[3]-left[3], a1-a0 if result == nil {
bound := 0.0 result = append([]ColorStop(nil), stops[:i]...)
for c := 0; c < 3; c++ {
dc := right[c] - left[c]
bound = math.Max(bound, 2*math.Abs(dc*dk)+2*math.Abs(da)*(math.Abs(dc)+math.Abs(dk)))
}
steps := int(math.Ceil(math.Sqrt(bound * 255 / 4)))
if steps > 1 && result == nil {
result = append(make([]ColorStop, 0, len(stops)+steps-1), stops[:i]...)
}
for j := 1; j < steps; j++ {
t := float64(j) / float64(steps)
var values [4]float64
for c := range values {
values[c] = left[c]*(1-t) + right[c]*t
}
opacity := a0*(1-t) + a1*t
black := (1 - values[3]) * opacity * 255
color := color.RGBA{R: uint8(math.Round((1 - values[0]) * black)), G: uint8(math.Round((1 - values[1]) * black)), B: uint8(math.Round((1 - values[2]) * black)), A: uint8(math.Round(opacity * 255))}
result = append(result, ColorStop{Offset: stops[i-1].Offset*(1-t) + stop.Offset*t, Color: color})
} }
result = append(result, cmykGradientStops(left, right, stops[i-1], stop)...)
} }
} }
if result != nil { if result != nil {
@@ -308,6 +311,49 @@ func (r *Renderer) renderGradientStops(segments []ShdSegment, alpha *int) []Colo
return result return result
} }
// cmykGradientStops 在加黑截断点分段,将预乘插值误差控制在四分之一灰阶内
// 入参: left 左端分量, right 右端分量, start 左端色标, end 右端色标
// 返回: []ColorStop 不含端点的显示色标
func cmykGradientStops(left, right [4]float64, start, end ColorStop) []ColorStop {
positions := []float64{0, 1}
a0, a1 := float64(start.Color.A)/255, float64(end.Color.A)/255
bound := 0.0
for channel := 0; channel < 3; channel++ {
first, last := left[channel]+left[3], right[channel]+right[3]
delta := last - first
bound = math.Max(bound, 2*math.Abs((a1-a0)*delta))
if delta != 0 {
position := (1 - first) / delta
if 0 < position && position < 1 {
positions = append(positions, position)
}
}
}
slices.Sort(positions)
positions = slices.Compact(positions)
var result []ColorStop
for i := 1; i < len(positions); i++ {
first, last := positions[i-1], positions[i]
steps := max(1, int(math.Ceil((last-first)*math.Sqrt(bound*255/2))))
for step := 1; step <= steps; step++ {
t := first + (last-first)*float64(step)/float64(steps)
if t >= 1 {
continue
}
var values [4]float64
for channel := range values {
values[channel] = left[channel]*(1-t) + right[channel]*t
}
r, g, b := meshRGB("CMYK", values)
result = append(result, ColorStop{
Offset: start.Offset*(1-t) + end.Offset*t,
Color: profileColor([3]float64{r, g, b}, a0*(1-t)+a1*t),
})
}
}
return result
}
// gradientPositions 按显式锚点补全渐变分段位置,保留原顺序 // gradientPositions 按显式锚点补全渐变分段位置,保留原顺序
// 入参: segments 渐变分段 // 入参: segments 渐变分段
// 返回: []float64 分段位置 // 返回: []float64 分段位置
+17 -20
View File
@@ -230,7 +230,10 @@ func (r *Renderer) resolveMesh(fill *FillColor) (*MeshShading, error) {
vertices := make([]MeshVertex, len(points)) vertices := make([]MeshVertex, len(points))
for i, point := range points { for i, point := range points {
space := point.Color.ColorSpace space := point.Color.ColorSpace
kind, values := r.colorComponents(point.Color.Value, point.Color.Index, space) kind, values, err := r.colorComponents(point.Color.Value, point.Color.Index, space)
if err != nil {
return nil, err
}
profile, err := r.Reader.colorProfile(r.colorDefinition(space)) profile, err := r.Reader.colorProfile(r.colorDefinition(space))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -244,7 +247,11 @@ func (r *Renderer) resolveMesh(fill *FillColor) (*MeshShading, error) {
mesh := &MeshShading{} mesh := &MeshShading{}
if extend == 1 && back != nil { if extend == 1 && back != nil {
space := back.ColorSpace space := back.ColorSpace
mesh.Background = r.ResolveColor(back.Value, back.Index, space, mergeAlpha(back.Alpha, fill.Alpha)) var err error
mesh.Background, err = r.ResolveColor(back.Value, back.Index, space, mergeAlpha(back.Alpha, fill.Alpha))
if err != nil {
return nil, err
}
} }
for _, triangle := range indices { for _, triangle := range indices {
mesh.Triangles = append(mesh.Triangles, [3]MeshVertex{vertices[triangle[0]], vertices[triangle[1]], vertices[triangle[2]]}) mesh.Triangles = append(mesh.Triangles, [3]MeshVertex{vertices[triangle[0]], vertices[triangle[1]], vertices[triangle[2]]})
@@ -279,17 +286,14 @@ func (v MeshVertex) sameSpace(other MeshVertex) bool {
// 入参: values 插值分量 // 入参: values 插值分量
// 返回: float64 红、绿、蓝分量 // 返回: float64 红、绿、蓝分量
func (v MeshVertex) rgb(values [4]float64) (float64, float64, float64) { func (v MeshVertex) rgb(values [4]float64) (float64, float64, float64) {
if v.profile != nil { for i := range values {
for i := range values { values[i] = math.Max(0, math.Min(1, values[i]))
values[i] = math.Max(0, math.Min(1, values[i]))
}
rgb, err := v.profile.rgb(values)
if err != nil {
return 0, 0, 0
}
return rgb[0], rgb[1], rgb[2]
} }
return meshRGB(v.Space, values) rgb, err := displayColor(v.Space, values, v.profile)
if err != nil {
return 0, 0, 0
}
return rgb[0], rgb[1], rgb[2]
} }
// meshTriangleIndices 校验控制点并生成共享边或格构三角形编号 // meshTriangleIndices 校验控制点并生成共享边或格构三角形编号
@@ -344,14 +348,7 @@ func meshTriangleIndices(points []ShdPoint, columns, extend int) ([][3]int, erro
// 入参: space 颜色模型, values 分量 // 入参: space 颜色模型, values 分量
// 返回: float64 红、绿、蓝分量 // 返回: float64 红、绿、蓝分量
func meshRGB(space string, values [4]float64) (float64, float64, float64) { func meshRGB(space string, values [4]float64) (float64, float64, float64) {
switch space { return (MeshVertex{Space: space}).rgb(values)
case "GRAY":
return values[0], values[0], values[0]
case "CMYK":
return (1 - values[0]) * (1 - values[3]), (1 - values[1]) * (1 - values[3]), (1 - values[2]) * (1 - values[3])
default:
return values[0], values[1], values[2]
}
} }
// meshByte 将归一化颜色分量量化为八位整数 // meshByte 将归一化颜色分量量化为八位整数
+6
View File
@@ -93,6 +93,9 @@ func (r *Renderer) ResolvePaint(fill *FillColor) (Paint, error) {
if node := fill.AxialShd; node != nil { if node := fill.AxialShd; node != nil {
start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint) start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint)
stops := r.renderGradientStops(node.Segment, fill.Alpha) stops := r.renderGradientStops(node.Segment, fill.Alpha)
if r.renderError != nil {
return Paint{}, r.renderError
}
sample, err := r.profileGradient(node.Segment, fill.Alpha) sample, err := r.profileGradient(node.Segment, fill.Alpha)
if err != nil { if err != nil {
return Paint{}, err return Paint{}, err
@@ -107,6 +110,9 @@ func (r *Renderer) ResolvePaint(fill *FillColor) (Paint, error) {
if node := fill.RadialShd; node != nil && node.EndRadius > 0 { if node := fill.RadialShd; node != nil && node.EndRadius > 0 {
start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint) start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint)
stops := r.renderGradientStops(node.Segment, fill.Alpha) stops := r.renderGradientStops(node.Segment, fill.Alpha)
if r.renderError != nil {
return Paint{}, r.renderError
}
sample, err := r.profileGradient(node.Segment, fill.Alpha) sample, err := r.profileGradient(node.Segment, fill.Alpha)
if err != nil { if err != nil {
return Paint{}, err return Paint{}, err
+5 -2
View File
@@ -41,7 +41,10 @@ func (r *Renderer) profileGradient(segments []ShdSegment, alpha *int) (func(floa
} }
gradient := &profileGradient{positions: gradientPositions(segments), colors: make([]MeshVertex, len(segments))} gradient := &profileGradient{positions: gradientPositions(segments), colors: make([]MeshVertex, len(segments))}
for i, segment := range segments { for i, segment := range segments {
kind, values := r.colorComponents(segment.Color.Value, segment.Color.Index, segment.Color.ColorSpace) kind, values, err := r.colorComponents(segment.Color.Value, segment.Color.Index, segment.Color.ColorSpace)
if err != nil {
return nil, err
}
profile, err := r.Reader.colorProfile(r.colorDefinition(segment.Color.ColorSpace)) profile, err := r.Reader.colorProfile(r.colorDefinition(segment.Color.ColorSpace))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -63,7 +66,7 @@ func (g *profileGradient) At(t float64) color.RGBA {
return color.RGBA{} return color.RGBA{}
} }
first := g.colors[0] first := g.colors[0]
if t <= g.positions[0] { if t < g.positions[0] {
r, green, b := first.rgb(first.Values) r, green, b := first.rgb(first.Values)
return profileColor([3]float64{r, green, b}, first.Alpha) return profileColor([3]float64{r, green, b}, first.Alpha)
} }
+11
View File
@@ -81,6 +81,7 @@ type pdfImporter struct {
clipTexts map[*pdfgo.TextClip][]TextObject clipTexts map[*pdfgo.TextClip][]TextObject
clipPaths *renderCache[[32]byte, pdfClipPath] clipPaths *renderCache[[32]byte, pdfClipPath]
cmykSpace string cmykSpace string
rgbSpace string
iccSpaces map[pdfICCKey]string iccSpaces map[pdfICCKey]string
objects *pdfObjectBuffer objects *pdfObjectBuffer
formResources *pdfFormCache formResources *pdfFormCache
@@ -173,7 +174,17 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
} }
defer reader.Close() defer reader.Close()
importer.reader = reader importer.reader = reader
outputIntents, err := reader.OutputIntents()
if err != nil {
return nil, PDFImportReport{}, fmt.Errorf("import PDF output intents: %w", err)
}
importer.report.Warnings = diagnostics importer.report.Warnings = diagnostics
if len(outputIntents) != 0 {
if options.Strict {
return nil, PDFImportReport{}, &pdfgo.UnsupportedError{Feature: "PDF output intent conversion"}
}
importer.report.Warnings = append(importer.report.Warnings, pdfgo.Diagnostic{Message: "PDF output intents not transferred to OFD"})
}
importer.pageIndexes = make(map[pdfgo.Reference]int) importer.pageIndexes = make(map[pdfgo.Reference]int)
importer.resolveFile = options.ResolveFile importer.resolveFile = options.ResolveFile
importer.resolveReference = options.ResolveReference importer.resolveReference = options.ResolveReference
+1 -1
View File
@@ -948,7 +948,7 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
rgb, err = codecpdf.OutputColor(space, values) rgb, err = codecpdf.OutputColor(space, values)
} }
} else if c.transfers != nil { } else if c.transfers != nil {
rgb, err = space.RGB(pixel.Values[:space.Components()], "RelativeColorimetric") rgb, err = space.SRGB(pixel.Values[:space.Components()], "RelativeColorimetric")
} else { } else {
if colorReady && pixel.Values == colorValues { if colorReady && pixel.Values == colorValues {
rgb = colorRGB rgb = colorRGB
+14 -4
View File
@@ -445,12 +445,22 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
ctm = pdfNumbers(m[:]...) ctm = pdfNumbers(m[:]...)
} }
paint := func(value pdfgo.Paint) (*FillColor, error) { paint := func(value pdfgo.Paint) (*FillColor, error) {
if value.Tiling != nil || pdfGradientPattern(value) { if value.Tiling == nil && value.Axial == nil && value.Radial == nil && value.Mesh == nil && value.Function == nil && value.Shading == nil {
return p.paintColor(value, box) return p.color(value, mark.Style.RenderingIntent)
} }
return drawing.paintColor(value, origin) if value.Tiling != nil || pdfGradientPattern(value) {
return p.paintColor(value, box, mark.Style.RenderingIntent)
}
return drawing.paintColor(value, origin, mark.Style.RenderingIntent)
}
fillColor, err := p.color(mark.Style.Fill, mark.Style.RenderingIntent)
if err != nil {
return err
}
strokePaint, err := p.color(mark.Style.Stroke, mark.Style.RenderingIntent)
if err != nil {
return err
} }
fillColor, strokePaint := p.color(mark.Style.Fill), p.color(mark.Style.Stroke)
if mark.Fill { if mark.Fill {
fillColor, err = paint(mark.Style.Fill) fillColor, err = paint(mark.Style.Fill)
if err != nil { if err != nil {
+17 -9
View File
@@ -32,8 +32,9 @@ var errPDFGroupRaster = errors.New("PDF transparency group requires local compos
// pdfPaintRegion 保存透明组内的单色填充区域 // pdfPaintRegion 保存透明组内的单色填充区域
type pdfPaintRegion struct { type pdfPaintRegion struct {
paint pdfgo.Paint paint pdfgo.Paint
path GeometryPath path GeometryPath
intent pdfgo.Name
} }
// group 将单色图元的透明组转换为互不重叠的填充区域,保留矢量与组不透明度 // group 将单色图元的透明组转换为互不重叠的填充区域,保留矢量与组不透明度
@@ -138,6 +139,7 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
var regions []pdfPaintRegion var regions []pdfPaintRegion
var combined GeometryPath var combined GeometryPath
var paint *pdfgo.Paint var paint *pdfgo.Paint
var intent pdfgo.Name
var clips []pdfgo.Path var clips []pdfgo.Path
opaque := true opaque := true
scale := math.Hypot(p.matrix[0], p.matrix[1]) scale := math.Hypot(p.matrix[0], p.matrix[1])
@@ -182,12 +184,14 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
opaque = opaque && color.Alpha == 1 opaque = opaque && color.Alpha == 1
if paint == nil { if paint == nil {
paint, clips = &color, path.Style.Clips paint, clips = &color, path.Style.Clips
intent = path.Style.RenderingIntent
} else if !reflect.DeepEqual(clips, path.Style.Clips) { } else if !reflect.DeepEqual(clips, path.Style.Clips) {
return fmt.Errorf("%w: independently clipped group content", errPDFGroupRaster) return fmt.Errorf("%w: independently clipped group content", errPDFGroupRaster)
} else if color.Alpha != 1 || !reflect.DeepEqual(*paint, color) { } else if color.Alpha != 1 || !reflect.DeepEqual(*paint, color) || intent != path.Style.RenderingIntent {
regions = append(regions, pdfPaintRegion{*paint, combined}) regions = append(regions, pdfPaintRegion{*paint, combined, intent})
combined = nil combined = nil
paint = &color paint = &color
intent = path.Style.RenderingIntent
} }
outline := shape outline := shape
evenOdd := path.Path.EvenOdd evenOdd := path.Path.EvenOdd
@@ -251,7 +255,7 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
if len(combined) == 0 { if len(combined) == 0 {
return nil return nil
} }
regions = append(regions, pdfPaintRegion{*paint, combined}) regions = append(regions, pdfPaintRegion{*paint, combined, intent})
if !opaque { if !opaque {
regions, err = pdfCompositeRegions(geometry, regions) regions, err = pdfCompositeRegions(geometry, regions)
if err != nil { if err != nil {
@@ -301,7 +305,11 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
if err != nil { if err != nil {
return err return err
} }
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: data, Fill: &fill, Stroke: &stroke, FillColor: p.color(region.paint), Clips: objectClips} color, err := p.color(region.paint, region.intent)
if err != nil {
return err
}
object := PathObject{Boundary: pdfBoundary(box), AbbreviatedData: data, Fill: &fill, Stroke: &stroke, FillColor: color, Clips: objectClips}
p.appendObject(GraphicObject{Type: "PathObject", PathObject: object}) p.appendObject(GraphicObject{Type: "PathObject", PathObject: object})
p.report.PathObjects++ p.report.PathObjects++
} }
@@ -523,7 +531,7 @@ func pdfCompositeRegions(geometry GeometryBackend, regions []pdfPaintRegion) ([]
return nil, err return nil, err
} }
if len(outside) != 0 { if len(outside) != 0 {
next = append(next, pdfPaintRegion{backdrop.paint, outside}) next = append(next, pdfPaintRegion{backdrop.paint, outside, backdrop.intent})
} }
overlap, err := geometry.Combine(backdrop.path, source.path, GeometryIntersect) overlap, err := geometry.Combine(backdrop.path, source.path, GeometryIntersect)
if err != nil { if err != nil {
@@ -534,7 +542,7 @@ func pdfCompositeRegions(geometry GeometryBackend, regions []pdfPaintRegion) ([]
for i := range paint.RGB { for i := range paint.RGB {
paint.RGB[i] = (source.paint.RGB[i]*source.paint.Alpha + backdrop.paint.RGB[i]*backdrop.paint.Alpha*(1-source.paint.Alpha)) / paint.Alpha paint.RGB[i] = (source.paint.RGB[i]*source.paint.Alpha + backdrop.paint.RGB[i]*backdrop.paint.Alpha*(1-source.paint.Alpha)) / paint.Alpha
} }
next = append(next, pdfPaintRegion{paint, overlap}) next = append(next, pdfPaintRegion{paint, overlap, ""})
} }
remaining, err = geometry.Combine(remaining, backdrop.path, GeometrySubtract) remaining, err = geometry.Combine(remaining, backdrop.path, GeometrySubtract)
if err != nil { if err != nil {
@@ -542,7 +550,7 @@ func pdfCompositeRegions(geometry GeometryBackend, regions []pdfPaintRegion) ([]
} }
} }
if len(remaining) != 0 { if len(remaining) != 0 {
next = append(next, pdfPaintRegion{source.paint, remaining}) next = append(next, pdfPaintRegion{source.paint, remaining, source.intent})
} }
result = next result = next
} }
+48 -37
View File
@@ -106,12 +106,14 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
} }
if source.ImageMask { if source.ImageMask {
fill := mark.Style.Fill.RGB fill := mark.Style.Fill.RGB
if values := mark.Style.Fill.CMYK; values != nil { if space := mark.Style.Fill.Space; space != nil {
fill, err = space.SRGB(mark.Style.Fill.Values[:space.Components()], mark.Style.RenderingIntent)
} else if values := mark.Style.Fill.CMYK; values != nil {
space := &pdfgo.ColorSpace{Model: "DeviceCMYK"} space := &pdfgo.ColorSpace{Model: "DeviceCMYK"}
fill, err = space.RGB(values[:], mark.Style.RenderingIntent) fill, err = space.SRGB(values[:], mark.Style.RenderingIntent)
if err != nil { }
return err if err != nil {
} return err
} }
key.tint = color.NRGBA64{ key.tint = color.NRGBA64{
R: uint16(math.Round(fill[0] * 65535)), R: uint16(math.Round(fill[0] * 65535)),
@@ -145,41 +147,50 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
} }
} }
if id == "" { if id == "" {
var data []byte
if !source.ImageMask && key.process == "" && p.compression.Mode != CompressionLossy {
data, err = codecpdf.EncodeCMYKTIFF(p.ctx, source)
if err != nil {
return err
}
}
var jbig2Original, jpegOriginal []byte var jbig2Original, jpegOriginal []byte
var decoded image.Image var decoded image.Image
if key.process != "" {
decoded, err = p.imageProcess(source, key.process)
if err != nil {
return err
}
if key.size != (image.Point{}) {
decoded, err = pdfgo.ResizeImage(p.ctx, decoded, key.size)
if err != nil {
return err
}
}
} else {
jbig2Original, err = source.JBIG2FileContext(p.ctx)
if err != nil {
return err
}
jpegOriginal, err = source.JPEGFileContext(p.ctx)
if err != nil {
return err
}
if len(jbig2Original) == 0 && len(jpegOriginal) == 0 && (source.ImageMask || key.size != (image.Point{})) {
decoded, err = source.DecodeImageSizeContext(p.ctx, key.size)
if err != nil {
return err
}
}
}
if source.ImageMask {
decoded = &codecpdf.StencilImage{Source: decoded, Fill: key.tint}
}
data := jbig2Original
if len(data) == 0 { if len(data) == 0 {
data = jpegOriginal if key.process != "" {
decoded, err = p.imageProcess(source, key.process)
if err != nil {
return err
}
if key.size != (image.Point{}) {
decoded, err = pdfgo.ResizeImage(p.ctx, decoded, key.size)
if err != nil {
return err
}
}
} else {
jbig2Original, err = source.JBIG2FileContext(p.ctx)
if err != nil {
return err
}
jpegOriginal, err = source.JPEGFileContext(p.ctx)
if err != nil {
return err
}
if len(jbig2Original) == 0 && len(jpegOriginal) == 0 && (source.ImageMask || key.size != (image.Point{})) {
decoded, err = source.DecodeImageSizeContext(p.ctx, key.size)
if err != nil {
return err
}
}
}
if source.ImageMask {
decoded = &codecpdf.StencilImage{Source: decoded, Fill: key.tint}
}
data = jbig2Original
if len(data) == 0 {
data = jpegOriginal
}
} }
optimized := false optimized := false
if len(data) == 0 && decoded != nil && !source.ImageMask && p.compression.Mode == CompressionLossy { if len(data) == 0 && decoded != nil && !source.ImageMask && p.compression.Mode == CompressionLossy {
+7 -4
View File
@@ -166,7 +166,7 @@ func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error)
shading.Point = append(shading.Point, ShdPoint{X: point.X - box.X, Y: point.Y - box.Y, EdgeFlag: flag, Color: ShdColor{Value: value, ColorSpace: icc}}) shading.Point = append(shading.Point, ShdPoint{X: point.X - box.X, Y: point.Y - box.Y, EdgeFlag: flag, Color: ShdColor{Value: value, ColorSpace: icc}})
continue continue
} }
rgb, err := mesh.Space.RGB(values[:mesh.Space.Components()], mesh.Intent) rgb, err := mesh.Space.SRGB(values[:mesh.Space.Components()], mesh.Intent)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -174,7 +174,10 @@ func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error)
if mesh.Space.Model == "DeviceCMYK" && !mesh.Space.Calibrated() { if mesh.Space.Model == "DeviceCMYK" && !mesh.Space.Calibrated() {
color.CMYK = (*[4]float64)(values) color.CMYK = (*[4]float64)(values)
} }
converted := p.color(color) converted, err := p.color(color, mesh.Intent)
if err != nil {
return nil, err
}
shading.Point = append(shading.Point, ShdPoint{X: point.X - box.X, Y: point.Y - box.Y, EdgeFlag: flag, Color: ShdColor{Value: converted.Value, ColorSpace: converted.ColorSpace}}) shading.Point = append(shading.Point, ShdPoint{X: point.X - box.X, Y: point.Y - box.Y, EdgeFlag: flag, Color: ShdColor{Value: converted.Value, ColorSpace: converted.ColorSpace}})
} }
previous.Points = triangle.Points previous.Points = triangle.Points
@@ -190,8 +193,8 @@ func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error)
} }
shading.Extend, shading.BackColor = 1, &background shading.Extend, shading.BackColor = 1, &background
} }
color := p.color(paint) alpha := int(math.Round(paint.Alpha * 255))
color.Value = "" color := &FillColor{Alpha: &alpha}
color.GouraudShd = shading color.GouraudShd = shading
return color, nil return color, nil
} }
+50 -16
View File
@@ -158,7 +158,10 @@ func (p *pdfImporter) gradientSegments(stops []pdfgo.GradientStop, space *pdfgo.
if cmyk { if cmyk {
paint.CMYK = &stop.Values paint.CMYK = &stop.Values
} }
color := p.color(paint) color, err := p.color(paint, intent)
if err != nil {
return nil, err
}
segments[i] = ShdSegment{Position: stop.Position, Color: ShdColor{Value: color.Value, ColorSpace: color.ColorSpace}} segments[i] = ShdSegment{Position: stop.Position, Color: ShdColor{Value: color.Value, ColorSpace: color.ColorSpace}}
} }
return segments, nil return segments, nil
@@ -283,7 +286,7 @@ func (p *pdfImporter) gradientPattern(paint pdfgo.Paint, box Box) (*FillColor, e
if _, ok := matrix.Inverse(); !ok { if _, ok := matrix.Inverse(); !ok {
return nil, fmt.Errorf("invalid PDF gradient bounds matrix") return nil, fmt.Errorf("invalid PDF gradient bounds matrix")
} }
fill, err := p.paintColor(plain, box) fill, err := p.paintColor(plain, box, intent)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -337,7 +340,8 @@ func (p *pdfImporter) gradientPattern(paint pdfgo.Paint, box Box) (*FillColor, e
objects = append(objects, object) objects = append(objects, object)
} }
} }
color := p.color(paint) alpha := int(math.Round(paint.Alpha * 255))
color := &FillColor{Alpha: &alpha}
color.Pattern = &Pattern{Width: box.W, Height: box.H, RelativeTo: "Page", CTM: pdfNumbers(1, 0, 0, 1, box.X, box.Y), CellContent: PatternContent{PathObject: objects}} color.Pattern = &Pattern{Width: box.W, Height: box.H, RelativeTo: "Page", CTM: pdfNumbers(1, 0, 0, 1, box.X, box.Y), CellContent: PatternContent{PathObject: objects}}
return color, nil return color, nil
} }
@@ -352,7 +356,7 @@ func (p *pdfImporter) gradientBackgroundColor(values [4]float64, space *pdfgo.Co
stop := pdfgo.GradientStop{Values: values} stop := pdfgo.GradientStop{Values: values}
if !pdfICCTransfer(space) && (space.Model != "DeviceCMYK" || space.Calibrated()) { if !pdfICCTransfer(space) && (space.Model != "DeviceCMYK" || space.Calibrated()) {
var err error var err error
stop.RGB, err = space.RGB(values[:space.Components()], intent) stop.RGB, err = space.SRGB(values[:space.Components()], intent)
if err != nil { if err != nil {
return ShdColor{}, err return ShdColor{}, err
} }
@@ -726,26 +730,55 @@ func pdfShadingClippedNode(node pdfCompositeNode, box pdfgo.Rectangle, matrix pd
return node return node
} }
// color 保留设备色彩分量并复用文档颜色空间 // color 保留设备及ICC源分量,其他校准色以16位显示颜色输出
// 入参: paint PDF颜色与不透明度 // 入参: paint PDF颜色与不透明度, intent 实际渲染意图
// 返回: *FillColor OFD颜色 // 返回: *FillColor OFD颜色, error 配置或颜色转换错误
func (p *pdfImporter) color(paint pdfgo.Paint) *FillColor { func (p *pdfImporter) color(paint pdfgo.Paint, intent pdfgo.Name) (*FillColor, error) {
alpha := int(math.Round(paint.Alpha * 255)) alpha := int(math.Round(paint.Alpha * 255))
if paint.CMYK != nil && (paint.Space == nil || !paint.Space.Calibrated()) { if pdfICCTransfer(paint.Space) {
id, err := p.iccColorSpace(paint.Space, intent)
if err != nil {
return nil, err
}
value, err := pdfICCValues(paint.Values[:], paint.Space.Components())
if err != nil {
return nil, err
}
return &FillColor{ColorSpace: id, Value: value, Alpha: &alpha}, nil
}
cmyk := paint.CMYK
if cmyk == nil && paint.Space != nil && paint.Space.Model == "DeviceCMYK" && !paint.Space.Calibrated() {
cmyk = &paint.Values
}
if cmyk != nil && (paint.Space == nil || !paint.Space.Calibrated()) {
if p.cmykSpace == "" { if p.cmykSpace == "" {
p.cmykSpace = p.editor.nextID() p.cmykSpace = p.editor.nextID()
p.editor.resources = append(p.editor.resources, editorResource{space: &ColorSpace{ID: p.cmykSpace, Type: "CMYK", BitsPerComponent: 16}}) p.editor.resources = append(p.editor.resources, editorResource{space: &ColorSpace{ID: p.cmykSpace, Type: "CMYK", BitsPerComponent: 16}})
} }
v := paint.CMYK v := cmyk
return &FillColor{ColorSpace: p.cmykSpace, Value: pdfNumbers(math.Round(v[0]*65535), math.Round(v[1]*65535), math.Round(v[2]*65535), math.Round(v[3]*65535)), Alpha: &alpha} return &FillColor{ColorSpace: p.cmykSpace, Value: pdfNumbers(math.Round(v[0]*65535), math.Round(v[1]*65535), math.Round(v[2]*65535), math.Round(v[3]*65535)), Alpha: &alpha}, nil
} }
return &FillColor{Value: pdfNumbers(math.Round(paint.RGB[0]*255), math.Round(paint.RGB[1]*255), math.Round(paint.RGB[2]*255)), Alpha: &alpha} if paint.Space.Calibrated() {
if p.rgbSpace == "" {
id, err := p.editor.AddColorSpace(ColorSpace{Type: "RGB", BitsPerComponent: 16}, nil)
if err != nil {
return nil, err
}
p.rgbSpace = id
}
value, err := pdfICCValues(paint.RGB[:], 3)
if err != nil {
return nil, err
}
return &FillColor{ColorSpace: p.rgbSpace, Value: value, Alpha: &alpha}, nil
}
return &FillColor{Value: pdfNumbers(math.Round(paint.RGB[0]*255), math.Round(paint.RGB[1]*255), math.Round(paint.RGB[2]*255)), Alpha: &alpha}, nil
} }
// paintColor 将页面渐变转换为对象局部坐标 // paintColor 将页面渐变转换为对象局部坐标
// 入参: paint PDF画刷, box 对象边界 // 入参: paint PDF画刷, box 对象边界, intent 实际渲染意图
// 返回: *FillColor OFD颜色或渐变, error 图案转换错误 // 返回: *FillColor OFD颜色或渐变, error 图案转换错误
func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error) { func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box, intent pdfgo.Name) (*FillColor, error) {
if err := p.gradientError(paint); err != nil { if err := p.gradientError(paint); err != nil {
return nil, err return nil, err
} }
@@ -755,7 +788,8 @@ func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error)
if paint.Mesh != nil { if paint.Mesh != nil {
return p.meshColor(paint, box) return p.meshColor(paint, box)
} }
color := p.color(paint) alpha := int(math.Round(paint.Alpha * 255))
color := &FillColor{Alpha: &alpha}
if paint.Tiling != nil { if paint.Tiling != nil {
pattern, err := p.tilingPattern(paint.Tiling, paint) pattern, err := p.tilingPattern(paint.Tiling, paint)
if err != nil { if err != nil {
@@ -798,7 +832,7 @@ func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error)
return color, nil return color, nil
} }
if paint.Axial == nil { if paint.Axial == nil {
return color, nil return p.color(paint, intent)
} }
gradient := paint.Axial gradient := paint.Axial
start := p.matrix.Apply(gradient.Start) start := p.matrix.Apply(gradient.Start)
+6 -3
View File
@@ -325,7 +325,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
} }
object.Boundary = pdfBoundary(box) object.Boundary = pdfBoundary(box)
if stroke && !outlineStroke { if stroke && !outlineStroke {
strokePaint, err := p.paintColor(mark.Style.Stroke, box) strokePaint, err := p.paintColor(mark.Style.Stroke, box, mark.Style.RenderingIntent)
if err != nil { if err != nil {
return err return err
} }
@@ -339,9 +339,12 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
object.Fill = &fill object.Fill = &fill
object.Stroke = &stroke object.Stroke = &stroke
object.Visible = &visible object.Visible = &visible
object.FillColor = p.color(mark.Style.Fill) object.FillColor, err = p.color(mark.Style.Fill, mark.Style.RenderingIntent)
if err != nil {
return err
}
if fill && !outlineStroke { if fill && !outlineStroke {
object.FillColor, err = p.paintColor(mark.Style.Fill, box) object.FillColor, err = p.paintColor(mark.Style.Fill, box, mark.Style.RenderingIntent)
if err != nil { if err != nil {
return err return err
} }