TeX rendering in a Java application
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. :)
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:
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:
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:
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 internalStringBuilder
; 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.