TeX rendering in a Java application

Info Friendly note: most of the conversion process involves input parsing, evaluation, font loading, rendering and output so that I wouldn't expect a nearly real-time conversion.

That said, let's go to business. :)

Info Warning: boring non-TeX technical Java stuff ahead. :)

I had a quick look at both SnuggleTeX and JEuclid to come up with this answer. Sorry, I didn't have time to come up with a better example.

The first one, SnuggleTeX, is described as "a free and open-source Java library for converting fragments of LaTeX to XML (usually XHTML + MathML)." The second one, JEuclid, is described as "a complete MathML rendering solution." What I actually did was to redirect one's output to the other's input.

First, with SnuggleTeX, you can obtain the needed code from the minimal example in its own homepage:

/* Create vanilla SnuggleEngine and new SnuggleSession */
SnuggleEngine engine = new SnuggleEngine();
SnuggleSession session = engine.createSession();

/* Parse some very basic Math Mode input */
SnuggleInput input = new SnuggleInput("$$ x+2=3 $$");
session.parseInput(input);

/* Convert the results to an XML String, which in this case will
 * be a single MathML <math>...</math> element. */
String xmlString = session.buildXMLString();

Now you have the MathML representation of your LaTeX input. Let's check JEuclid, from its API, and there's the Converter class with the following method:

BufferedImage render(Node node, LayoutContext context) 

Then you can use net.sourceforge.jeuclid.MathMLParserSupport to parse your XML string to org.w3c.dom.Document. Calling the render method with the correct parameters will give you a BufferedImage representing your input.

My attempt:

My code

It took around 1.4 secs to render this image.

I didn't like the output, but to be honest, I just wrote this app in 2 minutes as a [cough... cough... bad...] proof of concept. :) I'm almost sure the rendering quality can be improved, but I'm quite busy right now. Anyway, I think you can try something similar and then decide if this approach is worth a shot. :)

Update: It seems JEuclid has a JMathComponent in order to display MathML content in a Component.


