2008年10月24日 星期五

Google JMesa、Flying Saucer、iText 的中文問題

Google JMesa 是一個功能蠻強大的 HTML Data Grid,不只提供分頁、篩選等功能,還可以將結果匯出成 Excel 或 PDF 檔案。之前上課的時候我在 Lab 裡面用到,不過因為處理的都是英文資料,所以看不出問題,這兩天有位學員問我中文亂碼怎麼處理,我才意識到有這個問題。

以 JMesa 本身提供的範例來說,我看了一下 WEB-INF\lib,看到 iText 的 JAR 檔案,我本來以為只要按照調整 iText 的方式就可以成功,所以就放心了。結果我 Trace了一下Source Code:

1. BasicPresidentController 會呼叫 TableFacade 的 render 方法
2. TableFacade 的 render 方法會長個 PdfViewExporter,呼叫 export 方法
3. PdfViewExporter 的 export 方法會長個 org.xhtmlrenderer.pdf.ITextRenderer,呼叫 createPDF 方法

這時問題就來了,JMesa 沒有直接用 iText,將結果輸出為 PDF,這樣要處理的細節比較多,比較累。因為 JMesa 可以產生 HTML,透過 CSS 與 JavaScript 做出其他功能,所以它就利用 Flying Saucer 這個封裝 iText 的 HTML/XHTML Parser,也就是上面看到的 xhtmlrenderer 相關套件,把畫面上看到的 HTML+CSS 透過 Flying Saucer 底層封裝的 iText 去產生 PDF:

JMesa -> HTML+CSS -> Flying Saucer -> iText -> PDF

所以,我們不能直接用修改 iText 的方式去解決中文,而要用 Flying Saucer 的方式去解決,不過底層一樣是 iText,所以其實改法大同小異,網路上搜尋一下,可以找到底下的做法

import com.lowagie.text.pdf.BaseFont;

...

ITextRenderer renderer = new ITextRenderer();
ITextFontResolver resolver = renderer.getFontResolver();
resolver.addFont(
"C:/WINDOWS/Fonts/中文字型名稱.TTF",
BaseFont.IDENTITY_H,
BaseFont.NOT_EMBEDDED 或 BaseFont.EMBEDDED
);

不過呢,這個做法不 Work。

解決這個問題,要從上面箭頭的流程由右往左一步一步解決:

iText -> PDF

iText 要能夠產生中文 PDF,第一要有它可以處理的中文字型,目前最常用的 Unicode 編碼TrueType 字型不見得可以順利載入,因為 iText 對 TrueType Collection 與 OpenType 支援的程度似乎還沒有很好,所以要先找出 iText 可以用的中文字型,免得明明一切步驟都對,就敗在字型這一關。幾乎每台裝了 Office 的 Windows 電腦都會有 Arial Unicode MS (arialuni.ttf) 這個字型,iText 也認得,所以我就以這個字型做示範。

第二步,要試出正確的編碼。有些中文字型可以用 BaseFont.IDENTITY_H,有些可以用 UniCNS-UCS2-H,要試過才知道。Arial Unicode MS 用的是 BaseFont.IDENTITY_H,但是像 Adobe Reader 附贈的中黑體就要用 UniCNS-UCS2-H。

第三步,要有 iTextAsian.jar 檔案,跟 iText 的 JAR 檔案放在一起。Flying Saucer 跟 JMesa 內含的 iText 版本比較舊,目前最新是 2.1.x 版,但是跟 Flying Saucer 搭配會有問題,因為 Flying Saucer 會用到新版 iText 已經不提供的 API。所以,我把 iText 的 JAR 檔案換成 2.0.8 版,2.1 之後的版本就不行了。

Flying Saucer -> iText -> PDF

Flying Saucer 目前最新是 Release 8 – Second Code Drop (R8pre2) 版本,JMesa 內含的舊了一點,所以我也更新了。

我準備了一個簡單的 XHTML 檔案:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">

<head>
<title>中文測試</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style type="text/css">
name
{
font-family: "Arial Unicode MS";
color: blue;
font-size: 48;
}
</style>
</head>

<body>
<name>名偵探小怪獸</name>
</body>

</html>

然後模仿網站上找到的範例,直接寫一個 Java 類別去產生 PDF 檔案:

import java.io.*;
import org.xhtmlrenderer.pdf.*;
import com.lowagie.text.pdf.*;

