mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:59:27 +08:00
343 lines
13 KiB
Go
343 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) {
|
|
return space.SRGB(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
|
|
var previous, previousInitial, previousResult, composed CompositePixel
|
|
var previousAlpha float64
|
|
ready := false
|
|
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]
|
|
if ready && pixels[i] == previous && initial[i] == previousInitial && *pixel == previousResult && alpha == previousAlpha {
|
|
pixels[i] = composed
|
|
continue
|
|
}
|
|
previous, previousInitial, previousResult, previousAlpha = pixels[i], initial[i], *pixel, alpha
|
|
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
|
|
composed, ready = pixels[i], true
|
|
}
|
|
}
|
|
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
|
|
}
|