Another solution is to call mimetex.cgi (available here: http://www.forkosh.com/mimetex.html) from Java.

I do not pretend that this solution is "better" than the ones previously given. The purpose is just to give alternatives.

Example of result:

enter image description here

Code leading to this result:

import java.awt.*;
import java.io.*;
import java.util.ArrayList;
import javax.swing.*;

public class Exemple106_MimetexInterface {

private static String MIMETEX_EXE = "c:\\Program Files (x86)\\mimetex\\mimetex.cgi";

final private static int BUFFER_SIZE = 1024;

/**
 * Convert LaTeX code to GIF
 *
 * @param latexString LaTeX code
 * @return GIF image, under byte[] format
 */
public static byte[] getLaTeXImage(String latexString) {
    byte[] imageData = null;
    try {
        // mimetex is asked (on the command line) to convert
        // the LaTeX expression to .gif on standard output:
        Process proc = Runtime.getRuntime().exec(MIMETEX_EXE + " -d \"" + latexString + "\"");
        // get the output stream of the process:
        BufferedInputStream bis = (BufferedInputStream) proc.getInputStream();
        // read output process by bytes blocks (size: BUFFER_SIZE)
        // and stores the result in a byte[] Arraylist:
        int bytesRead;
        byte[] buffer = new byte[BUFFER_SIZE];
        ArrayList<byte[]> al = new ArrayList<byte[]>();
        while ((bytesRead = bis.read(buffer)) != -1) {
            al.add(buffer.clone());
        }
        // convert the Arraylist in an unique array:
        int nbOfArrays = al.size();
        if (nbOfArrays == 1) {
            imageData = buffer;
        } else {
            imageData = new byte[BUFFER_SIZE * nbOfArrays];
            byte[] temp;
            for (int k = 0; k < nbOfArrays; k++) {
                temp = al.get(k);
                for (int i = 0; i < BUFFER_SIZE; i++) {
                    imageData[BUFFER_SIZE * k + i] = temp[i];
                }
            }
        }
        bis.close();
        proc.destroy();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return imageData;
}

/**
 * demonstration main
 *
 * @param args command line arguments
 */
public static void main(String[] args) {
    JFrame jframe = new JFrame();
    jframe.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    jframe.setLayout(new BorderLayout());

    String LATEX_EXPRESSION_1 = "4$A=\\(\\array{3,c.cccBCCC$&1&2&3\\\\\\hdash~1&a_{11}&a_{12}&a_{13}\\\\2&a_{21}&a_{22}&a_{23}\\\\3&a_{31}&a_{32}&a_{33}}\\) ";
    byte[] imageData1 = getLaTeXImage(LATEX_EXPRESSION_1);
    JLabel button1 = new JLabel(new ImageIcon(imageData1));
    jframe.add(button1, BorderLayout.NORTH);

    String LATEX_EXPRESSION_2 = "4$\\array{rccclBCB$&f&\\longr[75]^{\\alpha:{-1$f\\rightar~g}}&g\\\\3$\\gamma&\\longd[50]&&\\longd[50]&3$\\gamma\\\\&u&\\longr[75]_\\beta&v}";
    byte[] imageData2 = getLaTeXImage(LATEX_EXPRESSION_2);
    JLabel button2 = new JLabel(new ImageIcon(imageData2));
    jframe.add(button2, BorderLayout.CENTER);

    String LATEX_EXPRESSION_3 = "4$\\hspace{5}\\unitlength{1}\\picture(175,100){~(50,50){\\circle(100)}(1,50){\\overbrace{\\line(46)}^{4$\\;\\;a}}(52,50) {\\line(125)}~(50,52;115;2){\\mid}~(52,55){\\longleftar[60]}(130,56){\\longrightar[35]}~(116,58){r}~(c85,50;80;2){\\bullet} (c85,36){3$-q}~(c165,36){3$q}(42,30){\\underbrace{\\line(32)}_{1$a^2/r\\;\\;\\;}}~}";
    byte[] imageData3 = getLaTeXImage(LATEX_EXPRESSION_3);
    JLabel button3 = new JLabel(new ImageIcon(imageData3));
    jframe.add(button3, BorderLayout.SOUTH);

    jframe.pack();
    jframe.setLocationRelativeTo(null);
    jframe.setVisible(true);
}
}

Nicolas


For a limited subset of math-oriented plain TeX macros, I've forked JMathTeX to produce a version that can convert 1,000 simple TeX formulas into SVG documents in about 500 milliseconds on modern hardware, using only a couple of third-party Java libraries. The code is 100% pure Java: no external programs or web services are required to generate formulas.

Here's a screenshot of my desktop application that integrates the JMathTeX fork:

Application screenshot

Here's a sample program that demonstrates using the API; the program exports equations to the system's temporary directory as SVG files:

public class FormulaTest {
  private static final String DIR_TEMP = getProperty( "java.io.tmpdir" );

  private final static String[] EQUATIONS = {
      "(a+b)^2=a^2 + 2ab + b^2",
      "S_x = sqrt((SS_x)/(N-1))",
      "e^{\\pi i} + 1 = 0",
      "\\sigma=\\sqrt{\\sum_{i=1}^{k} p_i(x_i-\\mu)^2}",
      "\\sqrt[n]{\\pi}",
      "\\sqrt[n]{|z| . e^{i \\theta}} = " +
          "\\sqrt[n]{|z| . e^{i (\\frac{\\theta + 2 k \\pi}{n})}}," +
          " k \\in \\lbrace 0, ..., n-1 \\rbrace, n \\in NN",
      "\\vec{u}^2 \\tilde{\\nu}",
      "\\sum_{i=1}^n i = (\\sum_{i=1}^{n-1} i) + n =\n" +
          "\\frac{(n-1)(n)}{2} + n = \\frac{n(n+1)}{2}",
      "\\int_{a}^{b} x^2 dx",
      "G_{\\mu \\nu} = \\frac{8 \\pi G}{c^4} T_{{\\mu \\nu}}",
      "\\prod_{i=a}^{b} f(i)",
      "u(n) \\Leftrightarrow \\frac{1}{1-e^{-jw}} + " +
          "\\sum_{k=-\\infty}^{\\infty} \\pi \\delta (\\omega + 2\\pi k)\n"
  };

  public void test_Parser_SimpleFormulas_GeneratesSvg() throws IOException {
    final var size = 100f;
    final var texFont = new DefaultTeXFont( size );
    final var env = new TeXEnvironment( texFont );
    final var g = new SvgGraphics2D();
    g.scale( size, size );

    for( int j = 0; j < EQUATIONS.length; j++ ) {
      final var formula = new TeXFormula( EQUATIONS[ j ] );
      final var box = formula.createBox( env );
      final var layout = new TeXLayout( box, size );

      g.initialize( layout.getWidth(), layout.getHeight() );
      box.draw( g, layout.getX(), layout.getY() );

      final var path = Path.of( DIR_TEMP, format( "eq-%02d.svg", j ) );
      try( final var fos = new FileOutputStream( path.toFile() );
           final var out = new OutputStreamWriter( fos, UTF_8 ) ) {
        out.write( g.toString() );
      }
    }
  }

  public static void main( String[] args ) throws IOException {
    final var test = new FormulaTest();
    test.test_Parser_SimpleFormulas_GeneratesSvg();
  }
}

Getting real-time rendering required the following core changes:

  • Replace Batik's SVGGraphics2D with a tailored version based on JFreeSVG. This produces SVG strings about three times faster than the fastest Graphics2D-to-SVG converter available. The JFreeSVG version does not reuse an internal StringBuilder; creating new string objects for the SVG content introduces more latency than appending to the same pre-sized buffer.
  • Change TeXFormula's parser to use conditional logic instead of throwing exceptions for flow control. This avoids filling out a stack trace for every character in a macro/command name until that name matches a known command. Instead, the command is first parsed and then looked up in a map.
  • Replace double-to-string conversion with the Ryu algorithm. This gave an immediate doubling of efficiency in SVG document creation; the hot spot for generating SVG is converting font glyphs path values into strings. The Ryu algorithm is the fastest known procedure for decimal-to-string conversion as of 2018.

There were numerous other micro-optimizations made, but those items listed were the lion's share of the speed up. FWIW, JLaTeXMath has undergone a massive rewrite of its parser, as well, to address performance.

Tags:

Java

Rendering