[TH] How to render the Thai string correctly?

จากบทความการใช้งาน u8g2 ที่สามารถเรนเดอร์ (Render) ภาษาไทย (Thai string) ได้ผ่านทางฟังก์ชัน drawUTF8() ของไลบรารี u8g2 แต่การแสดงผลไม่ถูกต้อง ดังภาพที่ 1 ด้วยเหตุนี้จึงต้องปรับปรุงโค้ดของไลบรารีเพิ่มเติมเพื่อให้การแสดงผลถูกต้องดังภาพที่ 2

ภาพที่ 1 การแสดงผลของ drawUTF8() ก่อนปรับปรุง
ภาพที่ 2 การแสดงผลของ drawUTF8() หลังปรับปรุง

ปัญหาและแนวทางแก้ไขปัญหา

จากภาพที่ 1 จะพบว่าการแสดงผลของเครื่องหมายไม่ถูกต้อง เนื่องจากเครื่องหมายวรรณยุกต์์และสระบางตัวจะต้องอยู่ตำแหน่งเดียวกับพยัญชนะด้านหน้า ดังนั้น เราจะแก้ปัญหาด้วยการตรวจสอบตัวอักขระก่อนทำการแสดงผล เพื่อให้ได้ตำแหน่งการแสดงผลที่ถูกต้องออกมาดังภาพที่ 2

ฟังก์ชันที่เกี่ยวข้อง

ฟังก์ชันแสดงผลที่เรียกใช้ในตัวอย่างโปรแกรมคือ u8g2.drawUTF8() ซึ่งคำสั่งนี้ปรากฏอยู่ในไฟล์ u8g2lib.h และเขียนไว้ดังนี้

u8g2_uint_t drawUTF8(u8g2_uint_t x, u8g2_uint_t y, const char *s) 
{ 
    return u8g2_DrawUTF8(&u8g2, x, y, s); 
}

นั่นหมายความว่ามีการเรียกใช้ u8g2_DrawUTF8() อีกทอดหนึง ซึ่งฟังก์ชันด้งกล่าวอยู่ในไฟล์ชื่อ u8g2_font.c ซึ่งอยู่ในโฟลเดอร์ csrc และเขียนโค้ดเอาไว้เป็นดังต่อไปนี้

u8g2_uint_t u8g2_DrawUTF8(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, const char *str)
{
  u8g2->u8x8.next_cb = u8x8_utf8_next;
  return u8g2_draw_string(u8g2, x, y, str);
}

จากโค้ดของ u8g2_DrawUTF8() มีคำสั่ง 2 คำสั่ง และพบว่ามีการเรียกใช้ u8g2_draw_string() ซึ่งเป็นฟังก์ชันเป้าหมายที่แท้จริงสำหรับการแก้ไขในครั้งนี้

u8g2_draw_string()

ภายใต้โค้ดของ u8g2_draw_string() จะมีโค้ดเขียนไว้ดังนี้


static u8g2_uint_t u8g2_draw_string(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, const char *str) U8G2_NOINLINE;
static u8g2_uint_t u8g2_draw_string(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, const char *str)
{
  uint16_t e;
  u8g2_uint_t delta, sum;
  u8x8_utf8_init(u8g2_GetU8x8(u8g2));
  sum = 0;
  for(;;)
  {
    e = u8g2->u8x8.next_cb(u8g2_GetU8x8(u8g2), (uint8_t)*str);
    if ( e == 0x0ffff )
      break;
    str++;
    if ( e != 0x0fffe )
    {
      delta = u8g2_DrawGlyph(u8g2, x, y, e);
    
#ifdef U8G2_WITH_FONT_ROTATION
      switch(u8g2->font_decode.dir)
      {
	case 0:
	  x += delta;
	  break;
	case 1:
	  y += delta;
	  break;
	case 2:
	  x -= delta;
	  break;
	case 3:
	  y -= delta;
	  break;
      }
      
      /*
      // requires 10 bytes more on avr
      x = u8g2_add_vector_x(x, delta, 0, u8g2->font_decode.dir);
      y = u8g2_add_vector_y(y, delta, 0, u8g2->font_decode.dir);
      */

#else
      x += delta;
#endif

      sum += delta;    
    }
  }
  return sum;
}

สิ่งที่เราจะต้องทำคือ ตรวจสอบว่า อักขระจากข้อมูล *str นั้นเป็นวรณณยุกต์และสระที่เราต้องการหรือไม่ เพื่อปรับค่า x ที่จะถูกส่งไปให้ฟังก์ชัน u8g2_DrawGlyph() เพื่อระบุตำแหน่งของแกน x สำหรับแสดงผล นอกจากนี้ พบว่าตัวแปร delta เป็นค่าความกว้างของตัวอักษรที่วาด ดังนั้น เพื่อความปลอดภัยควรประกาศให้ delta มีค่าเป็น 0 ในตอนประกาศตัวแปรดังนี้

u8g2_uint_t delta=0, sum;

หลังจากนั้นจะพบว่ามีการแปลงข้อมูลตัวอักษรให้เป็นรหัสแบบ Unicode ด้วยคำสั่ง ต่อไปนี้

e = u8g2->u8x8.next_cb(u8g2_GetU8x8(u8g2), (uint8_t)*str);

และทำการตรวจสอบว่า e เป็น 0x0ffff หรือไม่ ถ้าใช่จะออกจากการวาดอักขระ แต่ถ้า e ไม่เป็น 0x0fffe จะทำการวาดอักขระในบรรทัดคำสั่งต่อไปนี้

