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
This commit is contained in:
parent
96acfc6a9f
commit
3cb33d48d3
5 changed files with 168 additions and 0 deletions
|
@ -7,6 +7,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Note: Additional examples on how to add Stylesheets are in the custom_stylesheets example
|
||||||
|
|
||||||
func inlineSVGWithClasses(res http.ResponseWriter, req *http.Request) {
|
func inlineSVGWithClasses(res http.ResponseWriter, req *http.Request) {
|
||||||
res.Write([]byte(
|
res.Write([]byte(
|
||||||
"<!DOCTYPE html><html><head>" +
|
"<!DOCTYPE html><html><head>" +
|
||||||
|
|
21
_examples/custom_stylesheets/inlineOutput.svg
Normal file
21
_examples/custom_stylesheets/inlineOutput.svg
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="512" height="512">\n<style type="text/css"><![CDATA[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; }]]></style><path d="M 0 0
|
||||||
|
L 512 0
|
||||||
|
L 512 512
|
||||||
|
L 0 512
|
||||||
|
L 0 0" class="background"/><path d="M 5 5
|
||||||
|
L 507 5
|
||||||
|
L 507 507
|
||||||
|
L 5 507
|
||||||
|
L 5 5" class="canvas"/><path d="M 256 256
|
||||||
|
L 507 256
|
||||||
|
A 251 251 128.56 0 1 100 452
|
||||||
|
L 256 256
|
||||||
|
Z" class="blue"/><path d="M 256 256
|
||||||
|
L 100 452
|
||||||
|
A 251 251 128.56 0 1 201 12
|
||||||
|
L 256 256
|
||||||
|
Z" class="green"/><path d="M 256 256
|
||||||
|
L 201 12
|
||||||
|
A 251 251 102.85 0 1 506 256
|
||||||
|
L 256 256
|
||||||
|
Z" class="gray"/><text x="313" y="413" class="blue">Blue</text><text x="73" y="226" class="green">Green</text><text x="344" y="133" class="gray">Gray</text></svg>
|
After Width: | Height: | Size: 987 B |
87
_examples/custom_stylesheets/main.go
Normal file
87
_examples/custom_stylesheets/main.go
Normal file
|
@ -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(
|
||||||
|
`<?xml version="1.0" standalone="no"?>`+
|
||||||
|
`<?xml-stylesheet href="/main.css" type="text/css"?>`+
|
||||||
|
`<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">`))
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
|
@ -28,6 +28,25 @@ func SVG(width, height int) (Renderer, error) {
|
||||||
}, nil
|
}, 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.
|
// vectorRenderer renders chart commands to a bitmap.
|
||||||
type vectorRenderer struct {
|
type vectorRenderer struct {
|
||||||
dpi float64
|
dpi float64
|
||||||
|
@ -222,12 +241,23 @@ type canvas struct {
|
||||||
textTheta *float64
|
textTheta *float64
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
|
css string
|
||||||
|
nonce string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *canvas) Start(width, height int) {
|
func (c *canvas) Start(width, height int) {
|
||||||
c.width = width
|
c.width = width
|
||||||
c.height = height
|
c.height = height
|
||||||
c.w.Write([]byte(fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%d" height="%d">\n`, c.width, c.height)))
|
c.w.Write([]byte(fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%d" height="%d">\n`, c.width, c.height)))
|
||||||
|
if c.css != "" {
|
||||||
|
c.w.Write([]byte(`<style type="text/css"`))
|
||||||
|
if c.nonce != "" {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
|
||||||
|
c.w.Write([]byte(fmt.Sprintf(` nonce="%s"`, c.nonce)))
|
||||||
|
}
|
||||||
|
// To avoid compatibility issues between XML and CSS (f.e. with child selectors) we should encapsulate the CSS with CDATA.
|
||||||
|
c.w.Write([]byte(fmt.Sprintf(`><![CDATA[%s]]></style>`, c.css)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *canvas) Path(d string, style Style) {
|
func (c *canvas) Path(d string, style Style) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package chart
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -89,3 +90,30 @@ func TestCanvasClassSVG(t *testing.T) {
|
||||||
|
|
||||||
as.Equal("class=\"test-class\"", canvas.styleAsSVG(set))
|
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(`<style type="text/css"><![CDATA[%s]]></style>`, 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(`<style type="text/css" nonce="%s"><![CDATA[%s]]></style>`, canvas.nonce, canvas.css))
|
||||||
|
}
|
Loading…
Reference in a new issue