東拼西湊個 webfont service

一直以來,敝社網站面臨著有點令人尷尬的情況:為了閱讀上的美觀,網頁版型的標題應該要是襯線體(明體/宋體),但在不同平台上看到的字體卻總是無法有一致的體驗。為此,我們有幾種可能的作法:

  1. 把各種系統有的襯線體都寫進 CSS,讓瀏覽器自己去 fallback
  2. 直接用 CSS 把整個字體讀進來
  3. 使用 typekit 或是 justfont 這類的動態 webfont service

因為辦公室 90% 以上設備都是 Mac 或是 iOS(辦公室只有我用 PC,孤單寂寞覺得冷),好一段時間我們都使用 solution 1,依序以「冬青體、宋體 TC、儷宋體、新細明體」的順序寫進 CSS。但時不時會因為標題出現了冬青體沒有的漢字,fallback 變成了宋體、儷宋甚至是新細明體時的缺字或是 baseline 歪掉了的問題。baseline 的問題或許可以用 CSS 去解,但兩套不同字體的字重,甚或是不同語系的漢字筆順寫法,總是會在無意間就強暴了閱讀者的眼睛(用 Windows 則永遠都是新細明體,反正一直以來都很醜所以..)。

至於 solution 2,我們曾經嘗試過直接讀入 Google Font 提供的、體積較小的 cwTeXMing,但只有 Big5 字集的結果,是顯示時反而更容易出現缺字的情形,而超過 2MB 的大小,初次讀入頁面的時間與流量成本也是非常可觀的。其他支援 Unicode 的 IPA 明朝或是花園明朝的粗體實在不甚好看,體積也十分龐大,算不上是堪用的解法。

於是我們轉向了 solution 3,直接使用別人提供的解決方案確實是很愉快,把字體設定好,套上 style 便可打完收工,但 script 裝上去兩三天,我們就把免費的 quota 吃完了(typekit 是 25K PV/month,justfont 是 10K PV/month)。要嘛就交個保護費,要嘛就退回到 solution 1。因為服務費用貴貴的,所以我們就繼續讓 Windows 使用者眼睛受傷了(錯)。


精美的思源宋體

這個問題就這樣被我放置 play 了三四個月,直到最近思源宋體的推出。思源宋體在各種字重還有不同語系的筆畫寫法上都有著墨,顯示效果也比其他開源字體來得優異許多。在推出當天便可以用 typekit 所提供的 webfont 服務掛上網頁。但問題依舊,按次計算的費用十分可觀。
如果是在自家網頁顯示思源宋體,到底該怎麼辦呢?快速地想了一下,或許可以這樣:

  1. 付保護費
  2. 寫隻小程式,在 server 端把字體 render 成透明背景的圖片掛進頁面
  3. 自幹 webfont service

solution 1 好貴,我付不起(炸)(醜哭)。網頁整頁只用到標題幾個字,跟整頁字體都使用 webfont 的成本是一樣的,怎麼看都覺得很微妙。
solution 2 已經是行之有年的作法,但遇到 RWD 網頁就會爆炸,不同字體大小或是螢幕解析度也會帶來各種大大小小的問題,實在不是很好。至於一個字一張圖或是把字體轉成 SVG 圖片之類的作法因為太硬派了我們還是當作沒看到好了。

那麼,solution 3 呢?之前曾經看過 timdream 小帥提 – 提姆提拉米蘇 在 IE 6 上(對,不要懷疑,就是 IE 6)實作過中文的 EOT font subset,效果不錯,但印象中似乎是固定的文字集。我也忘了當時他是用甚麼工具去把 font subset 拆出來了(補充:小帥提在 comment 中表示是使用 WangShen Lu 大大提供的 script)。

於是就用 font subset 當關鍵字翻找了一下網路,很快地便找到了 Google 的網頁字體最佳化指南,當中提到了 fonttools 這套工具,其中提供了 pyftsubset 這個指令,可以依照傳入的文字生出不同的 font subset。

那麼,這麼好用的工具要怎麼安裝呢?請點進 GitHub 看一下,因為太簡單了所以這裡也不贅述。

於是我很快速地把 fonttools 裝起來,看了一下 pyftsubset 的說明:


pyftsubset –help

看來只要用 --text 或是 --text-file 就能讓程式生出有那幾個字的 font subset 了。於是便開心地下載了思源黑體的 OTF 檔(我們只用 SemiBold 這個字重),然後打了指令測試:


pyftsubset SourceHanSerifTC-SemiBold.otf –text-file=test.txt

嗯,在 R700 這台老筆電上跑一下就跑完了,看起來沒有錯誤,生出來的 subset 大概 12KB,所以趕緊來寫個 HTML 測試測試:


測試用的 HTML

「唔喔喔!真的會動耶!」


IT’S ALIVE!!

寫到這邊,其實會 Python 的朋友們已經可以自己動手用 fonttools 包個 service 出來了。

但資質駑鈍如我,還是比較孰悉 PHP,所以就用了萬能的 system call 去吐產生的字體檔,輸入的 query string 就是需要使用的文字字元。

作法有很多種,我是直接把輸出導到 /dev/stdout,把結果直接吐出來,因為程式碼太醜了所以我就不貼上來了,大致上就是用 shell_exec("pyftsubset 一大串醜醜的東西") 這樣(遠目)

