LaTex formula Rendering Klasse
LaTex formula rendering class
This class renders a LaTex formula to an image with transparent background. The image files
are cached, so that the shell processes are not started twice if the formula was already
requested - that's good for the performance. The sample script returns not something
like <img src="..." />
, but directly the image binary. The sample source code shows
how to use it. Note that this very likely won't work if you have only a webspace. You need
Latex installed, ImageMagick, and need the possibility to create restricted user accounts
(for security reasons, Latex can execute shell commands).
Diese Klasse rendert eine LaTex-Formel zu einer Bilddatei mit transparentem Hintergrund. Die Bilder werden gecached, so dass ein existierendes Bild direkt gesendet wird und nicht der Konvertierungsprozess mehrmalig angestoßen wird. Das Beispiel-Skript sendet direkt eine Bilddatei an den Browser. Beachte, dass die Klasse wahrscheinlich nicht auf einem einfachen Webspace funktioniert, weil Latex und ImageMagick installiert sein muss. Weiterhin ist es aus Sicherheitsgründen wichtig, Latex als eingeschränkter Nutzer auszuführen (Latex kann shell-Kommandos ausführen).
Sample source code
Anwendungsbeispiel
<?php
require_once(__DIR__ . '/swlib/swlib.class.php');
use sw\ResourceFile;
use sw\LaTexRenderer;
use sw\Tracer;
use \Exception;
// Input check
if (isset($_GET['f'])) {
if (get_magic_quotes_gpc()) {
$_GET['f'] = stripslashes($_GET['f']);
}
$_GET['f'] = trim($_GET['f'], " \t\n\r");
try {
// Configure the renderer
// Note that you should run this binaries with another user than your
// normal web server, one with restricted user. I use a shell script,
// "sudo_restricted.sh", which contains a line like
// "sudo -n -H -u <restricted user> $1 $2 $3 $4 $5 $6 $7 $8 $9"
// The user is of cause NOT root, but a restricted used registered in
// the sudoers file. The HOME directory of this user is the current one.
$sudo = dirname(__FILE__) . '/sudo_restricted.sh ';
LaTexRenderer::config(array(
'cache_dir' => dirname(__FILE__) . '/cache',
'latex_bin' => $sudo . '/usr/bin/latex',
'dvips_bin' => $sudo . '/usr/bin/dvips',
'convert_bin' => $sudo . '/usr/bin/convert',
'cache_prefix' => 'texf_',
'temp_dir' => '',
'font_size' => 16,
'packages' => array(
'amsmath', 'amsfonts', 'amssymb', 'latexsym', 'color'
)
));
$render = new LaTexRenderer();
$file = dirname(__FILE__) . '/cache/' . $render->renderFormulaToImage($_GET['f']);
$rc = new ResourceFile($file, '/');
$rc->download();
} catch (Exception $e) {
header('501 Internal Server Error');
}
} else {
// Html header and footer
print <<<HERE
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html><head><title>LaTeX Equations and Graphics in PHP</title></head><body>
<pre><form action="{$_SERVER['PHP_SELF']}" method="get">
<textarea rows="20" cols="60" name="f">{$_GET['f']}</textarea><br/>
<input type="submit" /></form>
</pre></body></html>
HERE;
}
Class source code
Klassen-Quelltext
<?php
/**
* Renders LaTeX to a png image. The rendering class has an own cache based on
* the checkums of the fomulas. If a new formula is entered specified, then the
* rendering process will run, and create the cache file. The callback
* onTexFilter() can be overloaded to add an own filter, e.g. to prevent too long
* texts, shell commands or require HTTP authentication to render formulas.
* @gpackage de.atwillys.sw.php.swLib
* @author Stefan Wilhelm
* @copyright Stefan Wilhelm, 2010
* @license GPL
* @version 1.0
*/
namespace sw;
class LaTeXRenderer {
/**
* Stores the class configuration
* @staticvar array
*/
private static $config = array(
'latex_bin' => '/usr/texbin/latex',
'dvips_bin' => '/usr/texbin/dvips',
'convert_bin' => '/opt/local/bin/convert',
'cache_dir' => '',
'cache_prefix' => 'texf_',
'temp_dir' => '',
'font_size' => 16,
'packages' => array(
'amsmath', 'amsfonts', 'amssymb', 'latexsym', 'color'
)
);
/**
* Sets and returns the class configuration
* @param array $config
* @return array
*/
public static function config($config=null) {
if (!empty($config)) {
if (!is_array($config)) {
throw new LException('Class ":class" must be configured using an array', array(':class' => __CLASS__));
} else {
self::$config = array_merge(self::$config, $config);
}
}
return self::$config;
}
/**
* Overload this function to filter the input TeX sources, or e.g. using
* HTTP authentication to enable it. You can return a text that renders
* an error message.
* @param string $tex
* @return string
*/
protected function onTexFilter($tex) {
return $tex;
}
/**
* Constructor, automatically initializes the undefined configuration
* if the temp_directory is empty.
*/
public function __construct() {
if (empty(self::$config['cache_dir']))
self::config(array('cache_dir' => './cache'));
if (empty(self::$config['temp_dir']))
self::config(array('temp_dir' => '/tmp'));
}
/**
* Render a formula to an image
* @return string
*/
public function renderFormulaToImage($tex) {
return $this->renderToImage("\\documentclass[10pt]{article}\n\$packages\\pagestyle{empty}\n\\begin{document}\n\\begin{displaymath}\n$tex\n\\end{displaymath}\n\\end{document}\n");
}
/**
* Renders a LaTeX source to an image (png) if the md5 checksum of the
* source text already exists, a cached image representation is returned.
* The images are located in in the cache directory.
* @param string $tex
* @return string
*/
private function renderToImage($tex) {
$tex = $this->onTexFilter($tex);
$fileName = trim(self::$config['cache_prefix'] . md5($tex));
$cacheFilePath = self::$config['cache_dir'] . "/$fileName.png";
if (!is_file($cacheFilePath)) {
$exception = null;
try {
$packages = self::$config['packages'];
foreach ($packages as $key => $package) {
$packages[$key] = "\usepackage{" . $package . "}";
}
$packages = implode("\n", $packages);
$font_size = self::$config['font_size'];
$tex = str_replace('$packages', $packages, $tex);
$current_dir = getcwd();
$tmpDir = self::$config['temp_dir'];
$cacheDir = self::$config['cache_dir'];
chdir($tmpDir);
file_put_contents("$tmpDir/$fileName.tex", $tex);
$output = array();
exec(self::$config['latex_bin'] . " --interaction=nonstopmode $tmpDir/$fileName.tex", $output, $return_var);
@unlink("$tmpDir/$fileName.tex");
@unlink("$tmpDir/$fileName.aux");
@unlink("$tmpDir/$fileName.log");
if (!is_file("$tmpDir/$fileName.dvi")) {
$error = '';
foreach ($output as $line) {
$line = trim($line);
if (!empty($line)) {
if (substr($line, 0, 1) == '!') {
$error = trim($line, "!. ");
} else if (!empty($error)) {
$line = ltrim($line, ' l.0123456789');
throw new LException("LaTeX: \":error\" in line :line", array(':error' => $error, ':line' => $line));
}
}
}
if (!empty($error)) {
throw new LException('LaTeX: :error', array(':error' => $error));
} else {
throw new LException('LaTeX did not generate output file');
}
}
$output = array();
exec(self::$config['dvips_bin'] . " -E $tmpDir/$fileName.dvi -o $tmpDir/$fileName.ps", $output, $return_var);
@unlink("$tmpDir/$fileName.dvi");
if (!is_file("$tmpDir/$fileName.ps")) {
throw new LException('LaTeX/dvips conversion failed: :output', array(':output' => implode("\n", $output)));
}
$output = array();
chmod("$tmpDir/$fileName.ps", 0666);
exec(escapeshellcmd(self::$config['convert_bin'] . " -define registry:temporary-path=$tmpDir -density 120 $tmpDir/$fileName.ps $tmpDir/$fileName.png"));
@unlink("$tmpDir/$fileName.ps");
if (is_file("$tmpDir/$fileName.png")) {
copy("$tmpDir/$fileName.png", "$cacheDir/$fileName.png");
@unlink("$tmpDir/$fileName.png");
} else {
throw new LException('Conversion from to ps to png failed: :output', array(':output' => implode("\n", $output)));
}
} catch (\Exception $e) {
$exception = $e;
}
chdir($current_dir);
// Rethrow the exception after cleanup and directory restored
if (!empty($exception)) {
throw $exception;
}
}
return "$fileName.png";
}
}