Thai Line Breaking using Uniscribe
เห็นจากข่าว Mozilla Pango-Break (Really) ก็ต้องขอแสดงความยินดีด้วยครับ ที่บักอันยาวนานนี้จะได้รับการแก้ไขสักที แต่ก็ยังเหลือบน Windows และ MAC ซึ่งพี่เทพว่าไว้ ว่า implement ฟังก์ชัน NS_GetComplexLineBreaks() แค่ฟังก์ชันเดียวเท่านั้น (ฟังดูเหมือนง่าย...) ซึ่งนั่งดูหน้าตาี้พารามิเตอร์ก็ช่างคล้ายกับ API ScriptBreak ของ Uniscribe แฮะ.
ว่าแล้วก็เข้าไปนั่งอ่าน API พบว่ามึนตึ๊บ ตัวอย่างอะไรก็ไม่มีเลย ช่างเป็นเอกสารสำหรับ expert อย่างแท้จริง. จริงๆ แล้ว พี่ฮุ้ย เคยเขียนโปรแกรมทดสอบตัดคำไทยด้วย Uniscribe เทียบกับตัวตัดคำใน Microsoft Office นานมาแล้ว แต่สอบถามดูปรากฎว่า โค้ดหายไปแล้ว ไปกับฮาร์ดดิสค์ที่พัง.
ไม่เคยเขียน C++ บน Windows กะเขา แต่อยากลองดูสนุกๆ ก็เลยนั่งอ่าน API และมั่วๆ ออกมา ได้ผลดังนี้
// HelloWorld.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
#include <string>
#include "Usp10.h"
using namespace std;
void GetUniscribeLineBreaks(const WCHAR* aText, int aLength, bool* aBreakBefore) {
if (aLength <= 0 || aText == 0 || aBreakBefore == 0)
return;
int cMaxItems = 100; // FIXME why 100?
SCRIPT_ITEM* pItems = new SCRIPT_ITEM[cMaxItems*sizeof(SCRIPT_ITEM) + 1];
int outitems = 0;
HRESULT result;
// FIXME why pItems[0]?
result = ScriptItemize(aText, aLength, cMaxItems, NULL, NULL, pItems, &outitems);
if (result < 0 || outitems < 1) return;
SCRIPT_ITEM pItem = pItems[0];
SCRIPT_ANALYSIS psa = pItem.a;
SCRIPT_LOGATTR* psla = new SCRIPT_LOGATTR[aLength];
result = ScriptBreak(aText, aLength, &psa, psla);
if (result < 0) return;
for (int i=0; i<aLength; i++) {
//printf("%d", psla[i].fSoftBreak);
aBreakBefore[i] = ( psla[i].fSoftBreak == 0 ? false : true );
}
}
int _tmain(int argc, _TCHAR* argv[])
{
const wstring thaistring = L"สวัสดีครับนี่เป็นการทดสอบภาษาไทย";
const WCHAR* pwcChars = thaistring.c_str();
int cChars = (int) thaistring.length();
bool* result = new bool[cChars];
printf("%s\t%d\n", pwcChars, cChars);
GetUniscribeLineBreaks(pwcChars, cChars, result);
for (int i=0; i < cChars; i++)
printf("%d", (bool) result[i]);
char anything;
cin >> anything;
return 0;
}
ผลลัพธ์การรัน เหมือนจะถูกต้องดี
//
#include "stdafx.h"
#include <iostream>
#include <string>
#include "Usp10.h"
using namespace std;
void GetUniscribeLineBreaks(const WCHAR* aText, int aLength, bool* aBreakBefore) {
if (aLength <= 0 || aText == 0 || aBreakBefore == 0)
return;
int cMaxItems = 100; // FIXME why 100?
SCRIPT_ITEM* pItems = new SCRIPT_ITEM[cMaxItems*sizeof(SCRIPT_ITEM) + 1];
int outitems = 0;
HRESULT result;
// FIXME why pItems[0]?
result = ScriptItemize(aText, aLength, cMaxItems, NULL, NULL, pItems, &outitems);
if (result < 0 || outitems < 1) return;
SCRIPT_ITEM pItem = pItems[0];
SCRIPT_ANALYSIS psa = pItem.a;
SCRIPT_LOGATTR* psla = new SCRIPT_LOGATTR[aLength];
result = ScriptBreak(aText, aLength, &psa, psla);
if (result < 0) return;
for (int i=0; i<aLength; i++) {
//printf("%d", psla[i].fSoftBreak);
aBreakBefore[i] = ( psla[i].fSoftBreak == 0 ? false : true );
}
}
int _tmain(int argc, _TCHAR* argv[])
{
const wstring thaistring = L"สวัสดีครับนี่เป็นการทดสอบภาษาไทย";
const WCHAR* pwcChars = thaistring.c_str();
int cChars = (int) thaistring.length();
bool* result = new bool[cChars];
printf("%s\t%d\n", pwcChars, cChars);
GetUniscribeLineBreaks(pwcChars, cChars, result);
for (int i=0; i < cChars; i++)
printf("%d", (bool) result[i]);
char anything;
cin >> anything;
return 0;
}
10000010001001000100100001000100ซึ่งมีหลายจุดที่ยังงงคือ จะกำหนดค่า cMaxItems เอาจากไหน และทำไมต้องใช้ SCRIPT_ITEM pItems ที่ return โดย ScriptItemize() มา มันจะมีกี่ element และเราจะใช้อันไหน. ผู้รู้ช่วยแนะนำด้วยนะครับ. เพิ่มเติม ขออนุญาตแปะโค้ดที่คุณวีร์ได้ปรับแก้ไว้ที่ wikia.com หน่อยนะครับ (เดี๋ยวใครมาลอกตัวอย่างผิดๆ ของผม :) )
void NS_GetComplexLineBreaks(const PRUnichar* aText, PRUint32 aLength, PRPackedBool* aBreakBefore) { NS_ASSERTION(aText, "aText shouldn't be null"); int cMaxItems = 20; SCRIPT_ITEM* pItems; int outitems = 0; HRESULT result; bool will_delete_item = false; // loop นี้ไปเรียก ScriptItemize เพื่อตัด text ออกเป็นก้อนใหญ่ๆก่อน // ต้องวน loop เพราะไม่รู้ว่า cMaxItems แค่ไหนที่จะพอดี ก็เลยวนขยายไปเรื่อย // จนกว่าจะพอ do { cMaxItems *= 2; if(will_delete_item) { delete[] pItems; } pItems = new SCRIPT_ITEM[cMaxItems*sizeof(SCRIPT_ITEM) + 1]; will_delete_item = true; result = ScriptItemize(aText, aLength, cMaxItems, NULL, NULL, pItems, &outitems); } while(result == E_OUTOFMEMORY); // ในแต่ละก้อนใหญ่ใน pItems ก็เอาแต่ละก่อนมาตัดเป็นก้อนเล็กอีกที for(int iItem = 0; iItem < outiTems; ++i) { // end_offset คือ ตำแหน่งใน aText ที่เป็นตำแหน่งสุดท้ายของ pItems[iItem] // ซึ่งคำนวณจากการดูตำแหน่งเริ่มต้นของ item ถัดไป // ยกเว้น item สุดท้าย end_offset = aLength int end_offset = (iItem + 1 == outItems ? aLength : pItems[iItem + 1].iCharPos); SCRIPT_ITEM pItem = pItems[iItem]; SCRIPT_ANALYSIS psa = pItem.a; int start_offset = pItem.iCharPos; // ผมคิดว่า ScriptBreak น่าจะเติม psla โดยเริ่มจาก 0 // ดังนั้นก็จองเท่าจำนวน unicode character ใน item ก็พอ // (เดาเอาทั้งหมด) SCRIPT_LOGATTR* psla = new SCRIPT_LOGATTR[end_offset - start_offset]; result = ScriptBreak(aText, aLength, &psa, psla); if (result < 0) { return; } for (int i=start_offset, int j=0; i < end_offset; ++i, ++j) { aBreakBefore[i] = ( psla[j].fSoftBreak == 0 ? false : true ); } } }
ความคิดเห็น
API นี้ (itemize, break) เหมือนเป็นต้นแบบให้กับ API เก่าของ Pango เลย (ใน Mozilla patch ผมใช้ API ใหม่ที่ง่ายกว่า) แต่ Pango ใช้ GList ของ glib ในการแทนข้อมูล เลยไม่ต้อง allocate array ล่วงหน้า..
ผมคิดว่า cMaxItems น่าจะเป็นค่าที่ปลอดภัยเข้าว่าไหม? คือจำนวน item ที่มากที่สุดที่เป็นไปได้คือ aLength (คือกรณีที่ผสมอักขระ ภาษาละตัว หรือสลับ "aกaกaกaก..." ไปเรื่อย ๆ)
อีกอย่าง คือต้อง iterate ทุก item ด้วย ไม่ใช่จัดการแค่ที่ pItems[0]
pItems นี่ผมก็งงๆ ทุก element ของ pItem มี Script analysis อยู่ (.a)? แต่ว่าเหมือนกันหมด? แต่ถ้าไม่เหมือนกันจะเลือกตัวไหน หรือต้องเอามายำกัน.
ว่าแต่ก็ไม่ได้มีความมั่นใจเลยครับว่าที่เขียนไปจะถูก T_T.