// Copyright ©2016 The Gonum Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package vgtex provides a vg.Canvas implementation for LaTeX, targeted at // the TikZ/PGF LaTeX package: https://sourceforge.net/projects/pgf // // vgtex generates PGF instructions that will be interpreted and rendered by LaTeX. // vgtex allows to put any valid LaTeX notation inside plot's strings. package vgtex // import "gonum.org/v1/plot/vg/vgtex" import ( "bufio" "bytes" "fmt" "image" "image/color" "image/png" "io" "math" "os" "strings" "time" "gonum.org/v1/plot/font" "gonum.org/v1/plot/vg" "gonum.org/v1/plot/vg/draw" ) const degPerRadian = 180 / math.Pi const ( defaultHeader = `%%%%%% generated by gonum/plot %%%%%% \documentclass{standalone} \usepackage{pgf} \begin{document} ` defaultFooter = "\\end{document}\n" ) func init() { draw.RegisterFormat("tex", func(w, h vg.Length) vg.CanvasWriterTo { return NewDocument(w, h) }) } // Canvas implements the vg.Canvas interface, translating drawing // primitives from gonum/plot to PGF. type Canvas struct { buf *bytes.Buffer w, h vg.Length stack []context // If document is true, Canvas.WriteTo will generate a standalone // .tex file that can be fed to, e.g., pdflatex. document bool id int64 // id is a unique identifier for this canvas } type context struct { color color.Color dashArray []vg.Length dashOffset vg.Length linew vg.Length } // New returns a new LaTeX canvas. func New(w, h vg.Length) *Canvas { return newCanvas(w, h, false) } // NewDocument returns a new LaTeX canvas that can be readily // compiled into a standalone document. func NewDocument(w, h vg.Length) *Canvas { return newCanvas(w, h, true) } func newCanvas(w, h vg.Length, document bool) *Canvas { c := &Canvas{ buf: new(bytes.Buffer), w: w, h: h, document: document, id: time.Now().UnixNano(), } if !document { c.wtex(`%%%% gonum/plot created for LaTeX/pgf`) c.wtex(`%%%% you need to add:`) c.wtex(`%%%% \usepackage{pgf}`) c.wtex(`%%%% to your LaTeX document`) } c.wtex("") c.wtex(`\begin{pgfpicture}`) c.stack = make([]context, 1) vg.Initialize(c) return c } func (c *Canvas) context() *context { return &c.stack[len(c.stack)-1] } // Size returns the width and height of the canvas. func (c *Canvas) Size() (w, h vg.Length) { return c.w, c.h } // SetLineWidth implements the vg.Canvas.SetLineWidth method. func (c *Canvas) SetLineWidth(w vg.Length) { c.context().linew = w } // SetLineDash implements the vg.Canvas.SetLineDash method. func (c *Canvas) SetLineDash(pattern []vg.Length, offset vg.Length) { c.context().dashArray = pattern c.context().dashOffset = offset } // SetColor implements the vg.Canvas.SetColor method. func (c *Canvas) SetColor(clr color.Color) { c.context().color = clr } // Rotate implements the vg.Canvas.Rotate method. func (c *Canvas) Rotate(rad float64) { c.wtex(`\pgftransformrotate{%g}`, rad*degPerRadian) } // Translate implements the vg.Canvas.Translate method. func (c *Canvas) Translate(pt vg.Point) { c.wtex(`\pgftransformshift{\pgfpoint{%gpt}{%gpt}}`, pt.X, pt.Y) } // Scale implements the vg.Canvas.Scale method. func (c *Canvas) Scale(x, y float64) { c.wtex(`\pgftransformxscale{%g}`, x) c.wtex(`\pgftransformyscale{%g}`, y) } // Push implements the vg.Canvas.Push method. func (c *Canvas) Push() { c.wtex(`\begin{pgfscope}`) c.stack = append(c.stack, *c.context()) } // Pop implements the vg.Canvas.Pop method. func (c *Canvas) Pop() { c.stack = c.stack[:len(c.stack)-1] c.wtex(`\end{pgfscope}`) c.wtex("") } // Stroke implements the vg.Canvas.Stroke method. func (c *Canvas) Stroke(p vg.Path) { if c.context().linew <= 0 { return } c.Push() c.wstyle() c.wpath(p) c.wtex(`\pgfusepath{stroke}`) c.Pop() } // Fill implements the vg.Canvas.Fill method. func (c *Canvas) Fill(p vg.Path) { c.Push() c.wstyle() c.wpath(p) c.wtex(`\pgfusepath{fill}`) c.Pop() } // FillString implements the vg.Canvas.FillString method. func (c *Canvas) FillString(f font.Face, pt vg.Point, text string) { c.Push() c.wcolor() pt.X += 0.5 * f.Width(text) c.wtex(`\pgftext[base,at={\pgfpoint{%gpt}{%gpt}}]{{\fontsize{%gpt}{%gpt}\selectfont %s}}`, pt.X, pt.Y, f.Font.Size, f.Font.Size, text) c.Pop() } // DrawImage implements the vg.Canvas.DrawImage method. // DrawImage will first save the image inside a PNG file and have the // generated LaTeX reference that file. // The file name will be "gonum-pgf-image--.png func (c *Canvas) DrawImage(rect vg.Rectangle, img image.Image) { fname := fmt.Sprintf("gonum-pgf-image-%v-%v.png", c.id, time.Now().UnixNano()) f, err := os.Create(fname) if err != nil { panic(err) } defer f.Close() err = png.Encode(f, img) if err != nil { panic(fmt.Errorf("vgtex: error encoding image to PNG: %v", err)) } var ( xmin = rect.Min.X ymin = rect.Min.Y width = rect.Size().X height = rect.Size().Y ) c.wtex(`\pgftext[base,left,at=\pgfpoint{%gpt}{%gpt}]{\pgfimage[height=%gpt,width=%gpt]{%s}}`, xmin, ymin, height, width, fname) } func (c *Canvas) indent(s string) string { return strings.Repeat(s, len(c.stack)) } func (c *Canvas) wtex(s string, args ...interface{}) { fmt.Fprintf(c.buf, c.indent(" ")+s+"\n", args...) } func (c *Canvas) wstyle() { c.wdash() c.wlineWidth() c.wcolor() } func (c *Canvas) wdash() { if len(c.context().dashArray) == 0 { c.wtex(`\pgfsetdash{}{0pt}`) return } str := `\pgfsetdash{` for _, d := range c.context().dashArray { str += fmt.Sprintf("{%gpt}", d) } str += fmt.Sprintf("}{%gpt}", c.context().dashOffset) c.wtex(str) } func (c *Canvas) wlineWidth() { c.wtex(`\pgfsetlinewidth{%gpt}`, c.context().linew) } func (c *Canvas) wcolor() { col := c.context().color if col == nil { col = color.Black } r, g, b, a := col.RGBA() // FIXME(sbinet) \color will last until the end of the current TeX group // use \pgfsetcolor and \pgfsetstrokecolor instead. // it needs a named color: define it on the fly (storing it at the beginning // of the document.) c.wtex( `\color[rgb]{%g,%g,%g}`, float64(r)/math.MaxUint16, float64(g)/math.MaxUint16, float64(b)/math.MaxUint16, ) opacity := float64(a) / math.MaxUint16 c.wtex(`\pgfsetstrokeopacity{%g}`, opacity) c.wtex(`\pgfsetfillopacity{%g}`, opacity) } func (c *Canvas) wpath(p vg.Path) { for _, comp := range p { switch comp.Type { case vg.MoveComp: c.wtex(`\pgfpathmoveto{\pgfpoint{%gpt}{%gpt}}`, comp.Pos.X, comp.Pos.Y) case vg.LineComp: c.wtex(`\pgflineto{\pgfpoint{%gpt}{%gpt}}`, comp.Pos.X, comp.Pos.Y) case vg.ArcComp: start := comp.Start * degPerRadian angle := comp.Angle * degPerRadian r := comp.Radius c.wtex(`\pgfpatharc{%g}{%g}{%gpt}`, start, angle, r) case vg.CurveComp: var a, b vg.Point switch len(comp.Control) { case 1: a = comp.Control[0] b = a case 2: a = comp.Control[0] b = comp.Control[1] default: panic("vgtex: invalid number of control points") } c.wtex(`\pgfcurveto{\pgfpoint{%gpt}{%gpt}}{\pgfpoint{%gpt}{%gpt}}{\pgfpoint{%gpt}{%gpt}}`, a.X, a.Y, b.X, b.Y, comp.Pos.X, comp.Pos.Y) case vg.CloseComp: c.wtex("%% path-close") default: panic(fmt.Errorf("vgtex: unknown path component type: %v", comp.Type)) } } } // WriteTo implements the io.WriterTo interface, writing a LaTeX/pgf plot. func (c *Canvas) WriteTo(w io.Writer) (int64, error) { var ( n int64 nn int err error ) b := bufio.NewWriter(w) if c.document { nn, err = b.Write([]byte(defaultHeader)) n += int64(nn) if err != nil { return n, err } } m, err := c.buf.WriteTo(b) n += m if err != nil { return n, err } nn, err = fmt.Fprintf(b, "\\end{pgfpicture}\n") n += int64(nn) if err != nil { return n, err } if c.document { nn, err = b.Write([]byte(defaultFooter)) n += int64(nn) if err != nil { return n, err } } return n, b.Flush() }