◂ BLOG
ZH2026.08.3114 MIN READ

想在網頁上重現鉛字印刷的質感,該怎麼做呢?

csssvgtypographyletterpress

活版印刷的字有一種很特別的樣子。金屬、油墨跟紙互相作用之後,會產生一種特別的美感。曾經風靡一時的康熙字典體,就是一個例子。邊緣不齊,墨色不勻,偶爾缺一角,反而產生了一種人情味,一種別具風味的復古感。

日星鑄字行的張老闆與 justfont 復刻了日星宋體貳號,那如果我想復刻這套字以鉛字的形式印到紙上的感覺,還缺什麼呢?

第一個是變化。字型是一組固定的輪廓,每一個「書」字打出來都長得一模一樣。但活版的鉛字是一顆顆排上去的,即便是同一批印刷稿、同一個字,都會因為沾墨狀態和壓印力道不同,產生不一樣的墨漬形狀。字型能把其中一次的樣子凍結下來,卻沒辦法讓它每一次都不同(其實還是有辦法,只是變成一個字要做很多份)。

而且紙的差異不在字型裡,壓力不在,磨損不在,排字工手上那點誤差也不在。這些都是字被印出來的當下才發生的事,得在字的外面做。

要在瀏覽器裡改變字的輪廓,又不動到文字本身,可以用 SVG 濾鏡。它跟 Photoshop 的濾鏡是同一個概念,差別在於 SVG 處理完後,仍能保留向量與文字,能選取、複製、搜尋。

那麼決定了工具,首先要先研究鉛字壓在紙上,到底發生了什麼事?

紙與螢幕

沒有紙就沒有活版,而紙從來不是一片乾淨的白。

抄紙的時候纖維會順著某個方向排列,所以紙面帶著很細的橫紋直紋。纖維本身也是粗糙的,湊近看是無數個深淺不一的山谷跟凹洞。這些東西單獨拿出來都不明顯,疊在一起才讓白紙看起來像紙。

實作上發現了一個有點反直覺的結論,紙的顆粒必須蓋在字的上面,不是下面。雖然墨是壓在紙上的,但薄薄的墨也會透出紙的那層紋理。

最後的解法是用 ::before::after 這兩個偽元素,一個放在字呈現纖維,一個蓋在字上面呈現顆粒。這樣整張紙不需要任何額外的標籤,一個 <div class="lp lp-paper"> 就有了。

油墨的暈染

鉛字的字面是金屬,刻出來的邊緣是銳利的。但印出來的字不是。

油墨會滲進紙的纖維,所以筆畫的邊緣會往外暈開一點,墨上得多,字會整個胖一圈,如果是小字,字腔甚至會被填掉,「口」變成一個黑塊。

要呈現這個感覺,在技術上叫做卷積,講白了就是「每個點都去看看鄰居有多黑,把鄰居的黑分一點過來」。分完之後邊緣就有了一圈半透明的暈,再決定那圈暈要有多少變成實心的墨,字就跟著胖起來。

繼續比對印刷稿與鉛字原型,會發現實際印出來的字筆畫起頭收尾是沒有尖角的,全部都是圓角修飾,而鉛字原型筆畫是更加銳利的。

換句話說,印刷這個動作本身就會改變字的形狀:尖的地方被磨圓,凹的地方被填滿。這兩個現象實作上是同一個運算,先把整個字的形狀模糊掉,再取平均。凸出來的尖角周圍沒什麼墨,模糊之後濃度掉到平均以下,就被削掉了;凹進去的內角周圍全是墨,模糊之後濃度更高,於是被填了起來。

印壓

活版是靠壓力把墨轉印到紙上的,壓多壓少差很多。

壓力不夠,紙跟字面接觸不完全,印出來會有沒沾到的空白。壓得太重,墨被擠到字的外圍,筆畫周圍多出一圈。

鉛字的磨損

鉛的質地軟,反覆印刷會自然磨耗,這也是日星那批銅模需要修復的原因。

