From e78ea6f9195f887678b7628d6dc1088566f1a89b Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Sun, 27 Sep 2026 13:44:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=B8=B2=E6=9F=93=E7=BC=96=E8=BE=91):=20?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=E7=BD=91=E6=A0=BC=E6=B8=90=E5=8F=98=E4=B8=8E?= =?UTF-8?q?=E5=BE=84=E5=90=91=E5=BB=B6=E4=BC=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_editor.go | 2 +- ofdgo_editor_color.go | 60 +++++++- ofdgo_object.go | 6 +- ofdgo_page.go | 18 +-- ofdgo_paint_mesh.go | 230 +++++++++++++++++++++++++++++++ ofdgo_paint_model.go | 25 ++-- ofdgo_raster.go | 53 ++++++- ofdgo_raster_scene.go | 5 + ofdgo_render_cache.go | 3 + ofdgo_render_compile.go | 62 +++++++-- ofdgo_render_shading.go | 3 + ofdgo_renderer_canvas.go | 27 +++- ofdgo_renderer_compile_canvas.go | 4 + ofdgo_renderer_paint_canvas.go | 8 +- ofdgo_renderer_path_canvas.go | 4 +- ofdgo_renderer_shading_canvas.go | 64 ++++++++- ofdgo_renderer_text_canvas.go | 8 +- ofdgo_writer.go | 36 +++++ 18 files changed, 567 insertions(+), 51 deletions(-) create mode 100644 ofdgo_paint_mesh.go diff --git a/ofdgo_editor.go b/ofdgo_editor.go index 42f462c..2c5ee2b 100644 --- a/ofdgo_editor.go +++ b/ofdgo_editor.go @@ -1173,7 +1173,7 @@ func creationColor(color *FillColor) error { if color == nil { return nil } - if color.ColorSpace != "" || color.Index != nil || color.Pattern != nil || color.AxialShd != nil || color.RadialShd != nil || color.unsupported { + if color.ColorSpace != "" || color.Index != nil || color.Pattern != nil || color.AxialShd != nil || color.RadialShd != nil || color.GouraudShd != nil || color.LaGouraudShd != nil || color.unsupported { return fmt.Errorf("only direct RGB colors are supported for creation") } values, err := creationNumbers(color.Value, 3) diff --git a/ofdgo_editor_color.go b/ofdgo_editor_color.go index c553ad5..7774ee0 100644 --- a/ofdgo_editor_color.go +++ b/ofdgo_editor_color.go @@ -111,7 +111,7 @@ func (e *Editor) Color(value *FillColor) (color.NRGBA, error) { if value == nil { return color.NRGBA{A: 255}, nil } - if value.Pattern != nil || value.AxialShd != nil || value.RadialShd != nil || value.unsupported { + if value.Pattern != nil || value.AxialShd != nil || value.RadialShd != nil || value.GouraudShd != nil || value.LaGouraudShd != nil || value.unsupported { return color.NRGBA{}, fmt.Errorf("color is not a supported solid color") } id := value.ColorSpace @@ -292,6 +292,20 @@ func collectPaintReferences(paint *FillColor, used map[string]bool) { for _, segment := range segments { used[segment.Color.ColorSpace] = true } + var points []ShdPoint + var back *ShdColor + if node := paint.GouraudShd; node != nil { + points, back = node.Point, node.BackColor + } + if node := paint.LaGouraudShd; node != nil { + points, back = node.Point, node.BackColor + } + for _, point := range points { + used[point.Color.ColorSpace] = true + } + if back != nil { + used[back.ColorSpace] = true + } if paint.Pattern != nil { for _, object := range paint.Pattern.CellContent.Objects { collectObjectReferences(object, used) @@ -339,10 +353,54 @@ func collectObjectReferences(object GraphicObject, used map[string]bool) { } } +// editorMeshColor 校验网格拓扑和文档颜色引用 +// 入参: value 网格颜色 +// 返回: error 非法拓扑或颜色 +func (e *Editor) editorMeshColor(value *FillColor) error { + if value.unsupported || value.Pattern != nil || value.AxialShd != nil || value.RadialShd != nil || value.GouraudShd != nil && value.LaGouraudShd != nil || value.Value != "" || value.Index != nil { + return fmt.Errorf("specify one paint source") + } + if value.Alpha != nil && (*value.Alpha < 0 || *value.Alpha > 255) { + return fmt.Errorf("color alpha must be between 0 and 255") + } + var points []ShdPoint + var back *ShdColor + var columns, extend int + if node := value.GouraudShd; node != nil { + points, back, extend = node.Point, node.BackColor, node.Extend + } else { + points, back = value.LaGouraudShd.Point, value.LaGouraudShd.BackColor + columns, extend = value.LaGouraudShd.VerticesPerRow, value.LaGouraudShd.Extend + if columns == 0 { + return fmt.Errorf("invalid lattice shading dimensions") + } + } + colors := make([]ShdColor, 0, len(points)+1) + for _, point := range points { + colors = append(colors, point.Color) + } + if back != nil { + colors = append(colors, *back) + } + for _, color := range colors { + if color.ColorSpace == "" { + color.ColorSpace = value.ColorSpace + } + if _, err := e.Color(&FillColor{Value: color.Value, Index: color.Index, ColorSpace: color.ColorSpace, Alpha: color.Alpha}); err != nil { + return err + } + } + _, err := meshTriangleIndices(points, columns, extend) + return err +} + // editorColor 校验当前文档的纯色和渐变 // 入参: value 颜色 // 返回: error 错误信息 func (e *Editor) editorColor(value *FillColor) error { + if value != nil && (value.GouraudShd != nil || value.LaGouraudShd != nil) { + return e.editorMeshColor(value) + } if value != nil && value.Pattern != nil { pattern := value.Pattern if value.unsupported || value.AxialShd != nil || value.RadialShd != nil || !finite(pattern.Width) || !finite(pattern.Height) || pattern.Width <= 0 || pattern.Height <= 0 || !finite(pattern.XStep) || !finite(pattern.YStep) || pattern.XStep < pattern.Width || pattern.YStep < pattern.Height { diff --git a/ofdgo_object.go b/ofdgo_object.go index 0b7f4c2..c139267 100644 --- a/ofdgo_object.go +++ b/ofdgo_object.go @@ -92,12 +92,16 @@ func (c *FillColor) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { type plain FillColor var value struct { plain - Other *struct{} `xml:",any"` + LegacyMesh *LaGouraudShd `xml:"LaGourandShd"` + Other *struct{} `xml:",any"` } if err := d.DecodeElement(&value, &start); err != nil { return err } *c = FillColor(value.plain) + if c.LaGouraudShd == nil { + c.LaGouraudShd = value.LegacyMesh + } c.unsupported = value.Other != nil return nil } diff --git a/ofdgo_page.go b/ofdgo_page.go index 084f630..c8a1e8a 100644 --- a/ofdgo_page.go +++ b/ofdgo_page.go @@ -115,14 +115,16 @@ type TextObject struct { // FillColor 填充颜色 type FillColor struct { - Value string `xml:"Value,attr"` - Index *int `xml:"Index,attr"` - ColorSpace string `xml:"ColorSpace,attr"` - Alpha *int `xml:"Alpha,attr"` - Pattern *Pattern `xml:"Pattern"` - AxialShd *AxialShd `xml:"AxialShd"` - RadialShd *RadialShd `xml:"RadialShd"` - unsupported bool + Value string `xml:"Value,attr"` + Index *int `xml:"Index,attr"` + ColorSpace string `xml:"ColorSpace,attr"` + Alpha *int `xml:"Alpha,attr"` + Pattern *Pattern `xml:"Pattern"` + AxialShd *AxialShd `xml:"AxialShd"` + RadialShd *RadialShd `xml:"RadialShd"` + GouraudShd *GouraudShd `xml:"GouraudShd"` + LaGouraudShd *LaGouraudShd `xml:"LaGouraudShd"` + unsupported bool } // Pattern 图案填充 diff --git a/ofdgo_paint_mesh.go b/ofdgo_paint_mesh.go new file mode 100644 index 0000000..35427c7 --- /dev/null +++ b/ofdgo_paint_mesh.go @@ -0,0 +1,230 @@ +// 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" +) + +// GouraudShd 高洛德渐变,按EdgeFlag连接三角形 +type GouraudShd struct { + Extend int `xml:"Extend,attr,omitempty"` + Point []ShdPoint `xml:"Point"` + BackColor *ShdColor `xml:"BackColor,omitempty"` +} + +// LaGouraudShd 格构高洛德渐变,按行排列控制点 +type LaGouraudShd struct { + VerticesPerRow int `xml:"VerticesPerRow,attr"` + Extend int `xml:"Extend,attr,omitempty"` + Point []ShdPoint `xml:"Point"` + BackColor *ShdColor `xml:"BackColor,omitempty"` +} + +// ShdPoint 网格控制点及基本颜色 +type ShdPoint struct { + X float64 `xml:"X,attr"` + Y float64 `xml:"Y,attr"` + EdgeFlag int `xml:"EdgeFlag,attr,omitempty"` + Color ShdColor `xml:"Color"` +} + +// MeshVertex 保存局部毫米坐标、原颜色分量及非预乘透明度 +type MeshVertex struct { + Point Point + Space string + Values [4]float64 + Alpha float64 +} + +// MeshShading 保存后端无关的三角网格,后出现的三角形覆盖先出现的三角形 +type MeshShading struct { + Triangles [][3]MeshVertex + Background color.RGBA +} + +// outline 返回方向一致的三角形轮廓,供绘制裁剪和对象度量共用 +// 返回: GeometryPath 非零绕数填充轮廓 +func (m *MeshShading) outline() GeometryPath { + var path GeometryPath + for _, t := range m.Triangles { + a, b, c := t[0].Point, t[1].Point, t[2].Point + cross := (b.X-a.X)*(c.Y-a.Y) - (b.Y-a.Y)*(c.X-a.X) + if cross == 0 { + continue + } + if cross < 0 { + b, c = c, b + } + path = append(path, GeometrySegment{Verb: GeometryMove, End: a}, GeometrySegment{Verb: GeometryLine, End: b}, GeometrySegment{Verb: GeometryLine, End: c}, GeometrySegment{Verb: GeometryClose}) + } + return path +} + +// resolveMesh 按GB/T 33190的三角形连接规则解析网格 +// 入参: fill 网格颜色 +// 返回: *MeshShading 网格画刷, error 无效控制点 +func (r *Renderer) resolveMesh(fill *FillColor) (*MeshShading, error) { + var points []ShdPoint + var back *ShdColor + var extend, columns int + if node := fill.GouraudShd; node != nil { + points, back, extend = node.Point, node.BackColor, node.Extend + } else { + node := fill.LaGouraudShd + points, back, extend, columns = node.Point, node.BackColor, node.Extend, node.VerticesPerRow + if columns == 0 { + return nil, fmt.Errorf("invalid lattice shading dimensions") + } + } + indices, err := meshTriangleIndices(points, columns, extend) + if err != nil { + return nil, err + } + vertices := make([]MeshVertex, len(points)) + for i, point := range points { + space := point.Color.ColorSpace + if space == "" { + space = fill.ColorSpace + } + kind, values := r.colorComponents(point.Color.Value, point.Color.Index, space) + alpha := 1.0 + if value := mergeAlpha(point.Color.Alpha, fill.Alpha); value != nil { + alpha = float64(*value) / 255 + } + vertices[i] = MeshVertex{Point{point.X, point.Y}, kind, values, alpha} + } + mesh := &MeshShading{} + if extend == 1 && back != nil { + space := back.ColorSpace + if space == "" { + space = fill.ColorSpace + } + mesh.Background = r.ResolveColor(back.Value, back.Index, space, mergeAlpha(back.Alpha, fill.Alpha)) + } + for _, triangle := range indices { + mesh.Triangles = append(mesh.Triangles, [3]MeshVertex{vertices[triangle[0]], vertices[triangle[1]], vertices[triangle[2]]}) + } + for i := range mesh.Triangles { + t := &mesh.Triangles[i] + if t[0].Space != t[1].Space || t[0].Space != t[2].Space { + for j := range t { + r, g, b := meshRGB(t[j].Space, t[j].Values) + t[j].Space, t[j].Values = "RGB", [4]float64{r, g, b} + } + } + } + return mesh, nil +} + +// meshTriangleIndices 校验控制点并生成共享边或格构三角形编号 +// 入参: points 控制点, columns 每行点数,零表示非格构, extend 延伸标志 +// 返回: [][3]int 三角形顶点编号, error 非法拓扑 +func meshTriangleIndices(points []ShdPoint, columns, extend int) ([][3]int, error) { + if len(points) < 3 || extend < 0 || extend > 1 { + return nil, fmt.Errorf("invalid mesh shading") + } + if columns != 0 && (columns < 2 || columns > len(points)/2 || len(points)%columns != 0) { + return nil, fmt.Errorf("invalid lattice shading dimensions") + } + for i, p := range points { + if !finite(p.X) || !finite(p.Y) || p.EdgeFlag < 0 || p.EdgeFlag > 2 { + return nil, fmt.Errorf("invalid mesh shading point %d", i) + } + } + var result [][3]int + if columns != 0 { + for i := columns; i < len(points); i++ { + if i%columns != 0 { + result = append(result, [3]int{i - columns - 1, i - columns, i - 1}, [3]int{i - columns, i - 1, i}) + } + } + return result, nil + } + var triangle [3]int + for i := 0; i < len(points); { + switch points[i].EdgeFlag { + case 0: + if len(points)-i < 3 { + return nil, fmt.Errorf("incomplete mesh shading triangle") + } + triangle = [3]int{i, i + 1, i + 2} + i += 3 + case 1, 2: + if i == 0 { + return nil, fmt.Errorf("mesh shading starts with a shared edge") + } + if points[i].EdgeFlag == 1 { + triangle[0] = triangle[1] + } + triangle[1], triangle[2] = triangle[2], i + i++ + } + result = append(result, triangle) + } + return result, nil +} + +// At 按三角形面积权重插值原颜色分量,返回预乘RGBA +// 入参: x 横坐标, y 纵坐标 +// 返回: color.RGBA 采样颜色 +func (m *MeshShading) At(x, y float64) color.RGBA { + for i := len(m.Triangles) - 1; i >= 0; i-- { + t := m.Triangles[i] + a, b, c := t[0].Point, t[1].Point, t[2].Point + d := (b.Y-c.Y)*(a.X-c.X) + (c.X-b.X)*(a.Y-c.Y) + if d == 0 { + continue + } + u := ((b.Y-c.Y)*(x-c.X) + (c.X-b.X)*(y-c.Y)) / d + v := ((c.Y-a.Y)*(x-c.X) + (a.X-c.X)*(y-c.Y)) / d + w := 1 - u - v + if u < -1e-12 || v < -1e-12 || w < -1e-12 { + continue + } + var values [4]float64 + alpha := 0.0 + for j, weight := range [3]float64{u, v, w} { + alpha += weight * t[j].Alpha + for k := range values { + values[k] += weight * t[j].Values[k] + } + } + red, green, blue := meshRGB(t[0].Space, values) + return color.RGBA{meshByte(red * alpha), meshByte(green * alpha), meshByte(blue * alpha), meshByte(alpha)} + } + return m.Background +} + +// meshRGB 将插值后的颜色分量转换为RGB +// 入参: space 颜色模型, values 分量 +// 返回: float64 红、绿、蓝分量 +func meshRGB(space string, values [4]float64) (float64, float64, float64) { + switch space { + case "GRAY": + return values[0], values[0], values[0] + case "CMYK": + return (1 - values[0]) * (1 - values[3]), (1 - values[1]) * (1 - values[3]), (1 - values[2]) * (1 - values[3]) + default: + return values[0], values[1], values[2] + } +} + +// meshByte 将归一化颜色分量量化为八位整数 +// 入参: value 分量 +// 返回: uint8 量化结果 +func meshByte(value float64) uint8 { return uint8(math.Round(math.Max(0, math.Min(1, value)) * 255)) } diff --git a/ofdgo_paint_model.go b/ofdgo_paint_model.go index 8da0ff0..78e070b 100644 --- a/ofdgo_paint_model.go +++ b/ofdgo_paint_model.go @@ -22,7 +22,7 @@ type ColorStop struct { Color color.RGBA } -// PaintKind 区分无画刷、纯色、轴向渐变、径向渐变和图案 +// PaintKind 区分无画刷、纯色、轴向渐变、径向渐变、图案和网格渐变 type PaintKind uint8 const ( @@ -31,6 +31,7 @@ const ( PaintLinear PaintRadial PaintPattern + PaintMesh ) // Paint 保存后端无关的颜色、渐变和图案,不修改源节点 @@ -39,6 +40,7 @@ type Paint struct { Color color.RGBA Gradient *Shading Pattern *PatternPaint + Mesh *MeshShading } // Shading 保存OFD对象局部坐标中的渐变,单位为毫米,纵轴向下 @@ -53,17 +55,24 @@ type Shading struct { // ResolvePaint 解析颜色、透明度、渐变分段和图案,不依赖绘图库 // 入参: fill 填充或转换后的描边节点 -// 返回: Paint 只读画刷 -func (r *Renderer) ResolvePaint(fill *FillColor) Paint { +// 返回: Paint 只读画刷, error 无效渐变 +func (r *Renderer) ResolvePaint(fill *FillColor) (Paint, error) { if fill == nil { - return Paint{} + return Paint{}, nil } if fill.Pattern != nil { - return Paint{Kind: PaintPattern, Pattern: parsePatternPaint(fill)} + return Paint{Kind: PaintPattern, Pattern: parsePatternPaint(fill)}, nil + } + if fill.GouraudShd != nil || fill.LaGouraudShd != nil { + mesh, err := r.resolveMesh(fill) + if err != nil { + return Paint{}, err + } + return Paint{Kind: PaintMesh, Mesh: mesh}, nil } base := r.parseFillColor(fill) if base == nil { - return Paint{} + return Paint{}, nil } paint := Paint{Kind: PaintSolid, Color: colorToRGBA(base)} if node := fill.AxialShd; node != nil { @@ -72,7 +81,7 @@ func (r *Renderer) ResolvePaint(fill *FillColor) Paint { if len(start) >= 2 && len(end) >= 2 && len(stops) != 0 && (!geometryEqual(start[0], end[0]) || !geometryEqual(start[1], end[1])) { paint.Kind = PaintLinear paint.Gradient = &Shading{Start: Point{X: start[0], Y: start[1]}, End: Point{X: end[0], Y: end[1]}, Stops: stops, Extend: node.Extend, MapType: node.MapType, MapUnit: node.MapUnit} - return paint + return paint, nil } } if node := fill.RadialShd; node != nil && node.EndRadius > 0 { @@ -83,5 +92,5 @@ func (r *Renderer) ResolvePaint(fill *FillColor) Paint { paint.Gradient = &Shading{Start: Point{X: start[0], Y: start[1]}, End: Point{X: end[0], Y: end[1]}, StartRadius: node.StartRadius, EndRadius: node.EndRadius, Stops: stops, Extend: node.Extend, MapType: node.MapType, MapUnit: node.MapUnit, Eccentricity: node.Eccentricity, Angle: node.Angle} } } - return paint + return paint, nil } diff --git a/ofdgo_raster.go b/ofdgo_raster.go index 8d1a289..df980b7 100644 --- a/ofdgo_raster.go +++ b/ofdgo_raster.go @@ -81,15 +81,18 @@ type RasterGradientKind uint8 const ( RasterLinear RasterGradientKind = iota RasterRadial + RasterMesh ) -// RasterGradient 局部坐标下的线性或双圆径向渐变,Spread为空时延续边界颜色 +// RasterGradient 局部坐标下的轴向、双圆径向或三角网格渐变 +// 轴向及径向渐变的Spread为空时延续边界颜色,网格渐变使用Mesh type RasterGradient struct { Kind RasterGradientKind Start, End RasterPoint R0, R1 float64 Stops []ColorStop Spread *RasterSpread + Mesh *MeshShading } // RasterSpread 保存OFD渐变延伸和周期,nil表示连续延伸 @@ -140,19 +143,30 @@ func (m RasterMatrix) Inverse() (RasterMatrix, error) { // 入参: x 横坐标, y 纵坐标 // 返回: color.RGBA 采样颜色 func (g *RasterGradient) At(x, y float64) color.RGBA { + if g.Kind == RasterMesh { + return g.Mesh.At(x, y) + } dx, dy := g.End.X-g.Start.X, g.End.Y-g.Start.Y px, py := x-g.Start.X, y-g.Start.Y t := 0.0 if g.Kind == RasterRadial { dr := g.R1 - g.R0 a, b, c := dx*dx+dy*dy-dr*dr, -2*(px*dx+py*dy+g.R0*dr), px*px+py*py-g.R0*g.R0 - t = rasterRadialPosition(a, b, c, g.R0, dr) - if g.Spread != nil && math.Hypot(dx, dy) < math.Abs(dr) { - d := b*b - 4*a*c - if d < 0 { + if g.Spread != nil && math.Hypot(dx, dy) >= math.Abs(dr) { + var ok bool + t, ok = rasterRadialDomain(a, b, c, g.R0, dr, g.Spread.Extend) + if !ok { return color.RGBA{} } - t = (-b - math.Copysign(math.Sqrt(d), dr)) / (2 * a) + } else { + t = rasterRadialPosition(a, b, c, g.R0, dr) + if g.Spread != nil && math.Hypot(dx, dy) < math.Abs(dr) { + d := b*b - 4*a*c + if d < 0 { + return color.RGBA{} + } + t = (-b - math.Copysign(math.Sqrt(d), dr)) / (2 * a) + } } } else if length := dx*dx + dy*dy; length > 0 { t = (px*dx + py*dy) / length @@ -175,6 +189,33 @@ func (g *RasterGradient) At(x, y float64) color.RGBA { return g.colorAt(t) } +// rasterRadialDomain 求延伸区间内半径非负的最后一个双圆参数 +// 入参: a、b、c 方程系数, radius 起始半径, delta 半径变化, extend 延伸标志 +// 返回: float64 参数, bool 是否覆盖采样点 +func rasterRadialDomain(a, b, c, radius, delta float64, extend int) (float64, bool) { + x, y := math.NaN(), math.NaN() + if a == 0 { + if b != 0 { + x = -c / b + } + } else if d := b*b - 4*a*c; d >= 0 { + q := -.5 * (b + math.Copysign(math.Sqrt(d), b)) + if q == 0 { + x = -b / (2 * a) + } else { + x, y = q/a, c/q + } + } + best := math.Inf(-1) + for _, t := range []float64{x, y} { + if !finite(t) || radius+delta*t < 0 || t < 0 && extend&1 == 0 || t > 1 && extend&2 == 0 { + continue + } + best = math.Max(best, t) + } + return best, !math.IsInf(best, -1) +} + // rasterRadialPosition 求双圆渐变中半径非负的有效参数 // 入参: a 二次项系数, b 一次项系数, c 常数项, radius 起始半径, delta 半径增量 // 返回: float64 渐变位置,范围为0至1 diff --git a/ofdgo_raster_scene.go b/ofdgo_raster_scene.go index d0ff52b..f687245 100644 --- a/ofdgo_raster_scene.go +++ b/ofdgo_raster_scene.go @@ -127,6 +127,11 @@ func cloneRasterPage(page *RasterPage) *RasterPage { if cmd.Paint.Gradient != nil { gradient := *cmd.Paint.Gradient gradient.Stops = slices.Clone(gradient.Stops) + if gradient.Mesh != nil { + mesh := *gradient.Mesh + mesh.Triangles = slices.Clone(mesh.Triangles) + gradient.Mesh = &mesh + } if gradient.Spread != nil { spread := *gradient.Spread gradient.Spread = &spread diff --git a/ofdgo_render_cache.go b/ofdgo_render_cache.go index 3e03cc8..59f68aa 100644 --- a/ofdgo_render_cache.go +++ b/ofdgo_render_cache.go @@ -79,6 +79,9 @@ func rasterCommandCost(command RasterCommand) int { } if command.Paint.Gradient != nil { cost += 256 + len(command.Paint.Gradient.Stops)*64 + if mesh := command.Paint.Gradient.Mesh; mesh != nil { + cost += 32 + len(mesh.Triangles)*240 + } } return cost } diff --git a/ofdgo_render_compile.go b/ofdgo_render_compile.go index 833d865..bf86502 100644 --- a/ofdgo_render_compile.go +++ b/ofdgo_render_compile.go @@ -137,10 +137,10 @@ func (c *semanticCompiler) fill(path GeometryPath, paint Paint, evenOdd bool, cl return c.command(path, paint, evenOdd, clip, shadingMatrix, nil) } var err error - if clip != nil || paint.Kind == PaintPattern || paint.Gradient != nil { + if clip != nil || paint.Kind == PaintPattern || paint.Gradient != nil || paint.Mesh != nil { path = closedGeometry(path) } - if evenOdd && (clip != nil || paint.Kind == PaintPattern || paint.Gradient != nil) { + if evenOdd && (clip != nil || paint.Kind == PaintPattern || paint.Gradient != nil || paint.Mesh != nil) { path, err = c.geometry.Normalize(path, true) if err != nil { return err @@ -158,6 +158,20 @@ func (c *semanticCompiler) fill(path GeometryPath, paint Paint, evenOdd bool, cl } } if c.measure { + if paint.Mesh != nil && paint.Mesh.Background.A == 0 { + outline, err := c.geometry.Normalize(paint.Mesh.outline(), false) + if err != nil { + return err + } + outline, err = c.geometry.Transform(outline, shadingMatrix) + if err != nil { + return err + } + path, err = clipGeometry(c.geometry, path, &outline) + if err != nil { + return err + } + } return c.addBounds(path, evenOdd) } if paint.Kind == PaintPattern { @@ -192,6 +206,18 @@ func (c *semanticCompiler) command(path GeometryPath, paint Paint, evenOdd bool, command.Transform = RasterMatrix(view.Values()) command.Paint.Gradient = gradient } + if paint.Mesh != nil { + inverse, ok := shadingMatrix.Invert() + if !ok { + return fmt.Errorf("invalid mesh shading transform") + } + path, err = c.geometry.Transform(path, inverse) + if err != nil { + return err + } + command.Transform = RasterMatrix(shadingMatrix.Values()) + command.Paint.Gradient = &RasterGradient{Kind: RasterMesh, Mesh: paint.Mesh} + } command.Path, err = c.segments(path) if err != nil { return err @@ -238,9 +264,6 @@ func semanticGradient(paint Paint, matrix Matrix) (*RasterGradient, Matrix) { g.End.X, g.End.Y = inverse.Transform(g.End.X, g.End.Y) matrix = matrix.Multiply(view) } - if math.Hypot(g.End.X-g.Start.X, g.End.Y-g.Start.Y) >= math.Abs(g.R1-g.R0) { - g.Spread = nil - } } return g, matrix } @@ -358,12 +381,20 @@ func (c *semanticCompiler) pathObject(object PathObject, state RenderState) erro style := c.objectStyle(object, state.Defaults, linear) shading, _ := renderObjectMatrix(object.Boundary, IdentityMatrix, state) if object.Fill != nil && *object.Fill { - if err := c.fill(path, c.renderer.ResolvePaint(style.fill), object.Rule == "Even-Odd", clip, matrix, shading); err != nil { + paint, err := c.renderer.ResolvePaint(style.fill) + if err != nil { + return err + } + if err := c.fill(path, paint, object.Rule == "Even-Odd", clip, matrix, shading); err != nil { return err } } if object.Stroke == nil || *object.Stroke { - return c.stroke(path, c.renderer.ResolvePaint(style.stroke), style.options, clip, matrix, shading) + paint, err := c.renderer.ResolvePaint(style.stroke) + if err != nil { + return err + } + return c.stroke(path, paint, style.options, clip, matrix, shading) } return nil } @@ -397,18 +428,29 @@ func (c *semanticCompiler) textObject(object TextObject, state RenderState) erro style.fill = withFillAlpha(&FillColor{}, object.Alpha) } shading, _ := renderObjectMatrix(object.Boundary, IdentityMatrix, state) + fill, err := c.renderer.ResolvePaint(style.fill) + if err != nil { + return err + } + var stroke Paint + if object.Stroke != nil && *object.Stroke { + stroke, err = c.renderer.ResolvePaint(style.stroke) + if err != nil { + return err + } + } for _, glyph := range positioned.Glyphs { if object.Fill == nil || *object.Fill { - if err := c.fill(glyph.Path, c.renderer.ResolvePaint(style.fill), false, positioned.Clip, positioned.Matrix, shading); err != nil { + if err := c.fill(glyph.Path, fill, false, positioned.Clip, positioned.Matrix, shading); err != nil { return err } } if object.Stroke != nil && *object.Stroke && len(glyph.Path) > 0 { - if err := c.stroke(glyph.Path, c.renderer.ResolvePaint(style.stroke), style.options, positioned.Clip, positioned.Matrix, shading); err != nil { + if err := c.stroke(glyph.Path, stroke, style.options, positioned.Clip, positioned.Matrix, shading); err != nil { return err } } - if err := c.fill(glyph.Underline, c.renderer.ResolvePaint(style.fill), false, positioned.Clip, positioned.Matrix, shading); err != nil { + if err := c.fill(glyph.Underline, fill, false, positioned.Clip, positioned.Matrix, shading); err != nil { return err } } diff --git a/ofdgo_render_shading.go b/ofdgo_render_shading.go index 93212f1..d41d14b 100644 --- a/ofdgo_render_shading.go +++ b/ofdgo_render_shading.go @@ -34,6 +34,9 @@ func reflectShdPosition(position float64) float64 { func (c *semanticCompiler) shadingClip(path GeometryPath, paint Paint, matrix Matrix) (GeometryPath, error) { gradient, view := semanticGradient(paint, matrix) spread := gradient.Spread + if gradient.Kind == RasterRadial && math.Hypot(gradient.End.X-gradient.Start.X, gradient.End.Y-gradient.Start.Y) >= math.Abs(gradient.R1-gradient.R0) { + return path, nil + } if spread == nil || spread.Extend == 3 { return path, nil } diff --git a/ofdgo_renderer_canvas.go b/ofdgo_renderer_canvas.go index 8a139cd..ed1bfc1 100644 --- a/ofdgo_renderer_canvas.go +++ b/ofdgo_renderer_canvas.go @@ -111,11 +111,13 @@ func (b CanvasBackend) Render(page *RasterPage) (image.Image, error) { paint = canvas.Paint{Gradient: stops.ToLinear(start, end)} case RasterRadial: paint = canvas.Paint{Gradient: stops.ToRadial(start, g.R0, end, g.R1)} + case RasterMesh: + paint = canvas.Paint{Gradient: rasterGradientCanvas{gradient: g}} default: return nil, fmt.Errorf("canvas command %d: unsupported gradient %d", i, g.Kind) } if g.Spread != nil { - paint = canvas.Paint{Gradient: rasterGradientCanvas{g}} + paint = canvas.Paint{Gradient: rasterGradientCanvas{gradient: g}} } } style := canvas.Style{Fill: paint, FillRule: canvas.NonZero} @@ -134,7 +136,7 @@ func (b CanvasBackend) Render(page *RasterPage) (image.Image, error) { if err != nil { return nil, err } - paint = canvas.Paint{Gradient: rasterStrokeGradientCanvas{rasterGradientCanvas{cmd.Paint.Gradient}, inverse}} + paint = canvas.Paint{Gradient: rasterStrokeGradientCanvas{rasterGradientCanvas{gradient: cmd.Paint.Gradient}, inverse}} } line := pathStyle{lineJoin: canvas.MiterJoin, miterLimit: defaultMiterLimit} line.applyLineJoin(options.Join, options.MiterLimit) @@ -182,7 +184,7 @@ type rasterStrokeGradientCanvas struct { // 返回: color.RGBA 预乘颜色 func (g rasterStrokeGradientCanvas) At(x, y float64) color.RGBA { p := g.inverse.Apply(RasterPoint{x, y}) - return g.gradient.At(p.X, p.Y) + return g.rasterGradientCanvas.At(p.X, p.Y) } // SetColorSpace 转换描边渐变颜色,不修改共享分段 @@ -194,21 +196,34 @@ func (g rasterStrokeGradientCanvas) SetColorSpace(space canvas.ColorSpace) canva } // rasterGradientCanvas 将公共渐变交给Canvas采样,不改变OFD周期语义 -type rasterGradientCanvas struct{ gradient *RasterGradient } +type rasterGradientCanvas struct { + gradient *RasterGradient + space canvas.ColorSpace +} // At 返回局部坐标的预乘颜色 // 入参: x 横坐标, y 纵坐标 // 返回: color.RGBA 渐变颜色 -func (g rasterGradientCanvas) At(x, y float64) color.RGBA { return g.gradient.At(x, y) } +func (g rasterGradientCanvas) At(x, y float64) color.RGBA { + value := g.gradient.At(x, y) + if g.space != nil { + return g.space.ToLinear(value) + } + return value +} // SetColorSpace 返回转换颜色空间后的独立渐变 // 入参: space 目标颜色空间 // 返回: canvas.Gradient 采样器 func (g rasterGradientCanvas) SetColorSpace(space canvas.ColorSpace) canvas.Gradient { + if g.gradient.Mesh != nil { + g.space = space + return g + } copy := *g.gradient copy.Stops = append([]ColorStop(nil), copy.Stops...) for i := range copy.Stops { copy.Stops[i].Color = space.ToLinear(copy.Stops[i].Color) } - return rasterGradientCanvas{©} + return rasterGradientCanvas{gradient: ©} } diff --git a/ofdgo_renderer_compile_canvas.go b/ofdgo_renderer_compile_canvas.go index 1368dfe..055faf5 100644 --- a/ofdgo_renderer_compile_canvas.go +++ b/ofdgo_renderer_compile_canvas.go @@ -100,6 +100,10 @@ func (c *canvasPageCompiler) fill(path *canvas.Path, paint canvas.Paint, rule ca g := &RasterGradient{} var stops canvas.Grad switch source := paint.Gradient.(type) { + case rasterGradientCanvas: + copy := *source.gradient + g = © + stops = canvasGradient(g.Stops) case *canvas.LinearGradient: g.Kind, g.Start, g.End = RasterLinear, rasterPoint(source.Start), rasterPoint(source.End) stops = source.Grad diff --git a/ofdgo_renderer_paint_canvas.go b/ofdgo_renderer_paint_canvas.go index 2ede48e..3a9a625 100644 --- a/ofdgo_renderer_paint_canvas.go +++ b/ofdgo_renderer_paint_canvas.go @@ -24,12 +24,18 @@ import ( // 入参: fillColor 填充颜色节点, x X坐标, y Y坐标, pageH 页面高度 // 返回: any 填充画刷 func (r *Renderer) parseFillPaint(fillColor *FillColor, x, y, pageH float64) any { - paint := r.ResolvePaint(fillColor) + paint, err := r.ResolvePaint(fillColor) + if err != nil { + r.renderError = err + return nil + } switch paint.Kind { case PaintNone, PaintPattern: return nil case PaintLinear, PaintRadial: return canvasShading(paint, x, y, pageH) + case PaintMesh: + return &shdPaint{gradient: rasterGradientCanvas{gradient: &RasterGradient{Kind: RasterMesh, Mesh: paint.Mesh}}, view: canvas.Matrix{{1, 0, x}, {0, -1, pageH - y}}} default: return paint.Color } diff --git a/ofdgo_renderer_path_canvas.go b/ofdgo_renderer_path_canvas.go index feff476..180247e 100644 --- a/ofdgo_renderer_path_canvas.go +++ b/ofdgo_renderer_path_canvas.go @@ -269,7 +269,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64 ctx.SetFillRule(fillRule) ctx.SetFill(fillPaint) ctx.SetStrokeColor(canvas.Transparent) - drawShdPath(ctx, fp, fillView) + r.drawShdPath(ctx, fp, fillView) } } shouldStroke := true @@ -306,7 +306,7 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64 } else { ctx.SetFill(strokePaint) ctx.SetStrokeColor(canvas.Transparent) - drawShdPath(ctx, sp, strokeView) + r.drawShdPath(ctx, sp, strokeView) } } else { ctx.DrawPath(0, 0, sp) diff --git a/ofdgo_renderer_shading_canvas.go b/ofdgo_renderer_shading_canvas.go index 81a22ac..8af4f58 100644 --- a/ofdgo_renderer_shading_canvas.go +++ b/ofdgo_renderer_shading_canvas.go @@ -15,6 +15,7 @@ package ofdgo import ( + "fmt" "image/color" "math" "strconv" @@ -22,6 +23,43 @@ import ( "github.com/tdewolff/canvas" ) +// drawSampledShading 按输出精度采样后端不能原生表达的局部渐变,不改写OFD对象 +// 入参: ctx 绘制上下文, path 已裁剪轮廓, view 渐变变换, gradient 渐变采样器 +func (r *Renderer) drawSampledShading(ctx *canvas.Context, path *canvas.Path, view canvas.Matrix, gradient rasterGradientCanvas) { + if path.Empty() { + return + } + bounds := path.FastBounds().And(shdCanvasBounds(ctx)) + if bounds.W() <= 0 || bounds.H() <= 0 { + return + } + scale := r.DPI / 25.4 + w, h := math.Ceil(bounds.W()*scale), math.Ceil(bounds.H()*scale) + page := &RasterPage{Width: w / scale, Height: h / scale, DPI: r.DPI} + if _, _, err := page.PixelSize(); err != nil { + r.renderError = err + return + } + compiler := &canvasPageCompiler{page: page, geometry: r.backends.Geometry} + style := ctx.Style + style.Fill = canvas.Paint{Gradient: gradient} + compiler.RenderPath(path.Copy().Transform(view.Inv()), style, canvas.Identity.Translate(-bounds.X0, -bounds.Y0).Mul(view)) + if compiler.err != nil { + r.renderError = compiler.err + return + } + if r.backends.Raster == nil { + r.renderError = fmt.Errorf("shading raster: %w", ErrBackendUnavailable) + return + } + img, err := r.backends.Raster.Render(page) + if err != nil { + r.renderError = err + return + } + ctx.DrawImage(bounds.X0, bounds.Y0, img, canvas.DPMM(scale)) +} + // shdPaint 渐变画刷 type shdPaint struct { gradient canvas.Gradient @@ -84,6 +122,13 @@ func resolveShdPaint(ctx *canvas.Context, paint any) (any, *canvas.Path, canvas. return nil, &canvas.Path{}, canvas.Identity } switch g := s.gradient.(type) { + case rasterGradientCanvas: + if g.gradient.Mesh != nil && g.gradient.Mesh.Background.A == 0 { + outline := g.gradient.Mesh.outline() + clip, _ := canvasObjectPath(outline) + return g, clip.Settle(canvas.NonZero).Transform(s.view), s.view + } + return g, nil, s.view case *canvas.LinearGradient: bounds := shdCanvasBounds(ctx).Transform(s.view.Inv()) clip := axialShdClip(g, s.extend, bounds) @@ -108,8 +153,13 @@ func resolveShdPaint(ctx *canvas.Context, paint any) (any, *canvas.Path, canvas. 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, s.view + if length >= math.Abs(dr) || s.mapType == "Repeat" { + stops := make([]ColorStop, len(g.Grad)) + for i, stop := range g.Grad { + stops[i] = ColorStop{Offset: stop.Offset, Color: stop.Color} + } + gradient := &RasterGradient{Kind: RasterRadial, Start: rasterPoint(g.C0), End: rasterPoint(g.C1), R0: g.R0, R1: g.R1, Stops: stops, Spread: &RasterSpread{Extend: s.extend, MapType: s.mapType, Period: s.period}} + return rasterGradientCanvas{gradient: gradient}, nil, s.view } bounds := shdCanvasBounds(ctx).Transform(s.view.Inv()) clip := radialShdClip(g, s.extend, bounds) @@ -161,7 +211,15 @@ func transformShdPaint(paint any, parentCTM *Matrix, bx, by, pageH float64, boun // drawShdPath 绘制渐变路径并保持图形轮廓不变 // 入参: ctx 画布上下文, path 填充路径, view 渐变变换 -func drawShdPath(ctx *canvas.Context, path *canvas.Path, view canvas.Matrix) { +func (r *Renderer) drawShdPath(ctx *canvas.Context, path *canvas.Path, view canvas.Matrix) { + if gradient, ok := ctx.Style.Fill.Gradient.(rasterGradientCanvas); ok { + if _, compiling := ctx.Renderer.(*canvasPageCompiler); !compiling { + if _, measuring := ctx.Renderer.(*boundsRenderer); !measuring { + r.drawSampledShading(ctx, path, view, gradient) + return + } + } + } if gradient, ok := ctx.Style.Fill.Gradient.(*repeatAxialGradient); ok { drawRepeatAxialPath(ctx, path, gradient, view) return diff --git a/ofdgo_renderer_text_canvas.go b/ofdgo_renderer_text_canvas.go index 63c1f50..f1810fb 100644 --- a/ofdgo_renderer_text_canvas.go +++ b/ofdgo_renderer_text_canvas.go @@ -293,7 +293,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 } else if shouldFill && fillPaint != nil { ctx.SetFill(fillPaint) ctx.SetStrokeColor(canvas.Transparent) - drawShdPath(ctx, applyClipPath(path.Copy(), fillClip), fillView) + r.drawShdPath(ctx, applyClipPath(path.Copy(), fillClip), fillView) } if len(strokeStyle.dashPattern) > 0 { path = path.Dash(strokeStyle.dashOffset, strokeStyle.dashPattern...) @@ -305,7 +305,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 } else { ctx.SetFill(strokeStyle.strokePaint) ctx.SetStrokeColor(canvas.Transparent) - drawShdPath(ctx, path, strokeView) + r.drawShdPath(ctx, path, strokeView) } if hasUnderline { underline := &canvas.Path{} @@ -320,7 +320,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 } else if strokeStyle.strokePattern != nil { r.renderPattern(ctx, strokeStyle.strokePattern, pageH, applyClipPath(underline.Transform(transform), strokeClip), objectCTM) } else { - drawShdPath(ctx, applyClipPath(underline.Transform(transform), strokeClip), strokeView) + r.drawShdPath(ctx, applyClipPath(underline.Transform(transform), strokeClip), strokeView) } } continue @@ -342,7 +342,7 @@ func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64 if fillPattern != nil { r.renderPattern(ctx, fillPattern, pageH, glyphPath, objectCTM) } else { - drawShdPath(ctx, glyphPath, fillView) + r.drawShdPath(ctx, glyphPath, fillView) } if hasUnderline { uw := sizeMM * 0.05 diff --git a/ofdgo_writer.go b/ofdgo_writer.go index 0eb9c40..9621ffd 100644 --- a/ofdgo_writer.go +++ b/ofdgo_writer.go @@ -855,6 +855,42 @@ func (x *ofdXML) color(name string, color *FillColor) { if color.Pattern != nil { x.pattern(color.Pattern) } + if color.GouraudShd != nil { + node := color.GouraudShd + x.meshShading("GouraudShd", node.Extend, 0, node.Point, node.BackColor) + } + if color.LaGouraudShd != nil { + node := color.LaGouraudShd + x.meshShading("LaGouraudShd", node.Extend, node.VerticesPerRow, node.Point, node.BackColor) + } + x.end(name) +} + +// meshShading 写出网格控制点,仅非格构渐变保存共享边标志 +// 入参: name 元素名称, extend 延伸标志, columns 格构列数, points 控制点, back 背景颜色 +func (x *ofdXML) meshShading(name string, extend, columns int, points []ShdPoint, back *ShdColor) { + var attrs ofdAttrs + if extend != 0 { + attrs.add("Extend", strconv.Itoa(extend)) + } + if columns != 0 { + attrs.add("VerticesPerRow", strconv.Itoa(columns)) + } + x.start(name, attrs) + for _, point := range points { + var attrs ofdAttrs + attrs.add("X", ofdNumber(point.X)) + attrs.add("Y", ofdNumber(point.Y)) + if columns == 0 && point.EdgeFlag != 0 { + attrs.add("EdgeFlag", strconv.Itoa(point.EdgeFlag)) + } + x.start("Point", attrs) + x.element("Color", point.Color) + x.end("Point") + } + if back != nil { + x.element("BackColor", back) + } x.end(name) }