การสร้าง Font stack เพื่อภาษาไทยงาม ๆ (บน Safari)

Submitted by ezybzy on Wed, 2021-09-01 - 12:00

หัวข้อนี้เป็นการเล่าวิวัฒนาการของ Font stack ที่ใช้ในเว็บนี้ (เท่าที่นึกออก) รวมถึงปัญหาและอุปสรรคที่เจอในระหว่างพัฒนา จนได้มาเป็น Font stack ตัวปัจจุบัน

อะไรคือ Font stack?

Font stack คือการนิยามรายการแบบอักษร (Font) ที่ใช้แสดงผล (ในกรณีนี้ขอยกมาเฉพาะเรื่องเกี่ยวกับเว็บไซต์) เพื่อแก้ปัญหาการแสดงแบบอักษรที่ผู้ใช้ (Client) ไม่ได้มีการติดตั้งไว้ (ซึ่งอาจจะรวมถึงโหลด Web font ไม่สำเร็จ) โดยการเลือกแบบอักษรทีละชื่อในรายการจากซ้ายไปขวาซึ่งหากพบแบบอักษรที่สามารถใช้งานได้ตัว Browser ก็จะหยุดกระบวนการดังกล่าว

อดีตที่เคยเป็นมาและการสร้าง font-family หลอก ๆ

ในยุคกลางของเว็บไซต์นี้ (ไม่กล้าใช้คำว่ายุคแรก เนื่องจากช่วงที่เริ่มไม่ได้มีการแก้ CSS เพื่อปรับเปลี่ยนแบบอักษร) ผมมีความต้องการให้ในเว็บนี้มีการใช้แบบอักษรที่ค่อนข้างสะท้อนรสนิยมส่วนตัวมาก ๆ ซึ่งในตอนนั้นชื่นชอบสื่อประชาสัมพันธ์ภาษาไทยของ Apple ที่มีการใช้แบบอักษรพิเศษในอักขระ (Glyph) ภาษาไทย และเนื่องจากแบบอักษรพิเศษดังกล่าวไม่ใช่ของฟรี ผมก็ไม่ได้คาดหวังการแสดงผลของผู้ใช้ทั่วไปมากว่าจะต้องดูแล้วดีเท่าที่ผมดูเอง (ซึ่งดูจากใน Mac, iOS เป็นหลัก) ผมจึงทุ่มเทกับการทำให้หน้าเว็บมันดูดีที่สุดในสายตาของผม จึงได้หยิบยกเทคนิคการสร้าง font-family หลอก ๆ มาใช้ในการสร้าง Font stack ที่ใช้แสดงผลในหน้าเว็บโดยในเบื้องต้น ผมทำเฉพาะส่วนของการแสดงหัวข้อต่าง ๆ ในเว็บ ตามตัวอย่าง CSS ด้านล่างนี้

@font-face {
  font-family: *customname*;
  font-style: normal;
  font-weight: *weight*;
  src: local('*proprietary-postscript-name*'), local('*proprietaryname*'),
    local('SukhumvitSet-Light'), local('Sukhumvit Set'),
    local('.SukhumvitSetUI-Light'), local('.Sukhumvit Set UI');
}

h1, h2, h3, h4, h5, h6 {
  font-family: *customname*;
}

ตัวอย่างที่อาจจะน่าสนใจใน CSS นี้คือ การอ้างถึงแบบอักษรซ่อนที่อยู่ใน iOS (ที่มี . นำหน้านั่นแหละ) และมีการอ้างชื่อแบบ Postscript (ชื่อที่ไม่มีเว้นวรรคแต่ใช้ - แทน) หลังจากนั้นก็มีพัฒนาการอีกนิดหน่อยคือ มีการใช้แบบอักษรพิเศษนี้เฉพาะอักขระภาษาไทย และให้อักขระภาษาอังกฤษใช้ตัวอักษรอื่น ๆ ที่ดูเข้าคู่กันแทน ซึ่งจะขอแสดงเทคนิคนี้ในก้อน CSS ถัดไป

การเปลี่ยนมาเป็น Kanit, Sarabun สำหรับอักขระไทย และแบบอักษรของระบบปฏิบัติการสำหรับส่วนที่เหลือ

เหตุผลเท่าที่พอจะนึกออกถึงการเลิกใช้แบบอักษรตัวนั้น น่าจะมาจากการที่ได้เห็นแบบอักษรไทยใหม่ ๆ ที่ดูน่าสนใจจาก Google Fonts ประกอบกับผมเริ่มมีความคิดอยากให้หน้าเว็บมันดูเหมือนกันในทุกระบบปฏิบัติการที่ได้ใช้ (ตอนนั้นใช้ Surface Pro ได้พักใหญ่ ๆ แล้ว) ผมจึงได้หยิบแบบอักษรตัวใหม่ คือ Kanit และ Sarabun มาใช้ในส่วนของหัวข้อและเนื้อความภาษาไทย และใช้แบบอักษรตามธีม Barrio สำหรับส่วนของภาษาอังกฤษ

การเปลี่ยนแปลงนี้ทำให้พบข้อจำกัดอย่างหนึ่งของเทคนิค font-family หลอก ๆ นั่นคือ มันไม่สามารถอ้างถึง Web font ได้ ทำให้ต้องไปหา URL ของแบบอักษรจากไฟล์ CSS ของ Google เอง

@font-face {
  font-family: *customname*;
  font-style: normal;
  font-weight: *weight*;
  src: local('-apple-system'), local('BlinkMacSystemFont'),
    local('.SFNSDisplay-Light'), local('.SFUIDisplay-Light'),
    local('SystemFont'), local('HelveticaNeue-Light'), local('Helvetica Neue'),
    local('Helvetica-Light'), local('Helvetica'), local('ArialMT'), local('Arial');
}
@font-face {
  font-family: *customname*;
  font-style: normal;
  font-weight: *weight*;
  src: local('Kanit Light'), local('Kanit-Light'),
    url(https://fonts.gstatic.com/s/kanit/v7/nKKZ-Go6G5tXcraBGwCKd6xBDFs.woff2) format('woff2');
  unicode-range: U+0E01-0E5B, U+200C-200D, U+25CC;
}

จากตัวอย่าง CSS นี้แสดงเฉพาะวิธีการนิยาม font-family หลอก ๆ ไม่ได้แสดงส่วนของ selector นิยาม heading และ body

ในภายหลังผมเริ่มมีความต้องการอยากจะปรับเปลี่ยนแบบอักษรในอนาคต การพัฒนาจึงถูกปรับเปลี่ยนจากการ Hard code ทุกอย่างบนไฟล์ CSS หลักเป็นวิธีอื่นเพื่อให้เข้ากับการตั้งค่าแบบอักษรของธีม Barrio ทำให้ผมทราบในภายหลังว่า URL ของแบบอักษรที่ใช้นี้มีการปรับเปลี่ยนได้ (จากตัวอย่างเห็นเป็น v7 ซึ่งตอนที่เริ่มทำนั้นยังเป็น v1) สิ่งนี้ทำให้ผมเริ่มตระหนักถึงภาระในการบำรุงรักษา (Maintenance) ค่าเหล่านี้ว่ามันมีความลำบากโดยไม่จำเป็น นั่นจึงทำให้ผมมองหาแนวทางการพัฒนาต่อไปเพื่อลดภาระนี้

การ Fallback ใน Font stack

ความสามารถหนึ่งที่ผมหลงลืมไปใน Font stack นั่นคือ การเลือกใช้แบบอักษรจนกว่าจะเจอตัวที่ใช้แสดงอักขระในข้อความนั้นได้อย่างสมบูรณ์ เนื่องจากแต่ก่อนตอนที่ใช้เทคนิคการนิยาม font-family หลอก ๆ นั้นผมได้ล็อก unicode-range ภาษาไทยไว้กับแบบอักษรที่ต้องการเสมอ เมื่อได้ทราบถึงเรื่อง Fallback นี้ จึงทำให้ผมสามารถปลดล็อกการใช้ Web font ได้แล้ว สิ่งที่ผมต้องทำจริง ๆ แล้วก็คือ การกำหนด Font stack ให้มีแบบอักษรที่ไม่มีตัวอักขระไทยขึ้นก่อนแล้วจึงตามด้วยแบบอักษรที่ผมต้องการให้ใช้อักขระภาษาไทย นั่นจึงนำมาสู่ตัวอย่าง CSS ชุดถัดไปที่ได้มีการนำเทคนิคเดิมออกไปทั้งหมด แทนที่ด้วย Font stack อย่างที่ควรจะเป็น (ไม่มีการแสดงโหลด Web font)

h1, h2, h3, h4, h5, h6 {
  font-family: var(--ezy-font-sans-serif), var(--ezy-head-th),
    sans-serif, var(--ezy-emoji);
}

ตัวอย่างที่แสดงนี้มีการใช้ CSS Variable ที่ได้ดัดแปลงค่ามาจาก --bs-font-sans-serif ซึ่งในรายการดังกล่าวจะแบ่งรายการอักษรออกเป็น 3 ชุด คือ ชุดอักษรของแต่ละระบบปฏิบัติการ, sans-serif, ชุดอักษร Emoji ของแต่ละระบบปฏิบัติการ ผมจำเป็นต้องเอาแบบอักษรไทยของผมไว้ด้านหน้า sans-serif หากไม่ทำเช่นนั้นตัวอักษรไทยของผมจะถูกแทนด้วยค่าปริยายของ Browser แทน

เหมือนปัญหาจะจบแล้ว แต่ก็ยังไม่จบครับ เนื่องจากใน Safari ใน macOS และ iOS นั้นมีพฤติกรรมในการแสดงผลแบบอักษร system-ui และ -apple-system ต่างจาก Chromium และ Firefox

การแก้ปัญหาการแสดงผลใน Safari

ปัญหาที่เจอนี้ไม่น่าจะถือว่าเป็นบั้กของ Safari (ซึ่งก็ไม่รู้ว่าควรมองว่าเป็นบั้กของ Chromium และ Firefox ด้วยรึเปล่า) เนื่องจากผมมองว่าเป็นรายละเอียดในการนิยามว่า system-ui และ -apple-system บน macOS และ iOS นั้นควรแสดงด้วยตัวอักษรใดกันแน่ สำหรับใน Chromium และ Firefox นั้น เลือกที่จะใช้แบบอักษร .SF NS ในการแสดงผล ซึ่งแบบอักษรดังกล่าวไม่มีตัวอักขระภาษาไทย จึงทำให้การ Fallback ที่คาดหวังไว้เป็นไปอย่างถูกต้อง

แต่สำหรับ Safari นั้นทาง Apple ในฐานะเจ้าของระบบปฏิบัติการ ได้เลือกให้ใช้ .AppleSystemUIFont ซึ่งเป็นแบบอักษรพิเศษอีกตัวที่ Browser ตัวอื่นไม่รู้จัก และความพิเศษของแบบอักษรตัวนี้คือความสามารถในการ Fallback อักขระภาษาไทย ไปใช้ตัวของ Thonburi ซึ่งผมไม่ต้องการ การแก้ปัญหาที่อาจจะคิดว่าง่ายสุดนั่นคือ ก็เลิกใช้ system-ui และ -apple-system ไปใช้ .SF NS เสียซิ ทำให้พบปัญหาต่อมานั่นคือ ตัว Safari บน macOS เอง ไม่สามารถแสดงผล .SF NS ได้อย่างถูกต้องเนื่องจากความซับซ้อนของแบบอักษรดังกล่าวเอง (อันนี้ก็ไม่แน่ใจว่า Safari มันคิดมากไป หรือ Browser อื่นมันคิดน้อยเลยข้าม ๆ อะไรที่มันไม่รู้จักไปจนแสดงผลได้ถูกต้อง) ส่วน Safari บน iOS พบว่าไม่สามารถแสดงผล .SF UI ได้ (บน iOS ไม่มี .SF NS) สำหรับกรณี iOS ยังไม่พบแนวทางแก้ปัญหา

ในช่วงแรกที่คิดแก้ปัญหานี้ ผมไปพบว่ามีการทำ CSS Hack เพื่อเช็คว่า Browser ที่ใช้คือ Safari (ผมมองว่ามันเป็นปัญหาเฉพาะ Safari เลยจะแก้เฉพาะกรณีของมัน มาสังเกตในภายหลังว่า ตัว Chromium เองก็ไม่สามารถใช้งานแบบอักษร .SF NS ได้ตรง ๆ ต้องใช้งานผ่าน system-ui จึงต้องใช้แนวทางนี้แก้ปัญหา) และพบว่าตัว Safari นั้นสามารถใช้งานแบบอักษรโดนอ้างชื่อแบบ Postscript ได้ (ตามที่ได้กล่าวถึงไปแล้ว) จึงได้ไปหาชื่อ Postscript ในแบบอักษร .SF NS ออกมาเฉพาะส่วนที่คิดว่าจำเป็นต้องใช้นั่นคือ

  • .SFNS-Regular
  • .SFNS-RegularItalic
  • .SFNS-Bold
  • .SFNS-BoldItalic

และได้มีการนิยาม selector เพิ่มเติม เพื่อดักการแสดงผล ตัวหนา, ตัวเอียง, และตัวหนาและเอียง ซึ่งพบว่าแสดงผลดังกล่าวสามารถเขียน selector ได้หลายแบบมาก ได้แก่

  • b, strong { ... }
  • i, em { ... }
  • b i, b em, strong i, strong em, i b, i strong, em b, em strong { ... }

ผมมองว่าดูเป็นการเพิ่มความงุนงงมากไปหน่อย จึงกลับมาคิดถึงเทคนิคการทำ font-family หลอก ๆ ว่าเราก็แค่นิยาม font-family ที่ได้กำหนดพวก font-weight และ font-style ที่เพียงพอและอ้างถึงชื่อแบบ Postscript เหล่านี้ก็ได้นี่ที่เหลือก็ให้กลไกของ Browser จัดการโดยเราไม่ต้องเขียน selector ดักเอง จึงทำให้ได้วิธีในการแก้ปัญหาของ Safari ดังนี้

  • นิยาม CSS Variable ไว้โดยแบ่งเป็น
    • ตัวแปรสำหรับแบบอักษรระบบที่จะใช้ (เบื้องต้นเป็น system-ui และ -apple-system)
    • ตัวแปรสำหรับแบบอักษรอื่น ๆ (ท่อนหลัง -apple-system ก่อนถึง sans-serif จาก font stack อันที่แล้ว)
    • ตัวแปรสำหรับแบบอักษรไทยของผม
    • ตัวแปรสำหรับ Emoji (ท่องหลัง sans-serif จาก font stack อันที่แล้ว)
  • ทำ CSS Hack เพื่อแยก Safari
    • มีการนิยามค่าตัวแปรแบบอักษรระบบที่จะใช้เป็นชื่อแบบอักษรหลอก
    • มีการนิยาม font-family ด้วยชื่อแบบอักษรหลอก ทุก font-weight และ font-style

ผลลัพธ์ของ CSS (บางส่วน) จะเป็นไปตามนี้

:root {
  --ezy-systemfont: system-ui,-apple-system;
  --ezy-font-sans-serif-no-system: "Segoe UI",Roboto,"Helvetica Neue",Arial,
    "Noto Sans","Liberation Sans";
  --ezy-emoji: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol",
    "Noto Color Emoji";
}

@supports selector(:nth-child(1 of x)) {
  :root {
    --ezy-systemfont: 🍎;
  }
  @font-face {
    font-family: 🍎;
    font-style: normal;
    font-weight: 100;
    src: local(".SFNS-Thin");
  }
  @font-face {
    font-family: 🍎;
    font-style: italic;
    font-weight: 100;
    src: local(".SFNS-ThinItalic");
  }
  /* Omit other styles and weights! */
}

h1, h2, h3, h4, h5, h6 {
  font-family: var(--ezy-systemfont), var(--ezy-font-sans-serif-no-system),
    var(--ezy-head-th), sans-serif, var(--ezy-emoji);
}

จริง ๆ ยังมีส่วนของ font-stretch ที่ยังไม่ได้ทำต่อเนื่องจากใน .SF NS นั้นมีการนิยาม stretch ที่ผมยังสับสนอยู่และคิดว่าในหน้าเว็บไม่ได้มีการใช้งานจึงข้ามส่วนนี้ไป ซึ่งอาจจะมองว่าวิธีที่ผมเลือกนี้ค่อนข้างจะ Over engineer ไปมากแต่ถ้ามองว่าเป็นฐานสำหรับการทำ font-stretch ซึ่งในส่วนนี้คงต้องรอดูกันว่าตัวธีมหรือตัวจัดการข้อความบนหน้าเว็บจะรองรับมันเมื่อใด

สรุป

สำหรับผู้อ่านที่อาจจะตาลาย กับเนื้อหาข้างต้นนี้ ผมขอสรุปสิ่งที่ Font stack ตัวปัจจุบันที่เว็บนี้ใช้งานอยู่ประกอบไปด้วย

  • รายการแบบอักษรที่ต้องการใช้ โดยผมเลือกที่จะเรียงแบบอักษรที่ไม่มีอักขระภาษาไทยไว้ก่อน ตามด้วยแบบอักษรที่มีอักขระภาษาไทยซึ่งเป็น Web font และเผื่อ sans-serif เป็นค่าปริยาย ตามด้วยแบบอักษรกลุ่มที่มีอักขระ Emoji
  • สำหรับกรณี Safai บน macOS และ iOS ผมได้มีการนิยาม font-family หลอก ๆ 1 ตัวซึ่งประกอบไปด้วยอักขระซึ่งอ้างถึงด้วยชื่อ Postscript ของ .SF NS โดยนิยามทุก font-weight และ font-style เพื่อให้กลไกของ Browser สามารถนำไปใช้งานได้ในทุกรูปแบบ

หวังว่าจะเป็นแรงบรรดาลใจในการแก้ปัญหาการแสดงผลบนหน้าเว็บของท่านผู้อ่านนะครับ

Tags