คราวที่แล้วพูดถึงว่าทำไม 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
#include
#include
2. สร้าง hb_font_t ขึ้นมา ด้วยคำสั่ง hb_ft_font_create()
hb_font_t* hb_font = hb_ft_font_create(face, 0);
และเราต้อง free ออกเวลาใช้เสร็จแล้ว ซึ่งก็แค่เรียก hb_font_destroy ก่อนที่จะเรียก FT_Done_Face() เท่านั้นเอง
hb_font_destroy(hb_font);
3. แก้ไขฟังก์ชั่น DrawText ให้ใช้งาน HarfBuzz
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)
3.1 ส่งค่า input ไปให้ harfbuzz
hb_buffer_t *buffer = hb_buffer_create();
hb_buffer_set_direction(buffer, HB_DIRECTION_LTR);
hb_buffer_set_script(buffer, HB_SCRIPT_THAI);
hb_buffer_add_utf16(buffer, (unsigned short*) (text.c_str()), text.length(), 0, text.length());
3.2 สั่งให้ harfbuzz คำนวนและรับค่านำกลับมาใช้
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 ควรจะอยู่
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);
}
- เราจะอ่านค่า code จาก glyph_infos แทนที่จะใช้ character code จาก input string เนื่องจากว่า glyph_infos เป็นอะเรย์ที่มีจำนวนสมาชิกเป็น glyph_count (ซึ่งได้จากฟังก์ชั่น hb_buffer_get_length) เราก็จะวนลูปโดยใช้ค่านี้เป็นจำนวนรอบ
- ใช้ฟังก์ชั่น FT_Load_Glyph แทน FT_Load_Char ความแตกต่างของสองฟังก์ชั่นนี้คือ FT_Load_Glyph จะรับค่าเป็น glyph index ในไฟล์เลย (ในขณะที่ FT_Load_Char จะรับค่าเป็น character code) ที่เราใช้ฟังก์ชั่นนี้แทน เพราะว่า glyph_infos นั้นจะเก็บค่าเป็น glyph index นั่นเองครับ
- ในส่วนของ dest.x และ dest.y นั้นจะมีการเอาค่า offset จาก glyph_positions มาคำนวนด้วย (ซึ่งก็เป็นการเพิ่มเข้าไปในค่า bearing ที่ได้จาก FreeType เดิม)
- ค่า advance เราจะใช้ค่าจาก glyph_positions เลยครับ ไม่ต้องเอาไปคำนวนอะไร
3.4 ทำลาย hb_buffer ทิ้ง
hb_buffer_destroy(buffer);
4 ผลลัพท์ที่ได้
![]() |
Harfbuzz + FreeType 2 |
เทียบกับก่อนหน้านี้ที่ใช้แค่ FreeType2 นะครับ
![]() |
FreeType 2 อย่างเดียว |
จะเห็นว่าผลลัพท์ที่ได้นั้นสวยกว่ากันเยอะเลย
- ThaiSans Neue บน f0nt.com โดยคุณ letsego
- SDL 2
- FreeType 2
- HarfBuzz