至於效能問題.. 用 system call 真的.. 會.. 比較.. 慢,不過只要 cache 做得好,除了第一次讀入的人比較悲劇之外,後面的 request 應該堪稱迅速。秉持著一貫地懶惰,我也只用了目前線上機器架構中既有的東西拼湊:對外放個 cache server (Varnish Cache),更外面再放個 CDN (Cloudflare, free tier),防止大量 requests 打爆機器。

嗯,server 端的部分真的就只有這樣,真正有作用的程式碼也大概只有兩行,就算寫得再爛都不會太難讀懂(掩面),需要特別注意的是要送出 CORs header,還有記得防止 XSS 還有 arbitrary code execution 之類的攻擊。

接著就是 client 部分啦。

我採取的是在讀入頁面之後,用 DOM selector 把需要套用字體的物件文字撈出來,sort、unique 之後,送回 server 生出相對應的字集字體,再用 jQuery 套上去的 approach,如此作法有幾個好處:

  1. 對於用到同樣字集的網頁,request uri 會長得一樣,cache 會比較簡單
  2. 產生的 request uri 相對短一點(真的就是短一點點)
  3. script 寫一次就可以到處撒(咦)

那麼,該怎麼動態產生 font-face 的部分呢?嗯,我也不知道(好不負責)

因為我很懶惰,所以用了現成的 FontFace jQuery Plugin 來載入字體。

很快速地讀了一下程式碼,依照需求寫出的 client 端程式碼看起來大概是這樣:


總之就是一坨義大利麵

然後只要打個 loadFont('.someClass'); 就可以動態載入字體了,真是非常愉快呢 ww

需要注意的是,如果在網頁裡要 call 很多次,每次的 font name 要不一樣,否則套用到不同物件時只會讀入第一次載入的 font name 的字集。在這裡我使用了 stack overflow 大法找到了很好很強大的解答,測試了一下會動所以就沒有再多做甚麼修改。

於是這東拼西湊,這個速度不是很快 (!) 而且因為用 query string 所以不能傳太多字進去 (!!) 然後還只能吐一種字體 (!!!) 還弱弱第只支援 modern browser 的 webfont service 就跑起來啦 wwww

從此之後,我們的網頁就有了精美的中文襯線體:


精美啊!

至於讀入字體時的 FOUT, FOIT, FOFT 問題,可以看一下這篇文章,裡面有提供幾個 robust 的解法,加上去就可以解決一些惱人的字體閃爍問題喔 :D

雖然自己 host 還是比不上那些 service provider 的速度,但租台小台 VPS 的成本非常低,加上 CDN 後效能其實沒那麼慘,在用量不大的狀況下就湊合著用吧。

以上就是這次的義大利麵料理指南,大家快去找個小廚房(利益揭示:連結是我的 DigitalOcean 推薦碼,申請成功您可以拿到 USD $10 credit,最小台的機器可以用兩個月喔)來料理一下吧 (?)

補充:剛剛 高偉格 大大說可以直接用 font-spider 來建 service,啊啊啊相見恨晚 QQ

20070802_Notes

因為有朋友說筆記要好好記,所以我就開了這個新的分類,
希望之後可以陸續增加新的東西 @@


Seminar @ NTU

Keywords

  • Parsing: 將字串做分析後,mapping 到某一資料結構上 (通常為 syntax trees) 的過程
  • Semantic Parsing: 將 NL 轉換為 MRL 的過程
  • MRL: Meaning Representation Language
  • Training Set: 在系統開發期間,被拿來餵進系統的訓練資料集
  • Test Set: 相對於 training set,test set 是在系統發展後,拿來評估系統效能的資料集
  • Cross-Validation: ..囧?
  • N-fold Cross-Validation: 將資料分為 N 份,做 N 次評估並取平均值,每一次取其中一組作為 test set

More about Semantic Parsing

  • Natural Language 的語法不如 Programming Language 嚴謹,常會有 ambiguous 的狀況出現
    • Eg. “I saw a man with a telescope.” 有兩種合理解釋
  • Training Data 越大,對於該訓練領域的 precision 越高,但可能會因取樣不足或不夠廣泛而出現系統偏頗的現象

SGP: Symbol Grounding Problem

To done deep-understanding, you have to solve the SGP.


There are 3 types of information needs:

  • Informational: Queries about “How”, “What”, etc.
  • Transactional: Queries about information or financial tradings
  • Navigational: Queries of finding a specific website

What’s the major when it comes to searching in the field of blogs?
Ans. “Informational,” especially queries about opinions.

There are some methods to obtain the blog data:

  • XML Feeds: RSS, ATOM, or something like that
  • HTML: in comparision with XML, it has less semantical information with its tags

Some problems when collecting data

  • There might hundreds of thousand requests within a day
  • How to tell which blog is a sblog or not, and there might be garbages anywhere in the pages

More about “Recall” and “Precision”

If the IR system returns 320 entities it might be about zoo,
and after we checked it through, 250 relatived,
and in the corpus, 400 entities are exactly relative.

The Recall will be: 250 / 400 = 0.625,
and the Precision is: 250 / 320 = 0.781

To make it clear,
Recall = (# of correct data returned) / (# of all correct data in corpus)
Precision = (# of correct data returned) / (# of data returned)

It will be a trade-off, when you want to improve one of them.
However, the higher they are, the better the system is.