Mercurial > octave
diff libinterp/corefcn/latex-text-renderer.cc @ 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 | |
children | af41ebf3d1b3 |
line wrap: on
line diff
--- /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; + } +}