mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
This commit is contained in:
1 parent
5e0bca66e3
commit
b5a8c9af9c
24 files changed
+935
-223
No files matched your search
@@ -10,7 +10,7 @@ require (
|
||||
github.com/tdewolff/font v0.0.0-20261010204837-88a7f6b56918
|
||||
github.com/xiaoqidun/j2kgo v1.0.1
|
||||
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/image v0.47.0
|
||||
golang.org/x/text v0.43.0
|
||||
|
||||
@@ -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/jbig2 v1.1.7 h1:118d2lQn2ZsupmXazK+2p23Jp5xQ09btBmPUoYoa/+Y=
|
||||
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-20261015084227-8b2c590c8095/go.mod h1:FUQ8+TfNoec6tZCdsckx+rrD25JACm7EzuZyFH37QHg=
|
||||
github.com/xiaoqidun/pdfgo v0.0.0-20261015160218-fa5114475eb5 h1:eJuai2A2q9R8Mo/oKXiIpNc41uzolkKplPWXUmYiHcU=
|
||||
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/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||
|
||||
@@ -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 为离散颜色分配查找表,预乘颜色及小图保持逐像素变换
|
||||
|
||||
@@ -76,11 +76,7 @@ func (c *TransferCache) Resolve(reader *pdfgo.Reader, style pdfgo.Style, model p
|
||||
// 入参: space 合成空间, values 非预乘分量
|
||||
// 返回: [3]float64 显示颜色, error 校准颜色转换错误
|
||||
func OutputColor(space *pdfgo.ColorSpace, values [4]float64) ([3]float64, error) {
|
||||
if space.Model == "DeviceCMYK" && !space.Calibrated() {
|
||||
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")
|
||||
return space.SRGB(values[:space.Components()], "RelativeColorimetric")
|
||||
}
|
||||
|
||||
// NormalBlend 判断混合模式是否为普通覆盖
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
package pdf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
@@ -23,6 +28,121 @@ import (
|
||||
"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 按当前填充色读取模板图像,不分配整幅彩色样本
|
||||
type StencilImage struct {
|
||||
Source image.Image
|
||||
|
||||
+25
-3
@@ -97,11 +97,33 @@ func (r *Renderer) colorDefinition(id string) *ColorSpace {
|
||||
return space
|
||||
}
|
||||
|
||||
// rgb 转换归一化源分量,不在ICC变换前量化为8位
|
||||
// srgb 转换归一化源分量,不在ICC变换前量化为8位
|
||||
// 入参: values 源分量
|
||||
// 返回: [3]float64 sRGB分量, error 变换错误
|
||||
func (p *colorProfile) rgb(values [4]float64) ([3]float64, error) {
|
||||
return p.space.RGB(values[:p.space.Components()], p.space.DefaultIntent())
|
||||
func (p *colorProfile) srgb(values [4]float64) ([3]float64, error) {
|
||||
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 将显示分量一次量化并按透明度预乘
|
||||
|
||||
+4
-15
@@ -278,7 +278,6 @@ func (e *Editor) Color(value *FillColor) (color.NRGBA, error) {
|
||||
if len(fields) != 0 && len(fields) != count {
|
||||
return color.NRGBA{}, fmt.Errorf("expected %d color components", count)
|
||||
}
|
||||
var components [4]uint8
|
||||
var normalized [4]float64
|
||||
limit := 1<<bits - 1
|
||||
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 {
|
||||
return color.NRGBA{}, fmt.Errorf("invalid color component %q", field)
|
||||
}
|
||||
components[i] = uint8((v*255 + limit/2) / 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
|
||||
if e.source != nil {
|
||||
reader = e.source.reader
|
||||
@@ -303,12 +295,9 @@ func (e *Editor) Color(value *FillColor) (color.NRGBA, error) {
|
||||
if err != nil {
|
||||
return color.NRGBA{}, err
|
||||
}
|
||||
if profile != nil {
|
||||
rgb, err := profile.rgb(normalized)
|
||||
if err != nil {
|
||||
return color.NRGBA{}, err
|
||||
}
|
||||
r, g, b = meshByte(rgb[0]), meshByte(rgb[1]), meshByte(rgb[2])
|
||||
rgb, err := displayColor(kind, normalized, profile)
|
||||
if err != nil {
|
||||
return color.NRGBA{}, err
|
||||
}
|
||||
a := 255
|
||||
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{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文档按需注册独立颜色空间
|
||||
|
||||
+126
-80
@@ -15,8 +15,10 @@
|
||||
package ofdgo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -30,100 +32,109 @@ type PatternPaint struct {
|
||||
|
||||
// ResolveColor 按文档颜色空间和调色板解析颜色,返回预乘透明度的RGBA
|
||||
// 入参: value 颜色分量, index 调色板索引, space 颜色空间标识, alpha 透明度
|
||||
// 返回: color.RGBA 标准颜色
|
||||
func (r *Renderer) ResolveColor(value string, index *int, space string, alpha *int) color.RGBA {
|
||||
return colorToRGBA(r.parseColorWithAlpha(value, index, space, alpha))
|
||||
// 返回: color.RGBA 标准颜色, error 颜色定义或配置错误
|
||||
func (r *Renderer) ResolveColor(value string, index *int, space string, alpha *int) (color.RGBA, error) {
|
||||
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渐变分段的位置与透明度,返回独立数据并保留原分段顺序
|
||||
// 入参: segments 渐变分段, alpha 透明度
|
||||
// 返回: []ColorStop 后端无关的渐变色标
|
||||
func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) []ColorStop {
|
||||
// 返回: []ColorStop 后端无关的渐变色标, error 颜色定义或配置错误
|
||||
func (r *Renderer) GradientStops(segments []ShdSegment, alpha *int) ([]ColorStop, error) {
|
||||
var gradient []ColorStop
|
||||
positions := gradientPositions(segments)
|
||||
for i, segment := range segments {
|
||||
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 解析带透明度的颜色
|
||||
// 入参: value 色值, index 调色板索引, space 颜色空间标识, alpha 透明度
|
||||
// 返回: color.Color 颜色对象
|
||||
func (r *Renderer) parseColorWithAlpha(value string, index *int, space string, alpha *int) color.Color {
|
||||
kind, values := r.colorComponents(value, index, space)
|
||||
profile, err := r.Reader.colorProfile(r.colorDefinition(space))
|
||||
result, err := r.ResolveColor(value, index, space, alpha)
|
||||
if err != nil {
|
||||
r.renderError = err
|
||||
return color.RGBA{}
|
||||
}
|
||||
if profile != nil {
|
||||
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)}
|
||||
return result
|
||||
}
|
||||
|
||||
// colorComponents 读取颜色空间及归一化分量,保留源位深供渐变插值
|
||||
// 入参: value 色值, index 调色板索引, space 颜色空间标识
|
||||
// 返回: string 颜色模型, [4]float64 单位分量
|
||||
func (r *Renderer) colorComponents(value string, index *int, space string) (string, [4]float64) {
|
||||
// 返回: string 颜色模型, [4]float64 单位分量, error 颜色定义错误
|
||||
func (r *Renderer) colorComponents(value string, index *int, space string) (string, [4]float64, error) {
|
||||
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
|
||||
if cs != nil {
|
||||
kind = strings.ToUpper(cs.Type)
|
||||
switch cs.BitsPerComponent {
|
||||
case 0:
|
||||
case 1, 2, 4, 8, 16:
|
||||
bits = cs.BitsPerComponent
|
||||
}
|
||||
if strings.TrimSpace(value) == "" && index != nil && 0 <= *index && *index < len(cs.Palette) {
|
||||
value = cs.Palette[*index]
|
||||
default:
|
||||
return "", [4]float64{}, fmt.Errorf("unsupported color depth %d", cs.BitsPerComponent)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
case "RGB":
|
||||
case "GRAY":
|
||||
count = 1
|
||||
case "CMYK":
|
||||
count = 4
|
||||
default:
|
||||
return "", [4]float64{}, fmt.Errorf("unsupported color space %q", kind)
|
||||
}
|
||||
parts := strings.Fields(value)
|
||||
var components [4]float64
|
||||
limit := 1<<bits - 1
|
||||
if len(parts) >= count {
|
||||
for i, part := range parts[:count] {
|
||||
v, err := parseColorComponent(part)
|
||||
if err != nil || v < 0 || limit < v {
|
||||
components = [4]float64{}
|
||||
break
|
||||
}
|
||||
components[i] = float64(v) / float64(limit)
|
||||
}
|
||||
if len(parts) != 0 && len(parts) != count {
|
||||
return "", components, fmt.Errorf("expected %d color components", count)
|
||||
}
|
||||
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 解析颜色分量
|
||||
@@ -157,6 +168,12 @@ func mergeAlpha(colorAlpha, objectAlpha *int) *int {
|
||||
if colorAlpha == nil && objectAlpha == nil {
|
||||
return nil
|
||||
}
|
||||
for _, value := range []*int{colorAlpha, objectAlpha} {
|
||||
if value != nil && (*value < 0 || *value > 255) {
|
||||
alpha := *value
|
||||
return &alpha
|
||||
}
|
||||
}
|
||||
alpha := 255
|
||||
if colorAlpha != nil {
|
||||
alpha = *colorAlpha
|
||||
@@ -164,7 +181,6 @@ func mergeAlpha(colorAlpha, objectAlpha *int) *int {
|
||||
if objectAlpha != nil {
|
||||
alpha = alpha * *objectAlpha / 255
|
||||
}
|
||||
alpha = clampColor(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))
|
||||
}
|
||||
|
||||
// renderGradientStops 在CMYK分量空间插值,将RGB分段误差控制在半个8位灰阶内
|
||||
// renderGradientStops 在源CMYK空间插值,保留截断边界及透明度变化
|
||||
// 入参: segments 原始颜色分段, alpha 对象透明度
|
||||
// 返回: []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 {
|
||||
return stops
|
||||
}
|
||||
@@ -271,31 +291,14 @@ func (r *Renderer) renderGradientStops(segments []ShdSegment, alpha *int) []Colo
|
||||
for i, stop := range stops {
|
||||
if i > 0 && stops[i-1].Offset < stop.Offset {
|
||||
a, b := segments[i-1].Color, segments[i].Color
|
||||
leftKind, left := r.colorComponents(a.Value, a.Index, a.ColorSpace)
|
||||
rightKind, right := r.colorComponents(b.Value, b.Index, b.ColorSpace)
|
||||
if leftKind == "CMYK" && rightKind == "CMYK" {
|
||||
a0, a1 := float64(stops[i-1].Color.A)/255, float64(stop.Color.A)/255
|
||||
dk, da := right[3]-left[3], a1-a0
|
||||
bound := 0.0
|
||||
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})
|
||||
leftKind, left, _ := r.colorComponents(a.Value, a.Index, a.ColorSpace)
|
||||
rightKind, right, _ := r.colorComponents(b.Value, b.Index, b.ColorSpace)
|
||||
leftSpace, rightSpace := r.colorDefinition(a.ColorSpace), r.colorDefinition(b.ColorSpace)
|
||||
if leftKind == "CMYK" && rightKind == "CMYK" && (leftSpace == nil || leftSpace.Profile == "") && (rightSpace == nil || rightSpace.Profile == "") {
|
||||
if result == nil {
|
||||
result = append([]ColorStop(nil), stops[:i]...)
|
||||
}
|
||||
result = append(result, cmykGradientStops(left, right, stops[i-1], stop)...)
|
||||
}
|
||||
}
|
||||
if result != nil {
|
||||
@@ -308,6 +311,49 @@ func (r *Renderer) renderGradientStops(segments []ShdSegment, alpha *int) []Colo
|
||||
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 按显式锚点补全渐变分段位置,保留原顺序
|
||||
// 入参: segments 渐变分段
|
||||
// 返回: []float64 分段位置
|
||||
|
||||
+17
-20
@@ -230,7 +230,10 @@ func (r *Renderer) resolveMesh(fill *FillColor) (*MeshShading, error) {
|
||||
vertices := make([]MeshVertex, len(points))
|
||||
for i, point := range points {
|
||||
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))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -244,7 +247,11 @@ func (r *Renderer) resolveMesh(fill *FillColor) (*MeshShading, error) {
|
||||
mesh := &MeshShading{}
|
||||
if extend == 1 && back != nil {
|
||||
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 {
|
||||
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 插值分量
|
||||
// 返回: float64 红、绿、蓝分量
|
||||
func (v MeshVertex) rgb(values [4]float64) (float64, float64, float64) {
|
||||
if v.profile != nil {
|
||||
for i := range values {
|
||||
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]
|
||||
for i := range values {
|
||||
values[i] = math.Max(0, math.Min(1, values[i]))
|
||||
}
|
||||
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 校验控制点并生成共享边或格构三角形编号
|
||||
@@ -344,14 +348,7 @@ func meshTriangleIndices(points []ShdPoint, columns, extend int) ([][3]int, erro
|
||||
// 入参: space 颜色模型, values 分量
|
||||
// 返回: float64 红、绿、蓝分量
|
||||
func meshRGB(space string, values [4]float64) (float64, float64, float64) {
|
||||
switch space {
|
||||
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]
|
||||
}
|
||||
return (MeshVertex{Space: space}).rgb(values)
|
||||
}
|
||||
|
||||
// meshByte 将归一化颜色分量量化为八位整数
|
||||
|
||||
@@ -93,6 +93,9 @@ func (r *Renderer) ResolvePaint(fill *FillColor) (Paint, error) {
|
||||
if node := fill.AxialShd; node != nil {
|
||||
start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint)
|
||||
stops := r.renderGradientStops(node.Segment, fill.Alpha)
|
||||
if r.renderError != nil {
|
||||
return Paint{}, r.renderError
|
||||
}
|
||||
sample, err := r.profileGradient(node.Segment, fill.Alpha)
|
||||
if err != nil {
|
||||
return Paint{}, err
|
||||
@@ -107,6 +110,9 @@ func (r *Renderer) ResolvePaint(fill *FillColor) (Paint, error) {
|
||||
if node := fill.RadialShd; node != nil && node.EndRadius > 0 {
|
||||
start, end := parseFloats(node.StartPoint), parseFloats(node.EndPoint)
|
||||
stops := r.renderGradientStops(node.Segment, fill.Alpha)
|
||||
if r.renderError != nil {
|
||||
return Paint{}, r.renderError
|
||||
}
|
||||
sample, err := r.profileGradient(node.Segment, fill.Alpha)
|
||||
if err != nil {
|
||||
return Paint{}, err
|
||||
|
||||
@@ -41,7 +41,10 @@ func (r *Renderer) profileGradient(segments []ShdSegment, alpha *int) (func(floa
|
||||
}
|
||||
gradient := &profileGradient{positions: gradientPositions(segments), colors: make([]MeshVertex, len(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))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -63,7 +66,7 @@ func (g *profileGradient) At(t float64) color.RGBA {
|
||||
return color.RGBA{}
|
||||
}
|
||||
first := g.colors[0]
|
||||
if t <= g.positions[0] {
|
||||
if t < g.positions[0] {
|
||||
r, green, b := first.rgb(first.Values)
|
||||
return profileColor([3]float64{r, green, b}, first.Alpha)
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ type pdfImporter struct {
|
||||
clipTexts map[*pdfgo.TextClip][]TextObject
|
||||
clipPaths *renderCache[[32]byte, pdfClipPath]
|
||||
cmykSpace string
|
||||
rgbSpace string
|
||||
iccSpaces map[pdfICCKey]string
|
||||
objects *pdfObjectBuffer
|
||||
formResources *pdfFormCache
|
||||
@@ -173,7 +174,17 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
|
||||
}
|
||||
defer reader.Close()
|
||||
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
|
||||
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.resolveFile = options.ResolveFile
|
||||
importer.resolveReference = options.ResolveReference
|
||||
|
||||
@@ -948,7 +948,7 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
|
||||
rgb, err = codecpdf.OutputColor(space, values)
|
||||
}
|
||||
} else if c.transfers != nil {
|
||||
rgb, err = space.RGB(pixel.Values[:space.Components()], "RelativeColorimetric")
|
||||
rgb, err = space.SRGB(pixel.Values[:space.Components()], "RelativeColorimetric")
|
||||
} else {
|
||||
if colorReady && pixel.Values == colorValues {
|
||||
rgb = colorRGB
|
||||
|
||||
+14
-4
@@ -445,12 +445,22 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
|
||||
ctm = pdfNumbers(m[:]...)
|
||||
}
|
||||
paint := func(value pdfgo.Paint) (*FillColor, error) {
|
||||
if value.Tiling != nil || pdfGradientPattern(value) {
|
||||
return p.paintColor(value, box)
|
||||
if value.Tiling == nil && value.Axial == nil && value.Radial == nil && value.Mesh == nil && value.Function == nil && value.Shading == nil {
|
||||
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 {
|
||||
fillColor, err = paint(mark.Style.Fill)
|
||||
if err != nil {
|
||||
|
||||
+17
-9
@@ -32,8 +32,9 @@ var errPDFGroupRaster = errors.New("PDF transparency group requires local compos
|
||||
|
||||
// pdfPaintRegion 保存透明组内的单色填充区域
|
||||
type pdfPaintRegion struct {
|
||||
paint pdfgo.Paint
|
||||
path GeometryPath
|
||||
paint pdfgo.Paint
|
||||
path GeometryPath
|
||||
intent pdfgo.Name
|
||||
}
|
||||
|
||||
// group 将单色图元的透明组转换为互不重叠的填充区域,保留矢量与组不透明度
|
||||
@@ -138,6 +139,7 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
|
||||
var regions []pdfPaintRegion
|
||||
var combined GeometryPath
|
||||
var paint *pdfgo.Paint
|
||||
var intent pdfgo.Name
|
||||
var clips []pdfgo.Path
|
||||
opaque := true
|
||||
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
|
||||
if paint == nil {
|
||||
paint, clips = &color, path.Style.Clips
|
||||
intent = path.Style.RenderingIntent
|
||||
} else if !reflect.DeepEqual(clips, path.Style.Clips) {
|
||||
return fmt.Errorf("%w: independently clipped group content", errPDFGroupRaster)
|
||||
} else if color.Alpha != 1 || !reflect.DeepEqual(*paint, color) {
|
||||
regions = append(regions, pdfPaintRegion{*paint, combined})
|
||||
} else if color.Alpha != 1 || !reflect.DeepEqual(*paint, color) || intent != path.Style.RenderingIntent {
|
||||
regions = append(regions, pdfPaintRegion{*paint, combined, intent})
|
||||
combined = nil
|
||||
paint = &color
|
||||
intent = path.Style.RenderingIntent
|
||||
}
|
||||
outline := shape
|
||||
evenOdd := path.Path.EvenOdd
|
||||
@@ -251,7 +255,7 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
|
||||
if len(combined) == 0 {
|
||||
return nil
|
||||
}
|
||||
regions = append(regions, pdfPaintRegion{*paint, combined})
|
||||
regions = append(regions, pdfPaintRegion{*paint, combined, intent})
|
||||
if !opaque {
|
||||
regions, err = pdfCompositeRegions(geometry, regions)
|
||||
if err != nil {
|
||||
@@ -301,7 +305,11 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
|
||||
if err != nil {
|
||||
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.report.PathObjects++
|
||||
}
|
||||
@@ -523,7 +531,7 @@ func pdfCompositeRegions(geometry GeometryBackend, regions []pdfPaintRegion) ([]
|
||||
return nil, err
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -534,7 +542,7 @@ func pdfCompositeRegions(geometry GeometryBackend, regions []pdfPaintRegion) ([]
|
||||
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
|
||||
}
|
||||
next = append(next, pdfPaintRegion{paint, overlap})
|
||||
next = append(next, pdfPaintRegion{paint, overlap, ""})
|
||||
}
|
||||
remaining, err = geometry.Combine(remaining, backdrop.path, GeometrySubtract)
|
||||
if err != nil {
|
||||
@@ -542,7 +550,7 @@ func pdfCompositeRegions(geometry GeometryBackend, regions []pdfPaintRegion) ([]
|
||||
}
|
||||
}
|
||||
if len(remaining) != 0 {
|
||||
next = append(next, pdfPaintRegion{source.paint, remaining})
|
||||
next = append(next, pdfPaintRegion{source.paint, remaining, source.intent})
|
||||
}
|
||||
result = next
|
||||
}
|
||||
|
||||
+48
-37
@@ -106,12 +106,14 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
||||
}
|
||||
if source.ImageMask {
|
||||
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"}
|
||||
fill, err = space.RGB(values[:], mark.Style.RenderingIntent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fill, err = space.SRGB(values[:], mark.Style.RenderingIntent)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key.tint = color.NRGBA64{
|
||||
R: uint16(math.Round(fill[0] * 65535)),
|
||||
@@ -145,41 +147,50 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
|
||||
}
|
||||
}
|
||||
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 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 {
|
||||
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
|
||||
if len(data) == 0 && decoded != nil && !source.ImageMask && p.compression.Mode == CompressionLossy {
|
||||
|
||||
+7
-4
@@ -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}})
|
||||
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 {
|
||||
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() {
|
||||
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}})
|
||||
}
|
||||
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
|
||||
}
|
||||
color := p.color(paint)
|
||||
color.Value = ""
|
||||
alpha := int(math.Round(paint.Alpha * 255))
|
||||
color := &FillColor{Alpha: &alpha}
|
||||
color.GouraudShd = shading
|
||||
return color, nil
|
||||
}
|
||||
|
||||
+50
-16
@@ -158,7 +158,10 @@ func (p *pdfImporter) gradientSegments(stops []pdfgo.GradientStop, space *pdfgo.
|
||||
if cmyk {
|
||||
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}}
|
||||
}
|
||||
return segments, nil
|
||||
@@ -283,7 +286,7 @@ func (p *pdfImporter) gradientPattern(paint pdfgo.Paint, box Box) (*FillColor, e
|
||||
if _, ok := matrix.Inverse(); !ok {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -337,7 +340,8 @@ func (p *pdfImporter) gradientPattern(paint pdfgo.Paint, box Box) (*FillColor, e
|
||||
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}}
|
||||
return color, nil
|
||||
}
|
||||
@@ -352,7 +356,7 @@ func (p *pdfImporter) gradientBackgroundColor(values [4]float64, space *pdfgo.Co
|
||||
stop := pdfgo.GradientStop{Values: values}
|
||||
if !pdfICCTransfer(space) && (space.Model != "DeviceCMYK" || space.Calibrated()) {
|
||||
var err error
|
||||
stop.RGB, err = space.RGB(values[:space.Components()], intent)
|
||||
stop.RGB, err = space.SRGB(values[:space.Components()], intent)
|
||||
if err != nil {
|
||||
return ShdColor{}, err
|
||||
}
|
||||
@@ -726,26 +730,55 @@ func pdfShadingClippedNode(node pdfCompositeNode, box pdfgo.Rectangle, matrix pd
|
||||
return node
|
||||
}
|
||||
|
||||
// color 保留设备色彩分量并复用文档颜色空间
|
||||
// 入参: paint PDF颜色与不透明度
|
||||
// 返回: *FillColor OFD颜色
|
||||
func (p *pdfImporter) color(paint pdfgo.Paint) *FillColor {
|
||||
// color 保留设备及ICC源分量,其他校准色以16位显示颜色输出
|
||||
// 入参: paint PDF颜色与不透明度, intent 实际渲染意图
|
||||
// 返回: *FillColor OFD颜色, error 配置或颜色转换错误
|
||||
func (p *pdfImporter) color(paint pdfgo.Paint, intent pdfgo.Name) (*FillColor, error) {
|
||||
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 == "" {
|
||||
p.cmykSpace = p.editor.nextID()
|
||||
p.editor.resources = append(p.editor.resources, editorResource{space: &ColorSpace{ID: p.cmykSpace, Type: "CMYK", BitsPerComponent: 16}})
|
||||
}
|
||||
v := paint.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}
|
||||
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}, 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 将页面渐变转换为对象局部坐标
|
||||
// 入参: paint PDF画刷, box 对象边界
|
||||
// 入参: paint PDF画刷, box 对象边界, intent 实际渲染意图
|
||||
// 返回: *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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -755,7 +788,8 @@ func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error)
|
||||
if paint.Mesh != nil {
|
||||
return p.meshColor(paint, box)
|
||||
}
|
||||
color := p.color(paint)
|
||||
alpha := int(math.Round(paint.Alpha * 255))
|
||||
color := &FillColor{Alpha: &alpha}
|
||||
if paint.Tiling != nil {
|
||||
pattern, err := p.tilingPattern(paint.Tiling, paint)
|
||||
if err != nil {
|
||||
@@ -798,7 +832,7 @@ func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error)
|
||||
return color, nil
|
||||
}
|
||||
if paint.Axial == nil {
|
||||
return color, nil
|
||||
return p.color(paint, intent)
|
||||
}
|
||||
gradient := paint.Axial
|
||||
start := p.matrix.Apply(gradient.Start)
|
||||
|
||||
+6
-3
@@ -325,7 +325,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||
}
|
||||
object.Boundary = pdfBoundary(box)
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -339,9 +339,12 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||
object.Fill = &fill
|
||||
object.Stroke = &stroke
|
||||
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 {
|
||||
object.FillColor, err = p.paintColor(mark.Style.Fill, box)
|
||||
object.FillColor, err = p.paintColor(mark.Style.Fill, box, mark.Style.RenderingIntent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user