ILIAS  release_8 Revision v8.24
class.ilMathJax.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
21
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
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
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
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) {
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) {
225 $this->rendering = $a_rendering;
226 $this->output = self::OUTPUT_SVG;
227 break;
228
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) {
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
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
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) {
422 $html = $image->read();
423 break;
424
427 $html = '<img src="data:' . $mime . ';base64,'
428 . base64_encode($image->read())
429 . '" style="width:' . $width . '; height:' . $height . ';" />';
430 break;
431
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}
Customizing of pimple-DIC for ILIAS.
Definition: Container.php:32
Repository for storing and loading the MathJax configuration.
Global Mathjax configuration.
Factory for objects used by ilMathJax.
Class for processing of latex formulas This class uses a sigleton pattern to store the rendering purp...
setDpi(int $a_dpi)
Set the dpi of the rendered images.
float $zoom_factor
init(string $a_purpose=self::PURPOSE_BROWSER)
Initialize the usage for a certain purpose This must be done before any rendering call.
const OUTPUT_SVG
const ENGINE_NONE
const RENDER_SVG_AS_IMG_EMBED
renderMathJax(string $a_tex)
Render image from tex code using the MathJax server.
string $rendering
clearCache()
Clear the cache of rendered graphics.
setZoomFactor(float $a_factor)
Set the zoom factor of the rendered images.
const ENGINE_SERVER
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 self $_instance
const ENGINE_CLIENT
string $output
const DEFAULT_DPI
static getInstance()
Singleton: get instance for use in ILIAS requests with a config loaded from the settings.
__construct(ilMathJaxConfig $config, ilMathJaxFactory $factory)
Protected constructor to force the use of an initialized instance.
const PURPOSE_PDF
const PURPOSE_DEFERRED_PDF
const RENDER_PNG_AS_IMG_EMBED
array $default_server_options
ilMathJaxConfig $config
const RENDER_PNG_AS_FO_FILE
const OUTPUT_PNG
getCacheSize()
Get the size of the image cache.
const PURPOSE_BROWSER
const RENDER_SVG_AS_XML_EMBED
includeMathJax(ilGlobalTemplateInterface $a_tpl=null)
Include the Mathjax javascript(s) in the page template.
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 $engine
setRendering(string $a_rendering)
Set the image type rendered by the server.
const DEFAULT_ZOOM
ilMathJaxFactory $factory
const PURPOSE_EXPORT
setEngine(string $a_engine)
Set the Rendering engine.
const ENGINE_DEFERRED
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static strIPos(string $a_haystack, string $a_needle, ?int $a_offset=null)
Definition: class.ilStr.php:54
static subStr(string $a_str, int $a_start, ?int $a_length=null)
Definition: class.ilStr.php:24
static strLen(string $a_string)
Definition: class.ilStr.php:63
$server
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
if($DIC->http() ->request() ->getMethod()=="GET" &&isset($DIC->http() ->request() ->getQueryParams()['tex'])) $tpl
Definition: latex.php:41