XML作为数据交换的标准格式,广泛应用在报表系统、电子发票、配置管理等场景中。但XML本身是结构化数据,直接阅读体验很差,把它转换成排版良好的PDF文档,是很多业务系统绕不开的需求。本文将从原理、工具、代码实现三个层面,详细讲解XML转PDF的几种主流方案。

一、为什么XML不能直接变成PDF
首先要理解一个概念:XML和PDF是两种完全不同性质的东西。XML是描述数据结构的标记语言,它只管数据是什么、层级关系如何,完全不管数据长什么样子。而PDF是版式文档,每一个字的位置、字体、大小、颜色都精确固定。所以两者之间不存在一键直转的可能,中间必须有一个渲染层来决定排版规则。
这个渲染层通常有两种实现思路:第一种是声明式,写一份样式文件告诉引擎每个XML节点应该渲染成什么样子,典型代表是XSL-FO;第二种是编程式,用代码解析XML,再把内容逐个写入PDF页面,典型代表是各类PDF生成库。两种思路各有优劣,声明式适合格式相对固定的报表类文档,编程式适合内容结构多变、需要动态计算的场景。
二、使用XSL-FO与Apache FOP转换
XSL-FO(Formatting Objects)是W3C制定的排版语言,专门用于描述文档的版面结构。Apache FOP是它的开源实现,整个转换流程分两步:先用XSLT把XML转换成XSL-FO中间格式,再由FOP引擎把XSL-FO渲染成PDF。这种方式的最大好处是排版逻辑和数据完全分离,改样式不用动程序代码。
假设有如下简单的XML订单数据:
<order>
<customer>张三</customer>
<items>
<item>
<name>机械键盘</name>
<price>399.00</price>
</item>
<item>
<name>无线鼠标</name>
<price>129.00</price>
</item>
</items>
</order>对应的XSL-FO样式文件可以这样写,核心是使用模板匹配把每个item渲染成表格行:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:template match="/">
<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
<fo:layout-master-set>
<fo:simple-page-master master-name="A4">
<fo:region-body margin="2cm"/>
</fo:simple-page-master>
</fo:layout-master-set>
<fo:page-sequence master-reference="A4">
<fo:flow flow-name="xsl-region-body">
<fo:block font-size="16pt" font-weight="bold">订单明细</fo:block>
<fo:table>
<fo:table-body>
<xsl:for-each select="order/items/item">
<fo:table-row>
<fo:table-cell>
<fo:block><xsl:value-of select="name"/></fo:block>
</fo:table-cell>
<fo:table-cell>
<fo:block><xsl:value-of select="price"/></fo:block>
</fo:table-cell>
</fo:table-row>
</xsl:for-each>
</fo:table-body>
</fo:table>
</fo:flow>
</fo:page-sequence>
</fo:root>
</xsl:template>
</xsl:stylesheet>使用Java调用FOP生成PDF的代码很短:
FopFactory fopFactory = FopFactory.newInstance(new File(".").toURI());
FOUserAgent userAgent = fopFactory.newFOUserAgent();
// 输出到PDF文件
OutputStream out = new BufferedOutputStream(new FileOutputStream("order.pdf"));
Fop fop = fopFactory.newFop(MimeConstants.MIME_PDF, userAgent, out);
// 执行XSLT转换
TransformerFactory factory = TransformerFactory.newInstance();
Transformer transformer = factory.newTransformer(new StreamSource(new File("style.xsl")));
Source src = new StreamSource(new File("order.xml"));
Result res = new SAXResult(fop.getDefaultHandler());
transformer.transform(src, res);
out.close();需要注意的是中文显示问题。FOP默认字体不支持中文,需要在配置文件fop.xconf中注册中文字体,比如指定操作系统里的simsun.ttc或开源的思源黑体,否则生成的PDF里中文会变成一串井号。这是FOP新手最容易踩的坑。
三、编程式方案:用iText或PDFBox直接生成
如果文档结构比较灵活,比如同一份数据要根据不同参数生成不同版式,编程式方案会更顺手。思路是先用DOM或SAX解析XML,再把数据写入PDF文档对象。以Java生态中最常用的iText为例:
// 解析XML
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
Document doc = dbf.newDocumentBuilder().parse(new File("order.xml"));
doc.getDocumentElement().normalize();
// 创建PDF
PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream("order.pdf"));
Document document = new Document(PageSize.A4);
document.open();
// 设置中文字体
BaseFont baseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
Font font = new Font(baseFont, 12);
// 读取节点写入PDF
NodeList items = doc.getElementsByTagName("item");
for (int i = 0; i < items.getLength(); i++) {
Element item = (Element) items.item(i);
String name = item.getElementsByTagName("name").item(0).getTextContent();
String price = item.getElementsByTagName("price").item(0).getTextContent();
document.add(new Paragraph("商品:" + name + " 价格:" + price, font));
}
document.close();注意上面代码中Document对象的创建顺序,iText要求先通过PdfWriter.getInstance绑定输出流,再调用document.open(),顺序颠倒会抛出异常。中文字体部分使用iText自带的STSong-Light字体配合Unicode编码映射,能正确输出简体中文。
Python生态同样有成熟方案,reportlab配合xml.etree.ElementTree是常见组合。reportlab的Platypus模块提供了段落、表格、分页等高级排版对象,写起来和拼积木一样直观:
from reportlab.lib.pagesizes import A4
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
from reportlab.platypus import SimpleDocTemplate, Paragraph, Table
from reportlab.lib.styles import ParagraphStyle
import xml.etree.ElementTree as ET
# 注册中文字体
pdfmetrics.registerFont(UnicodeCIDFont('STSong-Light'))
tree = ET.parse('order.xml')
root = tree.getroot()
style = ParagraphStyle(name='cn', fontName='STSong-Light', fontSize=12)
doc = SimpleDocTemplate('order.pdf', pagesize=A4)
story = [Paragraph('订单明细', style)]
data = [['商品', '价格']]
for item in root.findall('items/item'):
name = item.find('name').text
price = item.find('price').text
data.append([name, price])
story.append(Table(data))
doc.build(story)编程式方案的优点是控制粒度细,可以随时插入图表、二维码、水印等元素;缺点是排版逻辑耦合在业务代码里,格式调整需要改代码重新部署。如果团队有专职排版需求,还是建议优先考虑样式分离的方案。
四、其他方案与选型建议
除了上面两种主流路线,还有一些轻量选择。一是借助中间格式,先把XML通过XSLT转成HTML,再用wkhtmltopdf、Headless Chrome等工具把HTML渲染成PDF,这种方式学习成本最低,CSS排版能力强,特别适合从XML生成对排版美观度要求高的报告。二是各类在线转换工具,适合一次性、非敏感数据的临时转换,上传文件就能下载结果,但涉及企业内部数据时不建议使用,存在泄露风险。
选型时可以参考下面几条标准:文档格式是否固定且需要频繁改版,是则选XSL-FO;是否需要复杂计算和动态内容,是则选编程式库;是否需要高度还原网页样式,是则走HTML中转;数据量方面,FOP在处理数万条记录时可能出现内存压力,需要开启流式处理或分批生成,而iText的flush()机制对大文档更友好。
五、常见问题排查
中文乱码是最常见的问题,根源几乎都是PDF生成时没有注册支持中文的字体,解决办法是在工具配置中显式指定中文字体文件。其次是样式丢失,通常是XSLT模板匹配路径写错导致的,可以用浏览器的XSLT调试功能或单独执行XSLT转成文本查看中间结果来定位。最后是特殊字符问题,XML中包含&、<这类字符时必须先转义,否则解析阶段就会报错,生成PDF前做一次字符校验可以省去很多麻烦。
总体来说,XML转PDF没有唯一标准答案,理解渲染层这个核心概念后,根据团队技术栈和文档特点选择对应工具即可。建议先用小样例验证字体和排版效果,再接入完整业务数据,能显著降低返工成本。