mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
a2de650b52
commit
f952c5d91d
15 files changed
+1104
-1096
No files matched your search
@@ -340,3 +340,18 @@ func PaintChannels(values [4]float64, compatible bool, marked *[4]bool) [4]bool
|
||||
}
|
||||
return channels
|
||||
}
|
||||
|
||||
// OpaqueBlack 判断不受底层分色影响的全黑设备色
|
||||
// 入参: paint PDF颜色与不透明度
|
||||
// 返回: bool 是否为不透明全黑CMYK颜色
|
||||
func OpaqueBlack(paint pdfgo.Paint) bool {
|
||||
return paint.Alpha == 1 && paint.CMYK != nil && paint.CMYK[3] == 1
|
||||
}
|
||||
|
||||
// OverprintNeedsSeparation 判断彩色CMYK套印是否依赖分色输出
|
||||
// 入参: paint PDF颜色与不透明度
|
||||
// 返回: bool 是否需要保留分色语义
|
||||
func OverprintNeedsSeparation(paint pdfgo.Paint) bool {
|
||||
_, process := paint.Process.CMYKMask()
|
||||
return (paint.CMYK != nil || process) && !OpaqueBlack(paint)
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
// 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"
|
||||
"math"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
|
||||
// meshVertex 保存局部毫米坐标及原曲面参数
|
||||
type meshVertex struct {
|
||||
point pdfgo.Point
|
||||
u, v float64
|
||||
}
|
||||
|
||||
// MeshTriangle 保存按输出精度细分的曲面网格
|
||||
type MeshTriangle struct {
|
||||
vertices [3]meshVertex
|
||||
patch int
|
||||
}
|
||||
|
||||
// meshSample 保存采样点参数,patch为一基编号,零表示未覆盖
|
||||
type meshSample struct {
|
||||
u, v float64
|
||||
patch int
|
||||
}
|
||||
|
||||
// MeshRasterOptions 保存网格采样的坐标、精度及并行配置
|
||||
// Matrix将PDF页面坐标转换为毫米,Origin为输出块的毫米原点
|
||||
type MeshRasterOptions struct {
|
||||
Matrix pdfgo.Matrix
|
||||
Origin pdfgo.Point
|
||||
DPI float64
|
||||
Width, Height int
|
||||
Workers int
|
||||
SoftMask bool
|
||||
}
|
||||
|
||||
// MeshEdge 按坐标及颜色分量匹配相邻三角形的共享边,不改变顶点顺序
|
||||
// 入参: previous 前一三角形, current 当前三角形
|
||||
// 返回: int OFD共享边标志,零表示独立三角形
|
||||
func MeshEdge(previous, current pdfgo.MeshTriangle) int {
|
||||
if previous.Points[2] != current.Points[1] || !slices.Equal(previous.Colors[2], current.Colors[1]) {
|
||||
return 0
|
||||
}
|
||||
for _, index := range [2]int{1, 0} {
|
||||
if previous.Points[index] == current.Points[0] && slices.Equal(previous.Colors[index], current.Colors[0]) {
|
||||
return 2 - index
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// MeshTriangles 按曲面二阶导数界细分,将几何误差控制在输出像素的十六分之一内
|
||||
// 入参: ctx 取消上下文, mesh 原始曲面, matrix 页面变换, dpi 输出精度
|
||||
// 返回: []MeshTriangle 细分结果, error 取消或坐标错误
|
||||
func MeshTriangles(ctx context.Context, mesh *pdfgo.MeshGradient, matrix pdfgo.Matrix, dpi float64) ([]MeshTriangle, error) {
|
||||
matrix = matrix.Mul(mesh.Matrix)
|
||||
var triangles []MeshTriangle
|
||||
for index, patch := range mesh.Patches {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range patch.Points {
|
||||
for j := range patch.Points[i] {
|
||||
patch.Points[i][j] = matrix.Apply(patch.Points[i][j])
|
||||
}
|
||||
}
|
||||
var duu, dvv, duv float64
|
||||
for i := 0; i < 4; i++ {
|
||||
for j := 0; j < 4; j++ {
|
||||
a := patch.Points[i][j]
|
||||
if i < 2 {
|
||||
b, c := patch.Points[i+1][j], patch.Points[i+2][j]
|
||||
duu = math.Max(duu, 6*math.Hypot(c.X-2*b.X+a.X, c.Y-2*b.Y+a.Y))
|
||||
}
|
||||
if j < 2 {
|
||||
b, c := patch.Points[i][j+1], patch.Points[i][j+2]
|
||||
dvv = math.Max(dvv, 6*math.Hypot(c.X-2*b.X+a.X, c.Y-2*b.Y+a.Y))
|
||||
}
|
||||
if i < 3 && j < 3 {
|
||||
b, c, d := patch.Points[i+1][j], patch.Points[i][j+1], patch.Points[i+1][j+1]
|
||||
duv = math.Max(duv, 9*math.Hypot(d.X-b.X-c.X+a.X, d.Y-b.Y-c.Y+a.Y))
|
||||
}
|
||||
}
|
||||
}
|
||||
steps := math.Max(1, math.Ceil(math.Sqrt(8*(duu+dvv+2*duv)*dpi/25.4)))
|
||||
if !finite(steps) || steps >= math.Sqrt(float64(int(^uint(0)>>1)/2)) {
|
||||
return nil, fmt.Errorf("mesh subdivision exceeds platform integer range")
|
||||
}
|
||||
n := int(steps)
|
||||
row := make([]meshVertex, n+1)
|
||||
for i := range row {
|
||||
if i%CompositeTileSize == 0 {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
u := float64(i) / steps
|
||||
row[i] = meshVertex{patch.PointAt(u, 0), u, 0}
|
||||
}
|
||||
for j := 1; j <= n; j++ {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := float64(j) / steps
|
||||
left := meshVertex{patch.PointAt(0, v), 0, v}
|
||||
for i := 1; i <= n; i++ {
|
||||
u := float64(i) / steps
|
||||
right := meshVertex{patch.PointAt(u, v), u, v}
|
||||
a, b := row[i-1], row[i]
|
||||
triangles = append(triangles, MeshTriangle{[3]meshVertex{a, b, left}, index}, MeshTriangle{[3]meshVertex{b, right, left}, index})
|
||||
row[i-1], left = left, right
|
||||
}
|
||||
row[n] = left
|
||||
}
|
||||
}
|
||||
for index, triangle := range mesh.Triangles {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vertices := [3]meshVertex{
|
||||
{matrix.Apply(triangle.Points[0]), 0, 0},
|
||||
{matrix.Apply(triangle.Points[1]), 1, 0},
|
||||
{matrix.Apply(triangle.Points[2]), 0, 1},
|
||||
}
|
||||
triangles = append(triangles, MeshTriangle{vertices, len(mesh.Patches) + index})
|
||||
}
|
||||
return triangles, nil
|
||||
}
|
||||
|
||||
// MeshPixels 对输出块执行四倍超采样,曲面重叠按后补片及较大v、u取值
|
||||
// 入参: ctx 取消上下文, mesh 原曲面, triangles 已细分网格, space 混合空间, converter 已准备的颜色转换, options 采样配置
|
||||
// 返回: []ShadingPixel 颜色及覆盖, error 几何、颜色或取消错误
|
||||
func MeshPixels(ctx context.Context, mesh *pdfgo.MeshGradient, triangles []MeshTriangle, space *pdfgo.ColorSpace, converter pdfgo.ColorConverter, options MeshRasterOptions) ([]ShadingPixel, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
const samples = 4
|
||||
w, h := options.Width*samples, options.Height*samples
|
||||
grid := make([]meshSample, w*h)
|
||||
scale := options.DPI / 25.4 * samples
|
||||
for _, triangle := range triangles {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vertices := triangle.vertices
|
||||
for i := range vertices {
|
||||
vertices[i].point.X = (vertices[i].point.X - options.Origin.X) * scale
|
||||
vertices[i].point.Y = (vertices[i].point.Y - options.Origin.Y) * scale
|
||||
}
|
||||
a, b, d := vertices[0].point, vertices[1].point, vertices[2].point
|
||||
denominator := (b.Y-d.Y)*(a.X-d.X) + (d.X-b.X)*(a.Y-d.Y)
|
||||
if denominator == 0 {
|
||||
continue
|
||||
}
|
||||
minX, maxX := math.Min(a.X, math.Min(b.X, d.X)), math.Max(a.X, math.Max(b.X, d.X))
|
||||
minY, maxY := math.Min(a.Y, math.Min(b.Y, d.Y)), math.Max(a.Y, math.Max(b.Y, d.Y))
|
||||
if maxX < 0 || maxY < 0 || minX >= float64(w) || minY >= float64(h) {
|
||||
continue
|
||||
}
|
||||
endX := int(math.Min(float64(w), math.Ceil(maxX)))
|
||||
endY := int(math.Min(float64(h), math.Ceil(maxY)))
|
||||
for y := int(math.Max(0, math.Floor(minY))); y < endY; y++ {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for x := int(math.Max(0, math.Floor(minX))); x < endX; x++ {
|
||||
px, py := float64(x)+.5, float64(y)+.5
|
||||
l0 := ((b.Y-d.Y)*(px-d.X) + (d.X-b.X)*(py-d.Y)) / denominator
|
||||
l1 := ((d.Y-a.Y)*(px-d.X) + (a.X-d.X)*(py-d.Y)) / denominator
|
||||
l2 := 1 - l0 - l1
|
||||
if l0 < -1e-10 || l1 < -1e-10 || l2 < -1e-10 {
|
||||
continue
|
||||
}
|
||||
u := math.Max(0, math.Min(1, l0*vertices[0].u+l1*vertices[1].u+l2*vertices[2].u))
|
||||
v := math.Max(0, math.Min(1, l0*vertices[0].v+l1*vertices[1].v+l2*vertices[2].v))
|
||||
current := &grid[y*w+x]
|
||||
if triangle.patch+1 > current.patch || triangle.patch+1 == current.patch && (v > current.v || v == current.v && u > current.u) {
|
||||
*current = meshSample{u, v, triangle.patch + 1}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pixels := make([]ShadingPixel, options.Width*options.Height)
|
||||
var background [4]float64
|
||||
if mesh.Background != nil {
|
||||
var err error
|
||||
background, err = converter.Convert(mesh.Background[:mesh.Space.Components()])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var inverse pdfgo.Matrix
|
||||
if mesh.Bounds != nil {
|
||||
var ok bool
|
||||
inverse, ok = options.Matrix.Mul(mesh.Matrix).Inverse()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("singular mesh bounds transform")
|
||||
}
|
||||
}
|
||||
sampleRows := func(start, end int) error {
|
||||
colors := make(map[[4]float64][4]float64)
|
||||
colorants := PrepareShadingColorants(pdfgo.Paint{Mesh: mesh}, space, options.SoftMask)
|
||||
var sampler *pdfgo.MeshSampler
|
||||
if colorants == nil {
|
||||
sampler = mesh.NewSampler()
|
||||
}
|
||||
for y := start * samples; y < end*samples; y++ {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
for x := 0; x < w; x++ {
|
||||
if box := mesh.Bounds; box != nil {
|
||||
point := inverse.Apply(pdfgo.Point{X: options.Origin.X + (float64(x)+.5)/scale, Y: options.Origin.Y + (float64(y)+.5)/scale})
|
||||
if point.X < box.XMin || point.X > box.XMax || point.Y < box.YMin || point.Y > box.YMax {
|
||||
continue
|
||||
}
|
||||
}
|
||||
sample := grid[y*w+x]
|
||||
if sample.patch == 0 && mesh.Background == nil {
|
||||
continue
|
||||
}
|
||||
values := background
|
||||
if sample.patch != 0 {
|
||||
var err error
|
||||
if colorants != nil {
|
||||
values, err = colorants.MeshAt(sample.patch-1, sample.u, sample.v)
|
||||
} else {
|
||||
values, err = sampler.ValuesAt(sample.patch-1, sample.u, sample.v)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cached, ok := colors[values]; ok {
|
||||
values = cached
|
||||
} else if colorants == nil {
|
||||
source := values
|
||||
values, err = converter.Convert(values[:mesh.Space.Components()])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(colors) == 2*CompositeTileSize {
|
||||
clear(colors)
|
||||
}
|
||||
colors[source] = values
|
||||
}
|
||||
}
|
||||
pixel := &pixels[(y/samples)*options.Width+x/samples]
|
||||
for i, value := range values {
|
||||
pixel.Values[i] += value
|
||||
}
|
||||
pixel.Shape++
|
||||
}
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
workers := min(max(1, options.Workers), max(1, options.Height/32))
|
||||
if workers == 1 {
|
||||
if err := sampleRows(0, options.Height); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
var group sync.WaitGroup
|
||||
errors := make([]error, workers)
|
||||
for worker := range workers {
|
||||
group.Go(func() {
|
||||
errors[worker] = sampleRows(worker*options.Height/workers, (worker+1)*options.Height/workers)
|
||||
})
|
||||
}
|
||||
group.Wait()
|
||||
for _, err := range errors {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range pixels {
|
||||
pixel := &pixels[i]
|
||||
if pixel.Shape == 0 {
|
||||
continue
|
||||
}
|
||||
for j := range pixel.Values {
|
||||
pixel.Values[j] /= pixel.Shape
|
||||
}
|
||||
pixel.Shape /= samples * samples
|
||||
}
|
||||
return pixels, nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// 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 "image"
|
||||
|
||||
// CompositeTileSize 限定单次合成块的像素边长
|
||||
const CompositeTileSize = 256
|
||||
|
||||
// CompositeBuffers 在一次局部合成中保留至多八个块容量内的空闲缓冲
|
||||
type CompositeBuffers[T any] [8][]T
|
||||
|
||||
// RasterBuffers 保存按像素尺寸复用的独占覆盖画布
|
||||
type RasterBuffers [8]*image.RGBA
|
||||
|
||||
// CoverageRow 读取完整范围内的原生覆盖行,其他类型沿用逐点采样
|
||||
// 入参: coverage 覆盖图像, y 行坐标, left 起始列, right 结束列
|
||||
// 返回: []byte RGBA像素或空值
|
||||
func CoverageRow(coverage image.Image, y, left, right int) []byte {
|
||||
buffer, ok := coverage.(*image.RGBA)
|
||||
if !ok || y < buffer.Rect.Min.Y || y >= buffer.Rect.Max.Y || left < buffer.Rect.Min.X || right > buffer.Rect.Max.X || left >= right {
|
||||
return nil
|
||||
}
|
||||
start := buffer.PixOffset(left, y)
|
||||
return buffer.Pix[start : start+4*(right-left)]
|
||||
}
|
||||
|
||||
// Acquire 借用尚未初始化的独立缓冲,嵌套调用不复用活动缓冲
|
||||
// 入参: count 所需元素数量
|
||||
// 返回: []T 可由当前调用独占的缓冲
|
||||
func (s *CompositeBuffers[T]) Acquire(count int) []T {
|
||||
selected := -1
|
||||
for i, buffer := range s {
|
||||
if cap(buffer) >= count && (selected < 0 || cap(buffer) < cap(s[selected])) {
|
||||
selected = i
|
||||
}
|
||||
}
|
||||
if selected < 0 {
|
||||
return make([]T, count)
|
||||
}
|
||||
buffer := s[selected]
|
||||
s[selected] = nil
|
||||
return buffer[:count]
|
||||
}
|
||||
|
||||
// Release 归还已不再访问的缓冲,超出块容量或空闲槽数量时直接释放
|
||||
// 入参: buffer 当前调用借用的缓冲
|
||||
func (s *CompositeBuffers[T]) Release(buffer []T) {
|
||||
if len(buffer) == 0 || cap(buffer) > CompositeTileSize*CompositeTileSize {
|
||||
return
|
||||
}
|
||||
for i, stored := range s {
|
||||
if stored == nil {
|
||||
s[i] = buffer
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire 借用尺寸相同的独占画布,不清空已有像素
|
||||
// 入参: rect 输出像素范围
|
||||
// 返回: *image.RGBA 当前调用独占的画布
|
||||
func (s *RasterBuffers) Acquire(rect image.Rectangle) *image.RGBA {
|
||||
for i, stored := range s {
|
||||
if stored != nil && stored.Rect == rect {
|
||||
s[i] = nil
|
||||
return stored
|
||||
}
|
||||
}
|
||||
return image.NewRGBA(rect)
|
||||
}
|
||||
|
||||
// Release 归还块容量内的画布,超出容量或空闲槽数量时直接释放
|
||||
// 入参: buffer 当前调用借用的画布
|
||||
func (s *RasterBuffers) Release(buffer *image.RGBA) {
|
||||
if buffer == nil || cap(buffer.Pix) > 4*CompositeTileSize*CompositeTileSize {
|
||||
return
|
||||
}
|
||||
for i, stored := range s {
|
||||
if stored == nil {
|
||||
s[i] = buffer
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,12 @@ import (
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
|
||||
// ShadingPixel 保存抗锯齿覆盖率与混合空间分量
|
||||
type ShadingPixel struct {
|
||||
Values [4]float64
|
||||
Shape float64
|
||||
}
|
||||
|
||||
// ShadingColorants 复用原始浓度缓冲及原生通道映射,不跨并发采样共享
|
||||
type ShadingColorants struct {
|
||||
paint pdfgo.Paint
|
||||
|
||||
+95
-4
@@ -26,6 +26,12 @@ import (
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
|
||||
// 图案标记保留设备四色通道及非零对象形状
|
||||
const (
|
||||
pdfTransferChannels uint8 = 15
|
||||
pdfTransferShape uint8 = 16
|
||||
)
|
||||
|
||||
// pdfCompositeNode 保留局部合成所需的原始图元和透明组层级
|
||||
type pdfCompositeNode struct {
|
||||
path *pdfgo.PathMark
|
||||
@@ -125,7 +131,7 @@ type pdfCompositeCache struct {
|
||||
imageProcesses map[pdfImageProcessKey]*pdfgo.ImageProcess
|
||||
imageColor pdfCompositeImageColor
|
||||
masks map[*pdfgo.SoftMask][]pdfCompositeNode
|
||||
meshes map[*pdfgo.MeshGradient][]pdfMeshTriangle
|
||||
meshes map[*pdfgo.MeshGradient][]codecpdf.MeshTriangle
|
||||
linearMeshes *renderCache[pdfLinearMeshKey, []pdfgo.MeshTriangle]
|
||||
patterns map[pdfgo.Paint]*pdfCompositePattern
|
||||
shadingPatterns map[*pdfgo.ShadingPattern][]pdfCompositeNode
|
||||
@@ -136,11 +142,22 @@ type pdfCompositeCache struct {
|
||||
groups map[*pdfgo.GroupMark]Box
|
||||
}
|
||||
|
||||
// pdfCompositeTileSize 限定单次合成块的像素边长
|
||||
const pdfCompositeTileSize = codecpdf.CompositeTileSize
|
||||
|
||||
// pdfCompositeScratch 复用像素及蒙版缓冲,不跨页持有活动内容
|
||||
type pdfCompositeScratch struct {
|
||||
pixels codecpdf.CompositeBuffers[pdfCompositePixel]
|
||||
masks codecpdf.CompositeBuffers[float64]
|
||||
channels codecpdf.CompositeBuffers[uint8]
|
||||
rasters codecpdf.RasterBuffers
|
||||
}
|
||||
|
||||
// compositingCache 按需创建单页合成缓存,页面切换时由导入器释放
|
||||
// 返回: *pdfCompositeCache 当前页面缓存
|
||||
func (p *pdfImporter) compositingCache() *pdfCompositeCache {
|
||||
if p.compositeCache == nil {
|
||||
p.compositeCache = &pdfCompositeCache{images: map[*pdfgo.Image]*pdfgo.ImageComponents{}, masks: map[*pdfgo.SoftMask][]pdfCompositeNode{}, meshes: map[*pdfgo.MeshGradient][]pdfMeshTriangle{}}
|
||||
p.compositeCache = &pdfCompositeCache{images: map[*pdfgo.Image]*pdfgo.ImageComponents{}, masks: map[*pdfgo.SoftMask][]pdfCompositeNode{}, meshes: map[*pdfgo.MeshGradient][]codecpdf.MeshTriangle{}}
|
||||
}
|
||||
return p.compositeCache
|
||||
}
|
||||
@@ -665,7 +682,7 @@ func (p *pdfImporter) opaque(n pdfCompositeNode, space *pdfgo.ColorSpace) (bool,
|
||||
if style.SoftMask != nil || !codecpdf.NormalBlend(style.BlendMode) || fill && style.Fill.Alpha != 1 || stroke && style.Stroke.Alpha != 1 {
|
||||
return false, nil
|
||||
}
|
||||
if fill && style.FillOverprint && pdfOverprintNeedsSeparation(style.Fill) || stroke && style.StrokeOverprint && pdfOverprintNeedsSeparation(style.Stroke) {
|
||||
if fill && style.FillOverprint && codecpdf.OverprintNeedsSeparation(style.Fill) || stroke && style.StrokeOverprint && codecpdf.OverprintNeedsSeparation(style.Stroke) {
|
||||
return false, nil
|
||||
}
|
||||
if fill && p.gradientError(style.Fill) != nil || stroke && p.gradientError(style.Stroke) != nil {
|
||||
@@ -709,7 +726,7 @@ func (n pdfCompositeNode) direct() bool {
|
||||
if n.image != nil && s.FillOverprint {
|
||||
return false
|
||||
}
|
||||
return codecpdf.NormalBlend(s.BlendMode) && !(fill && s.FillOverprint && pdfOverprintNeedsSeparation(s.Fill) || stroke && s.StrokeOverprint && pdfOverprintNeedsSeparation(s.Stroke))
|
||||
return codecpdf.NormalBlend(s.BlendMode) && !(fill && s.FillOverprint && codecpdf.OverprintNeedsSeparation(s.Fill) || stroke && s.StrokeOverprint && codecpdf.OverprintNeedsSeparation(s.Stroke))
|
||||
}
|
||||
|
||||
// style 获取图元实际使用的图形状态
|
||||
@@ -984,3 +1001,77 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
|
||||
}
|
||||
return p.appendRasterGroup(output, box, 1, origins.origin)
|
||||
}
|
||||
|
||||
// acquireCoverage 借用当前块的独占画布,保留活动画布供嵌套合成读取
|
||||
// 返回: *image.RGBA 未清空画布,不支持缓冲绘制的后端返回nil, error 无效绘制区域
|
||||
func (c *pdfCompositor) acquireCoverage() (*image.RGBA, error) {
|
||||
if _, ok := c.importer.editor.Backends().Raster.(RasterBufferBackend); !ok {
|
||||
return nil, nil
|
||||
}
|
||||
page := RasterPage{Width: c.box.W, Height: c.box.H, DPI: c.importer.rasterDPI}
|
||||
w, h, err := page.PixelSize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.scratch == nil {
|
||||
c.scratch = &pdfCompositeScratch{}
|
||||
}
|
||||
return c.scratch.rasters.Acquire(image.Rect(0, 0, w, h)), nil
|
||||
}
|
||||
|
||||
// releaseCoverage 归还当前调用不再读取的画布,限制单次合成保留的内存
|
||||
// 入参: coverage 当前调用的覆盖图像
|
||||
func (c *pdfCompositor) releaseCoverage(coverage image.Image) {
|
||||
if _, ok := c.importer.editor.Backends().Raster.(RasterBufferBackend); !ok || c.scratch == nil {
|
||||
return
|
||||
}
|
||||
if buffer, ok := coverage.(*image.RGBA); ok {
|
||||
c.scratch.rasters.Release(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
// acquirePixels 为当前合成器借用独立像素缓冲
|
||||
// 入参: count 所需像素数量
|
||||
// 返回: []pdfCompositePixel 尚未初始化的独占缓冲
|
||||
func (c *pdfCompositor) acquirePixels(count int) []pdfCompositePixel {
|
||||
if c.scratch == nil {
|
||||
c.scratch = &pdfCompositeScratch{}
|
||||
}
|
||||
return c.scratch.pixels.Acquire(count)
|
||||
}
|
||||
|
||||
// releasePixels 归还当前调用已不再访问的像素缓冲
|
||||
// 入参: pixels 当前调用借用的缓冲
|
||||
func (c *pdfCompositor) releasePixels(pixels []pdfCompositePixel) {
|
||||
if c.scratch != nil {
|
||||
c.scratch.pixels.Release(pixels)
|
||||
}
|
||||
}
|
||||
|
||||
// acquireChannels 借用图案通道标记,仅在设备函数需要时分配
|
||||
// 入参: count 当前块像素数
|
||||
// 返回: []uint8 尚未初始化的独占通道缓冲
|
||||
func (c *pdfCompositor) acquireChannels(count int) []uint8 {
|
||||
if c.scratch == nil {
|
||||
c.scratch = &pdfCompositeScratch{}
|
||||
}
|
||||
return c.scratch.channels.Acquire(count)
|
||||
}
|
||||
|
||||
// releaseChannels 归还不再参与嵌套合成的通道标记
|
||||
// 入参: channels 当前调用借用的通道缓冲
|
||||
func (c *pdfCompositor) releaseChannels(channels []uint8) {
|
||||
if c.scratch != nil {
|
||||
c.scratch.channels.Release(channels)
|
||||
}
|
||||
}
|
||||
|
||||
// releaseMasks 在当前块绘制结束后归还蒙版结果,不复用其他块的坐标缓存
|
||||
func (c *pdfCompositor) releaseMasks() {
|
||||
if c.scratch != nil {
|
||||
for _, mask := range c.masks {
|
||||
c.scratch.masks.Release(mask)
|
||||
}
|
||||
}
|
||||
clear(c.masks)
|
||||
}
|
||||
@@ -262,7 +262,7 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
||||
if err := c.importer.ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
row := pdfCoverageRow(coverage, y, region.Min.X, region.Max.X)
|
||||
row := codecpdf.CoverageRow(coverage, y, region.Min.X, region.Max.X)
|
||||
for x := region.Min.X; x < region.Max.X; x++ {
|
||||
var a uint32
|
||||
if row != nil {
|
||||
@@ -306,11 +306,11 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
|
||||
}
|
||||
}
|
||||
} else if shading != nil {
|
||||
values = shading[index].values
|
||||
values = shading[index].Values
|
||||
if paint.Mesh == nil {
|
||||
shape = shading[index].shape
|
||||
shape = shading[index].Shape
|
||||
} else {
|
||||
shape *= shading[index].shape
|
||||
shape *= shading[index].Shape
|
||||
}
|
||||
if node.image != nil && paint.Mesh != nil {
|
||||
point := c.inverse.Apply(pdfgo.Point{X: c.box.X + (float64(x)+.5)*step, Y: c.box.Y + (float64(y)+.5)*step})
|
||||
@@ -708,11 +708,11 @@ func (c *pdfCompositor) mask(mask *pdfgo.SoftMask, inherited *pdfgo.ColorSpace)
|
||||
if err := local.draw(nodes, pixels, space); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := c.scratch.masks.acquire(len(pixels))
|
||||
result := c.scratch.masks.Acquire(len(pixels))
|
||||
complete := false
|
||||
defer func() {
|
||||
if !complete {
|
||||
c.scratch.masks.release(result)
|
||||
c.scratch.masks.Release(result)
|
||||
}
|
||||
}()
|
||||
values := make(map[[4]float64]float64)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
@@ -31,6 +32,12 @@ type pdfCompositePattern struct {
|
||||
transferOpaque bool
|
||||
}
|
||||
|
||||
// pdfShadingSourceKey 区分图案的使用几何及填充或描边
|
||||
type pdfShadingSourceKey struct {
|
||||
pattern *pdfgo.ShadingPattern
|
||||
geometry pdfCompositeKey
|
||||
}
|
||||
|
||||
// tiling 将图案作为非隔离组绘制,保留背景贡献及单元间隙
|
||||
// 入参: paint 图案画刷, backdrop 初始背景, space 混合空间, channels 可选源通道输出
|
||||
// 返回: []pdfCompositePixel 去除背景贡献的图案像素, error 解析或绘制错误
|
||||
@@ -355,7 +362,7 @@ func (p *pdfImporter) processOverprint(nodes []pdfCompositeNode) (bool, error) {
|
||||
if node.image != nil && !node.image.Image.ImageMask {
|
||||
continue
|
||||
}
|
||||
if fill && style.FillOverprint && pdfOverprintNeedsSeparation(style.Fill) || stroke && style.StrokeOverprint && pdfOverprintNeedsSeparation(style.Stroke) {
|
||||
if fill && style.FillOverprint && codecpdf.OverprintNeedsSeparation(style.Fill) || stroke && style.StrokeOverprint && codecpdf.OverprintNeedsSeparation(style.Stroke) {
|
||||
return true, nil
|
||||
}
|
||||
if style.Transfer != nil || style.Halftone != nil {
|
||||
@@ -375,7 +382,7 @@ func (p *pdfImporter) processOverprint(nodes []pdfCompositeNode) (bool, error) {
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if native != nil && (!pdfOpaqueBlack(*native) || !pdfOpaqueBlack(paint)) {
|
||||
if native != nil && (!codecpdf.OpaqueBlack(*native) || !codecpdf.OpaqueBlack(paint)) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
@@ -402,3 +409,367 @@ func (p *pdfImporter) processOverprint(nodes []pdfCompositeNode) (bool, error) {
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// resolveTransfer 无设备函数时保留无缓存快速路径,按需绑定单页解析缓存
|
||||
// 入参: style 原始图形状态, model 原生设备颜色空间
|
||||
// 返回: *pdfgo.TransferFunction 有效函数, error 网屏传递函数错误
|
||||
func (p *pdfImporter) resolveTransfer(style pdfgo.Style, model pdfgo.Name) (*pdfgo.TransferFunction, error) {
|
||||
if style.Halftone == nil && style.Transfer == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return p.compositingCache().transfers.Resolve(p.reader, style, model)
|
||||
}
|
||||
|
||||
// compositeHasTransfer 检查最终设备使用的通道,不将备用通道误判为有效函数
|
||||
// 入参: nodes 原始图元及透明组, model 原生设备颜色空间
|
||||
// 返回: bool 是否存在非恒等函数, error 网屏函数错误
|
||||
func (p *pdfImporter) compositeHasTransfer(nodes []pdfCompositeNode, model pdfgo.Name) (bool, error) {
|
||||
for _, node := range nodes {
|
||||
if node.group != nil {
|
||||
if found, err := p.compositeHasTransfer(node.children, model); err != nil || found {
|
||||
return found, err
|
||||
}
|
||||
} else {
|
||||
style, _, _ := node.style()
|
||||
transfer, err := p.resolveTransfer(style, model)
|
||||
if err != nil || transfer != nil {
|
||||
return transfer != nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// transferDevice 为四色叠印保留原生设备通道,其余页面使用连续RGB设备
|
||||
// 入参: space 页面混合空间, nodes 参与合成的图元
|
||||
// 返回: pdfgo.Name 原生设备空间, error 图案或图像解析错误
|
||||
func (p *pdfImporter) transferDevice(space *pdfgo.ColorSpace, nodes []pdfCompositeNode) (pdfgo.Name, error) {
|
||||
if space.Model == "DeviceCMYK" && !space.Calibrated() {
|
||||
if found, err := p.processOverprint(nodes); err != nil {
|
||||
return "", err
|
||||
} else if found {
|
||||
return "DeviceCMYK", nil
|
||||
}
|
||||
}
|
||||
return "DeviceRGB", nil
|
||||
}
|
||||
|
||||
// selectTransfer 按最上层基本图元及祖先的不透明状态选择最终设备函数
|
||||
// 入参: node 基本图元, outline 是否描边
|
||||
// 返回: *pdfgo.TransferFunction 当前函数或设备默认值, error 图案解析错误
|
||||
func (c *pdfCompositor) selectTransfer(node pdfCompositeNode, outline bool) (*pdfgo.TransferFunction, error) {
|
||||
style, _, _ := node.style()
|
||||
paint := style.Fill
|
||||
if outline {
|
||||
paint = style.Stroke
|
||||
}
|
||||
if c.transferBlocked || paint.Alpha != 1 || style.SoftMask != nil || !codecpdf.NormalBlend(style.BlendMode) {
|
||||
return nil, nil
|
||||
}
|
||||
transfer, err := c.importer.resolveTransfer(style, c.transferModel)
|
||||
if err != nil || transfer == nil {
|
||||
return transfer, err
|
||||
}
|
||||
if pattern := paint.Shading; pattern != nil && (node.image == nil || node.image.Image.ImageMask) {
|
||||
inner := pattern.Style
|
||||
if inner.Fill.Alpha != 1 || inner.SoftMask != nil || !codecpdf.NormalBlend(inner.BlendMode) {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
if node.image != nil {
|
||||
soft, err := node.image.Image.HasSoftMask()
|
||||
if err != nil || soft {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if paint.Tiling != nil && (node.image == nil || node.image.Image.ImageMask) {
|
||||
pattern, err := c.importer.compositePattern(paint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opaque, err := pattern.opaqueTransfer()
|
||||
if err != nil || !opaque {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return transfer, nil
|
||||
}
|
||||
|
||||
// opaqueTransfer 复用图案不透明性检查,图案内部函数不提前作用于源颜色
|
||||
// 返回: bool 是否允许使用外层设备函数, error 单元解析错误
|
||||
func (p *pdfCompositePattern) opaqueTransfer() (bool, error) {
|
||||
if !p.transferChecked {
|
||||
for _, child := range p.nodes {
|
||||
opaque, err := p.local.transferOpaque(child)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !opaque {
|
||||
p.transferChecked = true
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
p.transferChecked, p.transferOpaque = true, true
|
||||
}
|
||||
return p.transferOpaque, nil
|
||||
}
|
||||
|
||||
// transferOpaque 检查全部图案内容的不透明条件,不限制混合颜色空间
|
||||
// 入参: node 单元图元或透明组
|
||||
// 返回: bool 是否不透明, error 图案或图片遮罩引用错误
|
||||
func (p *pdfImporter) transferOpaque(node pdfCompositeNode) (bool, error) {
|
||||
if group := node.group; group != nil {
|
||||
if group.Alpha != 1 || group.SoftMask != nil || !codecpdf.NormalBlend(group.BlendMode) {
|
||||
return false, nil
|
||||
}
|
||||
for _, child := range node.children {
|
||||
if opaque, err := p.transferOpaque(child); err != nil || !opaque {
|
||||
return opaque, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
style, fill, stroke := node.style()
|
||||
if style.SoftMask != nil || !codecpdf.NormalBlend(style.BlendMode) || fill && style.Fill.Alpha != 1 || stroke && style.Stroke.Alpha != 1 {
|
||||
return false, nil
|
||||
}
|
||||
if node.image != nil {
|
||||
soft, err := node.image.Image.HasSoftMask()
|
||||
if err != nil || soft {
|
||||
return false, err
|
||||
}
|
||||
if !node.image.Image.ImageMask {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
for i, paint := range [2]pdfgo.Paint{style.Fill, style.Stroke} {
|
||||
if pattern := paint.Shading; pattern != nil && (i == 0 && fill || i == 1 && stroke) {
|
||||
inner := pattern.Style
|
||||
if inner.Fill.Alpha != 1 || inner.SoftMask != nil || !codecpdf.NormalBlend(inner.BlendMode) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if i == 0 && !fill || i == 1 && !stroke || paint.Tiling == nil {
|
||||
continue
|
||||
}
|
||||
pattern, err := p.compositePattern(paint)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if opaque, err := pattern.opaqueTransfer(); err != nil || !opaque {
|
||||
return opaque, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// halftone 检查连续色调转换,避免将打印网屏误用于屏幕显示
|
||||
// 入参: style 图形状态
|
||||
// 返回: error 严格模式无法保留设备网屏
|
||||
func (p *pdfImporter) halftone(style pdfgo.Style) error {
|
||||
h := style.Halftone
|
||||
message := "PDF continuous-tone appearance retained; device halftone not transferred to OFD"
|
||||
if h == nil || h.Type == 0 {
|
||||
if style.HalftoneOrigin == nil {
|
||||
return nil
|
||||
}
|
||||
message = "PDF continuous-tone appearance retained; device halftone origin not transferred to OFD"
|
||||
}
|
||||
if p.warning == nil {
|
||||
return &pdfgo.UnsupportedError{Feature: "device halftone conversion"}
|
||||
}
|
||||
if p.halftoneWarnings == nil {
|
||||
p.halftoneWarnings = make(map[*pdfgo.Halftone]bool)
|
||||
}
|
||||
if !p.halftoneWarnings[h] {
|
||||
p.halftoneWarnings[h] = true
|
||||
p.warning(pdfgo.Diagnostic{Message: message})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// compositeShadingPattern 在页面内复用图案内部图元,不缓存依赖背景的像素
|
||||
// 入参: pattern 只读着色图案
|
||||
// 返回: []pdfCompositeNode 隐式挖空组, error 遍历或资源错误
|
||||
func (p *pdfImporter) compositeShadingPattern(pattern *pdfgo.ShadingPattern) ([]pdfCompositeNode, error) {
|
||||
cache := p.compositingCache()
|
||||
if nodes, found := cache.shadingPatterns[pattern]; found {
|
||||
return nodes, nil
|
||||
}
|
||||
inverse, ok := p.matrix.Inverse()
|
||||
if !ok {
|
||||
return nil, &pdfgo.UnsupportedError{Feature: "shading pattern page transform"}
|
||||
}
|
||||
box := pdfBounds([]pdfgo.Point{
|
||||
inverse.Apply(pdfgo.Point{}), inverse.Apply(pdfgo.Point{X: p.pageWidth}),
|
||||
inverse.Apply(pdfgo.Point{X: p.pageWidth, Y: p.pageHeight}), inverse.Apply(pdfgo.Point{Y: p.pageHeight}),
|
||||
})
|
||||
nodes, err := p.collectCompositeNodes(func(visitor pdfgo.Visitor) error {
|
||||
return pattern.Walk(p.ctx, pdfgo.Rectangle{XMin: box.X, YMin: box.Y, XMax: box.X + box.W, YMax: box.Y + box.H}, visitor)
|
||||
}, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cache.shadingPatterns == nil {
|
||||
cache.shadingPatterns = make(map[*pdfgo.ShadingPattern][]pdfCompositeNode)
|
||||
}
|
||||
cache.shadingPatterns[pattern] = nodes
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
// shadingPattern 统一对象和图案的几何覆盖,按相交面积合成背景及内部着色
|
||||
// 入参: pattern 着色图案, node 使用图元, outline 是否描边, backdrop 初始背景, space 父级混合空间, channels 可选源通道输出
|
||||
// 返回: []pdfCompositePixel 源颜色、形状及不透明度, error 状态或合成错误
|
||||
func (c *pdfCompositor) shadingPattern(pattern *pdfgo.ShadingPattern, node pdfCompositeNode, outline bool, backdrop []pdfCompositePixel, space *pdfgo.ColorSpace, channels []uint8) ([]pdfCompositePixel, error) {
|
||||
nodes, err := c.importer.shadingSources(pattern, node, outline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
local := *c
|
||||
local.transfers, local.shadingSource = nil, true
|
||||
local.channels = channels
|
||||
result, err := local.shadingSourcePixels(nodes[len(nodes)-1], backdrop, space)
|
||||
if err != nil || len(nodes) == 1 {
|
||||
return result, err
|
||||
}
|
||||
var backgroundChannels []uint8
|
||||
if channels != nil {
|
||||
backgroundChannels = c.acquireChannels(len(channels))
|
||||
defer c.releaseChannels(backgroundChannels)
|
||||
clear(backgroundChannels)
|
||||
}
|
||||
local.channels = backgroundChannels
|
||||
background, err := local.shadingSourcePixels(nodes[0], backdrop, space)
|
||||
if err != nil {
|
||||
c.releasePixels(result)
|
||||
return nil, err
|
||||
}
|
||||
defer c.releasePixels(background)
|
||||
coverage, err := c.coverage(nodes[0], outline)
|
||||
if err != nil {
|
||||
c.releasePixels(result)
|
||||
return nil, err
|
||||
}
|
||||
defer c.releaseCoverage(coverage)
|
||||
step := 25.4 / c.importer.rasterDPI
|
||||
for i := range result {
|
||||
if i%pdfCompositeTileSize == 0 {
|
||||
if err := c.importer.ctx.Err(); err != nil {
|
||||
c.releasePixels(result)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
shape := 0.0
|
||||
if coverage != nil {
|
||||
a := baseimage.AlphaAt(coverage, i%c.width, i/c.width)
|
||||
shape = float64(a) / 65535
|
||||
}
|
||||
if nodes[0].image != nil && shape != 0 {
|
||||
point := c.inverse.Apply(pdfgo.Point{X: c.box.X + (float64(i%c.width)+.5)*step, Y: c.box.Y + (float64(i/c.width)+.5)*step})
|
||||
_, alpha, err := c.imageColor(nodes[0].image, point, space)
|
||||
if err != nil {
|
||||
c.releasePixels(result)
|
||||
return nil, err
|
||||
}
|
||||
shape *= alpha
|
||||
}
|
||||
remaining := 1.0
|
||||
if shape != 0 {
|
||||
remaining -= math.Min(1, result[i].Shape/shape)
|
||||
}
|
||||
alpha := result[i].Alpha + background[i].Alpha*remaining
|
||||
if alpha != 0 {
|
||||
for j := 0; j < space.Components(); j++ {
|
||||
result[i].Values[j] = (result[i].Values[j]*result[i].Alpha + background[i].Values[j]*background[i].Alpha*remaining) / alpha
|
||||
}
|
||||
}
|
||||
result[i].Alpha, result[i].Effect = alpha, alpha
|
||||
result[i].Shape += background[i].Shape * remaining
|
||||
if channels != nil && background[i].Shape*remaining > 0 {
|
||||
channels[i] |= backgroundChannels[i]
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// shadingSources 将内部状态绑定到使用图元,几何和裁剪仅计算一次
|
||||
// 入参: pattern 只读着色图案, node 使用图元, outline 是否描边
|
||||
// 返回: []pdfCompositeNode 背景及着色图元, error 图案遍历错误
|
||||
func (p *pdfImporter) shadingSources(pattern *pdfgo.ShadingPattern, node pdfCompositeNode, outline bool) ([]pdfCompositeNode, error) {
|
||||
cache := p.compositingCache()
|
||||
key := pdfShadingSourceKey{pattern, pdfCompositeKey{path: node.path, text: node.text, image: node.image, stroke: outline}}
|
||||
if sources, ok := cache.shadingSources[key]; ok {
|
||||
return sources, nil
|
||||
}
|
||||
nodes, err := p.compositeShadingPattern(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outer, _, _ := node.style()
|
||||
sources := make([]pdfCompositeNode, 0, len(nodes[0].children))
|
||||
for _, child := range nodes[0].children {
|
||||
style := child.path.Style
|
||||
style.LineWidth, style.Cap, style.Join, style.MiterLimit = outer.LineWidth, outer.Cap, outer.Join, outer.MiterLimit
|
||||
style.Dash, style.DashPhase, style.StrokeAdjust = outer.Dash, outer.DashPhase, outer.StrokeAdjust
|
||||
style.Clips = append(append([]pdfgo.Path(nil), outer.Clips...), style.Clips...)
|
||||
style.Stroke, style.StrokeOverprint = style.Fill, style.FillOverprint
|
||||
source := node
|
||||
if node.path != nil {
|
||||
mark := *node.path
|
||||
mark.Style, mark.Fill, mark.Stroke = style, !outline, outline
|
||||
source.path = &mark
|
||||
} else if node.text != nil {
|
||||
mark := *node.text
|
||||
mark.Style, mark.Mode = style, 0
|
||||
if outline {
|
||||
mark.Mode = 1
|
||||
}
|
||||
source.text = &mark
|
||||
} else {
|
||||
mark := *node.image
|
||||
mark.Style = style
|
||||
source.image = &mark
|
||||
}
|
||||
sources = append(sources, source)
|
||||
}
|
||||
if cache.shadingSources == nil {
|
||||
cache.shadingSources = make(map[pdfShadingSourceKey][]pdfCompositeNode)
|
||||
}
|
||||
cache.shadingSources[key] = sources
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
// shadingSourcePixels 求值单个内部图元并移除初始背景贡献
|
||||
// 入参: node 内部图元, backdrop 初始背景, space 混合空间
|
||||
// 返回: []pdfCompositePixel 源颜色和覆盖, error 合成或取消错误
|
||||
func (c *pdfCompositor) shadingSourcePixels(node pdfCompositeNode, backdrop []pdfCompositePixel, space *pdfgo.ColorSpace) ([]pdfCompositePixel, error) {
|
||||
result := c.acquirePixels(len(backdrop))
|
||||
complete := false
|
||||
defer func() {
|
||||
if !complete {
|
||||
c.releasePixels(result)
|
||||
}
|
||||
}()
|
||||
for i, pixel := range backdrop {
|
||||
result[i] = pdfCompositePixel{Values: pixel.Values, Alpha: pixel.Alpha}
|
||||
}
|
||||
if err := c.drawMark(node, result, space); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range result {
|
||||
if i%pdfCompositeTileSize == 0 {
|
||||
if err := c.importer.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
pixel := &result[i]
|
||||
if pixel.Effect != 0 {
|
||||
for j := 0; j < space.Components(); j++ {
|
||||
pixel.Values[j] = math.Max(0, math.Min(1, (pixel.Values[j]*pixel.Alpha-backdrop[i].Values[j]*backdrop[i].Alpha*(1-pixel.Effect))/pixel.Effect))
|
||||
}
|
||||
}
|
||||
pixel.Alpha = pixel.Effect
|
||||
}
|
||||
complete = true
|
||||
return result, nil
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
// 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 ofdgo
|
||||
|
||||
import "image"
|
||||
|
||||
// pdfCompositeTileSize 限定单次合成块的像素边长
|
||||
const pdfCompositeTileSize = 256
|
||||
|
||||
// pdfCompositeBuffers 在一次局部合成中保留至多八个块容量内的空闲缓冲
|
||||
type pdfCompositeBuffers[T any] [8][]T
|
||||
|
||||
// pdfCompositeScratch 复用像素及蒙版缓冲,不跨页持有活动内容
|
||||
type pdfCompositeScratch struct {
|
||||
pixels pdfCompositeBuffers[pdfCompositePixel]
|
||||
masks pdfCompositeBuffers[float64]
|
||||
channels pdfCompositeBuffers[uint8]
|
||||
rasters [8]*image.RGBA
|
||||
}
|
||||
|
||||
// pdfCoverageRow 读取完整范围内的原生覆盖行,其他类型沿用逐点采样
|
||||
// 入参: coverage 覆盖图像, y 行坐标, left 起始列, right 结束列
|
||||
// 返回: []byte RGBA像素或空值
|
||||
func pdfCoverageRow(coverage image.Image, y, left, right int) []byte {
|
||||
buffer, ok := coverage.(*image.RGBA)
|
||||
if !ok || y < buffer.Rect.Min.Y || y >= buffer.Rect.Max.Y || left < buffer.Rect.Min.X || right > buffer.Rect.Max.X || left >= right {
|
||||
return nil
|
||||
}
|
||||
start := buffer.PixOffset(left, y)
|
||||
return buffer.Pix[start : start+4*(right-left)]
|
||||
}
|
||||
|
||||
// acquireCoverage 借用当前块的独占画布,保留活动画布供嵌套合成读取
|
||||
// 返回: *image.RGBA 未清空画布,不支持缓冲绘制的后端返回nil, error 无效绘制区域
|
||||
func (c *pdfCompositor) acquireCoverage() (*image.RGBA, error) {
|
||||
if _, ok := c.importer.editor.Backends().Raster.(RasterBufferBackend); !ok {
|
||||
return nil, nil
|
||||
}
|
||||
page := RasterPage{Width: c.box.W, Height: c.box.H, DPI: c.importer.rasterDPI}
|
||||
w, h, err := page.PixelSize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.scratch == nil {
|
||||
c.scratch = &pdfCompositeScratch{}
|
||||
}
|
||||
rect := image.Rect(0, 0, w, h)
|
||||
for i, stored := range c.scratch.rasters {
|
||||
if stored != nil && stored.Rect == rect {
|
||||
c.scratch.rasters[i] = nil
|
||||
return stored, nil
|
||||
}
|
||||
}
|
||||
return image.NewRGBA(rect), nil
|
||||
}
|
||||
|
||||
// releaseCoverage 归还当前调用不再读取的画布,限制单次合成保留的内存
|
||||
// 入参: coverage 当前调用的覆盖图像
|
||||
func (c *pdfCompositor) releaseCoverage(coverage image.Image) {
|
||||
if _, ok := c.importer.editor.Backends().Raster.(RasterBufferBackend); !ok || c.scratch == nil {
|
||||
return
|
||||
}
|
||||
buffer, ok := coverage.(*image.RGBA)
|
||||
if !ok || buffer == nil || cap(buffer.Pix) > 4*pdfCompositeTileSize*pdfCompositeTileSize {
|
||||
return
|
||||
}
|
||||
for i, stored := range c.scratch.rasters {
|
||||
if stored == nil {
|
||||
c.scratch.rasters[i] = buffer
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// acquire 借用尚未初始化的独立缓冲,嵌套调用不复用活动缓冲
|
||||
// 入参: count 所需元素数量
|
||||
// 返回: []T 可由当前调用独占的缓冲
|
||||
func (s *pdfCompositeBuffers[T]) acquire(count int) []T {
|
||||
selected := -1
|
||||
for i, buffer := range s {
|
||||
if cap(buffer) >= count && (selected < 0 || cap(buffer) < cap(s[selected])) {
|
||||
selected = i
|
||||
}
|
||||
}
|
||||
if selected < 0 {
|
||||
return make([]T, count)
|
||||
}
|
||||
buffer := s[selected]
|
||||
s[selected] = nil
|
||||
return buffer[:count]
|
||||
}
|
||||
|
||||
// release 归还已不再访问的缓冲,超出块容量或空闲槽数量时直接释放
|
||||
// 入参: buffer 当前调用借用的缓冲
|
||||
func (s *pdfCompositeBuffers[T]) release(buffer []T) {
|
||||
if len(buffer) == 0 || cap(buffer) > pdfCompositeTileSize*pdfCompositeTileSize {
|
||||
return
|
||||
}
|
||||
for i, stored := range s {
|
||||
if stored == nil {
|
||||
s[i] = buffer
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// acquirePixels 为当前合成器借用独立像素缓冲
|
||||
// 入参: count 所需像素数量
|
||||
// 返回: []pdfCompositePixel 尚未初始化的独占缓冲
|
||||
func (c *pdfCompositor) acquirePixels(count int) []pdfCompositePixel {
|
||||
if c.scratch == nil {
|
||||
c.scratch = &pdfCompositeScratch{}
|
||||
}
|
||||
return c.scratch.pixels.acquire(count)
|
||||
}
|
||||
|
||||
// releasePixels 归还当前调用已不再访问的像素缓冲
|
||||
// 入参: pixels 当前调用借用的缓冲
|
||||
func (c *pdfCompositor) releasePixels(pixels []pdfCompositePixel) {
|
||||
if c.scratch != nil {
|
||||
c.scratch.pixels.release(pixels)
|
||||
}
|
||||
}
|
||||
|
||||
// acquireChannels 借用图案通道标记,仅在设备函数需要时分配
|
||||
// 入参: count 当前块像素数
|
||||
// 返回: []uint8 尚未初始化的独占通道缓冲
|
||||
func (c *pdfCompositor) acquireChannels(count int) []uint8 {
|
||||
if c.scratch == nil {
|
||||
c.scratch = &pdfCompositeScratch{}
|
||||
}
|
||||
return c.scratch.channels.acquire(count)
|
||||
}
|
||||
|
||||
// releaseChannels 归还不再参与嵌套合成的通道标记
|
||||
// 入参: channels 当前调用借用的通道缓冲
|
||||
func (c *pdfCompositor) releaseChannels(channels []uint8) {
|
||||
if c.scratch != nil {
|
||||
c.scratch.channels.release(channels)
|
||||
}
|
||||
}
|
||||
|
||||
// releaseMasks 在当前块绘制结束后归还蒙版结果,不复用其他块的坐标缓存
|
||||
func (c *pdfCompositor) releaseMasks() {
|
||||
if c.scratch != nil {
|
||||
for _, mask := range c.masks {
|
||||
c.scratch.masks.release(mask)
|
||||
}
|
||||
}
|
||||
clear(c.masks)
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
// 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 ofdgo
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
baseimage "github.com/xiaoqidun/ofdgo/internal/base/image"
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
|
||||
// pdfShadingSourceKey 区分图案的使用几何及填充或描边
|
||||
type pdfShadingSourceKey struct {
|
||||
pattern *pdfgo.ShadingPattern
|
||||
geometry pdfCompositeKey
|
||||
}
|
||||
|
||||
// compositeShadingPattern 在页面内复用图案内部图元,不缓存依赖背景的像素
|
||||
// 入参: pattern 只读着色图案
|
||||
// 返回: []pdfCompositeNode 隐式挖空组, error 遍历或资源错误
|
||||
func (p *pdfImporter) compositeShadingPattern(pattern *pdfgo.ShadingPattern) ([]pdfCompositeNode, error) {
|
||||
cache := p.compositingCache()
|
||||
if nodes, found := cache.shadingPatterns[pattern]; found {
|
||||
return nodes, nil
|
||||
}
|
||||
inverse, ok := p.matrix.Inverse()
|
||||
if !ok {
|
||||
return nil, &pdfgo.UnsupportedError{Feature: "shading pattern page transform"}
|
||||
}
|
||||
box := pdfBounds([]pdfgo.Point{
|
||||
inverse.Apply(pdfgo.Point{}), inverse.Apply(pdfgo.Point{X: p.pageWidth}),
|
||||
inverse.Apply(pdfgo.Point{X: p.pageWidth, Y: p.pageHeight}), inverse.Apply(pdfgo.Point{Y: p.pageHeight}),
|
||||
})
|
||||
nodes, err := p.collectCompositeNodes(func(visitor pdfgo.Visitor) error {
|
||||
return pattern.Walk(p.ctx, pdfgo.Rectangle{XMin: box.X, YMin: box.Y, XMax: box.X + box.W, YMax: box.Y + box.H}, visitor)
|
||||
}, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cache.shadingPatterns == nil {
|
||||
cache.shadingPatterns = make(map[*pdfgo.ShadingPattern][]pdfCompositeNode)
|
||||
}
|
||||
cache.shadingPatterns[pattern] = nodes
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
// shadingPattern 统一对象和图案的几何覆盖,按相交面积合成背景及内部着色
|
||||
// 入参: pattern 着色图案, node 使用图元, outline 是否描边, backdrop 初始背景, space 父级混合空间, channels 可选源通道输出
|
||||
// 返回: []pdfCompositePixel 源颜色、形状及不透明度, error 状态或合成错误
|
||||
func (c *pdfCompositor) shadingPattern(pattern *pdfgo.ShadingPattern, node pdfCompositeNode, outline bool, backdrop []pdfCompositePixel, space *pdfgo.ColorSpace, channels []uint8) ([]pdfCompositePixel, error) {
|
||||
nodes, err := c.importer.shadingSources(pattern, node, outline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
local := *c
|
||||
local.transfers, local.shadingSource = nil, true
|
||||
local.channels = channels
|
||||
result, err := local.shadingSourcePixels(nodes[len(nodes)-1], backdrop, space)
|
||||
if err != nil || len(nodes) == 1 {
|
||||
return result, err
|
||||
}
|
||||
var backgroundChannels []uint8
|
||||
if channels != nil {
|
||||
backgroundChannels = c.acquireChannels(len(channels))
|
||||
defer c.releaseChannels(backgroundChannels)
|
||||
clear(backgroundChannels)
|
||||
}
|
||||
local.channels = backgroundChannels
|
||||
background, err := local.shadingSourcePixels(nodes[0], backdrop, space)
|
||||
if err != nil {
|
||||
c.releasePixels(result)
|
||||
return nil, err
|
||||
}
|
||||
defer c.releasePixels(background)
|
||||
coverage, err := c.coverage(nodes[0], outline)
|
||||
if err != nil {
|
||||
c.releasePixels(result)
|
||||
return nil, err
|
||||
}
|
||||
defer c.releaseCoverage(coverage)
|
||||
step := 25.4 / c.importer.rasterDPI
|
||||
for i := range result {
|
||||
if i%pdfCompositeTileSize == 0 {
|
||||
if err := c.importer.ctx.Err(); err != nil {
|
||||
c.releasePixels(result)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
shape := 0.0
|
||||
if coverage != nil {
|
||||
a := baseimage.AlphaAt(coverage, i%c.width, i/c.width)
|
||||
shape = float64(a) / 65535
|
||||
}
|
||||
if nodes[0].image != nil && shape != 0 {
|
||||
point := c.inverse.Apply(pdfgo.Point{X: c.box.X + (float64(i%c.width)+.5)*step, Y: c.box.Y + (float64(i/c.width)+.5)*step})
|
||||
_, alpha, err := c.imageColor(nodes[0].image, point, space)
|
||||
if err != nil {
|
||||
c.releasePixels(result)
|
||||
return nil, err
|
||||
}
|
||||
shape *= alpha
|
||||
}
|
||||
remaining := 1.0
|
||||
if shape != 0 {
|
||||
remaining -= math.Min(1, result[i].Shape/shape)
|
||||
}
|
||||
alpha := result[i].Alpha + background[i].Alpha*remaining
|
||||
if alpha != 0 {
|
||||
for j := 0; j < space.Components(); j++ {
|
||||
result[i].Values[j] = (result[i].Values[j]*result[i].Alpha + background[i].Values[j]*background[i].Alpha*remaining) / alpha
|
||||
}
|
||||
}
|
||||
result[i].Alpha, result[i].Effect = alpha, alpha
|
||||
result[i].Shape += background[i].Shape * remaining
|
||||
if channels != nil && background[i].Shape*remaining > 0 {
|
||||
channels[i] |= backgroundChannels[i]
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// shadingSources 将内部状态绑定到使用图元,几何和裁剪仅计算一次
|
||||
// 入参: pattern 只读着色图案, node 使用图元, outline 是否描边
|
||||
// 返回: []pdfCompositeNode 背景及着色图元, error 图案遍历错误
|
||||
func (p *pdfImporter) shadingSources(pattern *pdfgo.ShadingPattern, node pdfCompositeNode, outline bool) ([]pdfCompositeNode, error) {
|
||||
cache := p.compositingCache()
|
||||
key := pdfShadingSourceKey{pattern, pdfCompositeKey{path: node.path, text: node.text, image: node.image, stroke: outline}}
|
||||
if sources, ok := cache.shadingSources[key]; ok {
|
||||
return sources, nil
|
||||
}
|
||||
nodes, err := p.compositeShadingPattern(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
outer, _, _ := node.style()
|
||||
sources := make([]pdfCompositeNode, 0, len(nodes[0].children))
|
||||
for _, child := range nodes[0].children {
|
||||
style := child.path.Style
|
||||
style.LineWidth, style.Cap, style.Join, style.MiterLimit = outer.LineWidth, outer.Cap, outer.Join, outer.MiterLimit
|
||||
style.Dash, style.DashPhase, style.StrokeAdjust = outer.Dash, outer.DashPhase, outer.StrokeAdjust
|
||||
style.Clips = append(append([]pdfgo.Path(nil), outer.Clips...), style.Clips...)
|
||||
style.Stroke, style.StrokeOverprint = style.Fill, style.FillOverprint
|
||||
source := node
|
||||
if node.path != nil {
|
||||
mark := *node.path
|
||||
mark.Style, mark.Fill, mark.Stroke = style, !outline, outline
|
||||
source.path = &mark
|
||||
} else if node.text != nil {
|
||||
mark := *node.text
|
||||
mark.Style, mark.Mode = style, 0
|
||||
if outline {
|
||||
mark.Mode = 1
|
||||
}
|
||||
source.text = &mark
|
||||
} else {
|
||||
mark := *node.image
|
||||
mark.Style = style
|
||||
source.image = &mark
|
||||
}
|
||||
sources = append(sources, source)
|
||||
}
|
||||
if cache.shadingSources == nil {
|
||||
cache.shadingSources = make(map[pdfShadingSourceKey][]pdfCompositeNode)
|
||||
}
|
||||
cache.shadingSources[key] = sources
|
||||
return sources, nil
|
||||
}
|
||||
|
||||
// shadingSourcePixels 求值单个内部图元并移除初始背景贡献
|
||||
// 入参: node 内部图元, backdrop 初始背景, space 混合空间
|
||||
// 返回: []pdfCompositePixel 源颜色和覆盖, error 合成或取消错误
|
||||
func (c *pdfCompositor) shadingSourcePixels(node pdfCompositeNode, backdrop []pdfCompositePixel, space *pdfgo.ColorSpace) ([]pdfCompositePixel, error) {
|
||||
result := c.acquirePixels(len(backdrop))
|
||||
complete := false
|
||||
defer func() {
|
||||
if !complete {
|
||||
c.releasePixels(result)
|
||||
}
|
||||
}()
|
||||
for i, pixel := range backdrop {
|
||||
result[i] = pdfCompositePixel{Values: pixel.Values, Alpha: pixel.Alpha}
|
||||
}
|
||||
if err := c.drawMark(node, result, space); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range result {
|
||||
if i%pdfCompositeTileSize == 0 {
|
||||
if err := c.importer.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
pixel := &result[i]
|
||||
if pixel.Effect != 0 {
|
||||
for j := 0; j < space.Components(); j++ {
|
||||
pixel.Values[j] = math.Max(0, math.Min(1, (pixel.Values[j]*pixel.Alpha-backdrop[i].Values[j]*backdrop[i].Alpha*(1-pixel.Effect))/pixel.Effect))
|
||||
}
|
||||
}
|
||||
pixel.Alpha = pixel.Effect
|
||||
}
|
||||
complete = true
|
||||
return result, nil
|
||||
}
|
||||
@@ -374,7 +374,7 @@ func (p *pdfImporter) appendPath(mark pdfgo.PathMark) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
if mark.Fill && mark.Style.FillOverprint && pdfOverprintNeedsSeparation(mark.Style.Fill) || mark.Stroke && mark.Style.StrokeOverprint && pdfOverprintNeedsSeparation(mark.Style.Stroke) {
|
||||
if mark.Fill && mark.Style.FillOverprint && codecpdf.OverprintNeedsSeparation(mark.Style.Fill) || mark.Stroke && mark.Style.StrokeOverprint && codecpdf.OverprintNeedsSeparation(mark.Style.Stroke) {
|
||||
return &pdfgo.UnsupportedError{Feature: "color separation overprint"}
|
||||
}
|
||||
if p.warning != nil && (mark.Fill && p.gradientError(mark.Style.Fill) != nil || mark.Stroke && p.gradientError(mark.Style.Stroke) != nil) {
|
||||
|
||||
+1
-1
@@ -171,7 +171,7 @@ func (p *pdfImporter) groupPaths(mark pdfgo.GroupMark, walk func(pdfgo.Visitor)
|
||||
if stroke {
|
||||
color, overprint = path.Style.Stroke, path.Style.StrokeOverprint
|
||||
}
|
||||
if color.Tiling != nil || color.Shading != nil || color.Axial != nil || color.Radial != nil || color.Function != nil || color.Mesh != nil || overprint && pdfOverprintNeedsSeparation(color) {
|
||||
if color.Tiling != nil || color.Shading != nil || color.Axial != nil || color.Radial != nil || color.Function != nil || color.Mesh != nil || overprint && codecpdf.OverprintNeedsSeparation(color) {
|
||||
return fmt.Errorf("%w: patterned or overprinted group content", errPDFGroupRaster)
|
||||
}
|
||||
if color.Alpha == 0 {
|
||||
|
||||
@@ -1,475 +0,0 @@
|
||||
// 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 ofdgo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
|
||||
// pdfMeshVertex 保存局部毫米坐标及原曲面参数
|
||||
type pdfMeshVertex struct {
|
||||
point pdfgo.Point
|
||||
u, v float64
|
||||
}
|
||||
|
||||
// pdfMeshTriangle 保存按输出精度细分的曲面网格
|
||||
type pdfMeshTriangle struct {
|
||||
vertices [3]pdfMeshVertex
|
||||
patch int
|
||||
}
|
||||
|
||||
// pdfMeshKey 区分网格及其输出混合空间
|
||||
type pdfMeshKey struct {
|
||||
mesh *pdfgo.MeshGradient
|
||||
space *pdfgo.ColorSpace
|
||||
conversion pdfgo.ColorConversion
|
||||
softMask bool
|
||||
}
|
||||
|
||||
// pdfMeshSample 保存采样点参数,patch为一基编号,零表示未覆盖
|
||||
type pdfMeshSample struct {
|
||||
u, v float64
|
||||
patch int
|
||||
}
|
||||
|
||||
// pdfLinearMeshKey 定位源网格中需分段展开的三角形,不包含输出变换
|
||||
type pdfLinearMeshKey struct {
|
||||
mesh *pdfgo.MeshGradient
|
||||
index int
|
||||
}
|
||||
|
||||
// linearMeshTriangles 在单页内复用只读分区,固定布局计量内存并丢弃超预算缓存
|
||||
// 入参: mesh 源网格, index 源三角形索引
|
||||
// 返回: []pdfgo.MeshTriangle 只读分区结果, error 转换或取消错误
|
||||
func (p *pdfImporter) linearMeshTriangles(mesh *pdfgo.MeshGradient, index int) ([]pdfgo.MeshTriangle, error) {
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cache := p.compositingCache()
|
||||
if cache.linearMeshes == nil {
|
||||
cache.linearMeshes = &renderCache[pdfLinearMeshKey, []pdfgo.MeshTriangle]{limit: 8 << 20}
|
||||
}
|
||||
key := pdfLinearMeshKey{mesh: mesh, index: index}
|
||||
if triangles, ok := cache.linearMeshes.get(key); ok {
|
||||
return triangles, nil
|
||||
}
|
||||
triangles, err := mesh.LinearTrianglesContext(p.ctx, index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(triangles) > (cache.linearMeshes.limit-64)/216 {
|
||||
return triangles, nil
|
||||
}
|
||||
stored := make([]pdfgo.MeshTriangle, len(triangles))
|
||||
colors := make([][3][4]float64, len(triangles))
|
||||
for i, triangle := range triangles {
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stored[i].Points = triangle.Points
|
||||
for j, values := range triangle.Colors {
|
||||
copy(colors[i][j][:], values)
|
||||
stored[i].Colors[j] = colors[i][j][:len(values):len(values)]
|
||||
}
|
||||
}
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cache.linearMeshes.put(key, stored, 64+len(stored)*216)
|
||||
return stored, nil
|
||||
}
|
||||
|
||||
// meshColor 将线性三角网格映射为OFD高洛德渐变,保留顶点和覆盖顺序
|
||||
// 入参: paint PDF网格画刷, box 对象边界
|
||||
// 返回: *FillColor OFD渐变, error 不可线性表达的颜色或坐标错误
|
||||
func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error) {
|
||||
if err := p.gradientError(paint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mesh := paint.Mesh
|
||||
matrix := p.matrix.Mul(mesh.Matrix)
|
||||
shading := &GouraudShd{}
|
||||
icc := ""
|
||||
if pdfICCTransfer(mesh.Space) {
|
||||
var err error
|
||||
icc, err = p.iccColorSpace(mesh.Space, mesh.Intent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var previous pdfgo.MeshTriangle
|
||||
var previousColors [3][4]float64
|
||||
var colors [3][4]float64
|
||||
for i := range previous.Colors {
|
||||
previous.Colors[i] = previousColors[i][:mesh.Space.Components()]
|
||||
}
|
||||
for index, triangle := range mesh.Triangles {
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var err error
|
||||
colors, err = mesh.LinearColors(index)
|
||||
var triangles []pdfgo.MeshTriangle
|
||||
if err != nil {
|
||||
triangles, err = p.linearMeshTriangles(mesh, index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
for i := range triangle.Colors {
|
||||
triangle.Colors[i] = colors[i][:mesh.Space.Components()]
|
||||
}
|
||||
triangles = []pdfgo.MeshTriangle{triangle}
|
||||
}
|
||||
for _, triangle := range triangles {
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
first, flag := 0, 0
|
||||
if len(shading.Point) > 0 {
|
||||
flag = pdfMeshEdge(previous, triangle)
|
||||
if flag != 0 {
|
||||
first = 2
|
||||
}
|
||||
}
|
||||
for i := first; i < len(triangle.Points); i++ {
|
||||
point := triangle.Points[i]
|
||||
point = matrix.Apply(point)
|
||||
if !finite(point.X) || !finite(point.Y) {
|
||||
return nil, fmt.Errorf("nonfinite mesh vertex")
|
||||
}
|
||||
values := triangle.Colors[i]
|
||||
if icc != "" {
|
||||
value, err := pdfICCValues(values, mesh.Space.Components())
|
||||
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: value, ColorSpace: icc}})
|
||||
continue
|
||||
}
|
||||
rgb, err := mesh.Space.SRGB(values[:mesh.Space.Components()], mesh.Intent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
color := pdfgo.Paint{RGB: rgb, Alpha: 1}
|
||||
if mesh.Space.Model == "DeviceCMYK" && !mesh.Space.Calibrated() {
|
||||
color.CMYK = (*[4]float64)(values)
|
||||
}
|
||||
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
|
||||
for i := range previous.Colors {
|
||||
copy(previous.Colors[i], triangle.Colors[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
if mesh.Background != nil {
|
||||
background, err := p.gradientBackgroundColor(*mesh.Background, mesh.Space, mesh.Intent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shading.Extend, shading.BackColor = 1, &background
|
||||
}
|
||||
alpha := int(math.Round(paint.Alpha * 255))
|
||||
color := &FillColor{Alpha: &alpha}
|
||||
color.GouraudShd = shading
|
||||
return color, nil
|
||||
}
|
||||
|
||||
// pdfMeshEdge 按坐标及颜色分量匹配相邻三角形的共享边,不改变顶点顺序
|
||||
// 入参: previous 前一三角形, current 当前三角形
|
||||
// 返回: int OFD共享边标志,零表示独立三角形
|
||||
func pdfMeshEdge(previous, current pdfgo.MeshTriangle) int {
|
||||
if previous.Points[2] != current.Points[1] || !slices.Equal(previous.Colors[2], current.Colors[1]) {
|
||||
return 0
|
||||
}
|
||||
for _, index := range [2]int{1, 0} {
|
||||
if previous.Points[index] == current.Points[0] && slices.Equal(previous.Colors[index], current.Colors[0]) {
|
||||
return 2 - index
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// meshTriangles 按曲面二阶导数界细分,将几何误差控制在输出像素的十六分之一内
|
||||
// 入参: mesh 原始曲面
|
||||
// 返回: []pdfMeshTriangle 细分结果, error 取消或坐标错误
|
||||
func (p *pdfImporter) meshTriangles(mesh *pdfgo.MeshGradient) ([]pdfMeshTriangle, error) {
|
||||
matrix := p.matrix.Mul(mesh.Matrix)
|
||||
var triangles []pdfMeshTriangle
|
||||
for index, patch := range mesh.Patches {
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range patch.Points {
|
||||
for j := range patch.Points[i] {
|
||||
patch.Points[i][j] = matrix.Apply(patch.Points[i][j])
|
||||
}
|
||||
}
|
||||
var duu, dvv, duv float64
|
||||
for i := 0; i < 4; i++ {
|
||||
for j := 0; j < 4; j++ {
|
||||
a := patch.Points[i][j]
|
||||
if i < 2 {
|
||||
b, c := patch.Points[i+1][j], patch.Points[i+2][j]
|
||||
duu = math.Max(duu, 6*math.Hypot(c.X-2*b.X+a.X, c.Y-2*b.Y+a.Y))
|
||||
}
|
||||
if j < 2 {
|
||||
b, c := patch.Points[i][j+1], patch.Points[i][j+2]
|
||||
dvv = math.Max(dvv, 6*math.Hypot(c.X-2*b.X+a.X, c.Y-2*b.Y+a.Y))
|
||||
}
|
||||
if i < 3 && j < 3 {
|
||||
b, c, d := patch.Points[i+1][j], patch.Points[i][j+1], patch.Points[i+1][j+1]
|
||||
duv = math.Max(duv, 9*math.Hypot(d.X-b.X-c.X+a.X, d.Y-b.Y-c.Y+a.Y))
|
||||
}
|
||||
}
|
||||
}
|
||||
steps := math.Max(1, math.Ceil(math.Sqrt(8*(duu+dvv+2*duv)*p.rasterDPI/25.4)))
|
||||
if !finite(steps) || steps >= math.Sqrt(float64(int(^uint(0)>>1)/2)) {
|
||||
return nil, fmt.Errorf("mesh subdivision exceeds platform integer range")
|
||||
}
|
||||
n := int(steps)
|
||||
row := make([]pdfMeshVertex, n+1)
|
||||
for i := range row {
|
||||
if i%pdfCompositeTileSize == 0 {
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
u := float64(i) / steps
|
||||
row[i] = pdfMeshVertex{patch.PointAt(u, 0), u, 0}
|
||||
}
|
||||
for j := 1; j <= n; j++ {
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v := float64(j) / steps
|
||||
left := pdfMeshVertex{patch.PointAt(0, v), 0, v}
|
||||
for i := 1; i <= n; i++ {
|
||||
u := float64(i) / steps
|
||||
right := pdfMeshVertex{patch.PointAt(u, v), u, v}
|
||||
a, b := row[i-1], row[i]
|
||||
triangles = append(triangles, pdfMeshTriangle{[3]pdfMeshVertex{a, b, left}, index}, pdfMeshTriangle{[3]pdfMeshVertex{b, right, left}, index})
|
||||
row[i-1], left = left, right
|
||||
}
|
||||
row[n] = left
|
||||
}
|
||||
}
|
||||
for index, triangle := range mesh.Triangles {
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vertices := [3]pdfMeshVertex{
|
||||
{matrix.Apply(triangle.Points[0]), 0, 0},
|
||||
{matrix.Apply(triangle.Points[1]), 1, 0},
|
||||
{matrix.Apply(triangle.Points[2]), 0, 1},
|
||||
}
|
||||
triangles = append(triangles, pdfMeshTriangle{vertices, len(mesh.Patches) + index})
|
||||
}
|
||||
return triangles, nil
|
||||
}
|
||||
|
||||
// mesh 对当前图块执行四倍超采样,曲面重叠按后补片及较大v、u取值
|
||||
// 入参: mesh 原曲面, space 输出混合空间, conversion 设备转换函数
|
||||
// 返回: []pdfShadingPixel 颜色及覆盖, error 几何或颜色错误
|
||||
func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace, conversion pdfgo.ColorConversion) ([]pdfShadingPixel, error) {
|
||||
if err := c.importer.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := pdfMeshKey{mesh, space, conversion, c.softMask}
|
||||
if pixels, ok := c.meshes[key]; ok {
|
||||
return pixels, nil
|
||||
}
|
||||
converter, err := space.PrepareConversion(mesh.Space, mesh.Intent, conversion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
triangles, ok := c.cache.meshes[mesh]
|
||||
if !ok {
|
||||
var err error
|
||||
triangles, err = c.importer.meshTriangles(mesh)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.cache.meshes == nil {
|
||||
c.cache.meshes = map[*pdfgo.MeshGradient][]pdfMeshTriangle{}
|
||||
}
|
||||
c.cache.meshes[mesh] = triangles
|
||||
}
|
||||
const samples = 4
|
||||
w, h := c.width*samples, c.height*samples
|
||||
grid := make([]pdfMeshSample, w*h)
|
||||
scale := c.importer.rasterDPI / 25.4 * samples
|
||||
for _, triangle := range triangles {
|
||||
if err := c.importer.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vertices := triangle.vertices
|
||||
for i := range vertices {
|
||||
vertices[i].point.X = (vertices[i].point.X - c.box.X) * scale
|
||||
vertices[i].point.Y = (vertices[i].point.Y - c.box.Y) * scale
|
||||
}
|
||||
a, b, d := vertices[0].point, vertices[1].point, vertices[2].point
|
||||
denominator := (b.Y-d.Y)*(a.X-d.X) + (d.X-b.X)*(a.Y-d.Y)
|
||||
if denominator == 0 {
|
||||
continue
|
||||
}
|
||||
minX, maxX := math.Min(a.X, math.Min(b.X, d.X)), math.Max(a.X, math.Max(b.X, d.X))
|
||||
minY, maxY := math.Min(a.Y, math.Min(b.Y, d.Y)), math.Max(a.Y, math.Max(b.Y, d.Y))
|
||||
if maxX < 0 || maxY < 0 || minX >= float64(w) || minY >= float64(h) {
|
||||
continue
|
||||
}
|
||||
endX := int(math.Min(float64(w), math.Ceil(maxX)))
|
||||
endY := int(math.Min(float64(h), math.Ceil(maxY)))
|
||||
for y := int(math.Max(0, math.Floor(minY))); y < endY; y++ {
|
||||
if err := c.importer.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for x := int(math.Max(0, math.Floor(minX))); x < endX; x++ {
|
||||
px, py := float64(x)+.5, float64(y)+.5
|
||||
l0 := ((b.Y-d.Y)*(px-d.X) + (d.X-b.X)*(py-d.Y)) / denominator
|
||||
l1 := ((d.Y-a.Y)*(px-d.X) + (a.X-d.X)*(py-d.Y)) / denominator
|
||||
l2 := 1 - l0 - l1
|
||||
if l0 < -1e-10 || l1 < -1e-10 || l2 < -1e-10 {
|
||||
continue
|
||||
}
|
||||
u := math.Max(0, math.Min(1, l0*vertices[0].u+l1*vertices[1].u+l2*vertices[2].u))
|
||||
v := math.Max(0, math.Min(1, l0*vertices[0].v+l1*vertices[1].v+l2*vertices[2].v))
|
||||
current := &grid[y*w+x]
|
||||
if triangle.patch+1 > current.patch || triangle.patch+1 == current.patch && (v > current.v || v == current.v && u > current.u) {
|
||||
*current = pdfMeshSample{u, v, triangle.patch + 1}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pixels := make([]pdfShadingPixel, c.width*c.height)
|
||||
var background [4]float64
|
||||
if mesh.Background != nil {
|
||||
var err error
|
||||
background, err = converter.Convert(mesh.Background[:mesh.Space.Components()])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var inverse pdfgo.Matrix
|
||||
if mesh.Bounds != nil {
|
||||
var ok bool
|
||||
inverse, ok = c.importer.matrix.Mul(mesh.Matrix).Inverse()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("singular mesh bounds transform")
|
||||
}
|
||||
}
|
||||
sampleRows := func(start, end int) error {
|
||||
colors := make(map[[4]float64][4]float64)
|
||||
colorants := codecpdf.PrepareShadingColorants(pdfgo.Paint{Mesh: mesh}, space, c.softMask)
|
||||
var sampler *pdfgo.MeshSampler
|
||||
if colorants == nil {
|
||||
sampler = mesh.NewSampler()
|
||||
}
|
||||
for y := start * samples; y < end*samples; y++ {
|
||||
if err := c.importer.ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
for x := 0; x < w; x++ {
|
||||
if box := mesh.Bounds; box != nil {
|
||||
point := inverse.Apply(pdfgo.Point{X: c.box.X + (float64(x)+.5)/scale, Y: c.box.Y + (float64(y)+.5)/scale})
|
||||
if point.X < box.XMin || point.X > box.XMax || point.Y < box.YMin || point.Y > box.YMax {
|
||||
continue
|
||||
}
|
||||
}
|
||||
sample := grid[y*w+x]
|
||||
if sample.patch == 0 && mesh.Background == nil {
|
||||
continue
|
||||
}
|
||||
values := background
|
||||
if sample.patch != 0 {
|
||||
var err error
|
||||
if colorants != nil {
|
||||
values, err = colorants.MeshAt(sample.patch-1, sample.u, sample.v)
|
||||
} else {
|
||||
values, err = sampler.ValuesAt(sample.patch-1, sample.u, sample.v)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cached, ok := colors[values]; ok {
|
||||
values = cached
|
||||
} else if colorants == nil {
|
||||
source := values
|
||||
values, err = converter.Convert(values[:mesh.Space.Components()])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(colors) == 2*pdfCompositeTileSize {
|
||||
clear(colors)
|
||||
}
|
||||
colors[source] = values
|
||||
}
|
||||
}
|
||||
pixel := &pixels[(y/samples)*c.width+x/samples]
|
||||
for i, value := range values {
|
||||
pixel.values[i] += value
|
||||
}
|
||||
pixel.shape++
|
||||
}
|
||||
}
|
||||
return c.importer.ctx.Err()
|
||||
}
|
||||
workers := min(conversionWorkers(c.importer.ctx, 4), max(1, c.height/32))
|
||||
if workers == 1 {
|
||||
if err := sampleRows(0, c.height); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
var group sync.WaitGroup
|
||||
errors := make([]error, workers)
|
||||
for worker := range workers {
|
||||
group.Go(func() {
|
||||
errors[worker] = sampleRows(worker*c.height/workers, (worker+1)*c.height/workers)
|
||||
})
|
||||
}
|
||||
group.Wait()
|
||||
for _, err := range errors {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range pixels {
|
||||
pixel := &pixels[i]
|
||||
if pixel.shape == 0 {
|
||||
continue
|
||||
}
|
||||
for j := range pixel.values {
|
||||
pixel.values[j] /= pixel.shape
|
||||
}
|
||||
pixel.shape /= samples * samples
|
||||
}
|
||||
if c.meshes == nil {
|
||||
c.meshes = map[pdfMeshKey][]pdfShadingPixel{}
|
||||
}
|
||||
c.meshes[key] = pixels
|
||||
return pixels, nil
|
||||
}
|
||||
+202
-10
@@ -27,10 +27,7 @@ import (
|
||||
)
|
||||
|
||||
// pdfShadingPixel 保存抗锯齿覆盖率与混合空间分量
|
||||
type pdfShadingPixel struct {
|
||||
values [4]float64
|
||||
shape float64
|
||||
}
|
||||
type pdfShadingPixel = codecpdf.ShadingPixel
|
||||
|
||||
// pdfGradientKey 区分渐变画刷及其输出混合空间
|
||||
type pdfGradientKey struct {
|
||||
@@ -49,6 +46,20 @@ type pdfICCKey struct {
|
||||
intent pdfgo.Name
|
||||
}
|
||||
|
||||
// pdfMeshKey 区分网格及其输出混合空间
|
||||
type pdfMeshKey struct {
|
||||
mesh *pdfgo.MeshGradient
|
||||
space *pdfgo.ColorSpace
|
||||
conversion pdfgo.ColorConversion
|
||||
softMask bool
|
||||
}
|
||||
|
||||
// pdfLinearMeshKey 定位源网格中需分段展开的三角形,不包含输出变换
|
||||
type pdfLinearMeshKey struct {
|
||||
mesh *pdfgo.MeshGradient
|
||||
index int
|
||||
}
|
||||
|
||||
// pdfICCTransfer 检查OFD可原生表达的ICC模型与单位分量范围
|
||||
// 入参: space 源颜色空间
|
||||
// 返回: bool 是否可保留原始配置及颜色分量
|
||||
@@ -682,16 +693,16 @@ func (c *pdfCompositor) gradient(paint pdfgo.Paint, space *pdfgo.ColorSpace, nod
|
||||
weight *= alpha
|
||||
}
|
||||
for i, value := range values {
|
||||
pixel.values[i] += value * weight
|
||||
pixel.Values[i] += value * weight
|
||||
}
|
||||
pixel.shape += weight
|
||||
pixel.Shape += weight
|
||||
}
|
||||
}
|
||||
if pixel.shape != 0 {
|
||||
for i := range pixel.values {
|
||||
pixel.values[i] /= pixel.shape
|
||||
if pixel.Shape != 0 {
|
||||
for i := range pixel.Values {
|
||||
pixel.Values[i] /= pixel.Shape
|
||||
}
|
||||
pixel.shape /= samples * samples
|
||||
pixel.Shape /= samples * samples
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -866,3 +877,184 @@ func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box, intent pdfgo.Name)
|
||||
color.AxialShd = shading
|
||||
return color, nil
|
||||
}
|
||||
|
||||
// linearMeshTriangles 在单页内复用只读分区,固定布局计量内存并丢弃超预算缓存
|
||||
// 入参: mesh 源网格, index 源三角形索引
|
||||
// 返回: []pdfgo.MeshTriangle 只读分区结果, error 转换或取消错误
|
||||
func (p *pdfImporter) linearMeshTriangles(mesh *pdfgo.MeshGradient, index int) ([]pdfgo.MeshTriangle, error) {
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cache := p.compositingCache()
|
||||
if cache.linearMeshes == nil {
|
||||
cache.linearMeshes = &renderCache[pdfLinearMeshKey, []pdfgo.MeshTriangle]{limit: 8 << 20}
|
||||
}
|
||||
key := pdfLinearMeshKey{mesh: mesh, index: index}
|
||||
if triangles, ok := cache.linearMeshes.get(key); ok {
|
||||
return triangles, nil
|
||||
}
|
||||
triangles, err := mesh.LinearTrianglesContext(p.ctx, index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(triangles) > (cache.linearMeshes.limit-64)/216 {
|
||||
return triangles, nil
|
||||
}
|
||||
stored := make([]pdfgo.MeshTriangle, len(triangles))
|
||||
colors := make([][3][4]float64, len(triangles))
|
||||
for i, triangle := range triangles {
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stored[i].Points = triangle.Points
|
||||
for j, values := range triangle.Colors {
|
||||
copy(colors[i][j][:], values)
|
||||
stored[i].Colors[j] = colors[i][j][:len(values):len(values)]
|
||||
}
|
||||
}
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cache.linearMeshes.put(key, stored, 64+len(stored)*216)
|
||||
return stored, nil
|
||||
}
|
||||
|
||||
// meshColor 将线性三角网格映射为OFD高洛德渐变,保留顶点和覆盖顺序
|
||||
// 入参: paint PDF网格画刷, box 对象边界
|
||||
// 返回: *FillColor OFD渐变, error 不可线性表达的颜色或坐标错误
|
||||
func (p *pdfImporter) meshColor(paint pdfgo.Paint, box Box) (*FillColor, error) {
|
||||
if err := p.gradientError(paint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mesh := paint.Mesh
|
||||
matrix := p.matrix.Mul(mesh.Matrix)
|
||||
shading := &GouraudShd{}
|
||||
icc := ""
|
||||
if pdfICCTransfer(mesh.Space) {
|
||||
var err error
|
||||
icc, err = p.iccColorSpace(mesh.Space, mesh.Intent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var previous pdfgo.MeshTriangle
|
||||
var previousColors [3][4]float64
|
||||
var colors [3][4]float64
|
||||
for i := range previous.Colors {
|
||||
previous.Colors[i] = previousColors[i][:mesh.Space.Components()]
|
||||
}
|
||||
for index, triangle := range mesh.Triangles {
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var err error
|
||||
colors, err = mesh.LinearColors(index)
|
||||
var triangles []pdfgo.MeshTriangle
|
||||
if err != nil {
|
||||
triangles, err = p.linearMeshTriangles(mesh, index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
for i := range triangle.Colors {
|
||||
triangle.Colors[i] = colors[i][:mesh.Space.Components()]
|
||||
}
|
||||
triangles = []pdfgo.MeshTriangle{triangle}
|
||||
}
|
||||
for _, triangle := range triangles {
|
||||
if err := p.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
first, flag := 0, 0
|
||||
if len(shading.Point) > 0 {
|
||||
flag = codecpdf.MeshEdge(previous, triangle)
|
||||
if flag != 0 {
|
||||
first = 2
|
||||
}
|
||||
}
|
||||
for i := first; i < len(triangle.Points); i++ {
|
||||
point := triangle.Points[i]
|
||||
point = matrix.Apply(point)
|
||||
if !finite(point.X) || !finite(point.Y) {
|
||||
return nil, fmt.Errorf("nonfinite mesh vertex")
|
||||
}
|
||||
values := triangle.Colors[i]
|
||||
if icc != "" {
|
||||
value, err := pdfICCValues(values, mesh.Space.Components())
|
||||
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: value, ColorSpace: icc}})
|
||||
continue
|
||||
}
|
||||
rgb, err := mesh.Space.SRGB(values[:mesh.Space.Components()], mesh.Intent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
color := pdfgo.Paint{RGB: rgb, Alpha: 1}
|
||||
if mesh.Space.Model == "DeviceCMYK" && !mesh.Space.Calibrated() {
|
||||
color.CMYK = (*[4]float64)(values)
|
||||
}
|
||||
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
|
||||
for i := range previous.Colors {
|
||||
copy(previous.Colors[i], triangle.Colors[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
if mesh.Background != nil {
|
||||
background, err := p.gradientBackgroundColor(*mesh.Background, mesh.Space, mesh.Intent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shading.Extend, shading.BackColor = 1, &background
|
||||
}
|
||||
alpha := int(math.Round(paint.Alpha * 255))
|
||||
color := &FillColor{Alpha: &alpha}
|
||||
color.GouraudShd = shading
|
||||
return color, nil
|
||||
}
|
||||
|
||||
// mesh 复用页内网格及当前块采样结果,不缓存失败结果
|
||||
// 入参: mesh 原曲面, space 输出混合空间, conversion 设备转换函数
|
||||
// 返回: []pdfShadingPixel 颜色及覆盖, error 几何、颜色或取消错误
|
||||
func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace, conversion pdfgo.ColorConversion) ([]pdfShadingPixel, error) {
|
||||
if err := c.importer.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := pdfMeshKey{mesh, space, conversion, c.softMask}
|
||||
if pixels, ok := c.meshes[key]; ok {
|
||||
return pixels, nil
|
||||
}
|
||||
converter, err := space.PrepareConversion(mesh.Space, mesh.Intent, conversion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
triangles, ok := c.cache.meshes[mesh]
|
||||
if !ok {
|
||||
var err error
|
||||
triangles, err = codecpdf.MeshTriangles(c.importer.ctx, mesh, c.importer.matrix, c.importer.rasterDPI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.cache.meshes == nil {
|
||||
c.cache.meshes = map[*pdfgo.MeshGradient][]codecpdf.MeshTriangle{}
|
||||
}
|
||||
c.cache.meshes[mesh] = triangles
|
||||
}
|
||||
options := codecpdf.MeshRasterOptions{Matrix: c.importer.matrix, Origin: pdfgo.Point{X: c.box.X, Y: c.box.Y}, DPI: c.importer.rasterDPI, Width: c.width, Height: c.height, Workers: conversionWorkers(c.importer.ctx, 4), SoftMask: c.softMask}
|
||||
pixels, err := codecpdf.MeshPixels(c.importer.ctx, mesh, triangles, space, converter, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.meshes == nil {
|
||||
c.meshes = map[pdfMeshKey][]pdfShadingPixel{}
|
||||
}
|
||||
c.meshes[key] = pixels
|
||||
return pixels, nil
|
||||
}
|
||||
+2
-1
@@ -21,6 +21,7 @@ import (
|
||||
"strconv"
|
||||
"unicode/utf8"
|
||||
|
||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
|
||||
@@ -68,7 +69,7 @@ func (p *pdfImporter) text(mark pdfgo.TextMark) error {
|
||||
return err
|
||||
}
|
||||
paintMode := mark.Mode % 4
|
||||
if (paintMode == 0 || paintMode == 2) && mark.Style.FillOverprint && pdfOverprintNeedsSeparation(mark.Style.Fill) || (paintMode == 1 || paintMode == 2) && mark.Style.StrokeOverprint && pdfOverprintNeedsSeparation(mark.Style.Stroke) {
|
||||
if (paintMode == 0 || paintMode == 2) && mark.Style.FillOverprint && codecpdf.OverprintNeedsSeparation(mark.Style.Fill) || (paintMode == 1 || paintMode == 2) && mark.Style.StrokeOverprint && codecpdf.OverprintNeedsSeparation(mark.Style.Stroke) {
|
||||
return &pdfgo.UnsupportedError{Feature: "text color separation overprint"}
|
||||
}
|
||||
if p.warning != nil && ((paintMode == 0 || paintMode == 2) && p.gradientError(mark.Style.Fill) != nil || (paintMode == 1 || paintMode == 2) && p.gradientError(mark.Style.Stroke) != nil) {
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
// 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 ofdgo
|
||||
|
||||
import (
|
||||
codecpdf "github.com/xiaoqidun/ofdgo/internal/codec/pdf"
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
|
||||
// 图案标记保留设备四色通道及非零对象形状
|
||||
const (
|
||||
pdfTransferChannels uint8 = 15
|
||||
pdfTransferShape uint8 = 16
|
||||
)
|
||||
|
||||
// resolveTransfer 无设备函数时保留无缓存快速路径,按需绑定单页解析缓存
|
||||
// 入参: style 原始图形状态, model 原生设备颜色空间
|
||||
// 返回: *pdfgo.TransferFunction 有效函数, error 网屏传递函数错误
|
||||
func (p *pdfImporter) resolveTransfer(style pdfgo.Style, model pdfgo.Name) (*pdfgo.TransferFunction, error) {
|
||||
if style.Halftone == nil && style.Transfer == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return p.compositingCache().transfers.Resolve(p.reader, style, model)
|
||||
}
|
||||
|
||||
// compositeHasTransfer 检查最终设备使用的通道,不将备用通道误判为有效函数
|
||||
// 入参: nodes 原始图元及透明组, model 原生设备颜色空间
|
||||
// 返回: bool 是否存在非恒等函数, error 网屏函数错误
|
||||
func (p *pdfImporter) compositeHasTransfer(nodes []pdfCompositeNode, model pdfgo.Name) (bool, error) {
|
||||
for _, node := range nodes {
|
||||
if node.group != nil {
|
||||
if found, err := p.compositeHasTransfer(node.children, model); err != nil || found {
|
||||
return found, err
|
||||
}
|
||||
} else {
|
||||
style, _, _ := node.style()
|
||||
transfer, err := p.resolveTransfer(style, model)
|
||||
if err != nil || transfer != nil {
|
||||
return transfer != nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// transferDevice 为四色叠印保留原生设备通道,其余页面使用连续RGB设备
|
||||
// 入参: space 页面混合空间, nodes 参与合成的图元
|
||||
// 返回: pdfgo.Name 原生设备空间, error 图案或图像解析错误
|
||||
func (p *pdfImporter) transferDevice(space *pdfgo.ColorSpace, nodes []pdfCompositeNode) (pdfgo.Name, error) {
|
||||
if space.Model == "DeviceCMYK" && !space.Calibrated() {
|
||||
if found, err := p.processOverprint(nodes); err != nil {
|
||||
return "", err
|
||||
} else if found {
|
||||
return "DeviceCMYK", nil
|
||||
}
|
||||
}
|
||||
return "DeviceRGB", nil
|
||||
}
|
||||
|
||||
// selectTransfer 按最上层基本图元及祖先的不透明状态选择最终设备函数
|
||||
// 入参: node 基本图元, outline 是否描边
|
||||
// 返回: *pdfgo.TransferFunction 当前函数或设备默认值, error 图案解析错误
|
||||
func (c *pdfCompositor) selectTransfer(node pdfCompositeNode, outline bool) (*pdfgo.TransferFunction, error) {
|
||||
style, _, _ := node.style()
|
||||
paint := style.Fill
|
||||
if outline {
|
||||
paint = style.Stroke
|
||||
}
|
||||
if c.transferBlocked || paint.Alpha != 1 || style.SoftMask != nil || !codecpdf.NormalBlend(style.BlendMode) {
|
||||
return nil, nil
|
||||
}
|
||||
transfer, err := c.importer.resolveTransfer(style, c.transferModel)
|
||||
if err != nil || transfer == nil {
|
||||
return transfer, err
|
||||
}
|
||||
if pattern := paint.Shading; pattern != nil && (node.image == nil || node.image.Image.ImageMask) {
|
||||
inner := pattern.Style
|
||||
if inner.Fill.Alpha != 1 || inner.SoftMask != nil || !codecpdf.NormalBlend(inner.BlendMode) {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
if node.image != nil {
|
||||
soft, err := node.image.Image.HasSoftMask()
|
||||
if err != nil || soft {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if paint.Tiling != nil && (node.image == nil || node.image.Image.ImageMask) {
|
||||
pattern, err := c.importer.compositePattern(paint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opaque, err := pattern.opaqueTransfer()
|
||||
if err != nil || !opaque {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return transfer, nil
|
||||
}
|
||||
|
||||
// opaqueTransfer 复用图案不透明性检查,图案内部函数不提前作用于源颜色
|
||||
// 返回: bool 是否允许使用外层设备函数, error 单元解析错误
|
||||
func (p *pdfCompositePattern) opaqueTransfer() (bool, error) {
|
||||
if !p.transferChecked {
|
||||
for _, child := range p.nodes {
|
||||
opaque, err := p.local.transferOpaque(child)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !opaque {
|
||||
p.transferChecked = true
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
p.transferChecked, p.transferOpaque = true, true
|
||||
}
|
||||
return p.transferOpaque, nil
|
||||
}
|
||||
|
||||
// transferOpaque 检查全部图案内容的不透明条件,不限制混合颜色空间
|
||||
// 入参: node 单元图元或透明组
|
||||
// 返回: bool 是否不透明, error 图案或图片遮罩引用错误
|
||||
func (p *pdfImporter) transferOpaque(node pdfCompositeNode) (bool, error) {
|
||||
if group := node.group; group != nil {
|
||||
if group.Alpha != 1 || group.SoftMask != nil || !codecpdf.NormalBlend(group.BlendMode) {
|
||||
return false, nil
|
||||
}
|
||||
for _, child := range node.children {
|
||||
if opaque, err := p.transferOpaque(child); err != nil || !opaque {
|
||||
return opaque, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
style, fill, stroke := node.style()
|
||||
if style.SoftMask != nil || !codecpdf.NormalBlend(style.BlendMode) || fill && style.Fill.Alpha != 1 || stroke && style.Stroke.Alpha != 1 {
|
||||
return false, nil
|
||||
}
|
||||
if node.image != nil {
|
||||
soft, err := node.image.Image.HasSoftMask()
|
||||
if err != nil || soft {
|
||||
return false, err
|
||||
}
|
||||
if !node.image.Image.ImageMask {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
for i, paint := range [2]pdfgo.Paint{style.Fill, style.Stroke} {
|
||||
if pattern := paint.Shading; pattern != nil && (i == 0 && fill || i == 1 && stroke) {
|
||||
inner := pattern.Style
|
||||
if inner.Fill.Alpha != 1 || inner.SoftMask != nil || !codecpdf.NormalBlend(inner.BlendMode) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if i == 0 && !fill || i == 1 && !stroke || paint.Tiling == nil {
|
||||
continue
|
||||
}
|
||||
pattern, err := p.compositePattern(paint)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if opaque, err := pattern.opaqueTransfer(); err != nil || !opaque {
|
||||
return opaque, err
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// halftone 检查连续色调转换,避免将打印网屏误用于屏幕显示
|
||||
// 入参: style 图形状态
|
||||
// 返回: error 严格模式无法保留设备网屏
|
||||
func (p *pdfImporter) halftone(style pdfgo.Style) error {
|
||||
h := style.Halftone
|
||||
message := "PDF continuous-tone appearance retained; device halftone not transferred to OFD"
|
||||
if h == nil || h.Type == 0 {
|
||||
if style.HalftoneOrigin == nil {
|
||||
return nil
|
||||
}
|
||||
message = "PDF continuous-tone appearance retained; device halftone origin not transferred to OFD"
|
||||
}
|
||||
if p.warning == nil {
|
||||
return &pdfgo.UnsupportedError{Feature: "device halftone conversion"}
|
||||
}
|
||||
if p.halftoneWarnings == nil {
|
||||
p.halftoneWarnings = make(map[*pdfgo.Halftone]bool)
|
||||
}
|
||||
if !p.halftoneWarnings[h] {
|
||||
p.halftoneWarnings[h] = true
|
||||
p.warning(pdfgo.Diagnostic{Message: message})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pdfOpaqueBlack 判断不受底层分色影响的全黑设备色
|
||||
// 入参: paint PDF颜色与不透明度
|
||||
// 返回: bool 是否为不透明全黑CMYK颜色
|
||||
func pdfOpaqueBlack(paint pdfgo.Paint) bool {
|
||||
return paint.Alpha == 1 && paint.CMYK != nil && paint.CMYK[3] == 1
|
||||
}
|
||||
|
||||
// pdfOverprintNeedsSeparation 判断彩色CMYK套印是否依赖分色输出
|
||||
// 入参: paint PDF颜色与不透明度
|
||||
// 返回: bool 是否需要保留分色语义
|
||||
func pdfOverprintNeedsSeparation(paint pdfgo.Paint) bool {
|
||||
_, process := paint.Process.CMYKMask()
|
||||
return (paint.CMYK != nil || process) && !pdfOpaqueBlack(paint)
|
||||
}
|
||||
Reference in new issue
Block a user