// 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 ( "fmt" basegeom "github.com/xiaoqidun/ofdgo/internal/base/geom" "math" "slices" "strconv" "sync" "github.com/tdewolff/canvas" ) // canvasGeometryMu 保护底层路径运算的共享扫描线对象池 var canvasGeometryMu sync.Mutex // settleCanvasPath 在共享对象池保护下归整路径 // 入参: path 路径, rule 填充规则 // 返回: *canvas.Path 归整路径 func settleCanvasPath(path *canvas.Path, rule canvas.FillRule) *canvas.Path { canvasGeometryMu.Lock() defer canvasGeometryMu.Unlock() return path.Settle(rule) } // combineCanvasPaths 在共享对象池保护下计算路径布尔运算 // 入参: left 左路径, right 右路径, operation 运算名称 // 返回: *canvas.Path 运算结果 func combineCanvasPaths(left, right *canvas.Path, operation string) *canvas.Path { canvasGeometryMu.Lock() defer canvasGeometryMu.Unlock() switch operation { case "or": return left.Or(right) case "not": return left.Not(right) case "xor": return left.Xor(right) default: return left.And(right) } } // strokeCanvasPath 在共享对象池保护下生成描边轮廓 // 入参: path 路径, width 线宽, cap 端点, join 连接, tolerance 容差 // 返回: *canvas.Path 描边轮廓 func strokeCanvasPath(path *canvas.Path, width float64, cap canvas.Capper, join canvas.Joiner, tolerance float64) *canvas.Path { canvasGeometryMu.Lock() defer canvasGeometryMu.Unlock() return path.Stroke(width, cap, join, tolerance) } // Curves 在输出边界转换椭圆弧,其他曲线保持原样 // 入参: path 页面路径 // 返回: GeometryPath 贝塞尔路径, error 路径错误 func (CanvasBackend) Curves(path GeometryPath) (GeometryPath, error) { p, err := geometryToCanvasPath(&path) if err != nil { return nil, err } return *geometryFromCanvasPath(p.ReplaceArcs()), nil } // Path 解析对象的标准紧缩路径并转换到页面坐标 // 入参: object 路径对象 // 返回: GeometryPath 页面路径, error 无效路径错误 func (b CanvasBackend) Path(object PathObject) (GeometryPath, error) { return objectGeometryPath(b, object) } // Region 解析动作区域并保持椭圆弧及退化变换的既有处理 // 入参: region 动作区域, matrix 页面变换 // 返回: GeometryPath 页面路径, error 解析错误 func (CanvasBackend) Region(region *Region, matrix Matrix) (GeometryPath, error) { p := actionRegionPath(region, matrix) return *geometryFromCanvasPath(p.Transform(canvas.Matrix{{1, 0, 0}, {0, -1, 0}})), nil } // Bounds 计算曲线路径的精确范围 // 入参: path 页面路径 // 返回: Box 路径范围, error 无效路径错误 func (CanvasBackend) Bounds(path GeometryPath) (Box, error) { p, err := geometryToCanvasPath(&path) if err != nil { return Box{}, err } if p.Empty() { return Box{}, nil } box := p.Bounds() return Box{X: box.X0, Y: -box.Y1, W: box.W(), H: box.H()}, nil } // Transform 变换路径,保持曲线与椭圆弧,不修改源路径 // 入参: path 页面路径, matrix 页面变换 // 返回: GeometryPath 新路径, error 无效路径错误 func (CanvasBackend) Transform(path GeometryPath, matrix Matrix) (GeometryPath, error) { curves := true for _, segment := range path { if segment.Verb == GeometryArc { curves = false break } } if curves { return path.Transform(matrix) } p, err := geometryToCanvasPath(&path) if err != nil { return nil, err } m := matrix.Values() for _, value := range m { if !finite(value) { return nil, fmt.Errorf("invalid geometry transform") } } if basegeom.Equal(m[0]*m[3]-m[1]*m[2], 0) { p = p.ReplaceArcs() } p = p.Transform(canvas.Matrix{{m[0], -m[2], m[4]}, {-m[1], m[3], -m[5]}}) return *geometryFromCanvasPath(p), nil } // Normalize 按填充规则整理自交区域 // 入参: path 页面路径, evenOdd 是否采用奇偶规则 // 返回: GeometryPath 非零填充路径, error 无效路径错误 func (CanvasBackend) Normalize(path GeometryPath, evenOdd bool) (GeometryPath, error) { p, err := geometryToCanvasPath(&path) if err != nil { return nil, err } rule := canvas.NonZero if evenOdd { rule = canvas.EvenOdd } p = settleCanvasPath(p, rule) if p.Empty() { return nil, nil } return *geometryFromCanvasPath(p), nil } // Combine 计算两个非零填充区域的布尔运算 // 入参: left 左路径, right 右路径, operation 运算类型 // 返回: GeometryPath 结果路径, error 运算错误 func (CanvasBackend) Combine(left, right GeometryPath, operation GeometryOperation) (GeometryPath, error) { a, err := geometryToCanvasPath(&left) if err != nil { return nil, err } b, err := geometryToCanvasPath(&right) if err != nil { return nil, err } var result *canvas.Path switch operation { case GeometryIntersect: result = applyClipPath(a, b) case GeometryUnion: result = combineCanvasPaths(a, b, "or") case GeometrySubtract: result = combineCanvasPaths(a, b, "not") case GeometryXor: result = combineCanvasPaths(a, b, "xor") default: return nil, fmt.Errorf("unsupported geometry operation %d", operation) } if result.Empty() { return nil, nil } return *geometryFromCanvasPath(result), nil } // Stroke 将绝对长度虚线与描边转换为填充区域,几何后端异常时使用本库非零描边 // 入参: path 页面路径, options 描边样式 // 返回: GeometryPath 描边区域, error 样式或路径错误 func (CanvasBackend) Stroke(path GeometryPath, options StrokeOptions) (result GeometryPath, err error) { if err := basegeom.ValidateStroke(options); err != nil { return nil, err } if slices.Contains(options.Dashes, 0) { return path.Stroke(options) } p, err := geometryToCanvasPath(&path) if err != nil { return nil, err } tolerance := options.Tolerance if tolerance == 0 { tolerance = canvas.Tolerance } options.Tolerance = tolerance defer func() { if failure := recover(); failure != nil { result, err = path.Stroke(options) if err != nil { err = fmt.Errorf("stroke fallback after %v: %w", failure, err) } } }() style := pathStyle{lineJoin: canvas.MiterJoin, lineCap: canvas.ButtCap, miterLimit: defaultMiterLimit} style.applyLineJoin(options.Join, options.MiterLimit, options.MiterLimitSet) style.lineCap = pathLineCap(options.Cap, style.lineCap) p = p.Dash(options.DashOffset, options.Dashes...) p = flattenCanvasPath(p, tolerance) p = strokeCanvasPath(p, options.Width, style.lineCap, style.lineJoin, tolerance) return *geometryFromCanvasPath(p), nil } // Clip 解析对象裁剪并与父裁剪相交,保留未裁剪与完全裁去的区别 // 入参: r 渲染器, clips OFD裁剪, matrix 裁剪坐标到页面的变换, parent 父裁剪 // 返回: *GeometryPath 页面裁剪, error 无效父路径错误 func (CanvasBackend) Clip(r *Renderer, clips *Clips, matrix Matrix, parent *GeometryPath) (*GeometryPath, error) { p, err := geometryToCanvasPath(parent) if err != nil { return nil, err } renderer := *r renderer.renderError = nil clip := renderer.buildClipPath(clips, 0, 0, 0, matrix) if renderer.renderError != nil { return nil, renderer.renderError } return geometryFromCanvasPath(r.intersectCachedClipPath(p, clip)), nil } // flattenCanvasPath 按子路径展开曲线,避免每次替换都复制后续独立轮廓 // 入参: path 原始路径, tolerance 展开误差 // 返回: *canvas.Path 直线路径,不修改原始路径 func flattenCanvasPath(path *canvas.Path, tolerance float64) *canvas.Path { if path.IsFlat() { return path } if data := path.Data(); len(data) > 0 && data[len(data)-1] == canvas.MoveToCmd { return path.Flatten(tolerance) } parts := path.Split() if len(parts) <= 1 { return path.Flatten(tolerance) } data := make([]float64, 0, len(path.Data())) for _, part := range parts { data = append(data, part.Flatten(tolerance).Data()...) } return canvas.NewPathFromData(data) } // canvasNativeStroke 判断描边是否由Canvas提供 // 入参: geometry 几何后端 // 返回: bool 是否采用原生描边 func canvasNativeStroke(geometry GeometryBackend) bool { _, ok := geometry.(CanvasBackend) return ok } // canvasStrokeOptions 转换Canvas描边样式,虚线由调用方传入 // 入参: width 线宽, cap 端点样式, join 转角样式, tolerance 展开误差 // 返回: StrokeOptions 公共描边样式 func canvasStrokeOptions(width float64, cap canvas.Capper, join canvas.Joiner, tolerance float64) StrokeOptions { options := StrokeOptions{Width: width, Cap: "Butt", Join: "Miter", MiterLimit: defaultMiterLimit, Tolerance: tolerance} switch cap.(type) { case canvas.RoundCapper: options.Cap = "Round" case canvas.SquareCapper: options.Cap = "Square" } switch value := join.(type) { case canvas.RoundJoiner: options.Join = "Round" case canvas.BevelJoiner: options.Join = "Bevel" case canvas.MiterJoiner: options.MiterLimit = value.Limit options.MiterLimitSet = value.Limit == 0 } return options } // canvasStrokePath 通过配置的几何后端生成描边,不修改源路径 // 入参: geometry 几何后端, path Canvas路径, options 描边样式 // 返回: *canvas.Path 描边轮廓, error 几何错误 func canvasStrokePath(geometry GeometryBackend, path *canvas.Path, options StrokeOptions) (*canvas.Path, error) { outline, err := geometry.Stroke(*geometryFromCanvasPath(path), options) if err != nil { return nil, err } return geometryToCanvasPath(&outline) } // canvasStrokeFillPath 为直接非零填充展开复杂描边,不将重叠轮廓交给区域运算 // 入参: geometry 几何后端, path Canvas路径, options 描边样式 // 返回: *canvas.Path 描边轮廓, error 几何错误 func canvasStrokeFillPath(geometry GeometryBackend, path *canvas.Path, options StrokeOptions) (*canvas.Path, error) { if canvasNativeStroke(geometry) && !slices.Contains(options.Dashes, 0) { if options.Tolerance == 0 { options.Tolerance = canvas.Tolerance } if err := basegeom.ValidateStroke(options); err != nil { return nil, err } if len(path.Dash(options.DashOffset, options.Dashes...).Data()) > 256 { outline, err := geometryFromCanvasPath(path).Stroke(options) if err != nil { return canvasStrokePath(geometry, path, options) } return geometryToCanvasPath(&outline) } } return canvasStrokePath(geometry, path, options) } // canvasFillClip 检查描边轮廓能否直接填充或按凸窗口独立裁剪 // 入参: clip 裁剪路径 // 返回: bool 是否不要求整理重叠区域 func canvasFillClip(clip *canvas.Path) bool { if clip == nil { return true } _, ok := convexCanvasPolygon(clip) return ok } // strokeCanvasPath 将文字和复杂画刷的描边交给配置的几何后端 // 入参: path Canvas路径, width 线宽, cap 端点样式, join 转角样式 // 返回: *canvas.Path 描边轮廓 func (r *Renderer) strokeCanvasPath(path *canvas.Path, width float64, cap canvas.Capper, join canvas.Joiner) *canvas.Path { return r.strokeDashedCanvasPath(path, width, cap, join, 0, nil, false) } // strokeDashedCanvasPath 通过公共几何后端展开虚线和端点样式,保留零长度绘制段 // 入参: path 路径, width 线宽, cap 端点样式, join 转角样式, offset 虚线偏移, dashes 虚线间隔序列, fill 是否直接填充或凸窗口裁剪 // 返回: *canvas.Path 描边填充轮廓 func (r *Renderer) strokeDashedCanvasPath(path *canvas.Path, width float64, cap canvas.Capper, join canvas.Joiner, offset float64, dashes []float64, fill bool) *canvas.Path { geometry, err := r.Geometry() if err != nil { r.renderError = err return &canvas.Path{} } options := canvasStrokeOptions(width, cap, join, canvas.Tolerance) options.DashOffset, options.Dashes = offset, dashes stroke := canvasStrokePath if fill { stroke = canvasStrokeFillPath } result, err := stroke(geometry, path, options) if err != nil { r.renderError = err return &canvas.Path{} } return result } // geometryToCanvasPath 将页面坐标路径转换为默认几何引擎的向上纵轴 // 入参: path 独立几何路径,nil表示不裁剪 // 返回: *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.NewPathFromData(make([]float64, 0, canvasPathCapacity(*path))) for _, s := range *path { switch s.Verb { case GeometryMove: p.MoveTo(s.End.X, -s.End.Y) case GeometryLine: p.LineTo(s.End.X, -s.End.Y) case GeometryQuad: p.QuadTo(s.Control1.X, -s.Control1.Y, s.End.X, -s.End.Y) case GeometryCubic: p.CubeTo(s.Control1.X, -s.Control1.Y, s.Control2.X, -s.Control2.Y, s.End.X, -s.End.Y) case GeometryArc: p.ArcTo(s.RadiusX, s.RadiusY, -s.Rotation, s.Large, !s.Sweep, s.End.X, -s.End.Y) case GeometryClose: p.Close() default: return nil, fmt.Errorf("unsupported geometry command %d", s.Verb) } } return p, nil } // canvasObjectPath 建立对象局部路径,保留折返线和退化贝塞尔的原始端点 // 入参: path 独立路径 // 返回: *canvas.Path 适配路径, error 不支持的指令 func canvasObjectPath(path GeometryPath) (*canvas.Path, error) { data := make([]float64, 0, canvasPathCapacity(path)) for _, s := range path { end := s.End switch s.Verb { case GeometryMove: data = append(data, canvas.MoveToCmd, end.X, end.Y, canvas.MoveToCmd) case GeometryLine: data = append(data, canvas.LineToCmd, end.X, end.Y, canvas.LineToCmd) case GeometryQuad: data = append(data, canvas.QuadToCmd, s.Control1.X, s.Control1.Y, end.X, end.Y, canvas.QuadToCmd) case GeometryCubic: data = append(data, canvas.CubeToCmd, s.Control1.X, s.Control1.Y, s.Control2.X, s.Control2.Y, end.X, end.Y, canvas.CubeToCmd) case GeometryArc: p := canvas.NewPathFromData(data) p.ArcTo(s.RadiusX, s.RadiusY, s.Rotation, s.Large, s.Sweep, end.X, end.Y) data = p.Data() case GeometryClose: p := canvas.NewPathFromData(data) p.Close() data = p.Data() default: return nil, fmt.Errorf("unsupported geometry command %d", s.Verb) } } return canvas.NewPathFromData(data), nil } // canvasPathCapacity 按指令估算路径容量,避免长路径逐段扩容 // 入参: path 独立几何路径 // 返回: int 坐标存储容量 func canvasPathCapacity(path GeometryPath) int { count := 0 for _, segment := range path { switch segment.Verb { case GeometryMove, GeometryLine, GeometryClose: count += 4 case GeometryQuad: count += 6 default: count += 8 } } return count } // geometryFromCanvasPath 将默认引擎路径转换为独立的页面坐标路径,保留曲线和椭圆弧 // 入参: path 默认引擎路径 // 返回: *GeometryPath 独立路径,nil表示不裁剪 func geometryFromCanvasPath(path *canvas.Path) *GeometryPath { if path == nil { return nil } result := make(GeometryPath, 0, path.Len()) scanner := path.Scanner() for scanner.Scan() { end := scanner.End() segment := GeometrySegment{End: Point{X: end.X, Y: -end.Y}} switch scanner.Cmd() { case canvas.MoveToCmd: segment.Verb = GeometryMove case canvas.LineToCmd: segment.Verb = GeometryLine case canvas.QuadToCmd: control := scanner.CP1() segment.Verb, segment.Control1 = GeometryQuad, Point{X: control.X, Y: -control.Y} case canvas.CubeToCmd: c1, c2 := scanner.CP1(), scanner.CP2() segment.Verb = GeometryCubic segment.Control1, segment.Control2 = Point{X: c1.X, Y: -c1.Y}, Point{X: c2.X, Y: -c2.Y} case canvas.ArcToCmd: segment.Verb = GeometryArc segment.RadiusX, segment.RadiusY, segment.Rotation, segment.Large, segment.Sweep = scanner.Arc() segment.Rotation, segment.Sweep = -segment.Rotation, !segment.Sweep case canvas.CloseCmd: segment.Verb = GeometryClose } result = append(result, segment) } return &result } // clipLinearFillGeometry 裁剪直线填充轮廓,保留原填充规则,不用于区域布尔运算 // 入参: path 闭合绘制轮廓, clip 凸裁剪窗口 // 返回: GeometryPath 填充轮廓, bool 是否适用, error 路径错误 func clipLinearFillGeometry(path, clip GeometryPath) (GeometryPath, bool, error) { a, err := geometryToCanvasPath(&path) if err != nil { return nil, false, err } b, err := geometryToCanvasPath(&clip) if err != nil { return nil, false, err } result, ok := intersectLinearCanvasContours(a, b) if !ok { return nil, false, nil } return *geometryFromCanvasPath(result), true, nil } // actionCanvasMatrix 转换页面坐标矩阵,不翻转Y轴 // 入参: matrix 页面变换 // 返回: canvas.Matrix 路径变换 func actionCanvasMatrix(matrix Matrix) canvas.Matrix { return canvas.Matrix{{matrix.a, matrix.c, matrix.e}, {matrix.b, matrix.d, matrix.f}} } // actionRegionPath 解析复杂点击区域并转换为页面坐标 // 入参: region 动作区域, matrix 坐标变换 // 返回: *canvas.Path 点击路径 func actionRegionPath(region *Region, matrix Matrix) *canvas.Path { path := &canvas.Path{} for _, area := range region.Area { start := parseFloats(area.Start) if len(start) != 2 { continue } part := &canvas.Path{} part.MoveTo(start[0], start[1]) for _, command := range area.Command { p1, p2, p3 := parseFloats(command.Point1), parseFloats(command.Point2), parseFloats(command.Point3) switch command.Type { case "Move": if len(p1) == 2 { part.Close() part.MoveTo(p1[0], p1[1]) } case "Line": if len(p1) == 2 { part.LineTo(p1[0], p1[1]) } case "QuadraticBezier": if len(p1) == 2 && len(p2) == 2 { part.QuadTo(p1[0], p1[1], p2[0], p2[1]) } case "CubicBezier": if len(p3) == 2 { first, second := part.Pos(), canvas.Point{X: p3[0], Y: p3[1]} if len(p1) == 2 { first = canvas.Point{X: p1[0], Y: p1[1]} } if len(p2) == 2 { second = canvas.Point{X: p2[0], Y: p2[1]} } part.CubeTo(first.X, first.Y, second.X, second.Y, p3[0], p3[1]) } case "Arc": size, end := parseFloats(command.EllipseSize), parseFloats(command.EndPoint) angle, err := strconv.ParseFloat(command.RotationAngle, 64) large, largeErr := strconv.ParseBool(command.LargeArc) sweep, sweepErr := strconv.ParseBool(command.SweepDirection) if len(end) == 2 && finite(end[0]) && finite(end[1]) && err == nil && finite(angle) && largeErr == nil && sweepErr == nil { rx, ry := 0.0, 0.0 if len(size) > 0 { rx, ry = math.Abs(size[0]), math.Abs(size[0]) } if len(size) > 1 { rx, ry = rx/2, math.Abs(size[1])/2 } if finite(rx) && finite(ry) { part.ArcTo(rx, ry, math.Mod(angle, 360), large, sweep, end[0], end[1]) } } case "Close": part.Close() } } part.Close() path = path.Append(part) } if canvas.Equal(matrix.a*matrix.d-matrix.b*matrix.c, 0) { path = path.ReplaceArcs() } return path.Transform(actionCanvasMatrix(matrix)) }