Harfbuzz ตอนที่สอง ใช้งานซะที

คราวที่แล้วพูดถึงว่าทำไม OpenType ถึงมีความสำคัญกับการวาดตัวหนังสือภาษาไทย มาวันนี้จะมาเริ่มนำเอา Harfbuzz ซึ่งเป็น Shaping Engine มาใช้งานกับ FreeType จริง ๆ บนโค๊ดเดิมที่ผมเคยเขียนไว้

Harfbuzz เป็น Shaping Engine ที่เป็น Open Source สามารถนำไปใช้งานได้โดยมี License เป็น MIT License ซึ่งเรียกได้ว่านำมาใช้ได้เลย (ขอเพียงแค่มีคำประกาศสิทธิติดไปกับตัวโปรแกรมเท่านั้น)

พูดถึง Shaping Engine ผมเองก็ยังไม่ค่อยเข้าใจเท่าไหร่ว่ามันคืออะไร เท่าที่ลอง Harfbuzz นั้นจะทำการวิเคราะห์ string ที่เราใส่ลงไป แล้วจะทำการหา glyph ที่เหมาะสม และคำนวนหาตำแหน่งที่ glyph นั้นควรจะถูกวาด ซึ่งทั้งสองอย่างนี้จริง ๆ ก็สามารถทำได้บน FreeType แต่ FreeType นั้นจะไม่สามารถวิเคราะห์ input string ทั้งสายได้ (เราใช้วิธีการค้นหา glyph โดยใช้ character code เท่านั้น) ดังนั้นคุณภาพของ output ที่ได้จาก FreeType นั้นไม่มีทางที่จะสู้ output ที่ได้จากการใช้งานร่วมกับ Harfbuzz ได้เลยครับ (เอาง่าย ๆ แค่สระลอยก็แก้ไม่ได้แล้ว นั่นล่ะ)


เนื่องจากว่ามีเอกสารที่พูดถึง Harfbuzz ค่อนข้างน้อย (บนเวปไซท์เองก็มีนิดเดียว) และผมเองก็ยังไม่ได้ศึกษารายละเอียดลึกซึ้งมากมายนัก ผมอาจจะเข้าใจผิด หรือ อาจจะยังไม่รู้ถึงศักยภาพที่แท้จริงของมันก็ได้

สิ่งหนึ่งที่ผมควรจะบอกก่อนคือ ผมทดลอง build HarfBuzz บน Mingw แล้วพบว่ามันหา FreeType ที่ติดตั้งอยู่ไม่เจอ คิดว่าเกิดจาก configure script นั้นทำงานไม่ถูกต้อง ผมเองต้องทำการเล่นแร่แปรธาตุตัวไฟล์ config มันประมาณนึงก่อนที่จะสามารถ compile Harfbuzz ให้ทำงานร่วมกับ FreeType ได้ ซึ่งในบล๊อกวันนี้จะไม่พูดถึงครับ และหวังว่าในอนาคตจะมีคนเข้าไปแก้ตรงนี้ หรือถ้ายังไงจะลองเขียนบน Linux แทนก็ได้ คิดว่าน่าจะไม่มีปัญหา (และคงไม่ต้องมานั่ง compile เองด้วยซ้ำ)

ว่าแล้วก็มาเริ่มต้นกันเลยดีกว่า ผมจะอ้างอิงจากโปรแกรมที่เคยเขียนไว้ที่บล๊อกนี้นะครับ

1. include header file ของ harfbuzz

ก็ … มีสองไฟล์ ไฟล์หนึ่งของ harfbuzz และอีกไฟล์เป็นฟังก์ชั่นช่วยเหลือสำหรับการใช้งานร่วมกับ FreeType
#include 
#include

2. สร้าง hb_font_t ขึ้นมา ด้วยคำสั่ง hb_ft_font_create()

ฟังก์ชั่น hb_ft_font_create เป็นฟังก์ชั่นช่วยเหลือสำหรับสร้าง hb_font object ซึ่งเราจะใช้เวลาใช้งาน harfbuzz ครับ เราสามารถที่จะสร้าง object นี้ได้เลยหลังจากที่สร้าง FT_Face ขึ้นมาแล้ว
hb_font_t* hb_font = hb_ft_font_create(face, 0);

และเราต้อง free ออกเวลาใช้เสร็จแล้ว ซึ่งก็แค่เรียก hb_font_destroy ก่อนที่จะเรียก FT_Done_Face() เท่านั้นเอง 

hb_font_destroy(hb_font);
ที่จริงถามว่าทำไมต้องทำลายก่อน เหตุผลจริง ๆ ไม่มีอะไรเลยครับ สำหรับตัวแปรที่ถูกสร้างขึ้นมาโดยที่มีความสัมพันธ์กัน เช่น a ถูกสร้างโดยอ้างอิงพอยน์เตอร์ไปหา b เราต้องทำลาย a ก่อนทำลาย b เพราะว่าอาจจะมีการอ้างอิงของ a ไปหา b ก่อน ทำให้ถ้าเกิดเราทำลาย b ก่อน เราก็จะมีการอ้างอิงที่ไม่ถูกต้องอยู่ในวัตถุ a และอาจจะสร้างปัญหาทีหลังได้

3. แก้ไขฟังก์ชั่น DrawText ให้ใช้งาน HarfBuzz

ผมเริ่มจากแก้ไขฟังก์ชั่นให้รับค่า hb_font_t* เข้ามาด้วย เพราะว่าเราจะต้องใช้ 
void DrawText(const std::wstring& text,
const unsigned int& color,
const int& baseline,
const int& x_start,
const FT_Face& face,
hb_font_t* hb_font,
SDL_Renderer*& renderer)
จากนั้นก็จะเริ่มเพิ่ม code ที่เกี่ยวกับ Harfbuzz ทั้งหมดตรงนี้ครับ 

3.1 ส่งค่า input ไปให้ harfbuzz

