feat(渲染编辑): 补全网格渐变与径向延伸
build.yaml / build (push) Successful in 42s

This commit is contained in:
xiaoqidun committed 2026-09-27 13:44:41 +08:00
1 parent affc84459e
commit e78ea6f919
18 files changed
+567 -51

No files matched your search

+1 -1
View File
@@ -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)
+59 -1
View File
@@ -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 {
+5 -1
View File
@@ -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
}
+10 -8
View File
@@ -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 图案填充
+230
View File
@@ -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)) }
+17 -8
View File
@@ -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
}
+47 -6
View File
@@ -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
+5
View File
@@ -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
+3
View File
@@ -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
}
+52 -10
View File
@@ -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
}
}
+3
View File
@@ -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
}
+21 -6
View File
@@ -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{&copy}
return rasterGradientCanvas{gradient: &copy}
}
+4
View File
@@ -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 = &copy
stops = canvasGradient(g.Stops)
case *canvas.LinearGradient:
g.Kind, g.Start, g.End = RasterLinear, rasterPoint(source.Start), rasterPoint(source.End)
stops = source.Grad
+7 -1
View File
@@ -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
}
+2 -2
View File
@@ -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)
+61 -3
View File
@@ -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
+4 -4
View File
@@ -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
+36
View File
@@ -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)
}