mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
1 parent
9c5ce1d66f
commit
ac198f3351
56 files changed
+3029
-825
No files matched your search
+233
-48
@@ -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))})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user