mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 08:46:55 +08:00
This commit is contained in:
1 parent
619899e297
commit
e8344e6387
27 files changed
+754
-150
No files matched your search
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
+39
-7
@@ -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,22 +1204,35 @@ 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
|
||||
}
|
||||
if err := creationPath(value); err != nil {
|
||||
return err
|
||||
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 错误信息
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+103
-61
@@ -51,72 +51,109 @@ 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")
|
||||
}
|
||||
var result GeometryPath
|
||||
for i := 0; i < len(tokens); {
|
||||
command := tokens[i]
|
||||
i++
|
||||
count := 0
|
||||
switch command {
|
||||
case "M", "S", "L":
|
||||
count = 2
|
||||
case "Q":
|
||||
count = 4
|
||||
case "B":
|
||||
count = 6
|
||||
case "A":
|
||||
count = 7
|
||||
case "C":
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid path command %q", command)
|
||||
}
|
||||
if len(tokens)-i < count {
|
||||
return nil, fmt.Errorf("missing arguments for path command %s", command)
|
||||
}
|
||||
var values [7]float64
|
||||
for j, token := range tokens[i : i+count] {
|
||||
if command == "A" && (j == 3 || j == 4) && (token == "true" || token == "false") {
|
||||
if token == "true" {
|
||||
values[j] = 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)
|
||||
}
|
||||
values[j] = value
|
||||
}
|
||||
i += count
|
||||
segment := GeometrySegment{}
|
||||
switch command {
|
||||
case "M", "S":
|
||||
segment.Verb, segment.End = GeometryMove, Point{values[0], values[1]}
|
||||
case "L":
|
||||
segment.Verb, segment.End = GeometryLine, Point{values[0], values[1]}
|
||||
case "Q":
|
||||
segment.Verb, segment.Control1, segment.End = GeometryQuad, Point{values[0], values[1]}, Point{values[2], values[3]}
|
||||
case "B":
|
||||
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")
|
||||
}
|
||||
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 := make(GeometryPath, 0, count)
|
||||
_, err = walkGeometryPath(data, func(segment GeometrySegment) {
|
||||
result = append(result, segment)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
case "Q":
|
||||
count = 4
|
||||
case "B":
|
||||
count = 6
|
||||
case "A":
|
||||
count = 7
|
||||
case "C":
|
||||
count = 0
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid path command %q", 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 i, token := range args {
|
||||
if command == "A" && (i == 3 || i == 4) && (token == "true" || token == "false") {
|
||||
if token == "true" {
|
||||
values[i] = 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
value, err := strconv.ParseFloat(token, 64)
|
||||
if err != nil || !finite(value) || strings.ContainsAny(token, "xX_") {
|
||||
return GeometrySegment{}, fmt.Errorf("invalid number %q", token)
|
||||
}
|
||||
values[i] = value
|
||||
}
|
||||
segment := GeometrySegment{}
|
||||
switch command {
|
||||
case "M", "S":
|
||||
segment.Verb, segment.End = GeometryMove, Point{values[0], values[1]}
|
||||
case "L":
|
||||
segment.Verb, segment.End = GeometryLine, Point{values[0], values[1]}
|
||||
case "Q":
|
||||
segment.Verb, segment.Control1, segment.End = GeometryQuad, Point{values[0], values[1]}, Point{values[2], values[3]}
|
||||
case "B":
|
||||
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 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
|
||||
}
|
||||
return segment, nil
|
||||
}
|
||||
|
||||
// SVG 将几何路径编码为SVG路径,不改变曲线或坐标精度
|
||||
// 返回: string SVG路径, error 不支持的路径指令
|
||||
func (p GeometryPath) SVG() (string, error) {
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
+59
-32
@@ -17,6 +17,8 @@ package ofdgo
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/xiaoqidun/pdfgo"
|
||||
)
|
||||
@@ -268,47 +270,72 @@ func (c *pdfCompositor) mesh(mesh *pdfgo.MeshGradient, space *pdfgo.ColorSpace,
|
||||
return nil, fmt.Errorf("singular mesh bounds transform")
|
||||
}
|
||||
}
|
||||
colors := make(map[[4]float64][4]float64)
|
||||
for y := 0; y < h; y++ {
|
||||
if err := c.importer.ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for x := 0; x < w; x++ {
|
||||
if box := mesh.Bounds; box != nil {
|
||||
point := inverse.Apply(pdfgo.Point{X: c.box.X + (float64(x)+.5)/scale, Y: c.box.Y + (float64(y)+.5)/scale})
|
||||
if point.X < box.XMin || point.X > box.XMax || point.Y < box.YMin || point.Y > box.YMax {
|
||||
sampleRows := func(start, end int) error {
|
||||
colors := make(map[[4]float64][4]float64)
|
||||
sampler := mesh.NewSampler()
|
||||
for y := start * samples; y < end*samples; y++ {
|
||||
if err := c.importer.ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
for x := 0; x < w; x++ {
|
||||
if box := mesh.Bounds; box != nil {
|
||||
point := inverse.Apply(pdfgo.Point{X: c.box.X + (float64(x)+.5)/scale, Y: c.box.Y + (float64(y)+.5)/scale})
|
||||
if point.X < box.XMin || point.X > box.XMax || point.Y < box.YMin || point.Y > box.YMax {
|
||||
continue
|
||||
}
|
||||
}
|
||||
sample := grid[y*w+x]
|
||||
if sample.patch == 0 && mesh.Background == nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
sample := grid[y*w+x]
|
||||
if sample.patch == 0 && mesh.Background == nil {
|
||||
continue
|
||||
}
|
||||
values := background
|
||||
if sample.patch != 0 {
|
||||
var err error
|
||||
values, err = mesh.ValuesAt(sample.patch-1, sample.u, sample.v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cached, ok := colors[values]; ok {
|
||||
values = cached
|
||||
} else {
|
||||
source := values
|
||||
values, err = converter.Convert(values[:mesh.Space.Components()])
|
||||
values := background
|
||||
if sample.patch != 0 {
|
||||
var err error
|
||||
values, err = sampler.ValuesAt(sample.patch-1, sample.u, sample.v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
if len(colors) < 2*pdfCompositeTileSize {
|
||||
if cached, ok := colors[values]; ok {
|
||||
values = cached
|
||||
} else {
|
||||
source := values
|
||||
values, err = converter.Convert(values[:mesh.Space.Components()])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(colors) == 2*pdfCompositeTileSize {
|
||||
clear(colors)
|
||||
}
|
||||
colors[source] = values
|
||||
}
|
||||
}
|
||||
pixel := &pixels[(y/samples)*c.width+x/samples]
|
||||
for i, value := range values {
|
||||
pixel.values[i] += value
|
||||
}
|
||||
pixel.shape++
|
||||
}
|
||||
pixel := &pixels[(y/samples)*c.width+x/samples]
|
||||
for i, value := range values {
|
||||
pixel.values[i] += value
|
||||
}
|
||||
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
|
||||
}
|
||||
pixel.shape++
|
||||
}
|
||||
}
|
||||
for i := range pixels {
|
||||
|
||||
+6
-3
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 = ©
|
||||
}
|
||||
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
|
||||
|
||||
@@ -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 是否适用分段裁剪
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 路径对象
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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
@@ -936,20 +936,22 @@ 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("Area")
|
||||
}
|
||||
x.end("Clip")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user