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

?
使用這三個(gè)工具的命令行界面開始我們的工作是個(gè)好方法,盡管這種方法并不適合產(chǎn)品級(jí)的系統(tǒng),因?yàn)樗枰疟P中寫入臨時(shí)的中間文件。這種額外的I/O會(huì)導(dǎo)致性能的降低。稍后,在我們用JAVA來調(diào)用這三個(gè)工具時(shí),這個(gè)問題就會(huì)得到解決。
?
第一步:轉(zhuǎn)換HTML為XHTML
?
第一步就是把HTML轉(zhuǎn)換為一個(gè)新的XHTML文件。當(dāng)然,如果文件本來已經(jīng)就是XHTML,那就不需要這一步了。
?
我用JTidy來完成這個(gè)轉(zhuǎn)換。JTidy是Tidy HTML解析器的JAVA版本。在轉(zhuǎn)換的過程中,JTidy會(huì)自動(dòng)添加缺少的標(biāo)簽來創(chuàng)建格式良好(well-formed)的XML文檔。我用的是在SourceForge上的最新版本r7-dev。
?
可以用以下的腳本來運(yùn)行JTidy:
#/bin/sh
java -classpath lib/Tidy.jar org.w3c.tidy.Tidy -asxml $1 >$2
?
此腳本設(shè)置了CLASSPATH并調(diào)用了JTidy。運(yùn)行時(shí),要輸入的文件是以命令行參數(shù)的形式傳給JTidy。默認(rèn)情況下,生成的XHTML將被輸出到標(biāo)準(zhǔn)輸出設(shè)備。-modify開關(guān)可以用來覆寫輸入文件。-asxml開關(guān)把JTidy的輸出重定向到格式良好的XML。
?
調(diào)用時(shí)像這樣:
tidy.sh hello.html hello.xml
?
hello.html(輸入)和hello.xml(輸出)的內(nèi)容如下:
?
<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文件中的那個(gè)</p>和</title>是JTidy自動(dòng)添加的[譯注1]。
?
?
第二步:轉(zhuǎn)換XHTML為XSL-FO[譯注2]
?
下面,XHTML將被轉(zhuǎn)換為XSL-FO,一種用來為XML文檔指定打印格式的語言。我通過用XSLT轉(zhuǎn)換器(Apache Xalan)處理XSL樣式表來完成這個(gè)轉(zhuǎn)換。我使用的樣式表是由Antenna House提供的xhtml2fo.xsl。Antenna House是一個(gè)出售XSL-FO上商用格式程序的公司。
?
xhtml2fo.xsl樣式表指定了如何把每個(gè)HTML標(biāo)簽翻譯成相應(yīng)的XSL-FO格式化命令序列。舉例來說,HTML中的H2標(biāo)簽在翻譯中被定義為:
?
??? <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標(biāo)簽,以上XSLT模板都會(huì)被調(diào)用。html:前綴表明H2標(biāo)簽是HTML的命名空間(namespace)。樣式表的命名空間在頂層xsl:stylesheet指示符的屬性中被指定。在xhtml2fo.xsl的最頂層,我們可以看到它指定了三個(gè)命名空間,分別對(duì)應(yīng)于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標(biāo)簽被輸出,并且H2的屬性被生成為fo:block標(biāo)簽的屬性和值。每個(gè)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塊的格式化后的外觀。當(dāng)你想改變PDF文檔中用同樣HTML標(biāo)簽的文字塊的外觀時(shí),使用屬性集可以使這種改變更加容易。只要改動(dòng)屬性的設(shè)置,那么輸出的文件中所有使用這些屬性的地方都會(huì)被改動(dòng)。
?
下一個(gè)指示符調(diào)用一個(gè)名為"process-common-attributes-and-children"的模板:
?
??? <xsl:call-template name="process-common-attributes-and-children"/>
?
這個(gè)模板在樣式表中被指定。它的作用是檢查一些普通的HTML屬性(如lang,id,align,valign,style)并生成相應(yīng)的XSL-FO指示符。要觸發(fā)對(duì)嵌在頂層H2標(biāo)簽中的任意標(biāo)簽的翻譯,process-common-attributes-and-children會(huì)調(diào)用:
?
??? <xsl:apply-templates/>
?
因此,如果輸入是
?
??? <h2> Hello <em> there </em> </h2>
?
那么在H2的模板中的<xsl:apply-templates/>就會(huì)觸發(fā)用來翻譯<em>標(biāo)簽的模板。
?
翻譯H2標(biāo)簽的輸出是:
?
??? <fo:block start-indent="10mm" ...
??????? original H2 tag content
??? </fo:block>
?
我們調(diào)用Xalan來應(yīng)用xhtml2fo.xsl。在調(diào)用Xalan之前,用Unix腳本xalan.sh來設(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
?
因?yàn)?/span>Xalan需要一個(gè)XML解析器,所以這里還需要Apache Xerces和xml-api JARs。所有的jar文件都可以在Xalan的發(fā)布包中找到。
?
要通過對(duì)XHTML應(yīng)用樣式表來新建一個(gè)XSL-FO文件,可以調(diào)用腳本:
?
??? xalan.sh? hello.xml hello.fo
?
我喜歡用Xalan的跟蹤開關(guān)(-tt)來顯示應(yīng)用的模板。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部分實(shí)現(xiàn)了XSL-FO標(biāo)準(zhǔn),并對(duì)PDF的輸出格式提供了最好的支持。而對(duì)Postscript還處于初級(jí)階段,對(duì)微軟的RTF的支持還在計(jì)劃中。FOP發(fā)布版包含shell腳本fop.sh/fop.bat,它們需要傳入XSL-FO文件作為輸入?yún)?shù)來生成目標(biāo)PDF文件。
?
在Unix下可以這樣運(yùn)行:
?
??? fop.sh hello.fo hello.pdf
?
唯一所需的前提條件就是把設(shè)置為這個(gè)腳本使用到的FOP目錄設(shè)置環(huán)境變量。
?
文件hello.pdf即為FOP的輸出,你在本文的源代碼中可以找到。
?
因?yàn)?/span>FOP目前并未完全實(shí)現(xiàn)XSL-FO標(biāo)準(zhǔn),所以有一定的局限性。具體它實(shí)現(xiàn)了標(biāo)準(zhǔn)的哪些子集,可以在FOP的網(wǎng)站上的Compliance部分找到詳細(xì)說明。
---------------------------------------------------------------------------
[譯注1] 此處原文是“在XML文件中的那個(gè)</p>是JTidy自動(dòng)添加的”。我使用JTidy轉(zhuǎn)換的結(jié)果是</title>也被添加,而且這符合JTidy的邏輯,因此這里稍作了修改。
?
[譯注2] 這一部分我在試著做的時(shí)候遇到很多問題。首先,有些地方作者描述的并不清楚,特別是對(duì)于模板的解釋那一部分。其次,在用Xalan做轉(zhuǎn)換時(shí)遇到了Connection time out的異常。這可能是由于xml文件中的dtd(xhtml1-strict.dtd)無法連接造成的。把該dtd下載到本地后,該異常即可消除。然后是無法找ent文件。所需要的這些ent都可以在xmlbuddy的安裝包里找到,拷過來就可以了。我不知道作者是不是沒有遇到過這些問題,也可能我這只是特例。