Upmath is an open-source frontend project led by Russian particle physicist Dr. Roman Parpalak. It is an online editor based on Markdown + LaTeX. See its GitHub repository.
It can display complex mathematical formulas and graphics on the Web, including figures drawn with LaTeX TikZ.
It converts Markdown text and LaTeX mathematical expressions into HTML pages and embeds rendered formula images in SVG format.
| Component | Technology Stack and Responsibilities |
|---|---|
Frontend (upmath.me) | JavaScript + HTML/CSS frontend editor. Converts Markdown + LaTeX into HTML and embeds formula images. Uses grunt as its build system. |
Backend rendering service (i.upmath.me) | PHP + TeX Live + nginx + Node.js + Grunt + SVG toolchain. Renders formulas into SVG and provides an API for the frontend. |
| Image rendering | Uses TeX Live and tools such as dvisvgm to render LaTeX formulas into SVG vector graphics, with support for complex graphics such as TikZ. (i.upmath.me) |
The user enters Markdown text and LaTeX formulas in the frontend editor.
The editor converts Markdown into HTML. For LaTeX formulas, it uses
html<script src="//i.upmath.me/latex.js"></script>
to call the backend rendering service.
The backend service renders the LaTeX into SVG images and returns them to the frontend.
The user ultimately sees an HTML page containing embedded SVG mathematical formulas, which can be copied, shared, or published.
Dual-module architecture: editing and rendering responsibilities are clearly separated. The frontend focuses on editing and presentation, while the backend focuses on rendering.
Rich technology stack: the frontend uses JavaScript + Grunt, while the backend combines TeX, PHP, SVG rendering tools, and Docker support, making the environment easy to reproduce.
Flexible integration: it can be embedded into blogs, forums, or any platform that supports HTML. A small script is enough to dynamically render mathematical content.
Purchase a VPS that meets your requirements from a VPS provider and obtain a public IPv4 address:
text<your IP address>
In this guide, we use the Debian 12 distribution.
bashsudo apt update sudo apt install -y git curl ca-certificates gnupg lsb-release
Add Docker's official GPG key:
bashsudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
Add the official Docker apt repository:
bashecho \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") \
$(lsb_release -cs) stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
Install the Docker engine:
bashsudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io
Test it:
bashsudo docker run hello-world
Now the basic VPS environment is ready.
To access the service through a domain later, purchase a domain name from a domain registrar:
text<your domain>
Since I have already forked and modified the project, you can skip this section.
The forked project is available at:
https://github.com/X2M7/i.upmath.me
The following modifications to the original source code are kept here only for historical reference.
The reason we choose to deploy from source rather than directly use the official Docker deployment is that we need Chinese-language support in the editor, which requires some modifications to the source code.
First, clone the source code from GitHub:
Terminalgit clone https://github.com/parpalak/upmath.me.git
Enter the source-code directory:
Terminalcd i.upmath.me
Enter the tpl directory inside the i.upmath.me project:
Terminalcd tpl
There are three files inside the tpl directory. You can view them with:
Terminalls
We need to modify document.php.
Open document.php with your preferred text editor:
PHP<?php
/** @var bool $hasDvisvgmOption */
/** @var string $documentContent */
/** @var \S2\Tex\Tpl\PackageCollection $extraPackages */
/**
* \documentclass[11pt,dvisvgm]{standalone}
* %\usepackage[paperwidth=180in,paperheight=180in]{geometry}
* \usepackage[paperwidth=180in, paperheight=180in,margin=0in]{geometry}
* %\usepackage[a4paper, total={6in, 8in}]{geometry}
* \standaloneconfig{crop=false}
*/
?>
\documentclass[11pt<?php if ($hasDvisvgmOption) { ?>,dvisvgm<?php } ?>]{article}
\usepackage[paperwidth=180in,paperheight=180in]{geometry}
\batchmode
% Note that we added these two lines
\usepackage[utf8]{inputenc}
\usepackage{CJKutf8}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{stmaryrd}
\newcommand{\R}{\mathbb{R}}
\newcommand{\lt}{<}
\newcommand{\gt}{>}
% Conditional definitions
\providecommand{\tg}{\operatorname{tg}}
\providecommand{\ctg}{\operatorname{ctg}}
\providecommand{\arctg}{\operatorname{arctg}}
\providecommand{\arcctg}{\operatorname{arcctg}}
\usepackage[verbose]{newunicodechar}
\newunicodechar{¬}{\ensuremath{\neg}}
\newunicodechar{Γ}{\ensuremath{\Gamma}}
\newunicodechar{γ}{\ensuremath{\gamma}}
\newunicodechar{λ}{\ensuremath{\lambda}}
\newunicodechar{φ}{\ensuremath{\varphi}}
\newunicodechar{ψ}{\ensuremath{\psi}}
\newunicodechar{ϕ}{\ensuremath{\varphi}}
\newunicodechar{ᵢ}{\ensuremath{{}_{i}}}
\newunicodechar{₀}{\ensuremath{{}_{0}}}
\newunicodechar{₁}{\ensuremath{{}_{1}}}
\newunicodechar{₂}{\ensuremath{{}_{2}}}
\newunicodechar{₃}{\ensuremath{{}_{3}}}
\newunicodechar{₄}{\ensuremath{{}_{4}}}
\newunicodechar{₅}{\ensuremath{{}_{5}}}
\newunicodechar{₆}{\ensuremath{{}_{6}}}
\newunicodechar{₇}{\ensuremath{{}_{7}}}
\newunicodechar{₈}{\ensuremath{{}_{8}}}
\newunicodechar{₉}{\ensuremath{{}_{9}}}
\newunicodechar{ₙ}{\ensuremath{{}_{n}}}
\newunicodechar{ℓ}{\ensuremath{\ell}}
\newunicodechar{→}{\ensuremath{\rightarrow}}
\newunicodechar{⇒}{\ensuremath{\supset}}
\newunicodechar{⇔}{\ensuremath{\Leftrightarrow}}
\newunicodechar{∅}{\ensuremath{\emptyset}}
\newunicodechar{∈}{\ensuremath{\in}}
\newunicodechar{∘}{\ensuremath{\circ}}
\newunicodechar{∙}{\ensuremath{\bullet}}
\newunicodechar{∧}{\ensuremath{\wedge}}
\newunicodechar{∨}{\ensuremath{\vee}}
\newunicodechar{∼}{\ensuremath{\sim}}
\newunicodechar{≠}{\ensuremath{\neq}}
\newunicodechar{≡}{\ensuremath{\equiv}}
\newunicodechar{⊃}{\ensuremath{\supset}}
\newunicodechar{⊕}{\ensuremath{\oplus}}
\newunicodechar{⊖}{\ensuremath{\ominus}}
\newunicodechar{⊢}{\ensuremath{\vdash}}
\newunicodechar{⊤}{\ensuremath{\top}}
\newunicodechar{⊥}{\ensuremath{\bot}}
\newunicodechar{⊻}{\ensuremath{\veebar}}
\newunicodechar{⟝}{\ensuremath{\vdash}}
\newunicodechar{⬓}{\ensuremath{\square}}
\newunicodechar{Σ}{\ensuremath{\sum}}
\newunicodechar{Π}{\ensuremath{\prod}}
\newunicodechar{ⱼ}{\ensuremath{{}_{j}}}
<?php
echo $extraPackages->getCode();
?>
\pagestyle{empty}
\setlength{\topskip}{0pt}
\setlength{\parindent}{0pt}
\setlength{\abovedisplayskip}{0pt}
\setlength{\belowdisplayskip}{0pt}
\begin{document}
\begin{CJK}{UTF8}{gbsn} % Note that we added this line
<?php
foreach (['newwrite', 'openout'] as $disabledCommand) {
echo '\\renewcommand{\\' . $disabledCommand . '}{\\errmessage{Command \\noexpand\\' . $disabledCommand . ' is disabled}}', "\n";
}
?>
<?php echo $documentContent; ?>
\end{CJK}% Note that we added this line
\end{document}
Notice that we added four lines to the original code.
To minimize the changes, we continue using pdfLaTeX for compilation, so we added support for the CJK package:
latex... \usepackage[utf8]{inputenc} \usepackage{CJKutf8} ...
Note: If you need support for additional LaTeX packages, you can add
latex\usepackage{<the LaTeX package you need>}
after this section.
We also added the CJK environment inside \begin{document} and \end{document}.
Here, UTF8 specifies the text encoding, while gbsn is a Song-style Chinese font. You can replace it as needed.
latex... \begin{document} \begin{CJK}{UTF8}{gbsn} % Note that we added this line ... \end{CJK}% Note that we added this line \end{document}
Save and exit.
This completes the package modification.
Return to the parent i.upmath.me/ directory:
Terminalcd ..
To ensure Chinese font support, create a Dockerfile before building the Docker container.
Use your preferred editor:
Terminalnano Dockerfile
Enter the following:
DockerfileFROM ghcr.io/parpalak/upmath-texlive-docker:2025.0.1 EXPOSE 80 WORKDIR /var/www/i.upmath.me RUN apt-get update && apt-get install -y --no-install-recommends \ fonts-noto-cjk \ latex-cjk-all \ && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get -y --no-install-recommends install \ nginx-extras lua-zlib \ zip unzip \ php8.2-fpm \ php8.2-curl \ php8.2-xml \ php8.2-gd \ composer \ librsvg2-bin \ optipng \ supervisor \ curl gnupg && \ mkdir -p /etc/apt/keyrings && \ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \ apt-get update && \ apt-get install -y nodejs && \ apt-get remove -y curl gnupg && \ apt-get autoremove -y && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ rm -rf /var/cache/apt/ COPY . . RUN mkdir -p logs RUN composer install --no-dev RUN npm install -g yarn grunt-cli && \ yarn install && \ grunt && \ yarn install --prod && \ npm uninstall -g yarn grunt-cli RUN mkdir -p /var/run/php-fpm/ RUN cp config.php.dist config.php \ && tlversion=$(cat /usr/local/texlive/20*/release-texlive.txt | head -n 1 | awk '{ print $5 }') \ && sed -i "s/\${tlversion}/${tlversion}/g" config.php RUN cp docker/nginx.conf /etc/nginx/nginx.conf RUN cp docker/www.conf /etc/php/8.2/fpm/pool.d/www.conf && \ cp docker/www-tex.conf /etc/php/8.2/fpm/pool.d/www-tex.conf RUN cp docker/superv.conf /etc/superv.conf ENTRYPOINT [ "/var/www/i.upmath.me/docker/entrypoint.sh" ]
Save and exit.
To achieve the following behavior:
?c= applies a color for dark mode;you need to modify the following files.
(1) Color rewriting, applied only when ?c= is present.
(a) lib/Renderer/SvgHelper.php
php<?php
namespace S2\Tex\Renderer;
class SvgHelper
{
private const POINTS_IN_PIXEL = 0.75;
private const TOP_SHIFT_RATIO = 0.5;
private static function normalizeHexColor(?string $c): ?string
{
if ($c === null) return null;
$c = strtolower(trim($c));
$c = ltrim($c, '#');
if (preg_match('/^[0-9a-f]{3}$/', $c)) {
return '#' . $c[0].$c[0] . $c[1].$c[1] . $c[2].$c[2];
}
if (preg_match('/^[0-9a-f]{6}$/', $c)) {
return '#' . $c;
}
return null;
}
/**
* Only apply recolor when ?c=xxxxxx (or ?color=xxxxxx) exists.
* - Root fill="currentColor"
* - Root style="color:#xxxxxx"
* - Replace explicit pure-black representations to currentColor
* - Do NOT set root stroke (avoid bold glyphs)
*/
private static function applyColorParam(string $svg): string
{
$color = self::normalizeHexColor($_GET['c'] ?? ($_GET['color'] ?? null));
if ($color === null) {
return $svg; // default output unchanged
}
// 1) Root svg: fill=currentColor + color=<hex>
$svg = preg_replace_callback('/<svg\b([^>]*)>/', static function (array $m) use ($color): string {
$attrs = $m[1];
// Ensure default fill follows currentColor
if (stripos($attrs, ' fill=') === false) {
$attrs .= ' fill="currentColor"';
}
// Update or append style="color:..."
if (preg_match('/\sstyle=("|\')([^"\']*)\1/i', $attrs, $sm)) {
$quote = $sm[1];
$style = $sm[2];
// remove existing color:...
$style = preg_replace('/(^|;)\s*color\s*:\s*[^;]+/i', '$1', $style);
$style = trim($style);
if ($style !== '' && substr($style, -1) !== ';') $style .= ';';
$style .= 'color:' . $color . ';';
$attrs = preg_replace('/\sstyle=("|\')([^"\']*)\1/i', ' style=' . $quote . $style . $quote, $attrs, 1);
} else {
$attrs .= ' style="color:' . $color . ';"';
}
return '<svg' . $attrs . '>';
}, $svg, 1);
// 2) Replace pure black in attributes -> currentColor
// Support BOTH single and double quotes.
$svg = preg_replace(
'/\b(fill|stroke|stop-color)\s*=\s*(["\'])(#000000|#000|black)\2/i',
'$1=$2currentColor$2',
$svg
);
// rgb(0,0,0) or rgb(0%,0%,0%) or rgb(0.0%,0.0%,0.0%)
$svg = preg_replace(
'/\b(fill|stroke|stop-color)\s*=\s*(["\'])rgb\(\s*0(?:\.0+)?%?\s*,\s*0(?:\.0+)?%?\s*,\s*0(?:\.0+)?%?\s*\)\2/i',
'$1=$2currentColor$2',
$svg
);
// rgb(0 0 0) space-separated variant
$svg = preg_replace(
'/\b(fill|stroke|stop-color)\s*=\s*(["\'])rgb\(\s*0(?:\.0+)?%?\s+0(?:\.0+)?%?\s+0(?:\.0+)?%?\s*\)\2/i',
'$1=$2currentColor$2',
$svg
);
// rgba(0,0,0,1)
$svg = preg_replace(
'/\b(fill|stroke|stop-color)\s*=\s*(["\'])rgba\(\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*1(?:\.0+)?\s*\)\2/i',
'$1=$2currentColor$2',
$svg
);
// 3) Replace pure black in inline style -> currentColor
$svg = preg_replace('/\b(fill|stroke|stop-color)\s*:\s*#000000\b/i', '$1:currentColor', $svg);
$svg = preg_replace('/\b(fill|stroke|stop-color)\s*:\s*#000\b/i', '$1:currentColor', $svg);
$svg = preg_replace('/\b(fill|stroke|stop-color)\s*:\s*black\b/i', '$1:currentColor', $svg);
$svg = preg_replace('/\b(fill|stroke|stop-color)\s*:\s*rgb\(\s*0(?:\.0+)?%?\s*,\s*0(?:\.0+)?%?\s*,\s*0(?:\.0+)?%?\s*\)\b/i', '$1:currentColor', $svg);
$svg = preg_replace('/\b(fill|stroke|stop-color)\s*:\s*rgb\(\s*0(?:\.0+)?%?\s+0(?:\.0+)?%?\s+0(?:\.0+)?%?\s*\)\b/i', '$1:currentColor', $svg);
$svg = preg_replace('/\b(fill|stroke|stop-color)\s*:\s*rgba\(\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*0(?:\.0+)?\s*,\s*1(?:\.0+)?\s*\)\b/i', '$1:currentColor', $svg);
return $svg;
}
public static function processSvgContent(string $svg, bool $useBaseline): string
{
$startPattern = '#<!--start (-?[\d.]+) (-?[\d.]+) -->#';
if (!preg_match($startPattern, $svg, $matchBaseline)) {
return self::applyColorParam($svg);
}
$viewBoxPattern = '#viewBox=["\'](-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)\s+(-?[\d.]+)["\']#';
if (!preg_match($viewBoxPattern, $svg, $matchViewBox)) {
return self::applyColorParam($svg);
}
[, $userStartX, $userStartY, $userWidth, $userHeight] = $matchViewBox;
if ($userWidth < 0.000001 || $userHeight < 0.000001) {
return self::applyColorParam($svg);
}
$userBaselineY = $matchBaseline[2];
$userFromTopToBaseline = max(0, $userBaselineY - $userStartY);
$userFromBottomToBaseline = $useBaseline
? max($userHeight - $userFromTopToBaseline, 0)
: $userHeight * 0.5;
$multiplier = OUTER_SCALE / self::POINTS_IN_PIXEL;
$viewportFromBottomToBaseline = $multiplier * $userFromBottomToBaseline;
$viewportHeight = $multiplier * $userHeight;
$viewportWidth = $multiplier * $userWidth;
$extendedViewportHeight = ceil($viewportHeight);
$extendedViewportWidth = ceil($viewportWidth);
$extendedViewportFromBottomToBaseline =
$viewportFromBottomToBaseline
+ (1 - self::TOP_SHIFT_RATIO) * ($extendedViewportHeight - $viewportHeight);
$extendedUserHeight = $userHeight * $extendedViewportHeight / $viewportHeight;
$extendedUserWidth = $userWidth * $extendedViewportWidth / $viewportWidth;
$svg = preg_replace(
'#<svg\b[^>]*>#',
sprintf(
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="%s" height="%s" viewBox="%s %s %s %s">',
round($extendedViewportWidth, 6),
round($extendedViewportHeight, 6),
$userStartX,
round($userStartY - self::TOP_SHIFT_RATIO * ($extendedUserHeight - $userHeight), 6),
round($extendedUserWidth, 6),
round($extendedUserHeight, 6)
),
$svg,
1
);
$script = sprintf(
'<script type="text/ecmascript">if(window.parent.postMessage)window.parent.postMessage("%s|%s|%s|"+window.location,"*");</script>',
round($extendedViewportFromBottomToBaseline * self::POINTS_IN_PIXEL, 5),
round($extendedViewportWidth * self::POINTS_IN_PIXEL, 5),
round($extendedViewportHeight * self::POINTS_IN_PIXEL, 5)
);
$svg = str_replace('</svg>', $script . "\n" . '</svg>', $svg);
return self::applyColorParam($svg);
}
}
(b) lib/Cache/CacheProvider.php
php<?php
/**
* @copyright 2020-2022 Roman Parpalak
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @package Upmath Latex Renderer
* @link https://i.upmath.me
*/
namespace S2\Tex\Cache;
use S2\Tex\Processor\Request;
class CacheProvider
{
protected string $cacheFailDir;
protected string $cacheSuccessDir;
public function __construct(string $cacheSuccessDir, string $cacheFailDir)
{
$this->cacheFailDir = $cacheFailDir;
$this->cacheSuccessDir = $cacheSuccessDir;
}
public function getCacheState(Request $request): CacheState
{
$cacheName = $this->cachePathFromRequest($request, false);
return new CacheState($cacheName, file_exists($cacheName));
}
/**
* Returns the cached path.
* This algorithm should be used by a web-server to process the cache files as a static content.
*/
public function cachePathFromRequest(Request $request, bool $hasError): string
{
// IMPORTANT:
// old code: md5(formula) -> will mix /svg/x and /svg/x?c=...
// new code: include extension + variant, and bump schema with "cache-v2"
$hash = md5(
$request->getFormula() . "\n" .
$request->getExtension() . "\n" .
$request->getVariant() . "\n" .
'cache-v2'
);
$prefixDir = $hasError ? $this->cacheFailDir : $this->cacheSuccessDir;
return $prefixDir . substr($hash, 0, 2) .
'/' . substr($hash, 2, 2) .
'/' . substr($hash, 4) .
'.' . $request->getExtension();
}
}
(2) Allow Request to receive the query parameters; otherwise variant will always be empty.
Modify www/render.php as follows:
php<?php
/**
* Entry point for rendering.
*
* @copyright 2014-2020 Roman Parpalak
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @package Upmath Latex Renderer
* @link https://i.upmath.me
*/
use hollodotme\FastCGI\Client;
use hollodotme\FastCGI\Requests\PostRequest;
use hollodotme\FastCGI\SocketConnections\UnixDomainSocket;
use Katzgrau\KLogger\Logger;
use S2\Tex\Cache\CacheProvider;
use S2\Tex\Processor\CachedResponse;
use S2\Tex\Processor\PostProcessor;
use S2\Tex\Processor\Processor;
use S2\Tex\Processor\Request;
use S2\Tex\Renderer\PngConverter;
use S2\Tex\Renderer\Renderer;
use S2\Tex\Templater;
require '../vendor/autoload.php';
require '../config.php';
$isDebug = defined('DEBUG') && DEBUG;
error_reporting($isDebug ? E_ALL : -1);
// Setting up external commands
define('LATEX_COMMAND', TEX_PATH . 'latex -output-directory=' . TMP_DIR);
define('DVISVG_COMMAND', TEX_PATH . 'dvisvgm %1$s -o %1$s.svg -n --exact -v0 --relative --zoom=' . OUTER_SCALE);
// define('DVIPNG_COMMAND', TEX_PATH . 'dvipng -T tight %1$s -o %1$s.png -D ' . (96 * OUTER_SCALE)); // outdated
define('SVG2PNG_COMMAND', 'rsvg-convert %1$s -d 96 -p 96 -b white'); // stdout
function error400($error = 'Invalid formula')
{
header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request');
include '400.php';
}
//ignore_user_abort();
ini_set('max_execution_time', 10);
header('X-Powered-By: Upmath Latex Renderer');
$templater = new Templater(TPL_DIR);
$pngConverter = new PngConverter(SVG2PNG_COMMAND);
$renderer = new Renderer($templater, TMP_DIR, TEX_PATH, LATEX_COMMAND, DVISVG_COMMAND);
$renderer
->setPngConverter($pngConverter)
->setIsDebug($isDebug);
if (defined('LOG_DIR')) {
$renderer->setLogger(new Logger(LOG_DIR));
}
$cacheProvider = new CacheProvider(CACHE_SUCCESS_DIR, CACHE_FAIL_DIR);
$processor = new Processor($renderer, $cacheProvider, $pngConverter);
try {
// IMPORTANT: use full REQUEST_URI so Request can see query (?c=...)
$request = Request::createFromUri($_SERVER['REQUEST_URI']);
} catch (Exception $e) {
error400($isDebug ? $e->getMessage() : 'Invalid formula');
die;
}
$response = $processor->process($request);
if (!$response->hasError()) {
$response->echoContent();
} else {
error400($isDebug ? $response->getError() : 'Invalid formula');
}
if (!$isDebug && !($response instanceof CachedResponse)) {
// Disconnecting from web-server
flush();
fastcgi_finish_request();
$postProc = new PostProcessor($cacheProvider);
$asyncRequest = $postProc->cacheResponseAndGetAsyncRequest($response, $_SERVER['HTTP_REFERER'] ?? 'no referer');
if ($asyncRequest !== null) {
$connection = new UnixDomainSocket(FASTCGI_SOCKET, 1000, 1000);
$client = new Client();
// IMPORTANT: pass color param to async processor too
$content = http_build_query([
'formula' => $asyncRequest->getFormula(),
'extension' => $asyncRequest->getExtension(),
'c' => $_GET['c'] ?? ($_GET['color'] ?? ''),
]);
$request = new PostRequest(realpath('../cache_processor.php'), $content);
$client->sendAsyncRequest($connection, $request);
}
}
(3) The asynchronous cache processor must also receive the variant; otherwise the asynchronous optimization process will write to the wrong cache.
Modify cache_processor.php as follows:
php<?php
/**
* Entry point for async cache optimizer.
*
* @copyright 2020-2022 Roman Parpalak
* @license http://www.opensource.org/licenses/mit-license.php MIT
* @package Upmath Latex Renderer
* @link https://i.upmath.me
*/
require 'vendor/autoload.php';
require 'config.php';
// Fallback commands, now HTTP service is used.
define('SVGO', realpath(SVGO_PATH) . '/svgo -i %1$s -o %1$s.new; rm %1$s; mv %1$s.new %1$s');
define('GZIP', 'gzip -cn6 %1$s > %1$s.gz.new; rm %1$s.gz; mv %1$s.gz.new %1$s');
// outdated, disabled due to SVG is now well-supported in browsers.
define('OPTIPNG', 'optipng %1$s');
define('PNGOUT', 'pngout %1$s');
use S2\Tex\Cache\CacheProvider;
use S2\Tex\Processor\DelayedProcessor;
use S2\Tex\Processor\Request;
$delayedProcessor = new DelayedProcessor(
new CacheProvider(CACHE_SUCCESS_DIR, CACHE_FAIL_DIR),
'http://localhost:' . (defined('HTTP_SVGO_PORT') ? HTTP_SVGO_PORT : '8800') . '/'
);
$delayedProcessor
->addSVGCommand(SVGO)
->addSVGCommand(GZIP)
// ->addPNGCommand(OPTIPNG)
// ->addPNGCommand(PNGOUT)
;
$formula = $_POST['formula'] ?? '';
$ext = $_POST['extension'] ?? 'svg';
// IMPORTANT: same variant logic as render.php/request parsing
$variant = Request::buildVariantFromParams($_POST);
$request = new Request($formula, $ext, $variant);
$delayedProcessor->process($request);
Run the following command on the server:
bashdocker run -d --name upmath -p 8080:80 ghcr.io/x2m7/i.upmath.me:latest
Verify locally:
bashcurl -I http://127.0.0.1:8080
If you receive a normal response such as 200 or 302, the internal service is running successfully.
bashsudo apt update sudo apt install -y \ php-fpm php-curl php-xml php-gd \ nodejs npm yarn \ texlive-full ghostscript dvisvgm \ librsvg2-bin \ nginx-extras
bashgit clone https://github.com/X2M7/i.upmath.me.git
cd i.upmath.me
bash# PHP dependencies
composer install --no-dev
# Frontend dependencies + build
yarn install
npx grunt
yarn install --prod
config.phpbashcp config.php.dist config.php
nano config.php
Check or modify the following settings carefully:
TEX_PATH: the directory containing the TeX executables, for example /usr/bin/ or TeX Live's .../bin/x86_64-linux/.TMP_DIR, CACHE_SUCCESS_DIR, and CACHE_FAIL_DIR: these directories must exist and must be readable and writable by nginx/php-fpm.It is recommended to create the directories and configure their permissions.
For example, if the project is located at /var/www/i.upmath.me:
bashsudo mkdir -p cache tmp
sudo chown -R www-data:www-data cache tmp
Create an internal-site configuration file:
bashsudo cp nginx.conf.dist /etc/nginx/sites-available/i.upmath.internal
sudo nano /etc/nginx/sites-available/i.upmath.internal
Make the following two necessary modifications according to your actual environment.
listen to a localhost-only port, for example:nginxlisten 127.0.0.1:8080;
root to the project's www directory, for example:nginxroot /path/to/i.upmath.me/www;
Enable the site and reload Nginx:
bashsudo ln -s /etc/nginx/sites-available/i.upmath.internal /etc/nginx/sites-enabled/i.upmath.internal
sudo nginx -t
sudo systemctl reload nginx
bashsudo cp http-svgo.service.dist /etc/systemd/system/http-svgo.service
sudo sed -i "s~@@DIR@@~$PWD~g" /etc/systemd/system/http-svgo.service
sudo systemctl daemon-reload
sudo systemctl enable --now http-svgo
bashcurl -I http://127.0.0.1:8080
If you receive a normal response such as 200 or 302, the internal service is running successfully.
Open the website of your domain registrar and add two DNS records for <your domain>:
| Host Record | Record Type | Routing Type | Record Value | TTL |
|---|---|---|---|---|
| www | A | Default | <your IP address> | 600 |
| @ | A | Default | <your IP address> | 600 |
Edit the default file under /etc/nginx/sites-available/:
bashnano /etc/nginx/sites-available/default
Add the following to the end of the file:
nginxserver { server_name <your domain> www.<your domain>; # Reverse proxy all requests to the internal service at <your IP address>:8080 location / { proxy_pass http://<your IP address>:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/<your domain>/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/<your domain>/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server { if ($host = www.<your domain>) { return 301 https://$host$request_uri; } # managed by Certbot if ($host = <your domain>) { return 301 https://$host$request_uri; } # managed by Certbot listen 80; server_name <your domain> www.<your domain>; return 404; # managed by Certbot }
Make sure to replace every occurrence of
text<your IP address>
and
text<your domain>
with the IPv4 address of your VPS and your actual domain name.
Test the Nginx configuration:
Terminalsudo nginx -t
Make sure there are no errors, then reload Nginx:
Terminalsudo systemctl reload nginx
To enable HTTPS, install Certbot:
Terminalsudo apt install certbot python3-certbot-nginx -y
Then request a certificate and let Certbot automatically modify the Nginx configuration:
Terminalsudo certbot --nginx -d <your domain> -d www.<your domain>
After completion, Nginx will automatically generate the configuration for port 443, and HTTP requests will automatically redirect to HTTPS.
You can now access your deployed Upmath service through:
text<your domain>
It also supports Chinese.
Done!
You are also welcome to use the Upmath service that I currently host: