ILIAS  release_5-3 Revision v5.3.23-19-g915713cf615
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 
9 class ilMathJax
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
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 
74  protected $rendering = self::RENDER_SVG_AS_XML_EMBED;
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 works in TCPDF and should work in most engines
191  $this->setRendering(self::RENDER_PNG_AS_IMG_EMBED);
192  $this->setDpi(600);
193  $this->setZoomFactor(0.17);
194  } elseif ($a_purpose == self::PURPOSE_DEFERRED_PDF && $this->settings->get('server_for_pdf')) {
195  $this->engine = self::ENGINE_DEFERRED;
196  }
197  }
198 
199  // if server is not generally enabled or not activated for the intended purpose
200  // then set engine for client-side rendering, if possible
201  if (!isset($this->engine) && $this->settings->get('enable')) {
202  $this->engine = self::ENGINE_CLIENT;
203  $this->mathjax_url = $this->settings->get('path_to_mathjax');
204  $this->includeMathJax();
205 
206  switch ((int) $this->settings->get("limiter")) {
207  case 1:
208  $this->start_limiter = "[tex]";
209  $this->end_limiter = "[/tex]";
210  break;
211 
212  case 2:
213  $this->start_limiter = '<span class="math">';
214  $this->end_limiter = '</span>';
215  break;
216 
217  default:
218  $this->start_limiter = "\(";
219  $this->end_limiter = "\)";
220  break;
221  }
222  }
223 
224  // neither server nor client side rendering is enabled
225  // the use the older mimetex as fallback, if configured in ilias.ini.php
226  if (!isset($this->engine) && !empty($this->mimetex_url)) {
227  $this->engine = self::ENGINE_MIMETEX;
228  }
229 
230  // no engine available or configured
231  if (!isset($this->engine)) {
232  $this->engine = self::ENGINE_NONE;
233  }
234 
235  return $this;
236  }
237 
238 
244  public function setRendering($a_rendering)
245  {
246  switch ($a_rendering) {
247  case self::RENDER_SVG_AS_XML_EMBED:
248  case self::RENDER_SVG_AS_IMG_EMBED:
249  case self::RENDER_SVG_AS_IMG_FILE:
250  $this->rendering = $a_rendering;
251  $this->output = 'svg';
252  break;
253 
254  case self::RENDER_PNG_AS_IMG_EMBED:
255  case self::RENDER_PNG_AS_IMG_FILE:
256  case self::RENDER_PNG_AS_FO_FILE:
257  $this->rendering = $a_rendering;
258  $this->output = 'png';
259  break;
260  }
261  return $this;
262  }
263 
269  public function setDpi($a_dpi)
270  {
271  $this->dpi = (float) $a_dpi;
272  return $this;
273  }
274 
280  public function setZoomFactor($a_factor)
281  {
282  $this->zoom_factor = (float) $a_factor;
283  return $this;
284  }
285 
291  public function includeMathJax($a_tpl = null)
292  {
293  global $tpl;
294 
295  if ($a_tpl == null) {
296  $a_tpl = $tpl;
297  }
298 
299  if ($this->engine == self::ENGINE_CLIENT) {
300  $a_tpl->addJavaScript($this->mathjax_url);
301  }
302  }
303 
304 
317  public function insertLatexImages($a_text, $a_start = '[tex]', $a_end = '[/tex]', $a_dir = null, $a_path = null)
318  {
319  // is this replacement still needed?
320  // it was defined in the old ilUtil::insertLatexImages function
321  // perhaps it was related to jsmath
322  if ($this->engine != self::ENGINE_MIMETEX) {
323  $a_text = preg_replace("/\\\\([RZN])([^a-zA-Z]|<\/span>)/", "\\mathbb{" . "$1" . "}" . "$2", $a_text);
324  }
325 
326  // this is a fix for bug5362
327  $a_start = str_replace("\\", "", $a_start);
328  $a_end = str_replace("\\", "", $a_end);
329 
330  $cpos = 0;
331  while (is_int($spos = stripos($a_text, $a_start, $cpos))) { // find next start
332  if (is_int($epos = stripos($a_text, $a_end, $spos + strlen($a_start)))) {
333  // extract the tex code inside the delimiters
334  $tex = substr($a_text, $spos + strlen($a_start), $epos - $spos - strlen($a_start));
335 
336  // undo a code protection done by the deferred engine before
337  if (substr($tex, 0, 7) == 'base64:') {
338  $tex = base64_decode(substr($tex, 7));
339  }
340 
341  // omit the html newlines added by the ILIAS page editor
342  // handle custom newlines in JSMath (still needed?)
343  $tex = str_replace('<br>', '', $tex);
344  $tex = str_replace('<br/>', '', $tex);
345  $tex = str_replace('<br />', '', $tex);
346  $tex = str_replace('\\\\', '\\cr', $tex);
347 
348  // replace, if tags do not go across div borders
349  if (!is_int(strpos($tex, '</div>'))) {
350  switch ($this->engine) {
351  case self::ENGINE_CLIENT:
352  // prepare code for processing in the browser
353  // add necessary html encodings
354  // use the configured mathjax delimiters
355  $tex = str_replace('<', '&lt;', $tex);
356  $replacement = $this->start_limiter . $tex . $this->end_limiter;
357  break;
358 
359  case self::ENGINE_SERVER:
360  // apply server-side processing
361  // mathjax-node expects pure tex code
362  // so revert any applied html encoding
363  $tex = html_entity_decode($tex, ENT_QUOTES, 'UTF-8');
364  $replacement = $this->renderMathJax($tex, $a_dir, $a_path);
365  break;
366 
367  case self::ENGINE_MIMETEX:
368  // use mimetex
369  $replacement = $this->renderMimetex($tex, $a_dir, $a_path);
370  break;
371 
372  case self::ENGINE_DEFERRED:
373  // protect code to save it for post production
374  $replacement = '[tex]' . 'base64:' . base64_encode($tex) . '[/tex]';
375  break;
376 
377  case self::ENGINE_NONE:
378  // show only the pure tex code
379  $replacement = htmlspecialchars($tex);
380  break;
381  }
382 
383  // replace tex code with prepared code or generated image
384  $a_text = substr($a_text, 0, $spos) . $replacement . substr($a_text, $epos + strlen($a_end));
385  }
386  }
387  $cpos = $spos + 1;
388  }
389  return $a_text;
390  }
391 
392 
401  protected function renderMathJax($a_tex, $a_output_dir = null, $a_image_path = null)
402  {
404  $options['math'] = $a_tex;
405  $options['dpi'] = $this->dpi;
406 
407  switch ($this->output) {
408  case 'png':
409  $options['svg'] = false;
410  $options['png'] = true;
411  $suffix = ".png";
412  break;
413 
414  case 'svg':
415  default:
416  $options['svg'] = true;
417  $options['png'] = false;
418  $suffix = ".svg";
419  break;
420  }
421 
422  // store cached rendered image in cascading sub directories
423  $hash = md5($a_tex . '#' . $this->dpi);
424  $file = $this->cache_dir . '/' . substr($hash, 0, 4) . '/' . substr($hash, 4, 4) . '/' . $hash . $suffix;
425 
426  try {
427  if (!is_file($file)) {
428  // file has to be rendered
429  if ($this->use_curl) {
430  $curl = curl_init($this->server_address);
431  curl_setopt($curl, CURLOPT_HEADER, false);
432  curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
433  curl_setopt($curl, CURLOPT_HTTPHEADER, array("Content-type: application/json"));
434  curl_setopt($curl, CURLOPT_POST, true);
435  curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($options));
436  curl_setopt($curl, CURLOPT_TIMEOUT, $this->server_timeout);
437 
438  $response = curl_exec($curl);
439  $status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
440  curl_close($curl);
441 
442  if ($status != 200) {
443  $lines = explode("\n", $response);
444  return "[TeX rendering failed: " . $lines[1] . " " . htmlspecialchars($a_tex) . "]";
445  }
446  } else {
447  $context = stream_context_create(
448  array(
449  'http' => array(
450  'method' => 'POST',
451  'content' => json_encode($options),
452  'header' => "Content-Type: application/json\r\n",
453  'timeout' => $this->server_timeout,
454  'ignore_errors' => true
455  )
456  )
457  );
458  $response = @file_get_contents($this->server_address, false, $context);
459  if (empty($response)) {
460  return "[TeX rendering failed: " . htmlspecialchars($a_tex) . "]";
461  }
462  }
463 
464  // create the parent directories recursively
465  @mkdir(dirname($file), 0777, true);
466 
467  // save a rendered image to the temp folder
468  file_put_contents($file, $response);
469  }
470 
471  // handle output of images for offline usage without embedding
472  if (isset($a_output_dir) && is_dir($a_output_dir)) {
473  @copy($file, $a_output_dir . '/' . $hash . $suffix);
474  $src = $a_image_path . '/' . $hash . $suffix;
475  } else {
476  $src = ILIAS_HTTP_PATH . '/' . $file;
477  }
478 
479  // generate the image tag
480  switch ($this->output) {
481  case 'png':
482  list($width, $height) = getimagesize($file);
483  $width = round($width * $this->zoom_factor);
484  $height = round($height * $this->zoom_factor);
485  $mime = 'image/png';
486  break;
487 
488  case 'svg':
489  default:
490  $svg = simplexml_load_file($file);
491  $width = round($svg['width'] * $this->zoom_factor);
492  $height = round($svg['height'] * $this->zoom_factor);
493  $mime = 'image/svg+xml';
494  break;
495  }
496 
497 
498  // generate the image tag
499  switch ($this->rendering) {
500  case self::RENDER_SVG_AS_XML_EMBED:
501  $html = empty($response) ? file_get_contents($file) : $response;
502  break;
503 
504  case self::RENDER_SVG_AS_IMG_EMBED:
505  case self::RENDER_PNG_AS_IMG_EMBED:
506  $html = '<img src="data:' . $mime . ';base64,'
507  . base64_encode(empty($response) ? file_get_contents($file) : $response)
508  . '" style="width:' . $width . '; height:' . $height . ';" />';
509  break;
510 
511  case self::RENDER_SVG_AS_IMG_FILE:
512  case self::RENDER_PNG_AS_IMG_FILE:
513  $html = '<img src="' . $src . '" style="width:' . $width . '; height:' . $height . ';" />';
514  break;
515 
516  case self::RENDER_PNG_AS_FO_FILE:
517  $html = '<fo:external-graphic src="url(' . realpath($file) . ')"'
518  . ' content-height="' . $height . 'px" content-width="' . $width . 'px"></fo:external-graphic>';
519  break;
520 
521  default:
522  $html = htmlspecialchars($a_tex);
523  break;
524  }
525 
526  return $html;
527  } catch (Exception $e) {
528  return "[TeX rendering failed: " . $e->getMessage() . "]";
529  }
530  }
531 
532 
541  protected function renderMimetex($a_tex, $a_output_dir = null, $a_image_path = null)
542  {
543  $call = $this->mimetex_url . '?'
544  . rawurlencode(str_replace('&amp;', '&', str_replace('&gt;', '>', str_replace('&lt;', '<', $a_tex))));
545 
546  if (empty($a_output_dir)) {
547  $html = '<img alt="' . htmlentities($a_tex) . '" src="' . $call . '" />';
548  } else {
549  $cnt = $this->mimetex_count++;
550 
551  // get image from cgi and write it to file
552  $fpr = @fopen($call, "r");
553  $lcnt = 0;
554  if ($fpr) {
555  while (!feof($fpr)) {
556  $buf = fread($fpr, 1024);
557  if ($lcnt == 0) {
558  if (is_int(strpos(strtoupper(substr($buf, 0, 5)), "GIF"))) {
559  $suffix = "gif";
560  } else {
561  $suffix = "png";
562  }
563  $fpw = fopen($a_output_dir . "/img" . $cnt . "." . $suffix, "w");
564  }
565  $lcnt++;
566  fwrite($fpw, $buf);
567  }
568  fclose($fpw);
569  fclose($fpr);
570  }
571 
572  $html = '<img alt="' . htmlentities($a_tex) . '" src=' . $a_image_path . '/img"' . $cnt . '.' . $suffix . '/' . '" />';
573  }
574 
575  return $html;
576  }
577 
582  public function getCacheSize()
583  {
584  $cache_dir = realpath($this->cache_dir);
585 
586  if (!is_dir($cache_dir)) {
587  $size = 0;
588  } else {
590  }
591 
592  $type = array("k", "M", "G", "T");
593  $size = $size / 1024;
594  $counter = 0;
595  while ($size >= 1024) {
596  $size = $size / 1024;
597  $counter++;
598  }
599 
600  return(round($size, 1) . " " . $type[$counter] . "B");
601  }
602 
606  public function clearCache()
607  {
608  ilUtil::delDir($this->cache_dir);
609  }
610 }
static $_instance
$size
Definition: RandomTest.php:84
renderMimetex($a_tex, $a_output_dir=null, $a_image_path=null)
Render image from tex code using mimetex.
Class for Server-side generation of latex formulas.
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.
includeMathJax($a_tpl=null)
Include Mathjax javascript in a template.
const ENGINE_MIMETEX
const RENDER_PNG_AS_IMG_EMBED
$type
renderMathJax($a_tex, $a_output_dir=null, $a_image_path=null)
Render image from tex code using the MathJax server.
$tpl
Definition: ilias.php:10
const ENGINE_SERVER
init($a_purpose=self::PURPOSE_BROWSER)
Initialize the usage This must be done before any rendering call.
const PURPOSE_PDF
const RENDER_SVG_AS_IMG_FILE
setRendering($a_rendering)
Set the image type rendered by the server.
$counter
setDpi($a_dpi)
Set the dpi of the rendered images.
const PURPOSE_BROWSER
__construct()
Singleton: protected constructor.
const RENDER_PNG_AS_IMG_FILE
setZoomFactor($a_factor)
Set the zoom factor for images.
const ENGINE_CLIENT
getCacheSize()
Get the size of the image cache.
const PURPOSE_EXPORT
Create styles array
The data for the language used.
const RENDER_PNG_AS_FO_FILE
static dirsize($directory)
get size of a directory or a file.
settings()
Definition: settings.php:2
clearCache()
Clear the cache of rendered graphics.
static getInstance()
Singleton: get instance.
if(!file_exists("$old.txt")) if($old===$new) if(file_exists("$new.txt")) $file
const RENDER_SVG_AS_IMG_EMBED
const ENGINE_DEFERRED
$response
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
$html
Definition: example_001.php:87
const RENDER_SVG_AS_XML_EMBED
const PURPOSE_DEFERRED_PDF
if(!isset($_REQUEST['ReturnTo'])) if(!isset($_REQUEST['AuthId'])) $options
Definition: as_login.php:20
const ENGINE_NONE