changeset 29470:2ae4764180c6

Initial implementation of a LaTeX interpreter (bug #59546). * NEWS: Announce basic support for "latex" interpreter. * plot.txi: Restructure the "Use of the interpreter Property" section to include 3 subsections, one for each interpreter type. In "Printing and Saving Plots" describe the new behavior. * print.m: Document new behavior. * contributors.in: Add Andrej Lojdl, a former GSoC student that worked on this subject. Even though there is not much left of his original work, it served as a very useful starting point. * base-text-renderer.h, base-text-renderer.cc: New enum to store symbolic constants for rotation angles. (base_text_renderer::rotate_pixels): New utility method. Code extracted from ft_text_renderer::render. (base_text_renderer::rotation_to_mode): Moved from ft_text_renderer class. (base_text_renderer::fix_bbox_anchor): New utility method. Code extracted from ft_text_renderer::text_to_pixels. * ft_text_renderer.cc (ft_text_renderer::render, ft_text_renderer::text_to_pixels): Make use of base class utility functions. * gl2ps-print.cc: New counter variable m_svg_def_index for safe inclusion of defs coming from dvisvgm. (gl2ps_renderer::format_svg_element): New function to manipulate svg elements obtained from dvisvgm so as to position them properly on the figure and change their color. (gl2ps_renderer::strlist_to_svg): If the str_list object returned from text_to_strlist contains an svg element, use format_svg_element to handle position and color and then return it. Otherwise, use <g> and <text> elements rather than <text> and <tspan> elements which are not supported by Qt's svg renderer (svg-tiny implementation). (gl2ps_renderer::strlist_to_ps): If the str_list object returned from text_to_strlist contains an svg element, raise a warning about the necessity of using -svgconvert. * latex-text-renderer.h, latex-text-renderer.cc: New files to hold the latex_interpreter class. * libinterp/corefcn/module.mk: Add new files to the build system. * text-renderer.h, text-renderer.cc (text_renderer): New data member latex_rep to hold a pointer to an instance of a latex_renderer. (text_renderer::~text_renderer, get_extent, set_anti_aliasing, set_font, set_color, text_to_pixels, text_to_strlist): Duplicate action of the latex_rep. (text_renderer::latex_ok): New method to test the usability of the latex_renderer. (text_renderer::string::svg_element, text_renderer::string::set_svg_element, text_renderer::string::get_svg_element): New string data member to hold preformated svg element. Provide accessor methods. * acinclude.m4: Add QtSvg to the list of imported QT_MODULES. * octave-svgconvert.cc: Overhaul program to make use of Qt's QSvgRenderer when rendering to PDF.
author Pantxo Diribarne <pantxo.diribarne@gmail.com>
date Mon, 22 Mar 2021 21:32:54 +0100
parents 46def32e6806
children 6806f7aa30c1
files NEWS doc/interpreter/contributors.in doc/interpreter/plot.txi libinterp/corefcn/base-text-renderer.cc libinterp/corefcn/base-text-renderer.h libinterp/corefcn/ft-text-renderer.cc libinterp/corefcn/gl2ps-print.cc libinterp/corefcn/latex-text-renderer.cc libinterp/corefcn/latex-text-renderer.h libinterp/corefcn/module.mk libinterp/corefcn/text-engine.h libinterp/corefcn/text-renderer.cc libinterp/corefcn/text-renderer.h m4/acinclude.m4 scripts/plot/appearance/text.m scripts/plot/util/print.m src/octave-svgconvert.cc
diffstat 17 files changed, 1149 insertions(+), 543 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Mar 29 07:54:26 2021 +0200
+++ b/NEWS	Mon Mar 22 21:32:54 2021 +0100
@@ -98,6 +98,12 @@
 
 - Support for Qt4 for both graphics and the GUI has been removed.
 
+- If a working LaTeX tool chain is found on the path, including `latex`,
+`dvipng`, and `dvisvgm` binaries, then text strings can now be rendered properly
+when using the `"latex"` value for the text objects' `"interpreter"` property
+and axes objects' `"ticklabelinterpreter"`.  Type `doc "latex interpreter"`
+for further info.
+
 - The additional property `"contextmenu"` has been added to all graphics
 objects.  It is equivalent to the previously used `"uicontextmenu"`
 property which is hidden now.
--- a/doc/interpreter/contributors.in	Mon Mar 29 07:54:26 2021 +0200
+++ b/doc/interpreter/contributors.in	Mon Mar 22 21:32:54 2021 +0100
@@ -229,6 +229,7 @@
 David Livings
 Barbara Locsi
 Sebastien Loisel
+Andrej Lojdl
 Erik de Castro Lopo
 Massimo Lorenzin
 Emil Lucretiu
--- a/doc/interpreter/plot.txi	Mon Mar 29 07:54:26 2021 +0200
+++ b/doc/interpreter/plot.txi	Mon Mar 22 21:32:54 2021 +0100
@@ -749,20 +749,31 @@
 @subsection Use of the @code{interpreter} Property
 @anchor{XREFinterpreterusage}
 
-All text objects---such as titles, labels, legends, and text---include
-the property @qcode{"interpreter"} that determines the manner in
-which special control sequences in the text are rendered.
+@code{text} (such as titles, labels, legend item) and @code{axes} objects
+feature a @ref{XREFtextinterpreter,,interpreter} and
+@ref{XREFaxesticklabelinterpreter,,ticklabelinterpreter} property respectively.
+It determines the manner in which special control sequences in the text are
+rendered.
 
 The interpreter property can take three values: @qcode{"none"}, @qcode{"tex"},
-@qcode{"latex"}.  If the interpreter is set to @qcode{"none"} then no special
+@qcode{"latex"}.
+
+@menu
+* @code{none} interpreter::
+* @code{tex} interpreter::
+* @code{latex} interpreter::
+@end menu
+
+@node @code{none} interpreter
+@subsubsection @code{none} interpreter
+@anchor{XREFnoneinterpreter}
+If the interpreter is set to @qcode{"none"} then no special
 rendering occurs---the displayed text is a verbatim copy of the specified text.
-Currently, the @qcode{"latex"} interpreter is not implemented for on-screen
-display and is equivalent to @qcode{"none"}.  Note that Octave does not parse
-or validate the text strings when in @qcode{"latex"} mode---it is the
-responsibility of the programmer to generate valid strings which may include
-wrapping sections that should appear in Math mode with @qcode{'$'} characters.
-
-The @qcode{"tex"} option implements a subset of @TeX{} functionality when
+
+@node @code{tex} interpreter
+@subsubsection @code{tex} interpreter
+@anchor{XREFtexinterpreter}
+The @qcode{"tex"} interpreter implements a subset of @TeX{} functionality when
 rendering text.  This allows the insertion of special glyphs such as Greek
 characters or mathematical symbols.  Special characters are inserted by using
 a backslash (\) character followed by a code, as shown in @ref{tab:extended}.
@@ -1053,7 +1064,7 @@
 @end tex
 @end float
 
-@subsubsection Degree Symbol
+@strong{Caution: Degree Symbol}
 @cindex Degree Symbol
 
 Conformance to both @TeX{} and @sc{matlab} with respect to the @code{\circ}
@@ -1062,6 +1073,56 @@
 has chosen to follow the @TeX{} specification, but has added the additional
 symbol @code{\deg} which maps to the degree symbol (U+00B0).
 
+@node @code{latex} interpreter
+@subsubsection @code{latex} interpreter
+@anchor{XREFlatexinterpreter}
+The @qcode{"latex"} interpreter only works if an external LaTeX tool chain is
+present.  Three binaries are needed: @code{latex}, @code{dvipng}, and
+@code{dvisvgm}.  If those binaries are installed but not on the path, one can
+still provide their respective path using the following environment variables:
+OCTAVE_LATEX_BINARY, OCTAVE_DVIPNG_BINARY, and OCTAVE_DVISVG_BINARY.
+
+Note that Octave does not parse or validate the text strings when in
+@qcode{"latex"} mode---it is the responsibility of the programmer to generate
+valid strings which may include wrapping sections that should appear in Math
+mode with @qcode{'$'} characters.
+See e.g. @url{https://www.latex-project.org/help/documentation/} for
+documentation about LaTeX typesetting.
+
+For debugging purpose, a convenience environment variable,
+OCTAVE_LATEX_DEBUG_FLAG, can be set to trigger more verbose output when Octave
+fails to have a given text compiled by an external latex engine. E.g.
+@qcode{"x^2"} is not a valid LaTeX string and the following example should fail
+
+@example
+@group
+setenv ("OCTAVE_LATEX_DEBUG_FLAG", "1")
+x = 1:10;
+plot (x, x.^2)
+title ("x^2", "interpreter", "latex")
+@end group
+@end example
+
+Searching the terminal output you should find some helpful info about the origin
+of the failure:
+
+@example
+@group
+...
+No file default.aux.
+! Missing $ inserted.
+<inserted text>
+                $
+l.6 x^
+      2
+! Missing $ inserted.
+...
+@end group
+@end example
+
+If no usable latex tool chain is found at the first text rendering, using
+the @qcode{"latex"} interpreter is equivalent to @qcode{"none"}.
+
 @node Printing and Saving Plots
 @subsection Printing and Saving Plots
 @cindex plotting, saving and printing plots
@@ -1091,9 +1152,19 @@
 such text.  In general, the @qcode{"tex"} interpreter (default) is the best
 all-around performer for both on-screen display and printing.  However, for the
 reproduction of complicated text formulas the @qcode{"latex"} interpreter is
-preferred.  The @qcode{"latex"} interpreter will not display symbols on-screen,
-but the printed output will be correct.  When printing, use one of the
-@code{standalone} options which provide full access to @LaTeX{} commands.
+preferred.  When printing with the @code{-painters} renderer, the default for
+all vector formats, two options may be considered:
+@itemize @bullet
+@item
+Use the @option{-svgconvert} option to allow for rendering LaTeX formulas. Note
+that the glyph are rendered as path and the original textual info are lost.
+@item
+Use one of the @option{-d*latex*} devices to produce a .tex file (plus support
+.eps or .pdf files) to be further processed by an external LaTeX engine.
+Note that the @code{print} function will first set the interpreter of all
+strings to @qcode{"latex"}, which means all strings must be valid latex strings.
+provided that all strings can be processed by LaTeX, not only those.
+@end itemize
 
 A complete example showing the capabilities of text printing using the
 @option{-dpdflatexstandalone} option is:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libinterp/corefcn/base-text-renderer.cc	Mon Mar 22 21:32:54 2021 +0100
@@ -0,0 +1,173 @@
+////////////////////////////////////////////////////////////////////////
+//
+// Copyright (C) 2021 The Octave Project Developers
+//
+// See the file COPYRIGHT.md in the top-level directory of this
+// distribution or <https://octave.org/copyright/>.
+//
+// This file is part of Octave.
+//
+// Octave is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Octave is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Octave; see the file COPYING.  If not, see
+// <https://www.gnu.org/licenses/>.
+//
+////////////////////////////////////////////////////////////////////////
+
+#if defined (HAVE_CONFIG_H)
+#  include "config.h"
+#endif
+
+#include "base-text-renderer.h"
+
+namespace octave
+{
+  void
+  base_text_renderer::rotate_pixels (uint8NDArray& pixels, int rot_mode) const
+  {
+    switch (rot_mode)
+      {
+      case ROTATION_0:
+        break;
+
+      case ROTATION_90:
+        {
+          Array<octave_idx_type> perm (dim_vector (3, 1));
+          perm(0) = 0;
+          perm(1) = 2;
+          perm(2) = 1;
+          pixels = pixels.permute (perm);
+
+          Array<idx_vector> idx (dim_vector (3, 1));
+          idx(0) = idx_vector (':');
+          idx(1) = idx_vector (pixels.dim2 ()-1, -1, -1);
+          idx(2) = idx_vector (':');
+          pixels = uint8NDArray (pixels.index (idx));
+        }
+        break;
+
+      case ROTATION_180:
+        {
+          Array<idx_vector> idx (dim_vector (3, 1));
+          idx(0) = idx_vector (':');
+          idx(1) = idx_vector (pixels.dim2 ()-1, -1, -1);
+          idx(2) = idx_vector (pixels.dim3 ()-1, -1, -1);
+          pixels = uint8NDArray (pixels.index (idx));
+        }
+        break;
+
+      case ROTATION_270:
+        {
+          Array<octave_idx_type> perm (dim_vector (3, 1));
+          perm(0) = 0;
+          perm(1) = 2;
+          perm(2) = 1;
+          pixels = pixels.permute (perm);
+
+          Array<idx_vector> idx (dim_vector (3, 1));
+          idx(0) = idx_vector (':');
+          idx(1) = idx_vector (':');
+          idx(2) = idx_vector (pixels.dim3 ()-1, -1, -1);
+          pixels = uint8NDArray (pixels.index (idx));
+        }
+        break;
+
+      }
+  }
+
+  int
+  base_text_renderer::rotation_to_mode (double rotation) const
+  {
+    // Wrap rotation to range [0, 360]
+    while (rotation < 0)
+      rotation += 360.0;
+    while (rotation > 360.0)
+      rotation -= 360.0;
+
+    if (rotation == 0.0)
+      return ROTATION_0;
+    else if (rotation == 90.0)
+      return ROTATION_90;
+    else if (rotation == 180.0)
+      return ROTATION_180;
+    else if (rotation == 270.0)
+      return ROTATION_270;
+    else
+      return ROTATION_0;
+  }
+
+  void
+  base_text_renderer::fix_bbox_anchor (Matrix& bbox, int halign,
+                                       int valign, int rot_mode,
+                                       bool handle_rotation) const
+  {
+    switch (halign)
+      {
+      case 1:
+        bbox(0) = -bbox(2)/2;
+        break;
+
+      case 2:
+        bbox(0) = -bbox(2);
+        break;
+
+      default:
+        bbox(0) = 0;
+        break;
+      }
+
+    switch (valign)
+      {
+      case 1:
+        bbox(1) = -bbox(3)/2;
+        break;
+
+      case 2:
+        bbox(1) = -bbox(3);
+        break;
+
+      case 3:
+        break;
+
+      case 4:
+        bbox(1) = -bbox(3)-bbox(1);
+        break;
+
+      default:
+        bbox(1) = 0;
+        break;
+      }
+
+    if (handle_rotation)
+      {
+        switch (rot_mode)
+          {
+          case ROTATION_90:
+            std::swap (bbox(0), bbox(1));
+            std::swap (bbox(2), bbox(3));
+            bbox(0) = -bbox(0)-bbox(2);
+            break;
+
+          case ROTATION_180:
+            bbox(0) = -bbox(0)-bbox(2);
+            bbox(1) = -bbox(1)-bbox(3);
+            break;
+
+          case ROTATION_270:
+            std::swap (bbox(0), bbox(1));
+            std::swap (bbox(2), bbox(3));
+            bbox(1) = -bbox(1)-bbox(3);
+            break;
+          }
+      }
+  }
+}
--- a/libinterp/corefcn/base-text-renderer.h	Mon Mar 29 07:54:26 2021 +0200
+++ b/libinterp/corefcn/base-text-renderer.h	Mon Mar 22 21:32:54 2021 +0100
@@ -45,6 +45,14 @@
   {
   public:
 
+    enum
+    {
+      ROTATION_0   = 0,
+      ROTATION_90  = 1,
+      ROTATION_180 = 2,
+      ROTATION_270 = 3
+    };
+
     base_text_renderer (void) : text_processor () { }
 
     // No copying!
@@ -85,6 +93,15 @@
                      std::list<text_renderer::string>& lst,
                      Matrix& box, int halign, int valign, double rotation,
                      const caseless_str& interpreter = "tex") = 0;
+
+    void rotate_pixels (uint8NDArray& pixels, int rot_mode) const;
+
+    int rotation_to_mode (double rotation) const;
+
+    void fix_bbox_anchor (Matrix& bbox, int halign,
+                          int valign, int rot_mode,
+                          bool handle_rotation) const;
+
   };
 }
 
--- a/libinterp/corefcn/ft-text-renderer.cc	Mon Mar 29 07:54:26 2021 +0200
+++ b/libinterp/corefcn/ft-text-renderer.cc	Mon Mar 22 21:32:54 2021 +0100
@@ -448,14 +448,6 @@
       MODE_RENDER = 1
     };
 
-    enum
-    {
-      ROTATION_0   = 0,
-      ROTATION_90  = 1,
-      ROTATION_180 = 2,
-      ROTATION_270 = 3
-    };
-
   public:
 
     ft_text_renderer (void)
@@ -526,8 +518,6 @@
 
   private:
 
-    int rotation_to_mode (double rotation) const;
-
     // Class to hold information about fonts and a strong
     // reference to the font objects loaded by FreeType.
 
@@ -1294,53 +1284,7 @@
       {
         elt->accept (*this);
 
-        switch (rotation)
-          {
-          case ROTATION_0:
-            break;
-
-          case ROTATION_90:
-            {
-              Array<octave_idx_type> perm (dim_vector (3, 1));
-              perm(0) = 0;
-              perm(1) = 2;
-              perm(2) = 1;
-              pixels = pixels.permute (perm);
-
-              Array<idx_vector> idx (dim_vector (3, 1));
-              idx(0) = idx_vector (':');
-              idx(1) = idx_vector (pixels.dim2 ()-1, -1, -1);
-              idx(2) = idx_vector (':');
-              pixels = uint8NDArray (pixels.index (idx));
-            }
-            break;
-
-          case ROTATION_180:
-            {
-              Array<idx_vector> idx (dim_vector (3, 1));
-              idx(0) = idx_vector (':');
-              idx(1) = idx_vector (pixels.dim2 ()-1, -1, -1);
-              idx(2) = idx_vector (pixels.dim3 ()-1, -1, -1);
-              pixels = uint8NDArray (pixels.index (idx));
-            }
-            break;
-
-          case ROTATION_270:
-            {
-              Array<octave_idx_type> perm (dim_vector (3, 1));
-              perm(0) = 0;
-              perm(1) = 2;
-              perm(2) = 1;
-              pixels = pixels.permute (perm);
-
-              Array<idx_vector> idx (dim_vector (3, 1));
-              idx(0) = idx_vector (':');
-              idx(1) = idx_vector (':');
-              idx(2) = idx_vector (pixels.dim3 ()-1, -1, -1);
-              pixels = uint8NDArray (pixels.index (idx));
-            }
-            break;
-          }
+        rotate_pixels (pixels, rotation);
       }
 
     return pixels;
