// 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 ( "image/color" "math" "strconv" "strings" "github.com/tdewolff/canvas" ) const defaultPathLineWidth = 0.353 const defaultMiterLimit = 3.528 type pathStyle struct { fillColor color.Color strokeColor color.Color fillPaint any strokePaint any fillPattern *patternPaint strokePattern *patternPaint lineWidth float64 lineCap canvas.Capper lineJoin canvas.Joiner miterLimit float64 dashOffset float64 dashPattern []float64 } // newPathStyle 创建路径样式 // 入参: dp 默认绘制参数, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度 // 返回: pathStyle 路径样式 func (r *Renderer) newPathStyle(dp *DrawParam, bx, by, pageH float64, alpha *int) pathStyle { style := pathStyle{ lineWidth: defaultPathLineWidth, lineCap: canvas.ButtCap, lineJoin: canvas.MiterJoiner{GapJoiner: canvas.BevelJoin, Limit: defaultMiterLimit}, miterLimit: defaultMiterLimit, } if dp != nil { style.applyDrawParam(r, dp, bx, by, pageH, alpha) } return style } // applyFillColor 应用填充颜色 // 入参: r 渲染器, fill 填充颜色, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度 func (s *pathStyle) applyFillColor(r *Renderer, fill *FillColor, bx, by, pageH float64, alpha *int) { fillColorNode := withFillAlpha(fill, alpha) s.fillPattern = parsePatternPaint(fillColorNode) s.fillColor = r.parseFillColor(fillColorNode) s.fillPaint = r.parseFillPaint(fillColorNode, bx, by, pageH) } // applyStrokeColor 应用描边颜色 // 入参: r 渲染器, stroke 描边颜色, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度 func (s *pathStyle) applyStrokeColor(r *Renderer, stroke *StrokeColor, bx, by, pageH float64, alpha *int) { strokeColorNode := withStrokeAlpha(stroke, alpha) s.strokePattern = parsePatternPaint((*FillColor)(strokeColorNode)) s.strokeColor = r.parseStrokeColor(strokeColorNode) s.strokePaint = r.parseStrokePaint(strokeColorNode, bx, by, pageH) } // pathLineCap 转换线帽样式 // 入参: cap 线帽名称, fallback 默认线帽 // 返回: canvas.Capper 线帽样式 func pathLineCap(cap string, fallback canvas.Capper) canvas.Capper { switch cap { case "Butt": return canvas.ButtCap case "Round": return canvas.RoundCap case "Square": return canvas.SquareCap } return fallback } // applyLineJoin 应用线连接样式及尖角限制 // 入参: join 线连接名称, limit 尖角限制 func (s *pathStyle) applyLineJoin(join string, limit float64) { if limit > 0 { s.miterLimit = limit } switch join { case "Miter": s.lineJoin = canvas.MiterJoin case "Round": s.lineJoin = canvas.RoundJoin case "Bevel": s.lineJoin = canvas.BevelJoin } if _, ok := s.lineJoin.(canvas.MiterJoiner); ok { s.lineJoin = canvas.MiterJoiner{GapJoiner: canvas.BevelJoin, Limit: s.miterLimit} } } // applyDrawParam 应用绘制参数样式 // 入参: r 渲染器, dp 绘制参数, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度 func (s *pathStyle) applyDrawParam(r *Renderer, dp *DrawParam, bx, by, pageH float64, alpha *int) { if dp.LineWidth > 0 { s.lineWidth = dp.LineWidth } if dp.FillColor != nil { s.applyFillColor(r, dp.FillColor, bx, by, pageH, alpha) } if dp.StrokeColor != nil { s.applyStrokeColor(r, dp.StrokeColor, bx, by, pageH, alpha) } if dp.Cap != "" { s.lineCap = pathLineCap(dp.Cap, s.lineCap) } s.applyLineJoin(dp.Join, dp.MiterLimit) if dp.DashPattern != "" { s.dashPattern = parseFloats(dp.DashPattern) s.dashOffset = dp.DashOffset } } // applyPathObject 应用路径对象样式 // 入参: r 渲染器, obj 路径对象, bx 边界X坐标, by 边界Y坐标, pageH 页面高度 func (s *pathStyle) applyPathObject(r *Renderer, obj PathObject, bx, by, pageH float64) { if obj.LineWidth > 0 { s.lineWidth = obj.LineWidth } if obj.FillColor != nil { s.applyFillColor(r, obj.FillColor, bx, by, pageH, obj.Alpha) } if obj.StrokeColor != nil { s.applyStrokeColor(r, obj.StrokeColor, bx, by, pageH, obj.Alpha) } if obj.Cap != "" { s.lineCap = pathLineCap(obj.Cap, canvas.ButtCap) } s.applyLineJoin(obj.Join, obj.MiterLimit) if obj.DashPattern != "" { s.dashPattern = parseFloats(obj.DashPattern) s.dashOffset = obj.DashOffset } } // scale 应用路径变换缩放 // 入参: ctm 变换矩阵 func (s *pathStyle) scale(ctm Matrix) { if scale := math.Sqrt(math.Abs(ctm.a*ctm.d - ctm.b*ctm.c)); scale > 0 { s.lineWidth *= scale s.dashOffset *= scale for i := range s.dashPattern { s.dashPattern[i] *= scale } } } // renderPath 渲染路径 // 入参: ctx 画布上下文, obj 路径对象, pageH 页面高度, defaults 默认绘制参数, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径 func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) { if obj.Visible != nil && !*obj.Visible { return } ctx.Push() bx, by := 0.0, 0.0 if obj.Boundary != "" { if box, err := ParseBox(obj.Boundary); err == nil { bx, by = box.X, box.Y } } localCTM := NewMatrix(obj.CTM) ctm := localCTM if parentCTM != nil { ctm = parentCTM.Multiply(ctm) } style := r.newPathStyle(defaults, bx, by, pageH, obj.Alpha) if obj.DrawParam != "" { if dp := r.getDrawParam(obj.DrawParam, nil); dp != nil { style.applyDrawParam(r, dp, bx, by, pageH, obj.Alpha) } } style.applyPathObject(r, obj, bx, by, pageH) style.scale(ctm) objectCTM := TranslationMatrix(bx, by).Multiply(ctm) if boundaryInCTM && parentCTM != nil { objectCTM = parentCTM.Multiply(TranslationMatrix(bx, by)).Multiply(localCTM) } transformShdPaint(style.fillPaint, parentCTM, bx, by, pageH, boundaryInCTM) transformShdPaint(style.strokePaint, parentCTM, bx, by, pageH, boundaryInCTM) pathCTM := ctm if boundaryInCTM { pathCTM = localCTM } p := r.buildPath(obj, pageH, pathCTM, false) if rectPath := r.buildTinyFillRectPath(obj, pageH, pathCTM, bx, by); rectPath != nil { p = rectPath } if boundaryInCTM && parentCTM != nil { p = p.Transform(canvas.Matrix{ {parentCTM.a, -parentCTM.c, parentCTM.c*pageH + parentCTM.e}, {-parentCTM.b, parentCTM.d, pageH*(1-parentCTM.d) - parentCTM.f}, }) } clipPath := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, bx, by, localCTM, parentCTM, boundaryInCTM)) shouldFill := false if obj.Fill != nil { shouldFill = *obj.Fill } if style.fillPaint == nil { style.fillPaint = style.fillColor } if shouldFill { fillPaint, shadingClip, fillView := resolveShdPaint(ctx, style.fillPaint) fillClip := intersectClipPath(clipPath, shadingClip) fp := p fillRule := canvas.NonZero if obj.Rule == "Even-Odd" { fillRule = canvas.EvenOdd if fillClip != nil || style.fillPattern != nil { fp = fp.Settle(fillRule) } } if fillClip != nil { fp = fp.Copy() fp.Close() fp = applyClipPath(fp, fillClip) } if style.fillPattern != nil { r.renderPattern(ctx, style.fillPattern, pageH, fp, objectCTM) } else if style.fillPaint != nil { ctx.SetFillRule(fillRule) ctx.SetFill(fillPaint) ctx.SetStrokeColor(canvas.Transparent) drawShdPath(ctx, fp, fillView) } } shouldStroke := true if obj.Stroke != nil { shouldStroke = *obj.Stroke } if shouldStroke { ctx.SetFillRule(canvas.NonZero) if style.strokePaint == nil { style.strokePaint = style.strokeColor } if style.strokePaint == nil { style.strokePaint = colorWithAlpha(canvas.Black, obj.Alpha) } strokePaint, shadingClip, strokeView := resolveShdPaint(ctx, style.strokePaint) ctx.SetFillColor(canvas.Transparent) ctx.SetStroke(strokePaint) ctx.SetStrokeWidth(style.lineWidth) ctx.SetStrokeCapper(style.lineCap) ctx.SetStrokeJoiner(style.lineJoin) sp := p if len(style.dashPattern) > 0 { sp = sp.Dash(style.dashOffset, style.dashPattern...) ctx.SetDashes(0) } strokeClip := intersectClipPath(clipPath, shadingClip) if strokeClip != nil || strokeView != canvas.Identity || style.strokePattern != nil { sp = sp.Copy() sp = sp.Stroke(style.lineWidth, style.lineCap, style.lineJoin, canvas.Tolerance) sp = applyClipPath(sp, strokeClip) if style.strokePattern != nil { r.renderPattern(ctx, style.strokePattern, pageH, sp, objectCTM) } else { ctx.SetFill(strokePaint) ctx.SetStrokeColor(canvas.Transparent) drawShdPath(ctx, sp, strokeView) } } else { ctx.DrawPath(0, 0, sp) } } ctx.Pop() } // renderPattern 渲染图案填充 // 入参: ctx 画布上下文, pattern 底纹画刷, pageH 页面高度, clip 绘制区域, objectCTM 对象变换矩阵 func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *patternPaint, pageH float64, clip *canvas.Path, objectCTM Matrix) { if clip == nil || clip.Empty() || len(pattern.CellContent.Objects) == 0 { return } xStep, yStep := pattern.XStep, pattern.YStep if xStep < pattern.Width { xStep = pattern.Width } if yStep < pattern.Height { yStep = pattern.Height } if xStep <= 0 || yStep <= 0 { return } patternCTM := NewMatrix(pattern.CTM) if pattern.RelativeTo != "Page" { patternCTM = objectCTM.Multiply(patternCTM) } invCTM, ok := patternCTM.Invert() if !ok { return } bounds := clip.FastBounds() points := [][2]float64{ {bounds.X0, pageH - bounds.Y0}, {bounds.X1, pageH - bounds.Y0}, {bounds.X1, pageH - bounds.Y1}, {bounds.X0, pageH - bounds.Y1}, } minX, maxX := 0.0, 0.0 minY, maxY := 0.0, 0.0 for i, point := range points { x, y := invCTM.Transform(point[0], point[1]) if i == 0 { minX, maxX = x, x minY, maxY = y, y continue } minX = math.Min(minX, x) maxX = math.Max(maxX, x) minY = math.Min(minY, y) maxY = math.Max(maxY, y) } startX := int(math.Floor(minX/xStep)) - 1 endX := int(math.Ceil(maxX/xStep)) + 1 startY := int(math.Floor(minY/yStep)) - 1 endY := int(math.Ceil(maxY/yStep)) + 1 defaults := &DrawParam{FillColor: &pattern.color, StrokeColor: (*StrokeColor)(&pattern.color)} for ix := startX; ix <= endX; ix++ { for iy := startY; iy <= endY; iy++ { tileCTM := patternCTM.Multiply(TranslationMatrix(float64(ix)*xStep, float64(iy)*yStep)) if ix%2 != 0 && (pattern.ReflectMethod == "Column" || pattern.ReflectMethod == "RowAndColumn") { tileCTM = tileCTM.Multiply(Matrix{a: -1, d: 1, e: pattern.Width}) } if iy%2 != 0 && (pattern.ReflectMethod == "Row" || pattern.ReflectMethod == "RowAndColumn") { tileCTM = tileCTM.Multiply(Matrix{a: 1, d: -1, f: pattern.Height}) } for _, obj := range pattern.CellContent.Objects { obj = mergeGraphicObjectAlpha(obj, pattern.alpha) r.renderObject(ctx, obj, pageH, defaults, &tileCTM, true, clip) } } } } // buildPath 解析路径并返回Canvas Path // 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, boundaryInCTM 边界是否参与CTM变换 // 返回: *canvas.Path 路径对象 func (r *Renderer) buildPath(obj PathObject, pageH float64, ctm Matrix, boundaryInCTM bool) *canvas.Path { bx, by := 0.0, 0.0 if obj.Boundary != "" { if box, err := ParseBox(obj.Boundary); err == nil { bx, by = box.X, box.Y } } point := func(x, y float64) (float64, float64) { if boundaryInCTM { tx, ty := ctm.Transform(x+bx, y+by) return tx, pageH - ty } tx, ty := ctm.Transform(x, y) return tx + bx, pageH - (ty + by) } p := &canvas.Path{} tokens := strings.Fields(obj.AbbreviatedData) for i := 0; i < len(tokens); { cmd := tokens[i] i++ switch cmd { case "M", "S": if i+1 < len(tokens) { x, _ := strconv.ParseFloat(tokens[i], 64) y, _ := strconv.ParseFloat(tokens[i+1], 64) tx, ty := point(x, y) p.MoveTo(tx, ty) i += 2 } case "L": if i+1 < len(tokens) { x, _ := strconv.ParseFloat(tokens[i], 64) y, _ := strconv.ParseFloat(tokens[i+1], 64) tx, ty := point(x, y) p.LineTo(tx, ty) i += 2 } case "B": if i+5 < len(tokens) { x1, _ := strconv.ParseFloat(tokens[i], 64) y1, _ := strconv.ParseFloat(tokens[i+1], 64) x2, _ := strconv.ParseFloat(tokens[i+2], 64) y2, _ := strconv.ParseFloat(tokens[i+3], 64) x3, _ := strconv.ParseFloat(tokens[i+4], 64) y3, _ := strconv.ParseFloat(tokens[i+5], 64) tx1, ty1 := point(x1, y1) tx2, ty2 := point(x2, y2) tx3, ty3 := point(x3, y3) p.CubeTo(tx1, ty1, tx2, ty2, tx3, ty3) i += 6 } case "Q": if i+3 < len(tokens) { x1, _ := strconv.ParseFloat(tokens[i], 64) y1, _ := strconv.ParseFloat(tokens[i+1], 64) x2, _ := strconv.ParseFloat(tokens[i+2], 64) y2, _ := strconv.ParseFloat(tokens[i+3], 64) tx1, ty1 := point(x1, y1) tx2, ty2 := point(x2, y2) p.QuadTo(tx1, ty1, tx2, ty2) i += 4 } case "A": if i+6 < len(tokens) { rx, _ := strconv.ParseFloat(tokens[i], 64) ry, _ := strconv.ParseFloat(tokens[i+1], 64) rot, _ := strconv.ParseFloat(tokens[i+2], 64) large, _ := strconv.ParseBool(tokens[i+3]) sweep, _ := strconv.ParseBool(tokens[i+4]) x, _ := strconv.ParseFloat(tokens[i+5], 64) y, _ := strconv.ParseFloat(tokens[i+6], 64) sx := math.Hypot(ctm.a, ctm.c) sy := math.Hypot(ctm.b, ctm.d) ctmRot := math.Atan2(ctm.b, ctm.a) * 180 / math.Pi tx, ty := point(x, y) sweep = !sweep p.ArcTo(rx*sx, ry*sy, -(rot + ctmRot), large, sweep, tx, ty) i += 7 } case "C": p.Close() } } return p } // buildObjectClipPath 构建对象裁剪路径并应用父级变换 // 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM // 返回: *canvas.Path 路径对象 func (r *Renderer) buildObjectClipPath(clips *Clips, pageH float64, bx, by float64, objectCTM Matrix, parentCTM *Matrix, boundaryInCTM bool) *canvas.Path { if !boundaryInCTM && parentCTM != nil { objectCTM = parentCTM.Multiply(objectCTM) } p := r.buildClipPath(clips, pageH, bx, by, objectCTM) if p != nil && boundaryInCTM && parentCTM != nil { p = p.Transform(canvas.Matrix{ {parentCTM.a, -parentCTM.c, parentCTM.c*pageH + parentCTM.e}, {-parentCTM.b, parentCTM.d, pageH*(1-parentCTM.d) - parentCTM.f}, }) } return p } // buildClipPath 构建裁剪路径 // 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM // 返回: *canvas.Path 路径对象 func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, objectCTM Matrix) *canvas.Path { if clips == nil { return nil } var p *canvas.Path for _, clip := range clips.Clip { var clipPath *canvas.Path for _, area := range clip.Area { areaCTM := NewMatrix(area.CTM) if clips.TransFlag == nil || *clips.TransFlag { 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) } if clipPath == nil { clipPath = cp } else { clipPath = unionClipPath(clipPath, cp) } } } if clipPath != nil { if p == nil { p = clipPath } else { p = intersectClipPath(p, clipPath) } } } return p } // intersectClipPath 求裁剪路径交集 // 入参: parent 父级裁剪路径, current 当前裁剪路径 // 返回: *canvas.Path 相交后的裁剪路径 func intersectClipPath(parent, current *canvas.Path) *canvas.Path { if parent == nil { return current } if current == nil { return parent } parentRect, parentOK := rectangularPath(parent) currentRect, currentOK := rectangularPath(current) if parentOK && currentOK { return parentRect.And(currentRect).ToPath() } if parent.Empty() || current.Empty() { return &canvas.Path{} } if parentOK && parentRect.Contains(current.FastBounds()) { return current } if currentOK && currentRect.Contains(parent.FastBounds()) { return parent } return parent.And(current) } // unionClipPath 合并裁剪区域 // 入参: left 左侧裁剪路径, right 右侧裁剪路径 // 返回: *canvas.Path 合并后的裁剪路径 func unionClipPath(left, right *canvas.Path) *canvas.Path { leftRect, leftOK := rectangularPath(left) rightRect, rightOK := rectangularPath(right) if leftOK && rightOK { bounds := leftRect.Add(rightRect) area := leftRect.Area() + rightRect.Area() - leftRect.And(rightRect).Area() if canvas.Equal(area, bounds.Area()) { return bounds.ToPath() } } return left.Or(right) } // applyClipPath 应用裁剪路径 // 入参: path 绘制路径, clip 裁剪路径 // 返回: *canvas.Path 裁剪后的绘制路径 func applyClipPath(path, clip *canvas.Path) *canvas.Path { if path == nil || clip == nil { return path } if path.Empty() || clip.Empty() { return &canvas.Path{} } if rect, ok := rectangularPath(clip); ok { bounds := path.FastBounds() if rect.Contains(bounds) { return path } if !rect.Overlaps(bounds) { return &canvas.Path{} } if pathRect, ok := rectangularPath(path); ok { return pathRect.And(rect).ToPath() } } return path.And(clip) } // rectangularPath 获取矩形路径区域 // 入参: path 路径对象 // 返回: canvas.Rect 矩形区域, bool 是否为矩形 func rectangularPath(path *canvas.Path) (canvas.Rect, bool) { if path == nil || path.HasSubpaths() || !path.Closed() { return canvas.Rect{}, 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 canvas.Rect{}, false } } points := path.Coords() if len(points) != 5 || !points[0].Equals(points[4]) { return canvas.Rect{}, false } rect := path.FastBounds() corners := 0 for _, point := range points[:4] { corner := 0 switch { case canvas.Equal(point.X, rect.X0) && canvas.Equal(point.Y, rect.Y0): corner = 1 case canvas.Equal(point.X, rect.X1) && canvas.Equal(point.Y, rect.Y0): corner = 2 case canvas.Equal(point.X, rect.X1) && canvas.Equal(point.Y, rect.Y1): corner = 4 case canvas.Equal(point.X, rect.X0) && canvas.Equal(point.Y, rect.Y1): corner = 8 default: return canvas.Rect{}, false } if corners&corner != 0 { return canvas.Rect{}, false } corners |= corner } return rect, corners == 15 } // buildTinyFillRectPath 构建微小填充矩形路径 // 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, bx 边界X坐标, by 边界Y坐标 // 返回: *canvas.Path 路径对象 func (r *Renderer) buildTinyFillRectPath(obj PathObject, pageH float64, ctm Matrix, bx, by float64) *canvas.Path { if obj.Fill == nil || !*obj.Fill || obj.Stroke == nil || *obj.Stroke { return nil } box, err := ParseBox(obj.Boundary) if err != nil || box.W <= 0 || box.H <= 0 || box.W > 0.6 || box.H > 0.6 { return nil } tokens := strings.Fields(obj.AbbreviatedData) if len(tokens) != 11 || tokens[0] != "M" || tokens[3] != "L" || tokens[6] != "L" || tokens[9] != "L" || tokens[10] != "C" { return nil } points := make([][2]float64, 0, 4) for i := 1; i < 10; i += 3 { x, errX := strconv.ParseFloat(tokens[i], 64) y, errY := strconv.ParseFloat(tokens[i+1], 64) if errX != nil || errY != nil { return nil } tx, ty := ctm.Transform(x, y) points = append(points, [2]float64{tx + bx, pageH - (ty + by)}) } minX, maxX := points[0][0], points[0][0] minY, maxY := points[0][1], points[0][1] for _, point := range points[1:] { minX = math.Min(minX, point[0]) maxX = math.Max(maxX, point[0]) minY = math.Min(minY, point[1]) maxY = math.Max(maxY, point[1]) } expand := math.Min(box.W, box.H) * 0.08 p := &canvas.Path{} p.MoveTo(minX-expand, minY-expand) p.LineTo(maxX+expand, minY-expand) p.LineTo(maxX+expand, maxY+expand) p.LineTo(minX-expand, maxY+expand) p.Close() return p }