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。上網找了一下,發現 W32TeXXeTeX,而且更新速度很快。用最小安裝,再加上幾個 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 卡,讓它跑起來更快呢?