各位好,欢迎来到今天的“硅基排版禅修班”。我是你们的讲师,一个在代码和化学式之间反复横跳的老司机。
今天我们不聊如何把 PHP 写成 Python,也不聊如何用 React 给一个写着“404”的 HTML 装个花哨的轮子。我们聊点硬核的,聊点能让你们在深夜对着满屏乱码的 Word 文档发出灵魂呐喊的东西——工业级文档自动化排版。
具体点说,我们怎么用 PHP 这个 Web 世界里的“万能胶”,去指挥 LaTeX 这个排版界的“老贵族”,批量生产那种看着就让人想把它锁进保险柜的化学品安全技术说明书(SDS)。
准备好了吗?让我们把那台还在转圈的 Word 光标给关了,咱们开启真正的技术流。
第一章:为什么 SDS 这么难搞?
首先,我们得聊聊 SDS(SDS),也就是我们俗称的 MSDS。这玩意儿,就像是你那个总是挑剔、死板、且不允许有一像素误差的前女友。它有 16 个固定的部分:
- 识别:这玩意儿叫啥?编号多少?
- 危害:这玩意儿吃了会死人,还是只是让你秃头?
- 急救:万一你把它溅到了眼睛里,是吐口水还是冲水?这决定了你下半辈子的生活质量。
- 灭火:着火了是用水泼,还是用干粉?
- 泄漏处理:泼洒了怎么办?扫地还是跑路?
- 暴露控制:我该戴防毒面具还是潜水服?
- 物理与化学特性:这玩意儿熔点多少?闪点多少?
- 稳定性:它会不会自己炸了?
- 分解产物:它炸了以后会产生什么剧毒气体?
- 毒理学信息:长期接触会有什么后果?
- 生态学信息:倒了水里,鱼会翻白眼吗?
- 处置:别吃了,扔了。
- 运输:是普通快递,还是得请红十字会来送?
- 法规信息:哪条法律管它?
- 其他信息:填空题。
- 技术说明:编不下去了,随便写点。
这就好比你要写一篇作文,题目固定,每个段落都有字数要求,甚至连标点符号的格式都有严格规定。如果你用 Word 手动排版,搞错一个标点,或者页码跳了一页,那可是要负法律责任的。在工业界,SDS 的排版错误等同于化学事故,因为你给的说明书错了,工人操作错了,事故就发生了。
所以,我们需要自动化。我们需要PHP。
第二章:PHP 与 LaTeX 的“罗曼蒂克史”
为什么不用 PHP 直接生成 PDF?或者用 Apache POI 操作 Word?
因为 Word 是基于“所见即所得”(WYSIWYG)的,它是“有状态”的,排版逻辑混乱,依赖 Office 的渲染引擎。而 PDF(尤其是通过 LaTeX 生成的),它是“所见即所得”的,它是“无状态”的,排版逻辑严谨到了变态的地步。
PHP 在这里扮演什么角色?PHP 是数据搬运工,是排版指挥官。它负责从数据库里把“苯的熔点是 5.5 摄氏度”这个数据抓出来,然后塞进 LaTeX 的模板里。
LaTeX 是什么? 它是排版界的“瑞士军刀”,也是“强迫症患者的福音”。它不关心怎么让字漂亮,它只关心数学公式和结构。你告诉它 Hello World,它可能默认会生成一个巨大的标题,而不是正文。你需要告诉它 section{Hello World},它才会给你一个正经的章节标题。
我们的战术核心:
- PHP:负责业务逻辑,获取化学品数据,清洗数据(比如把单位统一转换),组装 LaTeX 代码字符串。
- LaTeX:负责渲染,把 PHP 给的代码变成漂亮的 PDF。
- Shell Command (CLI):PHP 通过
shell_exec()调用 LaTeX 编译器(如pdflatex或xelatex)。
第三章:构建 SDS 的“骨架”
要生成 SDS,首先得有一个像样的 LaTeX 模板。我们不能从零开始写,那样太痛苦了。我们可以利用 ctex 宏包来处理中文(因为我们的 SDS 很多是中文的),利用 geometry 来控制页边距,利用 hyperref 来生成超链接。
让我们先看看一个基础的 SDS 页面结构。
% 这是一个示例的 SDS 代码片段
documentclass[12pt, a4paper]{article}
% 引入中文支持
usepackage[UTF8]{ctex}
% 设置页面边距,给页眉页脚留点余地
usepackage{geometry}
geometry{left=2.5cm, right=2.5cm, top=2.5cm, bottom=2.5cm}
% GHS 标签配色标准
usepackage{xcolor}
definecolor{g-red}{RGB}{194,0,0}
definecolor{g-yellow}{RGB}{255,204,0}
definecolor{g-blue}{RGB}{0,85,164}
definecolor{g-orange}{RGB}{204,85,0}
% 页眉页脚设置
usepackage{fancyhdr}
pagestyle{fancy}
fancyhf{} % 清空
fancyhead[C]{化学品安全技术说明书} % 页眉中间
fancyfoot[C]{第 thepage 页 / 共 pageref{LastPage} 页} % 页脚中间
begin{document}
% 定义一个 SDS 模板环境,方便我们在 PHP 里替换变量
begin{SDSPage} % 假设我们自定义了一个环境
% 标题区
begin{center}
Huge textbf{textcolor{g-blue}{化学品安全技术说明书}}
vspace{0.5cm}
Large textbf{第 1 部分:化学品及制造商标识}
end{center}
% 16 个部分通常分页显示
newpage
textbf{第 2 部分:危险性概述}
begin{itemize}
item textbf{GHS 分类}:
begin{itemize}
item 急性毒性,经口:类别 4
item 皮肤腐蚀/刺激:类别 2
item 严重眼损伤/眼刺激:类别 1A
end{itemize}
item textbf{信号词}:警告
item textbf{危害防范说明}:
P264 作业后彻底清洗。
P301+P310 如误吞咽:立即呼叫中毒控制中心/医生。
P305+P351+P338 如进入眼睛:用水小心冲洗几分钟;如有不适,就医。
end{itemize}
vspace{1cm}
hrule
vspace{0.5cm}
textbf{第 3 部分:成分/组成信息}
begin{tabular}{|c|c|c|c|}
hline
成分名称 & 化学式 & 含量 & CAS No. \
hline
二甲苯 & C8H10 & 90-99% & 1330-20-7 \
hline
甲苯 & C7H8 & 1-9% & 108-88-3 \
hline
end{tabular}
end{SDSPage}
end{document}
看,这段代码虽然枯燥,但它构建了一个 SDS 的基本框架。所有的变量(比如“二甲苯”的含量)都隐藏在代码的深处,等着 PHP 去填空。
第四章:PHP 的“填空”魔法
现在,我们要把 PHP 代码写出来。想象一下,我们的数据库里有一张表 chemicals,里面存了 10,000 种化学品的数据。
我们要写的不是循环 10,000 次 HTML 标签,而是循环 10,000 次 LaTeX 代码片段。这听起来很蠢,但 LaTeX 编译 PDF 的效率比 PHP 生成 HTML 然后转 PDF 要高得多,且排版质量天差地别。
4.1 数据准备
假设我们有一个简单的数组结构:
$chemicalData = [
'name' => '高纯度异丙醇',
'cas_no' => '67-63-0',
'gus_label' => 'g-red', // 对应 LaTeX 中的定义
'sections' => [
1 => [
'title' => '成分/组成信息',
'content' => '异丙醇 100% (CAS: 67-63-0)'
],
2 => [
'title' => '主要成分/组成信息',
'content' => '纯品,无杂质'
],
// ... 更多部分
]
];
4.2 构建 LaTeX 模板字符串
我们不能直接用 echo,因为那样很难管理结构。我们通常会使用字符串插值,或者模板引擎。但为了演示核心原理,我们用原生 PHP 的 sprintf 和字符串拼接来模拟。
<?php
/**
* SDS Generator Class
* 这就是我们的“老司机”类
*/
class SDSDocumentGenerator {
private $baseTemplate = <<<'EOT'
% 这是一个 SDS 文档模板
documentclass[12pt, a4paper]{article}
usepackage[UTF8]{ctex}
usepackage{geometry}
usepackage{xcolor}
usepackage{fancyhdr}
usepackage{longtable} % 处理跨页表格的利器
geometry{left=2.5cm, right=2.5cm, top=2.5cm, bottom=2.5cm}
% 定义 GHS 颜色
definecolor{g-red}{RGB}{194,0,0}
definecolor{g-yellow}{RGB}{255,204,0}
definecolor{g-blue}{RGB}{0,85,164}
definecolor{g-orange}{RGB}{204,85,0}
pagestyle{fancy}
fancyhead[C]{化学品安全技术说明书 - chemicalName}
fancyfoot[C]{第 thepage 页 / 共 pageref{LastPage} 页}
begin{document}
% 第一部分:标题
begin{center}
Huge textbf{textcolor{gusColor}{化学品安全技术说明书}} \
vspace{0.3cm}
Large textbf{chemicalName} \
large textit{CAS No: casNo}
end{center}
vspace{1cm}
hrule
vspace{0.5cm}
% 开始循环生成 16 个部分
% 这里用 longtable 是为了处理 SDS 中常见的跨页表格
EOT;
public function generate($data) {
$latexContent = $this->baseTemplate;
// 插入变量
$latexContent = str_replace('chemicalName', $data['name'], $latexContent);
$latexContent = str_replace('casNo', $data['cas_no'], $latexContent);
$latexContent = str_replace('gusColor', $data['gus_label'], $latexContent);
// 生成正文内容
$latexContent .= "nnsection{化学品及制造商标识}n";
$latexContent .= "制造商/供应商:[自动化填充]n";
$latexContent .= "地址:[自动化填充]nn";
// 动态生成危害警示部分
$latexContent .= "section{危险性概述}n";
$latexContent .= "该化学品具有以下危害性:n";
$latexContent .= " - 腐蚀性:textbf{是}n";
$latexContent .= " - 易燃性:textbf{是}n";
// 生成 SDS 的 16 个部分(这里简化了逻辑,实际需要复杂的 switch-case)
for ($i = 1; $i <= 16; $i++) {
// 简单模拟,实际项目中这里需要从数据库读取对应章节的内容
$latexContent .= "section{第 {$i} 部分:自动化生成的章节标题}n";
$latexContent .= "这里是关于第 {$i} 部分的详细说明数据,目前处于占位状态。n";
// 如果不是最后一页,插入新页
if ($i < 16) {
$latexContent .= "newpagen";
}
}
$latexContent .= "end{document}";
return $latexContent;
}
}
上面的代码展示了 PHP 如何像乐高积木一样,将数据块塞进 LaTeX 的“骨架”里。
第五章:那个该死的 GHS 标签
SDS 最具辨识度的部分是什么?是那个花花绿绿的 GHS 标签。
在美国,那是“安全页眉”;在中国,那是那个标准的 GHS 标签。这个标签有固定的布局:红色框框、黄色条纹、白底黑字。
在 Word 里,你只能画框。在 LaTeX 里,我们可以用 tikz 绘图包画出完美的几何图形。
5.1 TikZ 绘制 GHS 标签
让我们看一段 PHP 生成的 LaTeX 代码,这段代码专门用来画标签:
% PHP 输出的一段 LaTeX 代码
begin{tikzpicture}
% 定义 GHS 标签区域
fill[g-yellow] (0,0) rectangle (4,3.2);
fill[g-red] (0,0) rectangle (0.5,3.2); % 左侧红色条
fill[g-red] (0,0) rectangle (4,0.5); % 上方红色条
% 绘制警示符号(用 TikZ 画个简单的火焰或骷髅)
% 这里为了演示,画一个简单的三角形
fill[g-red] (2, 1.5) -- (1.2, 0.5) -- (2.8, 0.5) -- cycle;
% 标题文字
node at (2, 2.2) [white, font=bfseriesLarge] {GHS 标签};
node at (2, 1.0) [white, font=small] {GHS 分类:textbf{急性毒性}};
end{tikzpicture}
PHP 的任务是计算这个标签在页面上的位置。通常 SDS 的第一页顶部都有这个标签。
// PHP 逻辑:计算坐标
$labelWidth = 80; // mm
$labelHeight = 105; // mm
$marginLeft = 20; // mm
$marginTop = 15; // mm
$labelLaTeX = sprintf(
"n\begin{tikzpicture}[remember picture, overlay]n".
" \node[anchor=north west, xshift=%dmm, yshift=%dmm] at (current page.north west) {n".
" \begin{tikzpicture}n".
" \fill[g-yellow] (0,0) rectangle (%d,%d);n".
" \fill[g-red] (0,0) rectangle (2,%d);n".
" \node[white] at (%d/2, %d/2) {SDS 标签};n".
" \end{tikzpicture}n".
" };n".
"\end{tikzpicture}",
$marginLeft,
$marginTop,
$labelWidth,
$labelHeight,
$labelWidth,
$labelHeight
);
哇,这代码写得是不是特别有“强迫症”的感觉?每一毫米的偏移都精确计算。这就是 PHP 结合 LaTeX 的威力——绝对的精准。
第六章:处理复杂的表格与分页
SDS 中有一个部分叫“成分/组成信息”,里面全是表格。而且这些表格在 PDF 中很容易断页,如果表格标题在上一页,表格内容在下一页,那是排版大忌。
我们需要用到 longtable 包。
begin{longtable}{|l|l|l|l|}
hline
textbf{成分名称} & textbf{化学式} & textbf{含量 (%)} & textbf{CAS No.} \
hline
endhead % 表头在每页都显示
endhead % 表尾在最后一页显示
endfoot
hline
end{tabular}
PHP 在处理表格数据时,需要构建这个环境。如果你用的是 MVC 架构,通常会有一个专门的 Service 层来处理这种复杂的字符串拼接。
public function buildTable($rows) {
$tableStart = "
begin{longtable}{|l|l|l|l|}
hline
textbf{成分名称} & textbf{化学式} & textbf{含量 (%)} & textbf{CAS No.} \
hline
endhead % 表头
hline
endfoot % 表尾
hline
end{tabular}
";
$tableRows = "";
foreach ($rows as $row) {
$tableRows .= sprintf(
"hlinen%s & %s & %s & %s \n",
$row['name'],
$row['formula'],
$row['percentage'],
$row['cas']
);
}
$tableRows .= "hlinenend{longtable}";
return $tableStart . $tableRows;
}
这就像是 PHP 在编织一张渔网,LaTeX 会在 PDF 里把这张网拉得整整齐齐,绝对不会乱。
第七章:编译与错误处理
好了,代码生成完了。现在我们要用 PHP 去编译它。我们在 Linux 服务器上运行 PHP,而 LaTeX 通常安装为命令行工具。
$command = "xelatex -interaction=nonstopmode SDS_temp.tex";
$output = shell_exec($command);
这里有一个坑。LaTeX 编译通常需要运行两次才能生成正确的交叉引用(比如页码和目录)。
function compileLatex($texFile) {
// 第一次编译
$cmd = "xelatex -interaction=nonstopmode $texFile 2>&1";
shell_exec($cmd);
// 第二次编译(处理引用)
$cmd = "xelatex -interaction=nonstopmode $texFile 2>&1";
shell_exec($cmd);
// 生成 PDF
$pdfFile = str_replace('.tex', '.pdf', $texFile);
if (file_exists($pdfFile)) {
return true;
}
return false;
}
警告: shell_exec 在生产环境中可能会有安全风险(命令注入)。在生产环境里,你应该使用 PHP 的 exec() 函数,或者更好的,使用 PHP 扩展(如 php-latex 库)直接在 PHP 进程中调用 LaTeX 的 C 库,或者通过一个安全的封装 API 来调用。
但为了理解原理,我们这里用 shell_exec。
第八章:异常处理与日志记录
如果 PHP 生成的 LaTeX 代码里有语法错误,编译就会失败,PDF 就会生成失败,你的系统就会报错。但 LaTeX 的错误信息通常是英文的,甚至很晦涩。如果工人拿着一个 404 的 PDF 来投诉,你就完蛋了。
我们需要一个日志系统。
try {
$texContent = $generator->generate($chemicalData);
file_put_contents('/tmp/SDS_' . $chemicalData['id'] . '.tex', $texContent);
if (compileLatex('/tmp/SDS_' . $chemicalData['id'] . '.tex')) {
echo "PDF 生成成功: " . $chemicalData['id'] . ".pdf";
// 将 PDF 保存到文件系统或发送到用户邮箱
} else {
throw new Exception("LaTeX 编译失败,请检查日志");
}
} catch (Exception $e) {
error_log("SDS 生成失败: " . $e->getMessage());
// 发送邮件给管理员,或者返回给前端一个友好的错误提示
echo "生成失败,请联系管理员";
}
第九章:进阶技巧——自动排版页眉
SDS 通常要求每一页都有页眉和页脚。在 LaTeX 中,我们可以用 fancyhdr 来处理。
usepackage{fancyhdr}
pagestyle{fancy}
fancyhf{} % 清空所有页眉页脚
fancyhead[C]{化学品安全技术说明书 - chemicalName} % 页眉居中
fancyfoot[C]{第 thepage 页 / 共 pageref{LastPage} 页} % 页脚居中
在 PHP 中,我们只需要在模板字符串中包含 usepackage{fancyhdr} 即可。如果你有多人协作,不同的员工负责不同的章节,你可以用 pagenumbering{gobble} 来控制某个特定章节不显示页码,或者用 newpage 强制换页。
第十章:终极奥义——版本控制
这是一个有趣的点。LaTeX 文件本质上是文本文件。你完全可以把你的 SDS 模板文件(.tex)放入 Git 仓库!
这意味着什么?意味着你可以追踪 SDS 版本的变化。如果你修改了第 3 部分(成分信息)的定义,Git 会记录下是谁改的,什么时候改的,改了什么。
这在工业界意味着什么?意味着你可以对历史版本进行“回滚”。如果客户投诉现在的 SDS 有毒副作用写得不够详细,你可以直接回滚到上周的版本,并附上修改日志。
这是 Word 文档永远做不到的。
结语(不,不是总结)
好了,各位。我们从枯燥的 16 个部分聊到了如何用 PHP 像变魔术一样生成它们。
不要再用 Word 去折磨你的眼睛了。利用 PHP 的逻辑处理能力和 LaTeX 的排版美学,你可以建立一个高效的文档工厂。你的系统可以每天自动生成成千上万份 SDS,而且每一份都像是经过顶级排版师精心设计过的艺术品。
现在,去写代码吧。让你的 LaTeX 引擎轰鸣起来。