public class Test
{
public static void main(String[] args)
{
try
{
String inputFile = "test.xhtml";
String url = new File(inputFile).toURI().toURL().toString();
String outputFile = "test.pdf";
OutputStream os = new FileOutputStream(outputFile);
ITextRenderer renderer = new ITextRenderer();
ITextFontResolver resolver = renderer.getFontResolver();
resolver.addFont(
"C:/Windows/Fonts/arialuni.ttf",
BaseFont.IDENTITY_H,
BaseFont.EMBEDDED);
renderer.setDocument(url);
renderer.layout();
renderer.createPDF(os);
os.close();
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
}
}

這樣就成功了:

image

還可以看到字型的資訊:

image

到目前為止,跟網路上找到的解法,差別如下:


  1. 網路上只說明 Java 程式怎麼加入中文字型,可是一般卻沒有說明必須透過 CSS 去指定使用這個字型,所以雖然加入字型,但是卻沒有被使用到,所以還是會一直看到中文亂碼。

  2. 網路上沒有提醒要加入 iTextAsian.jar 檔案。

另外,其實除了 addFont 方法之外,還有 addFontDirectory 方法,可以一口氣把某個目錄所有字型都註冊。不過這個方法只要一碰到問題就執行不下去,比方說要嵌入不准嵌入的字型,所以不建議貿然使用。

HTML+CSS -> Flying Saucer -> iText -> PDF

接下來,要解決 JMesa 匯出 PDF 會產生亂碼的問題。既然 Flying Saucer 已經可以順利產生中文 PDF,JMesa 也可以在 Browser 畫面上看到中文,那我就先把網頁畫面另存新檔,改一下剛剛的 Java 類別,看看能不能 Work。

網頁內容如下,重點是加入 css 目錄內的 jmesa-pdf.css 這個檔案,因為那是 JMesa 餵給 Flying Saucer 產生 PDF 要使用的 CSS:

<html>

<head>
<link rel="stylesheet" type="text/css" href="jmesa-pdf.css"></link>
<title>JMesa</title>
</head>

<body>

...

<tr id="basic_row1" class="odd"
onmouseover="this.className='highlight'"
onmouseout="this.className='odd'" >
<td><a href="http://www.whitehouse.gov/history/presidents/">喬治</a></td>
<td>Washington</td>
<td>1789-1797</td>
<td>Soldier, Planter</td>
<td>02/1732</td>
</tr>

...

</body>

</html>

記得 iText -> PDF 要注意的三件事,也不要忘了 Java 程式裡面註冊的字型,HTML+CSS 裡面必須要用到。所以我們修改一下 jmesa-pdf.css 檔案裡面顯示 Table 內容的設定:

.jmesa .odd td, .jmesa .even td {
/* font-family: verdana, arial, helvetica, sans-serif; */
font-family: "Arial Unicode MS";
/* font-size: 11px; */
font-size: 16;
padding: 2px 3px 2px 3px;
}

這麼一來,我們就可以將 JMesa 顯示的中文畫面手動地拿給 Flying Saucer 產生中文 PDF。做這件事的目的,是要確定 JMesa 產生的 HTML+CSS 究竟是否正確,不要改了半天,才發現源頭根本就出了問題。

image

JMesa -> HTML+CSS -> Flying Saucer -> iText -> PDF

最後一步,就是要想辦法把前一步自動化,這部份花了我最多時間。

首先,因為這一步要改一下 JMesa 的 Source Code,所以請解開 JMesa 的 Source Code,跟 JMesa 的範例程式放在一起。又因為要重新 Build 出 JMesa,也要更新 iText 與 Flying Saucer,所以請下載以下的 Library:


  1. iText 2.0.8:原因如上

  2. iTextAsian.jar:原因如上

  3. Flying Saucer R8pre2:原因如上

  4. JMesa 2.3.4:JMesa 範例裡面放的 JMesa JAR 檔案,請刪除,因為我們要修改它的 Source

  5. Spring Framework 2.5.5:換掉範例裡面 2.0.2 版的 Spring 與 AOP 模組

  6. Struts 2.0.12:要用到 XWork 的 JAR 檔案

