Files
ofdgo/internal/codec/pdf/color.go
T
2026-10-14 10:40:08 +08:00

338 lines
13 KiB
Go

// 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 pdf
import (
"context"
"fmt"
"image"
"github.com/xiaoqidun/pdfgo"
)
// compositeCancelInterval 保持像素合成的取消检查间隔
const compositeCancelInterval = 256
// CompositePixel 保存非预乘源空间分量及累计透明度
type CompositePixel struct {
Values [4]float64
Alpha float64
Effect float64
Shape float64
}
// transferKey 区分图形状态、网屏覆盖函数及输出设备
type transferKey struct {
transfer *pdfgo.TransferFunction
halftone *pdfgo.Halftone
model pdfgo.Name
}
// TransferCache 在页面内复用设备传递函数,零值可用
// 缓存与所属阅读器同生命周期,不用于并发解析
type TransferCache struct {
functions map[transferKey]*pdfgo.TransferFunction
}
// Resolve 在页面内复用输出设备的有效函数,不修改原始图形状态
// 入参: reader 原始阅读器, style 原始图形状态, model 原生设备颜色空间
// 返回: *pdfgo.TransferFunction 有效函数, error 网屏传递函数错误
func (c *TransferCache) Resolve(reader *pdfgo.Reader, style pdfgo.Style, model pdfgo.Name) (*pdfgo.TransferFunction, error) {
if style.Halftone == nil && style.Transfer == nil {
return nil, nil
}
if model == "" {
model = "DeviceRGB"
}
if c.functions == nil {
c.functions = make(map[transferKey]*pdfgo.TransferFunction)
}
key := transferKey{style.Transfer, style.Halftone, model}
transfer, found := c.functions[key]
if !found {
var err error
transfer, err = reader.DeviceTransfer(style.Transfer, style.Halftone, model)
if err != nil {
return nil, err
}
c.functions[key] = transfer
}
return transfer, nil
}
// OutputColor 将合成结果映射到OFD输出,与保留的设备四色对象使用相同显示换算
// 入参: 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")
}
// NormalBlend 判断混合模式是否为普通覆盖
// 入参: mode 混合模式
// 返回: bool 是否普通覆盖
func NormalBlend(mode pdfgo.Name) bool {
return mode == "" || mode == "Normal" || mode == "Compatible"
}
// NativePaint 使用库层色料定义求值设备四色,不以备用色代替原生浓度
// 入参: paint 源画刷, space 设备四色空间, intent 渲染意图
// 返回: *pdfgo.Paint 原生画刷或nil, [4]bool 显式通道, error 色料定义错误
func NativePaint(paint pdfgo.Paint, space *pdfgo.ColorSpace, intent pdfgo.Name) (*pdfgo.Paint, [4]bool, error) {
if paint.Colorant == nil {
return nil, [4]bool{}, nil
}
result, err := paint.Colorant.Space.ResolveInGroup(paint.Colorant.Tints, &pdfgo.ColorantDevice{Space: space}, space, intent, false)
if err != nil || len(result.Alternates) != 0 {
return nil, [4]bool{}, err
}
return result.Process, result.ProcessMask, nil
}
// CompositeColor 将原始画刷变换到混合空间,渐变在源空间求值
// 入参: paint 画刷, point 页面坐标, space 混合空间, intent 渲染意图, conversion 设备转换函数
// 返回: [4]float64 混合分量, bool 是否着色, error 颜色错误
func CompositeColor(paint pdfgo.Paint, point pdfgo.Point, space *pdfgo.ColorSpace, intent pdfgo.Name, conversion pdfgo.ColorConversion) ([4]float64, bool, error) {
if paint.Shading != nil {
return [4]float64{}, false, &pdfgo.UnsupportedError{Feature: "local shading pattern compositing"}
}
if paint.Tiling != nil {
return [4]float64{}, false, &pdfgo.UnsupportedError{Feature: "local tiling pattern compositing"}
}
var values [4]float64
var source *pdfgo.ColorSpace
if paint.Function != nil {
g := paint.Function
if g.Bounds != nil {
inverse, ok := g.PatternMatrix.Inverse()
if !ok {
return values, false, fmt.Errorf("singular function shading bounds transform")
}
local := inverse.Apply(point)
if local.X < g.Bounds.XMin || local.X > g.Bounds.XMax || local.Y < g.Bounds.YMin || local.Y > g.Bounds.YMax {
return values, false, nil
}
}
inverse, ok := g.Matrix.Inverse()
if !ok {
return values, false, fmt.Errorf("singular function shading transform")
}
local := inverse.Apply(point)
source, intent = g.Space, g.Intent
if local.X < g.Domain.XMin || local.X > g.Domain.XMax || local.Y < g.Domain.YMin || local.Y > g.Domain.YMax {
if g.Background == nil {
return values, false, nil
}
values = *g.Background
} else {
var err error
values, err = g.ValuesAt(local)
if err != nil {
return values, false, err
}
}
} else if paint.Axial != nil || paint.Radial != nil {
var background *[4]float64
var bounds *pdfgo.Rectangle
var matrix pdfgo.Matrix
if g := paint.Axial; g != nil {
source, intent = g.Space, g.Intent
background, bounds, matrix = g.Background, g.Bounds, g.Matrix
} else {
g := paint.Radial
source, intent = g.Space, g.Intent
background, bounds, matrix = g.Background, g.Bounds, g.Matrix
}
if bounds != nil {
local := point
if matrix != (pdfgo.Matrix{}) {
inverse, ok := matrix.Inverse()
if !ok {
return values, false, fmt.Errorf("singular shading transform")
}
local = inverse.Apply(point)
}
if local.X < bounds.XMin || local.X > bounds.XMax || local.Y < bounds.YMin || local.Y > bounds.YMax {
return values, false, nil
}
}
t, visible := ShadingPosition(paint, point)
var err error
if !visible {
if background == nil {
return values, false, nil
}
values = *background
} else if paint.Axial != nil {
values, err = paint.Axial.ValuesAt(t)
} else {
values, err = paint.Radial.ValuesAt(t)
}
if err != nil {
return values, false, err
}
} else if paint.Space != nil {
values, source = paint.Values, paint.Space
} else if paint.CMYK != nil {
values, source = *paint.CMYK, &pdfgo.ColorSpace{Model: "DeviceCMYK"}
} else if paint.SourceSpace == "DeviceGray" {
values[0], source = paint.RGB[0], &pdfgo.ColorSpace{Model: "DeviceGray"}
} else {
copy(values[:], paint.RGB[:])
source = &pdfgo.ColorSpace{Model: "DeviceRGB"}
}
values, err := space.ConvertWith(values[:source.Components()], source, intent, conversion)
return values, true, err
}
// CompositeInterpolate 按形状覆盖混合预乘颜色及累计透明度
// 入参: target 原像素与输出, source 替换像素, shape 覆盖率
// 返回: error 像素或覆盖率错误
func CompositeInterpolate(target *CompositePixel, source CompositePixel, shape float64) error {
pixel := pdfgo.ColorantPixel{Values: target.Values[:], Alpha: target.Alpha, Shape: target.Shape, Effect: target.Effect}
if err := pixel.Interpolate(pdfgo.ColorantPixel{Values: source.Values[:], Alpha: source.Alpha, Shape: source.Shape, Effect: source.Effect}, shape); err != nil {
return err
}
target.Alpha, target.Effect = pixel.Alpha, pixel.Effect
return nil
}
// CompositeKnockout 移除未覆盖区域的初始背景贡献,保留形状与不透明度差异
// 入参: target 累计组结果, initial 初始背景, source 单个对象合成结果
// 返回: error 像素错误
func CompositeKnockout(target *CompositePixel, initial, source CompositePixel) error {
pixel := pdfgo.ColorantPixel{Values: target.Values[:], Alpha: target.Alpha, Shape: target.Shape, Effect: target.Effect}
if err := pixel.Knockout(
pdfgo.ColorantPixel{Values: initial.Values[:], Alpha: initial.Alpha, Shape: initial.Shape, Effect: initial.Effect},
pdfgo.ColorantPixel{Values: source.Values[:], Alpha: source.Alpha, Shape: source.Shape, Effect: source.Effect},
); err != nil {
return err
}
target.Alpha, target.Effect, target.Shape = pixel.Alpha, pixel.Effect, pixel.Shape
return nil
}
// CompositeGroupRegion 在组覆盖区域合成像素,保留原栅格及形状贡献
// 入参: ctx 取消上下文, pixels 父组输出, initial 初始背景, result 组结果, stride 行跨度, region 覆盖区域, space 组空间, parent 父空间, opacity 组不透明度, mask 蒙版, mode 混合模式, intent 渲染意图, conversion 设备转换函数
// 返回: error 缓冲、区域、颜色或混合错误
func CompositeGroupRegion(ctx context.Context, pixels, initial, result []CompositePixel, stride int, region image.Rectangle, space, parent *pdfgo.ColorSpace, opacity float64, mask []float64, mode, intent pdfgo.Name, conversion pdfgo.ColorConversion) error {
if err := ctx.Err(); err != nil {
return err
}
if len(pixels) != len(initial) || len(pixels) != len(result) || mask != nil && len(mask) != len(pixels) {
return fmt.Errorf("invalid PDF transparency group buffers")
}
if stride <= 0 || region.Min.X < 0 || region.Min.Y < 0 || region.Max.X > stride || !region.Empty() && region.Max.Y > len(pixels)/stride {
return fmt.Errorf("invalid PDF transparency group region")
}
group, destination := pdfgo.ColorantGroup{Space: space}, pdfgo.ColorantGroup{Space: parent}
compositor, err := destination.PrepareGroup(&group, mode, intent, conversion)
if err != nil {
return err
}
if region.Empty() {
return nil
}
components, parentComponents := space.Components(), parent.Components()
var target pdfgo.ColorantPixel
for y := region.Min.Y; y < region.Max.Y; y++ {
if err := ctx.Err(); err != nil {
return err
}
for i, end := y*stride+region.Min.X, y*stride+region.Max.X; i < end; i++ {
if i%compositeCancelInterval == 0 {
if err := ctx.Err(); err != nil {
return err
}
}
alpha := opacity
if mask != nil {
alpha *= mask[i]
}
pixel := &result[i]
target.Values = pixels[i].Values[:parentComponents]
target.Alpha = pixels[i].Alpha
target.Shape = pixels[i].Shape
target.Effect = pixels[i].Effect
if err := compositor.Composite(&target,
pdfgo.ColorantPixel{Values: initial[i].Values[:components], Alpha: initial[i].Alpha, Shape: initial[i].Shape, Effect: initial[i].Effect},
pdfgo.ColorantPixel{Values: pixel.Values[:components], Alpha: pixel.Alpha, Shape: pixel.Shape, Effect: pixel.Effect},
alpha,
); err != nil {
return err
}
pixels[i].Alpha, pixels[i].Effect, pixels[i].Shape = target.Alpha, target.Effect, target.Shape
}
}
return nil
}
// CompositePaintOver 以兼容叠印计算零色料分量,再应用对象混合模式
// 入参: backdrop 背景及输出, source 源分量, alpha 源透明度, space 混合空间, mode 混合模式, compatible 是否保留零色料背景, marked 原生过程通道,nil使用普通空间
// 返回: error 未支持的混合模式
func CompositePaintOver(backdrop *CompositePixel, source [4]float64, alpha float64, space *pdfgo.ColorSpace, mode pdfgo.Name, compatible bool, marked *[4]bool) error {
if !compatible && marked == nil {
return CompositeOver(backdrop, source, alpha, space, mode)
}
if alpha == 0 {
return nil
}
mask := PaintChannels(source, compatible, marked)
values, combined, err := space.CompositeOverprint(backdrop.Values[:space.Components()], source[:space.Components()], backdrop.Alpha, alpha, mode, mask[:space.Components()])
if err != nil {
return err
}
copy(backdrop.Values[:space.Components()], values[:space.Components()])
backdrop.Alpha = combined
backdrop.Effect = alpha + backdrop.Effect*(1-alpha)
return nil
}
// CompositeOver 按PDF透明模型合成源颜色,四色混合使用分量补色
// 入参: backdrop 背景及输出, source 源分量, alpha 源透明度, space 混合空间, mode 混合模式
// 返回: error 未支持的混合模式
func CompositeOver(backdrop *CompositePixel, source [4]float64, alpha float64, space *pdfgo.ColorSpace, mode pdfgo.Name) error {
if alpha == 0 {
return nil
}
values, combined, err := space.Composite(backdrop.Values[:space.Components()], source[:space.Components()], backdrop.Alpha, alpha, mode)
if err != nil {
return err
}
copy(backdrop.Values[:space.Components()], values[:space.Components()])
backdrop.Alpha = combined
backdrop.Effect = alpha + backdrop.Effect*(1-alpha)
return nil
}
// PaintChannels 选择兼容叠印实际使用源颜色的通道
// 入参: values 源四色分量, compatible 是否保留零分量背景, marked 指定通道,nil表示全部
// 返回: [4]bool 各通道是否取源颜色
func PaintChannels(values [4]float64, compatible bool, marked *[4]bool) [4]bool {
channels := [4]bool{true, true, true, true}
if marked != nil {
channels = *marked
}
if compatible {
for i := range channels {
channels[i] = channels[i] && values[i] != 0
}
}
return channels
}