转换HTML内容为PDF格式

作者:rainy14f

为网页提供PDF文件支持

概要


在这篇文章里,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

使用命令行界面

在转换过程中的每一步都包含了从一个输入文件生成输出文件的过程。这个过程可以用下图来表示:



使用这三个工具的命令行界面开始我们的工作是个好方法,尽管这种方法并不适合产品级的系统,因为它需要往磁盘中写入临时的中间文件。这种额外的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(输出)的内容如下:




<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的安装包里找到,拷过来就可以了。我不知道作者是不是没有遇到过这些问题,也可能我这只是特例。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值