星期三, 3月 21, 2007

JasperReports中使用標楷體字型與變體字型(粗體, 斜體)研究...

在使用JasperReports產生的報表中, 使用者要求使用標楷體字型, 且有些欄位必須使用斜體.
於是做了以下的研究...

在jrxml設計檔內, 設定要使用標楷體之字型.

<font fontName="標楷體" pdfFontName="kaiu.ttf" size="14" pdfEncoding ="Identity-H"/>


若是使用 iReport 設計時, 先將 kaiu.ttf 放入 classpath 中, 再啟動 iReport, 並在字型頁將 fontName 與 pdfFontName 設為標楷體, 且將pdfEncoding 設為 Identity-H.

JasperReports 提供很多的 API, 可以用很多種方式來產生 PDF.
例如使用下列的程式來產生PDF(必須將 kaiu.ttf 放入 classpath 中):

InputStream is = FontReport.class.getResourceAsStream("font.jasper");
JasperRunManager.runReportToPdfFile("font.jasper", "font.pdf", new HashMap(), new JREmptyDataSource());

如此產生之 PDF 即可看到標楷體.
不過, 當我們設定要產生斜體, 粗體, 斜粗體時, 會發現通通沒有效.都以一般字體顯示.

追蹤了 JasperReports1.2.7 的程式碼中, 發現在 net.sf.jasperreports.engine.export.JRPdfExporter.java 程式的1340~1351行中有以下幾行:

Font font = null;
PdfFont pdfFont = null;
FontKey key = new FontKey(jrFont.getFontName(), jrFont.isBold(), jrFont.isItalic());
if (fontMap != null && fontMap.containsKey(key))
{
pdfFont = (PdfFont) fontMap.get(key);
}
else
{
pdfFont = new PdfFont(jrFont.getPdfFontName(), jrFont.getPdfEncoding(), jrFont.isPdfEmbedded());
}

以上程式 1350 行可以看到在產生 pdfFont 的時後, 並沒有把斜體與粗體的資訊傳入, 導致後面產生字型的程式碼得不到粗體與斜體的資訊. 於是, 我們可以將 1350 修改如下(多傳入兩個參數):

pdfFont = new PdfFont(jrFont.getPdfFontName(), jrFont.getPdfEncoding(), jrFont.isPdfEmbedded(), jrFont.isBold(), jrFont.isItalic());

重新打包 JasperReports 之後, 再次執行產生 PDF 的程式, 就如我們所預期的, 可以印出正常,粗體,斜體,粗斜體的標楷體字型了.

若不想去修改 JasperReports 的程式碼的話, 那又該怎麼做呢?
仔細看到上述 JasperReports 的程式碼, 其會先去看 fontMap 是否有該字型, 若有該字型則直接使用(即讓1344行條件成立).

所以我們可以改成如下..
在jrxml設計檔內, 設定要使用標楷體之字型.

<font fontName="標楷體B" pdfFontName="kaiu.ttf" size="14" isBold="true" pdfEncoding ="Identity-H"/>

上面我們以粗體為例, 給一個虛設的 fontName, "標楷體B".
接著我們要在程式中, 告訴 JasperReports, "標楷體B" 真正對應到的字型, 如下:
 
InputStream is = FontReport.class.getResourceAsStream("font.jasper");
JasperPrint jasperPrint = JasperFillManager.fillReport(is, new HashMap(), new JREmptyDataSource());
JRPdfExporter exporter = new JRPdfExporter();
Map fontsMap = new HashMap();
fontsMap.put(new FontKey("標楷體B", true, false), new PdfFont("kaiu.ttf", "Identity-H", false, true, false));
exporter.setParameter(JRExporterParameter.FONT_MAP, fontsMap);
exporter.setParameter(JRExporterParameter.JASPER_PRINT, jasperPrint);
FileOutputStream fos = new FileOutputStream("font.pdf");
exporter.setParameter(JRExporterParameter.OUTPUT_STREAM, fos);
exporter.exportReport();

如此也能在不改 JasperReports 的情況下, 達到產生標楷體粗體的字型(其餘的依此類推)...
不過用此方法, 並不能用 iReport 編譯 jasper 檔(否則也會沒有效果),
我都是額外使用程式來編譯成 jasper 檔.

用使用第一種改 JasperReports 程式碼的做法時, 有些地方要注意的是,
英文字型通常都會提供這些變體字的字型檔. 而中文字型因為太大,
所以必須用模擬的方式來處理.
而使用 iReport 時, 若選粗體, 則 iReport 會將 pdf font name 改為粗體的字型,
如 pdf font name: Helvetica, 再勾選粗體時會變成 Helvetica-Bold
即使用的字型檔已經有粗體的效果, 再加上模擬的效果, 會讓結果更加的粗.
所以需要將 pdf font name 手動改回 Helvetica, 一律使用非粗體的字型, 讓程式去模擬粗體.

或是將 isBold="true" 改成 isBold="false", pdf font name 維持 "Helvetica-Bold".
不過若是使用此方式, 則轉成其他格式時, 如 html, 粗體就會不見了...

不知各位先進有沒有更好的處理方法可供參考?


星期四, 3月 15, 2007

Table Schema 設計的重要性

想起之前遇過的一個專案, 對於 SQL 下法不同而有很嚴重的效能影響, 今天整理下來做個紀錄.

此專案需要紀錄某個產品在某年某月共有多少個數量與價值, 看描述我們可以得到以下的 Table Schema(命名為 table1), 欄位如下:
id, code, year, month, qty, value (除了 code 為 varchar 之外, 其餘為 decimal)

