
FontFreeze 是我在 2022 年七月開啟的開源專案,之前我就已經有在一兩篇文章裡一筆帶過,而一直到現在才終於覺得該是時候來寫一些相關的開發心得文章了。
內容目錄
緣起
身為軟體工程師,程式碼的字型看得順不順眼這種事肯定是很重要的,因為一天當中絕大部分的時間都是在盯著程式碼看,試問各位能忍受整天盯著看不順眼的事物嗎?而且咱們這些工程師一堆都是亞斯,個人的頑固堅持更是少不了的,一定是非得要找到那個所有細節上都完美的字型、工作起來才會愉快。
然而,縱使有那麼多的 coding 專用字型,要打從一開始就完全完美還真的是近乎不可能。有的是@符號沒有繞一整圈、有的是0沒有加上斜線、又有一些則沒有提供連字符號(ligature 1),還有一些則根本打從一開始就太寬、太窄、太扁、太高、太粗……到頭來,我實在不曾看過一款字型是打從預設設定開始就是無可挑剔的。
重點來了:注意到我說的是「預設設定」。OpenType 字型這種東西是有一種功能叫做特徵(feature)的。藉由開啟或關閉字型設定的某些特徵,很多的 coding 字型都可以在一定的程度上面自訂其外觀。上面的圖片中就展示了 FiraCode 字型的例子:預設的@符號是沒有畫一整圈的,但是如果把ss05特徵打開,它就會變成是我要的樣子了。在 VS Code 當中,這個功能可以透過 editor.fontLigatures 選項來設定 2。
OK,到目前為止還行,但問題來了:我會用的 IDE 又不是只有 VS Code 而已。我也會去用 Visual Studio、Android Studio、XCode 等等的這些 IDE。截至本文寫成為止,Visual Studio 是完全不支援這些特徵切換的,而 Android Studio 則只支援連字符號開關、碰不了其它特徵。這樣一來,我在 VS Code 以外的環境中就又得去忍受不滿意的字型了。
而在當年,網路上出了一個救星:Font Squirrel Webfont Generator。這個線上工具的其中一個進階功能就是可以把字型的特徵給「定住」,產生一個預設狀態就是某些指定特徵有被開啟的特製字型,如此一來不管在什麼環境底下都會是我要的結果。它曾經一度可以滿足我的需求,但偏偏,後來這個工具不知道從什麼時候開始變得根本就不能用了:不管丟什麼字型給它,都沒辦法正確地輸出字型。
隨後我又找到了另外一個工具 OpenType Feature Freezer 也是在做這件事,但是它也有一些問題。首先它的 GUI 在我印象中並沒有很好用,其次則是它只支援跟單一字元有關的特徵(跟它的原理有關,等一下說明)、也就是說一些跟連字有關的特徵就沒辦法了。
另一個對我這個專案影響很大的工具是 vfit,它的功能是把可變字型(variable font 3)定住一組參數來產生單一實例。這個跟定住特徵也是非常相似的概念,但是當時就沒有一個工具兼具這些功能。隨著我認識了這些工具,加上 Font Squirrel 變得不能用,我就下定決心自己做一個新的工具來統整並且改良它們的功能。
開發
接下來發生的事情,就如同我在之前的文章中提到的,我就用了 Pyodide + fonttools 來把程式的核心打造了出來,甚至後來還為了讓它支援 WOFF2 格式的輸出而把 brotli 加到了 Pyodide 裡面。
當然,我在另一篇文章中曾經解釋過「除非實在沒別的辦法,不然 Pyodide 這個東西能不用則不用」、因為 Pyodide 很肥大而且速度很慢,但是在這邊的應用當中我至今沒有更好的選項,因為我始終找不到一個功能像 fonttools 這麼完整的開源 TrueType/OpenType 操作程式庫。注意到我說「操作」程式庫,因為絕大部份的 TrueType 程式庫的功能都只是讀取字型以及渲染而已,並沒有對字型的內容進行修改的功能。
我在猜,也許開源的應用程式 FontForge 的核心會具有不亞於 fonttools 的功能,但是在沒有完整文件的情況下我很難確定這一點,而且我不可能為了效能考量、就冒著做白工的風險、去花大量的心力把 FontForge 的核心抽離出來,尤其是今天 FontFreeze 的載入速度還算是相當可以被接受的情況之下。至於用 JavaScript/TypeScript 撰寫的程式庫,截至本文寫成為止在功能上都與 fonttools 相差甚遠。
原理
那麼 FontFreeze 是如何做到把字型凍結的呢?這要分成幾個部份來談。
先從「將可變字型的參數定住」講起,因為這個最單純,它就是直接使用 fonttools 內建的 instantiateVariableFont 功能(前面提到的 vfit 也是一樣)。直接把可變字型的實例跟各個可變軸的目標值當作參數傳進去,該函數就會自動弄出一個定下來的版本,所以這邊完全不需要我實作任何東西。
再來比較有學問的部份則是「如何強迫開啟或關閉特定的特徵」。事實上,OpenType 的規格當中並沒有功能可以直接提供這樣的能力——畢竟,特徵這種東西的用意、本來就是可以讓使用者自由選擇「要開還是要關」的。不過,各種 OpenType 規範所定義的特徵其實在行為上稍微有點不同。就以 cv01-cv99(Character Variant,字元變體)和 ss01-ss20(Stylistic Set,風格集) 這幾個通常我們最關切的特徵來說,在規範裡面寫著「UI 建議:預設應該要關閉,但使用者可以在設定頁面中開啟它們」。相對地,特徵 calt(Contextual Alternates,上下文替代 4)的規範則是說「UI 建議:預設應該要啟用」。諸如此類地,有些特徵打從一開始就建議客戶端在實作的時候要預設關閉、而有些則是建議預設開啟。而確實,大部分的應用程式也都是照著這樣的建議在實作的,所以例如 calt 的效果確實通常就是預設開啟的。
既然如此,就有辦法了:假設我們想要固定開啟特徵 cv01 好了,那麼只要將標籤(tag)標記為 cv01 的特徵紀錄(record)改成是 calt 的標籤 5,不就能使得它們預設就是開啟的嗎?而這就是 FontFreeze 的核心概念所在:它其實是把那些「希望固定開啟」的特徵所對應的特徵紀錄改成是 calt、以便利用 OpenType 的實作建議規範、來讓它們預設就是啟用的。反之,如果我希望強制關閉某個特徵,辦法則是把該特徵紀錄裡面的查找(lookup,本質上就是指向具體替換定義的指標)通通刪除光。注意到這邊我只會去改特徵紀錄的標籤或是改變裡面的查找、而不會去刪改特徵紀錄本身,因為要是打亂了特徵紀錄的索引、是會破壞字型的一些其它功能的。
利用這個方法凍結出來的字型,在絕大多數的(照著 OpenType 規範實作的)應用程式中都能夠如預期地運作,不過仍有少數的應用程式(例如 Notepad++)就是連 calt 都不理會。對於那些應用程式,能做到的事情就比較有限了,因為它們那樣的實作方式就是註定不可能有辦法正確顯示連字之類的功能。但是,至少單獨字形(glyph)的替換還是做得到的,而這正是前面提到的 OpenType Feature Freezer 所採用的原理:它完全不去碰特徵紀錄,而是直接把主要的字形換成是特徵裡面定義的替換字形。這個方法雖然只適用於跟單一字元有關的特徵,但是它對於應用程式的相容性更強,所以從 FontFreeze 1.3 版開始、我也一併採用了這個作法。
結語
那麼本篇就先針對字型相關知識的部份分享到這邊;這個專案還有一些其它的面向可以分享,之後有機會再來寫文章。
說來很好笑,FontFreeze 其實是我在 GitHub 上頭獲得最多星的專案,但是它在我的幾個主要的開源專案裡頭反而是我最不花心思的一個:它規模小、開發時間短、功能簡單但完備所以也不太需要頻繁維護。因此,它會獲得最多星的這件事,無非就是因為它照顧的族群就是 GitHub 的最大宗用戶——也就是軟體開發者們的緣故。相較之下,我其它的一些專案雖然投入的心力更多,但是因為那些專案的對象客群並非開發者,所以自然能拿到的星也比較有限。
-
即遇到特定字元的接連組合時,會將它們一起顯示成另外一個樣子。早年這個機制只會應用在字母上(例如仔細看襯線體的 field 的前兩個字母,視各位的系統而定,很可能跟分開的「fi」會有些微的不同 ),而據說到了 2015 年才首次由 FiraCode 將這個概念應用在程式字型的符號之上,例如遇到
!==會變化顯示成!==、>=會變化顯示成>=等等。 ↩ -
雖然它的名字叫這樣,但實際上它可以填入 CSS 中的
font-feature-settings之設定值。 ↩ -
這個跟特徵的切換不同,可變字型是可以調整字體的粗細、間距、傾斜度等等的參數的字型,而且這些參數是可以連續調整的。 ↩
-
簡單來說,相較於連字的觸發條件是「特定的幾個字元接連出現」,上下文替代的觸發條件則是「前面跟後面有些什麼」。例如我們可以規定,只有當「b」緊接著出現在「a」的後面的時候,把那個「b」換成特定的樣子。這個尤其在例如阿拉伯文中是一個常見的需求。 ↩
-
當然,那些特徵紀錄的內容並不符合
calt的定義,但是 OpenType 規範其實並沒有硬性規定calt裡面一定只能放上下文替換(contextual substitution)類型的東西。 ↩
留言