1. 程式人生 > >HTML內容轉換為PDF格式------java

HTML內容轉換為PDF格式------java

為網頁提供PDF檔案支援

摘要:

在這篇文章裡,Nick Afshartous描述了一種把HTML的內容轉換為PDF格式的方法。這種方法相當有用,比如說,一個web程式可以在它的頁面上提供如“下載為PDF”的功能。這種功能方便了列印和儲存,以供日後使用。Afshartous的轉換方法只使用開源的元件。也有一些商業產品可供使用。因此,本文描述的方法既在價格上可以承擔,又能夠獲得所用元件的原始碼。

 
概要




<p>Hello World!</p>
是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標籤在翻譯中被定義為:

[code]    <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部分找到詳細說明。

Java 程式

通過使用上述步驟中用過的三個工具的DOM API,我接下來會展示一個JAVA程式。它在執行時需要提供兩個命令列引數,會自動生成相應的PDF文件,並且不會產生任何臨時檔案。

第一個程式新建一個HTML檔案的InputStream物件,然後此物件被傳給JTidy。

JTidy有個方法叫parseDOM(),可以用來生成輸出的XHTML文件的Document物件。

    public static void main(String[] args) {

    // 開啟檔案
    if (args.length != 2) {
        System.out.println("Usage: Html2Pdf htmlFile styleSheet");
        System.exit(1);
    }

    FileInputStream input = null;
    String htmlFileName = args[0];
    try {
        input = new FileInputStream(htmlFileName);
    }
    catch (java.io.FileNotFoundException e) {
        System.out.println("File not found: " + htmlFileName);
    }

        Tidy tidy = new Tidy();
    Document xmlDoc = tidy.parseDOM(input, null);


JTidy的DOM實現並不支援XML名稱空間。因此,我們必需修改Antenna House的樣式表,讓它使用預設的名稱空間。比如,原來是:

    <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>


被修改後是:

    <xsl:template match="h2">
      <fo:block xsl:use-attribute-sets="h2">
        <xsl:call-template name="process-common-attributes-and-children"/>
      </fo:block>
    </xsl:template>


這個改動必需被應用到xhtml2f0.xsl中的所有模板,因為JTidy生成的Document物件以標籤作為根,如:



修改後的xhtml2fo.xsl包含在這篇文章附帶的原始碼中。

接著,xml2FO()方法呼叫Xalan,使樣式表應用於JTidy生成的DOM物件:

    Document foDoc = xml2FO(xmlDoc, args[1]);  


方法xml2FO()首先呼叫getTransformer()來獲得一個指定的樣式表的Transformer物件。然後,代表著轉換結果的那個Document被返回:

    private static Document xml2FO(Document xml, String styleSheet) {

    DOMSource xmlDomSource = new DOMSource(xml);
          DOMResult domResult = new DOMResult();

    Transformer transformer = getTransformer(styleSheet);

    if (transformer == null) {
        System.out.println("Error creating transformer for " + styleSheet);
        System.exit(1);
    }
    try {
        transformer.transform(xmlDomSource, domResult);
    }
    catch (javax.xml.transform.TransformerException e) {
        return null;
    }
    return (Document) domResult.getNode();

    }


接著,main方法用與HTML輸入檔案相同的字首來開啟一個FileOutputStream。然後呼叫fo2PDF()方法所獲得的結果被寫入OutputStream:

    String pdfFileName = htmlFileName.substring(0, htmlFileName.indexOf(".")) + ".pdf";
    try {
        OutputStream pdf = new FileOutputStream(new File(pdfFileName));
        pdf.write(fo2PDF(foDoc));
    }
    catch (java.io.FileNotFoundException e) {
        System.out.println("Error creating PDF: " + pdfFileName);
    }
    catch (java.io.IOException e) {
        System.out.println("Error writing PDF: " + pdfFileName);
    }


方法fo2PDF()會使用在轉換中產生的XSL-FO Document和一個ByteArrayOutputStream來生成一個FOP driver物件。通過呼叫Driver.run可以生成PDF檔案。結果被作為一個byte array返回:

    private static byte[] fo2PDF(Document foDocument) {

        DocumentInputSource fopInputSource = new DocumentInputSource(
                                                         foDocument);

        try {

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            Logger log = new ConsoleLogger(ConsoleLogger.LEVEL_WARN);

            Driver driver = new Driver(fopInputSource, out);
            driver.setLogger(log);
            driver.setRenderer(Driver.RENDER_PDF);
            driver.run();

            return out.toByteArray();

        } catch (Exception ex) {
            return null;
        }
    }


Html2Pdf.java的原始碼可以在這篇文章的附帶程式碼中找到。

使用DOM API來完成這整個過程,速度要比使用命令列介面快得多,因為它不需要往磁碟中寫入任何中間檔案。這種方法可以整合到伺服器裡,來處理併發的HTML-PDF轉換請求。

以前我曾以這裡展示的這個程式為基礎把生成PDF的功能整合到一個WEB應用。而生成PDF的過程是動態的,因此不需要考慮WEB頁面和相應PDF同步的問題,因為生成的PDF檔案並不是存放在伺服器上。

結論

綜述,在本文裡我描述了怎樣利用開源元件來實現HTML到PDF的轉換。雖然這種實現方法在價格和原始碼方面很有吸引力,但同時也有一定的折衷。一些商業元件可以提供更完整的標準實現。

比如說,FOP目前的版本是.91,不完全支援XSL-FO標準。儘管如此,相對其它的格式而言,對PDF提供了更多的支援。

在開始一個文件轉換的專案之前,你必需考慮對文件格式的需求,並把它們與已有元件所實現的功能做個對比。這將有助於做出正確的決定。

資源

# 下載本文中的原始碼:http://www.javaworld.com/javaworld/jw-04-2006/html/jw-0410-html.zip
# Adobe's Document Server 產品:http://www.adobe.com/products/server/documentserver/main.html
# Antenna House (出售商業的格式化程式):http://www.antennahouse.com
# xhtml2fo.xsl 把 XHTML 轉化為 XSL-FO 的樣式表:http://www.antennahouse.com/XSLsample/XSLsample.htm
# Apache FOP formatter 把 XSL-FO 翻譯為 PDF:http://xmlgraphics.apache.org/fop
# FOP 對 XSL-FO 標準的相容性:http://xmlgraphics.apache.org/fop/compliance.html
# JTidy,把 HTML 轉化為 XHTML:http://sourceforge.net/projects/jtidy/
# Xalan:http://xalan.apache.org/
# Matrix:http://www.matrix.org.cn/

附註

[譯註1]此處原文是“在XML檔案中的那個</p>是JTidy自動新增的”。我使用JTidy轉換的結果是也被新增,而且這符合JTidy的邏輯,因此這裡稍作了修改。

[譯註2]這一部分我在試著做的時候遇到很多問題。首先,有些地方作者描述的並不清楚,特別是對於模板的解釋那一部分。其次,在用Xalan做轉換時遇到了Connection time out的異常。這可能是由於xml檔案中的dtd(xhtml1-strict.dtd)無法連線造成的。把該dtd下載到本地後,該異常即可消除。然後是無法找ent檔案。所需要的這些ent都可以在xmlbuddy的安裝包裡找到,拷過來就可以了。我不知道作者是不是沒有遇到過這些問題,也可能我這只是特例。 


在這篇文章裡,Nick Afshartous描述了一種把HTML的內容轉換為PDF格式的方法。這種方法相當有用,比如說,一個web程式可以在它的頁面上提供如“下載為PDF”的功能。這種功能方便了列印和儲存,以供日後使用。Afshartous的轉換方法只使用開源的元件。也有一些商業產品可供使用。因此,在這篇文章裡描述的這種方法既在價格上可以承擔,又能夠獲得所用元件的原始碼。

把網頁內容以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


作者:Nick Afshartous;rainy14f(作者的blog:http://blog.matrix.org.cn/page/rainy14f)
原文:http://www.javaworld.com/javaworld/jw-04-2006/jw-0410-html.html
Matrix:http://www.matrix.org.cn/resource/article/44/44489_HTML+PDF.html
關鍵字:HTML;PDF


使用命令列介面

在轉換過程中的每一步都包含了從一個輸入檔案生成輸出檔案的過程。這個過程可以用下圖來表示:

image

使用這三個工具的命令列介面開始我們的工作是個好方法,儘管這種方法並不適合產品級的系統,因為它需要往磁碟中寫入臨時的中間檔案。這種額外的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(輸出)的內容如下: