作者:Nick Afshartous
英文原文:http://www.javaworld.com/javaworld/jw-04-2006/jw-0410-html.html
翻譯:http://shaofan.blogjava.net
把網頁內容以PDF的格式呈獻有利于內容的傳播。在一些應用中,提供格式便于打印的文檔是一個必需的功能,比如員工利益表等。事實上,法律規定Summmary Plan Descriptions(SPDs)必須能夠打印,即使它們是在線提供的也是如此。然而只打印網頁本身是不夠的,因為打印格式必包含表格內容和頁碼。
?
為了提供這樣的功能,開發人員可以把HTML內容轉換為PDF格式。在此即做介紹。這里介紹的這種方法只使用開源組件。一些商業產品也支持動態的文檔生成,比如說Adobe,它有Document Server產品線。但是,使用商業產品的開銷是相當可觀的。使用開源方案可以緩解開銷的問題,并增加了組件源碼的透明度。
?
轉換過程包含以下三步:
1.把HTML轉換為XHTML;
2.把XHTML轉換為XSL-FO(Extensible Stylesheet Language Formatting Objects擴展樣式表語言格式化對象)。這里使用XSL樣式表和XSLT轉換器;
3.把XSL-FO文檔傳遞給格式化程序來生成目標PDF文檔。
?
本文先介紹怎樣用命令行界面來做這種轉換,然后介紹怎樣在JAVA中使用DOM接口來做同樣的工作。
?
組件版本:
本文中的代碼在以下版本中進行了測試:
組件???? 版本
JDK ????1.5_06
JTidy ???r7-dev
Xalan-J ?2.7
FOP ????0.20.5
?
使用命令行界面
?
在轉換過程中的每一步都包含了從一個輸入文件生成輸出文件的過程。這個過程可以用下圖來表示:

?
使用這三個工具的命令行界面開始我們的工作是個好方法,盡管這種方法并不適合產品級的系統,因為它需要往磁盤中寫入臨時的中間文件。這種額外的I/O會導致性能的降低。稍后,在我們用JAVA來調用這三個工具時,這個問題就會得到解決。
?
第一步:轉換HTML為XHTML
?
第一步就是把HTML轉換為一個新的XHTML文件。當然,如果文件本來已經就是XHTML,那就不需要這一步了。
?
我用JTidy來完成這個轉換。JTidy是Tidy HTML解析器的JAVA版本。在轉換的過程中,JTidy會自動添加缺少的標簽來創建格式良好(well-formed)的XML文檔。我用的是在SourceForge上的最新版本r7-dev。
?
可以用以下的腳本來運行JTidy:
#/bin/sh
java -classpath lib/Tidy.jar org.w3c.tidy.Tidy -asxml $1 >$2
?
此腳本設置了CLASSPATH并調用了JTidy。運行時,要輸入的文件是以命令行參數的形式傳給JTidy。默認情況下,生成的XHTML將被輸出到標準輸出設備。-modify開關可以用來覆寫輸入文件。-asxml開關把JTidy的輸出重定向到格式良好的XML。
?
調用時像這樣:
tidy.sh hello.html hello.xml
?
hello.html(輸入)和hello.xml(輸出)的內容如下:
?
<html>
<head>
? <title>Hello World
</head>
<body>
?? <p> Hello World!
</body>
</html>
?
?
<!DOCTYPE html PUBLIC quot;-//W3C//DTD XHTML 1.0 Strict//EN"
??? quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns=quot;http://www.w3.org/1999/xhtml">
<head>
<meta name=quot;generator" content="HTML Tidy, see www.w3.org" />
<title>Hello World</title>
</head>
<body>
<p>Hello World!</p>
</body>
</html>
?
要注意的是,在XML文件中的那個</p>和</title>是JTidy自動添加的[譯注1]。
?
?
第二步:轉換XHTML為XSL-FO[譯注2]
?
下面,XHTML將被轉換為XSL-FO,一種用來為XML文檔指定打印格式的語言。我通過用XSLT轉換器(Apache Xalan)處理XSL樣式表來完成這個轉換。我使用的樣式表是由Antenna House提供的xhtml2fo.xsl。Antenna House是一個出售XSL-FO上商用格式程序的公司。
?
xhtml2fo.xsl樣式表指定了如何把每個HTML標簽翻譯成相應的XSL-FO格式化命令序列。舉例來說,HTML中的H2標簽在翻譯中被定義為:
?
??? <xsl:template match="html:h2">
????? <fo:block xsl:use-attribute-sets="h2">
??????? <xsl:call-template name="process-common-attributes-and-children"/>
????? </fo:block>
??? </xsl:template>
?
在處理的過程中,每次遇到H2標簽,以上XSLT模板都會被調用。html:前綴表明H2標簽是HTML的命名空間(namespace)。樣式表的命名空間在頂層xsl:stylesheet指示符的屬性中被指定。在xhtml2fo.xsl的最頂層,我們可以看到它指定了三個命名空間,分別對應于XSL,XSL-FO和HTML語言。
?
??? <xsl:stylesheet version="1.0"
??????????????????? xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
??? ????????????????xmlns:fo="http://www.w3.org/1999/XSL/Format"
??????????????????? xmlns:html="http://www.w3.org/1999/xhtml">...
?
模板中的第二行
?
??? <fo:block xsl:use-attribute-sets="h2">
?
致使fo:block標簽被輸出,并且H2的屬性被生成為fo:block標簽的屬性和值。每個XSL-FO塊(block)都是一段文字,它們的格式基于塊的屬性的值。
?
H2的屬性在樣式表中被定義為:
?
??? <xsl:attribute-set name="h2">
??????? <xsl:attribute name="start-indent">10mm
??????? <xsl:attribute name="end-indent">10mm
??????? <xsl:attribute name="space-before">1em
??????? <xsl:attribute name="space-after">0.5em
??????? <xsl:attribute name="font-size">x-large
??????? <xsl:attribute name="font-weight">bold
??????? <xsl:attribute name="color">black
?? </xsl:attribute-set>
?
start-indent及其后的屬性用來指定H2塊的格式化后的外觀。當你想改變PDF文檔中用同樣HTML標簽的文字塊的外觀時,使用屬性集可以使這種改變更加容易。只要改動屬性的設置,那么輸出的文件中所有使用這些屬性的地方都會被改動。
?
下一個指示符調用一個名為"process-common-attributes-and-children"的模板:
?
??? <xsl:call-template name="process-common-attributes-and-children"/>
?
這個模板在樣式表中被指定。它的作用是檢查一些普通的HTML屬性(如lang,id,align,valign,style)并生成相應的XSL-FO指示符。要觸發對嵌在頂層H2標簽中的任意標簽的翻譯,process-common-attributes-and-children會調用:
?
??? <xsl:apply-templates/>
?
因此,如果輸入是
?
??? <h2> Hello <em> there </em> </h2>
?
那么在H2的模板中的<xsl:apply-templates/>就會觸發用來翻譯<em>標簽的模板。
?
翻譯H2標簽的輸出是:
?
??? <fo:block start-indent="10mm" ...
??????? original H2 tag content
??? </fo:block>
?
我們調用Xalan來應用xhtml2fo.xsl。在調用Xalan之前,用Unix腳本xalan.sh來設置它需要用到的CLASSPATH變量。
?
#/bin/sh
?
export CLASSPATH='.;./lib/xalan.jar;./lib/xercesImpl.jar;./lib/xml-apis.jar;lib/serializer.jar'
?
java -classpath $CLASSPATH org.apache.xalan.xslt.Process -IN $1 -XSL xhtml2fo.xsl -OUT $2 -tt
?
因為Xalan需要一個XML解析器,所以這里還需要Apache Xerces和xml-api JARs。所有的jar文件都可以在Xalan的發布包中找到。
?
要通過對XHTML應用樣式表來新建一個XSL-FO文件,可以調用腳本:
?
??? xalan.sh? hello.xml hello.fo
?
我喜歡用Xalan的跟蹤開關(-tt)來顯示應用的模板。hello.fo文件如下:
?
<?xml version="1.0" encoding="UTF-8"?>
?
<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format"
??? xmlns:html="http://www.w3.org/1999/xhtml"
??? writing-mode="lr-tb"
??? hyphenate="false"
??? text-align="start"
??? role="html:html">
?
? <fo:layout-master-set>
??? <fo:simple-page-master page-width="auto" page-height="auto"
?????????????????????????? master-name="all-pages">
????? <fo:region-body column-gap="12pt" column-count="1" margin-left="1in"
????????????????????? margin-bottom="1in" margin-right="1in" margin-top="1in"/>
????? <fo:region-before display-align="before" extent="1in"
??????????????????????? region-name="page-header"/>
????? <fo:region-after display-align="after" extent="1in"
????????????????????? region-name="page-footer"/>
????? <fo:region-start extent="1in"/>
????? <fo:region-end extent="1in"/>
??? </fo:simple-page-master>
? </fo:layout-master-set>
?
? <fo:page-sequence master-reference="all-pages">
??? <fo:title>Hello World
??? <fo:static-content flow-name="page-header">
????? <fo:block font-size="small" text-align="center" space-before="0.5in"
??????????????? space-before.conditionality=;"retain">
??????? Hello World
????? </fo:block>
??? </fo:static-content>
?
??? <fo:static-content flow-name="page-footer">
????? <fo:block font-size="small" text-align="center" space-after="0.5in"
??????????????? space-after.conditionality=quot;retain">
??????? - <fo:page-number/> -
????? </fo:block>
??? </fo:static-content>
?
??? <fo:flow flow-name="xsl-region-body">
????? <fo:block role="html:body">
??????? <fo:block space-before="1em" space-after="1em" role="html:p">
????????? Hello World!
??????? </fo:block>
????? </fo:block>
??? </fo:flow>
?
? </fo:page-sequence>
?
</fo:root>
?
?
第三步:XSL-FO到PDF
?
第三步,也就是最后一步,就是把XSL-FO文檔傳遞給格式化程序來生成PDF。我用的是Apache FOP(Formatting Objects Processor)。FOP部分實現了XSL-FO標準,并對PDF的輸出格式提供了最好的支持。而對Postscript還處于初級階段,對微軟的RTF的支持還在計劃中。FOP發布版包含shell腳本fop.sh/fop.bat,它們需要傳入XSL-FO文件作為輸入參數來生成目標PDF文件。
?
在Unix下可以這樣運行:
?
??? fop.sh hello.fo hello.pdf
?
唯一所需的前提條件就是把設置為這個腳本使用到的FOP目錄設置環境變量。
?
文件hello.pdf即為FOP的輸出,你在本文的源代碼中可以找到。
?
因為FOP目前并未完全實現XSL-FO標準,所以有一定的局限性。具體它實現了標準的哪些子集,可以在FOP的網站上的Compliance部分找到詳細說明。
---------------------------------------------------------------------------
[譯注1] 此處原文是“在XML文件中的那個</p>是JTidy自動添加的”。我使用JTidy轉換的結果是</title>也被添加,而且這符合JTidy的邏輯,因此這里稍作了修改。
?
[譯注2] 這一部分我在試著做的時候遇到很多問題。首先,有些地方作者描述的并不清楚,特別是對于模板的解釋那一部分。其次,在用Xalan做轉換時遇到了Connection time out的異常。這可能是由于xml文件中的dtd(xhtml1-strict.dtd)無法連接造成的。把該dtd下載到本地后,該異常即可消除。然后是無法找ent文件。所需要的這些ent都可以在xmlbuddy的安裝包里找到,拷過來就可以了。我不知道作者是不是沒有遇到過這些問題,也可能我這只是特例。