  7. Java Portlet API:我從 Apache Pluto 裡面取得

上面的 5、6、7 三點可以不做,只是要換就換的徹底一點。 WEB-INF/lib 目錄下完整的檔案列表如下:

antlr-2.7.6.jar
asm-2.2.jar
cglib-nodep-2.1_3.jar
commons-beanutils-1.7.0.jar
commons-collections-3.0.jar
commons-dbcp-1.2.jar
commons-digester-1.5.jar
commons-io-1.3.jar
commons-lang-2.4.jar
commons-pool-1.2.jar
core-renderer.jar (新版的 Flying Saucer)
dom4j-1.6.1.jar
groovy-1.0.jar
hibernate-3.2.1.ga.jar
hsqldb-1.8.0.1.jar
iText-2.0.8.jar (新版的 iText)
iTextAsian.jar (新增)
jcl104-over-slf4j-1.4.3.jar
jexcel-2.6.6.jar
jstl-1.1.2.jar
jta-1.0.1B.jar
log4j-1.2.13.jar
minium.jar
poi-3.0-FINAL.jar
portlet-api-1.0.jar (新增,取自 Pluto)
servlet-api-2.4.jar
sitemesh-2.2.1.jar
slf4j-api-1.4.3.jar
slf4j-log4j12-1.4.3.jar
spring-test.jar (新增,取自新版的 Spring Framework,取代 spring-mock-1.2.6.jar)
spring-webmvc-portlet.jar (新增,取自新版的 Spring Framework)
spring-webmvc.jar (新增,取自新版的 Spring Framework)
spring.jar (新版的 Spring Framework)
standard-1.1.2.jar
tagsoup-1.1.3.jar
xercesImpl-2.6.1.jar
xwork-2.0.6.jar (新增,取自 Struts)

第二步,先能夠正確輸入與顯示中文資料。

修改 WEB-INF/presidents.txt 檔案,加入一些中文人名,這樣測試的時候就不必每次輸入中文,也可以順便測試中文顯示是否正確:

"喬治","Washington","Father of His Country", ...
"約翰","Adams","Atlas of Independence", ...
"Thomas","Jefferson","Man of the People, Sage of Monticello", ...
...

重新執行 JMesa 的範例,卻發現一樣顯示亂碼。這很簡單,應該是 JMesa 範例內的 JSP 檔案 Character Encoding 沒有設定 UTF-8,所以我就開始進行以下的動作。

修改 decorators/main.jsp 檔案,加入 UTF-8 設定:

<%@ taglib uri="sitemesh-decorator" prefix="decorator" %>
<%@ page contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ...>

<html>

...

</html>

修改 jsp/*.jsp 檔案,加入 UTF-8 設定。以 basic.jsp 為例:

<%@ page contentType="text/html; charset=UTF-8" %>
<html>

<head>
<title>Basic JMesa Example</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>

...

</html>

改到這邊,我就很高興的重新執行,結果可以看到中文,太好了!

搞笑的是,輸入中文的時候可以看到中文,但是在 Worksheet 範例按下 Save 按鈕的時候,原本正確的中文卻會變成亂碼。這應該是 HttpServletRequest 物件 Character Encoding 沒有設定 UTF-8。

修改 org.jmesa.web.HttpServletRequestWebContext 類別的 Constructor,加入 UTF-8 設定:

public HttpServletRequestWebContext(HttpServletRequest request)
{
try
{
request.setCharacterEncoding("UTF-8");
}
catch (Exception e)
{
e.printStackTrace();
}

this.request = request;
}

到這個階段,JMesa 的範例才能夠正確輸入與顯示中文 (喬治與約翰是從檔案讀入的資料,湯瑪士是手動輸入的資料):

image

我本來以為這一步不是那麼重要,所以就先去改第三步,結果反而走了很多冤枉路。Jakarta Commons Lang 如果沒有正確設定 Character Encoding,StringUtils.escapeXml 方法就不能正確運作,會把正確的內容翻成亂碼。所以這一步一定要先改好才行!

第三步,能夠匯出中文 PDF 檔案。

修改 org.jmesa.view.pdf.PdfViewExporter 類別的 export 方法:

public void export() throws Exception
{
byte[] contents = view.getBytes();
responseHeaders(response);

...

ITextRenderer renderer = new ITextRenderer();
ITextFontResolver resolver = renderer.getFontResolver();
resolver.addFont(
"C:/Windows/Fonts/arialuni.ttf",
BaseFont.IDENTITY_H,
BaseFont.EMBEDDED
);

DocumentBuilder builder =
DocumentBuilderFactory.newInstance().newDocumentBuilder();
InputStream is = new ByteArrayInputStream(contents);
Document doc = builder.parse(is);

renderer.setDocument(doc, getBaseUrl());
renderer.layout();
renderer.createPDF(response.getOutputStream());
}

重新執行之後,不只可以正確輸入與顯示中文,在 Basic 範例按下匯出 PDF 的按鈕,還可以看到正確的中文 PDF 自動產生:

image

總算是大功告成。

不只是 PDF 檔案,連 Excel 都不會有問題喔:

image

沒有留言: