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 ได้ที่นี่ครับ 
เครดิต/อ้างอิง

ใส่ความเห็น

อีเมลของคุณจะไม่แสดงให้คนอื่นเห็น ช่องข้อมูลจำเป็นถูกทำเครื่องหมาย *

This site uses Akismet to reduce spam. Learn how your comment data is processed.