// Copyright ©2020 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 text import ( "fmt" "image/color" "math" "strings" "github.com/go-latex/latex/drawtex" "github.com/go-latex/latex/font/ttf" "github.com/go-latex/latex/mtex" "github.com/go-latex/latex/tex" stdfnt "golang.org/x/image/font" "gonum.org/v1/plot/font" "gonum.org/v1/plot/vg" ) // Latex parses, formats and renders LaTeX. type Latex struct { // Fonts is the cache of font faces used by this text handler. Fonts *font.Cache // DPI is the dot-per-inch controlling the font resolution used by LaTeX. // If zero, the resolution defaults to 72. DPI float64 } var _ Handler = (*Latex)(nil) // Cache returns the cache of fonts used by the text handler. func (hdlr Latex) Cache() *font.Cache { return hdlr.Fonts } // Extents returns the Extents of a font. func (hdlr Latex) Extents(fnt font.Font) font.Extents { face := hdlr.Fonts.Lookup(fnt, fnt.Size) return face.Extents() } // Lines splits a given block of text into separate lines. func (hdlr Latex) Lines(txt string) []string { txt = strings.TrimRight(txt, "\n") return strings.Split(txt, "\n") } // Box returns the bounding box of the given non-multiline text where: // - width is the horizontal space from the origin. // - height is the vertical space above the baseline. // - depth is the vertical space below the baseline, a positive number. func (hdlr Latex) Box(txt string, fnt font.Font) (width, height, depth vg.Length) { cnv := drawtex.New() face := hdlr.Fonts.Lookup(fnt, fnt.Size) fnts := hdlr.fontsFor(fnt) box, err := mtex.Parse(txt, face.Font.Size.Points(), latexDPI, ttf.NewFrom(cnv, fnts)) if err != nil { panic(fmt.Errorf("could not parse math expression: %w", err)) } var sh tex.Ship sh.Call(0, 0, box.(tex.Tree)) width = vg.Length(box.Width()) height = vg.Length(box.Height()) depth = vg.Length(box.Depth()) // Add a bit of space, with a linegap as mtex.Box is returning // a very tight bounding box. // See gonum/plot#661. if depth != 0 { var ( e = face.Extents() linegap = e.Height - (e.Ascent + e.Descent) ) depth += linegap } dpi := vg.Length(hdlr.dpi() / latexDPI) return width * dpi, height * dpi, depth * dpi } // Draw renders the given text with the provided style and position // on the canvas. func (hdlr Latex) Draw(c vg.Canvas, txt string, sty Style, pt vg.Point) { cnv := drawtex.New() face := hdlr.Fonts.Lookup(sty.Font, sty.Font.Size) fnts := hdlr.fontsFor(sty.Font) box, err := mtex.Parse(txt, face.Font.Size.Points(), latexDPI, ttf.NewFrom(cnv, fnts)) if err != nil { panic(fmt.Errorf("could not parse math expression: %w", err)) } var sh tex.Ship sh.Call(0, 0, box.(tex.Tree)) w := box.Width() h := box.Height() d := box.Depth() dpi := hdlr.dpi() / latexDPI o := latex{ cnv: c, fonts: hdlr.Fonts, sty: sty, pt: pt, w: vg.Length(w * dpi), h: vg.Length((h + d) * dpi), cos: 1, sin: 0, } e := face.Extents() o.xoff = vg.Length(sty.XAlign) * o.w o.yoff = o.h + o.h*vg.Length(sty.YAlign) - (e.Height - e.Ascent) if sty.Rotation != 0 { sin64, cos64 := math.Sincos(sty.Rotation) o.cos = vg.Length(cos64) o.sin = vg.Length(sin64) o.cnv.Push() defer o.cnv.Pop() o.cnv.Rotate(sty.Rotation) } err = o.Render(w/latexDPI, (h+d)/latexDPI, dpi, cnv) if err != nil { panic(fmt.Errorf("could not render math expression: %w", err)) } } func (hdlr *Latex) fontsFor(fnt font.Font) *ttf.Fonts { rm := fnt rm.Variant = "Serif" rm.Weight = stdfnt.WeightNormal rm.Style = stdfnt.StyleNormal it := rm it.Style = stdfnt.StyleItalic bf := rm bf.Style = stdfnt.StyleNormal bf.Weight = stdfnt.WeightBold bfit := bf bfit.Style = stdfnt.StyleItalic return &ttf.Fonts{ Rm: hdlr.Fonts.Lookup(rm, fnt.Size).Face, Default: hdlr.Fonts.Lookup(rm, fnt.Size).Face, It: hdlr.Fonts.Lookup(it, fnt.Size).Face, Bf: hdlr.Fonts.Lookup(bf, fnt.Size).Face, BfIt: hdlr.Fonts.Lookup(bfit, fnt.Size).Face, } } // latexDPI is the default LaTeX resolution used for computing the LaTeX // layout of equations and regular text. // Dimensions are then rescaled to the desired resolution. const latexDPI = 72.0 func (hdlr Latex) dpi() float64 { if hdlr.DPI == 0 { return latexDPI } return hdlr.DPI } type latex struct { cnv vg.Canvas fonts *font.Cache sty Style pt vg.Point w vg.Length h vg.Length cos vg.Length sin vg.Length xoff vg.Length yoff vg.Length } var _ mtex.Renderer = (*latex)(nil) func (r *latex) Render(width, height, dpi float64, c *drawtex.Canvas) error { r.cnv.SetColor(r.sty.Color) for _, op := range c.Ops() { switch op := op.(type) { case drawtex.GlyphOp: r.drawGlyph(dpi, op) case drawtex.RectOp: r.drawRect(dpi, op) default: panic(fmt.Errorf("unknown drawtex op %T", op)) } } return nil } func (r *latex) drawGlyph(dpi float64, op drawtex.GlyphOp) { pt := r.pt if r.sty.Rotation != 0 { pt.X, pt.Y = r.rotate(pt.X, pt.Y) } pt = pt.Add(vg.Point{ X: r.xoff + vg.Length(op.X*dpi), Y: r.yoff - vg.Length(op.Y*dpi), }) fnt := font.Face{ Font: font.From(r.sty.Font, vg.Length(op.Glyph.Size)), Face: op.Glyph.Font, } r.cnv.FillString(fnt, pt, op.Glyph.Symbol) } func (r *latex) drawRect(dpi float64, op drawtex.RectOp) { x1 := r.xoff + vg.Length(op.X1*dpi) x2 := r.xoff + vg.Length(op.X2*dpi) y1 := r.yoff - vg.Length(op.Y1*dpi) y2 := r.yoff - vg.Length(op.Y2*dpi) pt := r.pt if r.sty.Rotation != 0 { pt.X, pt.Y = r.rotate(pt.X, pt.Y) } pts := []vg.Point{ vg.Point{X: x1, Y: y1}.Add(pt), vg.Point{X: x2, Y: y1}.Add(pt), vg.Point{X: x2, Y: y2}.Add(pt), vg.Point{X: x1, Y: y2}.Add(pt), vg.Point{X: x1, Y: y1}.Add(pt), } fillPolygon(r.cnv, r.sty.Color, pts) } func (r *latex) rotate(x, y vg.Length) (vg.Length, vg.Length) { u := x*r.cos + y*r.sin v := y*r.cos - x*r.sin return u, v } // FillPolygon fills a polygon with the given color. func fillPolygon(c vg.Canvas, clr color.Color, pts []vg.Point) { if len(pts) == 0 { return } c.SetColor(clr) p := make(vg.Path, 0, len(pts)+1) p.Move(pts[0]) for _, pt := range pts[1:] { p.Line(pt) } p.Close() c.Fill(p) }