Initial commit

This commit is contained in:
Joseph Rautenbach 2019-01-20 21:40:10 +02:00
parent ace97bd54d
commit 9cd9869f5b
8 changed files with 835 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/
temp/
output/

150
app.js Normal file
View File

@ -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;
}

466
package-lock.json generated Normal file
View File

@ -0,0 +1,466 @@
{
"name": "latex2image-web",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"accepts": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
"integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
"requires": {
"mime-types": "~2.1.18",
"negotiator": "0.6.1"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"body-parser": {
"version": "1.18.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
"integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
"requires": {
"bytes": "3.0.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "~1.6.3",
"iconv-lite": "0.4.23",
"on-finished": "~2.3.0",
"qs": "6.5.2",
"raw-body": "2.3.3",
"type-is": "~1.6.16"
}
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"content-disposition": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
"integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
},
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"destroy": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
"express": {
"version": "4.16.4",
"resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
"integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
"requires": {
"accepts": "~1.3.5",
"array-flatten": "1.1.1",
"body-parser": "1.18.3",
"content-disposition": "0.5.2",
"content-type": "~1.0.4",
"cookie": "0.3.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.1.1",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.4",
"qs": "6.5.2",
"range-parser": "~1.2.0",
"safe-buffer": "5.1.2",
"send": "0.16.2",
"serve-static": "1.13.2",
"setprototypeof": "1.1.0",
"statuses": "~1.4.0",
"type-is": "~1.6.16",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}
},
"finalhandler": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
"integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
"parseurl": "~1.3.2",
"statuses": "~1.4.0",
"unpipe": "~1.0.0"
}
},
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"glob": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"http-errors": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.0",
"statuses": ">= 1.4.0 < 2"
}
},
"iconv-lite": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"interpret": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz",
"integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw=="
},
"ipaddr.js": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
"integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
},
"mime": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
"integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
},
"mime-db": {
"version": "1.37.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
"integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg=="
},
"mime-types": {
"version": "2.1.21",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz",
"integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==",
"requires": {
"mime-db": "~1.37.0"
}
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"negotiator": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"parseurl": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
"integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"proxy-addr": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
"integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==",
"requires": {
"forwarded": "~0.1.2",
"ipaddr.js": "1.8.0"
}
},
"qs": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
},
"range-parser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
"integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
},
"raw-body": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
"integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
"requires": {
"bytes": "3.0.0",
"http-errors": "1.6.3",
"iconv-lite": "0.4.23",
"unpipe": "1.0.0"
}
},
"rechoir": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
"integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
"requires": {
"resolve": "^1.1.6"
}
},
"resolve": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz",
"integrity": "sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==",
"requires": {
"path-parse": "^1.0.6"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"send": {
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
"integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
"destroy": "~1.0.4",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.6.2",
"mime": "1.4.1",
"ms": "2.0.0",
"on-finished": "~2.3.0",
"range-parser": "~1.2.0",
"statuses": "~1.4.0"
}
},
"serve-static": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
"integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.2",
"send": "0.16.2"
}
},
"setprototypeof": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
},
"shelljs": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz",
"integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==",
"requires": {
"glob": "^7.0.0",
"interpret": "^1.0.0",
"rechoir": "^0.6.2"
}
},
"statuses": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
},
"type-is": {
"version": "1.6.16",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
"integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.18"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}
}
}

16
package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "latex2image-web",
"version": "1.0.0",
"description": "Web-based LaTeX to image converter using Node.js / Docker",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.18.3",
"express": "^4.16.4",
"shelljs": "^0.8.3"
}
}

11
static/custom.css Normal file
View File

@ -0,0 +1,11 @@
.navbar {
margin-bottom: 24px;
}
.initiallyHidden {
display: none;
}
#latexInputTextArea {
font-family: monospace;
}

87
static/index.html Normal file
View File

@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="Convert LaTeX math equations to PNG/JPG/SVG images">
<meta name="author" content="Joseph Rautenbach">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link rel="stylesheet" href="custom.css">
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script src="latex2image-client.js"></script>
<title>LaTeX2Image Convert LaTeX math equations to images</title>
</head>
<body>
<nav class="navbar navbar-light bg-light">
<div class="navbar-header">
<a class="navbar-brand" href="/"><img src="logo.svg"/></a>
</div>
</nav>
<div class="container">
<h3>Convert LaTeX math equations to PNG/JPG/SVG images</h3>
<p>LaTeX2Image allows LaTeX math equations to be exported directly to multiple image formats, and saved for use in other documents.</p>
<p>The full <a href="https://github.com/joeraut/latex2image-web">source code is available on GitHub</a>.
Behind the scenes, a Node.js app starts an isolated Docker container with a LaTeX installation for each request,
compiling the generated .tex file and converting it to an SVG vector image. If required, the SVG file is then converted to a raster image format.
</p>
<br>
</div>
<div class="container">
<div class="card">
<div class="card-body">
<div class="form-group">
<textarea class="form-control" id="latexInputTextArea" placeholder="Enter LaTeX math equation" rows="5"></textarea>
</div>
<div class="form-row">
<div class="form-group col-sm-6">
<label for="outputFormatSelect">Image format</label>
<select class="form-control" id="outputFormatSelect">
<option>PNG</option>
<option selected>SVG</option>
<option>JPG</option>
</select>
</div>
<div class="form-group col-sm-6">
<label for="outputScaleSelect">Scale</label>
<select class="form-control" id="outputScaleSelect">
<option>10%</option>
<option>25%</option>
<option>50%</option>
<option>75%</option>
<option>100%</option>
<option selected>125%</option>
<option>150%</option>
<option>200%</option>
<option>500%</option>
<option>1000%</option>
</select>
</div>
</div>
<input type='submit' class="btn btn-primary" id="convertButton" value="Convert">
&nbsp;&nbsp;
<input type='submit' class="btn btn-info" id="exampleButton" value="Show Example">
</div>
</div>
</div>
<br>
<div class="container initiallyHidden" id="result">
<div class="card initiallyHidden" id="resultCard">
<div class="card-body">
<img id="resultImage" />
<br><br>
<a href="#" class="btn btn-primary" role="button" id="downloadButton" target="_blank">Save Image</a>
</div>
</div>
<div class="alert alert-danger initiallyHidden" id="errorAlert"></div>
</div>
<br>
<div class="container">
<hr>
<footer>
<p>By <a href="https://joeraut.com">joeraut.com</a>, 2019 | <a href="https://github.com/joeraut/latex2image-web">source code</a></p>
</footer>
</div>
</body>
</html>

View File

@ -0,0 +1,72 @@
var hasShownBefore = false;
$(document).ready(function() {
function show(resultData) {
function afterSlideUp() {
var resultDataJSON;
if ((resultDataJSON = JSON.parse(resultData)) && !resultDataJSON.error) {
$('#resultImage').attr('src', resultDataJSON.imageURL);
$('#downloadButton').attr('href', resultDataJSON.imageURL);
$('#resultCard').show();
$('#errorAlert').hide();
} else {
$('#errorAlert').text(resultDataJSON.error);
$('#errorAlert').show();
$('#resultCard').hide();
}
$('#result').slideDown(330);
// Scroll window to bottom
$("html, body").animate({
scrollTop: $(document).height()
}, 1000);
hasShownBefore = true;
}
$('#result').slideUp(hasShownBefore ? 330 : 0, afterSlideUp);
}
$('#convertButton').click(function() {
if (!$('#latexInputTextArea').val()) {
show(JSON.stringify({
error: 'No LaTeX input provided'
}));
return;
}
$('#result').slideUp(hasShownBefore ? 330 : 0);
$('#convertButton').prop('disabled', true);
$('#exampleButton').prop('disabled', true);
$('#convertButton').prop('value', 'Converting...');
$.ajax({
url: '/convert',
type: 'POST',
data: {
latexInput: $('#latexInputTextArea').val(),
outputFormat: $('#outputFormatSelect').val(),
outputScale: $('#outputScaleSelect').val()
},
success: function(data) {
$('#convertButton').prop('disabled', false);
$('#exampleButton').prop('disabled', false);
$('#convertButton').prop('value', 'Convert');
show(data);
},
error: function() {
$('#convertButton').prop('disabled', false);
$('#exampleButton').prop('disabled', false);
$('#convertButton').prop('value', 'Convert');
alert('Error communicating with server');
}
});
});
// Show and convert a sample equation
$('#exampleButton').click(function() {
$('#latexInputTextArea').val('\\vec{F}=m \\frac{d \\vec{v}}{dt} + \\vec{v}\\frac{dm}{dt}');
$('#convertButton').click();
});
});

