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 @@ + \ 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(`