refactor(重构提升): 优化图案着色降低内存占用
build.yaml / build (push) Successful in 43s

This commit is contained in:
xiaoqidun committed 2026-10-05 22:57:00 +08:00
1 parent 09a49b2c26
commit b028a5d7c7
10 files changed
+310 -80

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c github.com/tdewolff/canvas v0.0.0-20260923214215-09804640d00c
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6 github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1 github.com/xiaoqidun/jbig2 v1.1.1
github.com/xiaoqidun/pdfgo v0.0.0-20261005090911-e5eb2af5dff1 github.com/xiaoqidun/pdfgo v0.0.0-20261005144606-00d3cdef7848
golang.org/x/crypto v0.57.0 golang.org/x/crypto v0.57.0
golang.org/x/image v0.46.0 golang.org/x/image v0.46.0
golang.org/x/text v0.42.0 golang.org/x/text v0.42.0
+2 -2
View File
@@ -62,8 +62,8 @@ github.com/tdewolff/test v1.0.12 h1:7F21DqIajswxuche0geHdrUZRCWE4oko4b7bcmkkrxk=
github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI= github.com/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ= github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
github.com/xiaoqidun/pdfgo v0.0.0-20261005090911-e5eb2af5dff1 h1:AoxEA4tvcE+8tP3i4fE2f3It3DzbNBmvEVVMTp59sDM= github.com/xiaoqidun/pdfgo v0.0.0-20261005144606-00d3cdef7848 h1:jacPfuuqF5qQVchUp935MHaE75ubIRBDjpJgEJD0Hk4=
github.com/xiaoqidun/pdfgo v0.0.0-20261005090911-e5eb2af5dff1/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ= github.com/xiaoqidun/pdfgo v0.0.0-20261005144606-00d3cdef7848/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+36 -25
View File
@@ -99,6 +99,7 @@ type pdfImporter struct {
compositeCache *pdfCompositeCache compositeCache *pdfCompositeCache
compositeNodes []pdfCompositeNode compositeNodes []pdfCompositeNode
compositeSpace *pdfgo.ColorSpace compositeSpace *pdfgo.ColorSpace
patternCells *renderCache[pdfPatternCellKey, *Pattern]
annotationData []byte annotationData []byte
resolveFile pdfgo.FileResolver resolveFile pdfgo.FileResolver
resolveReference pdfgo.ReferenceResolver resolveReference pdfgo.ReferenceResolver
@@ -210,6 +211,7 @@ func ImportPDF(ctx context.Context, source io.ReaderAt, size int64, options PDFI
importer.compositeCache = nil importer.compositeCache = nil
importer.compositeNodes = nil importer.compositeNodes = nil
importer.compositeSpace = nil importer.compositeSpace = nil
importer.patternCells = nil
importer.clipTexts = make(map[*pdfgo.TextClip][]TextObject) importer.clipTexts = make(map[*pdfgo.TextClip][]TextObject)
importer.clipPaths = nil importer.clipPaths = nil
importer.annotationData = nil importer.annotationData = nil
@@ -457,7 +459,7 @@ func (p *pdfImporter) pathDataContext(ctx context.Context, path pdfgo.Path, orig
return string(data), nil return string(data), nil
} }
// clips 保持裁剪路径的交集,不随对象CTM重复变换 // clips 复用只读裁剪几何,以区域CTM保留对象位移及路径交集
// 入参: paths 裁剪路径, origin 对象边界 // 入参: paths 裁剪路径, origin 对象边界
// 返回: *Clips OFD裁剪区域,无裁剪时为空, error 未解析的裁剪字形 // 返回: *Clips OFD裁剪区域,无裁剪时为空, error 未解析的裁剪字形
func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) { func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
@@ -465,23 +467,24 @@ func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
return nil, nil return nil, nil
} }
flag := false flag := false
clips := &Clips{TransFlag: &flag} clips := &Clips{TransFlag: &flag, Clip: make([]Clip, len(paths))}
for _, path := range paths { areas := make([]ClipArea, len(paths))
translation := ""
if origin.X != 0 || origin.Y != 0 {
translation = pdfNumbers(1, 0, 0, 1, -origin.X, -origin.Y)
}
for index, path := range paths {
if err := p.ctx.Err(); err != nil { if err := p.ctx.Err(); err != nil {
return nil, err return nil, err
} }
area := ClipArea{} area := &areas[index]
area.CTM = translation
if len(path.Segments) != 0 { if len(path.Segments) != 0 {
encoded, err := p.clipPath(path) encoded, err := p.clipPath(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
box := encoded.box area.Path = encoded.path[:1:1]
rule := "NonZero"
if path.EvenOdd {
rule = "Even-Odd"
}
area.Path = append(area.Path, PathObject{Boundary: pdfBoundary(Box{box.X - origin.X, box.Y - origin.Y, box.W, box.H}), AbbreviatedData: encoded.data, Rule: rule})
} }
for _, mark := range path.Text { for _, mark := range path.Text {
objects, ok := p.clipTexts[mark] objects, ok := p.clipTexts[mark]
@@ -498,16 +501,13 @@ func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
p.report.TextObjects = textCount p.report.TextObjects = textCount
objects = p.clipTexts[mark] objects = p.clipTexts[mark]
} }
for _, object := range objects { if len(path.Text) == 1 {
box, err := ParseBox(object.Boundary) area.Text = objects[:len(objects):len(objects)]
if err != nil { } else {
return nil, err area.Text = append(area.Text, objects...)
}
object.Boundary = pdfBoundary(Box{box.X - origin.X, box.Y - origin.Y, box.W, box.H})
area.Text = append(area.Text, object)
} }
} }
clips.Clip = append(clips.Clip, Clip{Area: []ClipArea{area}}) clips.Clip[index].Area = areas[index : index+1 : index+1]
} }
return clips, nil return clips, nil
} }
@@ -736,10 +736,10 @@ func (p *pdfImporter) paintColor(paint pdfgo.Paint, box Box) (*FillColor, error)
return color, nil return color, nil
} }
// tilingPattern 保留PDF图案单元与页面坐标之间的仿射关系 // buildTilingPattern 保留PDF图案单元与页面坐标之间的仿射关系
// 入参: source 平铺图案, base 无色图案基色 // 入参: source 平铺图案, base 无色图案基色
// 返回: *Pattern OFD图案, error 转换错误 // 返回: *Pattern OFD图案, error 转换错误
func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Paint) (*Pattern, error) { func (p *pdfImporter) buildTilingPattern(source *pdfgo.TilingPattern, base pdfgo.Paint) (*Pattern, error) {
const unit = 25.4 / 72 const unit = 25.4 / 72
box := source.BBox box := source.BBox
width, height := box.XMax-box.XMin, box.YMax-box.YMin width, height := box.XMax-box.XMin, box.YMax-box.YMin
@@ -753,6 +753,10 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain
cell := *p cell := *p
cell.matrix = pdfgo.Matrix{unit, 0, 0, -unit, -box.XMin * unit, box.YMax * unit} cell.matrix = pdfgo.Matrix{unit, 0, 0, -unit, -box.XMin * unit, box.YMax * unit}
cell.pageWidth, cell.pageHeight = width*unit, height*unit cell.pageWidth, cell.pageHeight = width*unit, height*unit
cell.rasterDPI *= m.MaxScale()
if !finite(cell.rasterDPI) || cell.rasterDPI < 0 {
return nil, fmt.Errorf("invalid PDF tiling pattern raster scale")
}
cell.objects, cell.pendingPath = nil, nil cell.objects, cell.pendingPath = nil, nil
visitor := pdfgo.Visitor{Path: cell.path, Text: cell.text, Image: cell.image, Reference: p.referencePage} visitor := pdfgo.Visitor{Path: cell.path, Text: cell.text, Image: cell.image, Reference: p.referencePage}
visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error { visitor.Group = func(mark pdfgo.GroupMark, walk func(pdfgo.Visitor) error) error {
@@ -769,24 +773,31 @@ func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Pain
return nil, err return nil, err
} }
pattern.CellContent.Objects = objects pattern.CellContent.Objects = objects
if width > xstep || height > ystep { if len(objects) != 0 && (width > xstep || height > ystep) {
columns, rows := math.Ceil(width/xstep), math.Ceil(height/ystep) columns, rows := math.Ceil(width/xstep), math.Ceil(height/ystep)
if !finite(columns*rows) || columns*rows > float64(int(^uint(0)>>1)) { if !finite(columns*rows) || columns*rows >= float64(uint(^uint(0)>>1)+1) {
return nil, fmt.Errorf("PDF tiling pattern instance count overflow") return nil, fmt.Errorf("PDF tiling pattern instance count overflow")
} }
bounds := make([]Box, len(objects)) bounds := make([]Box, len(objects))
for index, object := range objects { for index, object := range objects {
boundary := ""
if object.Type == "PathObject" && object.PathObject.Stroke != nil && !*object.PathObject.Stroke { if object.Type == "PathObject" && object.PathObject.Stroke != nil && !*object.PathObject.Stroke {
boundary = object.PathObject.Boundary
} else if object.Type == "ImageObject" && object.ImageObject.Border == nil {
boundary = object.ImageObject.Boundary
}
if boundary != "" {
var err error var err error
bounds[index], err = ParseBox(object.PathObject.Boundary) bounds[index], err = ParseBox(boundary)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
} }
left, right, top, bottom := pdfPatternInstanceRange(columns, rows, bounds, pattern.Width, pattern.Height, xstep*unit, ystep*unit)
var expanded pdfObjectBuffer var expanded pdfObjectBuffer
for row := 1 - int(rows); row <= 0; row++ { for row := top; row <= bottom; row++ {
for column := 1 - int(columns); column <= 0; column++ { for column := left; column <= right; column++ {
if err := p.ctx.Err(); err != nil { if err := p.ctx.Err(); err != nil {
return nil, err return nil, err
} }
+9 -3
View File
@@ -23,10 +23,11 @@ import (
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
// pdfClipPath 保存不可变的裁剪路径编码,不缓存对象位移或文字裁剪 // pdfClipPath 保存导入器只读的裁剪几何,不缓存对象位移或文字裁剪
type pdfClipPath struct { type pdfClipPath struct {
box Box box Box
data string data string
path *[1]PathObject
} }
// pdfClipPathKey 按路径完整内容与变换生成键,不依赖可变切片的地址 // pdfClipPathKey 按路径完整内容与变换生成键,不依赖可变切片的地址
@@ -94,7 +95,12 @@ func (p *pdfImporter) clipPath(path pdfgo.Path) (pdfClipPath, error) {
if err != nil { if err != nil {
return pdfClipPath{}, err return pdfClipPath{}, err
} }
result := pdfClipPath{box: box, data: data} rule := "NonZero"
p.clipPaths.put(key, result, len(data)+256) if path.EvenOdd {
rule = "Even-Odd"
}
boundary := pdfBoundary(box)
result := pdfClipPath{box: box, data: data, path: &[1]PathObject{{Boundary: boundary, AbbreviatedData: data, Rule: rule}}}
p.clipPaths.put(key, result, len(data)+len(boundary)+512)
return result, nil return result, nil
} }
+5 -1
View File
@@ -73,13 +73,17 @@ type pdfCompositeGeometry struct {
box Box box Box
} }
// pdfCompositeColorSampler 在单次绘制内复用完全相同参数的轴向颜色,不近似函数 // pdfCompositeColorSampler 复用渐变几何和相同轴向参数的颜色,不近似函数
type pdfCompositeColorSampler struct { type pdfCompositeColorSampler struct {
paint pdfgo.Paint paint pdfgo.Paint
space *pdfgo.ColorSpace space *pdfgo.ColorSpace
intent pdfgo.Name intent pdfgo.Name
conversion pdfgo.ColorConversion conversion pdfgo.ColorConversion
values map[float64][4]float64 values map[float64][4]float64
position pdfgo.GradientPosition
converter pdfgo.ColorConverter
prepared bool
converted bool
} }
// pdfCompositeCache 在单页内复用几何、图像分量及蒙版内容 // pdfCompositeCache 在单页内复用几何、图像分量及蒙版内容
+40 -6
View File
@@ -182,8 +182,10 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
var uniformValues [4]float64 var uniformValues [4]float64
uniformReady := false uniformReady := false
var sampler *pdfCompositeColorSampler var sampler *pdfCompositeColorSampler
if g := paint.Axial; node.image == nil && shading == nil && g != nil && g.Bounds == nil && g.Background == nil && if g := paint.Axial; node.image == nil && shading == nil && g != nil && g.Bounds == nil && g.Background == nil {
(g.Start.Y == g.End.Y && c.inverse[2] == 0 || g.Start.X == g.End.X && c.inverse[1] == 0) { sampler = &pdfCompositeColorSampler{paint: paint, space: space, intent: style.RenderingIntent, conversion: style.ColorConversion}
}
if g := paint.Radial; node.image == nil && shading == nil && g != nil && g.Bounds == nil && g.Background == nil {
sampler = &pdfCompositeColorSampler{paint: paint, space: space, intent: style.RenderingIntent, conversion: style.ColorConversion} sampler = &pdfCompositeColorSampler{paint: paint, space: space, intent: style.RenderingIntent, conversion: style.ColorConversion}
} }
geometry, err := c.geometry(node, outline) geometry, err := c.geometry(node, outline)
@@ -826,18 +828,50 @@ func pdfCompositeColor(paint pdfgo.Paint, point pdfgo.Point, space *pdfgo.ColorS
// 返回: [4]float64 混合分量, bool 是否着色, error 颜色错误 // 返回: [4]float64 混合分量, bool 是否着色, error 颜色错误
func (s *pdfCompositeColorSampler) color(point pdfgo.Point) ([4]float64, bool, error) { func (s *pdfCompositeColorSampler) color(point pdfgo.Point) ([4]float64, bool, error) {
g := s.paint.Axial g := s.paint.Axial
if g == nil || g.Bounds != nil || g.Background != nil || s.paint.Function != nil || s.paint.Tiling != nil { r := s.paint.Radial
if g == nil && r == nil || g != nil && (g.Bounds != nil || g.Background != nil) || r != nil && (r.Bounds != nil || r.Background != nil) || s.paint.Function != nil || s.paint.Tiling != nil {
return pdfCompositeColor(s.paint, point, s.space, s.intent, s.conversion) return pdfCompositeColor(s.paint, point, s.space, s.intent, s.conversion)
} }
position, visible := pdfShadingPosition(s.paint, point) if !s.prepared {
var err error
if g != nil {
s.position, err = g.PreparePosition()
} else {
s.position, err = r.PreparePosition()
}
if err != nil {
return [4]float64{}, false, err
}
s.prepared = true
}
position, visible := s.position.PositionAt(point)
if !visible { if !visible {
return [4]float64{}, false, nil return [4]float64{}, false, nil
} }
if values, ok := s.values[position]; ok { if values, ok := s.values[position]; ok {
return values, true, nil return values, true, nil
} }
values, visible, err := pdfCompositeColor(s.paint, point, s.space, s.intent, s.conversion) var values [4]float64
if err == nil && visible && len(s.values) < 2*pdfCompositeTileSize { var source *pdfgo.ColorSpace
var intent pdfgo.Name
var err error
if g != nil {
values, err = g.ValuesAt(position)
source, intent = g.Space, g.Intent
} else {
values, err = r.ValuesAt(position)
source, intent = r.Space, r.Intent
}
if err == nil {
if !s.converted {
s.converter, err = s.space.PrepareConversion(source, intent, s.conversion)
s.converted = err == nil
}
if err == nil {
values, err = s.converter.Convert(values[:source.Components()])
}
}
if err == nil && g != nil && len(s.values) < 2*pdfCompositeTileSize {
if s.values == nil { if s.values == nil {
s.values = make(map[float64][4]float64) s.values = make(map[float64][4]float64)
} }
+21 -2
View File
@@ -58,7 +58,18 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
} }
} }
result := c.acquirePixels(len(backdrop)) result := c.acquirePixels(len(backdrop))
complete := false
defer func() {
if !complete {
c.releasePixels(result)
}
}()
for i, pixel := range backdrop { for i, pixel := range backdrop {
if i%pdfCompositeTileSize == 0 {
if err := c.importer.ctx.Err(); err != nil {
return nil, err
}
}
result[i] = pdfCompositePixel{values: pixel.values, alpha: pixel.alpha} result[i] = pdfCompositePixel{values: pixel.values, alpha: pixel.alpha}
} }
candidate := c.acquirePixels(len(backdrop)) candidate := c.acquirePixels(len(backdrop))
@@ -83,6 +94,9 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
} }
pixels := candidate[:width*height] pixels := candidate[:width*height]
for y := range height { for y := range height {
if err := c.importer.ctx.Err(); err != nil {
return nil, err
}
for x := range width { for x := range width {
index := (top+y)*c.width + left + x index := (top+y)*c.width + left + x
initial := result[index] initial := result[index]
@@ -94,11 +108,15 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
} }
cell := pdfCompositor{importer: &pattern.local, box: Box{X: c.box.X + float64(left)*pixelStep - dx, Y: c.box.Y + float64(top)*pixelStep - dy, W: float64(width) * pixelStep, H: float64(height) * pixelStep}, cell := pdfCompositor{importer: &pattern.local, box: Box{X: c.box.X + float64(left)*pixelStep - dx, Y: c.box.Y + float64(top)*pixelStep - dy, W: float64(width) * pixelStep, H: float64(height) * pixelStep},
width: width, height: height, inverse: localInverse, cache: pattern.local.compositingCache(), scratch: c.scratch, masks: map[*pdfgo.SoftMask][]float64{}} width: width, height: height, inverse: localInverse, cache: pattern.local.compositingCache(), scratch: c.scratch, masks: map[*pdfgo.SoftMask][]float64{}}
if err := cell.draw(pattern.nodes, pixels, space); err != nil { err := cell.draw(pattern.nodes, pixels, space)
cell.releaseMasks()
if err != nil {
return nil, err return nil, err
} }
cell.releaseMasks()
for y := range height { for y := range height {
if err := c.importer.ctx.Err(); err != nil {
return nil, err
}
for x := range width { for x := range width {
index := (top+y)*c.width + left + x index := (top+y)*c.width + left + x
pdfCompositePatternCell(&result[index], backdrop[index], pixels[y*width+x], disjoint) pdfCompositePatternCell(&result[index], backdrop[index], pixels[y*width+x], disjoint)
@@ -120,6 +138,7 @@ func (c *pdfCompositor) tiling(paint pdfgo.Paint, backdrop []pdfCompositePixel,
} }
pixel.alpha = pixel.effect pixel.alpha = pixel.effect
} }
complete = true
return result, nil return result, nil
} }
+31 -4
View File
@@ -91,6 +91,9 @@ func (p *pdfImporter) meshTriangles(mesh *pdfgo.MeshGradient) ([]pdfMeshTriangle
matrix := p.matrix.Mul(mesh.Matrix) matrix := p.matrix.Mul(mesh.Matrix)
var triangles []pdfMeshTriangle var triangles []pdfMeshTriangle
for index, patch := range mesh.Patches { for index, patch := range mesh.Patches {
if err := p.ctx.Err(); err != nil {
return nil, err
}
for i := range patch.Points { for i := range patch.Points {
for j := range patch.Points[i] { for j := range patch.Points[i] {
patch.Points[i][j] = matrix.Apply(patch.Points[i][j]) patch.Points[i][j] = matrix.Apply(patch.Points[i][j])
@@ -121,6 +124,11 @@ func (p *pdfImporter) meshTriangles(mesh *pdfgo.MeshGradient) ([]pdfMeshTriangle
n := int(steps) n := int(steps)
row := make([]pdfMeshVertex, n+1) row := make([]pdfMeshVertex, n+1)
for i := range row { for i := range row {
if i%pdfCompositeTileSize == 0 {
if err := p.ctx.Err(); err != nil {
return nil, err
}
}
u := float64(i) / steps u := float64(i) / steps
row[i] = pdfMeshVertex{patch.PointAt(u, 0), u, 0} row[i] = pdfMeshVertex{patch.PointAt(u, 0), u, 0}
} }
@@ -158,10 +166,17 @@ func (p *pdfImporter) meshTriangles(mesh *pdfgo.MeshGradient) ([]pdfMeshTriangle
// 入参: mesh 原曲面, space 输出混合空间, conversion 设备转换函数 // 入参: mesh 原曲面, space 输出混合空间, conversion 设备转换函数
// 返回: []pdfShadingPixel 颜色及覆盖, error 几何或颜色错误 // 返回: []pdfShadingPixel 颜色及覆盖, error 几何或颜色错误
func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace, conversion pdfgo.ColorConversion) ([]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} key := pdfMeshKey{mesh, space, conversion}
if pixels, ok := c.meshes[key]; ok { if pixels, ok := c.meshes[key]; ok {
return pixels, nil return pixels, nil
} }
converter, err := space.PrepareConversion(mesh.Space, mesh.Intent, conversion)
if err != nil {
return nil, err
}
triangles, ok := c.cache.meshes[mesh] triangles, ok := c.cache.meshes[mesh]
if !ok { if !ok {
var err error var err error
@@ -200,6 +215,9 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace,
endX := int(math.Min(float64(w), math.Ceil(maxX))) endX := int(math.Min(float64(w), math.Ceil(maxX)))
endY := int(math.Min(float64(h), math.Ceil(maxY))) endY := int(math.Min(float64(h), math.Ceil(maxY)))
for y := int(math.Max(0, math.Floor(minY))); y < endY; y++ { 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++ { for x := int(math.Max(0, math.Floor(minX))); x < endX; x++ {
px, py := float64(x)+.5, float64(y)+.5 px, py := float64(x)+.5, float64(y)+.5
l0 := ((b.Y-d.Y)*(px-d.X) + (d.X-b.X)*(py-d.Y)) / denominator l0 := ((b.Y-d.Y)*(px-d.X) + (d.X-b.X)*(py-d.Y)) / denominator
@@ -221,7 +239,7 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace,
var background [4]float64 var background [4]float64
if mesh.Background != nil { if mesh.Background != nil {
var err error var err error
background, err = space.ConvertWith(mesh.Background[:mesh.Space.Components()], mesh.Space, mesh.Intent, conversion) background, err = converter.Convert(mesh.Background[:mesh.Space.Components()])
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -234,6 +252,7 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace,
return nil, fmt.Errorf("singular mesh bounds transform") return nil, fmt.Errorf("singular mesh bounds transform")
} }
} }
colors := make(map[[4]float64][4]float64)
for y := 0; y < h; y++ { for y := 0; y < h; y++ {
if err := c.importer.ctx.Err(); err != nil { if err := c.importer.ctx.Err(); err != nil {
return nil, err return nil, err
@@ -256,9 +275,17 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace,
if err != nil { if err != nil {
return nil, err return nil, err
} }
values, err = space.ConvertWith(values[:mesh.Space.Components()], mesh.Space, mesh.Intent, conversion) if cached, ok := colors[values]; ok {
if err != nil { values = cached
return nil, err } else {
source := values
values, err = converter.Convert(values[:mesh.Space.Components()])
if err != nil {
return nil, err
}
if len(colors) < 2*pdfCompositeTileSize {
colors[source] = values
}
} }
} }
pixel := &pixels[(y/samples)*c.width+x/samples] pixel := &pixels[(y/samples)*c.width+x/samples]
+128
View File
@@ -0,0 +1,128 @@
// 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"
"reflect"
"github.com/xiaoqidun/pdfgo"
)
// pdfPatternCellLimit 限制单页图案矢量快照的估算字节数
const pdfPatternCellLimit = 8 << 20
// pdfPatternCellKey 隔离图案来源、基色、坐标、精度与目标文档资源
type pdfPatternCellKey struct {
source *pdfgo.TilingPattern
base pdfgo.Paint
matrix pdfgo.Matrix
dpi float64
editor *Editor
}
// pdfPatternInstanceRange 收缩已知路径及图片的实例范围,未知边界保留完整范围
// 入参: columns 横向实例数, rows 纵向实例数, bounds 对象边界, width 单元宽, height 单元高, xstep 横向步距, ystep 纵向步距
// 返回: int 左界, int 右界, int 上界, int 下界
func pdfPatternInstanceRange(columns, rows float64, bounds []Box, width, height, xstep, ystep float64) (int, int, int, int) {
left, right, top, bottom := 0.0, 1-columns, 0.0, 1-rows
for _, box := range bounds {
if box.W <= 0 || box.H <= 0 || !finite(box.X) || !finite(box.Y) || !finite(box.X+box.W) || !finite(box.Y+box.H) {
return 1 - int(columns), 0, 1 - int(rows), 0
}
left = math.Min(left, math.Max(1-columns, math.Ceil(-(box.X+box.W)/xstep)-1))
right = math.Max(right, math.Min(0, math.Floor((width-box.X)/xstep)+1))
top = math.Min(top, math.Max(1-rows, math.Ceil(-(box.Y+box.H)/ystep)-1))
bottom = math.Max(bottom, math.Min(0, math.Floor((height-box.Y)/ystep)+1))
}
return int(left), int(right), int(top), int(bottom)
}
// tilingPattern 有界复用矢量图案,返回可独立编辑的对象副本
// 入参: source 平铺图案, base 无色图案基色
// 返回: *Pattern OFD图案, error 转换或取消错误
func (p *pdfImporter) tilingPattern(source *pdfgo.TilingPattern, base pdfgo.Paint) (*Pattern, error) {
if err := p.ctx.Err(); err != nil {
return nil, err
}
if p.resolveFile != nil || p.resolveReference != nil {
return p.buildTilingPattern(source, base)
}
if p.patternCells == nil {
p.patternCells = &renderCache[pdfPatternCellKey, *Pattern]{limit: pdfPatternCellLimit}
}
key := pdfPatternCellKey{source: source, matrix: p.matrix, dpi: p.rasterDPI, editor: p.editor}
if source.PaintType == 2 {
base.Tiling, base.Axial, base.Radial, base.Mesh, base.Function, base.Shading = nil, nil, nil, nil, nil, nil
base.Alpha = 1
key.base = base
}
if cached, ok := p.patternCells.get(key); ok {
return cloneEditorData(cached), p.ctx.Err()
}
pattern, err := p.buildTilingPattern(source, base)
if err != nil {
return nil, err
}
if err := p.ctx.Err(); err != nil {
return nil, err
}
cost := pdfPatternCellCost(reflect.ValueOf(pattern), p.patternCells.limit, 0)
if cost <= p.patternCells.limit {
p.patternCells.put(key, cloneEditorData(pattern), cost)
}
return pattern, p.ctx.Err()
}
// pdfPatternCellCost 有界估算公开数据的快照成本,不扫描共享的私有排版来源
// 入参: value 对象值, budget 剩余预算, depth 嵌套深度
// 返回: int 字节成本,超预算时返回预算加一
func pdfPatternCellCost(value reflect.Value, budget, depth int) int {
if depth > 128 || budget < 0 {
return budget + 1
}
cost := int(value.Type().Size())
if cost > budget {
return budget + 1
}
switch value.Kind() {
case reflect.String:
if value.Len() > budget-cost {
return budget + 1
}
cost += value.Len()
case reflect.Pointer:
if !value.IsNil() {
cost += pdfPatternCellCost(value.Elem(), budget-cost, depth+1)
}
case reflect.Slice, reflect.Array:
for i := range value.Len() {
cost += pdfPatternCellCost(value.Index(i), budget-cost, depth+1)
if cost > budget {
return budget + 1
}
}
case reflect.Struct:
for i := range value.NumField() {
if value.Type().Field(i).IsExported() {
cost += pdfPatternCellCost(value.Field(i), budget-cost, depth+1)
if cost > budget {
return budget + 1
}
}
}
}
return cost
}
+37 -36
View File
@@ -17,7 +17,6 @@ package ofdgo
import ( import (
"fmt" "fmt"
"image" "image"
"math"
"github.com/xiaoqidun/pdfgo" "github.com/xiaoqidun/pdfgo"
) )
@@ -242,6 +241,22 @@ func (c *pdfCompositor) gradient(paint pdfgo.Paint, space *pdfgo.ColorSpace, nod
return make([]pdfShadingPixel, c.width*c.height), nil return make([]pdfShadingPixel, c.width*c.height), nil
} }
} }
var position pdfgo.GradientPosition
if paint.Axial != nil || paint.Radial != nil {
var err error
if paint.Axial != nil {
position, err = paint.Axial.PreparePosition()
} else {
position, err = paint.Radial.PreparePosition()
}
if err != nil {
return nil, err
}
}
converter, err := space.PrepareConversion(source, intent, conversion)
if err != nil {
return nil, err
}
step := 25.4 / c.importer.rasterDPI step := 25.4 / c.importer.rasterDPI
pixels := make([]pdfShadingPixel, c.width*c.height) pixels := make([]pdfShadingPixel, c.width*c.height)
for y := range c.height { for y := range c.height {
@@ -283,11 +298,22 @@ func (c *pdfCompositor) gradient(paint pdfgo.Paint, space *pdfgo.ColorSpace, nod
if visible { if visible {
values, err = g.ValuesAt(local) values, err = g.ValuesAt(local)
if err == nil { if err == nil {
values, err = space.ConvertWith(values[:source.Components()], source, intent, conversion) values, err = converter.Convert(values[:source.Components()])
} }
} }
} else { } else {
values, visible, err = pdfCompositeColor(paint, point, space, intent, conversion) var parameter float64
parameter, visible = position.PositionAt(point)
if visible {
if paint.Axial != nil {
values, err = paint.Axial.ValuesAt(parameter)
} else {
values, err = paint.Radial.ValuesAt(parameter)
}
if err == nil {
values, err = converter.Convert(values[:source.Components()])
}
}
} }
if err != nil { if err != nil {
return nil, err return nil, err
@@ -359,40 +385,15 @@ func pdfShadingClippedNode(node pdfCompositeNode, box pdfgo.Rectangle, matrix pd
// 入参: paint 渐变画刷, point 页面坐标 // 入参: paint 渐变画刷, point 页面坐标
// 返回: float64 归一化参数, bool 是否着色 // 返回: float64 归一化参数, bool 是否着色
func pdfShadingPosition(paint pdfgo.Paint, point pdfgo.Point) (float64, bool) { func pdfShadingPosition(paint pdfgo.Paint, point pdfgo.Point) (float64, bool) {
var geometry pdfgo.GradientPosition
var err error
if g := paint.Axial; g != nil { if g := paint.Axial; g != nil {
dx, dy := g.End.X-g.Start.X, g.End.Y-g.Start.Y geometry, err = g.PreparePosition()
t := ((point.X-g.Start.X)*dx + (point.Y-g.Start.Y)*dy) / (dx*dx + dy*dy) } else {
return t, finite(t) && (t >= 0 || g.Extend[0]) && (t <= 1 || g.Extend[1]) geometry, err = paint.Radial.PreparePosition()
} }
g := paint.Radial if err != nil {
if g.Matrix != (pdfgo.Matrix{}) { return 0, false
inverse, ok := g.Matrix.Inverse()
if !ok {
return 0, false
}
point = inverse.Apply(point)
} }
dx, dy, dr := g.End.X-g.Start.X, g.End.Y-g.Start.Y, g.EndRadius-g.StartRadius return geometry.PositionAt(point)
px, py := point.X-g.Start.X, point.Y-g.Start.Y
a, b, c := dx*dx+dy*dy-dr*dr, -2*(px*dx+py*dy+g.StartRadius*dr), px*px+py*py-g.StartRadius*g.StartRadius
roots := [2]float64{math.NaN(), math.NaN()}
if a == 0 {
if b != 0 {
roots[0] = -c / b
}
} else if discriminant := b*b - 4*a*c; discriminant >= 0 {
q := -.5 * (b + math.Copysign(math.Sqrt(discriminant), b))
if q == 0 {
roots[0] = -b / (2 * a)
} else {
roots[0], roots[1] = q/a, c/q
}
}
t := math.Inf(-1)
for _, root := range roots {
if finite(root) && g.StartRadius+root*dr >= 0 && (root >= 0 || g.Extend[0]) && (root <= 1 || g.Extend[1]) && root > t {
t = root
}
}
return t, finite(t)
} }