ILIAS  release_8 Revision v8.19
All Data Structures Namespaces Files Functions Variables Modules Pages
class.ilMathJax.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
21 
29 class ilMathJax
30 {
31  public const PURPOSE_BROWSER = 'browser'; // direct display of page in the browser
32  public const PURPOSE_EXPORT = 'export'; // html export of contents
33  public const PURPOSE_PDF = 'pdf'; // server-side PDF generation (only TCPDF and XSL-FO, not PhantomJS!!!)
34  public const PURPOSE_DEFERRED_PDF = 'deferred_pdf'; // defer rendering for server-side pdf generation (XSL-FO)
35  // this needs a second call with PURPOSE_PDF at the end
36 
37  public const ENGINE_SERVER = 'server'; // code is treated by one of the rendering modes below
38  public const ENGINE_CLIENT = 'client'; // code delimiters are
39  public const ENGINE_DEFERRED = 'deferred'; // protect code for a deferred rendering
40  public const ENGINE_NONE = 'none'; // don't render the code, just show it
41 
42  public const RENDER_SVG_AS_XML_EMBED = 'svg_as_xml_embed'; // embed svg code directly in html (default for browser view)
43  public const RENDER_SVG_AS_IMG_EMBED = 'svg_as_img_embed'; // embed svg base64 encoded in an img tag (default for HTML export)
44  public const RENDER_PNG_AS_IMG_EMBED = 'png_as_img_embed'; // embed png base64 encoded in an img tag (default for PDF generation)
45  public const RENDER_PNG_AS_FO_FILE = 'png_as_fo_file'; // refer to a png file from an fo tag (for PDF generation with XSL-FO)
46 
47  protected const OUTPUT_SVG = 'svg'; // svg output format for server side rendering
48  protected const OUTPUT_PNG = 'png'; // png output format for server side rendering
49 
50  protected const DEFAULT_DPI = 150; // default DIP of rendered images
51  protected const DEFAULT_ZOOM = 1.0; // default zoom factor of included images
52 
56  protected static ?self $_instance;
57 
62 
67 
71  protected ?string $engine;
72 
76  protected string $rendering = self::RENDER_SVG_AS_XML_EMBED;
77 
81  protected string $output = self::OUTPUT_SVG;
82 
86  protected int $dpi;
87 
91  protected float $zoom_factor;
92 
96  protected array $default_server_options = array(
97  "format" => "TeX",
98  "math" => '', // TeX code
99  "svg" => true,
100  "mml" => false,
101  "png" => false,
102  "speakText" => false,
103  "speakRuleset" => "mathspeak",
104  "speakStyle" => "default",
105  "ex" => 6,
106  "width" => 1000000,
107  "linebreaks" => false,
108  );
109 
113  protected function __construct(ilMathJaxConfig $config, ilMathJaxFactory $factory)
114  {
115  $this->config = $config;
116  $this->factory = $factory;
117  $this->init(self::PURPOSE_BROWSER);
118  }
119 
123  public static function getInstance(): ilMathJax
124  {
125  if (!isset(self::$_instance)) {
126  // #37803: here we can't use ilSettingsFactory because of race conditions in ilSettingsFactory::settingsFor()
127  $repo = new ilMathJaxConfigSettingsRepository(new ilSetting('MathJax'));
128  self::$_instance = new self($repo->getConfig(), new ilMathJaxFactory());
129  }
130  return self::$_instance;
131  }
132 
138  public static function getIndependent(ilMathJaxConfig $config, ilMathJaxFactory $factory): ilMathJax
139  {
140  return new self($config, $factory);
141  }
142 
147  public function init(string $a_purpose = self::PURPOSE_BROWSER): ilMathJax
148  {
149  // reset the class variables
150  $this->engine = null;
151  $this->rendering = self::RENDER_SVG_AS_XML_EMBED;
152  $this->output = self::OUTPUT_SVG;
153  $this->dpi = self::DEFAULT_DPI;
154  $this->zoom_factor = self::DEFAULT_ZOOM;
155 
156  // try the server-side rendering first, set this engine, if possible
157  if ($this->config->isServerEnabled()) {
158  if ($a_purpose === self::PURPOSE_BROWSER && $this->config->isServerForBrowser()) {
159  // delivering svg directly in page may be faster than loading image files
160  $this->setEngine(self::ENGINE_SERVER);
161  $this->setRendering(self::RENDER_SVG_AS_XML_EMBED);
162  } elseif ($a_purpose === self::PURPOSE_EXPORT && $this->config->isServerForExport()) {
163  // offline pages must always embed the svg as image tags
164  // otherwise the html base tag may conflict with references in svg
165  $this->setEngine(self::ENGINE_SERVER);
166  $this->setRendering(self::RENDER_SVG_AS_IMG_EMBED);
167  } elseif ($a_purpose === self::PURPOSE_PDF && $this->config->isServerForPdf()) {
168  // embedded png should work in most pdf engines
169  // details can be set by the rendering engine
170  $this->setEngine(self::ENGINE_SERVER);
171  $this->setRendering(self::RENDER_PNG_AS_IMG_EMBED);
172  } elseif ($a_purpose === self::PURPOSE_DEFERRED_PDF && $this->config->isServerForPdf()) {
173  // final engine and rendering is set before the pdf is created
174  $this->setEngine(self::ENGINE_DEFERRED);
175  }
176  }
177 
178  // support client-side rendering if enabled
179  if ($this->config->isClientEnabled()) {
180 
181  // included mathjax script may render code which is not found by the server-side rendering
182  // see https://docu.ilias.de/goto_docu_wiki_wpage_5614_1357.html
183  $this->includeMathJax();
184 
185  // set engine for client-side rendering, if server is not used for the purpose
186  if (!isset($this->engine)) {
187  $this->setEngine(self::ENGINE_CLIENT);
188  }
189  }
190 
191  // no engine available or configured
192  if (!isset($this->engine)) {
193  $this->engine = self::ENGINE_NONE;
194  }
195 
196  return $this;
197  }
198 
202  protected function setEngine(string $a_engine): ilMathJax
203  {
204  switch ($a_engine) {
205  case self::ENGINE_CLIENT:
206  case self::ENGINE_SERVER:
207  case self::ENGINE_DEFERRED:
208  $this->engine = $a_engine;
209  break;
210  default:
211  $this->engine = self::ENGINE_NONE;
212  }
213 
214  return $this;
215  }
216 
220  public function setRendering(string $a_rendering): ilMathJax
221  {
222  switch ($a_rendering) {
223  case self::RENDER_SVG_AS_XML_EMBED:
224  case self::RENDER_SVG_AS_IMG_EMBED:
225  $this->rendering = $a_rendering;
226  $this->output = self::OUTPUT_SVG;
227  break;
228 
229  case self::RENDER_PNG_AS_IMG_EMBED:
230  case self::RENDER_PNG_AS_FO_FILE:
231  $this->rendering = $a_rendering;
232  $this->output = self::OUTPUT_PNG;
233  break;
234  }
235  return $this;
236  }
237 
241  public function setDpi(int $a_dpi): ilMathJax
242  {
243  $this->dpi = $a_dpi;
244  return $this;
245  }
246 
250  public function setZoomFactor(float $a_factor): ilMathJax
251  {
252  $this->zoom_factor = $a_factor;
253  return $this;
254  }
255 
259  public function includeMathJax(ilGlobalTemplateInterface $a_tpl = null): ilMathJax
260  {
261  if ($this->config->isClientEnabled()) {
262  $tpl = $a_tpl ?? $this->factory->template();
263 
264  if (!empty($this->config->getClintPolyfillUrl())) {
265  $tpl->addJavaScript($this->config->getClintPolyfillUrl());
266  }
267  if (!empty($this->config->getClientScriptUrl())) {
268  $tpl->addJavaScript($this->config->getClientScriptUrl());
269  }
270  }
271 
272  return $this;
273  }
274 
284  public function insertLatexImages(string $a_text, ?string $a_start = '[tex]', ?string $a_end = '[/tex]'): string
285  {
286  // don't change anything if mathjax is not configured
287  if ($this->engine === self::ENGINE_NONE) {
288  return $a_text;
289  }
290 
291  // this is a fix for bug5362
292  $a_start = str_replace("\\", "", $a_start ?? '[tex]');
293  $a_end = str_replace("\\", "", $a_end ?? '[/tex]');
294 
295  // current position to start the search for delimiters
296  $cpos = 0;
297  // find position of start delimiter
298  while (is_int($spos = ilStr::strIPos($a_text, $a_start, $cpos))) {
299  // find position of end delimiter
300  if (is_int($epos = ilStr::strIPos($a_text, $a_end, $spos + ilStr::strLen($a_start)))) {
301  // extract the tex code inside the delimiters
302  $tex = ilStr::subStr($a_text, $spos + ilStr::strLen($a_start), $epos - $spos - ilStr::strLen($a_start));
303 
304  // undo a code protection done by the deferred engine before
305  if (ilStr::subStr($tex, 0, 7) === 'base64:') {
306  $tex = base64_decode(substr($tex, 7));
307  }
308 
309  // omit the html newlines added by the ILIAS page editor
310  $tex = str_replace(array('<br>', '<br/>', '<br />'), '', $tex);
311 
312  // tex specific replacements
313  $tex = preg_replace("/\\\\([RZN])([^a-zA-Z])/", "\\mathbb{" . "$1" . "}" . "$2", $tex);
314 
315  // check, if tags go across div borders
316  if (is_int(ilStr::strIPos($tex, '<div>')) || is_int(ilStr::strIPos($tex, '</div>'))) {
317  // keep the original code including delimiters, continue search behind
318  $cpos = $epos + ilStr::strLen($a_end);
319  } else {
320  switch ($this->engine) {
321  case self::ENGINE_CLIENT:
322  // prepare code for processing in the browser
323  // add necessary html encodings
324  // use the configured mathjax delimiters
325  $tex = str_replace('<', '&lt;', $tex);
326  $replacement = $this->config->getClientLimiterStart() . $tex
327  . $this->config->getClientLimiterEnd();
328  break;
329 
330  case self::ENGINE_SERVER:
331  // apply server-side processing
332  // mathjax-node expects pure tex code
333  // so revert any applied html encoding
334  $tex = html_entity_decode($tex, ENT_QUOTES, 'UTF-8');
335  $replacement = $this->renderMathJax($tex);
336  break;
337 
338  case self::ENGINE_DEFERRED:
339  // protect code to save it for post production
340  $replacement = '[tex]' . 'base64:' . base64_encode($tex) . '[/tex]';
341  break;
342 
343  default:
344  // keep the original
345  $replacement = $tex;
346  break;
347  }
348 
349  // replace delimiters and tex code with prepared code or generated image
350  $a_text = ilStr::subStr($a_text, 0, $spos) . $replacement
351  . ilStr::subStr($a_text, $epos + ilStr::strLen($a_end));
352 
353  // continue search behind replacement
354  $cpos = $spos + ilStr::strLen($replacement);
355  }
356  } else {
357  // end delimiter position not found => stop search
358  break;
359  }
360 
361  if ($cpos >= ilStr::strlen($a_text)) {
362  // current position at the end => stop search
363  break;
364  }
365  }
366  return $a_text;
367  }
368 
372  protected function renderMathJax(string $a_tex): string
373  {
375  $options['math'] = $a_tex;
376  $options['dpi'] = $this->dpi;
377 
378  switch ($this->output) {
379  case self::OUTPUT_PNG:
380  $options['svg'] = false;
381  $options['png'] = true;
382  $suffix = ".png";
383  break;
384 
385  case self::OUTPUT_SVG:
386  default:
387  $options['svg'] = true;
388  $options['png'] = false;
389  $suffix = ".svg";
390  break;
391  }
392 
393  $image = $this->factory->image($a_tex, $this->output, $this->dpi);
394 
395  try {
396  if (!$image->exists()) {
397  $server = $this->factory->server($this->config);
398  $image->write($server->call($options));
399  }
400 
401  // get the image properties
402  switch ($this->output) {
403  case self::OUTPUT_PNG:
404  [$width, $height] = getimagesize($image->absolutePath());
405  $width = round($width * $this->zoom_factor);
406  $height = round($height * $this->zoom_factor);
407  $mime = 'image/png';
408  break;
409 
410  case self::OUTPUT_SVG:
411  default:
412  $svg = simplexml_load_string(file_get_contents($image->absolutePath()));
413  $width = round($svg['width'] * $this->zoom_factor);
414  $height = round($svg['height'] * $this->zoom_factor);
415  $mime = 'image/svg+xml';
416  break;
417  }
418 
419  // generate the html code
420  switch ($this->rendering) {
421  case self::RENDER_SVG_AS_XML_EMBED:
422  $html = $image->read();
423  break;
424 
425  case self::RENDER_SVG_AS_IMG_EMBED:
426  case self::RENDER_PNG_AS_IMG_EMBED:
427  $html = '<img src="data:' . $mime . ';base64,'
428  . base64_encode($image->read())
429  . '" style="width:' . $width . '; height:' . $height . ';" />';
430  break;
431 
432  case self::RENDER_PNG_AS_FO_FILE:
433  $html = '<fo:external-graphic src="' . $image->absolutePath() . '"'
434  . ' content-height="' . $height . 'px" content-width="' . $width . 'px"></fo:external-graphic>';
435  break;
436 
437  default:
438  $html = htmlspecialchars($a_tex);
439  break;
440  }
441  return $html;
442  } catch (Exception $e) {
443  return "[TeX rendering failed: " . $e->getMessage() . htmlentities($a_tex) . "]";
444  }
445  }
446 
450  public function getCacheSize(): string
451  {
452  return $this->factory->image('', $this->output, $this->dpi)->getCacheSize();
453  }
454 
458  public function clearCache(): void
459  {
460  $image = $this->factory->image('', $this->output, $this->dpi);
461  $image->clearCache();
462  }
463 }
const DEFAULT_ZOOM
includeMathJax(ilGlobalTemplateInterface $a_tpl=null)
Include the Mathjax javascript(s) in the page template.
ilMathJaxConfig $config
Class for processing of latex formulas This class uses a sigleton pattern to store the rendering purp...
const RENDER_PNG_AS_IMG_EMBED
insertLatexImages(string $a_text, ?string $a_start='[tex]', ?string $a_end='[/tex]')
Replace all tex code within given start and end delimiters in a text If client-side rendering is enab...
string $rendering
const ENGINE_SERVER
Factory for objects used by ilMathJax.
string $output
const PURPOSE_PDF
float $zoom_factor
Global Mathjax configuration.
static subStr(string $a_str, int $a_start, ?int $a_length=null)
Definition: class.ilStr.php:24
ilMathJaxFactory $factory
renderMathJax(string $a_tex)
Render image from tex code using the MathJax server.
array $default_server_options
static strLen(string $a_string)
Definition: class.ilStr.php:63
const OUTPUT_PNG
const PURPOSE_BROWSER
__construct(ilMathJaxConfig $config, ilMathJaxFactory $factory)
Protected constructor to force the use of an initialized instance.
const OUTPUT_SVG
static self $_instance
const ENGINE_CLIENT
getCacheSize()
Get the size of the image cache.
const DEFAULT_DPI
string $engine
const PURPOSE_EXPORT
const RENDER_PNG_AS_FO_FILE
init(string $a_purpose=self::PURPOSE_BROWSER)
Initialize the usage for a certain purpose This must be done before any rendering call...
$server
clearCache()
Clear the cache of rendered graphics.
static getIndependent(ilMathJaxConfig $config, ilMathJaxFactory $factory)
Get an independent instance with a specific config for use in unit tests or on the mathjax settings p...
static getInstance()
Singleton: get instance for use in ILIAS requests with a config loaded from the settings.
setDpi(int $a_dpi)
Set the dpi of the rendered images.
setEngine(string $a_engine)
Set the Rendering engine.
const RENDER_SVG_AS_IMG_EMBED
const ENGINE_DEFERRED
if($DIC->http() ->request() ->getMethod()=="GET" &&isset($DIC->http() ->request() ->getQueryParams()['tex'])) $tpl
Definition: latex.php:41
static strIPos(string $a_haystack, string $a_needle, ?int $a_offset=null)
Definition: class.ilStr.php:54
setRendering(string $a_rendering)
Set the image type rendered by the server.
setZoomFactor(float $a_factor)
Set the zoom factor of the rendered images.
const RENDER_SVG_AS_XML_EMBED
const PURPOSE_DEFERRED_PDF
Repository for storing and loading the MathJax configuration.
const ENGINE_NONE