-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
835 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules/ | ||
temp/ | ||
output/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
var fs = require('fs'); | ||
var shell = require('shelljs'); | ||
|
||
var express = require('express'); | ||
|
||
var port = 3001; | ||
|
||
var staticDir = 'static/'; | ||
var tempDirRoot = 'temp/'; | ||
var outputDir = 'output/'; | ||
var httpOutputURL = 'https://latex2image.joeraut.com/output/'; | ||
|
||
// Command to compile .tex file to .dvi file. Timeout kills LaTeX after 5 seconds if held up | ||
var latexCMD = 'timeout 5 latex -interaction nonstopmode -halt-on-error equation.tex'; | ||
|
||
// Command to convert .dvi to .svg file | ||
var dvisvgmCMD = 'dvisvgm --no-fonts --scale=OUTPUT_SCALE --exact equation.dvi'; | ||
|
||
var dockerImageName = 'blang/latex:ubuntu'; // https://github.com/blang/latex-docker | ||
|
||
// Command to run the above commands in a new Docker container (with LaTeX preinstalled) | ||
var dockerCMD = `cd TEMP_DIR_NAME && exec docker run --rm -i --user="$(id -u):$(id -g)" --net=none -v "$PWD":/data "${dockerImageName}" /bin/sh -c "${latexCMD} && ${dvisvgmCMD}"`; | ||
|
||
// Commands to convert .svg to .png/.jpg and compress | ||
var svgToImageCMD = 'svgexport SVG_FILE_NAME OUT_FILE_NAME'; | ||
var imageMinCMD = 'imagemin IN_FILE_NAME > OUT_FILE_NAME'; | ||
|
||
// Checklist of valid formats and scales, to verify form values are correct | ||
var validFormats = ['SVG', 'PNG', 'JPG']; | ||
var validScales = ['10%', '25%', '50%', '75%', '100%', '125%', '150%', '200%', '500%', '1000%']; | ||
// Percentage scales mapped to floating point values used in arguments | ||
var validScalesInternal = ['0.1', '0.25', '0.5', '0.75', '1.0', '1.25', '1.5', '2.0', '5.0', '10.0']; | ||
|
||
var fontSize = 12; | ||
|
||
// LaTeX document template | ||
var preamble = ` | ||
\\usepackage{amsmath} | ||
\\usepackage{amssymb} | ||
\\usepackage{amsfonts} | ||
`; | ||
|
||
var documentTemplate = ` | ||
\\documentclass[${fontSize}pt]{article} | ||
${preamble} | ||
\\thispagestyle{empty} | ||
\\begin{document} | ||
\\[ | ||
EQUATION | ||
\\] | ||
\\end{document}`; | ||
|
||
// Create temp and output directories on first run | ||
if (!fs.existsSync(tempDirRoot)) { | ||
fs.mkdirSync(tempDirRoot); | ||
} | ||
if (!fs.existsSync(outputDir)) { | ||
fs.mkdirSync(outputDir); | ||
} | ||
|
||
var app = express(); | ||
|
||
var bodyParser = require('body-parser'); | ||
app.use(bodyParser.json()); | ||
app.use(bodyParser.urlencoded({extended: true})); | ||
|
||
// Allow static html files and output files to be accessible | ||
app.use('/', express.static(staticDir)); | ||
app.use('/output', express.static(outputDir)); | ||
|
||
// POST call for LaTeX to image conversion. Convert and return image URL or error message | ||
app.post('/convert', function (req, res) { | ||
// Ensure valid inputs | ||
if (req.body.latexInput) { | ||
if (validScales.includes(req.body.outputScale)) { | ||
if (validFormats.includes(req.body.outputFormat)) { | ||
var id = generateID(); // Generate unique ID for filename | ||
|
||
shell.mkdir(`${tempDirRoot}${id}`); | ||
|
||
var document = documentTemplate.replace('EQUATION', req.body.latexInput); | ||
fs.writeFileSync(`${tempDirRoot}${id}/equation.tex`, document); // Write generated .tex file | ||
|
||
var result = {}; | ||
|
||
var finalDockerCMD = dockerCMD.replace('TEMP_DIR_NAME', `${tempDirRoot}${id}`); | ||
finalDockerCMD = finalDockerCMD.replace('OUTPUT_SCALE', validScalesInternal[validScales.indexOf(req.body.outputScale)]); | ||
|
||
var fileFormat = req.body.outputFormat.toLowerCase(); | ||
|
||
// Asynchronously compile and render the LaTeX to svg | ||
shell.exec(finalDockerCMD, {async: true}, function() { | ||
if (fs.existsSync(`${tempDirRoot}${id}/equation.svg`)) { | ||
if (fileFormat === 'svg') { // Converting to SVG, no further processing required | ||
shell.cp(`${tempDirRoot}${id}/equation.svg`, `${outputDir}img-${id}.svg`); | ||
result.imageURL = `${httpOutputURL}img-${id}.svg`; | ||
} else { | ||
|
||
// Convert svg to png/jpg | ||
var finalSvgToImageCMD = svgToImageCMD.replace('SVG_FILE_NAME', `${tempDirRoot}${id}/equation.svg`); | ||
finalSvgToImageCMD = finalSvgToImageCMD.replace('OUT_FILE_NAME', `${tempDirRoot}${id}/equation.${fileFormat}`); | ||
if (fileFormat === 'jpg') { // Add a white background for jpg images | ||
finalSvgToImageCMD += ' "svg {background: white}"'; | ||
} | ||
shell.exec(finalSvgToImageCMD); | ||
|
||
// Compress the resultant image | ||
var finalImageMinCMD = imageMinCMD.replace('IN_FILE_NAME', `${tempDirRoot}${id}/equation.${fileFormat}`); | ||
finalImageMinCMD = finalImageMinCMD.replace('OUT_FILE_NAME', `${tempDirRoot}${id}/equation_compressed.${fileFormat}`); | ||
shell.exec(finalImageMinCMD); | ||
|
||
// Final image | ||
shell.cp(`${tempDirRoot}${id}/equation_compressed.${fileFormat}`, `${outputDir}img-${id}.${fileFormat}`); | ||
|
||
result.imageURL = `${httpOutputURL}img-${id}.${fileFormat}`; | ||
} | ||
} else { | ||
result.error = 'Error converting LaTeX to image. Please ensure the input is valid.'; | ||
} | ||
|
||
shell.rm('-r', `${tempDirRoot}${id}`); // Delete temporary files for this conversion | ||
|
||
res.end(JSON.stringify(result)); | ||
}); | ||
|
||
} else { | ||
res.end(JSON.stringify({error: 'Invalid image format'})); | ||
} | ||
} else { | ||
res.end(JSON.stringify({error: 'Invalid scale'})); | ||
} | ||
} else { | ||
res.end(JSON.stringify({error: 'No LaTeX input provided'})); | ||
} | ||
}); | ||
|
||
// Start the server | ||
app.listen(port, function() { | ||
console.log(`Latex2image listening on port ${port}`); | ||
}); | ||
|
||
|
||
function generateID() { // Generate a random 16-char hexadecimal ID | ||
var output = ''; | ||
for (var i = 0; i < 16; i++) { | ||
output += '0123456789abcdef'.charAt(Math.floor(Math.random() * 16)); | ||
} | ||
|
||
return output; | ||
} |
Oops, something went wrong.