...

Source file src/gonum.org/v1/plot/vg/vgsvg/vgsvg.go

Documentation: gonum.org/v1/plot/vg/vgsvg

     1  // Copyright ©2015 The Gonum Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package vgsvg uses svgo (github.com/ajstarks/svgo)
     6  // as a backend for vg.
     7  //
     8  // By default, gonum/plot uses the Liberation fonts.
     9  // When embedding was not requested during plot creation, it may happen that
    10  // the generated SVG plot may not display well if the Liberation fonts are not
    11  // available to the program displaying the SVG plot.
    12  // See gonum.org/v1/plot/vg/vgsvg#Example_standardFonts for how to work around
    13  // this issue.
    14  //
    15  // Alternatively, users may want to install the Liberation fonts on their system:
    16  //   - https://en.wikipedia.org/wiki/Liberation_fonts
    17  package vgsvg // import "gonum.org/v1/plot/vg/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  // pr is the precision to use when outputting float64s.
    48  const pr = 5
    49  
    50  const (
    51  	// DefaultWidth and DefaultHeight are the default canvas
    52  	// dimensions.
    53  	DefaultWidth  = 4 * vg.Inch
    54  	DefaultHeight = 4 * vg.Inch
    55  )
    56  
    57  // Canvas implements the vg.Canvas interface, drawing to a SVG document.
    58  //
    59  // By default, fonts used by the canvas are not embedded in the produced
    60  // SVG document. This results in smaller but less portable SVG plots.
    61  // Users wanting completely portable SVG documents should create SVG canvases
    62  // with the EmbedFonts function.
    63  type Canvas struct {
    64  	svg  *svgo.SVG
    65  	w, h vg.Length
    66  
    67  	hdr   *bytes.Buffer // hdr is the SVG prelude, it may contain embedded fonts.
    68  	buf   *bytes.Buffer // buf is the SVG document.
    69  	stack []context
    70  
    71  	// Switch to embed fonts in SVG file.
    72  	// The default is to *not* embed fonts.
    73  	// Embedding fonts makes the SVG file larger but also more portable.
    74  	embed bool
    75  	fonts map[string]struct{} // set of already embedded fonts
    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  // UseWH specifies the width and height of the canvas.
    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  // EmbedFonts specifies whether fonts should be embedded inside
   100  // the SVG canvas.
   101  func EmbedFonts(v bool) option {
   102  	return func(c *Canvas) {
   103  		c.embed = v
   104  	}
   105  }
   106  
   107  // New returns a new image canvas.
   108  func New(w, h vg.Length) *Canvas {
   109  	return NewWith(UseWH(w, h))
   110  }
   111  
   112  // NewWith returns a new image canvas created according to the specified
   113  // options. The currently accepted options is UseWH. If size is not
   114  // specified, the default is used.
   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  	// This is like svg.Start, except it uses floats
   133  	// and specifies the units.
   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  	// Swap the origin to the bottom left.
   150  	// This must be matched with a </g> when saving,
   151  	// before the closing </svg>.
   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  // circle adds circle path data to the given writer.
   279  // Circles must be drawn using two arcs because
   280  // SVG disallows the start and end point of an arc
   281  // from being at the same location.
   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  // remainder returns the remainder of x/y.
   309  // We don't use math.Remainder because it
   310  // seems to return incorrect values due to how
   311  // IEEE defines the remainder operation…
   312  func remainder(x, y float64) float64 {
   313  	return (x/y - math.Trunc(x/y)) * y
   314  }
   315  
   316  // arc adds arc path data to the given writer.
   317  // Arc can only be used if the arc's angle is
   318  // less than a full circle, if it is greater then
   319  // circle should be used instead.
   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  // sweep returns the arc sweep flag value for
   331  // the given angle.
   332  func sweep(a float64) int {
   333  	if a < 0 {
   334  		return 0
   335  	}
   336  	return 1
   337  }
   338  
   339  // large returns the arc's large flag value for
   340  // the given angle.
   341  func large(a float64) int {
   342  	if math.Abs(a) >= math.Pi {
   343  		return 1
   344  	}
   345  	return 0
   346  }
   347  
   348  // FillString draws str at position pt using the specified font.
   349  // Text passed to FillString is escaped with html.EscapeString.
   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  // DrawImage implements the vg.Canvas.DrawImage method.
   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  		// invert y so image is not upside-down
   396  		`transform="scale(1, -1)"`,
   397  	)
   398  }
   399  
   400  // svgFontDescr returns a SVG compliant font name from the provided font face.
   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  	// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/font-family
   418  	var buf sfnt.Buffer
   419  	name, err := fnt.Face.Name(&buf, sfnt.NameIDFamily)
   420  	if err != nil {
   421  		// this should never happen unless the underlying sfnt.Font data
   422  		// is somehow corrupted.
   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  	// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/font-variant
   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  		// handle mismatch between the meaning of gonum/plot/font.Font#Variant
   442  		// and SVG's meaning for font-variant.
   443  		// For SVG, mono, ... serif is encoded in the font-family attribute
   444  		// whereas for gonum/plot it describes a variant among a collection of fonts.
   445  		//
   446  		// It shouldn't matter much if an invalid font-variant value is written
   447  		// out (browsers will just ignore it; Firefox 98 and Chromium 91 do so.)
   448  		return "normal"
   449  	case "":
   450  		return "none"
   451  	default:
   452  		return str
   453  	}
   454  }
   455  
   456  func svgStyleName(sty xfnt.Style) string {
   457  	// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/font-style
   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  	// see:
   472  	//  https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/font-weight
   473  	//  https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight
   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  // WriteTo writes the canvas to an io.Writer.
   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  	// Close the groups and svg in the output buffer
   557  	// so that the Canvas is not closed and can be
   558  	// used again if needed.
   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  // nEnds returns the number of group ends
   575  // needed before the SVG is saved.
   576  func (c *Canvas) nEnds() int {
   577  	n := 1 // close the transform that moves the origin
   578  	for _, ctx := range c.stack {
   579  		n += ctx.gEnds
   580  	}
   581  	return n
   582  }
   583  
   584  // style returns a style string composed of
   585  // all of the given elements.  If the elements
   586  // are all empty then the empty string is
   587  // returned.
   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  // elm returns a style element string with the
   606  // given key and value.  If the value matches
   607  // default then the empty string is returned.
   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  // dashArrayString returns a string representing the
   617  // dash array specification.
   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  // colorString returns the hexadecimal string representation of the color
   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  // opacityString returns the opacity value of the given color.
   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