XML文件本质上是带标记的纯文本,它只负责描述数据的层级和字段含义;而PDF是页面坐标固定的版式文档,关注的是文字在纸张上的位置、字体、分页和嵌入资源。两者之间没有直接另存为的通道,所谓转换,实际上是以XML作为数据源,经过解析、套用模板、计算布局、嵌入字体这几个步骤,最终生成PDF对象。

一、先理解XML到PDF的转换链路
XML关注的是节点树,例如一个简单的发票文件可能包含<invoice>、<amount>、<buyer>等元素,这些元素本身不会告诉渲染器应该使用多大的字号、是否分页、标题是否居中。PDF则完全不同,它需要明确知道每个字符放在哪个坐标、每页包含哪些内容、字体文件是否已经嵌入。因此,直接修改文件后缀名或者用记事本打开后另存为PDF,都只能得到一个无法阅读的文件。
常见的转换链路可以分成模板渲染和程序绘制两大类。模板渲染包括XSL-FO和HTML加CSS打印样式,核心思路是先定义一套版式规则,再把XML节点映射到规则中;程序绘制则是在代码里解析XML,然后用图形库逐个创建段落、表格和线条。选择哪条路线,主要取决于XML结构是否稳定、页面样式是否复杂,以及团队是否具备维护XSLT或CSS模板的能力。
还有一个容易被忽略的问题是字体。XML里可能存储的是中文、日文或韩文内容,但PDF渲染引擎默认不一定包含对应字体。如果未注册中文字体,输出结果很容易出现空白、方框或者整段丢失。无论是Apache FOP、WeasyPrint还是ReportLab,都需要在生成前配置字体路径和字体族,确保中文能正常嵌入PDF。
二、使用XSL-FO与Apache FOP完成标准化转换
XSL-FO方案适合格式固定、数据量大的文档,例如发票、合同、报告和账单。它的工作方式分成两步:先用XSLT样式表把XML转换为XSL-FO文档,再由Apache FOP等格式化引擎读取FO文件并渲染为PDF。XSLT负责数据筛选和结构重组,例如把<item>列表转换成表格行;XSL-FO则负责精确的版式定义,包括页面大小、边距、页眉页脚和分页条件。
以下是一个简单的XML数据文件,记录了一条账单信息:
<?xml version="1.0" encoding="UTF-8"?> <invoice> <title>测试服务账单</title> <amount currency="CNY">1280.00</amount> </invoice>
针对这份XML,可以编写一个XSLT模板,把<invoice>中的内容映射为FO块对象:
<?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>
<fo:layout-master-set>
<fo:simple-page-master master-name="A4" page-width="210mm" page-height="297mm">
<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">
<xsl:value-of select="invoice/title"/>
</fo:block>
<fo:block>金额:<xsl:value-of select="invoice/amount"/></fo:block>
</fo:flow>
</fo:page-sequence>
</fo:root>
</xsl:template>
</xsl:stylesheet>
准备好两个文件后,可以通过命令行调用FOP完成输出。FOP基于Java运行,需要提前下载对应的二进制发行包,并确保已经配置了Java环境:
fop -xml data.xml -xsl style.xsl -pdf out.pdf
这种方式的优点是版式控制非常精细,适合长期批量生成的场景。一旦模板稳定,只需要替换XML数据就能输出风格一致的PDF。缺点是学习曲线较陡,XSLT和XSL-FO的语法都不算直观,调试分页和空白也比较费时。另外,如果XML中包含中文,必须在FOP的字体配置文件中注册中文字体,例如SimSun、Noto Sans CJK等,否则页面可能出现空白字符。
三、借助CSS分页样式与WeasyPrint输出PDF
如果团队更熟悉前端技术,使用CSS打印样式是更容易维护的方案。思路是把XML转换为HTML,或者直接让XML关联CSS,再通过支持分页媒体特性的引擎生成PDF。WeasyPrint是一个Python库,它能够解析HTML和CSS,并按照@page规则输出PDF,支持页边距、页码、页眉页脚等常用打印样式。
下面的示例先用Python的标准库解析XML,拼接出HTML内容,再交给WeasyPrint渲染。为了方便说明,这里仍然使用上一节的data.xml文件:
import xml.etree.ElementTree as ET
from weasyprint import HTML
tree = ET.parse('data.xml')
root = tree.getroot()
title = root.findtext('title')
amount = root.findtext('amount')
html_content = (
'<html><head><style>'
'@page { size: A4; margin: 2cm; @top-center { content: "测试文档"; } }'
'h1 { font-size: 20px; }'
'</style></head><body>'
f'<h1>{title}</h1>'
f'<p>金额:{amount}</p>'
'</body></html>'
)
HTML(string=html_content).write_pdf('out.pdf')
CSS方案的优势在于样式表达直观,前端开发者可以快速上手。复杂表格、边框、背景色、字体族等都可以用熟悉的CSS属性定义,页码和页眉也能通过@page规则控制。对于需要调整页面边距、纸张方向或者偶数页奇数页不同页眉的文档,CSS分页语法通常比XSL-FO更简洁。
不过,CSS引擎之间的兼容性仍然需要关注。WeasyPrint对CSS Paged Media支持较好,但某些高级属性如多栏布局、脚注、自动断行规则在不同版本的实现中可能表现不同。此外,XML到HTML的转换步骤需要自行处理节点转义和结构映射,如果数据量很大,拼接HTML字符串时要避免插入未经过滤的文本,防止样式或结构被意外破坏。
四、用编程库直接绘制PDF:数据驱动场景更灵活
当XML结构非常不规则,或者需要在PDF中加入图表、自定义表格、动态页数时,直接使用编程库绘制是最灵活的选择。Python的ReportLab和Java的iText都提供了丰富的图形对象,但代码量通常会比模板方案更大。以ReportLab为例,可以先解析XML节点,再通过Platypus布局对象构建段落、表格和分页。
假设有一个联系人XML文件,每个<contact>节点包含姓名和电话,下面这段代码会遍历节点并生成简单的PDF报告:
from reportlab.lib.pagesizes import A4
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet
import xml.etree.ElementTree as ET
doc = SimpleDocTemplate('contacts.pdf', pagesize=A4)
styles = getSampleStyleSheet()
story = []
tree = ET.parse('contacts.xml')
root = tree.getroot()
for contact in root.findall('contact'):
name = contact.findtext('name')
phone = contact.findtext('phone')
story.append(Paragraph(name, styles['Heading2']))
story.append(Paragraph(phone, styles['BodyText']))
story.append(Spacer(1, 12))
doc.build(story)
这种方式的优点是完全不依赖中间格式,所有页面元素都由代码创建,因此可以实现任意复杂的布局。它特别适合需要根据XML内容动态插入表格、图片或改变分页逻辑的自动化流程。缺点也很明显:样式修改往往需要改代码,而不像CSS或XSLT模板那样可以直接调整;同时,编写代码时需要考虑字体注册、段落样式、表格边框、中文换行等一系列细节。
如果XML数据主要用于持久化和交换,而PDF只是最终交付形式,还可以考虑把XML导入数据库或转换为中间对象,再由统一的PDF渲染服务批量输出。这种方式将数据解析和版式渲染解耦,便于后续维护,但系统复杂度会进一步增加。无论采用哪条路径,首先明确XML的数据结构和PDF的版式要求,才能选出维护成本最低的转换方案。