From f91a61944eaff37cc251268033a1bec3c72eca26 Mon Sep 17 00:00:00 2001 From: Mike Jensen Date: Sun, 19 Jan 2025 22:11:27 -0700 Subject: [PATCH] vector_renderer: SVG rendering performance gains by using bytes.Buffer Validated using benchmarks to both reduce memory pressure and be faster. --- chartdraw/vector_renderer.go | 126 ++++++++++++++++++------------ chartdraw/vector_renderer_test.go | 12 ++- 2 files changed, 87 insertions(+), 51 deletions(-) diff --git a/chartdraw/vector_renderer.go b/chartdraw/vector_renderer.go index 046f0eb..df7cd24 100644 --- a/chartdraw/vector_renderer.go +++ b/chartdraw/vector_renderer.go @@ -166,7 +166,7 @@ func (vr *vectorRenderer) FillStroke() { // drawPath draws the path set into the p slice. func (vr *vectorRenderer) drawPath() { - vr.c.Path(strings.Join(vr.p, "\n"), vr.s.GetFillAndStrokeOptions()) + vr.c.Path(vr.p, vr.s.GetFillAndStrokeOptions()) vr.p = vr.p[:0] // clear the path } @@ -267,52 +267,79 @@ func (c *canvas) Start(width, height int) { } } -func (c *canvas) Path(d string, style Style) { - if d == "" { +func (c *canvas) Path(parts []string, style Style) { + if len(parts) == 0 { return } - var strokeDashArrayProperty string - if len(style.StrokeDashArray) > 0 { - strokeDashArrayProperty = c.getStrokeDashArray(style) + bb := bytes.NewBuffer(make([]byte, 0, 80)) + bb.WriteString(``)) + bb.WriteString(`" `) + c.styleAsSVG(bb, style, false) + bb.WriteString(`/>`) + + _, _ = c.w.Write(bb.Bytes()) } func (c *canvas) Text(x, y int, body string, style Style) { if body == "" { return } - if c.textTheta == nil { - _, _ = c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style, true), body))) - } else { - transform := fmt.Sprintf(` transform="rotate(%0.2f,%d,%d)"`, RadiansToDegrees(*c.textTheta), x, y) - _, _ = c.w.Write([]byte(fmt.Sprintf(`%s`, x, y, c.styleAsSVG(style, true), transform, body))) + bb := bytes.NewBuffer(make([]byte, 0, 128)) + bb.WriteString(`') + bb.WriteString(body) + bb.WriteString("") + + _, _ = c.w.Write(bb.Bytes()) } func (c *canvas) Circle(x, y, r int, style Style) { - _, _ = c.w.Write([]byte(fmt.Sprintf(``, x, y, r, c.styleAsSVG(style, false)))) + bb := bytes.NewBuffer(make([]byte, 0, 80)) + bb.WriteString(``) + + _, _ = c.w.Write(bb.Bytes()) } func (c *canvas) End() { _, _ = c.w.Write([]byte("")) } -// getStrokeDashArray returns the stroke-dasharray property of a style. -func (c *canvas) getStrokeDashArray(s Style) string { +// writeStrokeDashArray writes the stroke-dasharray property of a style. +func (c *canvas) writeStrokeDashArray(bb *bytes.Buffer, s Style) { if len(s.StrokeDashArray) > 0 { - var sb strings.Builder - sb.WriteString("stroke-dasharray=\"") + bb.WriteString("stroke-dasharray=\"") for i, v := range s.StrokeDashArray { if i > 0 { - sb.WriteString(", ") + bb.WriteString(", ") } - sb.WriteString(fmt.Sprintf("%0.1f", v)) + bb.WriteString(fmt.Sprintf("%0.1f", v)) } - sb.WriteString("\"") - return sb.String() + bb.WriteString("\"") } - return "" } // GetFontFace returns the font face for the style. @@ -328,7 +355,7 @@ func (c *canvas) getFontFace(s Style) string { } // styleAsSVG returns the style as a svg style or class string. -func (c *canvas) styleAsSVG(s Style, applyText bool) string { +func (c *canvas) styleAsSVG(bb *bytes.Buffer, s Style, applyText bool) { sw := s.StrokeWidth sc := s.StrokeColor fc := s.FillColor @@ -336,55 +363,58 @@ func (c *canvas) styleAsSVG(s Style, applyText bool) string { fnc := s.FontColor if s.ClassName != "" { - var sb strings.Builder - sb.WriteString("class=\"") - sb.WriteString(s.ClassName) + bb.WriteString("class=\"") + bb.WriteString(s.ClassName) if !sc.IsZero() { - sb.WriteRune(' ') - sb.WriteString("stroke") + bb.WriteRune(' ') + bb.WriteString("stroke") } if !fc.IsZero() { - sb.WriteRune(' ') - sb.WriteString("fill") + bb.WriteRune(' ') + bb.WriteString("fill") } if applyText && (fs != 0 || s.Font != nil) { - sb.WriteRune(' ') - sb.WriteString("text") + bb.WriteRune(' ') + bb.WriteString("text") } - sb.WriteString("\"") - return sb.String() + bb.WriteString("\"") + return } - var pieces []string + bb.WriteString("style=\"") + if sw != 0 && !sc.IsTransparent() { - pieces = append(pieces, "stroke-width:"+formatFloatMinimized(sw)) - pieces = append(pieces, "stroke:"+sc.String()) + bb.WriteString("stroke-width:") + bb.WriteString(formatFloatMinimized(sw)) + bb.WriteString(";stroke:") + bb.WriteString(sc.String()) } else { - pieces = append(pieces, "stroke:none") + bb.WriteString("stroke:none") } if applyText && !fnc.IsTransparent() { - pieces = append(pieces, "fill:"+fnc.String()) + bb.WriteString(";fill:") + bb.WriteString(fnc.String()) } else if !fc.IsTransparent() { - pieces = append(pieces, "fill:"+fc.String()) + bb.WriteString(";fill:") + bb.WriteString(fc.String()) } else { - pieces = append(pieces, "fill:none") + bb.WriteString(";fill:none") } if applyText { if fs != 0 { - pieces = append(pieces, "font-size:"+formatFloatMinimized(drawing.PointsToPixels(c.dpi, fs))+"px") + bb.WriteString(";font-size:") + bb.WriteString(formatFloatMinimized(drawing.PointsToPixels(c.dpi, fs))) + bb.WriteString("px") } if s.Font != nil { - pieces = append(pieces, c.getFontFace(s)) + bb.WriteRune(';') + bb.WriteString(c.getFontFace(s)) } } - if len(pieces) == 0 { - return "" - } - - return "style=\"" + strings.Join(pieces, ";") + "\"" + bb.WriteRune('"') } // formatFloatNoTrailingZero formats a float without trailing zeros, so it is as small as possible. diff --git a/chartdraw/vector_renderer_test.go b/chartdraw/vector_renderer_test.go index 7bd3ecb..d284aa4 100644 --- a/chartdraw/vector_renderer_test.go +++ b/chartdraw/vector_renderer_test.go @@ -65,7 +65,9 @@ func TestCanvasStyleSVG(t *testing.T) { canvas := &canvas{dpi: DefaultDPI} - svgString := canvas.styleAsSVG(set, false) + var bb bytes.Buffer + canvas.styleAsSVG(&bb, set, false) + svgString := bb.String() assert.NotEmpty(t, svgString) assert.True(t, strings.HasPrefix(svgString, "style=\"")) assert.Contains(t, svgString, "stroke:white") @@ -75,7 +77,9 @@ func TestCanvasStyleSVG(t *testing.T) { assert.NotContains(t, svgString, "font-family") assert.True(t, strings.HasSuffix(svgString, "\"")) - svgString = canvas.styleAsSVG(set, true) + bb.Reset() + canvas.styleAsSVG(&bb, set, true) + svgString = bb.String() assert.True(t, strings.HasPrefix(svgString, "style=\"")) assert.Contains(t, svgString, "stroke:white") assert.Contains(t, svgString, "stroke-width:5") @@ -94,7 +98,9 @@ func TestCanvasClassSVG(t *testing.T) { canvas := &canvas{dpi: DefaultDPI} - assert.Equal(t, "class=\"test-class\"", canvas.styleAsSVG(set, false)) + var bb bytes.Buffer + canvas.styleAsSVG(&bb, set, false) + assert.Equal(t, "class=\"test-class\"", bb.String()) } func TestCanvasCustomInlineStylesheet(t *testing.T) {