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"
|
||||
)
|
||||
|
||||
// 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(
|
||||
"<!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
|
||||
}
|
||||
|
||||
// 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(`<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) {
|
||||
|
|
|
@ -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(`<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