Upmath是俄罗斯粒子物理学家Dr. Роман Парпалак主导完成的一个开源前端项目——一个基于Markdown+LaTeX的在线编辑器,见它的项目Github主页。它可以实现在Web上显示复杂的数学公式及图形(比如LaTeX Tikz所画的图)的网页内容。
它是将Markdown文本与LaTeX数学表达式转换成HTML页面,并嵌入公式图像(SVG格式)。
| 项目组件 | 技术栈与职责 |
|---|---|
前端 (upmath.me) | JavaScript + HTML/CSS 前端编辑器,处理 Markdown + LaTeX 转 HTML,并嵌入公式图像。使用 grunt 构建流程。 |
后端渲染服务 (i.upmath.me) | PHP + TeX Live + nginx + Node.js + Grunt + SVG 工具链:渲染公式为 SVG,并提供 API 服务给前端调用。 |
| 图像渲染方式 | 使用 TeX Live 和工具链(如 dvisvgm)将 LaTeX 公式渲染为 SVG 矢量图,支持复杂图形(如 TikZ)。(i.upmath.me) |
用户在前端编辑器中输入 Markdown 文本及 LaTeX 公式。
编辑器将 Markdown 转为 HTML。LaTeX 公式部分则使用 的方式,调用后端渲染服务。
后端服务渲染 LaTeX 为 SVG 图片,返回给前端展示。
最终用户在浏览器中看到的是 HTML 页面嵌入的 SVG 数学公式,可复制、分享或发布。
双模块架构:清晰分离编辑与渲染职责,前端专注编辑与呈现,后端专注渲染服务。
技术栈丰富:前端为 JavaScript + Grunt 构建,后端融合 TeX、PHP、SVG 渲染工具,也支持 Docker 部署,便于复现环境。
灵活应用:可嵌入到博客、论坛等任何支持 HTML 的平台,只需贴入一段脚本即可动态渲染数学内容。
在VPS购买的网站购买一个按需要求的VPS,并且获得了公网IPv4:<你的IP地址>,我们这里使用的Debian 12发行版。 需要
在Debian上我们运行
Terminalsudo apt update sudo apt install -y nginx git curl ca-certificates gnupg lsb-release
添加 Docker 官方 GPG key
Terminalsudo 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
添加 Docker 官方 apt 源
Terminalecho \ "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
安装 Docker 引擎
Terminalsudo apt update sudo apt install -y docker-ce docker-ce-cli containerd.io
测试
Terminalsudo docker run hello-world
这样我们就准备好了基本的VPS环境,为了实现后面的域名访问,可以在一些域名购买网站上购买一个域名:<你的域名>。
由于我已经Fork一个修改好的项目,可以跳过!!!
Fork项目在:
https://github.com/X2M7/i.upmath.me
我们之所以选择源码部署,而不直接选择官方Docker部署的原因是因为我们需要在编辑器有使用中文的需求以及对暗色模式的需求,需要对源代码进行一定改动。下面是在原来代码基础上的修改内容,仅作历史保存:
首先,我们从Github下载源码
Terminalgit clone https://github.com/parpalak/upmath.me.git
并进入源码所在的文件夹
Terminalcd i.upmath.me
进入i.upmath.me项目源码文件夹下的tpl文件夹
Terminalcd tpl
tpl文件夹里面有三个文件,可以通过ls查看
Terminalls
我们需要更改document.php这个文件,选择你喜欢的文本编辑器来编辑document.php
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
% 注意我们添加了这两行
\usepackage[utf8]{inputenc}
\usepackage{CJKutf8}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{stmaryrd}
\newcommand{\R}{\mathbb{R}}
\newcommand{\lt}{<}
\newcommand{\gt}{>}
\usepackage{esint} % 新加对oiint的支持
% 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} % 注意我们添加了这一行
<?php
foreach (['newwrite', 'openout'] as $disabledCommand) {
echo '\\renewcommand{\\' . $disabledCommand . '}{\\errmessage{Command \\noexpand\\' . $disabledCommand . ' is disabled}}', "\n";
}
?>
<?php echo $documentContent; ?>
\end{CJK}% 注意我们添加了这一行
\end{document}
请注意在原来代码中,我们添加了4行,为了尽量减少修改,我们仍然使用pdfLaTeX编译,因此,我们增加了对CJK包的支持
... \usepackage[utf8]{inputenc} \usepackage{CJKutf8} ...
注:如果你需要更多LaTeX的包的支持,就可以在这里的后面添加\usepackage{<你需要的LaTeX包>}
并在\begin{document}和\end{document}之内,添加了CJK的使用,UTF8是文本格式,而gbsn是宋体(你可以按需更改)
... \begin{document} \begin{CJK}{UTF8}{gbsn} % 注意我们添加了这一行 ... \end{CJK}% 注意我们添加了这一行 \end{document}
保存并退出,这样我们就完成了这个包的修改。回到上一级i.upmath.me/
Terminalcd ..
为了保证有中文字体的支持,我们在生成Docker容器前写一个Dockerfile,使用你喜欢的编辑器
Terminalnano Dockerfile
并在其中输入
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" ]
保存并退出即可。
为了实现“默认不变+?c=暗色改色+不串缓存”,你一共需要改动这些文件:
(1) 颜色改写(只在带?c=时生效)
(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) 让Request能拿到query(否则 variant 永远为空),修改www/render.php如下:
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) 异步缓存处理也必须带上 variant(否则异步优化会写错缓存),修改cache_processor.php如下:
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.gz; rm %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);
在服务器终端运行
bashdocker run -d --name upmath -p 8080:80 ghcr.io/x2m7/i.upmath.me:latest
bashgit clone https://github.com/X2M7/i.upmath.me.git
cd i.upmath.me
docker build -t i-upmath-me:local .
docker run --rm -p 8080:80 i-upmath-me:local
然后打开确定容器顺利运行:
目标:部署完成后,你能在服务器本机访问
http://127.0.0.1:8080
proc_open() 可用)librsvg2-bin 用于 SVG → PNG(你若只用 SVG 可不装)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
# PHP 依赖
composer install --no-dev
# 前端构建
yarn install
npx grunt
yarn install --prod
config.phpbashcp config.php.dist config.php
nano config.php
你通常需要确认/修改这些:
TEX_PATH:TeX 可执行文件路径(比如 /usr/bin/ 或 TeXLive 的 bin 目录)TMP_DIR、CACHE_SUCCESS_DIR、CACHE_FAIL_DIR:确保 nginx/php-fpm 有读写权限(权限建议做法:把 cache/tmp 目录 owner 给运行 php-fpm 的用户/组,或给可写权限)
这里建议你单独建一个“内部站点”配置,例如:
bashsudo cp nginx.conf.dist /etc/nginx/sites-available/i.upmath.internal
sudo nano /etc/nginx/sites-available/i.upmath.internal
把 server 段改成类似这种“内部服务”形态(核心点:listen 127.0.0.1:8080;):
nginxserver { listen 127.0.0.1:8080; server_name _; root /path/to/i.upmath.me/www; index index.php; # 其余 location/php/lua/静态缓存规则 # 建议直接沿用项目自带 nginx.conf.dist 的内容, # 这里只改 listen/root 等与路径相关的部分即可。 include /etc/nginx/snippets/fastcgi-php.conf; }
启用并重载:
bashsudo ln -s /etc/nginx/sites-available/i.upmath.internal /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
注意:上面
root请换成你实际的项目路径,比如/var/www/i.upmath.me/www。
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
能返回 200/302 等正常响应即代表“内部服务已起来”。
打开购买域名的网站,在<你的域名>中添加两条域名解析
| 主机记录 | 记录类型 | 线路类型 | 记录值 | TTL |
|---|---|---|---|---|
| www | A | 默认 | <你的IP地址> | 600 |
| @ | A | 默认 | <你的IP地址> | 600 |
编辑/etc/nginx/sites-available/下的default文件
Terminalnano /etc/nginx/sites-available/default
在default最后添加
server { server_name <你的域名> www.<你的域名>; # 所有请求反代到内部服务 <你的IP地址>:8080 location / { proxy_pass http://<你的IP地址>: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/<你的域名>/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/<你的域名>/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.<你的域名>) { return 301 https://$host$request_uri; } # managed by Certbot if ($host = <你的域名>) { return 301 https://$host$request_uri; } # managed by Certbot listen 80; server_name <你的域名> www.<你的域名>; return 404; # managed by Certbot }
注意替换所有的<你的IP地址>和<你的域名>为你所使用VPS的IPv4地址和你的域名。
测试Nginx配置
Terminalsudo nginx -t
确保没有报错,那么重启Nginx
Terminalsudo systemctl reload nginx
为了开启HTTPS,我们需要安装Certbot
Terminalsudo apt install certbot python3-certbot-nginx -y
并且申请证书并自动修改Nginx
Terminalsudo certbot --nginx -d <你的域名> -d www.<你的域名>
完成后, Nginx会自动生成443端口的配置,HTTP自动跳转到HTTPS。
这样你就可以通过<你的域名>访问搭建好的Upmath服务了!它还支持中文!大功告成!
当然也欢迎使用我目前搭建好的Upmath服务:https://xumin.net