...

Source file src/gonum.org/v1/plot/text/latex.go

Documentation: gonum.org/v1/plot/text

     1  // Copyright ©2020 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 text
     6  
     7  import (
     8  	"fmt"
     9  	"image/color"
    10  	"math"
    11  	"strings"
    12  
    13  	"github.com/go-latex/latex/drawtex"
    14  	"github.com/go-latex/latex/font/ttf"
    15  	"github.com/go-latex/latex/mtex"
    16  	"github.com/go-latex/latex/tex"
    17  	stdfnt "golang.org/x/image/font"
    18  
    19  	"gonum.org/v1/plot/font"
    20  	"gonum.org/v1/plot/vg"
    21  )
    22  
    23  // Latex parses, formats and renders LaTeX.
    24  type Latex struct {
    25  	// Fonts is the cache of font faces used by this text handler.
    26  	Fonts *font.Cache
    27  
    28  	// DPI is the dot-per-inch controlling the font resolution used by LaTeX.
    29  	// If zero, the resolution defaults to 72.
    30  	DPI float64
    31  }
    32  
    33  var _ Handler = (*Latex)(nil)
    34  
    35  // Cache returns the cache of fonts used by the text handler.
    36  func (hdlr Latex) Cache() *font.Cache {
    37  	return hdlr.Fonts
    38  }
    39  
    40  // Extents returns the Extents of a font.
    41  func (hdlr Latex) Extents(fnt font.Font) font.Extents {
    42  	face := hdlr.Fonts.Lookup(fnt, fnt.Size)
    43  	return face.Extents()
    44  }
    45  
    46  // Lines splits a given block of text into separate lines.
    47  func (hdlr Latex) Lines(txt string) []string {
    48  	txt = strings.TrimRight(txt, "\n")
    49  	return strings.Split(txt, "\n")
    50  }
    51  
    52  // Box returns the bounding box of the given non-multiline text where:
    53  //   - width is the horizontal space from the origin.
    54  //   - height is the vertical space above the baseline.
    55  //   - depth is the vertical space below the baseline, a positive number.
    56  func (hdlr Latex) Box(txt string, fnt font.Font) (width, height, depth vg.Length) {
    57  	cnv := drawtex.New()
    58  	face := hdlr.Fonts.Lookup(fnt, fnt.Size)
    59  	fnts := hdlr.fontsFor(fnt)
    60  	box, err := mtex.Parse(txt, face.Font.Size.Points(), latexDPI, ttf.NewFrom(cnv, fnts))
    61  	if err != nil {
    62  		panic(fmt.Errorf("could not parse math expression: %w", err))
    63  	}
    64  
    65  	var sh tex.Ship
    66  	sh.Call(0, 0, box.(tex.Tree))
    67  
    68  	width = vg.Length(box.Width())
    69  	height = vg.Length(box.Height())
    70  	depth = vg.Length(box.Depth())
    71  
    72  	// Add a bit of space, with a linegap as mtex.Box is returning
    73  	// a very tight bounding box.
    74  	// See gonum/plot#661.
    75  	if depth != 0 {
    76  		var (
    77  			e       = face.Extents()
    78  			linegap = e.Height - (e.Ascent + e.Descent)
    79  		)
    80  		depth += linegap
    81  	}
    82  
    83  	dpi := vg.Length(hdlr.dpi() / latexDPI)
    84  	return width * dpi, height * dpi, depth * dpi
    85  }
    86  
    87  // Draw renders the given text with the provided style and position
    88  // on the canvas.
    89  func (hdlr Latex) Draw(c vg.Canvas, txt string, sty Style, pt vg.Point) {
    90  	cnv := drawtex.New()
    91  	face := hdlr.Fonts.Lookup(sty.Font, sty.Font.Size)
    92  	fnts := hdlr.fontsFor(sty.Font)
    93  	box, err := mtex.Parse(txt, face.Font.Size.Points(), latexDPI, ttf.NewFrom(cnv, fnts))
    94  	if err != nil {
    95  		panic(fmt.Errorf("could not parse math expression: %w", err))
    96  	}
    97  
    98  	var sh tex.Ship
    99  	sh.Call(0, 0, box.(tex.Tree))
   100  
   101  	w := box.Width()
   102  	h := box.Height()
   103  	d := box.Depth()
   104  
   105  	dpi := hdlr.dpi() / latexDPI
   106  	o := latex{
   107  		cnv:   c,
   108  		fonts: hdlr.Fonts,
   109  		sty:   sty,
   110  		pt:    pt,
   111  		w:     vg.Length(w * dpi),
   112  		h:     vg.Length((h + d) * dpi),
   113  		cos:   1,
   114  		sin:   0,
   115  	}
   116  	e := face.Extents()
   117  	o.xoff = vg.Length(sty.XAlign) * o.w
   118  	o.yoff = o.h + o.h*vg.Length(sty.YAlign) - (e.Height - e.Ascent)
   119  
   120  	if sty.Rotation != 0 {
   121  		sin64, cos64 := math.Sincos(sty.Rotation)
   122  		o.cos = vg.Length(cos64)
   123  		o.sin = vg.Length(sin64)
   124  
   125  		o.cnv.Push()
   126  		defer o.cnv.Pop()
   127  		o.cnv.Rotate(sty.Rotation)
   128  	}
   129  
   130  	err = o.Render(w/latexDPI, (h+d)/latexDPI, dpi, cnv)
   131  	if err != nil {
   132  		panic(fmt.Errorf("could not render math expression: %w", err))
   133  	}
   134  }
   135  
   136  func (hdlr *Latex) fontsFor(fnt font.Font) *ttf.Fonts {
   137  	rm := fnt
   138  	rm.Variant = "Serif"
   139  	rm.Weight = stdfnt.WeightNormal
   140  	rm.Style = stdfnt.StyleNormal
   141  
   142  	it := rm
   143  	it.Style = stdfnt.StyleItalic
   144  
   145  	bf := rm
   146  	bf.Style = stdfnt.StyleNormal
   147  	bf.Weight = stdfnt.WeightBold
   148  
   149  	bfit := bf
   150  	bfit.Style = stdfnt.StyleItalic
   151  
   152  	return &ttf.Fonts{
   153  		Rm:      hdlr.Fonts.Lookup(rm, fnt.Size).Face,
   154  		Default: hdlr.Fonts.Lookup(rm, fnt.Size).Face,
   155  		It:      hdlr.Fonts.Lookup(it, fnt.Size).Face,
   156  		Bf:      hdlr.Fonts.Lookup(bf, fnt.Size).Face,
   157  		BfIt:    hdlr.Fonts.Lookup(bfit, fnt.Size).Face,
   158  	}
   159  }
   160  
   161  // latexDPI is the default LaTeX resolution used for computing the LaTeX
   162  // layout of equations and regular text.
   163  // Dimensions are then rescaled to the desired resolution.
   164  const latexDPI = 72.0
   165  
   166  func (hdlr Latex) dpi() float64 {
   167  	if hdlr.DPI == 0 {
   168  		return latexDPI
   169  	}
   170  	return hdlr.DPI
   171  }
   172  
   173  type latex struct {
   174  	cnv   vg.Canvas
   175  	fonts *font.Cache
   176  	sty   Style
   177  	pt    vg.Point
   178  
   179  	w vg.Length
   180  	h vg.Length
   181  
   182  	cos vg.Length
   183  	sin vg.Length
   184  
   185  	xoff vg.Length
   186  	yoff vg.Length
   187  }
   188  
   189  var _ mtex.Renderer = (*latex)(nil)
   190  
   191  func (r *latex) Render(width, height, dpi float64, c *drawtex.Canvas) error {
   192  	r.cnv.SetColor(r.sty.Color)
   193  
   194  	for _, op := range c.Ops() {
   195  		switch op := op.(type) {
   196  		case drawtex.GlyphOp:
   197  			r.drawGlyph(dpi, op)
   198  		case drawtex.RectOp:
   199  			r.drawRect(dpi, op)
   200  		default:
   201  			panic(fmt.Errorf("unknown drawtex op %T", op))
   202  		}
   203  	}
   204  
   205  	return nil
   206  }
   207  
   208  func (r *latex) drawGlyph(dpi float64, op drawtex.GlyphOp) {
   209  	pt := r.pt
   210  	if r.sty.Rotation != 0 {
   211  		pt.X, pt.Y = r.rotate(pt.X, pt.Y)
   212  	}
   213  
   214  	pt = pt.Add(vg.Point{
   215  		X: r.xoff + vg.Length(op.X*dpi),
   216  		Y: r.yoff - vg.Length(op.Y*dpi),
   217  	})
   218  
   219  	fnt := font.Face{
   220  		Font: font.From(r.sty.Font, vg.Length(op.Glyph.Size)),
   221  		Face: op.Glyph.Font,
   222  	}
   223  	r.cnv.FillString(fnt, pt, op.Glyph.Symbol)
   224  }
   225  
   226  func (r *latex) drawRect(dpi float64, op drawtex.RectOp) {
   227  	x1 := r.xoff + vg.Length(op.X1*dpi)
   228  	x2 := r.xoff + vg.Length(op.X2*dpi)
   229  	y1 := r.yoff - vg.Length(op.Y1*dpi)
   230  	y2 := r.yoff - vg.Length(op.Y2*dpi)
   231  
   232  	pt := r.pt
   233  	if r.sty.Rotation != 0 {
   234  		pt.X, pt.Y = r.rotate(pt.X, pt.Y)
   235  	}
   236  
   237  	pts := []vg.Point{
   238  		vg.Point{X: x1, Y: y1}.Add(pt),
   239  		vg.Point{X: x2, Y: y1}.Add(pt),
   240  		vg.Point{X: x2, Y: y2}.Add(pt),
   241  		vg.Point{X: x1, Y: y2}.Add(pt),
   242  		vg.Point{X: x1, Y: y1}.Add(pt),
   243  	}
   244  
   245  	fillPolygon(r.cnv, r.sty.Color, pts)
   246  }
   247  
   248  func (r *latex) rotate(x, y vg.Length) (vg.Length, vg.Length) {
   249  	u := x*r.cos + y*r.sin
   250  	v := y*r.cos - x*r.sin
   251  	return u, v
   252  }
   253  
   254  // FillPolygon fills a polygon with the given color.
   255  func fillPolygon(c vg.Canvas, clr color.Color, pts []vg.Point) {
   256  	if len(pts) == 0 {
   257  		return
   258  	}
   259  
   260  	c.SetColor(clr)
   261  	p := make(vg.Path, 0, len(pts)+1)
   262  	p.Move(pts[0])
   263  	for _, pt := range pts[1:] {
   264  		p.Line(pt)
   265  	}
   266  	p.Close()
   267  	c.Fill(p)
   268  }
   269  

View as plain text