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;
}

ผลลัพธ์การรัน เหมือนจะถูกต้องดี
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 );
    }
  }
}

ความคิดเห็น

Thep กล่าวว่า
:-) กำลังจะเขียนเรื่อง Uniscribe API ที่ wikia อยู่พอดี

API นี้ (itemize, break) เหมือนเป็นต้นแบบให้กับ API เก่าของ Pango เลย (ใน Mozilla patch ผมใช้ API ใหม่ที่ง่ายกว่า) แต่ Pango ใช้ GList ของ glib ในการแทนข้อมูล เลยไม่ต้อง allocate array ล่วงหน้า..

ผมคิดว่า cMaxItems น่าจะเป็นค่าที่ปลอดภัยเข้าว่าไหม? คือจำนวน item ที่มากที่สุดที่เป็นไปได้คือ aLength (คือกรณีที่ผสมอักขระ ภาษาละตัว หรือสลับ "aกaกaกaก..." ไปเรื่อย ๆ)

อีกอย่าง คือต้อง iterate ทุก item ด้วย ไม่ใช่จัดการแค่ที่ pItems[0]
veer กล่าวว่า
ใน Abiword ใช้ while(hRes == E_OUTOFMEMORY) เอาครับ ไม่พอก็ขยาด cMaxItems. ปลอดภัยทุกภาษา.

pItems นี่ผมก็งงๆ ทุก element ของ pItem มี Script analysis อยู่ (.a)? แต่ว่าเหมือนกันหมด? แต่ถ้าไม่เหมือนกันจะเลือกตัวไหน หรือต้องเอามายำกัน.
veer กล่าวว่า
s/ขยาด/ขยาย
veer กล่าวว่า
คล้ายจะเข้าใจ pItems แล้วหลังจากไปอ่านใน items มา for each item in iTems { script_break(item) }?
veer กล่าวว่า
ผมลองไปสร้างวิมานในอากาศโดยดัดแปลง code ชุดนี้ + comment ของป๋าเทพ + ดูจาก abiword ได้เป็น code นี้ แต่ว่าไม่มี Windows ใช้ :-P. เลยไม่ได้ลอง build ดู.
Thep กล่าวว่า
โค้ดของ veer ดูจะเป็นตามที่ควรจะเป็นแล้วครับ เทคนิคที่ abiword ใช้ ก็โอเคเลย :-)
rchatsiri กล่าวว่า
อย่างงเลยครับ ไม่เคยมี concept เกี่ยวกับ text breaking มาก่อน..แต่พยายามอ่านอยู่คับ :)
veer กล่าวว่า
~inSiderboy: สู้เขาพ่อหนุ่ม :-). สงสัยอะไรก็ถาม(ป๋าเทพ)ได้. ถามผมก็ได้ถ้าไม่ได้ต้องการความน่าเชื่อถือ. ถามใน wikia ไปเลยก็ง่ายดี.
pattara กล่าวว่า
เจ๋งเลยครับ ขอบคุณคุณวีร์และพี่เทพ ตกลงเอาตามล่าสุดของคุณวีร์นี้นะครับ เดี๋ยวบอกพี่ฮุ้ยแกมี platform สำหรับ build Firefox บน win อยู่แล้ว จะได้ลอง build ดู
veer กล่าวว่า
ของที่ post บน wikia เป็น GFDL (ใช่เปล่าครับ?) ปะได้อยู่แล้วครับ. :-)

ว่าแต่ก็ไม่ได้มีความมั่นใจเลยครับว่าที่เขียนไปจะถูก T_T.

โพสต์ยอดนิยมจากบล็อกนี้

คุณเป็นผู้นำหรือผู้ตาม?

ขับรถในประเทศไทยอันตรายที่สุด

พจนานุกรม ฉบับราชบัณฑิตยสถาน พ.ศ. ๒๕๔๒