@@ -1388,27 +1332,6 @@
     return extent;
   }
 
-  int
-  ft_text_renderer::rotation_to_mode (double rotation) const
-  {
-    // Clip rotation to range [0, 360]
-    while (rotation < 0)
-      rotation += 360.0;
-    while (rotation > 360.0)
-      rotation -= 360.0;
-
-    if (rotation == 0.0)
-      return ROTATION_0;
-    else if (rotation == 90.0)
-      return ROTATION_90;
-    else if (rotation == 180.0)
-      return ROTATION_180;
-    else if (rotation == 270.0)
-      return ROTATION_270;
-    else
-      return ROTATION_0;
-  }
-
   void
   ft_text_renderer::text_to_pixels (const std::string& txt,
                                     uint8NDArray& pxls, Matrix& box,
@@ -1427,65 +1350,9 @@
     if (pxls.isempty ())
       return;  // nothing to render
 
-    switch (halign)
-      {
-      case 1:
-        box(0) = -box(2)/2;
-        break;
-
-      case 2:
-        box(0) = -box(2);
-        break;
-
-      default:
-        box(0) = 0;
-        break;
-      }
-
-    switch (valign)
-      {
-      case 1:
-        box(1) = -box(3)/2;
-        break;
-
-      case 2:
-        box(1) = -box(3);
-        break;
-
-      case 3:
-        break;
-
-      case 4:
-        box(1) = -box(3)-box(1);
-        break;
-
-      default:
-        box(1) = 0;
-        break;
-      }
-
-    if (handle_rotation)
-      {
-        switch (rot_mode)
-          {
-          case ROTATION_90:
-            std::swap (box(0), box(1));
-            std::swap (box(2), box(3));
-            box(0) = -box(0)-box(2);
-            break;
-
-          case ROTATION_180:
-            box(0) = -box(0)-box(2);
-            box(1) = -box(1)-box(3);
-            break;
-
-          case ROTATION_270:
-            std::swap (box(0), box(1));
-            std::swap (box(2), box(3));
-            box(1) = -box(1)-box(3);
-            break;
-          }
-      }
+    // Move X0 and Y0 depending on alignments and eventually swap all values
+    // for text rotated 90° 180° or 270°
+    fix_bbox_anchor (box, halign, valign, rot_mode, handle_rotation);
   }
 
   ft_text_renderer::ft_font::ft_font (const ft_font& ft)
--- a/libinterp/corefcn/gl2ps-print.cc	Mon Mar 29 07:54:26 2021 +0200
+++ b/libinterp/corefcn/gl2ps-print.cc	Mon Mar 22 21:32:54 2021 +0100
@@ -64,8 +64,8 @@
 
     gl2ps_renderer (opengl_functions& glfcns, FILE *_fp,
                     const std::string& _term)
-      : opengl_renderer (glfcns), fp (_fp), term (_term),
-        fontsize (), fontname (), buffer_overflow (false)
+      : opengl_renderer (glfcns), fp (_fp), term (_term), fontsize (),
+        fontname (), buffer_overflow (false), m_svg_def_index (0)
     { }
 
     ~gl2ps_renderer (void) = default;
@@ -266,7 +266,11 @@
                                Matrix box, double rotation,
                                std::list<text_renderer::string>& lst);
 
-    // Build an svg text element from a list of parsed strings.
+    // Build an svg text element from a list of parsed strings
+    std::string format_svg_element (std::string str, Matrix bbox,
+                                    double rotation, ColumnVector coord_pix,
+                                    Matrix color);
+
     std::string strlist_to_svg (double x, double y, double z, Matrix box,
                                 double rotation,
                                 std::list<text_renderer::string>& lst);
@@ -283,6 +287,7 @@
     double fontsize;
     std::string fontname;
     bool buffer_overflow;
+    std::size_t m_svg_def_index;
   };
 
   static bool
@@ -487,8 +492,7 @@
                         error ("gl2ps_renderer::draw: internal pipe error");
                       }
                   }
-                else if (! header_found
-                         && term.find ("svg") != std::string::npos)
+                else if (term.find ("svg") != std::string::npos)
                   {
                     // FIXME: gl2ps uses pixel units for SVG format.
                     //        Modify resulting svg to use points instead.
@@ -496,7 +500,7 @@
                     //        make header_found true for SVG if gl2ps is fixed.
                     std::string srchstr (str);
                     size_t pos = srchstr.find ("px");
-                    if (pos != std::string::npos)
+                    if (! header_found && pos != std::string::npos)
                       {
                         header_found = true;
                         srchstr[pos+1] = 't';  // "px" -> "pt"
@@ -794,20 +798,134 @@
   }
 
   std::string
+  gl2ps_renderer::format_svg_element (std::string str, Matrix box,
+                                      double rotation, ColumnVector coord_pix,
+                                      Matrix color)
+  {
+    // Extract <defs> elements and change their id to avoid conflict with
+    // defs coming from another svg string
+    std::string::size_type n1 = str.find ("<defs>");
+    if (n1 == std::string::npos)
+      return std::string ();
+
+    std::string id, new_id;
+    n1 = str.find ("<path", ++n1);
+    std::string::size_type n2;
+
+    while (n1 != std::string::npos)
+      {
+        // Extract the identifier id='identifier'
+        n1 = str.find ("id='", n1) + 4;
+        n2 = str.find ("'", n1);
+        id = str.substr (n1, n2-n1);
+
+        new_id = std::to_string (m_svg_def_index) + "-" + id ;
+
+        str.replace (n1, n2-n1, new_id);
+
+        std::string::size_type n_ref = str.find ("#" + id);
+
+        while (n_ref != std::string::npos)
+          {
+            str.replace (n_ref + 1, id.length (), new_id);
+            n_ref = str.find ("#" + id);
+          }
+
+        n1 = str.find ("<path", n1);
+      }
+
+    m_svg_def_index++;
+
+    n1 = str.find ("<defs>");
+    n2 = str.find ("</defs>") + 7;
+
+    std::string defs = str.substr (n1, n2-n1);
+
+    // Extract the group containing the <use> elements and transform its
+    // coordinates using the bbox and coordinates info.
+
+    // Extract the original viewBox anchor
+    n1 = str.find ("viewBox='") + 9;
+    if (n1 == std::string::npos)
+      return std::string ();
+
+    n2 = str.find (" ", n1);
+    double original_x0 = std::stod (str.substr (n1, n2-n1));
+
+    n1 = n2+1;
+    n2 = str.find (" ", n1);
+    double original_y0 = std::stod (str.substr (n1, n2-n1));
+
+    // First look for local transform in the original svg
+    std::string orig_trans;
+    n1 = str.find ("<g id='page1' transform='");
+    if (n1 != std::string::npos)
+      {
+        n1 += 25;
+        n2 = str.find ("'", n1);
+        orig_trans = str.substr (n1, n2-n1);
+        n1 = n2 + 1;
+      }
+    else
+      {
+        n1 = str.find ("<g id='page1'");
+        n1 += 13;
+      }
+
+    n2 = str.find ("</g>", n1) + 4;
+
+    // The first applied transformation is the right-most
+    // 1* Apply original transform
+    std::string tform = orig_trans;
+
+    // 2* Move the anchor to the final position
+    tform = std::string ("translate")
+      + "(" + std::to_string (box(0) - original_x0 + coord_pix(0))
+      + "," + std::to_string (-(box(3) + box(1)) - original_y0 + coord_pix(1))
+      + ") " + tform;
+
+    // 3* Rotate around the final position
+    if (rotation != 0)
+      tform = std::string ("rotate")
+        + "(" + std::to_string (-rotation)
+        + "," + std::to_string (coord_pix(0))
+        + "," + std::to_string (coord_pix(1))
+        + ") " + tform;
+
+    // Fill color
+    std::string fill = "fill='rgb("
+      + std::to_string (static_cast<uint8_t> (color(0) * 255.0)) + ","
+      + std::to_string (static_cast<uint8_t> (color(1) * 255.0)) + ","
+      + std::to_string (static_cast<uint8_t> (color(2) * 255.0)) + ")' ";
+
+    std::string use_group = "<g "
+      + fill
+      + "transform='" + tform + "'"
+      + str.substr (n1, n2-n1);
+
+    return defs + "\n" + use_group;
+  }
+
+  std::string
   gl2ps_renderer::strlist_to_svg (double x, double y, double z,
                                   Matrix box, double rotation,
                                   std::list<text_renderer::string>& lst)
   {
+    //Use pixel coordinates to conform to gl2ps
+    ColumnVector coord_pix = get_transform ().transform (x, y, z, false);
+
     if (lst.empty ())
       return "";
 
-    //Use pixel coordinates to conform to gl2ps
-    ColumnVector coord_pix = get_transform ().transform (x, y, z, false);
+    // This may already be an svg image.
+    std::string svg = lst.front ().get_svg_element ();
+    if (! svg.empty ())
+      return format_svg_element (svg, box, rotation, coord_pix,
+                                 lst.front ().get_color ());
 
+    // Rotation and translation are applied to the whole group
     std::ostringstream os;
-    os << R"(<text xml:space="preserve" )";
-
-    // Rotation and translation are applied to the whole text element
+    os << R"(<g xml:space="preserve" )";
     os << "transform=\""
        << "translate(" << coord_pix(0) + box(0) << "," << coord_pix(1) - box(1)
        << ") rotate(" << -rotation << "," << -box(0) << "," << box(1)
@@ -826,10 +944,10 @@
        << "font-size=\"" << size << "\">";
 
 
-    // build a tspan for each element in the strlist
+    // Build a text element for each element in the strlist
     for (p = lst.begin (); p != lst.end (); p++)
       {
-        os << "<tspan ";
+        os << "<text ";
 
         if (name.compare (p->get_family ()))
           os << "font-family=\"" << p->get_family () << "\" ";
@@ -882,9 +1000,9 @@
                   os << chr.str ();
               }
           }
-        os << "</tspan>";
+        os << "</text>";
       }
