diff --git a/internal/codec/pdf/color.go b/internal/codec/pdf/color.go index 3ff3d8d..9cdcd78 100644 --- a/internal/codec/pdf/color.go +++ b/internal/codec/pdf/color.go @@ -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) +} diff --git a/internal/codec/pdf/mesh.go b/internal/codec/pdf/mesh.go new file mode 100644 index 0000000..4c79508 --- /dev/null +++ b/internal/codec/pdf/mesh.go @@ -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 +} diff --git a/internal/codec/pdf/scratch.go b/internal/codec/pdf/scratch.go new file mode 100644 index 0000000..d2e37b0 --- /dev/null +++ b/internal/codec/pdf/scratch.go @@ -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 + } + } +} diff --git a/internal/codec/pdf/shading.go b/internal/codec/pdf/shading.go index 36e8978..7c40348 100644 --- a/internal/codec/pdf/shading.go +++ b/internal/codec/pdf/shading.go @@ -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 diff --git a/ofdgo_pdf_composite.go b/ofdgo_pdf_composite.go index 84b828e..8ff5778 100644 --- a/ofdgo_pdf_composite.go +++ b/ofdgo_pdf_composite.go @@ -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) +} diff --git a/ofdgo_pdf_composite_draw.go b/ofdgo_pdf_composite_draw.go index c05318e..6299e94 100644 --- a/ofdgo_pdf_composite_draw.go +++ b/ofdgo_pdf_composite_draw.go @@ -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) diff --git a/ofdgo_pdf_composite_pattern.go b/ofdgo_pdf_composite_pattern.go index aefa140..00368d6 100644 --- a/ofdgo_pdf_composite_pattern.go +++ b/ofdgo_pdf_composite_pattern.go @@ -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 +} diff --git a/ofdgo_pdf_composite_scratch.go b/ofdgo_pdf_composite_scratch.go deleted file mode 100644 index 73f2336..0000000 --- a/ofdgo_pdf_composite_scratch.go +++ /dev/null @@ -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) -} diff --git a/ofdgo_pdf_composite_shading.go b/ofdgo_pdf_composite_shading.go deleted file mode 100644 index 1ab28af..0000000 --- a/ofdgo_pdf_composite_shading.go +++ /dev/null @@ -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 -} diff --git a/ofdgo_pdf_geometry.go b/ofdgo_pdf_geometry.go index 14f3454..f1c8474 100644 --- a/ofdgo_pdf_geometry.go +++ b/ofdgo_pdf_geometry.go @@ -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) { diff --git a/ofdgo_pdf_group.go b/ofdgo_pdf_group.go index f3d7654..651725b 100644 --- a/ofdgo_pdf_group.go +++ b/ofdgo_pdf_group.go @@ -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 { diff --git a/ofdgo_pdf_mesh.go b/ofdgo_pdf_mesh.go deleted file mode 100644 index 9436fea..0000000 --- a/ofdgo_pdf_mesh.go +++ /dev/null @@ -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 -} diff --git a/ofdgo_pdf_shading.go b/ofdgo_pdf_shading.go index bc9dacc..f64f51f 100644 --- a/ofdgo_pdf_shading.go +++ b/ofdgo_pdf_shading.go @@ -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 +} diff --git a/ofdgo_pdf_text.go b/ofdgo_pdf_text.go index 2e206d3..23ba965 100644 --- a/ofdgo_pdf_text.go +++ b/ofdgo_pdf_text.go @@ -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) { diff --git a/ofdgo_pdf_transfer.go b/ofdgo_pdf_transfer.go deleted file mode 100644 index 089bee6..0000000 --- a/ofdgo_pdf_transfer.go +++ /dev/null @@ -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) -}