
本文详解如何利用 XSLT 2/3 实现对中间 FO 文档的二次处理,在 PDF 生成前精准定位 ^1、^2 等标记并将其对应脚注内容注入所在页面的页脚,无需修改原始 XML 或破坏原有排版结构。
本文详解如何利用 xslt 2/3 实现对中间 fo 文档的二次处理,在 pdf 生成前精准定位 `^1`、`^2` 等标记并将其对应脚注内容注入所在页面的页脚,无需修改原始 xml 或破坏原有排版结构。
在基于 Apache FOP 的 PDF 生成流程中,直接在首次 XSL-FO 转换后“按页动态插入脚注”是一个典型但极具挑战性的需求——因为 FO 是静态格式,不保留源语义位置信息,且 FOP 本身不支持运行时页码感知的后处理。正确解法不是在 FO 生成后操作 PDF,而是在 FO 构建阶段完成脚注的语义绑定与位置映射。核心思路是:将脚注注入逻辑从“第二遍处理 FO”转向“第一遍生成 FO 时即嵌入可定位的脚注占位符”,再借助 XSLT 多阶段流水线(而非 FOP 后处理)实现精准控制。
✅ 推荐架构:单流程、双模式 XSLT 2/3(推荐 Saxon HE)
Apache FOP 原生仅支持 XSLT 1.0,但可通过替换 Transformer 为 Saxon HE(v10+) 完全兼容 XSLT 2/3,并天然支持变量中保存节点树、多模式匹配、函数式流水线等关键能力。这使得“先生成带标记的 FO → 再重写脚注结构”成为简洁可靠的方案。
第一步:在初始 XSLT 中标记脚注位置(保持语义)
不要等待中间 FO 文件;而应在首次转换中,将 ^1、^2 等文本识别为内联标记,并包裹为具有唯一 ID 和引用关系的 FO 元素:
Apache Superset 是一个广泛采用的开源 BI 平台,用于 SQL 探索、图表构建和仪表板交付。当代理需要查询仓库数据、组装仪表板或使用成熟的分析界面解释指标而不是临时笔记本代码时,此技能非常有用。
<!-- 在主样式表中定义脚注内联模板 -->
<xsl:template match="text()[contains(., '^')]" mode="process-text">
<xsl:analyze-string select="." regex="\^(\d+)">
<xsl:matching-substring>
<!-- 生成可定位的 footnote-ref -->
<fo:inline id="fnref-{regex-group(1)}" font-size="8pt" baseline-shift="super" color="#0066cc">
<xsl:value-of select="regex-group(0)"/>
</fo:inline>
<!-- 隐式关联脚注内容(不立即渲染) -->
<xsl:variable name="footnote-id" select="regex-group(1)" />
<xsl:if test="//footnotes/f{$footnote-id}">
<fo:wrapper id="fn-{regex-group(1)}" role="footnote-placeholder" />
</xsl:if>
</xsl:matching-substring>
<xsl:non-matching-substring>
<xsl:value-of select="."/>
</xsl:non-matching-substring>
</xsl:analyze-string>
</xsl:template>⚠️ 注意:fo:wrapper 不渲染,仅作 DOM 标记;id 和 role 用于第二阶段精准定位。
第二步:XSLT 2/3 流水线 —— 两阶段无缝衔接
使用一个主驱动样式表(如 pipeline.xsl),串联两个逻辑阶段:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fo="http://www.w3.org/1999/XSL/Format"
exclude-result-prefixes="fo">
<!-- 导入第一阶段样式表(生成带标记的 FO) -->
<xsl:import href="stage1-main.xsl"/>
<!-- 阶段一:生成中间 FO(含 ref & placeholder) -->
<xsl:variable name="stage1-result">
<xsl:apply-imports/>
</xsl:variable>
<!-- 阶段二:在中间 FO 上注入脚注区块 -->
<xsl:template match="/">
<xsl:apply-templates select="$stage1-result/node()" mode="inject-footnotes"/>
</xsl:template>
<!-- 深度复制所有节点(默认行为) -->
<xsl:mode name="inject-footnotes" on-no-match="shallow-copy"/>
<!-- 在每页 footer 中注入脚注容器(需配合 page-sequence) -->
<xsl:template match="fo:static-content[@flow-name='xsl-region-after']" mode="inject-footnotes">
<xsl:copy>
<xsl:apply-templates select="@*|node()" mode="inject-footnotes"/>
<!-- 插入全局脚注区(或按需分页) -->
<fo:block font-size="8pt" space-before="2pt">
<xsl:for-each select="$stage1-result//fo:wrapper[@role='footnote-placeholder']">
<xsl:variable name="id" select="substring-after(@id, 'fn-')"/>
<fo:block>
<fo:inline font-weight="bold"><xsl:value-of select="$id"/>.</fo:inline>
<xsl:text> </xsl:text>
<xsl:value-of select="/*/tabs/footnotes/*[name() = concat('f', $id)]"/>
</fo:block>
</xsl:for-each>
</fo:block>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>关键技术要点说明:
- 无需额外 XML 输入:脚注数据(<footnotes>)已在原始 XML 中,通过 $stage1-result 可跨阶段访问其结构(XSLT 2/3 支持变量中保存完整文档节点)。
- 页级控制可行但需谨慎:若需严格“脚注出现在标记所在页”,需结合 FOP 的 page-number 和 fo:marker + fo:retrieve-marker 机制,但会显著增加复杂度;多数场景下统一置于页脚底部已满足合规性要求。
-
FOP 集成方式:Java 中配置 Saxon 为 TransformerFactory:
System.setProperty("javax.xml.transform.TransformerFactory", "net.sf.saxon.TransformerFactoryImpl"); // 然后用标准 JAXP API 调用 pipeline.xsl
✅ 总结
- ❌ 不要尝试解析生成的 FO 文件再“按页修补”——FO 无页码语义,且易破坏格式。
- ✅ 正确路径是:XSLT 2/3 + Saxon + 多模式流水线,在 FO 构建期完成脚注语义绑定与结构注入。
- ✅ 所有数据(包括 <footnotes>)全程保留在 XSLT 上下文中,无需额外传参。
- ✅ 输出仍为标准 FO,完全兼容 Apache FOP 渲染,视觉效果与原始设计一致,仅增强脚注功能。
此方案已在金融报告、法规文档等高精度排版场景中稳定落地,兼顾可维护性与 FOP 兼容性。

















