1<?php
2/**
3 * LaTeX Rendering Class
4 *
5 * Primary Author: Wizardry and Steamworks <wizardry.steamworks@outlook.com>
6 * Original Author: Benjamin Zeiss <zeiss@math.uni-goettingen.de>
7 *
8 * @license GPL 2
9 */
10
11class LatexRender {
12    protected $_picture_path = "";
13    protected $_picture_path_httpd = "";
14    protected $_tmp_dir = "";
15
16    public $_keep_tmp = false;
17    public $_latex_path = "latex";
18    public $_dvips_path = "dvips";
19    public $_convert_path = "convert";
20    public $_identify_path = "identify";
21    public $_xsize_limit = 1000;
22    public $_ysize_limit = 500;
23    public $_string_length_limit = 2000;
24    public $_image_format = "png";
25    public $_font_size = 10;
26    public $_latexclass = "article";
27    public $_tmp_filename;
28    public $_latex_tags_blacklist = [];
29    public $_errorcode = 0;
30    public $_errorextra = "";
31    public $_filename;
32    public $_cmdoutput = "";
33    public $_preamble = "";
34    public $_postamble = "";
35
36    public function __construct($picture_path, $picture_path_httpd, $tmp_dir) {
37        $this->_picture_path = $picture_path;
38        $this->_picture_path_httpd = $picture_path_httpd;
39        $this->_tmp_dir = $tmp_dir;
40    }
41
42    public function getPicturePath() { return $this->_picture_path; }
43    public function getPicturePathHTTPD() { return $this->_picture_path_httpd; }
44
45    public function getFormulaURL($latex_formula) {
46        $latex_formula = str_ireplace(['&gt;', '&lt;'], ['>', '<'], $latex_formula);
47        $this->latex_document = $this->_preamble . "\n" . trim($latex_formula) . "\n" . $this->_postamble;
48
49        $formula_hash = md5($latex_formula);
50        $filename = "img" . $formula_hash . '.' . $this->_image_format;
51        $full_path_filename = $this->getPicturePath() . "/" . $filename;
52        $this->_filename = $full_path_filename;
53
54        if (is_readable($full_path_filename)) {
55            return $this->getPicturePathHTTPD() . "/" . $filename;
56        }
57
58        if (strlen($latex_formula) > $this->_string_length_limit) {
59            $this->_errorcode = 1;
60            $this->_errorextra = ': ' . strlen($latex_formula);
61            return false;
62        }
63
64        foreach ($this->_latex_tags_blacklist as $tag) {
65            if (stristr($latex_formula, $tag)) {
66                $this->_errorcode = 2;
67                return false;
68            }
69        }
70
71        if ($this->renderLatex($this->latex_document, $full_path_filename)) {
72            return $this->getPicturePathHTTPD() . $filename;
73        }
74
75        return false;
76    }
77
78    protected function getDimensions($filename) {
79        $output = $this->myexec($this->_identify_path . " " . escapeshellarg($filename), $status);
80        $result = explode(" ", $output);
81        if (!isset($result[2])) return ["x" => 0, "y" => 0];
82        $dim = explode("x", $result[2]);
83        return ["x" => (int)$dim[0], "y" => (int)$dim[1]];
84    }
85
86    public function renderLatex($latex_document, $destination) {
87        $current_dir = getcwd();
88        if (!is_dir($this->_tmp_dir)) return false;
89        chdir($this->_tmp_dir);
90
91        $this->_tmp_filename = md5(uniqid(rand(), true));
92        file_put_contents($this->_tmp_filename . ".tex", $latex_document);
93
94        $this->myexec($this->_latex_path . " --interaction=nonstopmode " . $this->_tmp_filename . ".tex", $status_latex);
95
96        if (!file_exists($this->_tmp_filename . ".dvi")) {
97            if (!$this->_keep_tmp) $this->cleanTemporaryDirectory();
98            chdir($current_dir);
99            $this->_errorcode = 4;
100            return false;
101        }
102
103        $this->myexec($this->_dvips_path . " " . $this->_tmp_filename . ".dvi -o " . $this->_tmp_filename . ".ps", $status_dvips);
104        $this->myexec($this->_convert_path . " " . $this->_tmp_filename . ".ps " . $this->_tmp_filename . "." . $this->_image_format, $status_convert);
105
106        if ($status_dvips || $status_convert) {
107            if (!$this->_keep_tmp) $this->cleanTemporaryDirectory();
108            chdir($current_dir);
109            $this->_errorcode = 6;
110            return false;
111        }
112
113        $dim = $this->getDimensions($this->_tmp_filename . "." . $this->_image_format);
114        if ($dim["x"] > $this->_xsize_limit || $dim["y"] > $this->_ysize_limit) {
115            if (!$this->_keep_tmp) $this->cleanTemporaryDirectory();
116            chdir($current_dir);
117            $this->_errorcode = 5;
118            $this->_errorextra = ": " . $dim["x"] . "x" . $dim["y"];
119            return false;
120        }
121
122        $status_code = copy($this->_tmp_filename . "." . $this->_image_format, $destination);
123        chdir($current_dir);
124
125        if (!$this->_keep_tmp) $this->cleanTemporaryDirectory();
126        if (!$status_code) { $this->_errorcode = 7; return false; }
127
128        return true;
129    }
130
131    protected function myexec($cmd, &$status) {
132        $cmd = "$cmd 2>&1";
133        $lastline = exec($cmd, $output, $status);
134        $this->_cmdoutput .= "\n>>>>> $cmd\n" . trim(implode(PHP_EOL, $output)) . PHP_EOL . "  --- exit status " . $status;
135        return $lastline;
136    }
137
138    public function cleanTemporaryDirectory() {
139        foreach (['tex', 'aux', 'log', 'dvi', 'ps', $this->_image_format] as $ext) {
140            @unlink($this->_tmp_dir . "/" . $this->_tmp_filename . "." . $ext);
141        }
142    }
143}
144