新字的瑕疵來自鑄造,是表面細細一點一點的砂眼,舊字則是多次印刷崩掉的腳,或者凸起本身的磨損,那是完全不同的尺度。所以字愈舊,不只缺口變多,單個缺口本身也會變大。

內文與大字的斑駁

到這裡四個成因都調好了,看起來挺像樣。然後我把字級放大,感覺卻又不對了。

原因是這些缺角、砂眼的尺度,是用絕對長度定義的,不會跟著字級一起縮放。同一組參數,在 16px 的內文上是恰到好處的斑駁;放大到 60px,那些「一點點」的缺口還是原來那麼大,卻整塊整塊地卡在筆畫中間。

所以大字不能用同一招。內文級數走的路子是「挖」,拿噪點當遮罩,把零星的像素挖掉;大字則改成「調密度」,讓低頻的噪點去乘上墨的濃度。筆畫的結構完整保留,只是有的地方吃墨飽、有的地方虛 —— 像印章蓋不勻的那種樣子。

這就是為什麼最後會是四支濾鏡而不是一支:內文小字、一般行文、標題、大字,各有各的參數。而既然幾號字該配哪一支是固定的,那就乾脆讓字號本身同時決定字級跟濾鏡。

排版

前面講的都是單顆鉛字發生的事,而鉛字是一顆一顆從字架上撿出來排進版盤的,所以每顆字的狀態其實都不一樣。比旁邊的字凸一點,就會承受更大的印壓,物理世界也難以做到完美對齊,每顆鉛字也可能歪一點點。

麻煩的是這件事沒辦法用純 CSS 做,因為 CSS 碰不到單一個字。::first-letter 只能選到第一個字,而能選到「第 N 個字」的 ::nth-letter() 提案,講了十幾年至今沒有任何瀏覽器實作。所以只能退一步,把每個字包一個自己的標籤。

包好之後,歪斜跟濃淡就都回到純 CSS 的範圍了,用好幾個互質的週期疊在一起,每 11 個字歪一次、每 13 個字濃一次、每 17 個字、每 23 個字⋯⋯ 週期挑大的、彼此又除不盡,整段排下來就有感覺了。

玩玩看

四個成因加上字級,湊在一起。幾組預設是常見的印壞情況。

底下印的是實際算出來的濾鏡參數。你會看到動一個成因,好幾個數字會一起變 —— 那個對應關係其實才是整套東西的核心,剩下的都只是實作。

規格書的模糊

做到一半,我在 Safari 上發現整片字糊成一坨,前面辛苦做的缺角跟濃淡全部不見了。

查下來發現兇手是半徑只有 0.28 個像素的模糊。問題在於,這種次像素等級的模糊,兩家瀏覽器引擎的算法根本不一樣:WebKit 走的是規格書建議的近似作法,Chrome 用的 Skia 老老實實算真的高斯模糊。

解法是把模糊換成係數寫死的固定核心。規格把每個數字都定死了,引擎沒有近似的空間,兩邊就算得出一模一樣的結果。

活字在螢幕上

一路做下來,最意外的其實不是哪個技術細節,而是這些效果其實每一道都對應到一件物理世界實際發生的事:滲墨、壓力、磨損、鬆動。當初覺得「這樣看起來比較好看」的直覺,回頭查資料時多半都能找到一個物理上的理由,而看起來怪怪的,通常就是做錯了的地方。

整套東西打包成 kappan,MIT 授權。dist/ 裡的 CSS 與 SVG 濾鏡檔案貼上就能用,不需要建置步驟也不綁任何框架。

想看看實際效果,這裡有一本見本帖,版式仿的是十九世紀上海美華書館的活字樣本,字號用的是真的號數制。日星宋貳的銅模在張老闆手上復活了一次;能不能讓它印在紙上的那個樣子,也在螢幕上活過來,大概還有得研究。

◂ BACK TO BLOG千秋稻荷社印書館