เริ่มต้นเลยตรงจุดแรกคือ เราก็สร้าง hb_buffer_t ขึ้นมา 
hb_buffer_t *buffer = hb_buffer_create();
ตัว buffer นี้จะทำหน้าที่รับข้อมูล input ทั้งหมดที่ harfbuzz จะใช้นำมาประมวลผลครับ ซึ่งไม่ได้มีแค่ input string เท่านั้น แต่ยังมีเรื่องของทิศทางการวาดตัวอักษร (ซ้ายไปขวา/ขวาไปซ้าย) เรื่องของสคริปท์ภาษาของตัว string และอื่น ๆ
สำหรับในตัวอย่างนี้ผมจะบอกแค่ว่า ข้อความเป็นภาษาไทย และวาดจากซ้ายไปขวา
hb_buffer_set_direction(buffer, HB_DIRECTION_LTR);
hb_buffer_set_script(buffer, HB_SCRIPT_THAI);
เท่าที่ลองดู จริง ๆ แล้วถึงแม้ว่าจะไม่มีสองบรรทัดนี้ โปรแกรมก็ยังวาดถูกต้องครับ สันนิษฐานว่ามันคงมีการเซ็ตค่า default ไว้ด้วย  ดังนั้นใส่เอาไว้เพื่อความชัดเจนดีกว่า
ขั้นต่อไปคือการใส่ input string เข้าไป 
hb_buffer_add_utf16(buffer, (unsigned short*) (text.c_str()), text.length(), 0, text.length());
harfbuzz สามารถรับ input string เป็น utf-8 หรือ utf-16 ก็ได้ ในกรณีนี้ผมเลือกใช้ utf-16 เนื่องจากมันสะดวกกว่า (การคำนวนจำนวนของตัวอักษรใน utf-8 นั้นค่อนข้างซับซ้อนเอาเรื่องทีเดียว

3.2 สั่งให้ harfbuzz คำนวนและรับค่านำกลับมาใช้

สำหรับการสั่งให้ harfbuzz ทำงานนั้น เราเรียกฟังก์ชั่น hb_shape 
hb_shape(hb_font, buffer, NULL, 0);
ฟังก์ชั่นนี้จะทำการคำนวนค่าที่เราจะใช้ แต่ มันจะไม่คืนค่าอะไรเลย เราต้องเรียกฟังก์ชั่นอื่นเพื่อที่จะนำเอาผลลัพท์ที่มันคำนวนกลับมาใช้งานเองครับ โดยผมก็จะเรียกสามฟังก์ชั่นนี้
unsigned int glyph_count = hb_buffer_get_length(buffer);
hb_glyph_info_t *glyph_infos = hb_buffer_get_glyph_infos(buffer, NULL);
hb_glyph_position_t *glyph_positions = hb_buffer_get_glyph_positions(buffer, NULL);
  • ฟังก์ชั่น hb_buffer_get_length จะคืนค่าจำนวนของ glyph ที่เราจะใช้ 
  • ฟังก์ชั่น hb_buffer_get_glyph_infos จะคืนค่าเป็นข้อมูลของตัว glyph เช่นใช้ glyph รหัสไหนภายในไฟล์ฟอนท์
  • ฟังก์ชั่น hb_buffer_get_glyph_positions จะคืนค่าตำแหน่งที่ glyph ควรจะอยู่
สองฟังก์ชั่นล่างนั้นจะคืนค่าเป็น array ของ object ที่เก็บข้อมูลนั้น ๆ โดยมีจำนวน object เป็นค่าที่คืนจากฟังก์ชั่นแรก ที่ตำแหน่ง index ที่เท่ากัน อะเรย์ทั้งสองจะเก็บข้อมูลของ glyph เดียวกันครับ

3.3 เริ่มนำเอาผลลัพท์จาก harfbuzz ไปใช้งาน

ผมเริ่มจากเอาส่วนการวนลูปที่แก้แล้วมาแสดงให้ดูก่อนก็แล้วกันนะครับ
for (unsigned int i = 0; i < glyph_count; i++)
{
FT_Load_Glyph(face, glyph_infos[i].codepoint, FT_LOAD_RENDER);

SDL_Surface* surface = NULL;
CreateSurfaceFromFT_Bitmap(face->glyph->bitmap, color, surface);
SDL_Texture* glyph_texture = SDL_CreateTextureFromSurface(renderer,
surface);

SDL_Rect dest;
dest.x = x + (face->glyph->metrics.horiBearingX >> 6) + (glyph_positions[i].x_offset >> 6);

dest.y = baseline - ((face->glyph->metrics.horiBearingY >> 6) + (glyph_positions[i].y_offset >> 6));
dest.w = surface->w;
dest.h = surface->h;

SDL_RenderCopy(renderer, glyph_texture, NULL, &dest);

x += (glyph_positions[i].x_advance >> 6);

SDL_DestroyTexture(glyph_texture);
SDL_FreeSurface(surface);
}
ที่จริงก็จะเห็นได้ว่า มันเหมือนเดิมเกือบหมดเลย แต่ที่มีแก้เพิ่มคือ
  1. เราจะอ่านค่า code จาก glyph_infos แทนที่จะใช้ character code จาก input string เนื่องจากว่า glyph_infos เป็นอะเรย์ที่มีจำนวนสมาชิกเป็น glyph_count (ซึ่งได้จากฟังก์ชั่น hb_buffer_get_length) เราก็จะวนลูปโดยใช้ค่านี้เป็นจำนวนรอบ
  2. ใช้ฟังก์ชั่น FT_Load_Glyph แทน FT_Load_Char ความแตกต่างของสองฟังก์ชั่นนี้คือ FT_Load_Glyph จะรับค่าเป็น glyph index ในไฟล์เลย (ในขณะที่ FT_Load_Char จะรับค่าเป็น character code) ที่เราใช้ฟังก์ชั่นนี้แทน เพราะว่า glyph_infos นั้นจะเก็บค่าเป็น glyph index นั่นเองครับ
  3. ในส่วนของ dest.x และ dest.y นั้นจะมีการเอาค่า offset จาก glyph_positions มาคำนวนด้วย (ซึ่งก็เป็นการเพิ่มเข้าไปในค่า bearing ที่ได้จาก FreeType เดิม)
  4. ค่า advance เราจะใช้ค่าจาก glyph_positions เลยครับ ไม่ต้องเอาไปคำนวนอะไร
แค่นี้เอง …

3.4 ทำลาย hb_buffer ทิ้ง

มาถึงจุดนี้เราก็ทำการวาดทุกอย่างไปหมดแล้ว ดังนั้น hb_buffer ก็หมดประโยชน์ ก็สามารถทำลายทิ้งได้เลยโดยการเรียก
hb_buffer_destroy(buffer);

4 ผลลัพท์ที่ได้

นี่คือผลลัพท์ที่ได้จากการรันโปรแกรมครับ ใช้ฟอนท์ ThaiSans Neue 
Harfbuzz + FreeType 2

เทียบกับก่อนหน้านี้ที่ใช้แค่ FreeType2 นะครับ

FreeType 2 อย่างเดียว

จะเห็นว่าผลลัพท์ที่ได้นั้นสวยกว่ากันเยอะเลย

ก็ถือว่าจบแล้วสำหรับชุดของการใช้ HarfBuzz ร่วมกับ FreeType นะครับ ถ้ามีคำถามอะไรก็ลองทิ้งเอาไว้ใน Google+ ของผมก็แล้วกัน
ดาวน์โหลด Source Code ได้ที่นี่ครับ 
เครดิต/อ้างอิง

ความเห็นส่วนตัวกับ ลิขสิทธิ์ (copyright) ของ API

พักเรื่องฟอนท์ไว้ก่อนนะครับ วันนี้ขอกล่าวถึงเรื่องของ “ลิขสิทธิ์” เสียหน่อย ประเด็นร้อนในช่วงนี้ก็คือการที่เทพยากรณ์อย่าง Oracle พยายามที่จะเล่นประเด็นของ ลิขสิทธิ์ บนชุด API ของ Java ซึ่ง ทาง Oracle เองนั้นรู้สึกว่าตัวเองเสียประโยชน์จากการที่ Google ใช้ API บางส่วนของ Java (และภาษา Java) บน Platform ของตัวเองอย่าง Android

อันที่จริงในชั้นต้นศาลได้ตัดสินไปแล้วว่า API นั้นไม่มีลิขสิทธิ์ แต่เผอิญตอนนี้ Oracle ได้ยื่นอุทธรณ์ไปแล้วน่ะสิ

ก่อนจะพูดถึงว่าความเห็นของผมมีต่อเรื่องนี้อย่างไร ขออนุญาตพูดเรื่องคำว่า “API” เสียก่อน API หรือ Application Programming Interface ก็คือ ชุดของ Interface ที่เอาไว้ใช้เขียนโปรแกรม ก็คือบรรดาชื่อคำสั่งทั้งหลายที่โปรแกรมเมอร์นำไปใช้เขียนโปรแกรม ทั้งคลาส และฟังก์ชั่น (รวมถึงองค์ประกอบอื่น ๆ ด้วย) ตัว API เองนั้นจะมีแค่ชื่อของคลาสและฟังก์ชั่น โดยไม่รวมถึงส่วนของ Implementation นั่นก็คือ ตราบใดที่ชุดคำสั่งนั้นมีชื่อเหมือนกัน มีวิธีใช้เหมือนกัน (เช่นรับพารามิเตอร์เหมือนกัน คืนค่าแบบเดียวกัน) ถึงการทำงานภายในจะแตกต่างกันก็ถือว่าโอเค ใช้ร่วมกันได้

ยกตัวอย่างเช่น ตัว Microsoft .Net นั้น ก็มีผู้ที่สร้างชุด API ที่มีคำสั่งเหมือนกันขึ้นมา ก็คือ Mono ทั้งสองตัวนี้มีชื่อคลาสและฟังก์ชั่นเหมือนกัน แต่วิธีการทำงานภายในนั้นไม่เหมือนกัน เช่น System.Windows.Forms.Form บน .Net นั้นเรียกใช้งาน Windows API ในขณะที่บน Mono นั้นใช้วิธีการวาดทุกอย่างเองด้วยตัวเอง ใช้เฉพาะ API ระดับล่างของ X Windows เป็นต้น

ถ้าเรามองว่า การเขียนโปรแกรม คือการประพันธ์หนังสือสักเล่ม ผมมองว่า API นั้นก็เหมือนกับชุดของคำที่ผมสามารถนำมาใช้ ถ้าเกิดว่าคำที่ผมใช้ในการเขียนหนังสือนั้นมีลิขสิทธิ์ ผมคงต้องจ่ายเงินจำนวนมหาศาลให้กับทุกคนที่บัญญัติศัพท์ใหม่ขึ้นมา ซึ่งมันก็คงไม่เป็นธรรมสำหรับผมและโปรแกรมเมอร์อื่น ๆ บนโลกใบนี้

ผมมองว่า ตัว Implementation ของ API ก็เหมือนกับเป็นพจนานุกรมเล่มหนึ่ง ที่ระบุไว้ว่า ฟังก์ชั่น/คลาสนี้ชื่ออะไร มีความหมายว่าอย่างไร ใช้อย่างไร เหมือนกับที่พจนานุกรมกำหนดเอาไว้ว่า คำศัพท์คำหนึ่ง มีความหมายว่าอย่างไร เป็นคำประเภทไหน เป็นคำนาม กิริยา หรือคุณศัพท์ อะไรก็ว่าไป ซึ่งการที่พจนานุกรมนั้นมีลิขสิทธิ์ไม่ใช่เรื่องแปลก ไม่เช่นนั้นเราคงทำสำเนาขายกันเป็นล่ำเป็นสัน แต่ถ้าเอามองว่าชุดคำศัพท์ที่มีในพจนานุกรมเล่มนั้นมีลิขสิทธิ์ ผมว่ามันเกินไป

ที่สำคัญ ผมคิดว่า ถ้าเกิด API มีลิขสิทธิ์จริง โลกเราได้โกลาหลกับปัญหาลิขสิทธิ์แน่ ๆ Oracle เองก็คงจะถูกโปรแกรมเมอร์ที่สร้างคลาส String และฟังก์ชั่น toString() ก่อนหน้าที่จะมีภาษา Java ฟ้อง เพราะว่าละเมิดลิขสิทธิ์ ทั้ง ๆ ที่คลาสและฟัง์ชั่นนี้เองก็เป็นของพื้น ๆ ที่ใคร ๆ เขาก็ทำกัน

ดังนั้นผมคนหนึ่งละที่คงจะคัดค้านเรื่องลิขสิทธิ์บนตัว API

Harfbuzz ตอนแรก ภาษาไทย กับ OpenType

จะว่าพูดถึงการนำ HarfBuzz ไปใช้กับ FreeType (และ SDL2) แต่อยากจะเกริ่นนำเสียเล็กน้อย

เกี่ยวกับ OpenType

ในอดีต สมัยแรก ๆ ฟอนท์ที่เราใช้ ๆ กันจะอยู่ในรูปของ Fixed Font ก็คือ ตัวอักษรจะมีขนาดตายตัว และใช้พื้นที่การแสดงผลเท่ากันหมดทุกตัวอักษร ในยุคของ GUI ระยะแรก ๆ ก็ยังคงใช้ฟอนท์ที่เป็นลักษณะนี้ แต่หลังจากนั้นไม่นานก็มีการออกแบบฟอนท์ในรูปแบบของ Scalable Font ออกมา ซึ่งก็คือฟอนท์ที่สามารถย่อ-ขยายขนาดได้

Scalable Font ในยุคเริ่มต้นมีอยู่สองค่ายหลัก ๆ นั่นคือ TrueType จาก Apple และ Type1 จาก Adobe สำหรับบน Windows นั้น MS ได้ขอสิทธิการใช้งาน TrueType Font จาก Apple ทำให้ระบบปฎิบัติการหลักๆ สองตัวในตลาด (Mac OS และ Windows (ในตอนนั้นเป็นแค่ Shell)) ใช้ฟอนท์ในรูปแบบของ TrueType กันหมด TrueType จึงกลายเป็นรูปแบบฟอนท์ที่มีการใช้งานมากที่สุดไป

หลังจากนั้น Apple ได้พัฒนาส่วนขยายของ TrueType โดยมีชื่อว่า Apple Advanced Typography แต่ว่า Apple ไม่ได้ให้สิทธิการใช้งานเทคโนโลยีตัวนี้ Microsoft ก็เลยพัฒนาเทคโนโลยีใหม่ขึ้นมาบนพื้นฐานของ TrueType และได้ดึง Adobe เข้ามาร่วมวง ออกมาเป็น OpenType นั่นเอง

พูดกันง่าย ๆ OpenType นั้นมีพื้นฐานมาจาก TrueType แต่เพิ่มความสามารถของ Type1 จาก Adobe เข้ามาด้วยนั่นเอง

OpenType กับภาษาไทย

ในส่วนของภาษาไทย OpenType นั้นมีส่วนช่วยให้การวาดข้อความภาษาไทยบนหน้าจอนั้นดูดีขึ้นมากทีเดียว บน TrueType นั้น เราอาจจะเจอปัญหาประเภท วรรณยุกต์ลอย วรรณยุกต์ซ้อนกับตัว ป.ปลา ญ หญิงในคำว่า “กตัญญู” นั้นมีเชิง และอื่น ๆ ปัญหาดังกล่าวนี้สามารถแก้ไขได้ใน OpenType ซึ่งผู้ผลิตฟอนท์จะเป็นคนที่จัดการในส่วนนี้
ใน OpenType Spec สำหรับภาษาไทย มีการแนะนำให้ใช้เทคนิคสองอย่าง ก็คือ Glyph Substitution (GSUB) และ Glyph Positioning (GPOS) เมื่อข้อความที่ต้องจะวาดนั้นเข้ากับเงื่อนไขที่ระบุไว้
GSUB ก็คือการแทน Glyph ของตัวอักษรหนึ่ง ด้วย Glyph อีกตัวหนึ่ง เช่น ในกรณีคำว่า “กตัญญู” นั้น จะมีส่วนหนึ่งของข้อความที่เป็นตัว ญ ตามด้วยสระ ู ซึ่งอยู่ด้านล่าง ดังนั้น Glyph ของตัว ญ จะถูกแทนด้วย Glyph ที่ไม่มีเชิงแทน
ทั้งนี้ เราสามารถแทน Glyph หลาย ๆ Glyph ด้วย Glyph เดียว (เรียกว่าการทำ Ligature) หรือกลับกัน (Decompose) ได้ด้วย แล้วแต่ว่าผู้ออกแบบฟอนท์จะทำมายังไง
GPOS ก็คือการย้ายตำแหน่งของ Glyph เช่น ในกรณีทีมีวรรณยุกต์ตามหลังสระที่อยู่ด้านบน ก็ให้วางวรรณยุกต์สูงขึ้นไปอีก หรือ ถ้าตัวอักษรนำหน้ามีลักษณะที่สูงกว่าอักษรอื่น ๆ เช่น ตัว ป ก็ให้ย้ายวรรณยุกต์ไปข้างหน้า เป็นต้น
ทั้ง GSUB และ GPOS เป็นฟีเจอร์ของ OpenType ซึ่งตัว OpenType Engine (เช่น HarfBuzz) จะเป็นคนดูแลเองทั้งหมด
คราวหน้าจะพูดถึงการนำ HarfBuzz ไปใช้งานนะครับ