-    os << "</text>";
+    os << "</g>";
 
     return os.str ();
   }
@@ -894,6 +1012,26 @@
                                  Matrix box, double rotation,
                                  std::list<text_renderer::string>& lst)
   {
+    if (lst.empty ())
+      return "";
+    else if (lst.size () == 1)
+      {
+        static bool warned = false;
+        // This may be an svg image, not handled in native eps format.
+        if (! lst.front ().get_svg_element ().empty ())
+          {
+            if (! warned)
+              {
+                warned = true;
+                warning_with_id ("Octave:print:unhandled-svg-content",
+                                 "print: unhandled LaTeX strings. "
+                                 "Use -svgconvert option or -d*latex* output "
+                                 "device.");
+              }
+            return "";
+          }
+      }
+
     // Translate and rotate coordinates in order to use bottom-left alignment
     fix_strlist_position (x, y, z, box, rotation, lst);
     Matrix prev_color (1, 3, -1);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libinterp/corefcn/latex-text-renderer.cc	Mon Mar 22 21:32:54 2021 +0100
@@ -0,0 +1,502 @@
+////////////////////////////////////////////////////////////////////////
+//
+// Copyright (C) 2013-2021 The Octave Project Developers
+//
+// See the file COPYRIGHT.md in the top-level directory of this
+// distribution or <https://octave.org/copyright/>.
+//
+// This file is part of Octave.
+//
+// Octave is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Octave is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Octave; see the file COPYING.  If not, see
+// <https://www.gnu.org/licenses/>.
+//
+////////////////////////////////////////////////////////////////////////
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <iostream>
+#include <fstream>
+
+#include "base-text-renderer.h"
+#include "builtin-defun-decls.h"
+#include "dim-vector.h"
+#include "error.h"
+#include "file-ops.h"
+#include "interpreter.h"
+#include "interpreter-private.h"
+#include "oct-env.h"
+#include "oct-process.h"
+
+namespace octave
+{
+  // FIXME: get rid of this global variable
+  // Store both the generated pixmap and the svg image
+  typedef std::pair<uint8NDArray /*pixels*/, std::string /*svg*/> latex_data;
+  std::unordered_map<std::string, latex_data> latex_cache;
+
+  std::string
+  quote_string (std::string str)
+  {
+    return ('"' + str + '"');
+  }
+
+  class
+  OCTINTERP_API
+  latex_renderer : public base_text_renderer
+ {
+
+  public:
+
+    latex_renderer (void)
+      : m_fontsize (10.0), m_fontname ("cmr"), m_tmp_dir (),
+        m_color (dim_vector (1, 3), 0), m_latex_binary ("latex"),
+        m_dvipng_binary ("dvipng"), m_dvisvg_binary ("dvisvgm"), m_debug (false)
+      {
+        std::string bin = sys::env::getenv ("OCTAVE_LATEX_BINARY");
+        if (! bin.empty ())
+          m_latex_binary = quote_string (bin);
+
+        bin = sys::env::getenv ("OCTAVE_DVIPNG_BINARY");
+        if (! bin.empty ())
+          m_dvipng_binary = quote_string (bin);
+
+        bin = sys::env::getenv ("OCTAVE_DVISVG_BINARY");
+        if (! bin.empty ())
+          m_dvisvg_binary = quote_string (bin);
+
+        m_debug = ! sys::env::getenv ("OCTAVE_LATEX_DEBUG_FLAG").empty ();
+      }
+
+    ~latex_renderer (void)
+      {
+        if (! m_tmp_dir.empty () && ! m_debug)
+          octave::sys::recursive_rmdir (m_tmp_dir);
+      }
+
+    void set_font (const std::string& /*name*/, const std::string& /*weight*/,
+                   const std::string& /*angle*/, double size)
+    {
+      m_fontsize = size;
+    }
+
+    void set_color (const Matrix& c)
+    {
+      if (c.numel () == 3)
+        {
+          m_color(0) = static_cast<uint8_t> (c (0) * 255);
+          m_color(1) = static_cast<uint8_t> (c (1) * 255);
+          m_color(2) = static_cast<uint8_t> (c (2) * 255);
+        }
+    }
+
+    Matrix get_extent (text_element* /*elt*/, double /*rotation*/)
+    {
+      return Matrix (1, 2, 0.0);
+    }
+
+    Matrix get_extent (const std::string& txt, double rotation,
+                       const caseless_str& interpreter)
+    {
+      Matrix bbox;
+      uint8NDArray pixels;
+      text_to_pixels (txt, pixels, bbox, 0, 0, rotation, interpreter, false);
+      return bbox.extract_n (0, 2, 1, 2);
+    }
+
+    void text_to_strlist (const std::string& txt,
+                          std::list<text_renderer::string>& lst,
+                          Matrix& bbox, int halign, int valign, double rotation,
+                          const caseless_str& interp)
+    {
+      uint8NDArray pixels;
+      text_to_pixels (txt, pixels, bbox, halign, valign, rotation,
+                      interp, false);
+
+      text_renderer::font fnt;
+      text_renderer::string str ("", fnt, 0.0, 0.0);
+      str.set_color (m_color);
+      str.set_svg_element (latex_cache[key (txt, halign)].second);
+      lst.push_back (str);
+    }
+
+    void text_to_pixels (const std::string& txt, uint8NDArray& pxls,
+                         Matrix& bbox, int halign, int valign, double rotation,
+                         const caseless_str& interpreter,
+                         bool handle_rotation);
+
+    void set_anti_aliasing (bool /*val*/) { }
+
+    octave_map get_system_fonts (void) { return octave_map (); }
+
+    /* method that checks if all required programs are installed */
+    bool is_usable (void);
+
+  private:
+
+    std::string key (const std::string& txt, int halign)
+    {
+      return (txt + ":"
+              + std::to_string (m_fontsize) + ":"
+              + std::to_string (halign) + ":"
+              + std::to_string (m_color(0)) + ":"
+              + std::to_string (m_color(1)) + ":"
+              + std::to_string (m_color(2)));
+    }
+
+    uint8NDArray render (const std::string& txt, int halign = 0);
+
+    bool read_image (const std::string& png_file, uint8NDArray& data) const;
+
+    std::string write_tex_file (const std::string& txt, int halign);
+
+  private:
+    double m_fontsize;
+    std::string m_fontname;
+    std::string m_tmp_dir;
+    uint8NDArray m_color;
+    std::string m_latex_binary;
+    std::string m_dvipng_binary;
+    std::string m_dvisvg_binary;
+    bool m_debug;
+
+  };
+
+  bool
+  latex_renderer::is_usable (void)
+  {
+    static bool tested = false;
+    static bool ok = false;
+
+    if (! tested)
+      {
+        tested = true;
+
+        // For testing, render a questoin mark
+        uint8NDArray pixels = render ("?");
+
+        if (! pixels.isempty ())
+          ok = true;
+      }
+
+    return ok;
+  }
+
+  std::string
+  latex_renderer::write_tex_file (const std::string& txt, int halign)
+  {
+    if (m_tmp_dir.empty ())
+      {
+        //Create the temporary directory
+        m_tmp_dir = octave::sys::tempnam ("", "latex");
+
+        if (octave::sys::mkdir (m_tmp_dir, 0700) != 0)
+          {
+            warning_with_id ("Octave:LaTeX:internal-error",
+                             "latex_renderer: unable to create temp directory");
+            return std::string ();
+          }
+      }
+
+    std::string base_file_name
+      = octave::sys::file_ops::concat (m_tmp_dir, "default");
+
+    // Duplicate \n characters and align multi-line strings based on
+    // horizontalalignment
+    std::string latex_txt (txt);
+    size_t pos = 0;
+
+    while (true)
+      {
+        pos =  txt.find_first_of ("\n", pos);
+
+        if (pos == std::string::npos)
+          break;
+
+        latex_txt.replace (pos, 1, "\n\n");
+
+        pos += 1;
+      }
+
+    std::string env ("flushleft");
+    if (halign == 1)
+      env = "center";
+    else if (halign == 2)
+      env = "flushright";
+
+    latex_txt = std::string ("\\begin{" ) + env + "}\n"
+      + latex_txt + "\n"
+      + "\\end{" + env + "}\n";
+
+    // Write to temporary .tex file
+    std::ofstream file;
+    file.open (base_file_name + ".tex");
+    file << "\\documentclass[10pt, varwidth]{standalone}\n"
+         << "\\usepackage{amsmath}\n"
+         << "\\usepackage[utf8]{inputenc}\n"
+         << "\\begin{document}\n"
+         << latex_txt << "\n"
+         << "\\end{document}";
+    file.close ();
+
+    return base_file_name;
+  }
+
+  bool
+  latex_renderer::read_image (const std::string& png_file,
+                              uint8NDArray& data) const
+  {
+    uint8NDArray alpha;
+    uint8NDArray rgb;
+    int height;
+    int width;
+
+    try
+      {
+        // First get the image size to build the argument to __magick_read__
+        octave_value_list retval = F__magick_ping__ (ovl (png_file), 1);
+
+        octave_scalar_map info
+          = retval(0).xscalar_map_value ("latex_renderer::read_image: "
+                                         "Wrong type for info");
+        height = info.getfield ("rows").int_value ();
+        width = info.getfield ("columns").int_value ();
+        Cell region (dim_vector(1,2));
+        region(0) = range<double> (1.0, height);
+        region(1) = range<double> (1.0, width);
+        info.setfield ("region", region);
+        info.setfield ("index", octave_value (1));
+
+        // Retrieve the alpha map
+        retval = F__magick_read__ (ovl (png_file, info), 3);
+
+        alpha = retval(2).xuint8_array_value ("latex_renderer::read_image: "
+                                              "Wrong type for alpha");
+      }
+    catch (const octave::execution_exception& ee)
+      {
+        warning_with_id ("Octave:LaTeX:internal-error",
+                         "latex_renderer:: failed to read png data. %s",
+                         ee.message ().c_str ());
+
+        octave::interpreter& interp
+          = octave::__get_interpreter__ ("latex_renderer::read_image");
+
+        interp.recover_from_exception ();
+
+        return false;
+      }
+
+    data = uint8NDArray (dim_vector (4, width, height),
+                             static_cast<uint8_t> (0));
+
+    for (int i = 0; i < height; i++)
+      {
+        for (int j = 0; j < width; j++)
+          {
+            data(0, j, i) = m_color(0);
+            data(1, j, i) = m_color(1);
+            data(2, j, i) = m_color(2);
+            data(3, j, i) = alpha(height-i-1,j);
+          }
+      }
+
+    return true;
+  }
+
+  void
+  warn_helper (std::string caller, std::string txt, std::string cmd,
+               process_execution_result result, bool debug)
+  {
+    if (! debug)
+      warning_with_id ("Octave:LaTeX:internal-error",
+                       "latex_renderer: unable to compile \"%s\"",
+                       txt.c_str ());
+    else
+      warning_with_id ("Octave:LaTeX:internal-error",
+                       "latex_renderer: %s failed for string \"%s\"\n\
+* Command:\n\t%s\n\n* Error:\n%s\n\n* Stdout:\n%s",
+                       caller.c_str (), txt.c_str (), cmd.c_str (),
+                       result.err_msg ().c_str (),
+                       result.stdout_output ().c_str ());
+  }
+
+  uint8NDArray
+  latex_renderer::render (const std::string& txt, int halign)
+  {
+    // Render if it was not already done
+    auto it = latex_cache.find (key (txt, halign));
+
+    if (it != latex_cache.end ())
+      return it->second.first;
+
+    uint8NDArray data;
+
+    // First write the base .tex file
+    std::string base_file_name = write_tex_file (txt, halign);
+
+    if (base_file_name.empty ())
+      return data;
+
+    // Generate DVI file
+    std::string tex_file = quote_string (base_file_name + ".tex");
+    std::string dvi_file = quote_string (base_file_name + ".dvi");
+    std::string log_file = quote_string (base_file_name + ".log");
+
+    process_execution_result result;
+    std::string cmd = (m_latex_binary + " -interaction=nonstopmode "
+                       + "-output-directory=" + quote_string (m_tmp_dir) + " "
+                       + tex_file);
+
+#if defined (OCTAVE_USE_WINDOWS_API)
+    cmd = quote_string (cmd);
+#endif
+
+    result = run_command_and_return_output (cmd);
+
+    if (result.exit_status () != 0)
+      {
+        warn_helper ("latex", txt, cmd, result, m_debug);
+
+        write_tex_file ("?", halign);
+
+        result = run_command_and_return_output (cmd);
+        if (result.exit_status () != 0)
+          return data;
+      }
+
+    double size_factor = m_fontsize / 10.0;
+
+
+    // Convert DVI to SVG, read file and store its content for later use in
+    // gl2ps_print
+    std::string svg_file = base_file_name + ".svg";
+
+    cmd = (m_dvisvg_binary + " -n "
+           + "-TS" + std::to_string (size_factor) + " "
+           + "-v1 -o " + quote_string (svg_file) + " "
+           + dvi_file);
+
+#if defined (OCTAVE_USE_WINDOWS_API)
+    cmd = quote_string (cmd);
+#endif
+
+    result = run_command_and_return_output (cmd);
+
+    if (result.exit_status () != 0)
+      {
+        warn_helper ("dvisvg", txt, cmd, result, m_debug);
+        return data;
+      }
+
+    std::ifstream svg_stream (svg_file);
+    std::string svg_string;
+    svg_string.assign (std::istreambuf_iterator<char> (svg_stream),
+                       std::istreambuf_iterator<char> ());
+
+    // Convert DVI to PNG, read file and format pixel data for later use in
+    // OpenGL
+    std::string png_file = base_file_name + ".png";
+
+    cmd = (m_dvipng_binary + " " + dvi_file + " "
+           + "-q -o " + quote_string (png_file) + " "
+           + "-bg Transparent -D "
+           + std::to_string (std::floor (72.0 * size_factor)));
+
+#if defined (OCTAVE_USE_WINDOWS_API)
+    cmd = quote_string (cmd);
+#endif
+
+    result = run_command_and_return_output (cmd);
+
+    if (result.exit_status () != 0)
+      {
+        warn_helper ("dvipng", txt, cmd, result, m_debug);
+        return data;
+      }
+
+    if (! read_image (png_file, data))
+      return data;
+
+    // Cache pixel and svg data for this string
+    latex_cache[key (txt, halign)] = latex_data (data, svg_string);
+
+    if (m_debug)
+      std::cout << "* Caching " << key (txt, halign)
+                << " (numel: " << latex_cache.size () << ")\n";
+
+    return data;
+  }
+
+  void
+  latex_renderer::text_to_pixels (const std::string& txt, uint8NDArray& pixels,
+                                  Matrix& bbox, int halign, int valign,
+                                  double rotation,
+                                  const caseless_str& /*interpreter*/,
+                                  bool handle_rotation)
+  {
+    // Return early for empty strings
+    if (txt.empty ())
+      return;
+
+    if (is_usable ())
+      pixels = render (txt, halign);
+    else
+      pixels = uint8NDArray (dim_vector (4, 1, 1), static_cast<uint8_t> (0));
+
+    if (pixels.ndims () < 3 || pixels.isempty ())
+      return;  // nothing to render
+
+    // Store unrotated bbox size
+    bbox = Matrix (1, 4, 0.0);
+    bbox (2) = pixels.dim2 ();
+    bbox (3) = pixels.dim3 ();
+
+    // Now rotate pixels if necessary
+    int rot_mode = rotation_to_mode (rotation);
+
+    if (! pixels.isempty ())
+      rotate_pixels (pixels, rot_mode);
+
+    // Move X0 and Y0 depending on alignments and eventually swap values
+    // for text rotated 90° 180° or 270°
+    fix_bbox_anchor (bbox, halign, valign, rot_mode, handle_rotation);
+  }
+
+  base_text_renderer*
+  make_latex_text_renderer (void)
+  {
+    static bool latex_tested = false;
+    static bool latex_ok = true;
+
+    if (! latex_ok)
+      return nullptr;
+
+    latex_renderer *renderer = new latex_renderer ();
+
+    if (! latex_tested)
+      {
+        latex_tested = true;
+        latex_ok = renderer->is_usable ();
+        if (! latex_ok)
+          {
+            delete renderer;
+            renderer = nullptr;
+          }
+      }
+
+    return renderer;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libinterp/corefcn/latex-text-renderer.h	Mon Mar 22 21:32:54 2021 +0100
@@ -0,0 +1,38 @@
+////////////////////////////////////////////////////////////////////////
+//
+// Copyright (C) 2013-2021 The Octave Project Developers
+//
+// See the file COPYRIGHT.md in the top-level directory of this
+// distribution or <https://octave.org/copyright/>.
+//
+// This file is part of Octave.
+//
+// Octave is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Octave is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Octave; see the file COPYING.  If not, see
+// <https://www.gnu.org/licenses/>.
+//
+////////////////////////////////////////////////////////////////////////
+
+#if ! defined (octave_latex_text_renderer_h)
+#define octave_latex_text_renderer_h 1
+
+#include "octave-config.h"
+
+namespace octave
+{
+  class base_text_renderer;
+
+  extern base_text_renderer * make_latex_text_renderer (void);
+}
+
+#endif
--- a/libinterp/corefcn/module.mk	Mon Mar 29 07:54:26 2021 +0200
+++ b/libinterp/corefcn/module.mk	Mon Mar 22 21:32:54 2021 +0100
@@ -46,6 +46,7 @@
   %reldir%/hook-fcn.h \
   %reldir%/input.h \
   %reldir%/interpreter.h \
+  %reldir%/latex-text-renderer.h \
   %reldir%/load-path.h \
   %reldir%/load-save.h \
   %reldir%/ls-ascii-helper.h \
@@ -124,6 +125,7 @@
   %reldir%/__qp__.cc \
   %reldir%/amd.cc \
   %reldir%/balance.cc \
+  %reldir%/base-text-renderer.cc \
   %reldir%/besselj.cc \
   %reldir%/bitfcns.cc \
   %reldir%/bsxfun.cc \
@@ -189,6 +191,7 @@
   %reldir%/jsondecode.cc \
   %reldir%/jsonencode.cc \
   %reldir%/kron.cc \
+  %reldir%/latex-text-renderer.cc \
   %reldir%/load-path.cc \
   %reldir%/load-save.cc \
   %reldir%/lookup.cc \
--- a/libinterp/corefcn/text-engine.h	Mon Mar 29 07:54:26 2021 +0200
+++ b/libinterp/corefcn/text-engine.h	Mon Mar 22 21:32:54 2021 +0100
@@ -322,7 +322,7 @@
   text_processor
   {
   public:
-    virtual void visit (text_element_string& e) = 0;
+    virtual void visit (text_element_string&) { }
 
     virtual void visit (text_element_symbol&) { }
 
--- a/libinterp/corefcn/text-renderer.cc	Mon Mar 29 07:54:26 2021 +0200
+++ b/libinterp/corefcn/text-renderer.cc	Mon Mar 22 21:32:54 2021 +0100
@@ -28,28 +28,23 @@
 #endif
 
 #include "base-text-renderer.h"
+#include "error.h"
 #include "errwarn.h"
 #include "ft-text-renderer.h"
+#include "latex-text-renderer.h"
+#include "oct-env.h"
 #include "text-renderer.h"
 
 namespace octave
 {
-  static base_text_renderer *
-  make_text_renderer (void)
-  {
-    // Allow the possibility of choosing different text rendering
-    // implementations.
-
-    return make_ft_text_renderer ();
-  }
-
   text_renderer::text_renderer (void)
-    : rep (make_text_renderer ())
+    : rep (make_ft_text_renderer ()), latex_rep (make_latex_text_renderer ())
   { }
 
   text_renderer::~text_renderer (void)
   {
     delete rep;
+    delete latex_rep;
   }
 
   bool
@@ -71,6 +66,22 @@
     return rep != nullptr;
   }
 
+  bool
+  text_renderer::latex_ok (void) const
+  {
+    static bool warned = false;
+
+    if (! sys::env::getenv ("OCTAVE_LATEX_DEBUG_FLAG").empty ()
+        && ! latex_rep && ! warned)
+      {
+        warning_with_id ("Octave:LaTeX:internal-error",
+                         "latex_renderer: unusable latex tool-chain");
+        warned = true;
+      }
+
+    return latex_rep != nullptr;
+  }
+
   Matrix
   text_renderer::get_extent (text_element *elt, double rotation)
   {
@@ -83,9 +94,14 @@
   text_renderer::get_extent (const std::string& txt, double rotation,
                              const caseless_str& interpreter)
   {
-    static Matrix empty_extent (1, 4, 0.0);
+    static Matrix retval (1, 4, 0.0);
 
-    return ok () ? rep->get_extent (txt, rotation, interpreter) : empty_extent;
+    if (interpreter == "latex" && latex_ok ())
+      retval = latex_rep->get_extent (txt, rotation, interpreter);
+    else if (ok ())
+      retval = rep->get_extent (txt, rotation, interpreter);
+
+    return retval;
   }
 
   void
@@ -93,6 +109,9 @@
   {
     if (ok ())
       rep->set_anti_aliasing (val);
+
+    if (latex_ok ())
+      latex_rep->set_anti_aliasing (val);
   }
 
   octave_map
@@ -112,6 +131,9 @@
   {
     if (ok ())
       rep->set_font (name, weight, angle, size);
+
+    if (latex_ok ())
+      latex_rep->set_font (name, weight, angle, size);
   }
 
   void
@@ -119,6 +141,9 @@
   {
     if (ok ())
       rep->set_color (c);
+
+    if (latex_ok ())
+      latex_rep->set_color (c);
   }
 
   void
@@ -131,7 +156,10 @@
     static Matrix empty_bbox (1, 4, 0.0);
     static uint8NDArray empty_pxls;
 
-    if (ok ())
+    if (interpreter == "latex" && latex_ok ())
+      latex_rep->text_to_pixels (txt, pxls, bbox, halign, valign, rotation,
+                                 interpreter, handle_rotation);
+    else if (ok ())
       rep->text_to_pixels (txt, pxls, bbox, halign, valign, rotation,
                            interpreter, handle_rotation);
     else
@@ -151,7 +179,10 @@
     static Matrix empty_bbox (1, 4, 0.0);
     static std::list<text_renderer::string> empty_lst;
 
-    if (ok ())
+    if (interpreter == "latex" && latex_ok ())
+      latex_rep->text_to_strlist (txt, lst, bbox, halign, valign, rotation,
+                                  interpreter);
+    else if (ok ())
       rep->text_to_strlist (txt, lst, bbox, halign, valign, rotation,
                             interpreter);
     else
--- a/libinterp/corefcn/text-renderer.h	Mon Mar 29 07:54:26 2021 +0200
+++ b/libinterp/corefcn/text-renderer.h	Mon Mar 22 21:32:54 2021 +0100
@@ -59,6 +59,8 @@
 
     bool ok (void) const;
 
+    bool latex_ok (void) const;
+
     Matrix get_extent (text_element *elt, double rotation = 0.0);
 
     Matrix get_extent (const std::string& txt, double rotation = 0.0,
@@ -136,12 +138,13 @@
 
       string (const std::string& s, font& f, const double x0, const double y0)
         : str (s), family (f.get_name ()), fnt (f), x (x0), y (y0), z (0.0),
-          xdata (), code (0), color (Matrix (1,3,0.0))
+          xdata (), code (0), color (Matrix (1,3,0.0)), svg_element ()
       { }
 
       string (const string& s)
         : str (s.str), family (s.family), fnt (s.fnt), x (s.x), y (s.y),
-          z (s.z), xdata (s.xdata), code (s.code), color (s.color)
+          z (s.z), xdata (s.xdata), code (s.code), color (s.color),
+          svg_element (s.svg_element)
       { }
 
       ~string (void) = default;
@@ -200,6 +203,10 @@
 
       uint32_t get_code (void) const { return code; }
 
+      void set_svg_element (const std::string& svg) { svg_element = svg; }
+
+      std::string get_svg_element (void) const { return svg_element; }
+
       void set_color (const uint8NDArray& c)
       {
         color(0) = static_cast<double> (c(0)) / 255;
@@ -218,6 +225,7 @@
       std::vector<double> xdata;
       uint32_t code;
       Matrix color;
+      std::string svg_element;
     };
 
     void text_to_strlist (const std::string& txt,
@@ -228,6 +236,7 @@
   private:
 
     base_text_renderer *rep;
+    base_text_renderer *latex_rep;
   };
 }
 
--- a/m4/acinclude.m4	Mon Mar 29 07:54:26 2021 +0200
+++ b/m4/acinclude.m4	Mon Mar 22 21:32:54 2021 +0100
@@ -2000,7 +2000,7 @@
   case "$qt_version" in
     5)
       QT_OPENGL_MODULE="Qt5OpenGL"
-      QT_MODULES="Qt5Core Qt5Gui Qt5Network Qt5PrintSupport Qt5Help Qt5Xml"
+      QT_MODULES="Qt5Core Qt5Gui Qt5Help Qt5Network Qt5PrintSupport Qt5Svg Qt5Xml"
     ;;
     *)
       AC_MSG_ERROR([Unrecognized Qt version $qt_version])
--- a/scripts/plot/appearance/text.m	Mon Mar 29 07:54:26 2021 +0200
+++ b/scripts/plot/appearance/text.m	Mon Mar 22 21:32:54 2021 +0100
@@ -326,6 +326,24 @@
 %! ylabel (1:2);
 %! title (1:2);
 
+%!demo
+%! clf;
+%! title ("Use of the \"interpreter\" property")
+%! xlim ([0 1])
+%! ylim ([-1 1])
+%! text (0.1, 0.5, "\"none\": erf(x) = (2/\\pi^{1/2})\\int_0^x e^{y^2}dy", ...
+%!       "interpreter", "none", ...
+%!       "fontsize", 20, ...
+%!       "backgroundcolor", "r");
+%! text (0.1, 0, "\"tex\"(def.): erf(x) = (2/\\pi^{1/2})\\int_0^x e^{y^2}dy", ...
+%!       "fontsize", 20, ...
+%!       "backgroundcolor", "r");
+%! text (0.1, -0.5, "\"latex\": $erf(x) = (2/\\pi^{1/2})\\int_0^x e^{y^2}dy$", ...
+%!       "interpreter", "latex", ...
+%!       "fontsize", 20, ...
+%!       "backgroundcolor", "r");
+%! grid on;
+
 %!test
 %! hf = figure ("visible", "off");
 %! unwind_protect
--- a/scripts/plot/util/print.m	Mon Mar 29 07:54:26 2021 +0200
+++ b/scripts/plot/util/print.m	Mon Mar 22 21:32:54 2021 +0100
@@ -132,8 +132,13 @@
 ##
 ## @table @asis
 ## @item Font handling:
-## The actual font is embedded in the output file which allows for printing
-## arbitrary characters and fonts in all vector formats.
+## For interpreters "none" and "tex", the actual font is embedded in the output
+## file which allows for printing arbitrary characters and fonts in all vector
+## formats.
+##
+## Strings using the @qcode{"latex"} interpreter, are rendered using path
+## objects. This looks good but note that textual info (font, characters@dots{})
+## are lost.
 ##
 ## @item Output Simplification:
 ## By default, the option @option{-painters} renders patch and surface objects
--- a/src/octave-svgconvert.cc	Mon Mar 29 07:54:26 2021 +0200
+++ b/src/octave-svgconvert.cc	Mon Mar 22 21:32:54 2021 +0100
@@ -34,74 +34,42 @@
 #include <QPainter>
 #include <QPrinter>
 #include <QRegExp>
+#include <QSvgRenderer>
 
+// Render to pdf
 class pdfpainter : public QPainter
 {
 public:
-  pdfpainter (QString fname, QRectF sizepix, double dpi)
-    : m_fname (fname), m_sizef (sizepix), m_dpi (dpi), m_printer ()
+  pdfpainter (QString fname, QRectF sz, double dpi)
+    : m_printer ()
   {
-    double scl = get_scale ();
-    m_sizef.setWidth (m_sizef.width () * scl);
-    m_sizef.setHeight (m_sizef.height () * scl);
+    double scl = dpi / 72.0;
+    sz.setWidth (sz.width () * scl);
+    sz.setHeight (sz.height () * scl);
 
     // Printer settings
     m_printer.setOutputFormat (QPrinter::PdfFormat);
     m_printer.setFontEmbeddingEnabled (true);
-    m_printer.setOutputFileName (get_fname ());
+    m_printer.setOutputFileName (fname);
     m_printer.setFullPage (true);
-    m_printer.setPaperSize (get_rectf ().size (), QPrinter::DevicePixel);
-
-    // Painter settings
-    begin (&m_printer);
-    setViewport (get_rect ());
-    scale (get_scale (), get_scale ());
+    m_printer.setPaperSize (sz.size (), QPrinter::DevicePixel);
   }
 
   ~pdfpainter (void) { }
 
-  QString get_fname (void) const { return m_fname; }
-
-  QRectF get_rectf (void) const { return m_sizef; }
-
-  QRect get_rect (void) const { return m_sizef.toRect (); }
-
-  double get_scale (void) const { return m_dpi / 72.0; }
-
-  void finish (void) { end (); }
+  void render (const QByteArray svg_content)
+  {
+    QSvgRenderer renderer (svg_content);
+    begin (&m_printer);
+    renderer.render (this);
+    end ();
+  }
 
 private:
-  QString m_fname;
-  QRectF m_sizef;
-  double m_dpi;
   QPrinter m_printer;
 };
 
 // String conversion functions
-QVector<double> qstr2vectorf (QString str)
-{
-  QVector<double> pts;
-  QStringList coords = str.split (",");
-  for (QStringList::iterator p = coords.begin (); p != coords.end (); p += 1)
-    {
-      double pt = (*p).toDouble ();
-      pts.append (pt);
-    }
-  return pts;
-}
-
-QVector<double> qstr2vectord (QString str)
-{
-  QVector<double> pts;
-  QStringList coords = str.split (",");
-  for (QStringList::iterator p = coords.begin (); p != coords.end (); p += 1)
-    {
-      double pt = (*p).toDouble ();
-      pts.append (pt);
-    }
-
-  return pts;
-}
 
 QVector<QPointF> qstr2ptsvector (QString str)
 {
@@ -117,33 +85,6 @@
   return pts;
 }
 
-QVector<QPoint> qstr2ptsvectord (QString str)
-{
-  QVector<QPoint> pts;
-  str = str.trimmed ();
-  str.replace (" ", ",");
-  QStringList coords = str.split (",");
-  for (QStringList::iterator p = coords.begin (); p != coords.end (); p += 2)
-    {
-      QPoint pt ((*p).toDouble (), (*(p+1)).toDouble ());
-      pts.append (pt);
-    }
-  return pts;
-}
-
-// Extract field arguments in a style-like string, e.g. "bla field(1,34,56) bla"
-QString get_field (QString str, QString field)
-{
-  QString retval;
-  QRegExp rx (field + "\\(([^\\)]*)\\)");
-  int pos = 0;
-  pos = rx.indexIn (str, pos);
-  if (pos > -1)
-    retval = rx.cap (1);
-
-  return retval;
-}
-
 // Polygon reconstruction class
 class octave_polygon
 {
@@ -332,281 +273,6 @@
   QList<QPolygonF> m_polygons;
 };
 
-void draw (QDomElement& parent_elt, pdfpainter& painter)
-{
-  QDomNodeList nodes = parent_elt.childNodes ();
-
-  static QString clippath_id;
-  static QMap< QString, QVector<QPoint> > clippath;
-
-  // tspan elements must have access to the font and position extracted from
-  // their parent text element
-  static QFont font;
-  static double dx = 0, dy = 0;
-
-  for (int i = 0; i < nodes.count (); i++)
-    {
-      QDomNode node = nodes.at (i);
-      if (! node.isElement ())
-        continue;
-
-      QDomElement elt = node.toElement ();
-
-      if (elt.tagName () == "clipPath")
-        {
-          clippath_id = "#" + elt.attribute ("id");
-          draw (elt, painter);
-          clippath_id = QString ();
-        }
-      else if (elt.tagName () == "g")
-        {
-          bool current_clipstate = painter.hasClipping ();
-          QRegion current_clippath = painter.clipRegion ();
-
-          QString str = elt.attribute ("clip-path");
-          if (! str.isEmpty ())
-            {
-              QVector<QPoint> pts = clippath[get_field (str, "url")];
-              if (! pts.isEmpty ())
-                {
-                  painter.setClipRegion (QRegion (QPolygon (pts)));
-                  painter.setClipping (true);
-                }
-            }
-
-          draw (elt, painter);
-
-          // Restore previous clipping settings
-          painter.setClipRegion (current_clippath);
-          painter.setClipping (current_clipstate);
-        }
-      else if (elt.tagName () == "text")
-        {
-          // Font
-          font = QFont ();
-          QString str = elt.attribute ("font-family");
-          if (! str.isEmpty ())
-            font.setFamily (elt.attribute ("font-family"));
-
-          str = elt.attribute ("font-weight");
-          if (! str.isEmpty () && str != "normal")
-            font.setWeight (QFont::Bold);
-
-          str = elt.attribute ("font-style");
-          if (! str.isEmpty () && str != "normal")
-            font.setStyle (QFont::StyleItalic);
-
-          str = elt.attribute ("font-size");
-          if (! str.isEmpty ())
-            font.setPixelSize (str.toDouble ());
-
-          painter.setFont (font);
-
-          // Translation and rotation
-          painter.save ();
-          str = get_field (elt.attribute ("transform"), "translate");
-          if (! str.isEmpty ())
-            {
-              QStringList trans = str.split (",");
-              dx = trans[0].toDouble ();
-              dy = trans[1].toDouble ();
-
-              str = get_field (elt.attribute ("transform"), "rotate");
-              if (! str.isEmpty ())
-                {
-                  QStringList rot = str.split (",");
-                  painter.translate (dx+rot[1].toDouble (),
-                                     dy+rot[2].toDouble ());
-                  painter.rotate (rot[0].toDouble ());
-                  dx = rot[1].toDouble ();
-                  dy = rot[2].toDouble ();
-                }
-              else
-                {
-                  painter.translate (dx, dy);
-                  dx = 0;
-                  dy = 0;
-                }
-            }
-
-          draw (elt, painter);
-          painter.restore ();
-        }
-      else if (elt.tagName () == "tspan")
-        {
-          // Font
-          QFont saved_font (font);
-
-          QString str = elt.attribute ("font-family");
-          if (! str.isEmpty ())
-            font.setFamily (elt.attribute ("font-family"));
-
-          str = elt.attribute ("font-weight");
-          if (! str.isEmpty ())
-            {
-              if (str != "normal")
-                font.setWeight (QFont::Bold);
-              else
-                font.setWeight (QFont::Normal);
-            }
-
-          str = elt.attribute ("font-style");
-          if (! str.isEmpty ())
-            {
-              if (str != "normal")
-                font.setStyle (QFont::StyleItalic);
-              else
-                font.setStyle (QFont::StyleNormal);
-            }
-
-          str = elt.attribute ("font-size");
-          if (! str.isEmpty ())
-            font.setPixelSize (str.toDouble ());
-
-          painter.setFont (font);
-
-          // Color is specified in rgb
-          str = get_field (elt.attribute ("fill"), "rgb");
-          if (! str.isEmpty ())
-            {
-              QStringList clist = str.split (",");
-              painter.setPen (QColor (clist[0].toInt (), clist[1].toInt (),
-                                      clist[2].toInt ()));
-            }
-
-          QStringList xx = elt.attribute ("x").split (" ");
-          int y = elt.attribute ("y").toInt ();
-          str = elt.text ();
-          if (! str.isEmpty ())
-            {
-              int ii = 0;
-              foreach (QString s,  xx)
-                if (ii < str.size ())
-                  painter.drawText (s.toInt ()-dx, y-dy, str.at (ii++));
-            }
-
-          draw (elt, painter);
-          font = saved_font;
-        }
-      else if (elt.tagName () == "polyline")
-        {
-          // Color
-          QColor c (elt.attribute ("stroke"));
-          QString str = elt.attribute ("stroke-opacity");
-          if (! str.isEmpty () && str.toDouble () != 1.0
-              && str.toDouble () >= 0.0)
-            c.setAlphaF (str.toDouble ());
-
-          QPen pen;
-          pen.setColor (c);
-
-          // Line properties
-          str = elt.attribute ("stroke-width");
-          if (! str.isEmpty ())
-            {
-              double w = str.toDouble () * painter.get_scale ();
-              if (w > 0)
-                pen.setWidthF (w / painter.get_scale ());
-            }
-
-          str = elt.attribute ("stroke-linecap");
-          pen.setCapStyle (Qt::SquareCap);
-          if (str == "round")
-            pen.setCapStyle (Qt::RoundCap);
-          else if (str == "butt")
-            pen.setCapStyle (Qt::FlatCap);
-
-          str = elt.attribute ("stroke-linejoin");
-          pen.setJoinStyle (Qt::MiterJoin);
-          if (str == "round")
-            pen.setJoinStyle (Qt::RoundJoin);
-          else if (str == "bevel")
-            pen.setJoinStyle (Qt::BevelJoin);
-
-          str = elt.attribute ("stroke-dasharray");
-          pen.setStyle (Qt::SolidLine);
-          if (! str.isEmpty ())
-            {
-              QVector<double> pat = qstr2vectord (str);
-              if (pat.count () != 2 || pat[1] != 0)
-                {
-                  // Express pattern in linewidth units
-                  for (auto& p : pat)
-                    p /= pen.widthF ();
-
-                  pen.setDashPattern (pat);
-                }
-            }
-
-          painter.setPen (pen);
-          painter.drawPolyline (qstr2ptsvector (elt.attribute ("points")));
-        }
-      else if (elt.tagName () == "image")
-        {
-          // Images are represented as a base64 stream of png formatted data
-          QString href_att = elt.attribute ("xlink:href");
-          QString prefix ("data:image/png;base64,");
-          QByteArray data
-            = QByteArray::fromBase64 (href_att.mid (prefix.length ()).toLatin1 ());
-          QImage img;
-          if (img.loadFromData (data, "PNG"))
-            {
-              QRect pos(elt.attribute ("x").toInt (),
-                        elt.attribute ("y").toInt (),
-                        elt.attribute ("width").toInt (),
-                        elt.attribute ("height").toInt ());
-
-              // Translate
-              painter.save ();
-              QString str = get_field (elt.attribute ("transform"), "matrix");
-              if (! str.isEmpty ())
-                {
-                  QVector<double> m = qstr2vectorf (str);
-                  double scl = painter.get_scale ();
-                  QTransform tform(m[0]*scl, m[1]*scl, m[2]*scl,
-                                   m[3]*scl, m[4]*scl, m[5]*scl);
-                  painter.setTransform (tform);
-                }
-
-              painter.setRenderHint (QPainter::Antialiasing, false);
-              painter.drawImage (pos, img);
-              painter.setRenderHint (QPainter::Antialiasing, true);
-              painter.restore  ();
-            }
-        }
-      else if (elt.tagName () == "polygon")
-        {
-          if (! clippath_id.isEmpty ())
-            clippath[clippath_id] = qstr2ptsvectord (elt.attribute ("points"));
-          else
-            {
-              QString str = elt.attribute ("fill");
-              if (! str.isEmpty ())
-                {
-                  QColor color (str);
-
-                  str = elt.attribute ("fill-opacity");
-                  if (! str.isEmpty () && str.toDouble () != 1.0
-                      && str.toDouble () >= 0.0)
-                    color.setAlphaF (str.toDouble ());
-
-                  QPolygonF p (qstr2ptsvector (elt.attribute ("points")));
-
-                  if (p.count () > 2)
-                    {
-                      painter.setBrush (color);
-                      painter.setPen (Qt::NoPen);
-
-                      painter.setRenderHint (QPainter::Antialiasing, false);
-                      painter.drawPolygon (p);
-                      painter.setRenderHint (QPainter::Antialiasing, true);
-                    }
-                }
-            }
-        }
-    }
-}
-
 // Append a list of reconstructed child polygons to a QDomElement and remove
 // the original nodes
 
@@ -669,7 +335,7 @@
               if (! str.isEmpty ())
                 {
                   double alpha = str.toDouble ();
-                  if (alpha != 1.0 && str.toDouble () >= 0.0)
+                  if (alpha != 1.0 && alpha >= 0.0)
                     color.setAlphaF (alpha);
                 }
 
@@ -678,7 +344,7 @@
 
               if (color != current_color)
                 {
-                  // Reconstruct the previous series of triangle
+                  // Reconstruct the previous series of triangles
                   QList<QPolygonF> polygons = current_polygon.reconstruct ();
                   collection.push_back (QPair<QList<QDomNode>,QList<QPolygonF> >
                                         (replaced_nodes, polygons));
@@ -716,6 +382,61 @@
     replace_polygons (parent_elt, collection[ii].first, collection[ii].second);
 }
 
+// Split "text" elements into single characters elements
+void split_strings (QDomElement& parent_elt)
+{
+  QDomNodeList nodes = parent_elt.childNodes ();
+
+  for (int ii = 0; ii < nodes.count (); ii++)
+    {
+      QDomNode node = nodes.at (ii);
+
+      if (! node.isElement ())
+        continue;
+
+      QDomElement elt = node.toElement ();
+
+      if (elt.tagName () == "text")
+        {
+          QString str = elt.attribute ("x");
+          if (! str.isEmpty ())
+            {
+              QStringList xx = elt.attribute ("x").split (" ");
+              str = elt.text ();
+
+              if (! str.isEmpty () && xx.count () > 1)
+                {
+                  QDomNode last_node = node;
+
+                  for (int jj = 0; jj < (xx.count ()) && (jj < str.size ());
+                       jj++)
+                    {
+                      if (! last_node.isNull ())
+                        {
+                          QDomNode new_node = node.cloneNode ();
+                          new_node.toElement ().setAttribute ("x", xx.at (jj));
+
+                          QDomNodeList subnodes = new_node.childNodes ();
+
+                          // Change the text node of this element
+                          for (int kk = 0; kk < subnodes.count (); kk++)
+                            if (subnodes.at (kk).isText ())
+                              subnodes.at (kk).toText ().setData (str.at (jj));
+
+                          parent_elt.insertAfter (new_node, last_node);
+                          last_node = new_node;
+                        }
+                    }
+
+                  parent_elt.removeChild (node);
+                }
+            }
+        }
+      else
+        split_strings (elt);
+    }
+}
+
 int main(int argc, char *argv[])
 {
   const char *doc = "See \"octave-svgconvert -h\"";
@@ -771,8 +492,7 @@
   if (! document.setContent (&file, false, &msg))
     {
       std::cerr << "Failed to parse XML contents" << std::endl
-                << msg.toStdString ();
-      std::cerr << doc;
+                << msg.toStdString () << std::endl;
       file.close();
       return -1;
     }
@@ -840,11 +560,18 @@
   // Draw
   if (! strcmp (argv[2], "pdf"))
     {
+      // Remove clippath which is not supported in svg-tiny
+      QDomNodeList lst = root.elementsByTagName ("clipPath");
+      for (int ii = lst.count (); ii > 0; ii--)
+        lst.at (ii-1).parentNode ().removeChild (lst.at (ii-1));
+
+      // Split text strings into single characters with single x coordinates
+      split_strings (root);
+
       // PDF painter
       pdfpainter painter (fout.fileName (), vp, dpi);
 
-      draw (root, painter);
-      painter.finish ();
+      painter.render (document.toByteArray ());
     }
   else
     {