30
static/logo.svg Normal file
View File

@ -0,0 +1,30 @@
<?xml version='1.0'?>
<!-- This file was generated by dvisvgm 1.9.2 -->
<svg height='16.4812pt' version='1.1' viewBox='84.6199 86.0095 106.994 16.4812' width='106.994pt' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>
<defs>
<path d='M3.37136 -5.51532C3.33151 -5.61893 3.3076 -5.69066 3.1721 -5.69066C3.02864 -5.69066 3.00473 -5.6269 2.96488 -5.51532L1.18755 -0.820922C1.10785 -0.605729 0.940473 -0.263014 0.286924 -0.263014V0C0.541968 -0.0239103 0.964384 -0.0239103 1.10785 -0.0239103C1.36289 -0.0239103 1.60199 -0.0239103 1.99253 0V-0.263014C1.64184 -0.270984 1.43462 -0.430386 1.43462 -0.661519C1.43462 -0.71731 1.43462 -0.73325 1.47447 -0.828892L1.84907 -1.82516H4.01694L4.46326 -0.645579C4.50311 -0.549938 4.50311 -0.518057 4.50311 -0.502117C4.50311 -0.263014 4.08867 -0.263014 3.8655 -0.263014V0C4.54296 -0.0239103 4.5589 -0.0239103 4.99726 -0.0239103C5.44359 -0.0239103 5.58705 -0.0239103 6.04932 0V-0.263014H5.91382C5.41171 -0.263014 5.332 -0.334745 5.23636 -0.589788L3.37136 -5.51532ZM2.933 -4.6944L3.91333 -2.08817H1.94471L2.933 -4.6944Z' id='g0-65'/>
<path d='M5.26027 -2.00847H4.99726C4.96139 -1.80523 4.86575 -1.1477 4.7462 -0.956413C4.66252 -0.848817 3.98107 -0.848817 3.62242 -0.848817H1.41071C1.7335 -1.12379 2.46276 -1.88892 2.7736 -2.17584C4.59078 -3.84956 5.26027 -4.47123 5.26027 -5.65479C5.26027 -7.02964 4.17235 -7.95019 2.78555 -7.95019S0.585803 -6.76663 0.585803 -5.73848C0.585803 -5.12877 1.11183 -5.12877 1.1477 -5.12877C1.39875 -5.12877 1.70959 -5.30809 1.70959 -5.69066C1.70959 -6.0254 1.48244 -6.25255 1.1477 -6.25255C1.0401 -6.25255 1.01619 -6.25255 0.980324 -6.2406C1.20747 -7.05355 1.85305 -7.60349 2.63014 -7.60349C3.64633 -7.60349 4.268 -6.75467 4.268 -5.65479C4.268 -4.63861 3.68219 -3.75392 3.00075 -2.98879L0.585803 -0.286924V0H4.94944L5.26027 -2.00847Z' id='g1-50'/>
<path d='M7.6274 -3.06052H7.36438C7.07746 -1.19552 6.79054 -0.3467 4.77011 -0.3467H3.14421C2.61818 -0.3467 2.59427 -0.430386 2.59427 -0.824907V-4.0528H3.68219C4.82989 -4.0528 4.96139 -3.68219 4.96139 -2.65405H5.22441V-5.79826H4.96139C4.96139 -4.77011 4.82989 -4.3995 3.68219 -4.3995H2.59427V-7.31656C2.59427 -7.71108 2.61818 -7.79477 3.14421 -7.79477H4.73425C6.49166 -7.79477 6.86227 -7.19701 7.04159 -5.47547H7.30461L6.99377 -8.14147H0.490162V-7.79477H0.729265C1.59004 -7.79477 1.6259 -7.67522 1.6259 -7.23288V-0.908593C1.6259 -0.466252 1.59004 -0.3467 0.729265 -0.3467H0.490162V0H7.14919L7.6274 -3.06052Z' id='g1-69'/>
<path d='M2.59427 -7.25679C2.59427 -7.69913 2.63014 -7.81868 3.52677 -7.81868H3.78979V-8.16538C3.50286 -8.14147 2.47472 -8.14147 2.11606 -8.14147S0.71731 -8.14147 0.430386 -8.16538V-7.81868H0.6934C1.59004 -7.81868 1.6259 -7.69913 1.6259 -7.25679V-0.908593C1.6259 -0.466252 1.59004 -0.3467 0.6934 -0.3467H0.430386V0C0.71731 -0.0239103 1.74545 -0.0239103 2.10411 -0.0239103S3.50286 -0.0239103 3.78979 0V-0.3467H3.52677C2.63014 -0.3467 2.59427 -0.466252 2.59427 -0.908593V-7.25679Z' id='g1-73'/>
<path d='M6.81445 -3.06052H6.55143C6.43188 -1.86501 6.27646 -0.3467 4.18431 -0.3467H3.14421C2.61818 -0.3467 2.59427 -0.430386 2.59427 -0.824907V-7.24483C2.59427 -7.67522 2.61818 -7.81868 3.65828 -7.81868H4.01694V-8.16538C3.67024 -8.14147 2.61818 -8.14147 2.19975 -8.14147C1.8411 -8.14147 0.777086 -8.14147 0.490162 -8.16538V-7.81868H0.729265C1.59004 -7.81868 1.6259 -7.69913 1.6259 -7.25679V-0.908593C1.6259 -0.466252 1.59004 -0.3467 0.729265 -0.3467H0.490162V0H6.50361L6.81445 -3.06052Z' id='g1-76'/>
<path d='M7.80672 -8.1056H0.633624L0.418431 -5.4396H0.681445C0.848817 -7.32852 0.956413 -7.7589 2.84533 -7.7589C3.06052 -7.7589 3.39527 -7.7589 3.49091 -7.73499C3.73001 -7.69913 3.74197 -7.55567 3.74197 -7.2807V-0.920548C3.74197 -0.514072 3.74197 -0.3467 2.57036 -0.3467H2.13998V0C2.52254 -0.0239103 3.75392 -0.0239103 4.22017 -0.0239103S5.92976 -0.0239103 6.31233 0V-0.3467H5.88194C4.71034 -0.3467 4.71034 -0.514072 4.71034 -0.920548V-7.2807C4.71034 -7.57958 4.73425 -7.68717 4.92553 -7.73499C5.02117 -7.7589 5.36787 -7.7589 5.59502 -7.7589C7.48394 -7.7589 7.59153 -7.32852 7.7589 -5.4396H8.02192L7.80672 -8.1056Z' id='g1-84'/>
<path d='M4.67447 -4.59078L6.32428 -6.99377C6.52752 -7.2807 6.87422 -7.80672 8.00996 -7.81868V-8.16538C7.49589 -8.14147 7.36438 -8.14147 6.75467 -8.14147C6.43188 -8.14147 5.66675 -8.14147 5.39178 -8.16538V-7.81868C5.88194 -7.78281 6.03736 -7.49589 6.03736 -7.29265C6.03736 -7.16115 5.98954 -7.08941 5.91781 -6.98182L4.48319 -4.87771L2.86924 -7.26874C2.83337 -7.31656 2.78555 -7.40025 2.78555 -7.46002C2.78555 -7.6274 3.03661 -7.81868 3.467 -7.81868V-8.16538C3.18007 -8.14147 2.23562 -8.14147 1.88892 -8.14147C1.55417 -8.14147 0.71731 -8.14147 0.430386 -8.16538V-7.81868C1.32702 -7.81868 1.44658 -7.7589 1.69763 -7.37634L3.93325 -4.06476L1.92478 -1.13574C1.72154 -0.836862 1.33898 -0.358655 0.263014 -0.3467V0C0.777086 -0.0239103 0.908593 -0.0239103 1.51831 -0.0239103C1.8411 -0.0239103 2.61818 -0.0239103 2.89315 0V-0.3467C2.39103 -0.37061 2.23562 -0.669489 2.23562 -0.872727C2.23562 -1.00423 2.29539 -1.08792 2.36712 -1.19552L4.12453 -3.76588L6.06127 -0.872727C6.133 -0.765131 6.14496 -0.753176 6.14496 -0.705355C6.14496 -0.561893 5.92976 -0.358655 5.46351 -0.3467V0C5.75044 -0.0239103 6.68294 -0.0239103 7.02964 -0.0239103C7.36438 -0.0239103 8.2132 -0.0239103 8.50012 0V-0.3467C7.79477 -0.3467 7.50785 -0.3467 7.23288 -0.765131L4.67447 -4.59078Z' id='g1-88'/>
<path d='M4.61469 -3.19203C4.61469 -3.83761 4.61469 -4.31582 4.08867 -4.78207C3.67024 -5.16463 3.13225 -5.332 2.60623 -5.332C1.6259 -5.332 0.872727 -4.68643 0.872727 -3.90934C0.872727 -3.56264 1.09988 -3.39527 1.37484 -3.39527C1.66177 -3.39527 1.86501 -3.59851 1.86501 -3.88543C1.86501 -4.37559 1.43462 -4.37559 1.25529 -4.37559C1.53026 -4.87771 2.10411 -5.0929 2.58232 -5.0929C3.13225 -5.0929 3.83761 -4.63861 3.83761 -3.56264V-3.08443C1.43462 -3.04857 0.526027 -2.04433 0.526027 -1.12379C0.526027 -0.179328 1.6259 0.119552 2.35517 0.119552C3.14421 0.119552 3.68219 -0.358655 3.90934 -0.932503C3.95716 -0.37061 4.32777 0.0597758 4.84184 0.0597758C5.0929 0.0597758 5.7863 -0.107597 5.7863 -1.06401V-1.7335H5.52329V-1.06401C5.52329 -0.382565 5.23636 -0.286924 5.06899 -0.286924C4.61469 -0.286924 4.61469 -0.920548 4.61469 -1.09988V-3.19203ZM3.83761 -1.68568C3.83761 -0.514072 2.96488 -0.119552 2.45081 -0.119552C1.86501 -0.119552 1.37484 -0.549938 1.37484 -1.12379C1.37484 -2.70187 3.40722 -2.84533 3.83761 -2.86924V-1.68568Z' id='g1-97'/>
<path d='M4.57883 -2.7736C4.84184 -2.7736 4.86575 -2.7736 4.86575 -3.00075C4.86575 -4.20822 4.22017 -5.332 2.7736 -5.332C1.41071 -5.332 0.358655 -4.10062 0.358655 -2.61818C0.358655 -1.0401 1.57808 0.119552 2.90511 0.119552C4.32777 0.119552 4.86575 -1.17161 4.86575 -1.42267C4.86575 -1.4944 4.80598 -1.54222 4.73425 -1.54222C4.63861 -1.54222 4.61469 -1.48244 4.59078 -1.42267C4.27995 -0.418431 3.47895 -0.143462 2.97684 -0.143462S1.26725 -0.478207 1.26725 -2.54645V-2.7736H4.57883ZM1.2792 -3.00075C1.37484 -4.87771 2.4269 -5.0929 2.76164 -5.0929C4.04085 -5.0929 4.11258 -3.40722 4.12453 -3.00075H1.2792Z' id='g1-101'/>
<path d='M1.42267 -2.16389C1.98456 -1.79328 2.46276 -1.79328 2.59427 -1.79328C3.67024 -1.79328 4.47123 -2.60623 4.47123 -3.52677C4.47123 -3.84956 4.37559 -4.30386 3.99303 -4.68643C4.45928 -5.16463 5.02117 -5.16463 5.08095 -5.16463C5.12877 -5.16463 5.18854 -5.16463 5.23636 -5.14072C5.11681 -5.0929 5.05704 -4.97335 5.05704 -4.84184C5.05704 -4.67447 5.17659 -4.53101 5.36787 -4.53101C5.46351 -4.53101 5.6787 -4.59078 5.6787 -4.8538C5.6787 -5.06899 5.51133 -5.40374 5.0929 -5.40374C4.47123 -5.40374 4.00498 -5.02117 3.83761 -4.84184C3.47895 -5.11681 3.06052 -5.27223 2.60623 -5.27223C1.53026 -5.27223 0.729265 -4.45928 0.729265 -3.53873C0.729265 -2.85729 1.1477 -2.41494 1.26725 -2.30735C1.12379 -2.12802 0.908593 -1.78132 0.908593 -1.31507C0.908593 -0.621669 1.32702 -0.32279 1.42267 -0.263014C0.872727 -0.107597 0.32279 0.32279 0.32279 0.944458C0.32279 1.76936 1.44658 2.45081 2.91706 2.45081C4.33973 2.45081 5.52329 1.81719 5.52329 0.920548C5.52329 0.621669 5.4396 -0.0836862 4.72229 -0.454296C4.11258 -0.765131 3.51482 -0.765131 2.48667 -0.765131C1.75741 -0.765131 1.67372 -0.765131 1.45853 -0.992279C1.33898 -1.11183 1.23138 -1.33898 1.23138 -1.59004C1.23138 -1.79328 1.30311 -1.99651 1.42267 -2.16389ZM2.60623 -2.04433C1.55417 -2.04433 1.55417 -3.25181 1.55417 -3.52677C1.55417 -3.74197 1.55417 -4.23213 1.75741 -4.55492C1.98456 -4.90162 2.34321 -5.02117 2.59427 -5.02117C3.64633 -5.02117 3.64633 -3.8137 3.64633 -3.53873C3.64633 -3.32354 3.64633 -2.83337 3.44309 -2.51059C3.21594 -2.16389 2.85729 -2.04433 2.60623 -2.04433ZM2.92902 2.19975C1.78132 2.19975 0.908593 1.61395 0.908593 0.932503C0.908593 0.836862 0.932503 0.37061 1.3868 0.0597758C1.64981 -0.107597 1.75741 -0.107597 2.59427 -0.107597C3.58655 -0.107597 4.93748 -0.107597 4.93748 0.932503C4.93748 1.63786 4.02889 2.19975 2.92902 2.19975Z' id='g1-103'/>
<path d='M8.57186 -2.90511C8.57186 -4.01694 8.57186 -4.35168 8.29689 -4.73425C7.95019 -5.2005 7.38829 -5.27223 6.98182 -5.27223C5.98954 -5.27223 5.48742 -4.55492 5.29614 -4.08867C5.12877 -5.00922 4.48319 -5.27223 3.73001 -5.27223C2.57036 -5.27223 2.11606 -4.27995 2.02042 -4.04085H2.00847V-5.27223L0.382565 -5.14072V-4.79402C1.19552 -4.79402 1.29116 -4.71034 1.29116 -4.12453V-0.884682C1.29116 -0.3467 1.15965 -0.3467 0.382565 -0.3467V0C0.6934 -0.0239103 1.33898 -0.0239103 1.67372 -0.0239103C2.02042 -0.0239103 2.666 -0.0239103 2.97684 0V-0.3467C2.21171 -0.3467 2.06824 -0.3467 2.06824 -0.884682V-3.10834C2.06824 -4.36364 2.89315 -5.03313 3.63437 -5.03313S4.54296 -4.42341 4.54296 -3.69415V-0.884682C4.54296 -0.3467 4.41146 -0.3467 3.63437 -0.3467V0C3.94521 -0.0239103 4.59078 -0.0239103 4.92553 -0.0239103C5.27223 -0.0239103 5.91781 -0.0239103 6.22864 0V-0.3467C5.46351 -0.3467 5.32005 -0.3467 5.32005 -0.884682V-3.10834C5.32005 -4.36364 6.14496 -5.03313 6.88618 -5.03313S7.79477 -4.42341 7.79477 -3.69415V-0.884682C7.79477 -0.3467 7.66326 -0.3467 6.88618 -0.3467V0C7.19701 -0.0239103 7.84259 -0.0239103 8.17733 -0.0239103C8.52403 -0.0239103 9.16961 -0.0239103 9.48045 0V-0.3467C8.88269 -0.3467 8.58381 -0.3467 8.57186 -0.705355V-2.90511Z' id='g1-109'/>
</defs>
<g id='page1' transform='matrix(1.5 0 0 1.5 0 0)'>
<use x='56.4133' xlink:href='#g1-76' y='65.7534'/>
<use x='59.486' xlink:href='#g0-65' y='63.0303'/>
<use x='64.0731' xlink:href='#g1-84' y='65.7534'/>
<use x='70.5444' xlink:href='#g1-69' y='68.3271'/>
<use x='77.0457' xlink:href='#g1-88' y='65.7534'/>
<use x='88.6229' xlink:href='#g1-50' y='65.7534'/>
<use x='97.2887' xlink:href='#g1-73' y='65.7534'/>
<use x='101.496' xlink:href='#g1-109' y='65.7534'/>
<use x='111.214' xlink:href='#g1-97' y='65.7534'/>
<use x='117.046' xlink:href='#g1-103' y='65.7534'/>
<use x='122.877' xlink:href='#g1-101' y='65.7534'/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB