From b778ab9d9043eb17540795ba4815d5da6386576e Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 1 Jul 2026 08:52:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=A2=9E=E5=BC=BA=E6=B8=B2=E6=9F=93):=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81Pattern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ofdgo_geom.go | 24 +++++++++++++ ofdgo_object.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++ ofdgo_page.go | 20 +++++++++++ ofdgo_renderer.go | 82 +++++++++++++++++++++++++++++++++++++++++++- ofdgo_text.go | 6 ++++ 5 files changed, 217 insertions(+), 1 deletion(-) diff --git a/ofdgo_geom.go b/ofdgo_geom.go index 087b483..c1e7acd 100644 --- a/ofdgo_geom.go +++ b/ofdgo_geom.go @@ -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 { diff --git a/ofdgo_object.go b/ofdgo_object.go index ba640da..0c42547 100644 --- a/ofdgo_object.go +++ b/ofdgo_object.go @@ -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 属性值 diff --git a/ofdgo_page.go b/ofdgo_page.go index 1bedf39..e7fa7f1 100644 --- a/ofdgo_page.go +++ b/ofdgo_page.go @@ -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"` diff --git a/ofdgo_renderer.go b/ofdgo_renderer.go index f8db016..932eecc 100644 --- a/ofdgo_renderer.go +++ b/ofdgo_renderer.go @@ -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) { diff --git a/ofdgo_text.go b/ofdgo_text.go index d1e178a..400af31 100644 --- a/ofdgo_text.go +++ b/ofdgo_text.go @@ -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) }