ILIAS  trunk Revision v11.0_alpha-1723-g8e69f309bab
All Data Structures Namespaces Files Functions Variables Enumerations Enumerator 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  // included mathjax script may render code which is not found by the server-side rendering
181  // see https://docu.ilias.de/goto_docu_wiki_wpage_5614_1357.html
182  $this->includeMathJax();
183 
184  // set engine for client-side rendering, if server is not used for the purpose
185  if (!isset($this->engine)) {
186  $this->setEngine(self::ENGINE_CLIENT);
187  }
188  }
189 
190  // no engine available or configured
191  if (!isset($this->engine)) {
192  $this->engine = self::ENGINE_NONE;
193  }
194 
195  return $this;
196  }
197 
201  protected function setEngine(string $a_engine): ilMathJax
202  {
203  switch ($a_engine) {
204  case self::ENGINE_CLIENT:
205  case self::ENGINE_SERVER:
206  case self::ENGINE_DEFERRED:
207  $this->engine = $a_engine;
208  break;
209  default:
210  $this->engine = self::ENGINE_NONE;
211  }
212 
213  return $this;
214  }
215 
219  public function setRendering(string $a_rendering): ilMathJax
220  {
221  switch ($a_rendering) {
222  case self::RENDER_SVG_AS_XML_EMBED:
223  case self::RENDER_SVG_AS_IMG_EMBED:
224  $this->rendering = $a_rendering;
225  $this->output = self::OUTPUT_SVG;
226  break;
227 
228  case self::RENDER_PNG_AS_IMG_EMBED:
229  case self::RENDER_PNG_AS_FO_FILE:
230  $this->rendering = $a_rendering;
231  $this->output = self::OUTPUT_PNG;
232  break;
233  }
234  return $this;
235  }
236 
240  public function setDpi(int $a_dpi): ilMathJax
241  {
242  $this->dpi = $a_dpi;
243  return $this;
244  }
245 
249  public function setZoomFactor(float $a_factor): ilMathJax
250  {
251  $this->zoom_factor = $a_factor;
252  return $this;
253  }
254 
259  {
260  if ($this->config->isClientEnabled()) {
261  $tpl = $a_tpl ?? $this->factory->template();
262 
263  if (!empty($this->config->getClintPolyfillUrl())) {
264  $tpl->addJavaScript($this->config->getClintPolyfillUrl());
265  }
266  if (!empty($this->config->getClientScriptUrl())) {
267  $tpl->addJavaScript($this->config->getClientScriptUrl());
268  }
269  }
270 
271  return $this;
272  }
273 
283  public function insertLatexImages(string $a_text, ?string $a_start = '[tex]', ?string $a_end = '[/tex]'): string
284  {
285  // don't change anything if mathjax is not configured
286  if ($this->engine === self::ENGINE_NONE) {
287  return $a_text;
288  }
289 
290  // this is a fix for bug5362
291  $a_start = str_replace("\\", "", $a_start ?? '[tex]');
292  $a_end = str_replace("\\", "", $a_end ?? '[/tex]');
293 
294  // current position to start the search for delimiters
295  $cpos = 0;
296  // find position of start delimiter
297  while (is_int($spos = ilStr::strIPos($a_text, $a_start, $cpos))) {
298  // find position of end delimiter
299  if (is_int($epos = ilStr::strIPos($a_text, $a_end, $spos + ilStr::strLen($a_start)))) {
300  // extract the tex code inside the delimiters
301  $tex = ilStr::subStr($a_text, $spos + ilStr::strLen($a_start), $epos - $spos - ilStr::strLen($a_start));
302 
303  // undo a code protection done by the deferred engine before
304  if (ilStr::subStr($tex, 0, 7) === 'base64:') {
305  $tex = base64_decode(substr($tex, 7));
306  }
307 
308  // omit the html newlines added by the ILIAS page editor
309  $tex = str_replace(array('<br>', '<br/>', '<br />'), '', $tex);
310 
311  // tex specific replacements
312  $tex = preg_replace("/\\\\([RZN])([^a-zA-Z])/", "\\mathbb{" . "$1" . "}" . "$2", $tex);
313 
314  // check, if tags go across div borders
315  if (is_int(ilStr::strIPos($tex, '<div>')) || is_int(ilStr::strIPos($tex, '</div>'))) {
316  // keep the original code including delimiters, continue search behind
317  $cpos = $epos + ilStr::strLen($a_end);
318  } else {
319  switch ($this->engine) {
320  case self::ENGINE_CLIENT:
321  // prepare code for processing in the browser
322  // add necessary html encodings
323  // use the configured mathjax delimiters
324  $tex = str_replace('<', '&lt;', $tex);
325  $replacement = $this->config->getClientLimiterStart() . $tex
326  . $this->config->getClientLimiterEnd();
327  break;
328 
329  case self::ENGINE_SERVER:
330  // apply server-side processing
331  // mathjax-node expects pure tex code
332  // so revert any applied html encoding
333  $tex = html_entity_decode($tex, ENT_QUOTES, 'UTF-8');
334  $replacement = $this->renderMathJax($tex);
335  break;
336 
337  case self::ENGINE_DEFERRED:
338  // protect code to save it for post production
339  $replacement = '[tex]' . 'base64:' . base64_encode($tex) . '[/tex]';
340  break;
341 
342  default:
343  // keep the original
344  $replacement = $tex;
345  break;
346  }
347 
348  // replace delimiters and tex code with prepared code or generated image
349  $a_text = ilStr::subStr($a_text, 0, $spos) . $replacement
350  . ilStr::subStr($a_text, $epos + ilStr::strLen($a_end));
351 
352  // continue search behind replacement
353  $cpos = $spos + ilStr::strLen($replacement);
354  }
355  } else {
356  // end delimiter position not found => stop search
357  break;
358  }
359 
360  if ($cpos >= ilStr::strlen($a_text)) {
361  // current position at the end => stop search
362  break;
363  }
364  }
365  return $a_text;
366  }
367 
371  protected function renderMathJax(string $a_tex): string
372  {
374  $options['math'] = $a_tex;
375  $options['dpi'] = $this->dpi;
376 
377  switch ($this->output) {
378  case self::OUTPUT_PNG:
379  $options['svg'] = false;
380  $options['png'] = true;
381  $suffix = ".png";
382  break;
383 
384  case self::OUTPUT_SVG:
385  default:
386  $options['svg'] = true;
387  $options['png'] = false;
388  $suffix = ".svg";
389  break;
390  }
391 
392  $image = $this->factory->image($a_tex, $this->output, $this->dpi);
393 
394  try {
395  if (!$image->exists()) {
396  $server = $this->factory->server($this->config);
397  $image->write($server->call($options));
398  }
399 
400  // get the image properties
401  switch ($this->output) {
402  case self::OUTPUT_PNG:
403  [$width, $height] = getimagesize($image->absolutePath());
404  $width = round($width * $this->zoom_factor);
405  $height = round($height * $this->zoom_factor);
406  $mime = 'image/png';
407  break;
408 
409  case self::OUTPUT_SVG:
410  default:
411  $svg = simplexml_load_string(file_get_contents($image->absolutePath()));
412  $width = round($svg['width'] * $this->zoom_factor);
413  $height = round($svg['height'] * $this->zoom_factor);
414  $mime = 'image/svg+xml';
415  break;
416  }
417 
418  // generate the html code
419  switch ($this->rendering) {
420  case self::RENDER_SVG_AS_XML_EMBED:
421  $html = $image->read();
422  break;
423 
424  case self::RENDER_SVG_AS_IMG_EMBED:
425  case self::RENDER_PNG_AS_IMG_EMBED:
426  $html = '<img src="data:' . $mime . ';base64,'
427  . base64_encode($image->read())
428  . '" style="width:' . $width . '; height:' . $height . ';" />';
429  break;
430 
431  case self::RENDER_PNG_AS_FO_FILE:
432  $html = '<fo:external-graphic src="' . $image->absolutePath() . '"'
433  . ' content-height="' . $height . 'px" content-width="' . $width . 'px"></fo:external-graphic>';
434  break;
435 
436  default:
437  $html = htmlspecialchars($a_tex);
438  break;
439  }
440  return $html;
441  } catch (Exception $e) {
442  return "[TeX rendering failed: " . $e->getMessage() . htmlentities($a_tex) . "]";
443  }
444  }
445 
449  public function getCacheSize(): string
450  {
451  return $this->factory->image('', $this->output, $this->dpi)->getCacheSize();
452  }
453 
457  public function clearCache(): void
458  {
459  $image = $this->factory->image('', $this->output, $this->dpi);
460  $image->clearCache();
461  }
462 }
const DEFAULT_ZOOM
ilMathJaxConfig $config
Class for processing of latex formulas This class uses a sigleton pattern to store the rendering purp...
static strIPos(string $a_haystack, string $a_needle, int $a_offset=0)
Definition: class.ilStr.php:54
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
factory()
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
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
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...
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.
includeMathJax(?ilGlobalTemplateInterface $a_tpl=null)
Include the Mathjax javascript(s) in the page template.
setEngine(string $a_engine)
Set the Rendering engine.
$server
Definition: shib_login.php:24
const RENDER_SVG_AS_IMG_EMBED
const ENGINE_DEFERRED
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