feat(渲染编辑): 完善渲染编辑验签并优化性能
build.yaml / build (push) Successful in 45s

This commit is contained in:
xiaoqidun committed 2026-10-02 13:08:34 +08:00
1 parent 9c5ce1d66f
commit ac198f3351
56 files changed
+3029 -825

No files matched your search

+233 -48
View File
@@ -26,11 +26,13 @@ import (
// pdfCompositeNode 保留局部合成所需的原始图元和透明组层级
type pdfCompositeNode struct {
path *pdfgo.PathMark
text *pdfgo.TextMark
image *pdfgo.ImageMark
group *pdfgo.GroupMark
children []pdfCompositeNode
path *pdfgo.PathMark
text *pdfgo.TextMark
image *pdfgo.ImageMark
group *pdfgo.GroupMark
children []pdfCompositeNode
textObject *pdfgo.TextObject
glyph *pdfgo.TextMark
}
// pdfCompositePixel 保存非预乘源空间分量及累计透明度
@@ -43,13 +45,16 @@ type pdfCompositePixel struct {
// pdfCompositor 在指定局部区域中按源颜色空间合成PDF图元
type pdfCompositor struct {
importer *pdfImporter
box Box
width, height int
inverse pdfgo.Matrix
cache *pdfCompositeCache
masks map[*pdfgo.SoftMask][]float64
meshes map[pdfMeshKey][]pdfMeshPixel
importer *pdfImporter
box Box
width, height int
inverse pdfgo.Matrix
cache *pdfCompositeCache
masks map[*pdfgo.SoftMask][]float64
meshes map[pdfMeshKey][]pdfShadingPixel
gradients map[pdfGradientKey][]pdfShadingPixel
transfers []*pdfgo.TransferFunction
transferBlocked bool
}
// pdfCompositeKey 区分图元的填充与描边几何
@@ -68,11 +73,16 @@ type pdfCompositeGeometry struct {
// pdfCompositeCache 在单页内复用几何、图像分量及蒙版内容
type pdfCompositeCache struct {
geometry map[pdfCompositeKey]pdfCompositeGeometry
images map[*pdfgo.Image]*pdfgo.ImageComponents
masks map[*pdfgo.SoftMask][]pdfCompositeNode
meshes map[*pdfgo.MeshGradient][]pdfMeshTriangle
patterns map[pdfgo.Paint]*pdfCompositePattern
geometry map[pdfCompositeKey]pdfCompositeGeometry
images map[*pdfgo.Image]*pdfgo.ImageComponents
imageMatrix map[*pdfgo.ImageMark]pdfgo.Matrix
masks map[*pdfgo.SoftMask][]pdfCompositeNode
meshes map[*pdfgo.MeshGradient][]pdfMeshTriangle
patterns map[pdfgo.Paint]*pdfCompositePattern
shadings map[pdfGradientKey]pdfCompositeGeometry
transfers map[pdfTransferKey]*pdfgo.TransferFunction
textDisjoint map[*pdfgo.GroupMark]bool
groups map[*pdfgo.GroupMark]Box
}
// compositingCache 按需创建单页合成缓存,页面切换时由导入器释放
@@ -95,6 +105,21 @@ func (c *pdfCompositor) coverage(node pdfCompositeNode, stroke bool) (image.Imag
return nil, nil
}
}
geometry, err := c.geometry(node, stroke)
if err != nil {
return nil, err
}
b := geometry.box
if geometry.scene == nil || b.X >= c.box.X+c.box.W || b.Y >= c.box.Y+c.box.H || b.X+b.W <= c.box.X || b.Y+b.H <= c.box.Y {
return nil, nil
}
return c.importer.renderGroupScene(geometry.scene, c.box)
}
// geometry 缓存图元或组的实际覆盖场景与像素对齐边界
// 入参: node 图元或组, stroke 是否描边
// 返回: pdfCompositeGeometry 覆盖场景, error 编译错误
func (c *pdfCompositor) geometry(node pdfCompositeNode, stroke bool) (pdfCompositeGeometry, error) {
key := pdfCompositeKey{path: node.path, text: node.text, image: node.image, stroke: stroke}
geometry, ok := c.cache.geometry[key]
if !ok {
@@ -102,15 +127,82 @@ func (c *pdfCompositor) coverage(node pdfCompositeNode, stroke bool) (image.Imag
var err error
geometry.scene, geometry.box, err = c.importer.compileGroup(func(local *pdfImporter) error { return node.coverage(local, stroke) })
if err != nil {
return nil, err
return geometry, err
}
c.cache.geometry[key] = geometry
}
b := geometry.box
if geometry.scene == nil || b.X >= c.box.X+c.box.W || b.Y >= c.box.Y+c.box.H || b.X+b.W <= c.box.X || b.Y+b.H <= c.box.Y {
return nil, nil
return geometry, nil
}
// groupBounds 缓存组内实际覆盖范围,外部字体沿用描述符保守边界
// 入参: node 透明组
// 返回: Box 组覆盖范围, error 几何编译错误
func (c *pdfCompositor) groupBounds(node pdfCompositeNode) (Box, error) {
if box, found := c.cache.groups[node.group]; found {
return box, nil
}
return c.importer.renderGroupScene(geometry.scene, c.box)
var box Box
for _, child := range node.children {
if err := c.importer.ctx.Err(); err != nil {
return Box{}, err
}
if child.group != nil {
bounds, err := c.groupBounds(child)
if err != nil {
return Box{}, err
}
box = unionTextBox(box, bounds)
continue
}
bounds, err := c.markBounds(child)
if err != nil {
return Box{}, err
}
box = unionTextBox(box, bounds)
}
if c.cache.groups == nil {
c.cache.groups = make(map[*pdfgo.GroupMark]Box)
}
c.cache.groups[node.group] = box
return box, nil
}
// markBounds 合并图元的填充与描边范围,外部字体不在排除阶段加载
// 入参: node 基本图元
// 返回: Box 图元覆盖范围, error 几何编译错误
func (c *pdfCompositor) markBounds(node pdfCompositeNode) (Box, error) {
_, fill, stroke := node.style()
var box Box
for _, outline := range []bool{false, true} {
if outline && !stroke || !outline && !fill {
continue
}
if node.text != nil && len(node.text.Font.Program) == 0 && node.text.Font.BoundingBox != nil {
box = unionTextBox(box, c.importer.compositeTextBounds(*node.text, outline))
continue
}
geometry, err := c.geometry(node, outline)
if err != nil {
return Box{}, err
}
box = unionTextBox(box, geometry.box)
}
return box, nil
}
// pixelBounds 将页面范围映射到当前块,保留一像素余量供浮点边界与抗锯齿使用
// 入参: box 页面范围
// 返回: image.Rectangle 当前块内的像素范围
func (c *pdfCompositor) pixelBounds(box Box) image.Rectangle {
if box.W <= 0 || box.H <= 0 {
return image.Rectangle{}
}
step := 25.4 / c.importer.rasterDPI
x0 := int(math.Max(0, math.Min(float64(c.width), math.Floor((box.X-c.box.X)/step)-1)))
y0 := int(math.Max(0, math.Min(float64(c.height), math.Floor((box.Y-c.box.Y)/step)-1)))
x1 := int(math.Max(0, math.Min(float64(c.width), math.Ceil((box.X+box.W-c.box.X)/step)+1)))
y1 := int(math.Max(0, math.Min(float64(c.height), math.Ceil((box.Y+box.H-c.box.Y)/step)+1)))
return image.Rect(x0, y0, x1, y1)
}
// compositeTextBounds 按字体描述符计算保守边界,仅用于排除不相交的外部字体
@@ -158,35 +250,59 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
if err := p.halftone(mark.Style); err != nil {
return err
}
if err := p.resolveTransfer(&mark.Style); err != nil {
return err
}
nodes = append(nodes, pdfCompositeNode{path: &mark})
return nil
}
v.Text = func(mark pdfgo.TextMark) error {
if err := p.halftone(mark.Style); err != nil {
return err
}
var text []pdfCompositeNode
if mark.Font.Subtype != "Type3" {
nodes = append(nodes, pdfCompositeNode{text: &mark})
return nil
}
if mark.Clip != nil {
return &pdfgo.UnsupportedError{Feature: "Type3 text clipping"}
}
glyphs := active[mark.Font]
if glyphs == nil {
glyphs = make(map[string]bool)
active[mark.Font] = glyphs
}
for index, glyph := range mark.Glyphs {
if glyphs[glyph.Name] {
return fmt.Errorf("recursive PDF Type3 glyph %q", glyph.Name)
}
glyphs[glyph.Name] = true
err := p.reader.WalkType3Glyph(p.ctx, mark, index, v)
delete(glyphs, glyph.Name)
if err != nil {
if err := p.halftone(mark.Style); err != nil {
return err
}
if err := p.resolveTransfer(&mark.Style); err != nil {
return err
}
if len(mark.Glyphs) > 1 && (mark.Object != nil && !mark.Object.Knockout || mark.Style.AlphaIsShape || mark.Style.Fill.Alpha != 1 || mark.Style.Stroke.Alpha != 1 || mark.Style.SoftMask != nil || !pdfNormalBlend(mark.Style.BlendMode) || mark.Mode%4 == 1 || mark.Mode%4 == 2) {
for i := range mark.Glyphs {
glyph := mark
glyph.Glyphs, glyph.Positions, glyph.Clip = mark.Glyphs[i:i+1], mark.Positions[i:i+1], nil
text = append(text, pdfCompositeNode{text: &glyph})
}
} else {
text = append(text, pdfCompositeNode{text: &mark})
}
} else {
glyphs := active[mark.Font]
if glyphs == nil {
glyphs = make(map[string]bool)
active[mark.Font] = glyphs
}
for index, glyph := range mark.Glyphs {
if glyphs[glyph.Name] {
return fmt.Errorf("recursive PDF Type3 glyph %q", glyph.Name)
}
glyphs[glyph.Name] = true
children, err := collect(func(visitor pdfgo.Visitor) error { return p.reader.WalkType3Glyph(p.ctx, mark, index, visitor) })
delete(glyphs, glyph.Name)
if err != nil {
return err
}
part := mark
part.Glyphs, part.Positions = mark.Glyphs[index:index+1], mark.Positions[index:index+1]
text = append(text, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1}, children: children, glyph: &part})
}
}
if mark.Object != nil && mark.Object.Knockout {
if len(nodes) == 0 || nodes[len(nodes)-1].textObject != mark.Object {
nodes = append(nodes, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1, Knockout: true}, textObject: mark.Object})
}
node := &nodes[len(nodes)-1]
node.children = append(node.children, text...)
} else {
nodes = append(nodes, text...)
}
return nil
}
@@ -194,6 +310,9 @@ func (p *pdfImporter) collectCompositeNodes(walk func(pdfgo.Visitor) error) ([]p
if err := p.halftone(mark.Style); err != nil {
return err
}
if err := p.resolveTransfer(&mark.Style); err != nil {
return err
}
nodes = append(nodes, pdfCompositeNode{image: &mark})
return nil
}
@@ -236,7 +355,24 @@ func (p *pdfImporter) compositePage(space *pdfgo.ColorSpace, walk func(pdfgo.Vis
}
}
p.compositeNodes = nil
p.transferBackdrop = false
p.compositeSpace = space
if pdfCompositeHasTransfer(nodes) {
if p.warning == nil {
return &pdfgo.UnsupportedError{Feature: "device transfer function rasterization"}
}
if overprint, err := p.processOverprint(nodes); err != nil {
return err
} else if overprint {
return &pdfgo.UnsupportedError{Feature: "per-colorant transfer function overprinting"}
}
if err := p.compositeRegion(nil, pdfCompositeNode{group: &pdfgo.GroupMark{Alpha: 1, ColorSpace: space}, children: nodes}, space, true); err != nil {
return err
}
p.compositeNodes = nodes
p.transferBackdrop = true
return nil
}
return p.compositeObjects(nodes)
}
@@ -250,6 +386,17 @@ func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error {
if err != nil {
return fmt.Errorf("inspect graphic %d: %w", i+1, err)
}
transfer := pdfCompositeHasTransfer([]pdfCompositeNode{node})
if p.transferBackdrop || transfer {
direct = false
for _, source := range [][]pdfCompositeNode{p.compositeNodes, {node}} {
if overprint, err := p.processOverprint(source); err != nil {
return err
} else if overprint {
return &pdfgo.UnsupportedError{Feature: "per-colorant transfer function overprinting"}
}
}
}
if direct {
if err := node.emit(p.visitor()); err != nil {
return fmt.Errorf("convert graphic %d: %w", i+1, err)
@@ -266,6 +413,7 @@ func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error {
}
}
p.compositeNodes = append(p.compositeNodes, node)
p.transferBackdrop = p.transferBackdrop || transfer
}
return p.flushPath()
}
@@ -276,7 +424,7 @@ func (p *pdfImporter) compositeObjects(nodes []pdfCompositeNode) error {
func (n pdfCompositeNode) opaque(space *pdfgo.ColorSpace) bool {
if n.group != nil {
g := n.group
if g.Knockout || g.Alpha != 1 || g.SoftMask != nil || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.Equal(space) {
if g.Knockout && n.textObject == nil || g.Alpha != 1 || g.SoftMask != nil || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.Equal(space) {
return false
}
for _, child := range n.children {
@@ -309,7 +457,8 @@ func (n pdfCompositeNode) opaque(space *pdfgo.ColorSpace) bool {
// 返回: bool 是否可直接转换
func (n pdfCompositeNode) direct() bool {
if g := n.group; g != nil {
if g.Knockout || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.SRGBEquivalent() {
single := n.textObject != nil && len(n.children) == 1 && (n.children[0].glyph != nil || n.children[0].text != nil && len(n.children[0].text.Glyphs) == 1)
if g.Knockout && !single || !pdfNormalBlend(g.BlendMode) || g.ColorSpace != nil && !g.ColorSpace.SRGBEquivalent() {
return false
}
for _, child := range n.children {
@@ -320,6 +469,9 @@ func (n pdfCompositeNode) direct() bool {
return true
}
s, fill, stroke := n.style()
if fill && stroke && (s.Fill.Alpha != 1 || s.Stroke.Alpha != 1 || s.SoftMask != nil) {
return false
}
if n.image != nil && s.FillOverprint {
return false
}
@@ -352,6 +504,14 @@ func (n pdfCompositeNode) emit(visitor pdfgo.Visitor) error {
if n.image != nil {
return visitor.Image(*n.image)
}
if n.textObject != nil || n.glyph != nil {
for _, child := range n.children {
if err := child.emit(visitor); err != nil {
return err
}
}
return nil
}
return visitor.Group(*n.group, func(v pdfgo.Visitor) error {
for _, child := range n.children {
if err := child.emit(v); err != nil {
@@ -391,6 +551,7 @@ func (n pdfCompositeNode) coverage(p *pdfImporter, stroke bool) error {
style, _, _ := n.style()
style.Fill, style.Stroke = pdfgo.Paint{RGB: [3]float64{1, 1, 1}, Alpha: 1}, pdfgo.Paint{RGB: [3]float64{1, 1, 1}, Alpha: 1}
style.SoftMask, style.BlendMode = nil, "Normal"
style.Transfer = nil
style.FillOverprint, style.StrokeOverprint = false, false
if n.path != nil {
mark := *n.path
@@ -452,11 +613,21 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
cache := p.compositingCache()
step := 25.4 / p.rasterDPI
buffer := make([]pdfCompositePixel, min(256, w)*min(256, h))
var transfers []*pdfgo.TransferFunction
var previousTransfers []*pdfgo.TransferFunction
if pdfCompositeHasTransfer(backdrop) || pdfCompositeHasTransfer([]pdfCompositeNode{node}) {
transfers = make([]*pdfgo.TransferFunction, len(buffer))
previousTransfers = make([]*pdfgo.TransferFunction, len(buffer))
}
for y := 0; y < h; y += 256 {
for x := 0; x < w; x += 256 {
runtime.Gosched()
width, height := min(256, w-x), min(256, h-y)
c := pdfCompositor{importer: p, box: Box{X: box.X + float64(x)*step, Y: box.Y + float64(y)*step, W: float64(width) * step, H: float64(height) * step}, width: width, height: height, inverse: inverse, cache: cache, masks: map[*pdfgo.SoftMask][]float64{}}
if transfers != nil {
c.transfers = transfers[:width*height]
clear(c.transfers)
}
pixels := buffer[:width*height]
clear(pixels)
if err := c.draw(backdrop, pixels, space); err != nil {
@@ -465,14 +636,21 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
for i := range pixels {
pixels[i].effect = 0
}
copy(previousTransfers, c.transfers)
if err := c.draw([]pdfCompositeNode{node}, pixels, space); err != nil {
return err
}
for i, pixel := range pixels {
if pixel.effect == 0 {
if pixel.effect == 0 && (c.transfers == nil || c.transfers[i] == previousTransfers[i]) {
continue
}
rgb, err := pdfCompositeOutputColor(space, pixel.values)
var rgb [3]float64
var err error
if c.transfers != nil {
rgb, err = space.RGB(pixel.values[:space.Components()], "RelativeColorimetric")
} else {
rgb, err = pdfCompositeOutputColor(space, pixel.values)
}
if err != nil {
return err
}
@@ -483,6 +661,13 @@ func (p *pdfImporter) compositeRegion(backdrop []pdfCompositeNode, node pdfCompo
}
alpha = 1
}
if c.transfers != nil && c.transfers[i] != nil {
values, err := c.transfers[i].Apply(rgb[:], "DeviceRGB", "")
if err != nil {
return err
}
copy(rgb[:], values[:3])
}
output.SetNRGBA(x+i%width, y+i/width, color.NRGBA{R: uint8(math.Round(rgb[0] * 255)), G: uint8(math.Round(rgb[1] * 255)), B: uint8(math.Round(rgb[2] * 255)), A: uint8(math.Round(alpha * 255))})
}
}