GNU Octave  9.1.0
A high-level interpreted language, primarily intended for numerical computations, mostly compatible with Matlab
latex-text-renderer.cc
Go to the documentation of this file.
1 ////////////////////////////////////////////////////////////////////////
2 //
3 // Copyright (C) 2021-2024 The Octave Project Developers
4 //
5 // See the file COPYRIGHT.md in the top-level directory of this
6 // distribution or <https://octave.org/copyright/>.
7 //
8 // This file is part of Octave.
9 //
10 // Octave is free software: you can redistribute it and/or modify it
11 // under the terms of the GNU General Public License as published by
12 // the Free Software Foundation, either version 3 of the License, or
13 // (at your option) any later version.
14 //
15 // Octave is distributed in the hope that it will be useful, but
16 // WITHOUT ANY WARRANTY; without even the implied warranty of
17 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 // GNU General Public License for more details.
19 //
20 // You should have received a copy of the GNU General Public License
21 // along with Octave; see the file COPYING. If not, see
22 // <https://www.gnu.org/licenses/>.
23 //
24 ////////////////////////////////////////////////////////////////////////
25 
26 #ifdef HAVE_CONFIG_H
27 # include "config.h"
28 #endif
29 
30 #include <iostream>
31 #include <fstream>
32 
33 #include "base-text-renderer.h"
34 #include "builtin-defun-decls.h"
35 #include "dim-vector.h"
36 #include "error.h"
37 #include "graphics.h"
38 #include "file-ops.h"
39 #include "interpreter.h"
40 #include "interpreter-private.h"
41 #include "oct-env.h"
42 #include "oct-process.h"
43 
45 
46 std::string
47 quote_string (std::string str)
48 {
49  return ('"' + str + '"');
50 }
51 
52 class
53 OCTINTERP_API
54 latex_renderer : public base_text_renderer
55 {
56 public:
57 
58  latex_renderer ()
59  : m_fontsize (10.0), m_fontname ("cmr"), m_tmp_dir (),
60  m_color (dim_vector (1, 3), 0), m_latex_binary ("latex"),
61  m_dvipng_binary ("dvipng"), m_dvisvg_binary ("dvisvgm"),
62  m_debug (false), m_testing (true)
63  {
64  std::string bin = sys::env::getenv ("OCTAVE_LATEX_BINARY");
65  if (! bin.empty ())
66  m_latex_binary = quote_string (bin);
67 
68  bin = sys::env::getenv ("OCTAVE_DVIPNG_BINARY");
69  if (! bin.empty ())
70  m_dvipng_binary = quote_string (bin);
71 
72  bin = sys::env::getenv ("OCTAVE_DVISVG_BINARY");
73  if (! bin.empty ())
74  m_dvisvg_binary = quote_string (bin);
75 
76  m_debug = ! sys::env::getenv ("OCTAVE_LATEX_DEBUG_FLAG").empty ();
77  }
78 
79  OCTAVE_DISABLE_COPY_MOVE (latex_renderer)
80 
81  ~latex_renderer ()
82  {
83  if (! m_tmp_dir.empty () && ! m_debug)
84  sys::recursive_rmdir (m_tmp_dir);
85  }
86 
87  void set_font (const std::string& /*name*/, const std::string& /*weight*/,
88  const std::string& /*angle*/, double size)
89  {
90  m_fontsize = size;
91  }
92 
93  void set_color (const Matrix& c)
94  {
95  if (c.numel () == 3)
96  {
97  m_color(0) = static_cast<uint8_t> (c (0) * 255);
98  m_color(1) = static_cast<uint8_t> (c (1) * 255);
99  m_color(2) = static_cast<uint8_t> (c (2) * 255);
100  }
101  }
102 
103  Matrix get_extent (text_element * /*elt*/, double /*rotation*/)
104  {
105  return Matrix (1, 2, 0.0);
106  }
107 
108  Matrix get_extent (const std::string& txt, double rotation,
109  const caseless_str& interpreter)
110  {
111  Matrix bbox;
112  uint8NDArray pixels;
113 
114  text_to_pixels (txt, pixels, bbox, 0, 0, rotation, interpreter, false);
115 
116  return bbox.extract_n (0, 2, 1, 2);
117  }
118 
119  void text_to_strlist (const std::string& txt,
120  std::list<text_renderer::string>& lst,
121  Matrix& bbox, int halign, int valign, double rotation,
122  const caseless_str& interp)
123  {
124  uint8NDArray pixels;
125  text_to_pixels (txt, pixels, bbox, halign, valign, rotation,
126  interp, false);
127 
129  text_renderer::string str ("", fnt, 0.0, 0.0);
130  str.set_color (m_color);
131 
133 
134  gh_manager::latex_data ldata = gh_mgr.get_latex_data (key (txt, halign));
135 
136  str.set_svg_element (ldata.second);
137 
138  lst.push_back (str);
139  }
140 
141  void text_to_pixels (const std::string& txt, uint8NDArray& pxls,
142  Matrix& bbox, int halign, int valign, double rotation,
143  const caseless_str& interpreter,
144  bool handle_rotation);
145 
146  void set_anti_aliasing (bool /*val*/) { }
147 
148  octave_map get_system_fonts () { return octave_map (); }
149 
150  bool ok ();
151 
152 private:
153 
154  std::string key (const std::string& txt, int halign)
155  {
156  return (txt + ":"
157  + std::to_string (m_fontsize) + ":"
158  + std::to_string (halign) + ":"
159  + std::to_string (m_color(0)) + ":"
160  + std::to_string (m_color(1)) + ":"
161  + std::to_string (m_color(2)));
162  }
163 
164  void warn_helper (std::string caller, std::string txt, std::string cmd,
165  process_execution_result result);
166 
167  uint8NDArray render (const std::string& txt, int halign = 0);
168 
169  bool read_image (const std::string& png_file, uint8NDArray& data) const;
170 
171  std::string write_tex_file (const std::string& txt, int halign);
172 
173 private:
174  double m_fontsize;
175  std::string m_fontname;
176  std::string m_tmp_dir;
177  uint8NDArray m_color;
178  std::string m_latex_binary;
179  std::string m_dvipng_binary;
180  std::string m_dvisvg_binary;
181  bool m_debug;
182  bool m_testing;
183 
184 };
185 
186 bool
187 latex_renderer::ok ()
188 {
189  // Only run the test once in a session
190  static bool tested = false;
191 
192  static bool isok = false;
193 
194  if (! tested)
195  {
196  tested = true;
197 
198  // For testing, render a questoin mark
199  uint8NDArray pixels = render ("?");
200 
201  if (! pixels.isempty ())
202  isok = true;
203  else
204  warning_with_id ("Octave:LaTeX:internal-error",
205  "latex_renderer: a run-time test failed and the 'latex' interpreter has been disabled.");
206  }
207 
208  m_testing = false;
209 
210  return isok;
211 }
212 
213 std::string
214 latex_renderer::write_tex_file (const std::string& txt, int halign)
215 {
216  if (m_tmp_dir.empty ())
217  {
218  //Create the temporary directory
219 #if defined (OCTAVE_USE_WINDOWS_API)
220  static std::string base_tmp_dir;
221 
222  if (base_tmp_dir.empty ())
223  {
224  base_tmp_dir = sys::env::get_temp_directory ();
225 
226  // Make sure we don't get short 8.3 path on Windows since some
227  // versions of latex on that platform don't support them
228  // (see bug #62779)
229  if (base_tmp_dir.find ('~') != std::string::npos)
230  base_tmp_dir = sys::canonicalize_file_name (base_tmp_dir);
231  }
232 
233  m_tmp_dir = sys::tempnam (base_tmp_dir, "latex");
234 #else
235  m_tmp_dir = sys::tempnam ("", "latex");
236 #endif
237 
238  if (sys::mkdir (m_tmp_dir, 0700) != 0)
239  {
240  warning_with_id ("Octave:LaTeX:internal-error",
241  "latex_renderer: unable to create temp directory");
242  return std::string ();
243  }
244  }
245 
246  std::string base_file_name
247  = sys::file_ops::concat (m_tmp_dir, "default");
248 
249  // Duplicate \n characters and align multi-line strings based on
250  // horizontalalignment
251  std::string latex_txt (txt);
252  std::size_t pos = 0;
253 
254  while (true)
255  {
256  pos = txt.find_first_of ("\n", pos);
257 
258  if (pos == std::string::npos)
259  break;
260 
261  latex_txt.replace (pos, 1, "\n\n");
262 
263  pos += 1;
264  }
265 
266  std::string env ("flushleft");
267  if (halign == 1)
268  env = "center";
269  else if (halign == 2)
270  env = "flushright";
271 
272  latex_txt = std::string ("\\begin{" ) + env + "}\n"
273  + latex_txt + "\n"
274  + "\\end{" + env + "}\n";
275 
276  // Write to temporary .tex file
277  std::ofstream file;
278  file.open (base_file_name + ".tex");
279  file << "\\documentclass[10pt, varwidth]{standalone}\n"
280  << "\\usepackage{amsmath}\n"
281  << "\\usepackage[utf8]{inputenc}\n"
282  << "\\begin{document}\n"
283  << latex_txt << "\n"
284  << "\\end{document}";
285  file.close ();
286 
287  return base_file_name;
288 }
289 
290 bool
291 latex_renderer::read_image (const std::string& png_file,
292  uint8NDArray& data) const
293 {
294  uint8NDArray alpha;
295  uint8NDArray rgb;
296  int height;
297  int width;
298 
299  try
300  {
301  // First get the image size to build the argument to __magick_read__
302  octave_value_list retval = F__magick_ping__ (ovl (png_file), 1);
303 
304  octave_scalar_map info
305  = retval(0).xscalar_map_value ("latex_renderer::read_image: "
306  "Wrong type for info");
307  height = info.getfield ("rows").int_value ();
308  width = info.getfield ("columns").int_value ();
309  Cell region (dim_vector(1, 2));
310  region(0) = range<double> (1.0, height);
311  region(1) = range<double> (1.0, width);
312  info.setfield ("region", region);
313  info.setfield ("index", octave_value (1));
314 
315  // Retrieve the alpha map
316  retval = F__magick_read__ (ovl (png_file, info), 3);
317 
318  alpha = retval(2).xuint8_array_value ("latex_renderer::read_image: "
319  "Wrong type for alpha");
320  }
321  catch (const execution_exception& ee)
322  {
323  warning_with_id ("Octave:LaTeX:internal-error",
324  "latex_renderer:: failed to read png data. %s",
325  ee.message ().c_str ());
326 
327  interpreter& interp = __get_interpreter__ ();
328 
329  interp.recover_from_exception ();
330 
331  return false;
332  }
333 
334  data = uint8NDArray (dim_vector (4, width, height),
335  static_cast<uint8_t> (0));
336 
337  for (int i = 0; i < height; i++)
338  {
339  for (int j = 0; j < width; j++)
340  {
341  data(0, j, i) = m_color(0);
342  data(1, j, i) = m_color(1);
343  data(2, j, i) = m_color(2);
344  data(3, j, i) = alpha(height-i-1, j);
345  }
346  }
347 
348  return true;
349 }
350 
351 void
352 latex_renderer::warn_helper (std::string caller, std::string txt,
353  std::string cmd, process_execution_result result)
354 {
355  if (m_testing && ! m_debug)
356  return;
357 
358  if (! m_debug)
359  warning_with_id ("Octave:LaTeX:internal-error",
360  "latex_renderer: unable to compile \"%s\"",
361  txt.c_str ());
362  else
363  warning_with_id ("Octave:LaTeX:internal-error",
364  "latex_renderer: %s failed for string \"%s\"\n\
365 * Command:\n\t%s\n\n* Error:\n%s\n\n* Stdout:\n%s",
366  caller.c_str (), txt.c_str (), cmd.c_str (),
367  result.err_msg ().c_str (),
368  result.stdout_output ().c_str ());
369 }
370 
372 latex_renderer::render (const std::string& txt, int halign)
373 {
374  // Render if it was not already done
376 
377  gh_manager::latex_data ldata = gh_mgr.get_latex_data (key (txt, halign));
378 
379  if (! ldata.first.isempty ())
380  return ldata.first;
381 
382  uint8NDArray data;
383 
384  // First write the base .tex file
385  std::string base_file_name = write_tex_file (txt, halign);
386 
387  if (base_file_name.empty ())
388  return data;
389 
390  // Generate DVI file
391  std::string tex_file = quote_string (base_file_name + ".tex");
392  std::string dvi_file = quote_string (base_file_name + ".dvi");
393  std::string log_file = quote_string (base_file_name + ".log");
394 
396  std::string cmd = (m_latex_binary + " -interaction=nonstopmode "
397  + "-output-directory=" + quote_string (m_tmp_dir) + " "
398  + tex_file);
399 
400 #if defined (OCTAVE_USE_WINDOWS_API)
401  cmd = quote_string (cmd);
402 #endif
403 
404  result = run_command_and_return_output (cmd);
405 
406  if (result.exit_status () != 0)
407  {
408  warn_helper ("latex", txt, cmd, result);
409 
410  if (txt != "?")
411  {
412  write_tex_file ("?", halign);
413 
414  result = run_command_and_return_output (cmd);
415  if (result.exit_status () != 0)
416  return data;
417  }
418  else
419  return data;
420  }
421 
422  double size_factor = m_fontsize / 10.0;
423 
424 
425  // Convert DVI to SVG, read file and store its content for later use in
426  // gl2ps_print
427  std::string svg_file = base_file_name + ".svg";
428 
429  cmd = (m_dvisvg_binary + " -n "
430  + "-TS" + std::to_string (size_factor) + " "
431  + "-v1 -o " + quote_string (svg_file) + " "
432  + dvi_file);
433 
434 #if defined (OCTAVE_USE_WINDOWS_API)
435  cmd = quote_string (cmd);
436 #endif
437 
438  result = run_command_and_return_output (cmd);
439 
440  if (result.exit_status () != 0)
441  {
442  warn_helper ("dvisvg", txt, cmd, result);
443  return data;
444  }
445 
446  std::ifstream svg_stream (svg_file);
447  std::string svg_string;
448  svg_string.assign (std::istreambuf_iterator<char> (svg_stream),
449  std::istreambuf_iterator<char> ());
450 
451  // Convert DVI to PNG, read file and format pixel data for later use in
452  // OpenGL
453  std::string png_file = base_file_name + ".png";
454 
455  cmd = (m_dvipng_binary + " " + dvi_file + " "
456  + "-q -o " + quote_string (png_file) + " "
457  + "-bg Transparent -D "
458  + std::to_string (std::floor (72.0 * size_factor)));
459 
460 #if defined (OCTAVE_USE_WINDOWS_API)
461  cmd = quote_string (cmd);
462 #endif
463 
464  result = run_command_and_return_output (cmd);
465 
466  if (result.exit_status () != 0)
467  {
468  warn_helper ("dvipng", txt, cmd, result);
469  return data;
470  }
471 
472  if (! read_image (png_file, data))
473  return data;
474 
475  // Cache pixel and svg data for this string
476  ldata.first = data;
477  ldata.second = svg_string;
478 
479  gh_mgr.set_latex_data (key (txt, halign), ldata);
480 
481  if (m_debug)
482  std::cout << "* Caching " << key (txt, halign) << std::endl;
483 
484  return data;
485 }
486 
487 void
488 latex_renderer::text_to_pixels (const std::string& txt, uint8NDArray& pixels,
489  Matrix& bbox, int halign, int valign,
490  double rotation,
491  const caseless_str& /*interpreter*/,
492  bool handle_rotation)
493 {
494  // Return early for empty strings
495  if (txt.empty ())
496  {
497  bbox = Matrix (1, 4, 0.0);
498  return;
499  }
500 
501  if (ok ())
502  pixels = render (txt, halign);
503  else
504  pixels = uint8NDArray (dim_vector (4, 1, 1), static_cast<uint8_t> (0));
505 
506  if (pixels.ndims () < 3 || pixels.isempty ())
507  return; // nothing to render
508 
509  // Store unrotated bbox size
510  bbox = Matrix (1, 4, 0.0);
511  bbox (2) = pixels.dim2 ();
512  bbox (3) = pixels.dim3 ();
513 
514  // Now rotate pixels if necessary
515  int rot_mode = rotation_to_mode (rotation);
516 
517  if (! pixels.isempty ())
518  rotate_pixels (pixels, rot_mode);
519 
520  // Move X0 and Y0 depending on alignments and eventually swap values
521  // for text rotated 90° 180° or 270°
522  fix_bbox_anchor (bbox, halign, valign, rot_mode, handle_rotation);
523 }
524 
527 {
528  latex_renderer *renderer = new latex_renderer ();
529 
530  return renderer;
531 }
532 
533 OCTAVE_END_NAMESPACE(octave)
ComplexNDArray concat(NDArray &ra, ComplexNDArray &rb, const Array< octave_idx_type > &ra_idx)
Definition: CNDArray.cc:418
octave_value_list F__magick_ping__(const octave_value_list &=octave_value_list(), int=0)
octave_value_list F__magick_read__(const octave_value_list &=octave_value_list(), int=0)
octave_idx_type dim3() const
Size of the specified dimension.
Definition: Array.h:479
int ndims() const
Size of the specified dimension.
Definition: Array.h:671
octave_idx_type dim2() const
Definition: Array.h:467
bool isempty() const
Size of the specified dimension.
Definition: Array.h:651
octave_idx_type numel() const
Number of elements in the array.
Definition: Array.h:414
Definition: Cell.h:43
Definition: dMatrix.h:42
Matrix extract_n(octave_idx_type r1, octave_idx_type c1, octave_idx_type nr, octave_idx_type nc) const
Definition: dMatrix.cc:407
Vector representing the dimensions (size) of an Array.
Definition: dim-vector.h:94
Definition: oct-env.h:40
latex_data get_latex_data(const std::string &key) const
Definition: gh-manager.h:212
std::pair< uint8NDArray, std::string > latex_data
Definition: gh-manager.h:40
void set_latex_data(const std::string &key, latex_data val)
Definition: gh-manager.h:224
void recover_from_exception()
void setfield(const std::string &key, const octave_value &val)
Definition: oct-map.cc:190
octave_value getfield(const std::string &key) const
Definition: oct-map.cc:183
int int_value(bool req_int=false, bool frc_str_conv=false) const
Definition: ov.h:812
std::string stdout_output() const
Definition: oct-process.h:65
std::string err_msg() const
Definition: oct-process.h:63
OCTAVE_BEGIN_NAMESPACE(octave) static octave_value daspk_fcn
void warning_with_id(const char *id, const char *fmt,...)
Definition: error.cc:1078
gh_manager & __get_gh_manager__()
interpreter & __get_interpreter__()
base_text_renderer * make_latex_text_renderer()
std::complex< T > floor(const std::complex< T > &x)
Definition: lo-mappers.h:130
std::ofstream ofstream(const std::string &filename, const std::ios::openmode mode)
Definition: lo-sysdep.cc:635
std::ifstream ifstream(const std::string &filename, const std::ios::openmode mode)
Definition: lo-sysdep.cc:621
std::string tempnam(const std::string &dir, const std::string &pfx)
Definition: file-ops.cc:749
int mkdir(const std::string &nm, mode_t md)
Definition: file-ops.cc:413
std::string canonicalize_file_name(const std::string &name)
Definition: file-ops.cc:798
int recursive_rmdir(const std::string &name)
Definition: file-ops.cc:655
std::string quote_string(std::string str)
process_execution_result run_command_and_return_output(const std::string &cmd_str)
Definition: oct-process.cc:51
octave_value_list ovl(const OV_Args &... args)
Construct an octave_value_list with less typing.
Definition: ovl.h:219
intNDArray< octave_uint8 > uint8NDArray
Definition: uint8NDArray.h:36