// 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" "image/color" "math" "slices" "strings" "github.com/tdewolff/canvas" ) // pathStyle 路径绘制样式 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 lineJoinName string 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 尖角截断值, explicit 是否显式指定零截断值 func (s *pathStyle) applyLineJoin(join string, limit float64, explicit bool) { if limit > 0 || explicit { s.miterLimit = limit } if s.lineJoinName == "" { switch s.lineJoin.(type) { case canvas.RoundJoiner: s.lineJoinName = "Round" case canvas.BevelJoiner: s.lineJoinName = "Bevel" default: s.lineJoinName = "Miter" } } switch join { case "Miter", "Round", "Bevel": s.lineJoinName = join } switch s.lineJoinName { case "Round": s.lineJoin = canvas.RoundJoin case "Bevel": s.lineJoin = canvas.BevelJoin default: s.lineJoin = canvas.MiterJoiner{GapJoiner: canvas.BevelJoin, Limit: s.miterLimit} if s.miterLimit <= 1 { s.lineJoin = canvas.BevelJoin } } } // 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 || dp.LineWidthSet { 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, dp.MiterLimitSet) if dp.DashPattern != "" || dp.dashPatternSet { s.dashPattern = parseFloats(dp.DashPattern) } if dp.DashOffset != nil { 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 || obj.LineWidthSet { 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, obj.MiterLimitSet) if obj.DashPattern != "" || obj.dashPatternSet { s.dashPattern = parseFloats(obj.DashPattern) } if obj.DashOffset != nil { 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 r.textOnly { return } if bounds, ok := ctx.Renderer.(*boundsRenderer); ok { compiler, err := newSemanticCompiler(r) if err != nil { r.renderError = err return } compiler.measure, compiler.contours = true, bounds.collect err = compiler.pathObject(obj, RenderState{Defaults: defaults, Parent: parentCTM, BoundaryInCTM: boundaryInCTM, Clip: geometryFromCanvasPath(parentClip)}) if err != nil { r.renderError = err return } bounds.box = unionTextBox(bounds.box, compiler.measurement.Bounds) bounds.contours = append(bounds.contours, compiler.measurement.Contours...) return } 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) style.lineWidth = r.deviceLineWidth(style.lineWidth) 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 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 := r.intersectCachedClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, obj.Boundary, 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 := r.intersectCachedClipPath(clipPath, shadingClip) paths := canvas.Paths(p.Copy().Split()) for _, path := range paths { path.Close() } fp := paths.Merge() fillRule := canvas.NonZero if fillClip != nil && !fp.Bounds().Overlaps(fillClip.Bounds()) { fp = &canvas.Path{} } 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 = r.settleCanvasPath(fp, fillRule) } } if fillClip != nil { 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) r.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 zeroDash := slices.Contains(style.dashPattern, 0) if len(style.dashPattern) > 0 && !zeroDash { sp = sp.Dash(style.dashOffset, style.dashPattern...) ctx.SetDashes(0) } strokeClip := r.intersectCachedClipPath(clipPath, shadingClip) if bounds, ok := rectangularPath(strokeClip); ok { margin := style.lineWidth / 2 * math.Max(math.Sqrt2, style.miterLimit) if finite(margin) && bounds.Contains(sp.Bounds().Expand(margin)) { strokeClip = nil } } _, repeat := strokePaint.(*repeatAxialGradient) if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil || !canvasNativeStroke(r.backends.Geometry) || zeroDash { sp = sp.Copy() fill := style.strokePattern == nil && canvasFillClip(strokeClip) if zeroDash { sp = r.strokeDashedCanvasPath(sp, style.lineWidth, style.lineCap, style.lineJoin, style.dashOffset, style.dashPattern, fill) } else { sp = r.strokeDashedCanvasPath(sp, style.lineWidth, style.lineCap, style.lineJoin, 0, nil, fill) } if fill { sp = applyFillClipPath(sp, strokeClip) } else { sp = applyClipPath(sp, strokeClip) } if style.strokePattern != nil { r.renderPattern(ctx, style.strokePattern, pageH, sp, objectCTM) } else { ctx.SetFill(strokePaint) ctx.SetStrokeColor(canvas.Transparent) r.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 bounds, ok := ctx.Renderer.(*boundsRenderer); ok { if pattern.Alpha == nil || *pattern.Alpha > 0 { bounds.add(clip) } return } if r.pageText != nil { text := r.pageText r.pageText = nil defer func() { r.pageText = text }() } if clip == nil || clip.Empty() || pattern.CellContent.empty() { return } if r.patternReferences[pattern.Pattern] { r.renderError = fmt.Errorf("cyclic pattern paint") return } if r.patternReferences == nil { r.patternReferences = make(map[*Pattern]bool) } r.patternReferences[pattern.Pattern] = true parentPage := r.patternPage defer func() { delete(r.patternReferences, pattern.Pattern) r.patternPage = parentPage }() 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) } else if parentPage != nil { patternCTM = parentPage.Multiply(patternCTM) } invCTM, ok := patternCTM.Invert() if !ok { return } bounds := clip.Bounds() 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}) } r.patternPage = &tileCTM for obj := range pattern.CellContent.objects() { obj = mergeGraphicObjectAlpha(obj, pattern.Alpha) r.renderObject(ctx, &obj, pageH, defaults, &tileCTM, true, clip) if r.renderError != nil { return } } } } } // buildPath 解析路径并返回Canvas Path // 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, boundaryInCTM 边界是否参与CTM变换 // 返回: *canvas.Path 路径对象 func (r *Renderer) buildPath(obj PathObject, pageH float64, ctm Matrix, boundaryInCTM bool) *canvas.Path { p, err := r.parseCanvasPath(obj.AbbreviatedData) if err != nil { r.renderError = err return &canvas.Path{} } box, _ := ParseBox(obj.Boundary) if boundaryInCTM { ctm = ctm.Multiply(TranslationMatrix(box.X, box.Y)) } else { ctm = TranslationMatrix(box.X, box.Y).Multiply(ctm) } if canvas.Equal(ctm.a*ctm.d-ctm.b*ctm.c, 0) { p = p.ReplaceArcs() } 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 }