ILIAS  release_5-2 Revision v5.2.25-18-g3f80b828510
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  {
177  $this->server_address = $this->settings->get('server_address');
178  $this->server_timeout = $this->settings->get('server_timeout');
179 
180  if($a_purpose == self::PURPOSE_BROWSER && $this->settings->get('server_for_browser'))
181  {
182  $this->engine = self::ENGINE_SERVER;
183  // delivering svg directly in page may be faster than loading image files
184  $this->setRendering(self::RENDER_SVG_AS_XML_EMBED);
185  }
186  elseif($a_purpose == self::PURPOSE_EXPORT && $this->settings->get('server_for_export'))
187  {
188  $this->engine = self::ENGINE_SERVER;
189  // offline pages must always embed the svg as image tags
190  // otherwise the html base tag may conflict with references in svg
191  $this->setRendering(self::RENDER_SVG_AS_IMG_EMBED);
192  }
193  elseif($a_purpose == self::PURPOSE_PDF && $this->settings->get('server_for_pdf'))
194  {
195  $this->engine = self::ENGINE_SERVER;
196  // embedded png works in TCPDF and should work in most engines
197  $this->setRendering(self::RENDER_PNG_AS_IMG_EMBED);
198  $this->setDpi(600);
199  $this->setZoomFactor(0.17);
200 
201  }
202  elseif ($a_purpose == self::PURPOSE_DEFERRED_PDF && $this->settings->get('server_for_pdf'))
203  {
204  $this->engine = self::ENGINE_DEFERRED;
205  }
206  }
207 
208  // if server is not generally enabled or not activated for the intended purpose
209  // then set engine for client-side rendering, if possible
210  if (!isset($this->engine) && $this->settings->get('enable'))
211  {
212  $this->engine = self::ENGINE_CLIENT;
213  $this->mathjax_url = $this->settings->get('path_to_mathjax');
214  $this->includeMathJax();
215 
216  switch ((int) $this->settings->get("limiter"))
217  {
218  case 1:
219  $this->start_limiter = "[tex]";
220  $this->end_limiter = "[/tex]";
221  break;
222 
223  case 2:
224  $this->start_limiter = '<span class="math">';
225  $this->end_limiter = '</span>';
226  break;
227 
228  default:
229  $this->start_limiter = "\(";
230  $this->end_limiter = "\)";
231  break;
232  }
233  }
234 
235  // neither server nor client side rendering is enabled
236  // the use the older mimetex as fallback, if configured in ilias.ini.php
237  if (!isset($this->engine) && !empty($this->mimetex_url))
238  {
239  $this->engine = self::ENGINE_MIMETEX;
240  }
241 
242  // no engine available or configured
243  if (!isset($this->engine))
244  {
245  $this->engine = self::ENGINE_NONE;
246  }
247 
248  return $this;
249  }
250 
251 
257  public function setRendering($a_rendering)
258  {
259  switch($a_rendering)
260  {
261  case self::RENDER_SVG_AS_XML_EMBED:
262  case self::RENDER_SVG_AS_IMG_EMBED:
263  case self::RENDER_SVG_AS_IMG_FILE:
264  $this->rendering = $a_rendering;
265  $this->output = 'svg';
266  break;
267 
268  case self::RENDER_PNG_AS_IMG_EMBED:
269  case self::RENDER_PNG_AS_IMG_FILE:
270  case self::RENDER_PNG_AS_FO_FILE:
271  $this->rendering = $a_rendering;
272  $this->output = 'png';
273  break;
274  }
275  return $this;
276  }
277 
283  public function setDpi($a_dpi)
284  {
285  $this->dpi = (float) $a_dpi;
286  return $this;
287  }
288 
294  public function setZoomFactor($a_factor)
295  {
296  $this->zoom_factor = (float) $a_factor;
297  return $this;
298  }
299 
305  public function includeMathJax($a_tpl = null)
306  {
307  global $tpl;
308 
309  if ($a_tpl == null)
310  {
311  $a_tpl = $tpl;
312  }
313 
314  if ($this->engine == self::ENGINE_CLIENT)
315  {
316  $a_tpl->addJavaScript($this->mathjax_url);
317  }
318  }
319 
320 
333  public function insertLatexImages($a_text, $a_start = '[tex]', $a_end = '[/tex]', $a_dir = null, $a_path = null)
334  {
335  // is this replacement still needed?
336  // it was defined in the old ilUtil::insertLatexImages function
337  // perhaps it was related to jsmath
338  if ($this->engine != self::ENGINE_MIMETEX)
339  {
340  $a_text = preg_replace("/\\\\([RZN])([^a-zA-Z]|<\/span>)/", "\\mathbb{"."$1"."}"."$2", $a_text);
341  }
342 
343  // this is a fix for bug5362
344  $a_start = str_replace("\\", "", $a_start);
345  $a_end = str_replace("\\", "", $a_end);
346 
347  $cpos = 0;
348  while (is_int($spos = stripos($a_text, $a_start, $cpos))) // find next start
349  {
350  if (is_int($epos = stripos($a_text, $a_end, $spos + strlen($a_start))))
351  {
352  // extract the tex code inside the delimiters
353  $tex = substr($a_text, $spos + strlen($a_start), $epos - $spos - strlen($a_start));
354 
355  // undo a code protection done by the deferred engine before
356  if (substr($tex, 0, 7) == 'base64:')
357  {
358  $tex = base64_decode(substr($tex, 7));
359  }
360 
361  // omit the html newlines added by the ILIAS page editor
362  // handle custom newlines in JSMath (still needed?)
363  $tex = str_replace('<br>', '', $tex);
364  $tex = str_replace('<br/>', '', $tex);
365  $tex = str_replace('<br />', '', $tex);
366  $tex = str_replace('\\\\' , '\\cr', $tex);
367 
368  // replace, if tags do not go across div borders
369  if (!is_int(strpos($tex, '</div>')))
370  {
371  switch ($this->engine)
372  {
373  case self::ENGINE_CLIENT:
374  // prepare code for processing in the browser
375  // add necessary html encodings
376  // use the configured mathjax delimiters
377  $tex = str_replace('<', '&lt;', $tex);
378  $replacement = $this->start_limiter . $tex . $this->end_limiter;
379  break;
380 
381  case self::ENGINE_SERVER:
382  // apply server-side processing
383  // mathjax-node expects pure tex code
384  // so revert any applied html encoding
385  $tex = html_entity_decode($tex, ENT_QUOTES, 'UTF-8');
386  $replacement = $this->renderMathJax($tex, $a_dir, $a_path);
387  break;
388 
389  case self::ENGINE_MIMETEX:
390  // use mimetex
391  $replacement = $this->renderMimetex($tex, $a_dir, $a_path);
392  break;
393 
394  case self::ENGINE_DEFERRED:
395  // protect code to save it for post production
396  $replacement = '[tex]' . 'base64:' . base64_encode($tex) .'[/tex]';
397  break;
398 
399  case self::ENGINE_NONE:
400  // show only the pure tex code
401  $replacement = htmlspecialchars($tex);
402  break;
403  }
404 
405  // replace tex code with prepared code or generated image
406  $a_text = substr($a_text, 0, $spos) . $replacement . substr($a_text, $epos + strlen($a_end));
407  }
408  }
409  $cpos = $spos + 1;
410  }
411  return $a_text;
412  }
413 
414 
423  protected function renderMathJax($a_tex, $a_output_dir = null, $a_image_path = null)
424  {
426  $options['math'] = $a_tex;
427  $options['dpi'] = $this->dpi;
428 
429  switch ($this->output)
430  {
431  case 'png':
432  $options['svg'] = false;
433  $options['png'] = true;
434  $suffix = ".png";
435  break;
436 
437  case 'svg':
438  default:
439  $options['svg'] = true;
440  $options['png'] = false;
441  $suffix = ".svg";
442  break;
443  }
444 
445  // store cached rendered image in cascading sub directories
446  $hash = md5($a_tex . '#' . $this->dpi);
447  $file = $this->cache_dir . '/' . substr($hash, 0, 4) . '/' . substr($hash, 4, 4) . '/' . $hash . $suffix;
448 
449  try
450  {
451  if (!is_file($file))
452  {
453  // file has to be rendered
454  if ($this->use_curl)
455  {
456  $curl = curl_init($this->server_address);
457  curl_setopt($curl, CURLOPT_HEADER, false);
458  curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
459  curl_setopt($curl, CURLOPT_HTTPHEADER, array("Content-type: application/json"));
460  curl_setopt($curl, CURLOPT_POST, true);
461  curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($options));
462  curl_setopt($curl, CURLOPT_TIMEOUT, $this->server_timeout);
463 
464  $response = curl_exec($curl);
465  $status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
466  curl_close($curl);
467 
468  if ($status != 200)
469  {
470  $lines = explode("\n", $response);
471  return "[TeX rendering failed: " . $lines[1] . " " . htmlspecialchars($a_tex) . "]";
472  }
473  }
474  else
475  {
476  $context = stream_context_create(
477  array(
478  'http' => array(
479  'method' => 'POST',
480  'content' => json_encode($options),
481  'header' => "Content-Type: application/json\r\n",
482  'timeout' => $this->server_timeout,
483  'ignore_errors' => true
484  )
485  ));
486  $response = @file_get_contents($this->server_address, false, $context);
487  if (empty($response))
488  {
489  return "[TeX rendering failed: " . htmlspecialchars($a_tex) . "]";
490  }
491  }
492 
493  // create the parent directories recursively
494  @mkdir(dirname($file), 0777, true);
495 
496  // save a rendered image to the temp folder
497  file_put_contents($file, $response);
498  }
499 
500  // handle output of images for offline usage without embedding
501  if (isset($a_output_dir) && is_dir($a_output_dir))
502  {
503  @copy($file, $a_output_dir . '/' . $hash . $suffix);
504  $src = $a_image_path . '/' . $hash . $suffix;
505  }
506  else
507  {
508  $src = ILIAS_HTTP_PATH . '/' . $file;
509  }
510 
511  // generate the image tag
512  switch ($this->output)
513  {
514  case 'png':
515  list($width, $height) = getimagesize($file);
516  $width = round($width * $this->zoom_factor);
517  $height = round($height * $this->zoom_factor);
518  $mime = 'image/png';
519  break;
520 
521  case 'svg':
522  default:
523  $svg = simplexml_load_file($file);
524  $width = round($svg['width'] * $this->zoom_factor);
525  $height = round($svg['height'] * $this->zoom_factor);
526  $mime = 'image/svg+xml';
527  break;
528  }
529 
530 
531  // generate the image tag
532  switch ($this->rendering)
533  {
534  case self::RENDER_SVG_AS_XML_EMBED:
535  $html = empty($response) ? file_get_contents($file) : $response;
536  break;
537 
538  case self::RENDER_SVG_AS_IMG_EMBED:
539  case self::RENDER_PNG_AS_IMG_EMBED:
540  $html = '<img src="data:' . $mime . ';base64,'
541  . base64_encode(empty($response) ? file_get_contents($file) : $response)
542  . '" style="width:' . $width . '; height:' . $height . ';" />';
543  break;
544 
545  case self::RENDER_SVG_AS_IMG_FILE:
546  case self::RENDER_PNG_AS_IMG_FILE:
547  $html = '<img src="' . $src . '" style="width:' . $width . '; height:' . $height . ';" />';
548  break;
549 
550  case self::RENDER_PNG_AS_FO_FILE:
551  $html = '<fo:external-graphic src="url(' . realpath($file) . ')"'
552  . ' content-height="' . $height . 'px" content-width="' . $width . 'px"></fo:external-graphic>';
553  break;
554 
555  default:
556  $html = htmlspecialchars($a_tex);
557  break;
558  }
559 
560  return $html;
561  }
562  catch (Exception $e)
563  {
564  return "[TeX rendering failed: " . $e->getMessage() . "]";
565  }
566  }
567 
568 
577  protected function renderMimetex($a_tex, $a_output_dir = null, $a_image_path = null)
578  {
579  $call = $this->mimetex_url.'?'
580  .rawurlencode(str_replace('&amp;', '&', str_replace('&gt;', '>', str_replace('&lt;', '<', $a_tex))));
581 
582  if (empty($a_output_dir))
583  {
584  $html = '<img alt="'.htmlentities($a_tex).'" src="'.$call.'" />';
585  }
586  else
587  {
588  $cnt = $this->mimetex_count++;
589 
590  // get image from cgi and write it to file
591  $fpr = @fopen($call, "r");
592  $lcnt = 0;
593  if ($fpr)
594  {
595  while(!feof($fpr))
596  {
597  $buf = fread($fpr, 1024);
598  if ($lcnt == 0)
599  {
600  if (is_int(strpos(strtoupper(substr($buf, 0, 5)), "GIF")))
601  {
602  $suffix = "gif";
603  }
604  else
605  {
606  $suffix = "png";
607  }
608  $fpw = fopen($a_output_dir."/img".$cnt.".".$suffix, "w");
609  }
610  $lcnt++;
611  fwrite($fpw, $buf);
612  }
613  fclose($fpw);
614  fclose($fpr);
615  }
616 
617  $html = '<img alt="'.htmlentities($a_tex).'" src='.$a_image_path.'/img"'.$cnt.'.'.$suffix.'/'.'" />';
618  }
619 
620  return $html;
621  }
622 
627  public function getCacheSize()
628  {
629  $cache_dir = realpath($this->cache_dir);
630 
631  if (!is_dir($cache_dir))
632  {
633  $size = 0;
634  }
635  else
636  {
638  }
639 
640  $type = array("k", "M", "G", "T");
641  $size = $size / 1024;
642  $counter = 0;
643  while($size >= 1024)
644  {
645  $size = $size / 1024;
646  $counter++;
647  }
648 
649  return(round($size,1)." ".$type[$counter]."B");
650  }
651 
655  function clearCache()
656  {
657  ilUtil::delDir($this->cache_dir);
658  }
659 }
ILIAS Setting Class.
static $_instance
$size
Definition: RandomTest.php:79
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
renderMathJax($a_tex, $a_output_dir=null, $a_image_path=null)
Render image from tex code using the MathJax server.
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.
global $tpl
Definition: ilias.php:8
$counter
setDpi($a_dpi)
Set the dpi of the rendered images.
const PURPOSE_BROWSER
__construct()
Singleton: protected constructor.
const RENDER_PNG_AS_IMG_FILE
if(!is_array($argv)) $options
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
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
const ENGINE_NONE