From f27c5b666e43dea5f9476a547cb7dac5f1fc5624 Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 9 Sep 2026 13:03:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=B8=B2=E6=9F=93=E8=83=BD=E5=8A=9B):=20?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=BE=84=E5=90=91=E6=B8=90=E5=8F=98=E5=BB=B6?= =?UTF-8?q?=E4=BC=B8=E4=B8=8E=E9=95=9C=E5=83=8F=E6=B8=90=E5=8F=98=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=EF=BC=8C=E4=BF=9D=E6=8C=81=E5=85=A8=E7=9F=A2=E9=87=8F?= =?UTF-8?q?=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_page.go | 4 + ofdgo_paint.go | 17 ++-- ofdgo_renderer_path.go | 16 ++-- ofdgo_renderer_text.go | 20 ++--- ofdgo_shading.go | 195 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 221 insertions(+), 31 deletions(-) create mode 100644 ofdgo_shading.go diff --git a/ofdgo_page.go b/ofdgo_page.go index 012f5d0..c8bdd69 100644 --- a/ofdgo_page.go +++ b/ofdgo_page.go @@ -184,6 +184,8 @@ type StrokeColor struct { // AxialShd 轴向渐变 type AxialShd struct { + MapType string `xml:"MapType,attr"` + MapUnit float64 `xml:"MapUnit,attr"` Extend string `xml:"Extend,attr"` StartPoint string `xml:"StartPoint,attr"` EndPoint string `xml:"EndPoint,attr"` @@ -192,6 +194,8 @@ type AxialShd struct { // RadialShd 径向渐变 type RadialShd struct { + MapType string `xml:"MapType,attr"` + MapUnit float64 `xml:"MapUnit,attr"` Extend string `xml:"Extend,attr"` StartPoint string `xml:"StartPoint,attr"` StartRadius float64 `xml:"StartRadius,attr"` diff --git a/ofdgo_paint.go b/ofdgo_paint.go index 6ad80c9..1e71357 100644 --- a/ofdgo_paint.go +++ b/ofdgo_paint.go @@ -165,10 +165,10 @@ func parseFillPaint(fillColor *FillColor, x, y, pageH float64) any { return nil } if gradient := parseAxialShdGradient(fillColor.AxialShd, fillColor.Alpha, x, y, pageH); gradient != nil { - return gradient + return newShdPaint(gradient, fillColor.AxialShd.Extend, fillColor.AxialShd.MapType, fillColor.AxialShd.MapUnit) } if gradient := parseRadialShdGradient(fillColor.RadialShd, fillColor.Alpha, x, y, pageH); gradient != nil { - return gradient + return newShdPaint(gradient, fillColor.RadialShd.Extend, fillColor.RadialShd.MapType, fillColor.RadialShd.MapUnit) } if fillColor.AxialShd != nil { return parseShdColor(fillColor.AxialShd.Segment, fillColor.Alpha) @@ -209,10 +209,10 @@ func parseStrokePaint(strokeColor *StrokeColor, x, y, pageH float64) any { return nil } if gradient := parseAxialShdGradient(strokeColor.AxialShd, strokeColor.Alpha, x, y, pageH); gradient != nil { - return gradient + return newShdPaint(gradient, strokeColor.AxialShd.Extend, strokeColor.AxialShd.MapType, strokeColor.AxialShd.MapUnit) } if gradient := parseRadialShdGradient(strokeColor.RadialShd, strokeColor.Alpha, x, y, pageH); gradient != nil { - return gradient + return newShdPaint(gradient, strokeColor.RadialShd.Extend, strokeColor.RadialShd.MapType, strokeColor.RadialShd.MapUnit) } if strokeColor.AxialShd != nil { return parseShdColor(strokeColor.AxialShd.Segment, strokeColor.Alpha) @@ -312,14 +312,9 @@ func parseAxialShdGradient(axialShd *AxialShd, alpha *int, x, y, pageH float64) } // axialShdClip 获取轴向渐变的延伸裁剪区域 -// 入参: ctx 画布上下文, paint 画刷, shading 轴向渐变节点 +// 入参: ctx 画布上下文, gradient 轴向渐变, extend 延伸方向 // 返回: *canvas.Path 裁剪区域 -func axialShdClip(ctx *canvas.Context, paint any, shading *AxialShd) *canvas.Path { - gradient, ok := paint.(*canvas.LinearGradient) - if !ok || shading == nil { - return nil - } - extend, _ := strconv.Atoi(shading.Extend) +func axialShdClip(ctx *canvas.Context, gradient *canvas.LinearGradient, extend int) *canvas.Path { if extend == 3 { return nil } diff --git a/ofdgo_renderer_path.go b/ofdgo_renderer_path.go index 2068630..fe7e271 100644 --- a/ofdgo_renderer_path.go +++ b/ofdgo_renderer_path.go @@ -30,8 +30,6 @@ type pathStyle struct { strokeColor color.Color fillPaint any strokePaint any - fillAxialShd *AxialShd - strokeAxialShd *AxialShd fillPattern *Pattern fillPatternColor color.Color lineWidth float64 @@ -68,7 +66,6 @@ func (s *pathStyle) applyFillColor(fill *FillColor, bx, by, pageH float64, alpha s.fillPatternColor = patternColor(fillColorNode) s.fillColor = parseFillColor(fillColorNode) s.fillPaint = parseFillPaint(fillColorNode, bx, by, pageH) - s.fillAxialShd = fillColorNode.AxialShd } // applyStrokeColor 应用描边颜色 @@ -77,7 +74,6 @@ func (s *pathStyle) applyStrokeColor(stroke *StrokeColor, bx, by, pageH float64, strokeColorNode := withStrokeAlpha(stroke, alpha) s.strokeColor = parseStrokeColor(strokeColorNode) s.strokePaint = parseStrokePaint(strokeColorNode, bx, by, pageH) - s.strokeAxialShd = strokeColorNode.AxialShd } // pathLineCap 转换线帽样式 @@ -215,7 +211,8 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64 style.fillPaint = style.fillColor } if shouldFill { - fillClip := intersectClipPath(clipPath, axialShdClip(ctx, style.fillPaint, style.fillAxialShd)) + fillPaint, shadingClip := resolveShdPaint(ctx, style.fillPaint) + fillClip := intersectClipPath(clipPath, shadingClip) fp := p fillRule := canvas.NonZero if obj.Rule == "Even-Odd" { @@ -233,7 +230,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64 r.renderPattern(ctx, style.fillPattern, style.fillPatternColor, pageH, fp, ctm, bx, by) } else if style.fillPaint != nil { ctx.SetFillRule(fillRule) - ctx.SetFill(style.fillPaint) + ctx.SetFill(fillPaint) ctx.SetStrokeColor(canvas.Transparent) ctx.DrawPath(0, 0, fp) } @@ -250,15 +247,16 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64 if style.strokePaint == nil { style.strokePaint = colorWithAlpha(canvas.Black, obj.Alpha) } + strokePaint, shadingClip := resolveShdPaint(ctx, style.strokePaint) ctx.SetFillColor(canvas.Transparent) - ctx.SetStroke(style.strokePaint) + ctx.SetStroke(strokePaint) ctx.SetStrokeWidth(style.lineWidth) ctx.SetStrokeCapper(style.lineCap) ctx.SetStrokeJoiner(style.lineJoin) if len(style.dashPattern) > 0 { ctx.SetDashes(style.dashOffset, style.dashPattern...) } - strokeClip := intersectClipPath(clipPath, axialShdClip(ctx, style.strokePaint, style.strokeAxialShd)) + strokeClip := intersectClipPath(clipPath, shadingClip) if strokeClip != nil { sp := p.Copy() if len(style.dashPattern) > 0 { @@ -266,7 +264,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64 } sp = sp.Stroke(style.lineWidth, style.lineCap, style.lineJoin, canvas.Tolerance) sp = applyClipPath(sp, strokeClip) - ctx.SetFill(style.strokePaint) + ctx.SetFill(strokePaint) ctx.SetStrokeColor(canvas.Transparent) ctx.DrawPath(0, 0, sp) } else { diff --git a/ofdgo_renderer_text.go b/ofdgo_renderer_text.go index 6bbc0f1..74c3d64 100644 --- a/ofdgo_renderer_text.go +++ b/ofdgo_renderer_text.go @@ -76,24 +76,21 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 fillColor = colorWithAlpha(canvas.Black, obj.Alpha) } var fillPaint any = fillColor - var fillColorNode *FillColor if dp != nil && dp.FillColor != nil { - fillColorNode = withFillAlpha(dp.FillColor, obj.Alpha) + fillColorNode := withFillAlpha(dp.FillColor, obj.Alpha) fillColor = parseFillColor(fillColorNode) fillPaint = parseFillPaint(fillColorNode, bx, by, pageH) } if obj.FillColor != nil { - fillColorNode = withFillAlpha(obj.FillColor, obj.Alpha) + fillColorNode := withFillAlpha(obj.FillColor, obj.Alpha) fillColor = parseFillColor(fillColorNode) fillPaint = parseFillPaint(fillColorNode, bx, by, pageH) } if fillPaint == nil { fillPaint = fillColor } - fillClip := clipPath - if fillColorNode != nil { - fillClip = intersectClipPath(fillClip, axialShdClip(ctx, fillPaint, fillColorNode.AxialShd)) - } + fillPaint, shadingClip := resolveShdPaint(ctx, fillPaint) + fillClip := intersectClipPath(clipPath, shadingClip) strokeStyle := newPathStyle(nil, defaultStroke, 0, obj.Alpha) strokeClip := clipPath if shouldStroke { @@ -110,7 +107,8 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 strokeStyle.strokePaint = colorWithAlpha(canvas.Black, obj.Alpha) } strokeStyle.scale(ctm) - strokeClip = intersectClipPath(strokeClip, axialShdClip(ctx, strokeStyle.strokePaint, strokeStyle.strokeAxialShd)) + strokeStyle.strokePaint, shadingClip = resolveShdPaint(ctx, strokeStyle.strokePaint) + strokeClip = intersectClipPath(strokeClip, shadingClip) } fontStyle := canvas.FontRegular weight := obj.Weight @@ -150,7 +148,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 face := ff.Face(sizePt, fillPaint, fontStyle, canvas.FontNormal) glyphTransforms := r.textObjectGlyphTransforms(fontID, obj) hasUnderline := strings.Contains(obj.Decoration, "Underline") - _, axialFill := fillPaint.(*canvas.LinearGradient) + _, shadedFill := fillPaint.(canvas.Gradient) verticalAdvance := (obj.ReadDirection-obj.CharDirection)%180 != 0 advanceX, advanceY := 1.0, 0.0 switch obj.ReadDirection { @@ -174,7 +172,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 } dxs, dys := parseFloats(tc.DeltaX), parseFloats(tc.DeltaY) xs, ys := parseFloats(tc.X), parseFloats(tc.Y) - drawAsPath := embeddedFont || textCodePositioned(tc, xs, ys) || fillClip != nil || axialFill || shouldStroke + drawAsPath := embeddedFont || textCodePositioned(tc, xs, ys) || fillClip != nil || shadedFill || shouldStroke cx, cy := 0.0, 0.0 previousAdvance := 0.0 if len(xs) > 0 { @@ -265,7 +263,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 } if fillPaint != nil { ctx.SetFill(fillPaint) - if fillClip != nil || axialFill { + if fillClip != nil || shadedFill { scaleX := hScale if advanceLimit > 0 && glyphWidth*scaleX > advanceLimit { scaleX = advanceLimit / glyphWidth diff --git a/ofdgo_shading.go b/ofdgo_shading.go new file mode 100644 index 0000000..58e2774 --- /dev/null +++ b/ofdgo_shading.go @@ -0,0 +1,195 @@ +// 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 ( + "math" + "strconv" + + "github.com/tdewolff/canvas" +) + +type shdPaint struct { + gradient canvas.Gradient + extend int + mapType string + period float64 +} + +// newShdPaint 创建渐变画刷 +// 入参: gradient 渐变对象, extend 延伸方向, mapType 绘制方式, mapUnit 渐变区间长度 +// 返回: *shdPaint 渐变画刷 +func newShdPaint(gradient canvas.Gradient, extend, mapType string, mapUnit float64) *shdPaint { + flags, _ := strconv.Atoi(extend) + length := 0.0 + switch g := gradient.(type) { + case *canvas.LinearGradient: + length = g.End.Sub(g.Start).Length() + case *canvas.RadialGradient: + length = g.C1.Sub(g.C0).Length() + } + period := 1.0 + if mapUnit > 0 && length > 0 { + period = mapUnit / length + } + return &shdPaint{gradient: gradient, extend: flags, mapType: mapType, period: period} +} + +// resolveShdPaint 解析渐变画刷和裁剪区域 +// 入参: ctx 画布上下文, paint 画刷 +// 返回: any 画刷, *canvas.Path 裁剪区域 +func resolveShdPaint(ctx *canvas.Context, paint any) (any, *canvas.Path) { + s, ok := paint.(*shdPaint) + if !ok { + return paint, nil + } + switch g := s.gradient.(type) { + case *canvas.LinearGradient: + clip := axialShdClip(ctx, g, s.extend) + if s.mapType != "Reflect" { + return g, clip + } + d := g.End.Sub(g.Start) + axis := canvas.Matrix{{d.X, -d.Y, g.Start.X}, {d.Y, d.X, g.Start.Y}} + area := shdCanvasBounds(ctx).Transform(axis.Inv()) + lo, hi := s.rangeLimits(area.X0, area.X1) + if hi <= lo { + return g, &canvas.Path{} + } + gradient := reflectShdGrad(g.Grad, s.period, lo, hi) + return gradient.ToLinear(g.Start.Add(d.Mul(lo)), g.Start.Add(d.Mul(hi))), clip + case *canvas.RadialGradient: + d, dr := g.C1.Sub(g.C0), g.R1-g.R0 + length := d.Length() + if length >= math.Abs(dr) { + return g, nil + } + clip := radialShdClip(ctx, g, s.extend) + if s.mapType != "Reflect" { + return g, clip + } + lo, hi := 0.0, 1.0 + if s.extend != 0 { + distance := 0.0 + bounds := shdCanvasBounds(ctx) + for _, point := range []canvas.Point{{X: bounds.X0, Y: bounds.Y0}, {X: bounds.X1, Y: bounds.Y0}, {X: bounds.X1, Y: bounds.Y1}, {X: bounds.X0, Y: bounds.Y1}} { + distance = math.Max(distance, point.Sub(g.C0).Length()) + } + limit := (distance + g.R0) / (math.Abs(dr) - length) + lo, hi = s.rangeLimits(-limit, limit) + } + if dr > 0 { + lo = math.Max(lo, -g.R0/dr) + } else { + hi = math.Min(hi, -g.R0/dr) + } + if hi <= lo { + return g, &canvas.Path{} + } + gradient := reflectShdGrad(g.Grad, s.period, lo, hi) + return gradient.ToRadial(g.C0.Add(d.Mul(lo)), math.Max(0, g.R0+dr*lo), g.C0.Add(d.Mul(hi)), math.Max(0, g.R0+dr*hi)), clip + } + return s.gradient, nil +} + +// rangeLimits 限制渐变延伸区间 +// 入参: lo 区间起点, hi 区间终点 +// 返回: float64 起点, float64 终点 +func (s *shdPaint) rangeLimits(lo, hi float64) (float64, float64) { + if s.extend&1 == 0 { + lo = math.Max(lo, 0) + } + if s.extend&2 == 0 { + hi = math.Min(hi, 1) + } + return lo, hi +} + +// shdCanvasBounds 获取渐变坐标系下的画布边界 +// 入参: ctx 画布上下文 +// 返回: canvas.Rect 画布边界 +func shdCanvasBounds(ctx *canvas.Context) canvas.Rect { + origin := ctx.CoordView().Dot(canvas.Point{}) + view := ctx.CoordSystemView().Mul(ctx.View()).Translate(origin.X, origin.Y) + if view.Det() == 0 { + return canvas.Rect{} + } + width, height := ctx.Size() + return (canvas.Rect{X1: width, Y1: height}).Transform(view.Inv()) +} + +// radialShdClip 获取包含双圆的径向渐变裁剪区域 +// 入参: ctx 画布上下文, g 径向渐变, extend 延伸方向 +// 返回: *canvas.Path 裁剪区域 +func radialShdClip(ctx *canvas.Context, g *canvas.RadialGradient, extend int) *canvas.Path { + inner, outer := g.C0, g.C1 + r0, r1 := g.R0, g.R1 + if r1 < r0 { + inner, outer, r0, r1 = outer, inner, r1, r0 + extend = (extend&1)<<1 | (extend&2)>>1 + } + if extend == 3 { + return nil + } + var clip *canvas.Path + if extend&2 == 0 { + clip = canvas.Circle(r1).Translate(outer.X, outer.Y) + } else { + clip = shdCanvasBounds(ctx).ToPath() + } + if extend&1 == 0 && r0 > 0 { + clip = clip.Not(canvas.Circle(r0).Translate(inner.X, inner.Y)) + } + return clip +} + +// reflectShdGrad 构造镜像渐变颜色分段 +// 入参: gradient 颜色分段, period 周期长度, lo 起点, hi 终点 +// 返回: canvas.Grad 镜像颜色分段 +func reflectShdGrad(gradient canvas.Grad, period, lo, hi float64) canvas.Grad { + base := append(canvas.Grad{{Offset: 0, Color: gradient.At(0)}}, gradient...) + base = append(base, canvas.Stop{Offset: 1, Color: gradient.At(1)}) + reversed := make(canvas.Grad, len(base)) + for i, stop := range base { + reversed[len(base)-1-i] = canvas.Stop{Offset: 1 - stop.Offset, Color: stop.Color} + } + result := canvas.Grad{{Offset: 0, Color: gradient.At(reflectShdPosition(lo / period))}} + for i, last := math.Floor(lo/period), math.Ceil(hi/period); i < last; i++ { + stops := base + if math.Mod(i, 2) != 0 { + stops = reversed + } + for _, stop := range stops { + position := (i + stop.Offset) * period + offset := (position - lo) / (hi - lo) + if lo < position && position < hi && result[len(result)-1].Offset < offset { + result = append(result, canvas.Stop{Offset: offset, Color: stop.Color}) + } + } + } + return append(result, canvas.Stop{Offset: 1, Color: gradient.At(reflectShdPosition(hi / period))}) +} + +// reflectShdPosition 获取镜像周期内的位置 +// 入参: position 渐变位置 +// 返回: float64 周期内的位置 +func reflectShdPosition(position float64) float64 { + index := math.Floor(position) + position -= index + if math.Mod(index, 2) != 0 { + return 1 - position + } + return position +}