ILIAS  release_7 Revision v7.30-3-g800a261c036
class.ilMathJax.php
Go to the documentation of this file.
1<?php
2/* Copyright (c) 1998-2016 ILIAS open source, Extended GPL, see docs/LICENSE */
3
4// fau: mathJaxServer - new class for mathjax rendering.
5
10{
11 const PURPOSE_BROWSER = 'browser'; // direct display of page in the browser
12 const PURPOSE_EXPORT = 'export'; // html export of contents
13 const PURPOSE_PDF = 'pdf'; // server-side PDF generation (only TCPDF and XSL-FO, not PhantomJS!!!)
14 const PURPOSE_DEFERRED_PDF = 'deferred_pdf'; // defer rendering for server-side pdf generation (XSL-FO)
15 // this needs a second call with PURPOSE_PDF at the end
16
17 const ENGINE_SERVER = 'server'; // code is treated by one of the rendering modes below
18 const ENGINE_CLIENT = 'client'; // code delimiters are
19 const ENGINE_MIMETEX = 'mimetex'; // fallback to old mimetex cgi (if configured in ilias.ini.php)
20 const ENGINE_DEFERRED = 'deferred'; // protect code for a deferred rendering
21 const ENGINE_NONE = 'none'; // don't render the code, just show it
22
23 const RENDER_SVG_AS_XML_EMBED = 'svg_as_xml_embed'; // embed svg code directly in html (default for browser view)
24 const RENDER_SVG_AS_IMG_EMBED = 'svg_as_img_embed'; // embed svg base64 encoded in an img tag (default for HTML export)
25 const RENDER_SVG_AS_IMG_FILE = 'svg_as_img_file'; // refer to an svg file from an img tag (if called with output dir)
26
27 const RENDER_PNG_AS_IMG_EMBED = 'png_as_img_embed'; // embed png base64 encoded in an img tag (default for PDF generation)
28 const RENDER_PNG_AS_IMG_FILE = 'png_as_img_file'; // refer to a png file from an img tag (if called with output dir)
29 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)
30
34 protected static $_instance = null;
35
39 protected $settings = null;
40
44 protected $engine = null;
45
49 protected $mathjax_url = '';
50
54 protected $start_limiter = '';
55
59 protected $end_limiter = '';
60
64 protected $server_address = '';
65
69 protected $server_timeout = 5;
70
75
79 protected $output = 'svg';
80
84 protected $dpi = 150;
85
89 protected $zoom_factor = 1.0;
90
91
97 protected $mimetex_url = URL_TO_LATEX;
98
102 protected $mimetex_count = 0;
103
109 protected $use_curl = true;
110
114 protected $cache_dir = '';
115
119 protected $default_options = array(
120 "format" => "TeX",
121 "math" => '', // TeX code
122 "svg" => true,
123 "mml" => false,
124 "png" => false,
125 "speakText" => false,
126 "speakRuleset" => "mathspeak",
127 "speakStyle" => "default",
128 "ex" => 6,
129 "width" => 1000000,
130 "linebreaks" => false,
131 );
132
133
137 protected function __construct()
138 {
139 // initiate the settings for browser as default
140 include_once "./Services/Administration/classes/class.ilSetting.php";
141 $this->settings = new ilSetting("MathJax");
142 $this->init(self::PURPOSE_BROWSER);
143
144 // set the connection method
145 $this->use_curl = extension_loaded('cURL');
146
147 // set the cache directory
148 $this->cache_dir = ilUtil::getWebspaceDir() . '/temp/tex';
149 }
150
155 public static function getInstance()
156 {
157 if (self::$_instance === null) {
158 self::$_instance = new self;
159 }
160 return self::$_instance;
161 }
162
169 public function init($a_purpose = self::PURPOSE_BROWSER)
170 {
171 // reset the choice of a former initialisation
172 unset($this->engine);
173
174 // try server-side rendering first, set this engine, if possible
175 if ($this->settings->get('enable_server')) {
176 $this->server_address = $this->settings->get('server_address');
177 $this->server_timeout = $this->settings->get('server_timeout');
178
179 if ($a_purpose == self::PURPOSE_BROWSER && $this->settings->get('server_for_browser')) {
180 $this->engine = self::ENGINE_SERVER;
181 // delivering svg directly in page may be faster than loading image files
182 $this->setRendering(self::RENDER_SVG_AS_XML_EMBED);
183 } elseif ($a_purpose == self::PURPOSE_EXPORT && $this->settings->get('server_for_export')) {
184 $this->engine = self::ENGINE_SERVER;
185 // offline pages must always embed the svg as image tags
186 // otherwise the html base tag may conflict with references in svg
187 $this->setRendering(self::RENDER_SVG_AS_IMG_EMBED);
188 } elseif ($a_purpose == self::PURPOSE_PDF && $this->settings->get('server_for_pdf')) {
189 $this->engine = self::ENGINE_SERVER;
190 // embedded png should work in most pdf engines
191 // details can be set by the rendering engine
192 $this->setRendering(self::RENDER_PNG_AS_IMG_EMBED);
193 } elseif ($a_purpose == self::PURPOSE_DEFERRED_PDF && $this->settings->get('server_for_pdf')) {
194 $this->engine = self::ENGINE_DEFERRED;
195 }
196 }
197
198 // if server is not generally enabled or not activated for the intended purpose
199 // then set engine for client-side rendering, if possible
200 if (!isset($this->engine) && $this->settings->get('enable')) {
201 $this->engine = self::ENGINE_CLIENT;
202 $this->mathjax_url = $this->settings->get('path_to_mathjax');
203 $this->includeMathJax();
204
205 switch ((int) $this->settings->get("limiter")) {
206 case 1:
207 $this->start_limiter = "[tex]";
208 $this->end_limiter = "[/tex]";
209 break;
210
211 case 2:
212 $this->start_limiter = '<span class="math">';
213 $this->end_limiter = '</span>';
214 break;
215
216 default:
217 $this->start_limiter = "\‍(";
218 $this->end_limiter = "\‍)";
219 break;
220 }
221 }
222
223 // neither server nor client side rendering is enabled
224 // the use the older mimetex as fallback, if configured in ilias.ini.php
225 if (!isset($this->engine) && !empty($this->mimetex_url)) {
226 $this->engine = self::ENGINE_MIMETEX;
227 }
228
229 // no engine available or configured
230 if (!isset($this->engine)) {
231 $this->engine = self::ENGINE_NONE;
232 }
233
234 return $this;
235 }
236
237
243 public function setRendering($a_rendering)
244 {
245 switch ($a_rendering) {
249 $this->rendering = $a_rendering;
250 $this->output = 'svg';
251 break;
252
256 $this->rendering = $a_rendering;
257 $this->output = 'png';
258 break;
259 }
260 return $this;
261 }
262
268 public function setDpi($a_dpi)
269 {
270 $this->dpi = (float) $a_dpi;
271 return $this;
272 }
273
279 public function setZoomFactor($a_factor)
280 {
281 $this->zoom_factor = (float) $a_factor;
282 return $this;
283 }
284
290 public function includeMathJax($a_tpl = null)
291 {
292 global $tpl;
293
294 if ($a_tpl == null) {
295 $a_tpl = $tpl;
296 }
297
298 if ($this->engine == self::ENGINE_CLIENT) {
299 $a_tpl->addJavaScript($this->mathjax_url);
300 }
301 }
302
303
316 public function insertLatexImages($a_text, $a_start = '[tex]', $a_end = '[/tex]', $a_dir = null, $a_path = null)
317 {
318 // is this replacement still needed?
319 // it was defined in the old ilUtil::insertLatexImages function
320 // perhaps it was related to jsmath
321 if ($this->engine != self::ENGINE_MIMETEX) {
322 $a_text = preg_replace("/\\\\([RZN])([^a-zA-Z]|<\/span>)/", "\\mathbb{" . "$1" . "}" . "$2", $a_text);
323 }
324
325 // this is a fix for bug5362
326 $a_start = str_replace("\\", "", $a_start);
327 $a_end = str_replace("\\", "", $a_end);
328
329 // current position to start the search for delimiters
330 $cpos = 0;
331 // find position of start delimiter
332 while (is_int($spos = ilStr::strIPos($a_text, $a_start, $cpos))) {
333 // find position of end delimiter
334 if (is_int($epos = ilStr::strIPos($a_text, $a_end, $spos + ilStr::strLen($a_start)))) {
335 // extract the tex code inside the delimiters
336 $tex = ilStr::subStr($a_text, $spos + ilStr::strLen($a_start), $epos - $spos - ilStr::strLen($a_start));
337
338 // undo a code protection done by the deferred engine before
339 if (ilStr::subStr($tex, 0, 7) == 'base64:') {
340 $tex = base64_decode(substr($tex, 7));
341 }
342
343 // omit the html newlines added by the ILIAS page editor
344 $tex = str_replace('<br>', '', $tex);
345 $tex = str_replace('<br/>', '', $tex);
346 $tex = str_replace('<br />', '', $tex);
347
348 // check, if tags go across div borders
349 if (is_int(ilStr::strPos($tex, '<div>')) || is_int(ilStr::strPos($tex, '</div>'))) {
350 // keep the original including delimiters, continue search behind
351 $cpos = $epos + ilStr::strLen($a_end);
352 } else {
353 switch ($this->engine) {
355 // prepare code for processing in the browser
356 // add necessary html encodings
357 // use the configured mathjax delimiters
358 $tex = str_replace('<', '&lt;', $tex);
359 $replacement = $this->start_limiter . $tex . $this->end_limiter;
360 break;
361
363 // apply server-side processing
364 // mathjax-node expects pure tex code
365 // so revert any applied html encoding
366 $tex = html_entity_decode($tex, ENT_QUOTES, 'UTF-8');
367 $replacement = $this->renderMathJax($tex, $a_dir, $a_path);
368 break;
369
371 // use mimetex
372 $replacement = $this->renderMimetex($tex, $a_dir, $a_path);
373 break;
374
376 // protect code to save it for post production
377 $replacement = '[tex]' . 'base64:' . base64_encode($tex) . '[/tex]';
378 break;
379
381 default:
382 // show only the pure tex code
383 $replacement = htmlspecialchars($tex);
384 break;
385 }
386
387 // replace delimiters and tex code with prepared code or generated image
388 $a_text = ilStr::subStr($a_text, 0, $spos) . $replacement . ilStr::subStr($a_text, $epos + ilStr::strLen($a_end));
389
390 // continue search behind replacement
391 $cpos = $spos + ilStr::strLen($replacement);
392 }
393 } else {
394 // end delimiter position not found => stop search
395 break;
396 }
397
398 if ($cpos >= ilStr::strlen($a_text)) {
399 // current position at the end => stop search
400 break;
401 }
402 }
403 return $a_text;
404 }
405
406
415 protected function renderMathJax($a_tex, $a_output_dir = null, $a_image_path = null)
416 {
417 $options = $this->default_options;
418 $options['math'] = $a_tex;
419 $options['dpi'] = $this->dpi;
420
421 switch ($this->output) {
422 case 'png':
423 $options['svg'] = false;
424 $options['png'] = true;
425 $suffix = ".png";
426 break;
427
428 case 'svg':
429 default:
430 $options['svg'] = true;
431 $options['png'] = false;
432 $suffix = ".svg";
433 break;
434 }
435
436 // store cached rendered image in cascading sub directories
437 $hash = md5($a_tex . '#' . $this->dpi);
438 $file = $this->cache_dir . '/' . substr($hash, 0, 4) . '/' . substr($hash, 4, 4) . '/' . $hash . $suffix;
439
440 try {
441 if (!is_file($file)) {
442 // file has to be rendered
443 if ($this->use_curl) {
444 $curl = curl_init($this->server_address);
445 curl_setopt($curl, CURLOPT_HEADER, false);
446 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
447 curl_setopt($curl, CURLOPT_HTTPHEADER, array("Content-type: application/json"));
448 curl_setopt($curl, CURLOPT_POST, true);
449 curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($options));
450 curl_setopt($curl, CURLOPT_TIMEOUT, $this->server_timeout);
451
452 $response = curl_exec($curl);
453 $status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
454 curl_close($curl);
455
456 if ($status != 200) {
457 $lines = explode("\n", $response);
458 return "[TeX rendering failed: " . $lines[1] . " " . htmlspecialchars($a_tex) . "]";
459 }
460 } else {
461 $context = stream_context_create(
462 array(
463 'http' => array(
464 'method' => 'POST',
465 'content' => json_encode($options),
466 'header' => "Content-Type: application/json\r\n",
467 'timeout' => $this->server_timeout,
468 'ignore_errors' => true
469 )
470 )
471 );
472 $response = @file_get_contents($this->server_address, false, $context);
473 if (empty($response)) {
474 return "[TeX rendering failed: " . htmlspecialchars($a_tex) . "]";
475 }
476 }
477
478 // create the parent directories recursively
479 @mkdir(dirname($file), 0777, true);
480
481 // save a rendered image to the temp folder
482 file_put_contents($file, $response);
483 }
484
485 // handle output of images for offline usage without embedding
486 if (isset($a_output_dir) && is_dir($a_output_dir)) {
487 @copy($file, $a_output_dir . '/' . $hash . $suffix);
488 $src = $a_image_path . '/' . $hash . $suffix;
489 } else {
490 $src = ILIAS_HTTP_PATH . '/' . $file;
491 }
492
493 // generate the image tag
494 switch ($this->output) {
495 case 'png':
496 list($width, $height) = getimagesize($file);
497 $width = round($width * $this->zoom_factor);
498 $height = round($height * $this->zoom_factor);
499 $mime = 'image/png';
500 break;
501
502 case 'svg':
503 default:
504 $svg = simplexml_load_file($file);
505 $width = round($svg['width'] * $this->zoom_factor);
506 $height = round($svg['height'] * $this->zoom_factor);
507 $mime = 'image/svg+xml';
508 break;
509 }
510
511
512 // generate the image tag
513 switch ($this->rendering) {
515 $html = empty($response) ? file_get_contents($file) : $response;
516 break;
517
520 $html = '<img src="data:' . $mime . ';base64,'
521 . base64_encode(empty($response) ? file_get_contents($file) : $response)
522 . '" style="width:' . $width . '; height:' . $height . ';" />';
523 break;
524
527 $html = '<img src="' . $src . '" style="width:' . $width . '; height:' . $height . ';" />';
528 break;
529
531 $html = '<fo:external-graphic src="' . realpath($file) . '"'
532 . ' content-height="' . $height . 'px" content-width="' . $width . 'px"></fo:external-graphic>';
533 break;
534
535 default:
536 $html = htmlspecialchars($a_tex);
537 break;
538 }
539
540 return $html;
541 } catch (Exception $e) {
542 return "[TeX rendering failed: " . $e->getMessage() . "]";
543 }
544 }
545
546
555 protected function renderMimetex($a_tex, $a_output_dir = null, $a_image_path = null)
556 {
557 $call = $this->mimetex_url . '?'
558 . rawurlencode(str_replace('&amp;', '&', str_replace('&gt;', '>', str_replace('&lt;', '<', $a_tex))));
559
560 if (empty($a_output_dir)) {
561 $html = '<img alt="' . htmlentities($a_tex) . '" src="' . $call . '" />';
562 } else {
563 $cnt = $this->mimetex_count++;
564
565 // get image from cgi and write it to file
566 $fpr = @fopen($call, "r");
567 $lcnt = 0;
568 if ($fpr) {
569 while (!feof($fpr)) {
570 $buf = fread($fpr, 1024);
571 if ($lcnt == 0) {
572 if (is_int(strpos(strtoupper(substr($buf, 0, 5)), "GIF"))) {
573 $suffix = "gif";
574 } else {
575 $suffix = "png";
576 }
577 $fpw = fopen($a_output_dir . "/img" . $cnt . "." . $suffix, "w");
578 }
579 $lcnt++;
580 fwrite($fpw, $buf);
581 }
582 fclose($fpw);
583 fclose($fpr);
584 }
585
586 $html = '<img alt="' . htmlentities($a_tex) . '" src=' . $a_image_path . '/img"' . $cnt . '.' . $suffix . '/' . '" />';
587 }
588
589 return $html;
590 }
591
596 public function getCacheSize()
597 {
598 $cache_dir = realpath($this->cache_dir);
599
600 if (!is_dir($cache_dir)) {
601 $size = 0;
602 } else {
604 }
605
606 $type = array("k", "M", "G", "T");
607 $size = $size / 1024;
608 $counter = 0;
609 while ($size >= 1024) {
610 $size = $size / 1024;
611 $counter++;
612 }
613
614 return(round($size, 1) . " " . $type[$counter] . "B");
615 }
616
620 public function clearCache()
621 {
622 ilUtil::delDir($this->cache_dir);
623 }
624}
$size
Definition: RandomTest.php:84
An exception for terminatinating execution or to throw for unit testing.
Class for Server-side generation of latex formulas.
const RENDER_SVG_AS_IMG_FILE
static $_instance
renderMathJax($a_tex, $a_output_dir=null, $a_image_path=null)
Render image from tex code using the MathJax server.
renderMimetex($a_tex, $a_output_dir=null, $a_image_path=null)
Render image from tex code using mimetex.
includeMathJax($a_tpl=null)
Include Mathjax javascript in a template.
const ENGINE_NONE
setZoomFactor($a_factor)
Set the zoom factor for images.
const RENDER_SVG_AS_IMG_EMBED
init($a_purpose=self::PURPOSE_BROWSER)
Initialize the usage This must be done before any rendering call.
clearCache()
Clear the cache of rendered graphics.
setRendering($a_rendering)
Set the image type rendered by the server.
const ENGINE_SERVER
const ENGINE_CLIENT
static getInstance()
Singleton: get instance.
const RENDER_PNG_AS_IMG_FILE
const ENGINE_MIMETEX
const PURPOSE_PDF
const PURPOSE_DEFERRED_PDF
const RENDER_PNG_AS_IMG_EMBED
const RENDER_PNG_AS_FO_FILE
getCacheSize()
Get the size of the image cache.
const PURPOSE_BROWSER
const RENDER_SVG_AS_XML_EMBED
setDpi($a_dpi)
Set the dpi of the rendered images.
__construct()
Singleton: protected constructor.
const PURPOSE_EXPORT
insertLatexImages($a_text, $a_start='[tex]', $a_end='[/tex]', $a_dir=null, $a_path=null)
Replace tex tags with formula image code New version of ilUtil::insertLatexImages.
const ENGINE_DEFERRED
ILIAS Setting Class.
static strPos($a_haystack, $a_needle, $a_offset=null)
Definition: class.ilStr.php:30
static strIPos($a_haystack, $a_needle, $a_offset=null)
Definition: class.ilStr.php:48
static subStr($a_str, $a_start, $a_length=null)
Definition: class.ilStr.php:15
static strLen($a_string)
Definition: class.ilStr.php:78
static delDir($a_dir, $a_clean_only=false)
removes a dir and all its content (subdirs and files) recursively
static getWebspaceDir($mode="filesystem")
get webspace directory
static dirsize($directory)
get size of a directory or a file.
if($DIC->http() ->request() ->getMethod()=="GET" &&isset($DIC->http() ->request() ->getQueryParams()['tex'])) $tpl
Definition: latex.php:41
$type
$response
settings()
Definition: settings.php:2
$context
Definition: webdav.php:26