Biomathematicus

Science, Technology, Engineering, Art, Mathematics

The problem: I had to install MediaWiki on my Windows server with the intention of using math notation. The math module would not produce any image.

The solution: The first step was to identify the culprit. Consider the page test.php with the following source code:

<?php
chdir("C:\\TMP\\wiki\\");
$command = "convert -density 300 -trim Input.ps Output.png";
$status_code = exec($command); 
?>

You will notice that the web page cannot produce the output image in PNG format, but if you run this instruction in the shell, it will execute flawlessly. Almost all advice on the web tells you “add the full location of the files to the command” or “give permissions to the X user in Y folders (and CMD.EXE)“. Of course, a significant number of comments say “well deserved for using Windows, blah, blah…“. All these comments come mainly from from people who just ignore the fundamentals of Windows OS.

The convert utility is in the global path, so no need to supply the full path. You should also add to the global path the location of LaTeX and GhostScript. The folder is changed in PHP before the exec instruction in this example, so, again, no need to supply the full path to the files. You only need to give “Execute” permissions to the IUSR groupย (that is, the user under which the web server runs)ย on the LaTeX, ImageMagick, and GhostScript folders. DO NOT try to change permissions to CMD.EXE.

The culprit seems to be PHP. Apparently there is an instruction that stops CONVERT with the DENSITY option (possibly a security check). The option “-density 300” causes the exec command in PHP to fail. If you remove it, an image of low resolution is produced.

The following program solves the problem:

<?php
chdir("C:\\TMP\\wiki\\");
// First: produce a PNG with GhostScript (i remaned it gs)
$command = "gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=png16m ".
           "-dGraphicsAlphaBits=1 -r150 -sOutputFile=".
           "Output.png Input.ps";
$status_code = exec($command); 
// Second: This PNG image needs to be trimmed. Use ImageMagick
$command = "convert Output.png -trim Output.png;
$status_code = exec($command); 
?>

A PHP page under Windows using IIS can indeed use CONVERT. To overcome the bug in PHP related to “-density”, it is necessary to use GhostScript to produce a PNG file, but GhostScript cannot do automated image cropping, thus the need to go to use CONVERT, and without the DENSITY option, it will execute perfectly.   

The final solution to the initial problem is the following Math.php file to be inserted in the MediaWiki instance. This solution complements the solution at http://www.mediawiki.org/wiki/User:WimW/math   I just changed what is described in this article.


<?php
// Modified by Juan B. Gutierrez on 7/31/2014
/** adapted by W. de Winter, Alterra, Wageningen - January 2014
*  Now functions as a regular tag-extension, suiteable for Media Wiki 1.22
* Required variables in LocalSettings.php (& example values):
*       $wgMathDirectory  = 'D:/internet/wiki/math';
*       $wgMathPath   = '/wiki/math/';
*       $wgTmpDirectory    = "D:/internet/wiki/temp";
*       #ImageMagick
*       $wgImageMagickConvertCommand  = "C:/xampp/ImageMagick/convert";
*       $wgImageMagickIdentifyCommand   = 'C:/xampp/ImageMagick/identify.exe'; 
*       #Tex
*       $wgLaTexCommand                 = 'C:/xampp/miktex/miktex/bin/latex.exe';
*       $wgDvipsCommand                 = 'C:/xampp/miktex/miktex/bin/dvips.exe';
**/
/** replaces original file as advised by
* http://www.mediawiki.org/wiki/Manual:Running_MediaWiki_on_Windows#Mathematics_Support
*/ 
/**
 * LaTeX Rendering Class
 * Copyright (C) 2003  Benjamin Zeiss <zeiss@math.uni-goettingen.de>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * --------------------------------------------------------------------
 * @author Benjamin Zeiss <zeiss@math.uni-goettingen.de>
 * @version v0.8
 * @package latexrender
 *
 */
 
/**
 * Error code:
 * 1. Too long formulas
 * 2. formula contains tags in the blacklist
 * 3. formula incorrect, can't be render
 * 4. can't exec tex command
 *    maybe directory unwritable,can't create temporary files
 * 6. formula image too big
 * 7. can't copy image file to cahed formula directory
 *    maybe ImageMagick fail
 */
 
 
 
$wgHooks['ParserFirstCallInit'][] = 'wfSampleParserInit';
 
// Hook our callback function into the parser
function wfSampleParserInit( Parser $parser ) {
 // When the parser sees the <sample> tag, it executes 
 // the wfSampleRender function (see below)
 $parser->setHook( 'math', 'wfSampleRender' );
        // Always return true from this function. The return value does not denote
        // success or otherwise have meaning - it just must always be true.
 return true;
}
 
// Execute 
function wfSampleRender( $input, array $args, Parser $parser, PPFrame $frame ) {
 
 $math = new MathRenderer();
 $html = $math->renderMath($input);
 return $html;
}
 
 
 
class LatexRender {
 
    // ====================================================================================
    // Variable Definitions
    // ====================================================================================
    var $_picture_path = "";
    var $_picture_path_httpd = "";
    var $_tmp_dir = "";
    // i was too lazy to write mutator functions for every single program used
    // just access it outside the class or change it here if nescessary
    var $_latex_path    = 'latex.exe';
    var $_dvips_path    = 'dvips.exe';
    var $_convert_path  = 'convert.exe';
    var $_identify_path = 'identify.exe';
 
    var $_formula_density = 300;
    var $_xsize_limit = 700;
    var $_ysize_limit = 700;
    var $_string_length_limit = 800;
    var $_font_size = 10;
    var $_latexclass = "article"; //install extarticle class if you wish to have smaller font sizes
    var $_tmp_filename;
        var $_image_format = "png"; //change to gif if you prefer but it's not clear
    // this most certainly needs to be extended. in the long term it is planned to use
    // a positive list for more security. this is hopefully enough for now. i'd be glad
    // to receive more bad tags !
    var $_latex_tags_blacklist = array(
        "include","def","command","loop","repeat","open","toks","output","input",
        "catcode","name","^^",
        "\\every","\\errhelp","\\errorstopmode","\\scrollmode","\\nonstopmode","\\batchmode",
        "\\read","\\write","csname","\\newhelp","\\uppercase", "\\lowercase","\\relax","\\aftergroup",
        "\\afterassignment","\\expandafter","\\noexpand","\\special"
        );
    var $_errorcode = 0;
        var $_errorextra = "";
 
 
    // ====================================================================================
    // constructor
    // ====================================================================================
 
    /**
     * Initializes the class
     *
     * @param string path where the rendered pictures should be stored
     * @param string same path, but from the httpd chroot
     */
    function LatexRender($picture_path,$picture_path_httpd,$tmp_dir) {
        $this->_picture_path = $picture_path;
        $this->_picture_path_httpd = $picture_path_httpd;
        $this->_tmp_dir = $tmp_dir;
        $this->_tmp_filename = md5(rand());
    }
 
    // ====================================================================================
    // public functions
    // ====================================================================================
 
    /**
     * Picture path Mutator function
     *
     * @param string sets the current picture path to a new location
     */
    function setPicturePath($name) {
        $this->_picture_path = $name;
    }
 
    /**
     * Picture path Mutator function
     *
     * @returns the current picture path
     */
    function getPicturePath() {
        return $this->_picture_path;
    }
 
    /**
     * Picture path HTTPD Mutator function
     *
     * @param string sets the current httpd picture path to a new location
     */
    function setPicturePathHTTPD($name) {
        $this->_picture_path_httpd = $name;
    }
 
    /**
     * Picture path HTTPD Mutator function
     *
     * @returns the current picture path
     */
    function getPicturePathHTTPD() {
        return $this->_picture_path_httpd;
    }
 
    /**
     * Tries to match the LaTeX Formula given as argument against the
     * formula cache. If the picture has not been rendered before, it'll
     * try to render the formula and drop it in the picture cache directory.
     *
     * @param string formula in LaTeX format
     * @returns the webserver based URL to a picture which contains the
     * requested LaTeX formula. If anything fails, the resultvalue is false.
     */
    function getFormulaURL($latex_formula) {
        // circumvent certain security functions of web-software which
        // is pretty pointless right here
 
        $latex_formula = preg_replace("/>/i", ">", $latex_formula);
        $latex_formula = preg_replace("/</i", "<", $latex_formula);
 
        $formula_hash = md5($latex_formula);
 
        $filename = 'math-' . $formula_hash.".".$this->_image_format;
        $full_path_filename = $this->getPicturePath()."/".$filename;
 
        if (is_file($full_path_filename)) {
            return $this->getPicturePathHTTPD()."/".$filename;
        } else {
            // security filter: reject too long formulas
            if (strlen($latex_formula) > $this->_string_length_limit) {
                $this->_errorcode = 1;
                return false;
            }
 
            // security filter: try to match against LaTeX-Tags Blacklist
            for ($i=0;$i<sizeof($this->_latex_tags_blacklist);$i++) {
                if (stristr($latex_formula,$this->_latex_tags_blacklist[$i])) {
                        $this->_errorcode = 2;
                    return false;
                }
            }
 
            // security checks assume correct formula, let's render it
            if ($this->renderLatex($latex_formula)) {
               return $this->getPicturePathHTTPD()."/".$filename;
            } else {
//                $this->_errorcode = 3;
//    $this->_errorextra = 
                return false;
            }
        }
    }
 
