diff --git a/go.mod b/go.mod index 8591713..6f5bca3 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index afff885..4ba2c51 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/base/image/color.go b/internal/base/image/color.go new file mode 100644 index 0000000..b61159a --- /dev/null +++ b/internal/base/image/color.go @@ -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)} +} diff --git a/internal/base/image/encoded.go b/internal/base/image/encoded.go index 4ec77f6..675c5b2 100644 --- a/internal/base/image/encoded.go +++ b/internal/base/image/encoded.go @@ -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 } diff --git a/internal/base/image/image.go b/internal/base/image/image.go index 1941ecd..461ae47 100644 --- a/internal/base/image/image.go +++ b/internal/base/image/image.go @@ -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 diff --git a/internal/base/image/storage.go b/internal/base/image/storage.go index 5223aa1..bd8e747 100644 --- a/internal/base/image/storage.go +++ b/internal/base/image/storage.go @@ -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: diff --git a/internal/base/image/tiff_planar.go b/internal/base/image/tiff_planar.go index 6946c63..64e8c4a 100644 --- a/internal/base/image/tiff_planar.go +++ b/internal/base/image/tiff_planar.go @@ -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 diff --git a/internal/base/image/tiff_profile.go b/internal/base/image/tiff_profile.go index 8078266..21f150e 100644 --- a/internal/base/image/tiff_profile.go +++ b/internal/base/image/tiff_profile.go @@ -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 为离散颜色分配查找表,预乘颜色及小图保持逐像素变换 diff --git a/internal/codec/pdf/color.go b/internal/codec/pdf/color.go index 609771c..3ff3d8d 100644 --- a/internal/codec/pdf/color.go +++ b/internal/codec/pdf/color.go @@ -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 判断混合模式是否为普通覆盖 diff --git a/internal/codec/pdf/image.go b/internal/codec/pdf/image.go index c9419f2..e415554 100644 --- a/internal/codec/pdf/image.go +++ b/internal/codec/pdf/image.go @@ -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 diff --git a/ofdgo_color_profile.go b/ofdgo_color_profile.go index ca2fcdb..ede4681 100644 --- a/ofdgo_color_profile.go +++ b/ofdgo_color_profile.go @@ -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 将显示分量一次量化并按透明度预乘 diff --git a/ofdgo_editor_color.go b/ofdgo_editor_color.go index e1d149d..6af7b87 100644 --- a/ofdgo_editor_color.go +++ b/ofdgo_editor_color.go @@ -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< 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文档按需注册独立颜色空间 diff --git a/ofdgo_paint.go b/ofdgo_paint.go index 64f23ab..60d734c 100644 --- a/ofdgo_paint.go +++ b/ofdgo_paint.go @@ -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<= 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 分段位置 diff --git a/ofdgo_paint_mesh.go b/ofdgo_paint_mesh.go index 0d79434..f2f64f7 100644 --- a/ofdgo_paint_mesh.go +++ b/ofdgo_paint_mesh.go @@ -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 将归一化颜色分量量化为八位整数 diff --git a/ofdgo_paint_model.go b/ofdgo_paint_model.go index 03d9be0..3f5d5a4 100644 --- a/ofdgo_paint_model.go +++ b/ofdgo_paint_model.go @@ -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 diff --git a/ofdgo_paint_profile.go b/ofdgo_paint_profile.go index fc892c6..5ab4fff 100644 --- a/ofdgo_paint_profile.go +++ b/ofdgo_paint_profile.go @@ -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) } diff --git a/ofdgo_pdf.go b/ofdgo_pdf.go index dadcb31..65aad6d 100644 --- a/ofdgo_pdf.go +++ b/ofdgo_pdf.go @@ -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 diff --git a/ofdgo_pdf_composite.go b/ofdgo_pdf_composite.go index 19386c0..16fa26c 100644 --- a/ofdgo_pdf_composite.go +++ b/ofdgo_pdf_composite.go @@ -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 diff --git a/ofdgo_pdf_geometry.go b/ofdgo_pdf_geometry.go index 0595d09..f8073d5 100644 --- a/ofdgo_pdf_geometry.go +++ b/ofdgo_pdf_geometry.go @@ -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 { diff --git a/ofdgo_pdf_group.go b/ofdgo_pdf_group.go index a1ad7bb..527c4ac 100644 --- a/ofdgo_pdf_group.go +++ b/ofdgo_pdf_group.go @@ -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 } diff --git a/ofdgo_pdf_image.go b/ofdgo_pdf_image.go index 21ee50d..470f8d7 100644 --- a/ofdgo_pdf_image.go +++ b/ofdgo_pdf_image.go @@ -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 { diff --git a/ofdgo_pdf_mesh.go b/ofdgo_pdf_mesh.go index 871680d..ded4600 100644 --- a/ofdgo_pdf_mesh.go +++ b/ofdgo_pdf_mesh.go @@ -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 } diff --git a/ofdgo_pdf_shading.go b/ofdgo_pdf_shading.go index 6121a7b..c4d5104 100644 --- a/ofdgo_pdf_shading.go +++ b/ofdgo_pdf_shading.go @@ -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) diff --git a/ofdgo_pdf_text.go b/ofdgo_pdf_text.go index 617d79d..a938054 100644 --- a/ofdgo_pdf_text.go +++ b/ofdgo_pdf_text.go @@ -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 }