1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package vgsvg
18
19 import (
20 "bufio"
21 "bytes"
22 "encoding/base64"
23 "fmt"
24 "html"
25 "image"
26 "image/color"
27 "image/png"
28 "io"
29 "math"
30 "strings"
31
32 svgo "github.com/ajstarks/svgo"
33 xfnt "golang.org/x/image/font"
34 "golang.org/x/image/font/sfnt"
35
36 "gonum.org/v1/plot/font"
37 "gonum.org/v1/plot/vg"
38 "gonum.org/v1/plot/vg/draw"
39 )
40
41 func init() {
42 draw.RegisterFormat("svg", func(w, h vg.Length) vg.CanvasWriterTo {
43 return New(w, h)
44 })
45 }
46
47
48 const pr = 5
49
50 const (
51
52
53 DefaultWidth = 4 * vg.Inch
54 DefaultHeight = 4 * vg.Inch
55 )
56
57
58
59
60
61
62
63 type Canvas struct {
64 svg *svgo.SVG
65 w, h vg.Length
66
67 hdr *bytes.Buffer
68 buf *bytes.Buffer
69 stack []context
70
71
72
73
74 embed bool
75 fonts map[string]struct{}
76 }
77
78 type context struct {
79 color color.Color
80 dashArray []vg.Length
81 dashOffset vg.Length
82 lineWidth vg.Length
83 gEnds int
84 }
85
86 type option func(*Canvas)
87
88
89 func UseWH(w, h vg.Length) option {
90 return func(c *Canvas) {
91 if w <= 0 || h <= 0 {
92 panic("vgsvg: w and h must both be > 0")
93 }
94 c.w = w
95 c.h = h
96 }
97 }
98
99
100
101 func EmbedFonts(v bool) option {
102 return func(c *Canvas) {
103 c.embed = v
104 }
105 }
106
107
108 func New(w, h vg.Length) *Canvas {
109 return NewWith(UseWH(w, h))
110 }
111
112
113
114
115 func NewWith(opts ...option) *Canvas {
116 buf := new(bytes.Buffer)
117 c := &Canvas{
118 svg: svgo.New(buf),
119 w: DefaultWidth,
120 h: DefaultHeight,
121 hdr: new(bytes.Buffer),
122 buf: buf,
123 stack: []context{{}},
124 embed: false,
125 fonts: make(map[string]struct{}),
126 }
127
128 for _, opt := range opts {
129 opt(c)
130 }
131
132
133
134 fmt.Fprintf(c.hdr, `<?xml version="1.0"?>
135 <!-- Generated by SVGo and Plotinum VG -->
136 <svg width="%.*gpt" height="%.*gpt" viewBox="0 0 %.*g %.*g"
137 xmlns="http://www.w3.org/2000/svg"
138 xmlns:xlink="http://www.w3.org/1999/xlink">`+"\n",
139 pr, c.w,
140 pr, c.h,
141 pr, c.w,
142 pr, c.h,
143 )
144
145 if c.embed {
146 fmt.Fprintf(c.hdr, "<defs>\n\t<style>\n")
147 }
148
149
150
151
152 c.svg.Gtransform(fmt.Sprintf("scale(1, -1) translate(0, -%.*g)", pr, c.h.Points()))
153
154 vg.Initialize(c)
155 return c
156 }
157
158 func (c *Canvas) Size() (w, h vg.Length) {
159 return c.w, c.h
160 }
161
162 func (c *Canvas) context() *context {
163 return &c.stack[len(c.stack)-1]
164 }
165
166 func (c *Canvas) SetLineWidth(w vg.Length) {
167 c.context().lineWidth = w
168 }
169
170 func (c *Canvas) SetLineDash(dashes []vg.Length, offs vg.Length) {
171 c.context().dashArray = dashes
172 c.context().dashOffset = offs
173 }
174
175 func (c *Canvas) SetColor(clr color.Color) {
176 c.context().color = clr
177 }
178
179 func (c *Canvas) Rotate(rot float64) {
180 rot = rot * 180 / math.Pi
181 c.svg.Rotate(rot)
182 c.context().gEnds++
183 }
184
185 func (c *Canvas) Translate(pt vg.Point) {
186 c.svg.Gtransform(fmt.Sprintf("translate(%.*g, %.*g)", pr, pt.X.Points(), pr, pt.Y.Points()))
187 c.context().gEnds++
188 }
189
190 func (c *Canvas) Scale(x, y float64) {
191 c.svg.ScaleXY(x, y)
192 c.context().gEnds++
193 }
194
195 func (c *Canvas) Push() {
196 top := *c.context()
197 top.gEnds = 0
198 c.stack = append(c.stack, top)
199 }
200
201 func (c *Canvas) Pop() {
202 for i := 0; i < c.context().gEnds; i++ {
203 c.svg.Gend()
204 }
205 c.stack = c.stack[:len(c.stack)-1]
206 }
207
208 func (c *Canvas) Stroke(path vg.Path) {
209 if c.context().lineWidth.Points() <= 0 {
210 return
211 }
212 c.svg.Path(c.pathData(path),
213 style(elm("fill", "#000000", "none"),
214 elm("stroke", "none", colorString(c.context().color)),
215 elm("stroke-opacity", "1", opacityString(c.context().color)),
216 elm("stroke-width", "1", "%.*g", pr, c.context().lineWidth.Points()),
217 elm("stroke-dasharray", "none", dashArrayString(c)),
218 elm("stroke-dashoffset", "0", "%.*g", pr, c.context().dashOffset.Points())))
219 }
220
221 func (c *Canvas) Fill(path vg.Path) {
222 c.svg.Path(c.pathData(path),
223 style(elm("fill", "#000000", colorString(c.context().color)),
224 elm("fill-opacity", "1", opacityString(c.context().color))))
225 }
226
227 func (c *Canvas) pathData(path vg.Path) string {
228 buf := new(bytes.Buffer)
229 var x, y float64
230 for _, comp := range path {
231 switch comp.Type {
232 case vg.MoveComp:
233 fmt.Fprintf(buf, "M%.*g,%.*g", pr, comp.Pos.X.Points(), pr, comp.Pos.Y.Points())
234 x = comp.Pos.X.Points()
235 y = comp.Pos.Y.Points()
236 case vg.LineComp:
237 fmt.Fprintf(buf, "L%.*g,%.*g", pr, comp.Pos.X.Points(), pr, comp.Pos.Y.Points())
238 x = comp.Pos.X.Points()
239 y = comp.Pos.Y.Points()
240 case vg.ArcComp:
241 r := comp.Radius.Points()
242 sin, cos := math.Sincos(comp.Start)
243 x0 := comp.Pos.X.Points() + r*cos
244 y0 := comp.Pos.Y.Points() + r*sin
245 if x0 != x || y0 != y {
246 fmt.Fprintf(buf, "L%.*g,%.*g", pr, x0, pr, y0)
247 }
248 if math.Abs(comp.Angle) >= 2*math.Pi {
249 x, y = circle(buf, c, &comp)
250 } else {
251 x, y = arc(buf, c, &comp)
252 }
253 case vg.CurveComp:
254 switch len(comp.Control) {
255 case 1:
256 fmt.Fprintf(buf, "Q%.*g,%.*g,%.*g,%.*g",
257 pr, comp.Control[0].X.Points(), pr, comp.Control[0].Y.Points(),
258 pr, comp.Pos.X.Points(), pr, comp.Pos.Y.Points())
259 case 2:
260 fmt.Fprintf(buf, "C%.*g,%.*g,%.*g,%.*g,%.*g,%.*g",
261 pr, comp.Control[0].X.Points(), pr, comp.Control[0].Y.Points(),
262 pr, comp.Control[1].X.Points(), pr, comp.Control[1].Y.Points(),
263 pr, comp.Pos.X.Points(), pr, comp.Pos.Y.Points())
264 default:
265 panic("vgsvg: invalid number of control points")
266 }
267 x = comp.Pos.X.Points()
268 y = comp.Pos.Y.Points()
269 case vg.CloseComp:
270 buf.WriteString("Z")
271 default:
272 panic(fmt.Sprintf("vgsvg: unknown path component type: %d", comp.Type))
273 }
274 }
275 return buf.String()
276 }
277
278
279
280
281
282 func circle(w io.Writer, c *Canvas, comp *vg.PathComp) (x, y float64) {
283 angle := 2 * math.Pi
284 if comp.Angle < 0 {
285 angle = -2 * math.Pi
286 }
287 angle += remainder(comp.Angle, 2*math.Pi)
288 if angle >= 4*math.Pi {
289 panic("Impossible angle")
290 }
291
292 s0, c0 := math.Sincos(comp.Start + 0.5*angle)
293 s1, c1 := math.Sincos(comp.Start + angle)
294
295 r := comp.Radius.Points()
296 x0 := comp.Pos.X.Points() + r*c0
297 y0 := comp.Pos.Y.Points() + r*s0
298 x = comp.Pos.X.Points() + r*c1
299 y = comp.Pos.Y.Points() + r*s1
300
301 fmt.Fprintf(w, "A%.*g,%.*g 0 %d %d %.*g,%.*g", pr, r, pr, r,
302 large(angle/2), sweep(angle/2), pr, x0, pr, y0)
303 fmt.Fprintf(w, "A%.*g,%.*g 0 %d %d %.*g,%.*g", pr, r, pr, r,
304 large(angle/2), sweep(angle/2), pr, x, pr, y)
305 return
306 }
307
308
309
310
311
312 func remainder(x, y float64) float64 {
313 return (x/y - math.Trunc(x/y)) * y
314 }
315
316
317
318
319
320 func arc(w io.Writer, c *Canvas, comp *vg.PathComp) (x, y float64) {
321 r := comp.Radius.Points()
322 sin, cos := math.Sincos(comp.Start + comp.Angle)
323 x = comp.Pos.X.Points() + r*cos
324 y = comp.Pos.Y.Points() + r*sin
325 fmt.Fprintf(w, "A%.*g,%.*g 0 %d %d %.*g,%.*g", pr, r, pr, r,
326 large(comp.Angle), sweep(comp.Angle), pr, x, pr, y)
327 return
328 }
329
330
331
332 func sweep(a float64) int {
333 if a < 0 {
334 return 0
335 }
336 return 1
337 }
338
339
340
341 func large(a float64) int {
342 if math.Abs(a) >= math.Pi {
343 return 1
344 }
345 return 0
346 }
347
348
349
350 func (c *Canvas) FillString(font font.Face, pt vg.Point, str string) {
351 name := svgFontDescr(font)
352 sty := style(
353 name,
354 elm("font-size", "medium", "%.*gpx", pr, font.Font.Size.Points()),
355 elm("fill", "#000000", colorString(c.context().color)),
356 )
357 if sty != "" {
358 sty = "\n\t" + sty
359 }
360 fmt.Fprintf(
361 c.buf,
362 `<text x="%.*g" y="%.*g" transform="scale(1, -1)"%s>%s</text>`+"\n",
363 pr, pt.X.Points(), pr, -pt.Y.Points(), sty, html.EscapeString(str),
364 )
365
366 if c.embed {
367 c.embedFont(name, font)
368 }
369 }
370
371
372 func (c *Canvas) DrawImage(rect vg.Rectangle, img image.Image) {
373 buf := new(bytes.Buffer)
374 err := png.Encode(buf, img)
375 if err != nil {
376 panic(fmt.Errorf("vgsvg: error encoding image to PNG: %+v", err))
377 }
378 str := "data:image/jpg;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())
379 rsz := rect.Size()
380 min := rect.Min
381 var (
382 width = rsz.X.Points()
383 height = rsz.Y.Points()
384 xmin = min.X.Points()
385 ymin = min.Y.Points()
386 )
387 fmt.Fprintf(
388 c.buf,
389 `<image x="%v" y="%v" width="%v" height="%v" xlink:href="%s" %s />`+"\n",
390 xmin,
391 -ymin-height,
392 width,
393 height,
394 str,
395
396 `transform="scale(1, -1)"`,
397 )
398 }
399
400
401 func svgFontDescr(fnt font.Face) string {
402 var (
403 family = svgFamilyName(fnt)
404 variant = svgVariantName(fnt.Font.Variant)
405 style = svgStyleName(fnt.Font.Style)
406 weight = svgWeightName(fnt.Font.Weight)
407 )
408
409 o := "font-family:" + family + ";" +
410 "font-variant:" + variant + ";" +
411 "font-weight:" + weight + ";" +
412 "font-style:" + style
413 return o
414 }
415
416 func svgFamilyName(fnt font.Face) string {
417
418 var buf sfnt.Buffer
419 name, err := fnt.Face.Name(&buf, sfnt.NameIDFamily)
420 if err != nil {
421
422
423 panic(fmt.Errorf(
424 "vgsvg: could not extract family name from font %q: %+v",
425 fnt.Font.Typeface,
426 err,
427 ))
428 }
429 return name
430 }
431
432 func svgVariantName(v font.Variant) string {
433
434 str := strings.ToLower(string(v))
435 switch str {
436 case "smallcaps":
437 return "small-caps"
438 case "mono", "monospace",
439 "sans", "sansserif", "sans-serif",
440 "serif":
441
442
443
444
445
446
447
448 return "normal"
449 case "":
450 return "none"
451 default:
452 return str
453 }
454 }
455
456 func svgStyleName(sty xfnt.Style) string {
457
458 switch sty {
459 case xfnt.StyleNormal:
460 return "normal"
461 case xfnt.StyleItalic:
462 return "italic"
463 case xfnt.StyleOblique:
464 return "oblique"
465 default:
466 panic(fmt.Errorf("vgsvg: invalid font style %+v (v=%d)", sty, int(sty)))
467 }
468 }
469
470 func svgWeightName(w xfnt.Weight) string {
471
472
473
474 switch w {
475 case xfnt.WeightThin:
476 return "100"
477 case xfnt.WeightExtraLight:
478 return "200"
479 case xfnt.WeightLight:
480 return "300"
481 case xfnt.WeightNormal:
482 return "normal"
483 case xfnt.WeightMedium:
484 return "500"
485 case xfnt.WeightSemiBold:
486 return "600"
487 case xfnt.WeightBold:
488 return "bold"
489 case xfnt.WeightExtraBold:
490 return "800"
491 case xfnt.WeightBlack:
492 return "900"
493 default:
494 panic(fmt.Errorf("vgsvg: invalid font weight %+v (v=%d)", w, int(w)))
495 }
496 }
497
498 func (c *Canvas) embedFont(name string, f font.Face) {
499 if _, dup := c.fonts[name]; dup {
500 return
501 }
502 c.fonts[name] = struct{}{}
503
504 raw := new(bytes.Buffer)
505 _, err := f.Face.WriteSourceTo(nil, raw)
506 if err != nil {
507 panic(fmt.Errorf("vg/vgsvg: could not read font raw data: %+v", err))
508 }
509
510 fmt.Fprintf(c.hdr, "\t\t@font-face{\n")
511 fmt.Fprintf(c.hdr, "\t\t\tfont-family:%q;\n", svgFamilyName(f))
512 fmt.Fprintf(c.hdr,
513 "\t\t\tfont-variant:%s;font-weight:%s;font-style:%s;\n",
514 svgVariantName(f.Font.Variant),
515 svgWeightName(f.Font.Weight),
516 svgStyleName(f.Font.Style),
517 )
518
519 fmt.Fprintf(
520 c.hdr,
521 "\t\t\tsrc: url(data:font/ttf;charset=utf-8;base64,%s) format(\"truetype\");\n",
522 base64.StdEncoding.EncodeToString(raw.Bytes()),
523 )
524 fmt.Fprintf(c.hdr, "\t\t}\n")
525 }
526
527 type cwriter struct {
528 w *bufio.Writer
529 n int64
530 }
531
532 func (c *cwriter) Write(p []byte) (int, error) {
533 n, err := c.w.Write(p)
534 c.n += int64(n)
535 return n, err
536 }
537
538
539 func (c *Canvas) WriteTo(w io.Writer) (int64, error) {
540 b := &cwriter{w: bufio.NewWriter(w)}
541
542 if c.embed {
543 fmt.Fprintf(c.hdr, "\t</style>\n</defs>\n")
544 }
545
546 _, err := c.hdr.WriteTo(b)
547 if err != nil {
548 return b.n, err
549 }
550
551 _, err = c.buf.WriteTo(b)
552 if err != nil {
553 return b.n, err
554 }
555
556
557
558
559 for i := 0; i < c.nEnds(); i++ {
560 _, err = fmt.Fprintln(b, "</g>")
561 if err != nil {
562 return b.n, err
563 }
564 }
565
566 _, err = fmt.Fprintln(b, "</svg>")
567 if err != nil {
568 return b.n, err
569 }
570
571 return b.n, b.w.Flush()
572 }
573
574
575
576 func (c *Canvas) nEnds() int {
577 n := 1
578 for _, ctx := range c.stack {
579 n += ctx.gEnds
580 }
581 return n
582 }
583
584
585
586
587
588 func style(elms ...string) string {
589 str := ""
590 for _, e := range elms {
591 if e == "" {
592 continue
593 }
594 if str != "" {
595 str += ";"
596 }
597 str += e
598 }
599 if str == "" {
600 return ""
601 }
602 return "style=\"" + str + "\""
603 }
604
605
606
607
608 func elm(key, def, f string, vls ...interface{}) string {
609 value := fmt.Sprintf(f, vls...)
610 if value == def {
611 return ""
612 }
613 return key + ":" + value
614 }
615
616
617
618 func dashArrayString(c *Canvas) string {
619 str := ""
620 for i, d := range c.context().dashArray {
621 str += fmt.Sprintf("%.*g", pr, d.Points())
622 if i < len(c.context().dashArray)-1 {
623 str += ","
624 }
625 }
626 if str == "" {
627 str = "none"
628 }
629 return str
630 }
631
632
633 func colorString(clr color.Color) string {
634 if clr == nil {
635 clr = color.Black
636 }
637 r, g, b, _a := clr.RGBA()
638 a := 255.0 / float64(_a)
639 return fmt.Sprintf("#%02X%02X%02X", int(float64(r)*a),
640 int(float64(g)*a), int(float64(b)*a))
641 }
642
643
644 func opacityString(clr color.Color) string {
645 if clr == nil {
646 clr = color.Black
647 }
648 _, _, _, a := clr.RGBA()
649 return fmt.Sprintf("%.*g", pr, float64(a)/math.MaxUint16)
650 }
651
View as plain text