Mercurial > octave
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 {