    // ====================================================================================
    // private functions
    // ====================================================================================
 
    /**
     * wraps a minimalistic LaTeX document around the formula and returns a string
     * containing the whole document as string. Customize if you want other fonts for
     * example.
     *
     * @param string formula in LaTeX format
     * @returns minimalistic LaTeX document containing the given formula
     */
    function wrap_formula($latex_formula) {
#        $string  = "\documentclass[".$this->_font_size."pt]{".$this->_latexclass."}\n";
#        $string .= "\usepackage[latin1]{inputenc}\n";
        $string  = "\documentclass{".$this->_latexclass."}\n";
        $string .= "\usepackage{amsmath}\n";
        $string .= "\usepackage{amsfonts}\n";
        $string .= "\usepackage{amssymb}\n";
        $string .= "\pagestyle{empty}\n";
        $string .= "\begin{document}\n";
        $string .= "$".$latex_formula."$\n";
        $string .= "\\end{document}\n";
 
        return $string;
    }
 
    /**
     * returns the dimensions of a picture file using 'identify' of the
     * imagemagick tools. The resulting array can be adressed with either
     * $dim[0] / $dim[1] or $dim["x"] / $dim["y"]
     *
     * @param string path to a picture
     * @returns array containing the picture dimensions
     */
    function getDimensions($filename) {
        $output=exec($this->_identify_path." ".$filename);
              //For some reason this didn't work for me, I used
              //$commander = "identify ".$filename;
              //$output=exec($commander);
              //instead. This should work if Identify works on the commandline
        $result=explode(" ",$output);
        $dim=explode("x",$result[2]);
        $dim["x"] = $dim[0];
        $dim["y"] = $dim[1];
 
        return $dim;
    }
 
    /**
     * Renders a LaTeX formula by the using the following method:
     *  - write the formula into a wrapped tex-file in a temporary directory
     *    and change to it
     *  - Create a DVI file using latex (tetex)
     *  - Convert DVI file to Postscript (PS) using dvips (tetex)
     *  - convert, trim and add transparancy by using 'convert' from the
     *    imagemagick package.
     *  - Save the resulting image to the picture cache directory using an
     *    md5 hash as filename. Already rendered formulas can be found directly
     *    this way.
     *
     * @param string LaTeX formula
     * @returns true if the picture has been successfully saved to the picture
     *          cache directory
     */
    function renderLatex($latex_formula) {
        $latex_document = $this->wrap_formula($latex_formula);
        $current_dir = getcwd();
 
        chdir($this->_tmp_dir);
 
        // create temporary latex file
        $fp = fopen($this->_tmp_dir."\\".$this->_tmp_filename.".tex","a+");
  //print $this->_tmp_dir."\\".$this->_tmp_filename.".tex";
        fputs($fp,$latex_document);
        fclose($fp);
 
        // create temporary dvi file
        // The \"'s are used in case the path has spaces in it (same as below for dvi)
        //$command = "\"".$this->_latex_path."\" --interaction=nonstopmode ".$this->_tmp_filename.".tex";
  $command = "\"".$this->_latex_path."\" --interaction=nonstopmode ".$this->_tmp_filename.".tex";  
            //In my case this didn't output in the right directory (amongst other things)
            //so I hardcoded everything in (If you use this, adjust it to your directories)
   $command = "latex --output-directory=C:\\TMP\\wiki\\ --interaction=nonstopmode C:\\TMP\\wiki\\".$this->_tmp_filename.".tex";
  
        $status_code = exec($command);
 
        if (!$status_code) { 
                $this->cleanTemporaryDirectory(); 
                chdir($current_dir); 
                $this->_errorcode = 4; 
    $this->_errorextra = "error executing LaTeX: " .$command;
                return false; 
        }
 
        // convert dvi file to postscript using dvips  \"".$this->_dvips_path."\"
        $command = "dvips -q -E ".$this->_tmp_filename.".dvi"." -o ".$this->_tmp_filename.".ps";
        $status_code = exec($command);
  
  chdir("C:\\TMP\\wiki\\");
        // Had to do two steps: 1st, use Ghostcript, and then use ImageMagick
  $command = "gs -dSAFER -dBATCH -dNOPAUSE -sDEVICE=png16m -dGraphicsAlphaBits=1 -r150 -sOutputFile=".
      $this->_tmp_filename.".".$this->_image_format." ".
      $this->_tmp_filename.".ps";
  $status_code = exec($command);
        $command = "convert ".
     $this->_tmp_filename.".".$this->_image_format.
     " -trim ".
     $this->_tmp_filename.".".$this->_image_format;
  $status_code = exec($command);
 
        /** if (!$status_code) { status tests always false...
                chdir($current_dir); 
                $this->_errorcode = 5; 
    $this->_errorextra = "conversion error: ".$command;
                return false; 
        } **/
 
        // test picture for correct dimensions
        $dim = $this->getDimensions($this->_tmp_filename.".".$this->_image_format);
 
        if ( ($dim["x"] > $this->_xsize_limit) or ($dim["y"] > $this->_ysize_limit)) {
            $this->cleanTemporaryDirectory();
            chdir($current_dir);
            $this->_errorcode = 6;
            $this->_errorextra = "dimension error: " . $dim["x"] . "x" . number_format($dim["y"],0,"","");
            return false;
        }
 
        // copy temporary formula file to cached formula directory
        $latex_hash = md5($latex_formula);
        $filename = $this->getPicturePath()."\\math-".$latex_hash.".".$this->_image_format;
 
        $status_code = copy($this->_tmp_filename.".".$this->_image_format,$filename);
 
        $this->cleanTemporaryDirectory();
 
        if (!$status_code) { 
                chdir($current_dir); 
                $this->_errorcode = 7; 
    $this->_errorextra = "error copying result: ".$this->_tmp_filename.".".$this->_image_format." --> ".$filename;
                return false; 
        }
        chdir($current_dir);
 
        return true;
    }
 