delta = u8g2_DrawGlyph(u8g2, x, y, e);

นั่นหมายความว่า เราต้องทำการตรวจสอบตัวอักษรที่จะส่งวาดก่อนเรียกคำสั่งด้านบน ดังนั้น จึงทำการตรวจสอบค่าของ *(str—) ว่าเป็นวรรณยุกต์หรือสระที่ต้องการตรวจสอบหรือไม่ ถ้าใช่ จะลดค่าของ x เท่ากับค่า delta โดยในโค้ดด้านล่างนี้ได้จัดเก็บรายการสระและวรรณยุกต์ที่ใช้ในการตรวจสอบเอาไว้ในตัวแปร t

...
  uint8_t s;
  uint8_t t[] = {
    (uint8_t)'่',
    (uint8_t)'้',
    (uint8_t)'๊',
    (uint8_t)'๋',
    (uint8_t)'ุ',
    (uint8_t)'ู',
    (uint8_t)'ิ',
    (uint8_t)'ี',
    (uint8_t)'ึ',
    (uint8_t)'ื',
    (uint8_t)'ั'
  };
...
for( ;; ) {
    s = (uint8_t)*str;
    e = u8g2->u8x8.next_cb(u8g2_GetU8x8(u8g2), s);
...
    for (int i=0; i<11; i++) {
      if (s == t[i]) {
        x -= delta;
        break;
      }
    }
    delta = u8g2_DrawGlyph(u8g2, x, y, e);
...
}

ฟังก์ชันที่ถูกแก้ไขแล้ว

จากการหลักการทำงานที่ต้องการปรับปรุง ได้ทำการแก้ไขโค้ดของฟังก์ชัน u8g2_draw_string() ออกมาได้ดังนี้

static u8g2_uint_t u8g2_draw_string(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, const char *str) U8G2_NOINLINE;
static u8g2_uint_t u8g2_draw_string(u8g2_t *u8g2, u8g2_uint_t x, u8g2_uint_t y, const char *str)
{
  uint16_t e;
  u8g2_uint_t delta=0, sum;
  u8x8_utf8_init(u8g2_GetU8x8(u8g2));
  sum = 0;
  uint8_t s;
  uint8_t t[] = {
    (uint8_t)'่',
    (uint8_t)'้',
    (uint8_t)'๊',
    (uint8_t)'๋',
    (uint8_t)'ุ',
    (uint8_t)'ู',
    (uint8_t)'ิ',
    (uint8_t)'ี',
    (uint8_t)'ึ',
    (uint8_t)'ื',
    (uint8_t)'ั'
  };
  for(;;)
  {
    s = (uint8_t)*str;
    e = u8g2->u8x8.next_cb(u8g2_GetU8x8(u8g2), s);

    if ( e == 0x0ffff )
      break;
    str++;
    if ( e != 0x0fffe )
    {
    for (int i=0; i<11; i++) {
      if (s == t[i]) {
        x -= delta;
        break;
      }
    }
    delta = u8g2_DrawGlyph(u8g2, x, y, e);
    
#ifdef U8G2_WITH_FONT_ROTATION
      switch(u8g2->font_decode.dir)
      {
	case 0:
	  x += delta;
	  break;
	case 1:
	  y += delta;
	  break;
	case 2:
	  x -= delta;
	  break;
	case 3:
	  y -= delta;
	  break;
      }
      
      /*
      // requires 10 bytes more on avr
      x = u8g2_add_vector_x(x, delta, 0, u8g2->font_decode.dir);
      y = u8g2_add_vector_y(y, delta, 0, u8g2->font_decode.dir);
      */

#else
      x += delta;
#endif

      sum += delta;    
    }
  }
  return sum;
}

ตัวอย่างโปรแกรม

โค้ดตัวอย่างโปรแกรมของบทความนี้เป็นดังนี้

#define U8G2_WITH_UNICODE

#include <Arduino.h>
#include <U8g2lib.h>
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);
//U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

void setup() {
  u8g2.begin();

  u8g2.clearBuffer();          
  u8g2.setFont(u8g2_font_etl14thai_t ); 
  u8g2.drawUTF8(10,10,"สวัสดี");
  u8g2.drawUTF8(10,30,"ที่นี่ที่ไหน");
  u8g2.drawUTF8(10,50,"มีใครอยู่ไหม?");
  u8g2.sendBuffer();          
}

void loop() {

}

สรุป

จากบทความนี้จะพบว่า เราสามารถแก้ปัญหาการแสดงภาษาไทยในไลบรารี u8g2 จากฟังก์ชัน drawUTF8() ได้ถูกต้องแล้ว โดยไลบรารียังคงทำงานได้อย่างถูกต้องและใช้ได้กับไมโครคอนโทรลเลอร์ ESP32, ESP8266 และ STM32F103 ได้ (ทางทีมงานเราเหลือให้ใช้งานอยู่เพียงเท่านี้) ส่วนชิพอื่น ๆ นั้นผู้อ่านต้องนำไปทดสอบและแก้ไขการทำงานกันต่อไป สุดท้ายนี้ จะพบว่า ถ้าเราเข้าใจรูปแบบของภาษา และขั้นตอนวิธีการทำงานของโค้ด เราสามารถปรับปรุงโค้ดได้ง่ายขึ้น ดังนั้น ขอให้สนุกกับการเขียนโปรแกรมครับ

(C) 2020-2022, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-11-16, 2022-01-19