feat(增强渲染): 支持Pattern

This commit is contained in:
2026-07-01 08:52:45 +08:00
parent 8396f1d0dc
commit b778ab9d90
5 changed files with 217 additions and 1 deletions
+24
View File
@@ -86,6 +86,30 @@ func (m Matrix) Transform(x, y float64) (float64, float64) {
return nx, ny
}
// Invert 求逆矩阵
// 返回: Matrix 逆矩阵, bool 是否可逆
func (m Matrix) Invert() (Matrix, bool) {
det := m.a*m.d - m.b*m.c
if math.Abs(det) < 1e-9 {
return IdentityMatrix, false
}
return Matrix{
a: m.d / det,
b: -m.b / det,
c: -m.c / det,
d: m.a / det,
e: (m.c*m.f - m.d*m.e) / det,
f: (m.b*m.e - m.a*m.f) / det,
}, true
}
// TranslationMatrix 构造平移矩阵
// 入参: x X轴偏移, y Y轴偏移
// 返回: Matrix 平移矩阵
func TranslationMatrix(x, y float64) Matrix {
return Matrix{a: 1, d: 1, e: x, f: y}
}
// YScale 获取Y轴缩放比例
// 返回: float64 缩放比例
func (m Matrix) YScale() float64 {
+86
View File
@@ -302,6 +302,92 @@ func (a *Appearance) decodePageBlock(d *xml.Decoder, start xml.StartElement) err
}
}
// UnmarshalXML 解析图案单元内容并保留对象顺序
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (p *PatternContent) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
*p = PatternContent{}
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := p.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
}
}
// decodeObject 解析图案单元内容子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (p *PatternContent) decodeObject(d *xml.Decoder, start xml.StartElement) error {
switch start.Name.Local {
case "TextObject":
var obj TextObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
p.TextObject = append(p.TextObject, obj)
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, TextObject: obj})
case "PathObject":
var obj PathObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
p.PathObject = append(p.PathObject, obj)
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, PathObject: obj})
case "ImageObject":
var obj ImageObject
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
p.ImageObject = append(p.ImageObject, obj)
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, ImageObject: obj})
case "CompositeGraphicUnit", "CompositeObject":
var obj CompositeGraphicUnit
if err := d.DecodeElement(&obj, &start); err != nil {
return err
}
p.CompositeGraphicUnit = append(p.CompositeGraphicUnit, obj)
p.Objects = append(p.Objects, GraphicObject{Type: start.Name.Local, CompositeGraphicUnit: obj})
case "PageBlock":
return p.decodePageBlock(d, start)
default:
return d.Skip()
}
return nil
}
// decodePageBlock 解析图案单元内容页块子对象
// 入参: d XML解码器, start 起始节点
// 返回: error 错误信息
func (p *PatternContent) decodePageBlock(d *xml.Decoder, start xml.StartElement) error {
for {
tok, err := d.Token()
if err != nil {
return err
}
switch node := tok.(type) {
case xml.StartElement:
if err := p.decodeObject(d, node); err != nil {
return err
}
case xml.EndElement:
if node.Name.Local == start.Name.Local {
return nil
}
}
}
}
// attrValue 获取XML属性值
// 入参: start 起始节点, name 属性名
// 返回: string 属性值
+20
View File
@@ -103,10 +103,30 @@ type TextObject struct {
type FillColor struct {
Value string `xml:"Value,attr"`
Alpha *int `xml:"Alpha,attr"`
Pattern *Pattern `xml:"Pattern"`
AxialShd *AxialShd `xml:"AxialShd"`
RadialShd *RadialShd `xml:"RadialShd"`
}
// Pattern 图案填充
type Pattern struct {
Width float64 `xml:"Width,attr"`
Height float64 `xml:"Height,attr"`
XStep float64 `xml:"XStep,attr"`
YStep float64 `xml:"YStep,attr"`
CTM string `xml:"CTM,attr"`
CellContent PatternContent `xml:"CellContent"`
}
// PatternContent 图案单元内容
type PatternContent struct {
Objects []GraphicObject `xml:"-"`
TextObject []TextObject `xml:"TextObject"`
PathObject []PathObject `xml:"PathObject"`
ImageObject []ImageObject `xml:"ImageObject"`
CompositeGraphicUnit []CompositeGraphicUnit `xml:"CompositeGraphicUnit"`
}
// TextCode 文本内容节点
type TextCode struct {
X string `xml:"X,attr"`
+81 -1
View File
@@ -551,6 +551,8 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
fillColor, strokeColor := colorWithAlpha(defaultFill, obj.Alpha), colorWithAlpha(defaultStroke, obj.Alpha)
var fillPaint any = fillColor
var strokePaint any = strokeColor
var fillPattern *Pattern
var fillPatternColor color.Color
lineWidth := defaultLW
if lineWidth == 0 {
lineWidth = 0.353
@@ -566,6 +568,8 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
}
if dp.FillColor != nil {
fillColorNode := withFillAlpha(dp.FillColor, obj.Alpha)
fillPattern = fillColorNode.Pattern
fillPatternColor = patternColor(fillColorNode)
fillColor = parseFillColor(fillColorNode)
fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0)
}
@@ -595,6 +599,8 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
}
if obj.FillColor != nil {
fillColorNode := withFillAlpha(obj.FillColor, obj.Alpha)
fillPattern = fillColorNode.Pattern
fillPatternColor = patternColor(fillColorNode)
fillColor = parseFillColor(fillColorNode)
fillPaint = parseFillPaint(fillColorNode, bx, by, pageH, 0, 0)
}
@@ -644,7 +650,15 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
if fillPaint == nil {
fillPaint = fillColor
}
if shouldFill && fillPaint != nil {
if shouldFill && fillPattern != nil {
fp := p
if clipPath != nil {
fp = p.Copy()
fp.Close()
fp = fp.And(clipPath)
}
r.renderPattern(ctx, fillPattern, fillPatternColor, pageH, fp, ctm)
} else if shouldFill && fillPaint != nil {
ctx.SetFill(fillPaint)
ctx.SetStrokeColor(canvas.Transparent)
fp := p
@@ -691,6 +705,72 @@ func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64
ctx.Pop()
}
// renderPattern 渲染图案填充
// 入参: ctx 画布上下文, pattern 图案对象, defaultColor 默认颜色, pageH 页面高度, clip 填充区域, parentCTM 父级CTM
func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *Pattern, defaultColor color.Color, pageH float64, clip *canvas.Path, parentCTM Matrix) {
if pattern == nil || clip == nil || len(pattern.CellContent.Objects) == 0 {
return
}
xStep, yStep := pattern.XStep, pattern.YStep
if xStep == 0 {
xStep = pattern.Width
}
if yStep == 0 {
yStep = pattern.Height
}
if xStep <= 0 || yStep <= 0 {
return
}
patternCTM := parentCTM.Multiply(NewMatrix(pattern.CTM))
invCTM, ok := patternCTM.Invert()
if !ok {
return
}
bounds := clip.FastBounds()
points := [][2]float64{
{bounds.X0, pageH - bounds.Y0},
{bounds.X1, pageH - bounds.Y0},
{bounds.X1, pageH - bounds.Y1},
{bounds.X0, pageH - bounds.Y1},
}
minX, maxX := 0.0, 0.0
minY, maxY := 0.0, 0.0
for i, point := range points {
x, y := invCTM.Transform(point[0], point[1])
if i == 0 {
minX, maxX = x, x
minY, maxY = y, y
continue
}
minX = math.Min(minX, x)
maxX = math.Max(maxX, x)
minY = math.Min(minY, y)
maxY = math.Max(maxY, y)
}
startX := int(math.Floor(minX/xStep)) - 1
endX := int(math.Ceil(maxX/xStep)) + 1
startY := int(math.Floor(minY/yStep)) - 1
endY := int(math.Ceil(maxY/yStep)) + 1
for ix := startX; ix <= endX; ix++ {
for iy := startY; iy <= endY; iy++ {
tileCTM := patternCTM.Multiply(TranslationMatrix(float64(ix)*xStep, float64(iy)*yStep))
for _, obj := range pattern.CellContent.Objects {
r.renderObject(ctx, obj, pageH, defaultColor, defaultColor, 0, &tileCTM)
}
}
}
}
// patternColor 获取图案单元默认颜色
// 入参: fillColor 填充颜色节点
// 返回: color.Color 默认颜色
func patternColor(fillColor *FillColor) color.Color {
if fillColor == nil || strings.TrimSpace(fillColor.Value) == "" {
return nil
}
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
}
// renderText 渲染文本
// 入参: ctx 画布上下文, obj 文本对象, pageH 页面高度, defaultFill 默认填充色, defaultStroke 默认描边色, parentCTM 父级CTM
func (r *Renderer) renderText(ctx *canvas.Context, obj TextObject, pageH float64, defaultFill, defaultStroke color.Color, parentCTM *Matrix) {
+6
View File
@@ -132,6 +132,9 @@ func parseFillColor(fillColor *FillColor) color.Color {
if fillColor == nil {
return nil
}
if fillColor.Pattern != nil {
return nil
}
if strings.TrimSpace(fillColor.Value) != "" {
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
}
@@ -148,6 +151,9 @@ func parseFillPaint(fillColor *FillColor, x, y, pageH, originX, originY float64)
if fillColor == nil {
return nil
}
if fillColor.Pattern != nil {
return nil
}
if strings.TrimSpace(fillColor.Value) != "" {
return parseColorWithAlpha(fillColor.Value, fillColor.Alpha)
}