Files
ofdgo/ofdgo_renderer_path.go
T

669 lines
21 KiB
Go

// 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 (
"image/color"
"math"
"strconv"
"strings"
"github.com/tdewolff/canvas"
)
const defaultPathLineWidth = 0.353
const defaultMiterLimit = 3.528
// pathStyle 路径绘制样式
type pathStyle struct {
fillColor color.Color
strokeColor color.Color
fillPaint any
strokePaint any
fillPattern *patternPaint
strokePattern *patternPaint
lineWidth float64
lineCap canvas.Capper
lineJoin canvas.Joiner
miterLimit float64
dashOffset float64
dashPattern []float64
}
// newPathStyle 创建路径样式
// 入参: dp 默认绘制参数, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
// 返回: pathStyle 路径样式
func (r *Renderer) newPathStyle(dp *DrawParam, bx, by, pageH float64, alpha *int) pathStyle {
style := pathStyle{
lineWidth: defaultPathLineWidth,
lineCap: canvas.ButtCap,
lineJoin: canvas.MiterJoiner{GapJoiner: canvas.BevelJoin, Limit: defaultMiterLimit},
miterLimit: defaultMiterLimit,
}
if dp != nil {
style.applyDrawParam(r, dp, bx, by, pageH, alpha)
}
return style
}
// applyFillColor 应用填充颜色
// 入参: r 渲染器, fill 填充颜色, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
func (s *pathStyle) applyFillColor(r *Renderer, fill *FillColor, bx, by, pageH float64, alpha *int) {
fillColorNode := withFillAlpha(fill, alpha)
s.fillPattern = parsePatternPaint(fillColorNode)
s.fillColor = r.parseFillColor(fillColorNode)
s.fillPaint = r.parseFillPaint(fillColorNode, bx, by, pageH)
}
// applyStrokeColor 应用描边颜色
// 入参: r 渲染器, stroke 描边颜色, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
func (s *pathStyle) applyStrokeColor(r *Renderer, stroke *StrokeColor, bx, by, pageH float64, alpha *int) {
strokeColorNode := withStrokeAlpha(stroke, alpha)
s.strokePattern = parsePatternPaint((*FillColor)(strokeColorNode))
s.strokeColor = r.parseStrokeColor(strokeColorNode)
s.strokePaint = r.parseStrokePaint(strokeColorNode, bx, by, pageH)
}
// pathLineCap 转换线帽样式
// 入参: cap 线帽名称, fallback 默认线帽
// 返回: canvas.Capper 线帽样式
func pathLineCap(cap string, fallback canvas.Capper) canvas.Capper {
switch cap {
case "Butt":
return canvas.ButtCap
case "Round":
return canvas.RoundCap
case "Square":
return canvas.SquareCap
}
return fallback
}
// applyLineJoin 应用线连接样式及尖角限制
// 入参: join 线连接名称, limit 尖角限制
func (s *pathStyle) applyLineJoin(join string, limit float64) {
if limit > 0 {
s.miterLimit = limit
}
switch join {
case "Miter":
s.lineJoin = canvas.MiterJoin
case "Round":
s.lineJoin = canvas.RoundJoin
case "Bevel":
s.lineJoin = canvas.BevelJoin
}
if _, ok := s.lineJoin.(canvas.MiterJoiner); ok {
s.lineJoin = canvas.MiterJoiner{GapJoiner: canvas.BevelJoin, Limit: s.miterLimit}
}
}
// applyDrawParam 应用绘制参数样式
// 入参: r 渲染器, dp 绘制参数, bx 边界X坐标, by 边界Y坐标, pageH 页面高度, alpha 对象透明度
func (s *pathStyle) applyDrawParam(r *Renderer, dp *DrawParam, bx, by, pageH float64, alpha *int) {
if dp.LineWidth > 0 {
s.lineWidth = dp.LineWidth
}
if dp.FillColor != nil {
s.applyFillColor(r, dp.FillColor, bx, by, pageH, alpha)
}
if dp.StrokeColor != nil {
s.applyStrokeColor(r, dp.StrokeColor, bx, by, pageH, alpha)
}
if dp.Cap != "" {
s.lineCap = pathLineCap(dp.Cap, s.lineCap)
}
s.applyLineJoin(dp.Join, dp.MiterLimit)
if dp.DashPattern != "" {
s.dashPattern = parseFloats(dp.DashPattern)
}
if dp.DashOffset != nil {
s.dashOffset = *dp.DashOffset
}
}
// applyPathObject 应用路径对象样式
// 入参: r 渲染器, obj 路径对象, bx 边界X坐标, by 边界Y坐标, pageH 页面高度
func (s *pathStyle) applyPathObject(r *Renderer, obj PathObject, bx, by, pageH float64) {
if obj.LineWidth > 0 {
s.lineWidth = obj.LineWidth
}
if obj.FillColor != nil {
s.applyFillColor(r, obj.FillColor, bx, by, pageH, obj.Alpha)
}
if obj.StrokeColor != nil {
s.applyStrokeColor(r, obj.StrokeColor, bx, by, pageH, obj.Alpha)
}
if obj.Cap != "" {
s.lineCap = pathLineCap(obj.Cap, canvas.ButtCap)
}
s.applyLineJoin(obj.Join, obj.MiterLimit)
if obj.DashPattern != "" {
s.dashPattern = parseFloats(obj.DashPattern)
}
if obj.DashOffset != nil {
s.dashOffset = *obj.DashOffset
}
}
// scale 应用路径变换缩放
// 入参: ctm 变换矩阵
func (s *pathStyle) scale(ctm Matrix) {
if scale := math.Sqrt(math.Abs(ctm.a*ctm.d - ctm.b*ctm.c)); scale > 0 {
s.lineWidth *= scale
s.dashOffset *= scale
for i := range s.dashPattern {
s.dashPattern[i] *= scale
}
}
}
// renderPath 渲染路径
// 入参: ctx 画布上下文, obj 路径对象, pageH 页面高度, defaults 默认绘制参数, parentCTM 父级CTM, boundaryInCTM 边界是否参与CTM变换, parentClip 父级裁剪路径
func (r *Renderer) renderPath(ctx *canvas.Context, obj PathObject, pageH float64, defaults *DrawParam, parentCTM *Matrix, boundaryInCTM bool, parentClip *canvas.Path) {
if r.pageText != nil {
return
}
if obj.Visible != nil && !*obj.Visible {
return
}
ctx.Push()
bx, by := 0.0, 0.0
if obj.Boundary != "" {
if box, err := ParseBox(obj.Boundary); err == nil {
bx, by = box.X, box.Y
}
}
localCTM := NewMatrix(obj.CTM)
ctm := localCTM
if parentCTM != nil {
ctm = parentCTM.Multiply(ctm)
}
style := r.newPathStyle(defaults, bx, by, pageH, obj.Alpha)
if obj.DrawParam != "" {
if dp := r.getDrawParam(obj.DrawParam, nil); dp != nil {
style.applyDrawParam(r, dp, bx, by, pageH, obj.Alpha)
}
}
style.applyPathObject(r, obj, bx, by, pageH)
style.scale(ctm)
objectCTM := TranslationMatrix(bx, by).Multiply(ctm)
if boundaryInCTM && parentCTM != nil {
objectCTM = parentCTM.Multiply(TranslationMatrix(bx, by)).Multiply(localCTM)
}
transformShdPaint(style.fillPaint, parentCTM, bx, by, pageH, boundaryInCTM)
transformShdPaint(style.strokePaint, parentCTM, bx, by, pageH, boundaryInCTM)
pathCTM := ctm
if boundaryInCTM {
pathCTM = localCTM
}
p := r.buildPath(obj, pageH, pathCTM, false)
if rectPath := r.buildTinyFillRectPath(obj, pageH, pathCTM, bx, by); rectPath != nil {
p = rectPath
}
if boundaryInCTM && parentCTM != nil {
p = p.Transform(canvas.Matrix{
{parentCTM.a, -parentCTM.c, parentCTM.c*pageH + parentCTM.e},
{-parentCTM.b, parentCTM.d, pageH*(1-parentCTM.d) - parentCTM.f},
})
}
clipPath := intersectClipPath(parentClip, r.buildObjectClipPath(obj.Clips, pageH, bx, by, localCTM, parentCTM, boundaryInCTM))
shouldFill := false
if obj.Fill != nil {
shouldFill = *obj.Fill
}
if style.fillPaint == nil {
style.fillPaint = style.fillColor
}
if shouldFill {
fillPaint, shadingClip, fillView := resolveShdPaint(ctx, style.fillPaint)
fillClip := intersectClipPath(clipPath, shadingClip)
fp := p
_, repeat := fillPaint.(*repeatAxialGradient)
if fillClip != nil || repeat {
paths := canvas.Paths(p.Copy().Split())
for _, path := range paths {
path.Close()
}
fp = paths.Merge()
}
fillRule := canvas.NonZero
if obj.Rule == "Even-Odd" {
fillRule = canvas.EvenOdd
if fillClip != nil || style.fillPattern != nil {
fp = fp.Settle(fillRule)
}
}
if fillClip != nil {
fp = applyClipPath(fp, fillClip)
}
if style.fillPattern != nil {
r.renderPattern(ctx, style.fillPattern, pageH, fp, objectCTM)
} else if style.fillPaint != nil {
ctx.SetFillRule(fillRule)
ctx.SetFill(fillPaint)
ctx.SetStrokeColor(canvas.Transparent)
drawShdPath(ctx, fp, fillView)
}
}
shouldStroke := true
if obj.Stroke != nil {
shouldStroke = *obj.Stroke
}
if shouldStroke {
ctx.SetFillRule(canvas.NonZero)
if style.strokePaint == nil {
style.strokePaint = style.strokeColor
}
if style.strokePaint == nil {
style.strokePaint = colorWithAlpha(canvas.Black, obj.Alpha)
}
strokePaint, shadingClip, strokeView := resolveShdPaint(ctx, style.strokePaint)
ctx.SetFillColor(canvas.Transparent)
ctx.SetStroke(strokePaint)
ctx.SetStrokeWidth(style.lineWidth)
ctx.SetStrokeCapper(style.lineCap)
ctx.SetStrokeJoiner(style.lineJoin)
sp := p
if len(style.dashPattern) > 0 {
sp = sp.Dash(style.dashOffset, style.dashPattern...)
ctx.SetDashes(0)
}
strokeClip := intersectClipPath(clipPath, shadingClip)
_, repeat := strokePaint.(*repeatAxialGradient)
if strokeClip != nil || strokeView != canvas.Identity || repeat || style.strokePattern != nil {
sp = sp.Copy()
sp = sp.Stroke(style.lineWidth, style.lineCap, style.lineJoin, canvas.Tolerance)
sp = applyClipPath(sp, strokeClip)
if style.strokePattern != nil {
r.renderPattern(ctx, style.strokePattern, pageH, sp, objectCTM)
} else {
ctx.SetFill(strokePaint)
ctx.SetStrokeColor(canvas.Transparent)
drawShdPath(ctx, sp, strokeView)
}
} else {
ctx.DrawPath(0, 0, sp)
}
}
ctx.Pop()
}
// renderPattern 渲染图案填充
// 入参: ctx 画布上下文, pattern 底纹画刷, pageH 页面高度, clip 绘制区域, objectCTM 对象变换矩阵
func (r *Renderer) renderPattern(ctx *canvas.Context, pattern *patternPaint, pageH float64, clip *canvas.Path, objectCTM Matrix) {
if clip == nil || clip.Empty() || len(pattern.CellContent.Objects) == 0 {
return
}
xStep, yStep := pattern.XStep, pattern.YStep
if xStep < pattern.Width {
xStep = pattern.Width
}
if yStep < pattern.Height {
yStep = pattern.Height
}
if xStep <= 0 || yStep <= 0 {
return
}
patternCTM := NewMatrix(pattern.CTM)
if pattern.RelativeTo != "Page" {
patternCTM = objectCTM.Multiply(patternCTM)
}
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
defaults := &DrawParam{FillColor: &pattern.color, StrokeColor: (*StrokeColor)(&pattern.color)}
for ix := startX; ix <= endX; ix++ {
for iy := startY; iy <= endY; iy++ {
tileCTM := patternCTM.Multiply(TranslationMatrix(float64(ix)*xStep, float64(iy)*yStep))
if ix%2 != 0 && (pattern.ReflectMethod == "Column" || pattern.ReflectMethod == "RowAndColumn") {
tileCTM = tileCTM.Multiply(Matrix{a: -1, d: 1, e: pattern.Width})
}
if iy%2 != 0 && (pattern.ReflectMethod == "Row" || pattern.ReflectMethod == "RowAndColumn") {
tileCTM = tileCTM.Multiply(Matrix{a: 1, d: -1, f: pattern.Height})
}
for _, obj := range pattern.CellContent.Objects {
obj = mergeGraphicObjectAlpha(obj, pattern.alpha)
r.renderObject(ctx, &obj, pageH, defaults, &tileCTM, true, clip)
}
}
}
}
// buildPath 解析路径并返回Canvas Path
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, boundaryInCTM 边界是否参与CTM变换
// 返回: *canvas.Path 路径对象
func (r *Renderer) buildPath(obj PathObject, pageH float64, ctm Matrix, boundaryInCTM bool) *canvas.Path {
bx, by := 0.0, 0.0
if obj.Boundary != "" {
if box, err := ParseBox(obj.Boundary); err == nil {
bx, by = box.X, box.Y
}
}
if boundaryInCTM {
ctm = ctm.Multiply(TranslationMatrix(bx, by))
} else {
ctm = TranslationMatrix(bx, by).Multiply(ctm)
}
p := &canvas.Path{}
tokens := strings.Fields(obj.AbbreviatedData)
for i := 0; i < len(tokens); {
cmd := tokens[i]
i++
switch cmd {
case "M", "S":
if i+1 < len(tokens) {
x, _ := strconv.ParseFloat(tokens[i], 64)
y, _ := strconv.ParseFloat(tokens[i+1], 64)
p.MoveTo(x, y)
i += 2
}
case "L":
if i+1 < len(tokens) {
x, _ := strconv.ParseFloat(tokens[i], 64)
y, _ := strconv.ParseFloat(tokens[i+1], 64)
p.LineTo(x, y)
i += 2
}
case "B":
if i+5 < len(tokens) {
x1, _ := strconv.ParseFloat(tokens[i], 64)
y1, _ := strconv.ParseFloat(tokens[i+1], 64)
x2, _ := strconv.ParseFloat(tokens[i+2], 64)
y2, _ := strconv.ParseFloat(tokens[i+3], 64)
x3, _ := strconv.ParseFloat(tokens[i+4], 64)
y3, _ := strconv.ParseFloat(tokens[i+5], 64)
p.CubeTo(x1, y1, x2, y2, x3, y3)
i += 6
}
case "Q":
if i+3 < len(tokens) {
x1, _ := strconv.ParseFloat(tokens[i], 64)
y1, _ := strconv.ParseFloat(tokens[i+1], 64)
x2, _ := strconv.ParseFloat(tokens[i+2], 64)
y2, _ := strconv.ParseFloat(tokens[i+3], 64)
p.QuadTo(x1, y1, x2, y2)
i += 4
}
case "A":
if i+6 < len(tokens) {
rx, _ := strconv.ParseFloat(tokens[i], 64)
ry, _ := strconv.ParseFloat(tokens[i+1], 64)
rot, _ := strconv.ParseFloat(tokens[i+2], 64)
large, _ := strconv.ParseBool(tokens[i+3])
sweep, _ := strconv.ParseBool(tokens[i+4])
x, _ := strconv.ParseFloat(tokens[i+5], 64)
y, _ := strconv.ParseFloat(tokens[i+6], 64)
p.ArcTo(rx, ry, rot, large, sweep, x, y)
i += 7
}
case "C":
p.Close()
}
}
if canvas.Equal(ctm.a*ctm.d-ctm.b*ctm.c, 0) {
p = p.ReplaceArcs()
}
return p.Transform(canvas.Matrix{{ctm.a, ctm.c, ctm.e}, {-ctm.b, -ctm.d, pageH - ctm.f}})
}
// buildObjectClipPath 构建对象裁剪路径并应用父级变换
// 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM, parentCTM 父级CTM, boundaryInCTM 边界是否参与父级CTM
// 返回: *canvas.Path 路径对象
func (r *Renderer) buildObjectClipPath(clips *Clips, pageH float64, bx, by float64, objectCTM Matrix, parentCTM *Matrix, boundaryInCTM bool) *canvas.Path {
if !boundaryInCTM && parentCTM != nil {
objectCTM = parentCTM.Multiply(objectCTM)
}
p := r.buildClipPath(clips, pageH, bx, by, objectCTM)
if p != nil && boundaryInCTM && parentCTM != nil {
p = p.Transform(canvas.Matrix{
{parentCTM.a, -parentCTM.c, parentCTM.c*pageH + parentCTM.e},
{-parentCTM.b, parentCTM.d, pageH*(1-parentCTM.d) - parentCTM.f},
})
}
return p
}
// buildClipPath 构建裁剪路径
// 入参: clips 裁剪对象, pageH 页面高度, bx 边界X坐标, by 边界Y坐标, objectCTM 对象CTM
// 返回: *canvas.Path 路径对象
func (r *Renderer) buildClipPath(clips *Clips, pageH float64, bx, by float64, objectCTM Matrix) *canvas.Path {
if clips == nil {
return nil
}
var p *canvas.Path
for _, clip := range clips.Clip {
renderer := &clipRenderer{}
for _, area := range clip.Area {
areaCTM := NewMatrix(area.CTM)
if clips.TransFlag == nil || *clips.TransFlag {
areaCTM = objectCTM.Multiply(areaCTM)
}
for _, pathObj := range area.Path {
ctm := areaCTM.Multiply(NewMatrix(pathObj.CTM))
cp := r.buildPath(pathObj, pageH, ctm, true)
cp.Translate(bx, -by)
cp.Close()
if pathObj.Rule == "Even-Odd" {
cp = cp.Settle(canvas.EvenOdd)
}
renderer.add(cp)
}
for _, textObj := range area.Text {
box, _ := ParseBox(textObj.Boundary)
ctm := NewMatrix(textObj.CTM)
m := areaCTM.Multiply(TranslationMatrix(box.X, box.Y)).Multiply(ctm)
ctx := canvas.NewContext(renderer)
ctx.SetView(canvas.Matrix{{m.a, -m.c, bx + m.e}, {-m.b, m.d, pageH - by - m.f}})
renderer.clip = r.buildObjectClipPath(textObj.Clips, pageH, box.X, box.Y, ctm, &areaCTM, true)
if renderer.clip != nil {
renderer.clip = renderer.clip.Translate(bx, -by)
}
textObj.Boundary, textObj.CTM = "", ""
textObj.Clips = nil
textObj.Alpha = nil
textObj.FillColor = &FillColor{Value: "0 0 0"}
textObj.StrokeColor = &StrokeColor{Value: "0 0 0"}
textRenderer := *r
textRenderer.pageText = nil
textRenderer.renderText(ctx, textObj, 0, r.drawParamDefaults(area.DrawParam, nil), nil, false, nil)
}
}
if renderer.path == nil {
renderer.path = &canvas.Path{}
}
p = intersectClipPath(p, renderer.path)
}
return p
}
// intersectClipPath 求裁剪路径交集
// 入参: parent 父级裁剪路径, current 当前裁剪路径
// 返回: *canvas.Path 相交后的裁剪路径
func intersectClipPath(parent, current *canvas.Path) *canvas.Path {
if parent == nil {
return current
}
if current == nil {
return parent
}
parentRect, parentOK := rectangularPath(parent)
currentRect, currentOK := rectangularPath(current)
if parentOK && currentOK {
return parentRect.And(currentRect).ToPath()
}
if parent.Empty() || current.Empty() {
return &canvas.Path{}
}
if parentOK && parentRect.Contains(current.FastBounds()) {
return current
}
if currentOK && currentRect.Contains(parent.FastBounds()) {
return parent
}
return parent.And(current)
}
// unionClipPath 合并裁剪区域
// 入参: left 左侧裁剪路径, right 右侧裁剪路径
// 返回: *canvas.Path 合并后的裁剪路径
func unionClipPath(left, right *canvas.Path) *canvas.Path {
leftRect, leftOK := rectangularPath(left)
rightRect, rightOK := rectangularPath(right)
if leftOK && rightOK {
bounds := leftRect.Add(rightRect)
area := leftRect.Area() + rightRect.Area() - leftRect.And(rightRect).Area()
if canvas.Equal(area, bounds.Area()) {
return bounds.ToPath()
}
}
return left.Or(right)
}
// applyClipPath 应用裁剪路径
// 入参: path 绘制路径, clip 裁剪路径
// 返回: *canvas.Path 裁剪后的绘制路径
func applyClipPath(path, clip *canvas.Path) *canvas.Path {
if path == nil || clip == nil {
return path
}
if path.Empty() || clip.Empty() {
return &canvas.Path{}
}
if rect, ok := rectangularPath(clip); ok {
bounds := path.FastBounds()
if rect.Contains(bounds) {
return path
}
if !rect.Overlaps(bounds) {
return &canvas.Path{}
}
if pathRect, ok := rectangularPath(path); ok {
return pathRect.And(rect).ToPath()
}
}
return path.And(clip)
}
// rectangularPath 获取矩形路径区域
// 入参: path 路径对象
// 返回: canvas.Rect 矩形区域, bool 是否为矩形
func rectangularPath(path *canvas.Path) (canvas.Rect, bool) {
if path == nil || path.HasSubpaths() || !path.Closed() {
return canvas.Rect{}, false
}
data := path.Data()
for i := 0; i < len(data); i += 4 {
if i+4 > len(data) || (data[i] != canvas.MoveToCmd && data[i] != canvas.LineToCmd && data[i] != canvas.CloseCmd) {
return canvas.Rect{}, false
}
}
points := path.Coords()
if len(points) != 5 || !points[0].Equals(points[4]) {
return canvas.Rect{}, false
}
rect := path.FastBounds()
corners := 0
for _, point := range points[:4] {
corner := 0
switch {
case canvas.Equal(point.X, rect.X0) && canvas.Equal(point.Y, rect.Y0):
corner = 1
case canvas.Equal(point.X, rect.X1) && canvas.Equal(point.Y, rect.Y0):
corner = 2
case canvas.Equal(point.X, rect.X1) && canvas.Equal(point.Y, rect.Y1):
corner = 4
case canvas.Equal(point.X, rect.X0) && canvas.Equal(point.Y, rect.Y1):
corner = 8
default:
return canvas.Rect{}, false
}
if corners&corner != 0 {
return canvas.Rect{}, false
}
corners |= corner
}
return rect, corners == 15
}
// buildTinyFillRectPath 构建微小填充矩形路径
// 入参: obj 路径对象, pageH 页面高度, ctm 变换矩阵, bx 边界X坐标, by 边界Y坐标
// 返回: *canvas.Path 路径对象
func (r *Renderer) buildTinyFillRectPath(obj PathObject, pageH float64, ctm Matrix, bx, by float64) *canvas.Path {
if obj.Fill == nil || !*obj.Fill || obj.Stroke == nil || *obj.Stroke {
return nil
}
box, err := ParseBox(obj.Boundary)
if err != nil || box.W <= 0 || box.H <= 0 || box.W > 0.6 || box.H > 0.6 {
return nil
}
tokens := strings.Fields(obj.AbbreviatedData)
if len(tokens) != 11 || tokens[0] != "M" || tokens[3] != "L" || tokens[6] != "L" || tokens[9] != "L" || tokens[10] != "C" {
return nil
}
points := make([][2]float64, 0, 4)
for i := 1; i < 10; i += 3 {
x, errX := strconv.ParseFloat(tokens[i], 64)
y, errY := strconv.ParseFloat(tokens[i+1], 64)
if errX != nil || errY != nil {
return nil
}
tx, ty := ctm.Transform(x, y)
points = append(points, [2]float64{tx + bx, pageH - (ty + by)})
}
minX, maxX := points[0][0], points[0][0]
minY, maxY := points[0][1], points[0][1]
for _, point := range points[1:] {
minX = math.Min(minX, point[0])
maxX = math.Max(maxX, point[0])
minY = math.Min(minY, point[1])
maxY = math.Max(maxY, point[1])
}
expand := math.Min(box.W, box.H) * 0.08
p := &canvas.Path{}
p.MoveTo(minX-expand, minY-expand)
p.LineTo(maxX+expand, minY-expand)
p.LineTo(maxX+expand, maxY+expand)
p.LineTo(minX-expand, maxY+expand)
p.Close()
return p
}