這個 Table Schema 看起來是很合理的, 但後來需要做統計報表的功能時. 就發現此設計的缺陷.

說明如下, 現在需要讓使用者挑選產品與年月起迄, 進而統計出總數量與總價值.
初步我們撰寫 SQL 如下(挑選條件: code為123, 從 94年2月到95年2月):


select sum(qty), sum(value) from table1 where code = '123'
and year * 100 + month between 9402 and 9502


如此可求得正確答案.
現在問題來了, 此 table 的資料量目前為 11,411,862 筆.
由此 SQL 跑出來所需要的時間花了 2 分鐘 17.313 秒..

此 table 已經有加上 code, year, month 之 index.
但為何沒有成效?
原因在於, 我們將 year, month 做了運算, 所以 index 派不上用場.

由於此專案的 Table Schema 無法更動, 且很要求報表效能....
那我們該如何解決此問題呢? 就要透過下列的 SQL 來改進了..


select sum(qty), sum(value) from table1 where code = '123'
and ((year = 94 and month between 2 and 12)
or (year = 95 and month between 1 and 2))


得到的結果是相同的, 但此 SQL 所需花費的時間僅 1.31 秒.
與前一個 SQL 的效能差了百倍以上...

雖然我們解決了效能問題, 但可以看到的是, SQL 寫的就比較醜..
而這還不是最麻煩的案例, 若要統計3年的量, 則 SQL 就不能這樣寫了...
而若是統計同年的 SQL, 則該 SQL 又顯的笨重.
就程式面要組 SQL 的角度來看, 程式如下:


String sql = "sum(qty) w, sum(value) v " +
"from table1 " +
"where code = ? ";
if (y1 == y2) { //同年
sql += "and year = ? and month between ? and ? ";
} else { //不同年
sql += "and ((year > ? and year < ?) " + //y1, y2 (1到12月)
"or (year = ? and month between ? and ?) " + //y1 (m1 ~ 12)
"or (year = ? and month between ? and ?)) "; //y2 (1 ~ m2)
}


可見這讓程式不好寫多了, 而且在開發階段, 資料量不大, 很容易被忽略效能的因素, 而寫出一剛開始的 SQL.

而我們若有此經驗, 我們該如何設計好 Table Schema 呢?
其實, 很簡單, 只要多加一個欄位, 如 year_month. 而此欄位存放 year * 100 + month 的值...
如此, 此欄位即可加上 index, SQL 寫法會變成如下:


select sum(qty), sum(value) from table1 where code = '123'
and year_month between 9402 and 9502


如此, 效能與語法都可以獲得改善, 唯一的缺點就是多佔了點硬碟空間...
不過能用硬體解決的, 還是儘量透過硬體解決囉..

在這實際的案子中, 除了年月之外, 還有分批次的觀念(一個月分四批次, 但若批次欄位為 null, 則代表是該月份的加總值), 又加上不能動 Table Schema...
所以實際的 SQL 程式比上面所列的更加複雜化, 效能差異更是 5 分鐘與 1 秒多的差別...

所以, 要設計好 Table Schema 不是一件容易的事. 需要全盤考量及經驗的累積.


星期四, 3月 08, 2007

透過 ANT 執行 JBuilder2005 Bmj 編譯程式

由於目前有一個用 JBuilder 2005開發的專案需要維護, 個人使用起 JBuilder2005 不是很順手, 所以就動手在 eclipse 的環境中建置此專案...

此專案在 JBuilder2005開發時, 採用 Borland Make 的編譯器來 build 程式. 為什麼要使用 Borland Mark 呢? 原因在於此專案必須在 Java1.4 的環境上執行, 而程式使用了 Java 5 的語法....
而 Borland Mark 有一個很強功能是, 他懂 Java5 的語法, 並可以使用 JDK1.4 來建置程式...

轉換到eclipse的過程非常順利(採用 jdk1.5). 不過 eclipse 用此方式產生出來的 class 就無法在環境上面跑..

於是, 我就動手研究 JBuilder 如何使用 Ant 來編譯程式...

JBuilder有個好用的功能是, 可以依專案自動產生 Ant Build File..
而我也使用了此功能產生了 Ant Build File...
接下來的步驟就是, 如何在 eclipse 或 command-line 模式上跑此 ant build script...

  1. 將 JBuilder 產生的 Build File 中 javac 的 Task 中之 bootclasspathref 改為 classpathref.
  2. 將 C:\Borland\JBuilder2005\lib\jbuilder.jar 加入 Classpath 中.
  3. 啟動 ant 時, 必須傳入參數設定給 JVM, 如下:
    -Xverify:none -Xbootclasspath/a:C:/Borland/JBuilder2005/lib/javac.jar
    -Dbuild.compiler=com.borland.jbuilder.ant.compiler.Bmj
以上的說明是採用描述的方式, 在 eclipse 必須要設定到 External Tools 中.
而這部份就省略了...
若要在 command-line mode 中執行 ANT...
則對應命令如下:
  1. set CLASSPATH=C:\Borland\JBuilder2005\lib\jbuilder.jar
  2. set ANT_OPTS=-Xverify:none -Xbootclasspath/a:C:/Bo
    rland/JBuilder2005/lib/javac.jar -Dbuild.compiler=com.borland.jbuilder.ant.compi
    ler.Bmj
  3. ant compile
關於這方面的資料網路上好像找不到...
又花了一個下午的時間才搞出來...
雖然花了不少時間轉換, 不過, 覺得 JBuilder 採用 adapter 的方式來讓 JDK1.4 可以支援 Java5 的語法也是蠻不錯的功能...

ps. 在JBuilder 2005 developer 或 enterprise 版本上才有這個功能...