diff --git a/ofdgo_pdf_composite.go b/ofdgo_pdf_composite.go index a0a5fb7..9ac06f3 100644 --- a/ofdgo_pdf_composite.go +++ b/ofdgo_pdf_composite.go @@ -72,6 +72,7 @@ type pdfCompositeCache struct { images map[*pdfgo.Image]*pdfgo.ImageComponents masks map[*pdfgo.SoftMask][]pdfCompositeNode meshes map[*pdfgo.MeshGradient][]pdfMeshTriangle + patterns map[pdfgo.Paint]*pdfCompositePattern } // compositingCache 按需创建单页合成缓存,页面切换时由导入器释放 diff --git a/ofdgo_pdf_composite_draw.go b/ofdgo_pdf_composite_draw.go index 18bdfae..3a21be8 100644 --- a/ofdgo_pdf_composite_draw.go +++ b/ofdgo_pdf_composite_draw.go @@ -109,6 +109,17 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf compatible := overprint && style.OverprintMode == 1 && node.image == nil && space.Model == "DeviceCMYK" && !space.Calibrated() && (paint.CMYK != nil || paint.Space != nil && paint.Space.Model == "DeviceCMYK" && !paint.Space.Calibrated()) step := 25.4 / c.importer.rasterDPI uniform := node.image == nil && paint.Axial == nil && paint.Radial == nil && paint.Tiling == nil && paint.Mesh == nil + var pattern []pdfCompositePixel + if paint.Tiling != nil && (node.image == nil || node.image.Image.ImageMask) { + backdrop := pixels + if initial != nil { + backdrop = initial + } + pattern, err = c.tiling(paint, backdrop, space) + if err != nil { + return err + } + } var mesh []pdfMeshPixel if paint.Mesh != nil { mesh, err = c.mesh(paint.Mesh, space) @@ -133,7 +144,20 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf opacity *= mask[index] } var values [4]float64 - if mesh != nil { + if pattern != nil { + sample := pattern[index] + values = sample.values + shape *= sample.shape + if sample.shape != 0 { + opacity *= sample.alpha / sample.shape + } + if node.image != nil { + point := c.inverse.Apply(pdfgo.Point{X: c.box.X + (float64(x)+.5)*step, Y: c.box.Y + (float64(y)+.5)*step}) + var alpha float64 + _, alpha, err = c.imageColor(*node.image, point, space) + opacity *= alpha + } + } else if mesh != nil { values = mesh[index].values shape *= mesh[index].shape } else if uniform { @@ -442,6 +466,9 @@ func (c *pdfCompositor) imageColor(mark pdfgo.ImageMark, point pdfgo.Point, spac } } if mark.Image.ImageMask { + if mark.Style.Fill.Tiling != nil { + return [4]float64{}, alpha * (1 - values[0]), nil + } color, visible, err := pdfCompositeColor(mark.Style.Fill, mark.Matrix.Apply(point), space, mark.Style.RenderingIntent) if !visible { return color, 0, err diff --git a/ofdgo_pdf_composite_pattern.go b/ofdgo_pdf_composite_pattern.go new file mode 100644 index 0000000..0772172 --- /dev/null +++ b/ofdgo_pdf_composite_pattern.go @@ -0,0 +1,110 @@ +// 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" + + "github.com/xiaoqidun/pdfgo" +) + +// pdfCompositePattern 保存图案单元的原始图元和独立几何缓存 +type pdfCompositePattern struct { + nodes []pdfCompositeNode + local pdfImporter + box Box +} + +// tiling 将图案作为非隔离组绘制,保留背景贡献及单元间隙 +// 入参: paint 图案画刷, backdrop 初始背景, space 混合空间 +// 返回: []pdfCompositePixel 去除背景贡献的图案像素, error 解析或绘制错误 +func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel, space *pdfgo.ColorSpace) ([]pdfCompositePixel, error) { + source := paint.Tiling + paint.Alpha = 1 + if c.cache.patterns == nil { + c.cache.patterns = make(map[pdfgo.Paint]*pdfCompositePattern) + } + pattern := c.cache.patterns[paint] + matrix := c.importer.matrix.Mul(source.Matrix) + inverse, ok := matrix.Inverse() + if !ok { + return nil, fmt.Errorf("invalid PDF tiling pattern matrix") + } + if pattern == nil { + nodes, err := pdfCompositeNodes(func(v pdfgo.Visitor) error { return source.Walk(c.importer.ctx, paint, v) }, c.importer.warning) + if err != nil { + return nil, err + } + b := source.BBox + box := pdfBounds([]pdfgo.Point{ + matrix.Apply(pdfgo.Point{X: b.XMin, Y: b.YMin}), matrix.Apply(pdfgo.Point{X: b.XMax, Y: b.YMin}), + matrix.Apply(pdfgo.Point{X: b.XMax, Y: b.YMax}), matrix.Apply(pdfgo.Point{X: b.XMin, Y: b.YMax}), + }) + if box.W <= 0 || box.H <= 0 { + return nil, fmt.Errorf("invalid PDF tiling pattern bounds") + } + local := *c.importer + local.matrix = matrix + local.matrix[4] -= box.X + local.matrix[5] -= box.Y + local.pageWidth, local.pageHeight = box.W, box.H + local.compositeCache = nil + pattern = &pdfCompositePattern{nodes: nodes, local: local, box: box} + c.cache.patterns[paint] = pattern + } + xstep, ystep := math.Abs(source.XStep), math.Abs(source.YStep) + area := pdfBounds([]pdfgo.Point{ + inverse.Apply(pdfgo.Point{X: c.box.X, Y: c.box.Y}), inverse.Apply(pdfgo.Point{X: c.box.X + c.box.W, Y: c.box.Y}), + inverse.Apply(pdfgo.Point{X: c.box.X + c.box.W, Y: c.box.Y + c.box.H}), inverse.Apply(pdfgo.Point{X: c.box.X, Y: c.box.Y + c.box.H}), + }) + left, right := math.Ceil((area.X-source.BBox.XMax)/xstep), math.Floor((area.X+area.W-source.BBox.XMin)/xstep) + top, bottom := math.Ceil((area.Y-source.BBox.YMax)/ystep), math.Floor((area.Y+area.H-source.BBox.YMin)/ystep) + limit := float64(int(^uint(0) >> 1)) + for _, value := range []float64{left, right, top, bottom} { + if !finite(value) || value <= -limit || value >= limit { + return nil, fmt.Errorf("PDF tiling pattern instance range overflow") + } + } + result := make([]pdfCompositePixel, len(backdrop)) + for i, pixel := range backdrop { + result[i] = pdfCompositePixel{values: pixel.values, alpha: pixel.alpha} + } + localInverse, _ := pattern.local.matrix.Inverse() + for row := int(top); row <= int(bottom); row++ { + for column := int(left); column <= int(right); column++ { + if err := c.importer.ctx.Err(); err != nil { + return nil, err + } + x, y := float64(column)*xstep, float64(row)*ystep + dx, dy := matrix[0]*x+matrix[2]*y+pattern.box.X, matrix[1]*x+matrix[3]*y+pattern.box.Y + cell := pdfCompositor{importer: &pattern.local, box: Box{X: c.box.X - dx, Y: c.box.Y - dy, W: c.box.W, H: c.box.H}, + width: c.width, height: c.height, inverse: localInverse, cache: pattern.local.compositingCache(), masks: map[*pdfgo.SoftMask][]float64{}} + if err := cell.draw(pattern.nodes, result, space); err != nil { + return nil, err + } + } + } + for i := range result { + 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 + } + return result, nil +}