Vấn Đề Nhỏ Liên Quan Đến Font TTF
Động cơ game của chúng tôi sử dụng thư viện stb_truetype để xử lý font TTF. Gần đây, khi sử dụng font Alibaba普惠体 do công ty cung cấp, chúng tôi gặp phải một lỗi bất thường: chiều cao ký tự tiếng Trung hiển thị ra chỉ đạt khoảng 70px thay vì 100px như mong muốn. Trong khi đó, các font tiếng Trung thông thường (ví dụ: font mặc định của Windows) lại hoạt động bình thường.
Sau khi tìm kiếm thông tin, tôi phát hiện một bài viết liên quan trên GitHub: stb issue #689, đồng thời cũng thấy ImGui từng gặp lỗi tương tự.
Cơ chế tính toán chiều cao font trong stb_truetype
Thư viện stb_truetype cung cấp hai hàm API để tính tỷ lệ phóng đại font:
stbtt_ScaleForPixelHeight()
stbtt_ScaleForMappingEmToPixels()
Tác giả thư viện khuyến nghị sử dụng hàm đầu tiên, nhưng vẫn giữ lại hàm thứ hai cho các trường hợp đặc biệt. Trong tài liệu, ông giải thích:
“Hàm này tính tỷ lệ sao cho kích thước EM của font tương ứng với chiều cao pixel đầu vào. Đây có thể là cách tính truyền thống của các API cũ, nhưng tôi không hoàn toàn chắc chắn.”
Ngoài ra, khi truy xuất giá trị ascent (khoảng cách từ đường cơ sở đến đỉnh chữ) và descent (khoảng cách từ đường cơ sở đến đáy chữ), cũng có hai hàm API khác nhau:
stbtt_GetFontVMetrics()
stbtt_GetFontVMetricsOS2()
Nguồn gốc mâu thuẫn trong font file
Qua nghiên cứu sâu, tôi nhận thấy font TTF chứa hai bảng dữ liệu mô tả ascent và descent:
- Bảng hhea (Horizontal Header Table): Được thiết kế bởi Apple, chứa thông tin gốc về cấu trúc font.
- Bảng OS/2 (TrueType OS/2 and Windows Metrics Table): Do Microsoft phát triển, bổ sung các thông số chi tiết hơn.
Theo tài liệu của Microsoft, hai bảng này có thể chứa dữ liệu khác nhau. Một chú thích trong tài liệu cảnh báo:
“Một số nền tảng cũ hoặc ứng dụng lỗi thời có thể bỏ qua hoàn toàn các trường trong bảng OS/2. Do đó, font CJK nên đảm bảo giá trị descender trong hhea.descender, OS/2.sTypoDescender và HorizAxis.ideo (nếu tồn tại) phải giống nhau. Tương tự, giá trị ascender trong hhea.ascender, OS/2.sTypoAscender và HorizAxis.idtp (nếu tồn tại) cũng cần đồng nhất.”
Tuy nhiên, Alibaba普惠体 đã vi phạm quy tắc này:
- Bảng hhea: ascent = 1050, descent = -322
- Bảng OS/2: ascent = 850, descent = -150
Phân tích nguyên nhân
Khi sử dụng stbtt_ScaleForPixelHeight()
, thư viện tính tỷ lệ phóng đại dựa trên công thức:
|
|
Với giá trị hhea.ascent - hhea.descent = 1050 - (-322) = 1372, tỷ lệ scale sẽ nhỏ hơn kỳ vọng, dẫn đến chiều cao ký tự hiển thị bị thu nhỏ.
Ngược lại, stbtt_ScaleForMappingEmToPixels()
sử dụng trường unitsPerEm trong bảng head (Font Header Table) để tính toán. Giá trị này thường bằng hiệu số giữa ascent và descent. Trong trường hợp của Alibaba普惠体, unitsPerEm = OS/2.sTypoAscender - OS/2.sTypoDescender = 850 - (-150) = 1000, phù hợp với kỳ vọng.
Giải pháp và suy luận
Hiện tượng này có thể xuất phát từ hai nguyên nhân:
- Lỗi trong font file: Nhà thiết kế font có thể đã nhập sai thông số vào bảng hhea hoặc OS/2 do hiểu nhầm về cách tính toán của các hệ điều hành.
- Khác biệt DPI mặc định giữa Apple và Microsoft: Sự khác biệt trong cách xử lý DPI có thể khiến các giá trị ascent/descent bị điều chỉnh không nhất quán.
Dù nguyên nhân là gì, đây rõ ràng là lỗi của font. Để khắc phục, chúng tôi đã sửa đổi mã nguồn động cơ để ưu tiên sử dụng bảng OS/2 thay vì hhea khi tính toán.
Bài học rút ra
- Kiểm tra kỹ font file: Với các font CJK, cần đảm bảo hai bảng hhea và OS/2 đồng nhất về ascent/descent.
- Linh hoạt trong API: Nên cung cấp tùy chọn cho phép người dùng chọn phương pháp tính toán chiều cao font (dựa trên hhea hay OS/2).
- Liên hệ nhà cung cấp font: Nếu phát hiện lỗi nghiêm trọng, cần yêu cầu sửa chữa font để tránh ảnh hưởng đến các dự án khác.
Vấn đề này tuy nhỏ nhưng cho thấy tầm quan trọng của việc hiểu rõ cấu trúc font và cách thư viện xử lý dữ liệu nội tại.