    /**
     * Cleans the temporary directory
     */
    function cleanTemporaryDirectory() {
 
        $current_dir = getcwd();
        chdir($this->_tmp_dir);
 
        unlink($this->_tmp_dir."/".$this->_tmp_filename.".tex");
        unlink($this->_tmp_dir."/".$this->_tmp_filename.".aux");
        unlink($this->_tmp_dir."/".$this->_tmp_filename.".log");
        unlink($this->_tmp_dir."/".$this->_tmp_filename.".dvi");
        unlink($this->_tmp_dir."/".$this->_tmp_filename.".ps");
        unlink($this->_tmp_dir."/".$this->_tmp_filename.".".$this->_image_format);
 
        chdir($current_dir);
    }
 
}
 
 
 
/**
 * LaTeX Rendering Class - Calling function
 * Copyright (C) 2003  Benjamin Zeiss <zeiss@math.uni-goettingen.de>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * --------------------------------------------------------------------
 * @author Benjamin Zeiss <zeiss@math.uni-goettingen.de>
 * @version v0.8
 * @package latexrender
 * Revised by Steve Mayer
 * This file can be included in many PHP programs by using something like (see example.php to see how it can be used)
 *              include_once('/full_path_here_to/latexrender/latex.php');
 *              $text_to_be_converted=latex_content($text_to_be_converted);
 * $text_to_be_converted will then contain the link to the appropriate image
 * or an error code as follows (the 500 values can be altered in class.latexrender.php):
 *      0 OK
 *      1 Formula longer than 500 characters
 *      2 Includes a blacklisted tag
 *      3 (Not used) Latex rendering failed
 *      4 Cannot create DVI file
 *      5 Picture larger than 500 x 500 followed by x x y dimensions
 *      6 Cannot copy image to pictures directory
 */
class MathRenderer {
 function renderMath($latex_formula) {
 
        global $wgMathDirectory, $wgMathPath, $wgTmpDirectory,
                        $wgLaTexCommand, 
                        $wgDvipsCommand,
                        $wgImageMagickConvertCommand, 
                        $wgImageMagickIdentifyCommand;
 
        $latex_formula = '\displaystyle ' . $latex_formula;
        $latex = new LatexRender ($wgMathDirectory, $wgMathPath, $wgTmpDirectory);
 
        #check Math dir
        if(!file_exists($wgMathDirectory)) @mkdir($wgMathDirectory);
        if(!file_exists($wgTmpDirectory)) @mkdir($wgTmpDirectory);
 
 
        $latex->_latex_path          = $wgLaTexCommand;
        $latex->_dvips_path          = $wgDvipsCommand;
        $latex->_convert_path        = $wgImageMagickConvertCommand;
        $latex->_identify_path       = $wgImageMagickIdentifyCommand;
 
        $url = $latex->getFormulaURL($latex_formula);
 
        $alt_latex_formula = htmlentities($latex_formula, ENT_QUOTES);
        $alt_latex_formula = str_replace("\r","",$alt_latex_formula);
        $alt_latex_formula = str_replace("\n","",$alt_latex_formula);
        $alt_latex_formula = str_replace('\displaystyle ','',$alt_latex_formula);
 
    if ($url != false)
        $text = "<img src='$url' title='$alt_latex_formula' alt='$alt_latex_formula' class=\"tex\" />";
    else
        $text = "[Error parsing LaTeX formula. Error $latex->_errorcode: $latex->_errorextra]";
 
    return $text;
 }
}
 
?>