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

2008年10月22日 星期三

LyX 與 W32TeX 的 XeTeX

網路上有很多先進說明了設定的方式,所以我只想針對 W32TeX 與 XeTeX 的部份記錄一些細節。

LyX 有好多種安裝檔案,我因為已經裝好 W32TeX,而且我不需要 DVI 與 PostScript 相關的支援,所以我下載的是最小的 LyX-1.5.6-1-Installer.exe。

安裝的時候,LyX 會詢問 TeX 執行檔案的位置,也就是 W32TeX 安裝位置下的 bin 目錄:

image

安裝之後,LyX 還是偷偷塞了Ghostscript 與 ImageMagick 進來。

接下來,要讓 LyX 透過 XeTeX/XeLaTeX 來產生 PDF 檔案,這部份可以修改 Home Directory 底下 Application Data\lyx15 目錄下的 preferences 檔案,或是 LyX 安裝位置下 Resources 目錄的 lyxrc.dist 檔案,加入底下這兩行:

\format "pdf4" "pdf" "PDF (xelatex)"  ""   ""   ""   "document,vector"
\converter "pdflatex" "pdf4" "xelatex $$i" "latex"
format 那一行會在 LyX 的 View 功能表多出一個 PDF (xelatex) 項目:

image

converter 那一行則會在 LyX 的 Tools、Preferences 畫面中 Converters 相關設定裡頭,多出一個 LaTeX (pdflatex) -> PDF (xelatex) 項目:

image

最後,在 LyX 的 Document、Settings 設定中,Language 裡面的 Encoding 選取 utf8-plain:

image

LaTeX Preamble 至少要寫底下這幾行,中文字型名稱可以更改:

\usepackage{fontspec}
\usepackage{xunicode}
\usepackage{xltxtra}

\setmainfont{PMingLiU} % 新細明體
\setmonofont{MingLiU} % 細明體

\XeTeXlinebreaklocale "zh"
\XeTeXlinebreakskip = 0pt plus 1pt

image

最後,只要確定 LyX 檔案是 UTF-8 編碼,那就可以了!

感謝以下兩位先進所提供的資訊:

1. http://bbs.ctex.org/viewthread.php?tid=41004

2. http://blog.bs2.to/post/EdwardLee/8545

2008年10月21日 星期二

Tomcat 與 HTTP Proxy

公司裡面所有的 Web Access 都必須透過 Proxy Server,一般只要在 Browser 裡面設定就好。

如果是 Java 程式自己要透過網路取得資料呢?請在執行時設定:

java -Dhttp.proxyHost=ServerName -Dhttp.proxyPort=ServerPort MainClass
如果是 Web 應用程式呢?以 Tomcat 為例,請修改 catalina.bat 的 JAVA_OPTS 參數,大概是在 118 行左右:

set JAVA_OPTS=%JAVA_OPTS%
-Dhttp.proxyHost=ServerName
-Dhttp.proxyPort=ServerPort
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
-Djava.util.logging.config.file="%CATALINA_BASE%\conf\logging.properties"

ServerName 請換成 Proxy Server 名稱,ServerPort 請改為實際的 Port No。

MiKTeX vs. W32TeX

我的 S31 不知道為了什麼緣故,居然沒辦法裝 MiKTeX,每次裝到最後就掛掉。最新的 2.7 版是這樣,上一代的 2.6 版也是這樣。好不容易找到一個比較舊的 2.6 版安裝成功,可是沒有我最近在用的 XeTeX。上網找了一下,發現 W32TeX 有 XeTeX,而且更新速度很快。用最小安裝,再加上幾個 PDF 相關的套件,以及 XeTeX,居然只要 100 MB 多一點,真是太酷了!

W32TeX 雖然日本人比較愛用,但是不管是處理英文或中文,完全都沒有問題喔!更方便的是,它的安裝過程其實只是解壓縮幾個檔案,完全不會動到 Windows 的 Registry,所以裝好一台機器之後,只要把目錄 ZIP 下來,就可以變成綠色軟體到處使用了!

IBM S31

最近把家裡的 IBM S31 拿出來整理,Pentium-III 600 MHz、256 MB RAM 的機器,即使是跑 XP,我還是覺得很慢。所以這一次,我刻意找出 Windows 2000 Professional,不加任何會讓機器變慢的 Service Pack,反正只是隨身攜帶、方便讀電子書的機器,偶爾做點筆記,上個小網,掛了就重灌。

裝好之後,上網找了相關的驅動程式,再加個 PCMCIA 無線網卡,只吃了 60 MB 的 RAM,跑起來比 XP 舒服多了。

接下來,該不該花點錢,買個 CF/SD 轉 IDE 的轉接卡,然後買塊 8/16/32G 的 CF/SD 卡,讓它跑起來更快呢?