
Demo:千秋印書館 - 在網頁上重現活版印刷的專案。
活版印刷的字有一種很特別的樣子。金屬、油墨跟紙互相作用之後,會產生一種特別的美感。曾經風靡一時的康熙字典體,就是一個例子。邊緣不齊,墨色不勻,偶爾缺一角,反而產生了一種人情味,一種別具風味的復古感。
日星鑄字行與 justfont 在去年共同推出了復刻鉛字銅模的日星宋貳,是一款非常漂亮且特別的宋體。但他著重的是字型本身的數位化,而不是鉛字印刷形態的保存。我一直在想,那有沒有辦法復刻鉛字印到紙上的感覺?
第一個要想辦法還原的是變化。字型是一組固定的輪廓,每一個「書」字打出來都長得一模一樣。但活版的鉛字是一顆顆排上去的,即便是同一批印刷稿、同一個字,都會因為沾墨狀態和壓印力道不同,產生不一樣的墨漬形狀。字型能把其中一次的樣子凍結下來,卻沒辦法讓它每一次都不同(其實還是有辦法,只是變成一個字要做很多份)。
而紙的差異不在字型裡,壓力、磨損、排版出來的誤差,這些都是字被印出來的當下才發生的事,得在字的外面做。
要在瀏覽器裡改變字的輪廓,又不動到文字本身,可以用 SVG 濾鏡。它跟 Photoshop 的濾鏡是同一個概念,差別在於 SVG 處理完後,仍能保留文字本身,能被選取、複製、搜尋。
決定了工具,接下來要先研究鉛字壓在紙上,到底發生了什麼事?
紙與螢幕
沒有紙就沒有活版,而紙從來不是一片乾淨的白。
抄紙的時候纖維會順著某個方向排列,所以紙面帶著很細的橫紋直紋。纖維本身也是粗糙的,湊近看是無數個深淺不一的山谷跟凹洞。這些東西單獨拿出來都不明顯,疊在一起才讓白紙看起來像紙。
實作上發現了一個有點反直覺的結論,紙的顆粒必須蓋在字的上面,不是下面。雖然墨是壓在紙上的,但薄薄的墨也會透出紙的那層紋理。
最後的解法是用 ::before 跟 ::after 這兩個偽元素,一個放在字呈現纖維,一個蓋在字上面呈現顆粒。這樣整張紙不需要任何額外的標籤,一個 <div class="lp lp-paper"> 就有了。
油墨的暈染
鉛字的字面是金屬,刻出來的邊緣是銳利的。但印出來的字不是。
油墨會滲進紙的纖維,所以筆畫的邊緣會往外暈開一點,墨上得多,字會整個胖一圈,如果是小字,字腔甚至會被填掉,「口」變成一個黑塊。
暈開多少也看紙。塗佈紙表面封住了,墨停在上面幾乎不暈;新聞紙很平,卻很吸墨,邊緣就滲開一圈。所以紙在這裡拆成兩個成因:粗糙管邊緣被推歪,吸墨管暈。
要呈現這個感覺,在技術上叫做卷積,講白了就是「每個點都去看看鄰居有多黑,把鄰居的黑分一點過來」。分完之後邊緣就有了一圈半透明的暈,再決定那圈暈要有多少變成實心的墨,字就跟著胖起來。
繼續比對印刷稿與鉛字原型,會發現實際印出來的字筆畫起頭收尾是沒有尖角的,全部都是圓角修飾,而鉛字原型筆畫是更加銳利的。
換句話說,印刷這個動作本身就會改變字的形狀:尖的地方被磨圓,凹的地方被填滿。這兩個現象實作上是同一個運算,先把整個字的形狀模糊掉,再取平均。凸出來的尖角周圍沒什麼墨,模糊之後濃度掉到平均以下,就被削掉了;凹進去的內角周圍全是墨,模糊之後濃度更高,於是被填了起來。
但這招只能用在大字。內文級數的明體橫畫不到一個像素,模糊之後濃度掉到門檻下,整條橫畫還沒進到後面的濾鏡就先被切掉了。所以小字的積墨另外做:一樣先模糊,讓相鄰筆畫的墨相加,但不切門檻,而是把原本的字身蓋回去。窄縫與濁點會黏起來,髮絲卻一根都不少。
印壓
活版上的壓力不夠,紙跟字面接觸不完全,印出來會有沒沾到的空白。壓得太重,墨被擠到字的外圍,筆畫周圍多出一圈。
「壓太重」跟「墨太多」其實是兩件不同的事,墨太多是墨膜厚,整條筆畫均勻地胖出去,字腔被填掉;壓太重是墨被字面擠到外圍,邊緣堆起一圈、中央反而變薄,被擠出去那圈墨順著纖維走,邊界也不規則。所以壓力自己有一組效果:把筆畫最外圈留實、中央壓淡,再讓暈圈單獨抖一次,字身本身不動。
壓過頭還有一個副作用,字會被壓出凹陷感。在以前,壓力上太大會導致鉛字更快的被磨損,當時的印刷工的技術展現是印的越輕越好,但到了現代,凹痕反而成活字印刷特有的美學。
那麼凹痕要怎麼呈現呢?坑本身是看不見的,看得見的是光打在坑壁上,光從左上來,筆畫左上側多一條背光的暗線,右下側多一條受光的亮線,落在墨外面那圈紙上。做法不能單純用打光濾鏡,而是把字身脹開一個像素當坑的範圍,往右下平移一格後,沒被蓋到的那圈是左上壁,反向平移剩下的是右下壁,各自填上半透明的黑與白,再用固定核心柔化一次。

鉛字的磨損
鉛的質地軟,反覆印刷會自然磨耗,這也是日星那批銅模需要修復的原因。
新字的瑕疵來自鑄造,是表面細細一點一點的砂眼,舊字則是多次印刷崩掉的腳,或者凸起本身的磨損,那是完全不同的尺度。所以字愈舊,不只缺口變多,單個缺口本身也會變大。
內文與大字的斑駁
到這裡幾個成因都調好了,看起來挺像樣。然後我把字級放大,感覺卻又不對了。
原因是這些缺角、砂眼的尺度,是用絕對長度定義的,不會跟著字級一起縮放。同一組參數,在 16px 的內文上是恰到好處的斑駁;放大到 60px,那些「一點點」的缺口還是原來那麼大,卻整塊整塊地卡在筆畫中間。
所以大字不能用同一招。內文級數走的路子是「挖」,拿噪點當遮罩,把零星的像素挖掉,缺口的位置固定、只調缺掉多少墨,參數拉過去時才不會整片突然消失;大字則改成「調密度」,讓低頻的噪點去乘上墨的濃度。筆畫的結構完整保留,只是有的地方吃墨飽、有的地方虛 —— 像印章蓋不勻的那種樣子。
這就是為什麼最後會是四支濾鏡而不是一支:內文小字、一般行文、標題、大字,各有各的參數。而既然幾號字該配哪一支是固定的,那就乾脆讓字號本身同時決定字級跟濾鏡。
字內的墨量
拿日星鑄字行的貳號宋體實印稿並排比對時,有一件事一直對不上:實印的字不是均勻的黑。「日」的上半比下半淡,「鑄」的左半比右半飽,同一顆字裡有一側吃墨足、一側虛。邊緣再怎麼調都做不出這個,因為它不是邊緣的事。
成因是鉛字沒有坐平。字面只要比版面歪一點點,壓力就不是整面均勻落下,高的那側墨轉印得足,低的那側就虛。波長跟字身差不多,是「一顆字裡有一塊淡」,不是筆畫上的斑點。
做法把大字那道「調密度」搬到每一級:一張低頻的噪點當濃淡場,乘進墨的透明度。差別在位置,內文級數的濾鏡最後會把邊緣拉硬,灰階全部被推回實心,所以這一道要排在拉硬之後,才留得住深淺。另一個坑是噴出來的噪點太集中在中間值,直接拿來用整張場幾乎都是 1,得先以中間為軸拉開對比,才會真的有一塊掉下去。
排版
前面講的都是單顆鉛字發生的事,而鉛字是一顆一顆從字架上撿出來排進版盤的,所以每顆字的狀態其實都不一樣。比旁邊的字凸一點,就會承受更大的印壓,物理世界也難以做到完美對齊,每顆鉛字也可能歪一點點。
麻煩的是這件事沒辦法用純 CSS 做,因為 CSS 碰不到單一個字。::first-letter 只能選到第一個字,而能選到「第 N 個字」的 ::nth-letter() 提案,講了十幾年至今沒有任何瀏覽器實作。所以只能退一步,把每個字包一個自己的標籤。
包好之後,歪斜跟濃淡就都回到純 CSS 的範圍了,用好幾個互質的週期疊在一起,每 11 個字歪一次、每 13 個字濃一次、每 17 個字、每 23 個字⋯⋯ 週期挑大的、彼此又除不盡,整段排下來就有感覺了。
玩玩看
六個成因加上字級,湊在一起。幾組預設是常見的印壞情況。
底下印的是實際算出來的濾鏡參數。你會看到動一個成因,好幾個數字會一起變 —— 那個對應關係其實才是整套東西的核心,剩下的都只是實作。
規格書的模糊
做到一半,我在 Safari 上發現整片字糊成一坨,前面辛苦做的缺角跟濃淡全部不見了。
查下來發現兇手是半徑只有 0.28 個像素的模糊。問題在於,這種次像素等級的模糊,兩家瀏覽器引擎的算法根本不一樣:WebKit 走的是規格書建議的近似作法,Chrome 用的 Skia 老老實實算真的高斯模糊。
解法是把模糊換成係數寫死的固定核心。規格把每個數字都定死了,引擎沒有近似的空間,兩邊就算得出一模一樣的結果。
活字在螢幕上
一路做下來,最意外的其實不是哪個技術細節,而是這些效果其實每一道都對應到一件物理世界實際發生的事:滲墨、壓力、磨損、鬆動。當初覺得「這樣看起來比較好看」的直覺,回頭查資料時多半都能找到一個物理上的理由,而看起來怪怪的,通常就是做錯了的地方。
整套東西打包成 kappan,MIT 授權。dist/ 裡的 CSS 與 SVG 濾鏡檔案貼上就能用,不需要建置步驟也不綁任何框架。想直接玩,kappan.chiaki.ch 是一頁鉛字產生器:打一段字、調墨調紙調壓力,印出來的圖可以存成 JPG、去背 PNG 或向量 SVG。
想看看實際效果,這裡有一本見本帖,版式仿的是十九世紀上海美華書館的活字樣本,字號用的是真的號數制。日星宋貳的銅模在張老闆手上復活了一次;能不能讓它印在紙上的那個樣子,也在螢幕上活過來,大概還有得研究。
Demo:千秋印書館 - 在網頁上重現活版印刷的專案。
活版印刷的字有一種很特別的樣子。金屬、油墨跟紙互相作用之後,會產生一種特別的美感。曾經風靡一時的康熙字典體,就是一個例子。邊緣不齊,墨色不勻,偶爾缺一角,反而產生了一種人情味,一種別具風味的復古感。
日星鑄字行與 justfont 在去年共同推出了復刻鉛字銅模的日星宋貳,是一款非常漂亮且特別的宋體。但他著重的是字型本身的數位化,而不是鉛字印刷形態的保存。我一直在想,那有沒有辦法復刻鉛字印到紙上的感覺?
第一個要想辦法還原的是變化。字型是一組固定的輪廓,每一個「書」字打出來都長得一模一樣。但活版的鉛字是一顆顆排上去的,即便是同一批印刷稿、同一個字,都會因為沾墨狀態和壓印力道不同,產生不一樣的墨漬形狀。字型能把其中一次的樣子凍結下來,卻沒辦法讓它每一次都不同(其實還是有辦法,只是變成一個字要做很多份)。
而紙的差異不在字型裡,壓力、磨損、排版出來的誤差,這些都是字被印出來的當下才發生的事,得在字的外面做。
要在瀏覽器裡改變字的輪廓,又不動到文字本身,可以用 SVG 濾鏡。它跟 Photoshop 的濾鏡是同一個概念,差別在於 SVG 處理完後,仍能保留文字本身,能被選取、複製、搜尋。
決定了工具,接下來要先研究鉛字壓在紙上,到底發生了什麼事?
紙與螢幕
沒有紙就沒有活版,而紙從來不是一片乾淨的白。
抄紙的時候纖維會順著某個方向排列,所以紙面帶著很細的橫紋直紋。纖維本身也是粗糙的,湊近看是無數個深淺不一的山谷跟凹洞。這些東西單獨拿出來都不明顯,疊在一起才讓白紙看起來像紙。
實作上發現了一個有點反直覺的結論,紙的顆粒必須蓋在字的上面,不是下面。雖然墨是壓在紙上的,但薄薄的墨也會透出紙的那層紋理。
最後的解法是用
::before跟::after這兩個偽元素,一個放在字呈現纖維,一個蓋在字上面呈現顆粒。這樣整張紙不需要任何額外的標籤,一個<div class="lp lp-paper">就有了。油墨的暈染
鉛字的字面是金屬,刻出來的邊緣是銳利的。但印出來的字不是。
油墨會滲進紙的纖維,所以筆畫的邊緣會往外暈開一點,墨上得多,字會整個胖一圈,如果是小字,字腔甚至會被填掉,「口」變成一個黑塊。
暈開多少也看紙。塗佈紙表面封住了,墨停在上面幾乎不暈;新聞紙很平,卻很吸墨,邊緣就滲開一圈。所以紙在這裡拆成兩個成因:粗糙管邊緣被推歪,吸墨管暈。
要呈現這個感覺,在技術上叫做卷積,講白了就是「每個點都去看看鄰居有多黑,把鄰居的黑分一點過來」。分完之後邊緣就有了一圈半透明的暈,再決定那圈暈要有多少變成實心的墨,字就跟著胖起來。
繼續比對印刷稿與鉛字原型,會發現實際印出來的字筆畫起頭收尾是沒有尖角的,全部都是圓角修飾,而鉛字原型筆畫是更加銳利的。
換句話說,印刷這個動作本身就會改變字的形狀:尖的地方被磨圓,凹的地方被填滿。這兩個現象實作上是同一個運算,先把整個字的形狀模糊掉,再取平均。凸出來的尖角周圍沒什麼墨,模糊之後濃度掉到平均以下,就被削掉了;凹進去的內角周圍全是墨,模糊之後濃度更高,於是被填了起來。
但這招只能用在大字。內文級數的明體橫畫不到一個像素,模糊之後濃度掉到門檻下,整條橫畫還沒進到後面的濾鏡就先被切掉了。所以小字的積墨另外做:一樣先模糊,讓相鄰筆畫的墨相加,但不切門檻,而是把原本的字身蓋回去。窄縫與濁點會黏起來,髮絲卻一根都不少。
印壓
活版上的壓力不夠,紙跟字面接觸不完全,印出來會有沒沾到的空白。壓得太重,墨被擠到字的外圍,筆畫周圍多出一圈。
「壓太重」跟「墨太多」其實是兩件不同的事,墨太多是墨膜厚,整條筆畫均勻地胖出去,字腔被填掉;壓太重是墨被字面擠到外圍,邊緣堆起一圈、中央反而變薄,被擠出去那圈墨順著纖維走,邊界也不規則。所以壓力自己有一組效果:把筆畫最外圈留實、中央壓淡,再讓暈圈單獨抖一次,字身本身不動。
壓過頭還有一個副作用,字會被壓出凹陷感。在以前,壓力上太大會導致鉛字更快的被磨損,當時的印刷工的技術展現是印的越輕越好,但到了現代,凹痕反而成活字印刷特有的美學。
那麼凹痕要怎麼呈現呢?坑本身是看不見的,看得見的是光打在坑壁上,光從左上來,筆畫左上側多一條背光的暗線,右下側多一條受光的亮線,落在墨外面那圈紙上。做法不能單純用打光濾鏡,而是把字身脹開一個像素當坑的範圍,往右下平移一格後,沒被蓋到的那圈是左上壁,反向平移剩下的是右下壁,各自填上半透明的黑與白,再用固定核心柔化一次。
鉛字的磨損
鉛的質地軟,反覆印刷會自然磨耗,這也是日星那批銅模需要修復的原因。
新字的瑕疵來自鑄造,是表面細細一點一點的砂眼,舊字則是多次印刷崩掉的腳,或者凸起本身的磨損,那是完全不同的尺度。所以字愈舊,不只缺口變多,單個缺口本身也會變大。
內文與大字的斑駁
到這裡幾個成因都調好了,看起來挺像樣。然後我把字級放大,感覺卻又不對了。
原因是這些缺角、砂眼的尺度,是用絕對長度定義的,不會跟著字級一起縮放。同一組參數,在 16px 的內文上是恰到好處的斑駁;放大到 60px,那些「一點點」的缺口還是原來那麼大,卻整塊整塊地卡在筆畫中間。
所以大字不能用同一招。內文級數走的路子是「挖」,拿噪點當遮罩,把零星的像素挖掉,缺口的位置固定、只調缺掉多少墨,參數拉過去時才不會整片突然消失;大字則改成「調密度」,讓低頻的噪點去乘上墨的濃度。筆畫的結構完整保留,只是有的地方吃墨飽、有的地方虛 —— 像印章蓋不勻的那種樣子。
這就是為什麼最後會是四支濾鏡而不是一支:內文小字、一般行文、標題、大字,各有各的參數。而既然幾號字該配哪一支是固定的,那就乾脆讓字號本身同時決定字級跟濾鏡。
字內的墨量
拿日星鑄字行的貳號宋體實印稿並排比對時,有一件事一直對不上:實印的字不是均勻的黑。「日」的上半比下半淡,「鑄」的左半比右半飽,同一顆字裡有一側吃墨足、一側虛。邊緣再怎麼調都做不出這個,因為它不是邊緣的事。
成因是鉛字沒有坐平。字面只要比版面歪一點點,壓力就不是整面均勻落下,高的那側墨轉印得足,低的那側就虛。波長跟字身差不多,是「一顆字裡有一塊淡」,不是筆畫上的斑點。
做法把大字那道「調密度」搬到每一級:一張低頻的噪點當濃淡場,乘進墨的透明度。差別在位置,內文級數的濾鏡最後會把邊緣拉硬,灰階全部被推回實心,所以這一道要排在拉硬之後,才留得住深淺。另一個坑是噴出來的噪點太集中在中間值,直接拿來用整張場幾乎都是 1,得先以中間為軸拉開對比,才會真的有一塊掉下去。
排版
前面講的都是單顆鉛字發生的事,而鉛字是一顆一顆從字架上撿出來排進版盤的,所以每顆字的狀態其實都不一樣。比旁邊的字凸一點,就會承受更大的印壓,物理世界也難以做到完美對齊,每顆鉛字也可能歪一點點。
麻煩的是這件事沒辦法用純 CSS 做,因為 CSS 碰不到單一個字。
::first-letter只能選到第一個字,而能選到「第 N 個字」的::nth-letter()提案,講了十幾年至今沒有任何瀏覽器實作。所以只能退一步,把每個字包一個自己的標籤。包好之後,歪斜跟濃淡就都回到純 CSS 的範圍了,用好幾個互質的週期疊在一起,每 11 個字歪一次、每 13 個字濃一次、每 17 個字、每 23 個字⋯⋯ 週期挑大的、彼此又除不盡,整段排下來就有感覺了。
玩玩看
六個成因加上字級,湊在一起。幾組預設是常見的印壞情況。
底下印的是實際算出來的濾鏡參數。你會看到動一個成因,好幾個數字會一起變 —— 那個對應關係其實才是整套東西的核心,剩下的都只是實作。
規格書的模糊
做到一半,我在 Safari 上發現整片字糊成一坨,前面辛苦做的缺角跟濃淡全部不見了。
查下來發現兇手是半徑只有 0.28 個像素的模糊。問題在於,這種次像素等級的模糊,兩家瀏覽器引擎的算法根本不一樣:WebKit 走的是規格書建議的近似作法,Chrome 用的 Skia 老老實實算真的高斯模糊。
解法是把模糊換成係數寫死的固定核心。規格把每個數字都定死了,引擎沒有近似的空間,兩邊就算得出一模一樣的結果。
活字在螢幕上
一路做下來,最意外的其實不是哪個技術細節,而是這些效果其實每一道都對應到一件物理世界實際發生的事:滲墨、壓力、磨損、鬆動。當初覺得「這樣看起來比較好看」的直覺,回頭查資料時多半都能找到一個物理上的理由,而看起來怪怪的,通常就是做錯了的地方。
整套東西打包成 kappan,MIT 授權。
dist/裡的 CSS 與 SVG 濾鏡檔案貼上就能用,不需要建置步驟也不綁任何框架。想直接玩,kappan.chiaki.ch 是一頁鉛字產生器:打一段字、調墨調紙調壓力,印出來的圖可以存成 JPG、去背 PNG 或向量 SVG。想看看實際效果,這裡有一本見本帖,版式仿的是十九世紀上海美華書館的活字樣本,字號用的是真的號數制。日星宋貳的銅模在張老闆手上復活了一次;能不能讓它印在紙上的那個樣子,也在螢幕上活過來,大概還有得研究。