refactor(重构提升): 修复裁剪并优化转换渲染
build.yaml / build (push) Successful in 47s

This commit is contained in:
xiaoqidun committed 2026-10-06 23:09:49 +08:00
1 parent 619899e297
commit e8344e6387
27 files changed
+690 -86

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/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1
github.com/xiaoqidun/pdfgo v0.0.0-20261006094416-cd6540bbc9f7
github.com/xiaoqidun/pdfgo v0.0.0-20261006150546-e717d961d710
golang.org/x/crypto v0.57.0
golang.org/x/image v0.46.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/xiaoqidun/jbig2 v1.1.1 h1:SjliS9ZO0e9HX5oYUfBuXrfFsXhWricrSz+XY4zORDI=
github.com/xiaoqidun/jbig2 v1.1.1/go.mod h1:8QonVWtENmh2kc6l+leqeGl7psb79DWlHDkCpdChjfQ=
github.com/xiaoqidun/pdfgo v0.0.0-20261006094416-cd6540bbc9f7 h1:ZvZaxIYiPd8BlcGDztowwmSHT1vFQK3mV3RBS2p27Cs=
github.com/xiaoqidun/pdfgo v0.0.0-20261006094416-cd6540bbc9f7/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
github.com/xiaoqidun/pdfgo v0.0.0-20261006150546-e717d961d710 h1:Czwt4LQ2IVRFS8+kb+WIsm2u9MP4AsT4dLb/9ahg7Pw=
github.com/xiaoqidun/pdfgo v0.0.0-20261006150546-e717d961d710/go.mod h1:4Pf0JO0BEf1uzRY+uZjlMjN/OPn3PYa0YMi6cqPPTLQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
+37 -5
View File
@@ -70,6 +70,7 @@ type Editor struct {
outlines []byte
encryption *encryptionState
output *outputOptimization
validatedPaths *renderCache[string, bool]
}
// editorResource 文档内嵌资源
@@ -948,8 +949,17 @@ func (v *editorValidation) validateObjectClips(clips *Clips) error {
return fmt.Errorf("clip must contain an area")
}
for _, area := range clip.Area {
if len(area.Path)+len(area.Text) == 0 || area.DrawParam != "" {
return fmt.Errorf("clip area requires paths or text without draw parameter references")
if len(area.Path)+len(area.Text) == 0 {
return fmt.Errorf("clip area requires paths or text")
}
if area.DrawParam != "" {
dp, err := v.editorDrawParam(area.DrawParam, make(map[string]bool))
if err != nil {
return err
}
if err := validateEditorStroke(PathObject{LineWidth: dp.LineWidth, Cap: dp.Cap, Join: dp.Join, MiterLimit: dp.MiterLimit, DashPattern: dp.DashPattern, DashOffset: dp.DashOffset}); err != nil {
return err
}
}
if area.CTM != "" {
if _, err := creationNumbers(area.CTM, 6); err != nil {
@@ -1194,23 +1204,36 @@ func creationDeltas(value string) error {
// 入参: value 路径数据
// 返回: error 错误信息
func creationPath(value string) error {
path, err := ParseGeometryPath(value)
if err == nil && len(path) == 0 {
count, err := walkGeometryPath(value, nil)
if err == nil && count == 0 {
return fmt.Errorf("empty path")
}
return err
}
// creationPath 在同次校验中复用相同路径的成功结果,不缓存错误或依赖可变资源
// creationPath 复用相同路径的成功校验,不缓存错误或依赖可变资源
// 入参: value 路径数据
// 返回: error 错误信息
func (v *editorValidation) creationPath(value string) error {
if v.paths[value] {
return nil
}
var cache *renderCache[string, bool]
valid := false
if v.Editor != nil {
cache = v.pathValidationCache()
valid, _ = cache.get(value)
}
if !valid {
if err := creationPath(value); err != nil {
return err
}
if cache != nil {
if cost := len(value) + 128; cost <= cache.limit {
cache.put(strings.Clone(value), true, cost)
}
}
}
if v.paths == nil || len(v.paths) >= editorPathValidationLimit {
v.paths = make(map[string]bool)
}
@@ -1218,6 +1241,15 @@ func (v *editorValidation) creationPath(value string) error {
return nil
}
// pathValidationCache 获取不依赖可变资源的路径语法缓存,按字节预算淘汰
// 返回: *renderCache[string, bool] 校验缓存
func (e *Editor) pathValidationCache() *renderCache[string, bool] {
if e.validatedPaths == nil {
e.validatedPaths = &renderCache[string, bool]{limit: 8 << 20}
}
return e.validatedPaths
}
// creationNumbers 解析创建接口的有限十进制数值序列
// 入参: value 数值序列, count 数值数量
// 返回: []float64 数值, error 错误信息
+1
View File
@@ -309,6 +309,7 @@ func (e *Editor) editDocuments(edit func(*Reader, []byte, *editorXML) (int, erro
next.OnWriteProgress, next.output = e.OnWriteProgress, e.output
next.encryption = e.encryption
next.backends, next.fontDirs, next.fontFS = e.backends, e.fontDirs, e.fontFS
next.validatedPaths = e.validatedPaths
*e = *next
}
e.recordTransaction(before)
+1
View File
@@ -161,6 +161,7 @@ func (e *Editor) restoreTransaction(state Editor) {
state.serial = max(e.serial, state.serial)
state.backends, state.fontDirs, state.fontFS = e.backends, e.fontDirs, e.fontFS
state.fontSourcesCache = e.fontSourcesCache
state.validatedPaths = e.validatedPaths
state.encryption, state.output, state.OnWriteProgress = e.encryption, e.output, e.OnWriteProgress
*e = state
}
+3 -3
View File
@@ -254,9 +254,9 @@ func editorXMLSupported(node *editorXML) bool {
allowed, children = "CodePosition CodeCount GlyphCount", "Glyphs"
case "Glyphs":
case "PathObject", "Path":
allowed, children = "Boundary CTM LineWidth MiterLimit Join Cap Rule DashPattern DashOffset Visible Stroke Fill Alpha", "StrokeColor FillColor AbbreviatedData"
allowed, children = "Boundary CTM DrawParam LineWidth MiterLimit Join Cap Rule DashPattern DashOffset Visible Stroke Fill Alpha", "StrokeColor FillColor AbbreviatedData"
if node.name.Local == "PathObject" {
allowed += " ID DrawParam"
allowed += " ID"
children += " Clips"
}
case "ImageObject":
@@ -270,7 +270,7 @@ func editorXMLSupported(node *editorXML) bool {
case "Clip":
children = "Area"
case "Area":
allowed, children = "CTM", "Path Text"
allowed, children = "CTM DrawParam", "Path Text"
case "AbbreviatedData":
default:
return false
+9
View File
@@ -214,5 +214,14 @@ func clipGeometry(geometry GeometryBackend, path GeometryPath, clip *GeometryPat
return path, nil
}
}
if rect, ok := geometryRectangleBounds(path); ok {
bounds, err := geometry.Bounds(*clip)
if err != nil {
return nil, err
}
if bounds.X >= rect.X && bounds.Y >= rect.Y && bounds.X+bounds.W <= rect.X+rect.W && bounds.Y+bounds.H <= rect.Y+rect.H {
return *clip, nil
}
}
return geometry.Combine(path, *clip, GeometryIntersect)
}
+66 -24
View File
@@ -51,18 +51,33 @@ type GeometrySegment struct {
// 入参: data 紧缩路径
// 返回: GeometryPath 独立路径, error 指令或参数错误
func ParseGeometryPath(data string) (GeometryPath, error) {
tokens := strings.Fields(data)
if len(tokens) == 0 {
return nil, nil
count, err := walkGeometryPath(data, nil)
if err != nil || count == 0 {
return nil, err
}
if tokens[0] != "M" && tokens[0] != "S" {
return nil, fmt.Errorf("path must start with M or S")
result := make(GeometryPath, 0, count)
_, err = walkGeometryPath(data, func(segment GeometrySegment) {
result = append(result, segment)
})
if err != nil {
return nil, err
}
var result GeometryPath
for i := 0; i < len(tokens); {
command := tokens[i]
i++
count := 0
return result, nil
}
// walkGeometryPath 逐指令解析紧缩路径,纯校验时不分配路径或词元列表
// 入参: data 紧缩路径, visit 指令访问器,nil仅校验
// 返回: int 指令数量, error 指令或参数错误
func walkGeometryPath(data string, visit func(GeometrySegment)) (int, error) {
var args [7]string
command := ""
count, read, segments := 0, 0, 0
for token := range strings.FieldsSeq(data) {
if command == "" {
if segments == 0 && token != "M" && token != "S" {
return 0, fmt.Errorf("path must start with M or S")
}
command, read = token, 0
switch command {
case "M", "S", "L":
count = 2
@@ -73,27 +88,51 @@ func ParseGeometryPath(data string) (GeometryPath, error) {
case "A":
count = 7
case "C":
count = 0
default:
return nil, fmt.Errorf("invalid path command %q", command)
return 0, fmt.Errorf("invalid path command %q", command)
}
if len(tokens)-i < count {
return nil, fmt.Errorf("missing arguments for path command %s", command)
} else {
args[read] = token
read++
}
if read != count {
continue
}
segment, err := geometryPathSegment(command, args[:count])
if err != nil {
return 0, err
}
if visit != nil {
visit(segment)
}
segments++
command = ""
}
if command != "" {
return 0, fmt.Errorf("missing arguments for path command %s", command)
}
return segments, nil
}
// geometryPathSegment 校验单条指令参数并保留原始数值精度
// 入参: command 标准指令, args 已按指令数量收集的参数
// 返回: GeometrySegment 路径段, error 参数错误
func geometryPathSegment(command string, args []string) (GeometrySegment, error) {
var values [7]float64
for j, token := range tokens[i : i+count] {
if command == "A" && (j == 3 || j == 4) && (token == "true" || token == "false") {
for i, token := range args {
if command == "A" && (i == 3 || i == 4) && (token == "true" || token == "false") {
if token == "true" {
values[j] = 1
values[i] = 1
}
continue
}
value, err := strconv.ParseFloat(token, 64)
if err != nil || !finite(value) || strings.ContainsAny(token, "xX_") {
return nil, fmt.Errorf("invalid number %q", token)
return GeometrySegment{}, fmt.Errorf("invalid number %q", token)
}
values[j] = value
values[i] = value
}
i += count
segment := GeometrySegment{}
switch command {
case "M", "S":
@@ -106,15 +145,13 @@ func ParseGeometryPath(data string) (GeometryPath, error) {
segment.Verb, segment.Control1, segment.Control2, segment.End = GeometryCubic, Point{values[0], values[1]}, Point{values[2], values[3]}, Point{values[4], values[5]}
case "A":
if values[0] < 0 || values[1] < 0 || values[3] != 0 && values[3] != 1 || values[4] != 0 && values[4] != 1 {
return nil, fmt.Errorf("invalid arc radii or flags")
return GeometrySegment{}, fmt.Errorf("invalid arc radii or flags")
}
segment = GeometrySegment{Verb: GeometryArc, RadiusX: values[0], RadiusY: values[1], Rotation: values[2], Large: values[3] != 0, Sweep: values[4] != 0, End: Point{values[5], values[6]}}
case "C":
segment.Verb = GeometryClose
}
result = append(result, segment)
}
return result, nil
return segment, nil
}
// SVG 将几何路径编码为SVG路径,不改变曲线或坐标精度
@@ -225,10 +262,15 @@ func (p GeometryPath) encode(svg bool) (string, error) {
// 入参: path 页面毫米路径
// 返回: PathObject 裁剪路径, error 无效指令错误
func geometryClipPath(path GeometryPath) (PathObject, error) {
box, err := path.Bounds()
if err != nil {
return PathObject{}, err
}
data, err := path.OFD()
if err != nil {
return PathObject{}, err
}
fill, stroke := true, false
return PathObject{Boundary: "0 0 1 1", Fill: &fill, Stroke: &stroke, AbbreviatedData: data}, nil
box.W, box.H = max(box.W, 1e-6), max(box.H, 1e-6)
return PathObject{Boundary: editorBoxString(box), CTM: TranslationMatrix(-box.X, -box.Y).String(), Fill: &fill, Stroke: &stroke, AbbreviatedData: data}, nil
}
+2 -2
View File
@@ -95,11 +95,11 @@ func NewInk(points []InkPoint, width float64, pressure bool) (PathObject, error)
path[i].End.X -= boundary.X
path[i].End.Y -= boundary.Y
}
object, err := geometryClipPath(path)
data, err := path.OFD()
if err != nil {
return PathObject{}, err
}
object.Boundary = editorBoxString(boundary)
object := PathObject{Boundary: editorBoxString(boundary), AbbreviatedData: data}
stroke := !filled
object.Fill, object.Stroke = &filled, &stroke
object.Cap, object.Join, object.LineWidth = "Round", "Round", width
+4
View File
@@ -523,6 +523,10 @@ func (p *pdfImporter) clips(paths []pdfgo.Path, origin Box) (*Clips, error) {
area.Text = append(area.Text, objects...)
}
}
if len(area.Path)+len(area.Text) == 0 {
fill, stroke := true, false
area.Path = []PathObject{{Boundary: "0 0 1 1", AbbreviatedData: "M 0 0 C", Fill: &fill, Stroke: &stroke}}
}
clips.Clip[index].Area = areas[index : index+1 : index+1]
}
return clips, nil
+2 -1
View File
@@ -100,7 +100,8 @@ func (p *pdfImporter) clipPath(path pdfgo.Path) (pdfClipPath, error) {
rule = "Even-Odd"
}
boundary := pdfBoundary(box)
result := pdfClipPath{box: box, data: data, path: &[1]PathObject{{Boundary: boundary, AbbreviatedData: data, Rule: rule}}}
fill, stroke := true, false
result := pdfClipPath{box: box, data: data, path: &[1]PathObject{{Boundary: boundary, AbbreviatedData: data, Rule: rule, Fill: &fill, Stroke: &stroke}}}
p.clipPaths.put(key, result, len(data)+len(boundary)+512)
return result, nil
}
+66 -7
View File
@@ -67,6 +67,12 @@ type pdfCompositeKey struct {
stroke bool
}
// pdfCompositeSceneKey 隔离不同图案坐标系,在单页内共享场景缓存预算
type pdfCompositeSceneKey struct {
owner *pdfCompositeCache
mark pdfCompositeKey
}
// pdfCompositeGeometry 保存后端中立的绘制命令及其页面边界
type pdfCompositeGeometry struct {
scene *RasterPage
@@ -88,7 +94,9 @@ type pdfCompositeColorSampler struct {
// pdfCompositeCache 在单页内复用几何、图像分量及蒙版内容
type pdfCompositeCache struct {
geometry map[pdfCompositeKey]pdfCompositeGeometry
geometry *renderCache[pdfCompositeSceneKey, pdfCompositeGeometry]
clips *renderCache[[32]byte, GeometryPath]
bounds map[pdfCompositeKey]Box
images map[*pdfgo.Image]*pdfgo.ImageComponents
imageMatrix map[*pdfgo.ImageMark]pdfgo.Matrix
masks map[*pdfgo.SoftMask][]pdfCompositeNode
@@ -106,16 +114,38 @@ type pdfCompositeCache struct {
// 返回: *pdfCompositeCache 当前页面缓存
func (p *pdfImporter) compositingCache() *pdfCompositeCache {
if p.compositeCache == nil {
p.compositeCache = &pdfCompositeCache{geometry: map[pdfCompositeKey]pdfCompositeGeometry{}, 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][]pdfMeshTriangle{}}
}
return p.compositeCache
}
// geometryCache 限制完整覆盖场景的保留量,边界独立保存以便排除无交集图元
// 返回: *renderCache[pdfCompositeSceneKey, pdfCompositeGeometry] 单页场景缓存
func (c *pdfCompositeCache) geometryCache() *renderCache[pdfCompositeSceneKey, pdfCompositeGeometry] {
if c.geometry == nil {
c.geometry = &renderCache[pdfCompositeSceneKey, pdfCompositeGeometry]{limit: 64 << 20}
}
return c.geometry
}
// clipCache 限制页面及嵌套图案共享的只读裁剪轮廓
// 返回: *renderCache[[32]byte, GeometryPath] 裁剪缓存
func (c *pdfCompositeCache) clipCache() *renderCache[[32]byte, GeometryPath] {
if c.clips == nil {
c.clips = &renderCache[[32]byte, GeometryPath]{limit: 8 << 20}
}
return c.clips
}
// coverage 复用已编译的覆盖场景,跳过与当前区域无交集的图元
// 几何编译前让出调度,避免单线程运行时积累已失效的临时对象
// 入参: node 图元, stroke 是否描边
// 返回: image.Image 当前区域覆盖或空值, error 编译或渲染错误
func (c *pdfCompositor) coverage(node pdfCompositeNode, stroke bool) (image.Image, error) {
key := pdfCompositeKey{path: node.path, text: node.text, image: node.image, stroke: stroke}
if b, ok := c.cache.bounds[key]; ok && (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
}
if node.text != nil && len(node.text.Font.Program) == 0 && node.text.Font.BoundingBox != nil {
b := c.importer.compositeTextBounds(*node.text, stroke)
if 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 {
@@ -138,19 +168,48 @@ func (c *pdfCompositor) coverage(node pdfCompositeNode, stroke bool) (image.Imag
// 返回: 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]
cache := c.cache.geometryCache()
sceneKey := pdfCompositeSceneKey{owner: c.cache, mark: key}
geometry, ok := cache.get(sceneKey)
if !ok {
runtime.Gosched()
var err error
geometry.scene, geometry.box, err = c.importer.compileGroup(func(local *pdfImporter) error { return node.coverage(local, stroke) })
geometry.scene, geometry.box, err = c.importer.compileCoverage(node, stroke)
if err != nil {
return geometry, err
}
c.cache.geometry[key] = geometry
if c.cache.bounds == nil {
c.cache.bounds = make(map[pdfCompositeKey]Box)
}
c.cache.bounds[key] = geometry.box
cost := 256
if geometry.scene != nil {
for _, command := range geometry.scene.Commands {
part := rasterCommandCost(command)
if part <= 0 || part > cache.limit-cost {
cost = cache.limit + 1
break
}
cost += part
}
}
cache.put(sceneKey, geometry, cost)
}
return geometry, nil
}
// geometryBounds 复用已度量边界,场景淘汰后不因范围判断而重新编译
// 入参: node 图元, stroke 是否描边
// 返回: Box 覆盖范围, error 编译错误
func (c *pdfCompositor) geometryBounds(node pdfCompositeNode, stroke bool) (Box, error) {
key := pdfCompositeKey{path: node.path, text: node.text, image: node.image, stroke: stroke}
if box, ok := c.cache.bounds[key]; ok {
return box, nil
}
geometry, err := c.geometry(node, stroke)
return geometry.box, err
}
// groupBounds 缓存组内实际覆盖范围,外部字体沿用描述符保守边界
// 入参: node 透明组
// 返回: Box 组覆盖范围, error 几何编译错误
@@ -198,11 +257,11 @@ func (c *pdfCompositor) markBounds(node pdfCompositeNode) (Box, error) {
box = unionTextBox(box, c.importer.compositeTextBounds(*node.text, outline))
continue
}
geometry, err := c.geometry(node, outline)
bounds, err := c.geometryBounds(node, outline)
if err != nil {
return Box{}, err
}
box = unionTextBox(box, geometry.box)
box = unionTextBox(box, bounds)
}
return box, nil
}
+2 -2
View File
@@ -188,11 +188,11 @@ func (c *pdfCompositor) drawPaint(node pdfCompositeNode, outline bool, paint pdf
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}
}
geometry, err := c.geometry(node, outline)
bounds, err := c.geometryBounds(node, outline)
if err != nil {
return err
}
region := c.pixelBounds(geometry.box)
region := c.pixelBounds(bounds)
for y := region.Min.Y; y < region.Max.Y; y++ {
if err := c.importer.ctx.Err(); err != nil {
return err
+2
View File
@@ -197,6 +197,8 @@ func (p *pdfImporter) compositePattern(paint pdfgo.Paint) (*pdfCompositePattern,
local.matrix[5] -= box.Y
local.pageWidth, local.pageHeight = box.W, box.H
local.compositeCache = nil
local.compositingCache().geometry = cache.geometryCache()
local.compositingCache().clips = cache.clipCache()
pattern := &pdfCompositePattern{nodes: nodes, local: local, box: box}
cache.patterns[paint] = pattern
return pattern, nil
+228
View File
@@ -0,0 +1,228 @@
// 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 (
"context"
"crypto/sha256"
"encoding/binary"
"fmt"
"image/color"
"math"
"github.com/xiaoqidun/pdfgo"
)
// pdfGeometryPath 直接转换PDF路径坐标,不经文本编码或曲线展平
// 入参: ctx 取消上下文, source PDF路径, matrix 坐标变换
// 返回: GeometryPath 独立几何路径, error 指令、坐标或取消错误
func pdfGeometryPath(ctx context.Context, source pdfgo.Path, matrix pdfgo.Matrix) (GeometryPath, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
path := make(GeometryPath, len(source.Segments))
for i, segment := range source.Segments {
if err := ctx.Err(); err != nil {
return nil, err
}
var count int
switch segment.Operator {
case "M":
path[i].Verb, count = GeometryMove, 1
case "L":
path[i].Verb, count = GeometryLine, 1
case "B":
path[i].Verb, count = GeometryCubic, 3
case "C":
path[i].Verb = GeometryClose
default:
return nil, fmt.Errorf("invalid PDF path operator %q", segment.Operator)
}
if len(segment.Points) != count {
return nil, fmt.Errorf("invalid PDF path operands for %s", segment.Operator)
}
var points [3]Point
for j, point := range segment.Points {
point = matrix.Apply(point)
points[j] = Point{X: point.X, Y: point.Y}
}
if count != 0 {
path[i].End = points[count-1]
}
if count == 3 {
path[i].Control1, path[i].Control2 = points[0], points[1]
}
}
if err := path.validate(); err != nil {
return nil, err
}
return path, ctx.Err()
}
// compileCoverage 直接编译无文字裁剪的填充覆盖,其他语义交给配置的页面编译器
// 入参: node 图元, stroke 是否描边
// 返回: *RasterPage 覆盖场景, Box 页面区域, error 编译错误
func (p *pdfImporter) compileCoverage(node pdfCompositeNode, stroke bool) (*RasterPage, Box, error) {
fallback := func() (*RasterPage, Box, error) {
return p.compileGroup(func(local *pdfImporter) error { return node.coverage(local, stroke) })
}
if stroke || node.path == nil && node.image == nil {
return fallback()
}
deferredClip := false
switch p.editor.Backends().Compiler.(type) {
case CanvasBackend, *CanvasBackend:
case OFDCompiler, *OFDCompiler:
deferredClip = true
default:
return fallback()
}
style, _, _ := node.style()
for _, clip := range style.Clips {
if len(clip.Text) != 0 {
return fallback()
}
}
geometry, err := p.renderer.Geometry()
if err != nil {
return nil, Box{}, err
}
switch geometry.(type) {
case CanvasBackend, *CanvasBackend:
default:
return fallback()
}
var path GeometryPath
evenOdd := false
if node.path != nil {
path, err = pdfGeometryPath(p.ctx, node.path.Path, p.matrix)
evenOdd = node.path.Path.EvenOdd
} else {
path, err = geometry.Transform(geometryRectangle(Box{W: 1, H: 1}), MatrixFromValues([6]float64(p.matrix.Mul(node.image.Matrix))))
}
if err != nil {
return nil, Box{}, err
}
if len(path) == 0 {
return nil, Box{}, fmt.Errorf("empty PDF painted path")
}
path = closedGeometry(path)
clip, err := p.coverageClip(style.Clips, geometry)
if err != nil || len(clip) == 0 {
return nil, Box{}, err
}
compiler := semanticCompiler{geometry: geometry, page: &RasterPage{Width: p.pageWidth, Height: p.pageHeight, DPI: p.rasterDPI}}
var bounds pdfPointBounds
for _, segment := range path {
if segment.Verb != GeometryClose {
bounds.add(pdfgo.Point{X: segment.End.X, Y: segment.End.Y})
}
if segment.Verb == GeometryCubic {
bounds.add(pdfgo.Point{X: segment.Control1.X, Y: segment.Control1.Y})
bounds.add(pdfgo.Point{X: segment.Control2.X, Y: segment.Control2.Y})
}
}
boundary := geometryRectangle(bounds.box())
clip, err = clipGeometry(geometry, clip, &boundary)
if err != nil {
return nil, Box{}, err
}
if deferredClip {
if err := compiler.command(path, Paint{Color: color.RGBA{255, 255, 255, 255}}, evenOdd, &clip, IdentityMatrix, nil); err != nil {
return nil, Box{}, err
}
return p.groupSceneBounds(compiler.page)
}
box, err := geometry.Bounds(path)
if err != nil {
return nil, Box{}, err
}
rect, rectangle := geometryRectangleBounds(clip)
if !rectangle || box.X < rect.X || box.Y < rect.Y || box.X+box.W > rect.X+rect.W || box.Y+box.H > rect.Y+rect.H {
if evenOdd {
path, err = geometry.Normalize(path, true)
if err != nil {
return nil, Box{}, err
}
evenOdd = false
}
path, err = clipGeometry(geometry, path, &clip)
if err != nil {
return nil, Box{}, err
}
}
if err := compiler.command(path, Paint{Color: color.RGBA{255, 255, 255, 255}}, evenOdd, nil, IdentityMatrix, nil); err != nil {
return nil, Box{}, err
}
return p.groupSceneBounds(compiler.page)
}
// coverageClip 按完整路径、变换和页面范围复用只读裁剪,不缓存失败结果
// 入参: paths 无文字裁剪的路径集合, geometry 几何后端
// 返回: GeometryPath 共享只读轮廓, error 几何或取消错误
func (p *pdfImporter) coverageClip(paths []pdfgo.Path, geometry GeometryBackend) (GeometryPath, error) {
if err := p.ctx.Err(); err != nil {
return nil, err
}
clip := geometryRectangle(Box{W: p.pageWidth, H: p.pageHeight})
if len(paths) == 0 {
return clip, nil
}
hash := sha256.New()
var size [16]byte
binary.LittleEndian.PutUint64(size[:8], math.Float64bits(p.pageWidth))
binary.LittleEndian.PutUint64(size[8:], math.Float64bits(p.pageHeight))
hash.Write(size[:])
for _, path := range paths {
key, err := pdfClipPathKey(p.ctx, path, p.matrix)
if err != nil {
return nil, err
}
hash.Write(key[:])
}
var key [32]byte
hash.Sum(key[:0])
cache := p.compositingCache().clipCache()
if cached, ok := cache.get(key); ok {
return cached, nil
}
for _, source := range paths {
part, err := pdfGeometryPath(p.ctx, source, p.matrix)
if err != nil {
return nil, err
}
part = closedGeometry(part)
if _, rectangle := geometryRectangleBounds(part); source.EvenOdd && !rectangle {
part, err = geometry.Normalize(part, true)
if err != nil {
return nil, err
}
}
clip, err = clipGeometry(geometry, part, &clip)
if err != nil {
return nil, err
}
if len(clip) == 0 {
break
}
}
if err := p.ctx.Err(); err != nil {
return nil, err
}
if len(clip) <= (cache.limit-128)/112 {
cache.put(key, clip, 128+len(clip)*112)
}
return clip, nil
}
+11 -3
View File
@@ -359,6 +359,7 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
editor.SetFontDirs(p.editor.fontDirs...)
editor.SetFontFS(p.editor.fontFS...)
editor.fontSourcesCache = p.renderer.fontSourcesCache
editor.validatedPaths = p.editor.pathValidationCache()
if _, err := editor.AddPage(p.pageWidth, p.pageHeight); err != nil {
return nil, box, err
}
@@ -402,11 +403,18 @@ func (p *pdfImporter) compileGroup(build func(*pdfImporter) error) (*RasterPage,
if scene == nil {
return nil, box, fmt.Errorf("PDF local compositing: invalid compiled scene")
}
geometry, err := renderer.Geometry()
return p.groupSceneBounds(scene)
}
// groupSceneBounds 计算已编译场景在页面内的像素对齐区域
// 入参: scene 中立绘制场景
// 返回: *RasterPage 非空场景, Box 页面区域, error 度量错误
func (p *pdfImporter) groupSceneBounds(scene *RasterPage) (*RasterPage, Box, error) {
geometry, err := p.renderer.Geometry()
if err != nil {
return nil, box, err
return nil, Box{}, err
}
box, err = scene.BoundsContext(p.ctx, geometry)
box, err := scene.BoundsContext(p.ctx, geometry)
if err != nil {
return nil, Box{}, err
}
+34 -7
View File
@@ -17,6 +17,8 @@ package ofdgo
import (
"fmt"
"math"
"runtime"
"sync"
"github.com/xiaoqidun/pdfgo"
)
@@ -268,10 +270,12 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace,
return nil, fmt.Errorf("singular mesh bounds transform")
}
}
sampleRows := func(start, end int) error {
colors := make(map[[4]float64][4]float64)
for y := 0; y < h; y++ {
sampler := mesh.NewSampler()
for y := start * samples; y < end*samples; y++ {
if err := c.importer.ctx.Err(); err != nil {
return nil, err
return err
}
for x := 0; x < w; x++ {
if box := mesh.Bounds; box != nil {
@@ -287,9 +291,9 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace,
values := background
if sample.patch != 0 {
var err error
values, err = mesh.ValuesAt(sample.patch-1, sample.u, sample.v)
values, err = sampler.ValuesAt(sample.patch-1, sample.u, sample.v)
if err != nil {
return nil, err
return err
}
if cached, ok := colors[values]; ok {
values = cached
@@ -297,13 +301,14 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace,
source := values
values, err = converter.Convert(values[:mesh.Space.Components()])
if err != nil {
return nil, err
return err
}
if len(colors) == 2*pdfCompositeTileSize {
clear(colors)
}
if len(colors) < 2*pdfCompositeTileSize {
colors[source] = values
}
}
}
pixel := &pixels[(y/samples)*c.width+x/samples]
for i, value := range values {
pixel.values[i] += value
@@ -311,6 +316,28 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace,
pixel.shape++
}
}
return c.importer.ctx.Err()
}
workers := min(runtime.GOMAXPROCS(0), 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 {
+6 -3
View File
@@ -146,6 +146,7 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
break
}
}
streamCached := id != ""
var contentKey pdfImageContentKey
cacheable := false
if id == "" {
@@ -207,13 +208,15 @@ func (p *pdfImporter) image(mark pdfgo.ImageMark) error {
if err != nil {
return err
}
if cacheable {
p.imageContents.put(contentKey, pdfImageContent{source: source, id: id}, len(source.Stream.Data)+65536)
}
}
if !streamCached {
if p.imageIDs == nil {
p.imageIDs = map[pdfImageKey][]pdfImageResource{}
}
p.imageIDs[key] = append(p.imageIDs[key], pdfImageResource{space: source.ColorSpace, id: id})
if cacheable {
p.imageContents.put(contentKey, pdfImageContent{source: source, id: id}, len(source.Stream.Data)+65536)
}
}
return p.appendImage(mark, id)
}
+109 -8
View File
@@ -15,7 +15,10 @@
package ofdgo
import (
"crypto/sha256"
"encoding/binary"
"image"
"math"
"slices"
"github.com/tdewolff/canvas"
@@ -101,14 +104,10 @@ func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, ob
areaCTM = objectCTM.Multiply(areaCTM)
}
for _, pathObj := range area.Path {
ctm := areaCTM.Multiply(NewMatrix(pathObj.CTM))
cp := r.buildPath(pathObj, pageH, ctm, true)
cp.Translate(bx, -by)
cp.Close()
if pathObj.Rule == "Even-Odd" {
cp = cp.Settle(canvas.EvenOdd)
renderer.add(r.buildClipAreaPath(pathObj, area.DrawParam, areaCTM, pageH, bx, by))
if r.renderError != nil {
return nil
}
renderer.add(cp)
}
for _, textObj := range area.Text {
box, _ := ParseBox(textObj.Boundary)
@@ -138,11 +137,96 @@ func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, ob
if renderer.path == nil {
renderer.path = &canvas.Path{}
}
p = intersectClipPath(p, renderer.path)
p = r.intersectCachedClipPath(p, renderer.path)
}
return p
}
// buildClipAreaPath 按绘制参数展开裁剪路径的填充及描边,再应用区域变换
// 入参: obj 裁剪路径, drawParam 区域绘制参数, parent 区域变换, pageH 页面高度, bx 横向位移, by 纵向位移
// 返回: *canvas.Path 裁剪轮廓
func (r *Renderer) buildClipAreaPath(obj PathObject, drawParam string, parent Matrix, pageH, bx, by float64) *canvas.Path {
if obj.Visible != nil && !*obj.Visible {
return &canvas.Path{}
}
box, _ := ParseBox(obj.Boundary)
local := NewMatrix(obj.CTM)
m := parent.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(local)
clip := r.buildObjectClipPath(obj.Clips, pageH, obj.Boundary, local, &parent, true)
obj.Boundary = ""
p := r.buildPath(obj, 0, IdentityMatrix, false)
var result *canvas.Path
if obj.Fill != nil && *obj.Fill {
paths := canvas.Paths(p.Copy().Split())
for _, path := range paths {
path.Close()
}
result = paths.Merge()
if obj.Rule == "Even-Odd" {
result = result.Settle(canvas.EvenOdd)
}
}
if obj.Stroke == nil || *obj.Stroke {
dp := r.drawParamDefaults(obj.DrawParam, r.drawParamDefaults(drawParam, nil))
if dp != nil {
copy := *dp
copy.FillColor, copy.StrokeColor = nil, nil
dp = &copy
}
obj.FillColor, obj.StrokeColor = nil, nil
style := r.newPathStyle(dp, 0, 0, 0, nil)
style.applyPathObject(r, obj, 0, 0, 0)
if style.lineWidth == 0 {
style.lineWidth = 25.4 / r.DPI
}
result = unionClipPath(result, r.strokeDashedCanvasPath(p, style.lineWidth, style.lineCap, style.lineJoin, style.dashOffset, style.dashPattern, false))
}
if result == nil {
return &canvas.Path{}
}
result = result.Transform(canvas.Matrix{{m.a, -m.c, m.e}, {-m.b, m.d, pageH - m.f}})
return applyClipPath(result, clip).Translate(bx, -by)
}
// intersectCachedClipPath 按完整几何内容复用复杂裁剪交集,缓存命中时返回独立副本
// 入参: parent 父级裁剪路径, current 当前裁剪路径
// 返回: *canvas.Path 相交后的裁剪路径
func (r *Renderer) intersectCachedClipPath(parent, current *canvas.Path) *canvas.Path {
if parent == nil || current == nil || len(parent.Data())+len(current.Data()) < 256 {
return intersectClipPath(parent, current)
}
hash := sha256.New()
var data [512]byte
binary.LittleEndian.PutUint64(data[:8], math.Float64bits(canvas.Tolerance))
binary.LittleEndian.PutUint64(data[8:16], math.Float64bits(canvas.Epsilon))
binary.LittleEndian.PutUint64(data[16:24], math.Float64bits(canvas.BentleyOttmannEpsilon))
hash.Write(data[:24])
for _, path := range []*canvas.Path{parent, current} {
values := path.Data()
binary.LittleEndian.PutUint64(data[:8], uint64(len(values)))
hash.Write(data[:8])
for len(values) > 0 {
count := min(len(values), len(data)/8)
for i, value := range values[:count] {
binary.LittleEndian.PutUint64(data[i*8:], math.Float64bits(value))
}
hash.Write(data[:count*8])
values = values[count:]
}
}
var key [32]byte
hash.Sum(key[:0])
cache := r.canvasClipIntersections()
if path, ok := cache.get(key); ok {
return path.Copy()
}
path := intersectClipPath(parent, current)
if cost := len(path.Data())*8 + 256; cost <= cache.limit {
cache.put(key, path.Copy(), cost)
}
return path
}
// intersectClipPath 求裁剪路径交集
// 入参: parent 父级裁剪路径, current 当前裁剪路径
// 返回: *canvas.Path 相交后的裁剪路径
@@ -185,6 +269,12 @@ func intersectClipPath(parent, current *canvas.Path) *canvas.Path {
if result, ok := intersectConvexCanvasPaths(parent, current); ok {
return result
}
if result, ok := intersectLinearCanvasContours(parent, current); ok {
return result.Settle(canvas.NonZero)
}
if result, ok := intersectLinearCanvasContours(current, parent); ok {
return result.Settle(canvas.NonZero)
}
return parent.And(current)
}
@@ -241,6 +331,12 @@ func applyClipPath(path, clip *canvas.Path) *canvas.Path {
if result, ok := intersectConvexCanvasPaths(path, clip); ok {
return result
}
if result, ok := intersectLinearCanvasContours(path, clip); ok {
return result.Settle(canvas.NonZero)
}
if result, ok := intersectLinearCanvasContours(clip, path); ok {
return result.Settle(canvas.NonZero)
}
return path.And(clip)
}
@@ -248,6 +344,11 @@ func applyClipPath(path, clip *canvas.Path) *canvas.Path {
// 入参: path 非零填充描边, clip 裁剪路径
// 返回: *canvas.Path 绘制轮廓
func applyFillClipPath(path, clip *canvas.Path) *canvas.Path {
if path != nil && clip != nil && len(path.Data()) >= 64 {
if result, ok := intersectLinearCanvasContours(path, clip); ok {
return result
}
}
if path != nil && clip != nil && path.HasSubpaths() && len(path.Split()) >= 8 {
if result, ok := intersectCanvasContours(path, clip); ok {
return result
+16
View File
@@ -35,6 +35,22 @@ func intersectConvexCanvasPaths(left, right *canvas.Path) (*canvas.Path, bool) {
return intersectCanvasPolygons(a, b)
}
// intersectLinearCanvasContours 精确裁剪直线轮廓,不展开曲线或改变填充绕数
// 入参: path 绘制路径, clip 单个凸裁剪窗口
// 返回: *canvas.Path 交集路径, bool 是否适用直线轮廓裁剪
func intersectLinearCanvasContours(path, clip *canvas.Path) (*canvas.Path, bool) {
if path == nil {
return nil, false
}
data := path.Data()
for i := 0; i < len(data); i += 4 {
if i+4 > len(data) || data[i] != canvas.MoveToCmd && data[i] != canvas.LineToCmd && data[i] != canvas.CloseCmd {
return nil, false
}
}
return intersectCanvasContours(path, clip)
}
// intersectCanvasContours 按凸窗口裁剪闭合轮廓,保留绕向、孔洞和重叠透明度
// 入参: path 绘制路径, clip 单个凸裁剪路径
// 返回: *canvas.Path 交集路径, bool 是否适用分段裁剪
+5 -2
View File
@@ -194,7 +194,7 @@ func (CanvasBackend) Clip(r *Renderer, clips *Clips, matrix Matrix, parent *Geom
if renderer.renderError != nil {
return nil, renderer.renderError
}
return geometryFromCanvasPath(intersectClipPath(p, clip)), nil
return geometryFromCanvasPath(r.intersectCachedClipPath(p, clip)), nil
}
// canvasNativeStroke 判断描边是否由Canvas提供
@@ -303,11 +303,14 @@ func (r *Renderer) strokeDashedCanvasPath(path *canvas.Path, width float64, cap
// geometryToCanvasPath 将页面坐标路径转换为默认几何引擎的向上纵轴
// 入参: path 独立几何路径,nil表示不裁剪
// 返回: *canvas.Path 默认引擎路径, error 不支持的路径指令
// 返回: *canvas.Path 默认引擎路径, error 无效路径
func geometryToCanvasPath(path *GeometryPath) (*canvas.Path, error) {
if path == nil {
return nil, nil
}
if err := path.validate(); err != nil {
return nil, err
}
p := &canvas.Path{}
for _, s := range *path {
switch s.Verb {
+1 -1
View File
@@ -95,7 +95,7 @@ func (r *Renderer) renderImage(ctx *canvas.Context, obj ImageObject, pageH float
{-ctm.b / imgW, ctm.d / imgH, pageH - box.Y - ctm.d - ctm.f},
}
}
clipPath := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, obj.Boundary, localCTM, parentCTM, boundaryInCTM))
clipPath := r.intersectCachedClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, obj.Boundary, localCTM, parentCTM, boundaryInCTM))
imageClip := clipPath
compiler, compiled := ctx.Renderer.(*canvasPageCompiler)
if compiled && imageClip != nil && imageClipContains(img, imageClip, m) {
+1 -1
View File
@@ -158,7 +158,7 @@ func (r *Renderer) measureImage(ctx *canvas.Context, obj ImageObject, pageH floa
}
}
p := canvas.Rectangle(1, 1).Transform(canvas.Matrix{{m.a, m.c, m.e}, {-m.b, -m.d, pageH - m.f}})
clip := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, obj.Boundary, ctm, parentCTM, boundaryInCTM))
clip := r.intersectCachedClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, obj.Boundary, ctm, parentCTM, boundaryInCTM))
ctx.Renderer.(*boundsRenderer).add(applyClipPath(p, clip))
if obj.Border != nil {
border := *obj.Border
+32 -9
View File
@@ -19,6 +19,7 @@ import (
"image/color"
"math"
"slices"
"strings"
"github.com/tdewolff/canvas"
)
@@ -235,7 +236,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
{-parentCTM.b, parentCTM.d, pageH*(1-parentCTM.d) - parentCTM.f},
})
}
clipPath := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, obj.Boundary, localCTM, parentCTM, boundaryInCTM))
clipPath := r.intersectCachedClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, obj.Boundary, localCTM, parentCTM, boundaryInCTM))
shouldFill := false
if obj.Fill != nil {
shouldFill = *obj.Fill
@@ -245,7 +246,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
}
if shouldFill {
fillPaint, shadingClip, fillView := resolveShdPaint(ctx, style.fillPaint)
fillClip := intersectClipPath(clipPath, shadingClip)
fillClip := r.intersectCachedClipPath(clipPath, shadingClip)
paths := canvas.Paths(p.Copy().Split())
for _, path := range paths {
path.Close()
@@ -254,6 +255,9 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
fillRule := canvas.NonZero
if obj.Rule == "Even-Odd" {
fillRule = canvas.EvenOdd
if bounds, ok := rectangularPath(fillClip); ok && bounds.Contains(fp.Bounds()) {
fillClip = nil
}
if fillClip != nil || style.fillPattern != nil {
fp = fp.Settle(fillRule)
}
@@ -294,7 +298,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
sp = sp.Dash(style.dashOffset, style.dashPattern...)
ctx.SetDashes(0)
}
strokeClip := intersectClipPath(clipPath, shadingClip)
strokeClip := r.intersectCachedClipPath(clipPath, shadingClip)
_, repeat := strokePaint.(*repeatAxialGradient)
if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil || !canvasNativeStroke(r.backends.Geometry) || zeroDash {
sp = sp.Copy()
@@ -424,12 +428,7 @@ func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *PatternPaint, pag
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, boundaryInCTM 边界是否参与CTM变换
// 返回: *canvas.Path 路径对象
func (r *Renderer) buildPath(obj PathObject, pageH float64, ctm Matrix, boundaryInCTM bool) *canvas.Path {
path, err := ParseGeometryPath(obj.AbbreviatedData)
if err != nil {
r.renderError = err
return &canvas.Path{}
}
p, err := canvasObjectPath(path)
p, err := r.parseCanvasPath(obj.AbbreviatedData)
if err != nil {
r.renderError = err
return &canvas.Path{}
@@ -446,6 +445,30 @@ func (r *Renderer) buildPath(obj PathObject, pageH float64, ctm Matrix, boundary
return p.Transform(canvas.Matrix{{ctm.a, ctm.c, ctm.e}, {-ctm.b, -ctm.d, pageH - ctm.f}})
}
// parseCanvasPath 复用局部路径解析,返回独立副本且不缓存错误或超预算结果
// 入参: data 紧缩路径
// 返回: *canvas.Path 局部路径, error 解析错误
func (r *Renderer) parseCanvasPath(data string) (*canvas.Path, error) {
key := canvasPathKey{data: data, epsilon: canvas.Epsilon}
cache := r.canvasParsedPaths()
if path, ok := cache.get(key); ok {
return path.Copy(), nil
}
geometry, err := ParseGeometryPath(data)
if err != nil {
return nil, err
}
path, err := canvasObjectPath(geometry)
if err != nil {
return nil, err
}
if cost := len(data) + len(path.Data())*8 + 256; cost <= cache.limit {
key.data = strings.Clone(data)
cache.put(key, path.Copy(), cost)
}
return path, nil
}
// buildTinyFillRectPath 构建微小填充矩形路径
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, bx 边界X坐标, by 边界Y坐标
// 返回: *canvas.Path 路径对象
+42
View File
@@ -27,6 +27,18 @@ type canvasFontKey struct {
style canvas.FontStyle
}
// canvasClipCacheKey 区分不依赖文档资源的共享裁剪缓存
type canvasClipCacheKey struct{}
// canvasPathCacheKey 区分不依赖文档资源的共享局部路径缓存
type canvasPathCacheKey struct{}
// canvasPathKey 按完整路径文本与几何容差区分解析结果
type canvasPathKey struct {
data string
epsilon float64
}
// canvasBackendState 保存Canvas字体、字形和编码图片适配缓存
type canvasBackendState struct {
images renderCache[*EncodedImage, image.Image]
@@ -36,6 +48,36 @@ type canvasBackendState struct {
fontOutlines map[*canvas.Font]*sfntOutliner
}
// canvasClipIntersections 获取子渲染器共享的几何交集缓存,限制结果保留量
// 返回: *renderCache[[32]byte, *canvas.Path] 裁剪缓存
func (r *Renderer) canvasClipIntersections() *renderCache[[32]byte, *canvas.Path] {
key := canvasClipCacheKey{}
if state, ok := r.sharedBackendStates[key]; ok {
return state.(*renderCache[[32]byte, *canvas.Path])
}
if r.sharedBackendStates == nil {
r.sharedBackendStates = make(map[any]any)
}
cache := &renderCache[[32]byte, *canvas.Path]{limit: 8 << 20}
r.sharedBackendStates[key] = cache
return cache
}
// canvasParsedPaths 获取子渲染器共享的局部路径缓存,不保留对象变换
// 返回: *renderCache[canvasPathKey, *canvas.Path] 路径缓存
func (r *Renderer) canvasParsedPaths() *renderCache[canvasPathKey, *canvas.Path] {
key := canvasPathCacheKey{}
if state, ok := r.sharedBackendStates[key]; ok {
return state.(*renderCache[canvasPathKey, *canvas.Path])
}
if r.sharedBackendStates == nil {
r.sharedBackendStates = make(map[any]any)
}
cache := &renderCache[canvasPathKey, *canvas.Path]{limit: 16 << 20}
r.sharedBackendStates[key] = cache
return cache
}
// canvasFontFamilies 获取子渲染器共享的只读字体族缓存,限制解析数据保留量
// 返回: *renderCache[canvasFontKey, *canvas.FontFamily] 字体族缓存
func (r *Renderer) canvasFontFamilies() *renderCache[canvasFontKey, *canvas.FontFamily] {
+3 -3
View File
@@ -50,7 +50,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
if boundaryInCTM && parentCTM != nil {
objectCTM = parentCTM.Multiply(TranslationMatrix(bx, by)).Multiply(localCTM)
}
clipPath := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, obj.Boundary, localCTM, parentCTM, boundaryInCTM))
clipPath := r.intersectCachedClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, obj.Boundary, localCTM, parentCTM, boundaryInCTM))
var dp *DrawParam
if obj.DrawParam != "" {
dp = r.getDrawParam(obj.DrawParam, nil)
@@ -99,7 +99,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
}
transformShdPaint(fillPaint, parentCTM, bx, by, pageH, boundaryInCTM)
fillPaint, shadingClip, fillView := resolveShdPaint(ctx, fillPaint)
fillClip := intersectClipPath(clipPath, shadingClip)
fillClip := r.intersectCachedClipPath(clipPath, shadingClip)
var strokeStyle pathStyle
strokeClip := clipPath
strokeView := canvas.Identity
@@ -124,7 +124,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64
}
transformShdPaint(strokeStyle.strokePaint, parentCTM, bx, by, pageH, boundaryInCTM)
strokeStyle.strokePaint, shadingClip, strokeView = resolveShdPaint(ctx, strokeStyle.strokePaint)
strokeClip = intersectClipPath(strokeClip, shadingClip)
strokeClip = r.intersectCachedClipPath(strokeClip, shadingClip)
}
fontStyle := canvas.FontRegular
weight := obj.Weight
+4 -2
View File
@@ -936,21 +936,23 @@ func (x *ofdXML) clips(clips *Clips) {
var attrs ofdAttrs
attrs.add("CTM", area.CTM)
attrs.add("DrawParam", area.DrawParam)
x.start("Area", attrs)
for _, path := range area.Path {
if x.err != nil {
return
}
x.start("Area", attrs)
x.object(GraphicObject{Type: "Path", PathObject: path}, false)
x.end("Area")
}
for _, text := range area.Text {
if x.err != nil {
return
}
x.start("Area", attrs)
x.object(GraphicObject{Type: "Text", TextObject: text}, false)
}
x.end("Area")
}
}
x.end("Clip")
}
x.end("Clips")