From 3cb33d48d32d580a77d3712f2db2be1388a00cac Mon Sep 17 00:00:00 2001 From: Justin Kromlinger Date: Fri, 12 Oct 2018 18:43:30 +0200 Subject: [PATCH] Add ability to set custom stylesheets for SVG renderer (#105) * Add ability to set custom stylesheets for SVG renderer This allow to set custom inline CSS and a optional CSP nonce. This solves the problem mentioned in #103 and is best used with it, as seen in the added examples. Without this one would have to write a custom renderer. * Add note with link to the custom_stylesheets example --- _examples/css_classes/main.go | 2 + _examples/custom_stylesheets/inlineOutput.svg | 21 +++++ _examples/custom_stylesheets/main.go | 87 +++++++++++++++++++ vector_renderer.go | 30 +++++++ vector_renderer_test.go | 28 ++++++ 5 files changed, 168 insertions(+) create mode 100644 _examples/custom_stylesheets/inlineOutput.svg create mode 100644 _examples/custom_stylesheets/main.go diff --git a/_examples/css_classes/main.go b/_examples/css_classes/main.go index d650e96..5046b72 100644 --- a/_examples/css_classes/main.go +++ b/_examples/css_classes/main.go @@ -7,6 +7,8 @@ import ( "net/http" ) +// Note: Additional examples on how to add Stylesheets are in the custom_stylesheets example + func inlineSVGWithClasses(res http.ResponseWriter, req *http.Request) { res.Write([]byte( "" + diff --git a/_examples/custom_stylesheets/inlineOutput.svg b/_examples/custom_stylesheets/inlineOutput.svg new file mode 100644 index 0000000..fdb2515 --- /dev/null +++ b/_examples/custom_stylesheets/inlineOutput.svg @@ -0,0 +1,21 @@ +\nBlueGreenGray \ No newline at end of file diff --git a/_examples/custom_stylesheets/main.go b/_examples/custom_stylesheets/main.go new file mode 100644 index 0000000..2432b2d --- /dev/null +++ b/_examples/custom_stylesheets/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "fmt" + "github.com/hashworks/go-chart" + "log" + "net/http" +) + +const style = "svg .background { fill: white; }" + + "svg .canvas { fill: white; }" + + "svg path.blue { fill: blue; stroke: lightblue; }" + + "svg path.green { fill: green; stroke: lightgreen; }" + + "svg path.gray { fill: gray; stroke: lightgray; }" + + "svg text.blue { fill: white; }" + + "svg text.green { fill: white; }" + + "svg text.gray { fill: white; }" + +func svgWithCustomInlineCSS(res http.ResponseWriter, _ *http.Request) { + res.Header().Set("Content-Type", chart.ContentTypeSVG) + + // Render the CSS with custom css + err := pieChart().Render(chart.SVGWithCSS(style, ""), res) + if err != nil { + fmt.Printf("Error rendering pie chart: %v\n", err) + } +} + +func svgWithCustomInlineCSSNonce(res http.ResponseWriter, _ *http.Request) { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src + // This should be randomly generated on every request! + const nonce = "RAND0MBASE64" + + res.Header().Set("Content-Security-Policy", fmt.Sprintf("style-src 'nonce-%s'", nonce)) + res.Header().Set("Content-Type", chart.ContentTypeSVG) + + // Render the CSS with custom css and a nonce. + // Try changing the nonce to a different string - your browser should block the CSS. + err := pieChart().Render(chart.SVGWithCSS(style, nonce), res) + if err != nil { + fmt.Printf("Error rendering pie chart: %v\n", err) + } +} + +func svgWithCustomExternalCSS(res http.ResponseWriter, _ *http.Request) { + // Add external CSS + res.Write([]byte( + ``+ + ``+ + ``)) + + res.Header().Set("Content-Type", chart.ContentTypeSVG) + err := pieChart().Render(chart.SVG, res) + if err != nil { + fmt.Printf("Error rendering pie chart: %v\n", err) + } +} + +func pieChart() chart.PieChart { + return chart.PieChart{ + // Note that setting ClassName will cause all other inline styles to be dropped! + Background: chart.Style{ClassName: "background"}, + Canvas: chart.Style{ + ClassName: "canvas", + }, + Width: 512, + Height: 512, + Values: []chart.Value{ + {Value: 5, Label: "Blue", Style: chart.Style{ClassName: "blue"}}, + {Value: 5, Label: "Green", Style: chart.Style{ClassName: "green"}}, + {Value: 4, Label: "Gray", Style: chart.Style{ClassName: "gray"}}, + }, + } +} + +func css(res http.ResponseWriter, req *http.Request) { + res.Header().Set("Content-Type", "text/css") + res.Write([]byte(style)) +} + +func main() { + http.HandleFunc("/", svgWithCustomInlineCSS) + http.HandleFunc("/nonce", svgWithCustomInlineCSSNonce) + http.HandleFunc("/external", svgWithCustomExternalCSS) + http.HandleFunc("/main.css", css) + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/vector_renderer.go b/vector_renderer.go index c154424..71c6a86 100644 --- a/vector_renderer.go +++ b/vector_renderer.go @@ -28,6 +28,25 @@ func SVG(width, height int) (Renderer, error) { }, nil } +// SVGWithCSS returns a new png/raster renderer with attached custom CSS +// The optional nonce argument sets a CSP nonce. +func SVGWithCSS(css string, nonce string) (func(width, height int)(Renderer, error)) { + return func(width, height int) (Renderer, error) { + buffer := bytes.NewBuffer([]byte{}) + canvas := newCanvas(buffer) + canvas.css = css + canvas.nonce = nonce + canvas.Start(width, height) + return &vectorRenderer{ + b: buffer, + c: canvas, + s: &Style{}, + p: []string{}, + dpi: DefaultDPI, + }, nil + } +} + // vectorRenderer renders chart commands to a bitmap. type vectorRenderer struct { dpi float64 @@ -222,12 +241,23 @@ type canvas struct { textTheta *float64 width int height int + css string + nonce string } func (c *canvas) Start(width, height int) { c.width = width c.height = height c.w.Write([]byte(fmt.Sprintf(`\n`, c.width, c.height))) + if c.css != "" { + c.w.Write([]byte(``, c.css))) + } } func (c *canvas) Path(d string, style Style) { diff --git a/vector_renderer_test.go b/vector_renderer_test.go index 19c38b6..d937d2d 100644 --- a/vector_renderer_test.go +++ b/vector_renderer_test.go @@ -2,6 +2,7 @@ package chart import ( "bytes" + "fmt" "strings" "testing" @@ -89,3 +90,30 @@ func TestCanvasClassSVG(t *testing.T) { as.Equal("class=\"test-class\"", canvas.styleAsSVG(set)) } + +func TestCanvasCustomInlineStylesheet(t *testing.T) { + b := strings.Builder{} + + canvas := &canvas{ + w: &b, + css: ".background { fill: red }", + } + + canvas.Start(200, 200) + + assert.New(t).Contains(b.String(), fmt.Sprintf(``, canvas.css)) +} + +func TestCanvasCustomInlineStylesheetWithNonce(t *testing.T) { + b := strings.Builder{} + + canvas := &canvas{ + w: &b, + css: ".background { fill: red }", + nonce: "RAND0MSTRING", + } + + canvas.Start(200, 200) + + assert.New(t).Contains(b.String(), fmt.Sprintf(``, canvas.nonce, canvas.css)) +} \ No newline at end of file