XSL 格式化對象(XSL-FO)標准是 XSL 標准中最鮮為人知的部分之一(人們更熟悉的部分是 XSLT)。這種情形令人遺憾,因為對於將 XML 文檔制作成 PDF 或 Postscript 文件這樣的任務,XSL-FO 會非常有用。一般說來,XSL-FO 控制 XML 文檔的布局和表現形式。實際上,大多數用戶發現 Html 頁面適合在電腦屏幕上閱讀,但他們更願意 PDF(因此使用 XSL-FO)作為印刷副本。
控制換頁
XSL-FO 的優點之一是可以自動將文檔的文本按需要放在多個頁面上。若一頁滿了,XSL-FO 自動插入下一頁。遺憾的是,這一算法有時可能會在錯誤的位置(如在段標題與段內容之間)插入換頁符。同樣,它有時將圖像標簽與實際圖像分開,或將表頭與表中的行分開。
圖 1 和圖 2 說明了這種問題。在圖 1 中,段標題在頁的底部,而該段的余下部分卻在另一頁上。
圖 1. 段標題後面不恰當的換頁
在圖 2 中,圖像標簽與圖像分開。
圖 2. 圖像標簽後面不恰當的換頁
之所以出現這些問題,是因為 XSL-FO 渲染器要處理一長串塊,但卻不知道這些塊之間的關系。它不知道標題是……嗯,一個標題。解決方案是告訴 XSL-FO 渲染器哪些塊相互關聯,或(更明確地說)哪些塊應保留在一起。為此,標准在 keep 和 break 類別中定義了幾種特性。 keep-with-previous 和 keep-with-next 特性指定塊應和前一塊還是下一塊保留在一起。
這些特性應用於 within-line 、 within-column 和 within-page 組件。顧名思義,這些組件控制進行塊分組的級別。通常,我使用 within-page 組件。
可用值有 auto (不作特殊處理)、 always (始終將這些塊放在同一頁)或一個整數。整數指定優先級,這樣,當數個 keep 特性發生沖突時,優先級數字最大的居先。 always 在所有值中優先級最高。
清單 1 摘錄自一個樣式表,使用了 keep 特性來防止標題與段落之間、或標簽與圖像之間的換頁。在 XSL-FO 中,其特性就是樣式表的屬性。
清單 1. keep 特性
<xsl:template match="doc:title" mode="keep">
<fo:block font-family="Times Roman"
font-size="12pt"
space-after="0.6em"
keep-with-next.within-page="always">
<xsl:apply-templates mode="keep"/>
</fo:block>
</xsl:template>
<xsl:template match="doc:figure" mode="keep">
<fo:block font-family="Times Roman"
font-size="8pt"
font-style="italic"
space-after="0.5em">
<xsl:value-of select="@label"/>
</fo:block>
<fo:block keep-with-previous.within-page="always">
<fo:external-graphic src="url({@src})"
width="{@width}"
height="{@height}"/>
</fo:block>
</xsl:template>
然而,需要警告的是,FOP(Formatting Objects Processor,格式化對象處理器)— Apache 的開放源碼 XSL 處理器 — 沒有完全實現 keep 特性。如果您使用 清單 1,FOP 就會忽略 keep 特性,而這樣仍可能會導致不恰當的換頁。就我所知,只有象 RenderX 的 XEP 或 Antenna House 的 XSL Formatter這樣的商業渲染器實現了 keep 特性 — 至少在我寫這篇文章時是如此。
巧用 FOP
既然商業實現對標准提供了更全面的支持,購買其副本可能是目前最佳的解決方案。但是,獲得許可證的費用大約是每台服務器 US$5,000(盡管受限的工作站許可證可能只要 US$79),所以並不是每個項目都有能力購買自己的 XSL-FO 渲染器。在項目的早期(此時預算有限),情況更是如此。
盡管存在通過 FOP 使用 keep 特性的變通方法,但它只是局部的解決方案。FOP 只識別表中行的 keep 特性。這一有限的支持可用來將表頭與表的余下部分保留在一起,但在完全支持出現之前,也可以利用它作為變通方法。該解決方案依賴所謂的 隱形表(專為布局引入的表,但正如其名,它是不可見的)。如果您編碼過 Html,那麼對於僅為布局而使用表的方式(譬如為了模擬列),您應該已經比較熟悉了。
清單 2 摘錄了另一個樣式表,它演示了使用 FOP 的隱形表中的 keep 特性。這個版本的樣式表可防止不恰當的換頁。模板 doc:figure 創建一個隱形表,其中標簽在第一行而圖像在第二行。 keep-with-previous 特性應用於第二行。
清單 2. 隱形表
<xsl:template match="doc:figure" mode="blind">
<fo:table table-layout="fixed" width="100%">
<fo:table-column column-width="proportional-column-width(1)"/>
<fo:table-body>
<fo:table-row padding-bottom="0.5em">
<fo:table-cell>
<fo:block font-family="Times Roman"
font-style="italic"
font-size="8pt">
<xsl:value-of select="@label"/>
</fo:block>
</fo:table-cell>
</fo:table-row>
<fo:table-row keep-with-previous="always">
<fo:table-cell>
<fo:block>
<fo:external-graphic src="url({@src})"
width="{@width}"
height="{@height}"/>
</fo:block>
</fo:table-cell>
</fo:table-row>
</fo:table-body>
</fo:table>
</xsl:template>
<xsl:template match="doc:section" mode="blind">
<fo:table table-layout="fixed" width="100%">
<fo:table-column column-number="1"/>
<fo:table-body>
<fo:table-row keep-with-next="always">
<fo:table-cell>
<xsl:apply-templates select="doc:title" mode="blind"/>
</fo:table-cell>
</fo:table-row>
<fo:table-row>
<fo:table-cell>
<xsl:apply-templates select="*[2]" mode="blind"/>
</fo:table-cell>
</fo:table-row>
</fo:table-body>
</fo:table>
<xsl:apply-templates select="*[position() > 2]" mode="blind"/>
</xsl:template>
模板 doc:section 類似。它同樣創建了一個有兩行的隱形表。標題在第一行,第一段在第二行。選擇第一段時必須當心。我使用根據位置來選擇段的方法以提高靈活性。
實際上,隱形表最適合用於圖像標簽和表頭,而不是段落標題。FOP 不能分割表單元,這意味著如果將較長的段包括在隱形表中,那麼 FOP 將不會分割這一段。這進而會導致一些嚴格說來不必要的換頁。您需要考慮在文檔的何處使用這一技術。
改進方法之一可能是只對短的段生成隱形表(可以用 string-length() 函數計算段的長度)。
結束語
感謝 XSL-FO,有了它,將 XML 文檔轉換成 PDF 就不再困難,XSL-FO 賦予您對文檔最終布局的許多控制權,本篇技巧已經演示了這一點。