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