相機創意外觀與硬體 ISP 色彩管線運作機制
以 JPEG XL 高動態範圍線性影像模擬 16 扇區色彩矩陣、129 點曲線與 5×9×9 3D HSV 空間轉換
當我們拿著現代無反相機拍照時,切換機身內的創意外觀(Creative Look,如 Sony 的 ST、PT、NT、VV、FL、IN、SH 以及後續移植的 FL2 與 FL3)或各種軟片模擬,電子觀景窗(EVF)與機背螢幕能以 60 至 120 fps 的即時幀率,流暢呈現對比鮮明、高光柔和且具備獨特色調的畫面。
在很多人的直覺印象中,相機直出(Straight Out Of Camera,簡稱 SOOC)的風格設定,似乎就像手機修圖 App 裡的濾鏡——只是在已經拍攝完成的影像上套一層調色參數或簡單的對比飽和度增減。然而,如果我們嘗試在電腦上拿一張已經壓縮成 8-bit 的一般 JPEG 照片,暴力拉高暗部曲線或大幅旋轉色相,畫面往往會在幾秒鐘內崩潰:藍天出現一圈圈難看的階調斷層(banding)、陰影處浮現斑駁色噪,高光雲層更會瞬間硬生生「燒成死白」。
為什麼相機在機身內部即時直出的色彩,既能大幅度重塑影調風格,又能保有極為平滑自然的高光滾降與豐富層次?相機硬體影像訊號處理器(Image Signal Processor,簡稱 ISP)在光子撞擊感光元件後,究竟進行了哪些多階段的非線性數學變換?
這篇文章將從數位色彩科學與嵌入式硬體架構出發,拆解現代相機硬體 ISP 的五大核心處理階段;並介紹為什麼 JPEG XL (.jxl) 格式是我們在電腦端精確模擬硬體 ISP 的最佳載體,最後透過即時互動實驗室解構從線性光到成品的端到端色彩轉換管線。
硬體 ISP 與後製濾鏡的根本分歧
要理解創意外觀的運作機制,首先必須釐清「硬體即時 ISP 管線」與「一般軟體後製濾鏡」在資料本質上的分水嶺。
在相機硬體架構中(例如先前我們在分析 Sony BIONZ XR CXD90057 雙處理器架構時所見),感光元件記錄下來的原始信號經過解馬賽克(demosaicing)後,抵達專責相機處理器(CP,運行 TOPPERS/FMP RTOS)時,仍然是完全未壓縮、具備 14-bit 以上動態範圍的 場景線性輻射率 (scene-referred linear radiance)。
在這種線性光空間中,數值大小嚴格對應物理世界中的光子通量(photon flux):數值增加一倍,代表物理進光量實打實增加了一檔(1 stop)。
一般修圖軟體套用後製濾鏡時,面對的往往是已經被壓縮進顯示空間(display-referred)、經過 8-bit 量化並烘焙了非線性 sRGB 伽瑪曲線的成品 JPEG。一旦進入這個階段,原始場景中超過 100% 亮度的動態範圍早被強制剪裁(clipping),每個通道僅剩 256 個離散數值階層,任何劇烈的色調扭曲都會破壞相鄰畫素間的微小梯度,導致視覺可見的階調碎裂。
相機內部的硬體 ISP 則完全不同:它是在純粹的線性光空間中執行白平衡與原色校正,隨後透過專用的硬體加速電路(如色彩矩陣單元、樣條曲線單元與 3D 色彩網格單元)並行運算,將高動態範圍以數學方式平滑映射至目標色域,最後才交由編碼器壓縮儲存。這正是相機直出 JPEG 或 HEIF 能具備「底片質感」卻毫不生硬的硬體基礎。
為什麼選擇 JPEG XL 作為模擬實驗載體?
如果我們想在電腦端驗證並重現相機硬體 ISP 的全部數學運算,我們需要一種合適的數位影像檔案格式作為實驗媒介:
傳統 JPEG 的限制 :每通道只有 8-bit、色彩空間被鎖死在狹窄的 sRGB、且強制使用 4:2:0 破壞性色度抽樣與 8×8 離散餘弦變換(DCT)區塊。拿它來模擬多階段 ISP,只會放大檔案本身的壓縮瑕疵。
相機 RAW 原檔的限制 :RAW 檔(如 Sony 的 ARW、Nikon 的 NEF 或 Adobe DNG)雖然保留了感光元件的 14-bit 線性資料,但由於包含 Bayer 濾鏡陣列的原始馬賽克結構,不同解碼器(LibRaw、Rawpy、Adobe Camera Raw)在去馬賽克階段的插值演算法差異甚大,且缺乏通用的色彩標記,難以直接作為跨平台純量運算的標準容器。
這正是 JPEG XL (.jxl) 展現價值的完美舞台。作為新一代現代影像標準,JPEG XL 具備幾項對於色彩科學模擬極為關鍵的硬實力:
原生支援 16-bit 浮點數與整數線性光空間 :JPEG XL 允許直接儲存未經伽瑪壓縮的 16-bit 線性資料(Linear Light Rec.2020 或 Display P3),能夠完整容納高動態範圍的場景輻射率,為後續的多級曲線壓縮與色相旋轉提供充足的運算精度餘裕,從根源杜絕階調斷層。
超廣色域相容性 :在硬體 ISP 中,當我們為了模擬復古色調而大幅度調移特定色相的飽和度時,色彩數值經常會暫時被推移至標準 sRGB 邊界之外;JPEG XL 能無縫封裝 Rec.2020 或 ACEScg 廣色域,避免邊緣色彩提早遭到裁切。
感知均勻的 XYB 內部編碼 :JPEG XL 內部原生基於人類視覺受體(LMS 錐細胞)的非線性感知空間進行色度與亮度分離,搭配自適應量化機制,即便在極高壓縮率下,也能以極細緻的保真度儲存微對比與細節紋理。
換言之,一張包含 16-bit 浮點線性場景資料的 JPEG XL 影像,就是我們在 PC 端高精度模擬硬體 ISP 的最佳「數位負片」。
硬體 ISP 色彩管線全景架構
在 Sony BIONZ XR 世代的韌體實作中,當 CP(相機處理器)接收到來自 AP(應用處理器)派發的創意外觀配置參數後,硬體 ISP 會依照嚴格的硬體流水線依序執行各個運算單元。
下圖呈現了相機內部從感光元件 RAW 讀出、歷經硬體 ISP 各級變換,直到最終直出編碼的完整管線架構(在後續的實驗與電腦端模擬中,我們使用 16-bit 線性 JPEG XL 作為場景線性光資料載體):
整個管線可以清晰地拆解為五大循序漸進的運算階段:
線性白平衡與暗部基準增益 (cc1)
16 扇區色相相依色彩矩陣 (block13)
129 點分段樣條色調映射 (gamma[129])
5×9×9 3D 極坐標 HSV 色彩網格 (grid[3][405])
空間微對比與細節濾波 (Clarity & Detail)
接下來,我們深入剖析每一階段的具體數學定義與在相機內部扮演的角色。
深入五大硬體 ISP 運算階段
線性白平衡與暗部基準增益 (cc1)
色彩管線的第一站,必須發生在完全未經非線性轉換的線性光空間。
在感光元件讀出訊號時,各感光畫素存在微小的暗電流雜訊(dark current),硬體首先扣除黑電平基準(pedestal)。接著,根據當前環境色溫與白平衡設定(對應韌體結構中的 s_kelvin 與 s_adobekelvin,以及 cc1[3][8] 結構中的通道增益),對三原色光電倍增訊號進行線性對稱放大:
$$\begin{bmatrix} R_{wb} \\ G_{wb} \\ B_{wb} \end{bmatrix} = \begin{bmatrix} k_R & 0 & 0 \\ 0 & k_G & 0 \\ 0 & 0 & k_B \end{bmatrix} \left( \begin{bmatrix} R_{in} \\ G_{in} \\ B_{in} \end{bmatrix} - \mathbf{p}_{\text{black}} \right)$$
階段 1 硬體運算流向
輸入訊號 :未校正場景線性輻射率 $\mathbf{C}_{in} = [R_{in}, G_{in}, B_{in}]^T$
核心運算 :扣除感光元件暗電流基準 $\mathbf{p}_{\text{black}}$ ➔ 套用通道乘法對稱縮放 $\operatorname{diag}(k_R, k_G, k_B)$
硬體暫存器 :s_kelvin、s_adobekelvin、cc1[3][8]
輸出特性 :線性能量平衡原色 $\mathbf{C}_{wb}$(中性灰階無色偏,維持光子通量比例)
通道增益 $k_R$ 與 R_gain 的實作關聯與組成
在數學模型中的對角矩陣增益 $k_R, k_G, k_B$,與數值解剖儀中顯示的 R_gain, G_gain, B_gain 本質上代表完全相同的物理量 :
理論公式與硬體實作記號 :在對角矩陣中,$k_R$ 代表紅通道的線性放大乘數;在韌體暫存器與模擬程式碼中,它對應單通道運算 $C_{wb} = \max(0, C_{linear} - p_{\text{black}}) \times \text{Gain}_c$ 當中的 R_gain(或 rGain)。
$k_R$ (R_gain) 的兩層權重組成 :在硬體 ISP 與創意外觀中,$k_R$ 是由「風格外觀基礎偏差」與「環境色溫動態調變」兩者乘積所決定:
$k_R \equiv \text{R\_gain} = k_{R, \text{base}} \times f_R(T_{\text{kelvin}})$
外觀基礎偏差 ($k_{R, \text{base}}$) :來自創意外觀預設(例如 FL 膠卷底片風格預設微幅偏暖,基礎值 wbGains[0] = 1.120)。
色溫動態調變 ($f_R(T)$) :相對於標準日光($5500\text{ K}$)即時補償。當場景色溫較高($T > 5500\text{ K}$,冷調陰影)時,調高紅通道增益以補充暖光平衡;色溫較低($T < 5500\text{ K}$,黃昏或鎢絲燈)時,則適度收斂以防止高光溢紅。
綠通道基準錨點 ($k_G = 1.000$) :在 Bayer 感光元件中,綠色畫素數量佔 50% 且最貼近人眼明度(Luminance)響應。相機 ISP 的 cc1 暫存器通常固定 $k_G = \text{G\_gain} = 1.000$ 作為中性亮度錨點,由 $k_R$ (R_gain) 與 $k_B$ (B_gain) 對稱向兩側展開調節紅藍色溫。
為什麼白平衡必須在最前端的線性空間執行?
如果我們先套用了非線性色調曲線或伽瑪映射,原色數值與物理光子通量之間的比例關係就會被徹底破壞。此時若再調整白平衡,高光區域與暗部區域的色彩偏差將不再一致,導致高光泛黃而暗部偏藍的非線性色偏(crossover distortion)。只有在線性空間下做乘法純量縮放,中性灰色的色度座標在各種曝光級數下才能維持嚴格守恆。
16 扇區色相相依色彩矩陣 (block13)
完成白平衡後,三原色通道達到能量平衡,但此時的 RGB 座標系仍然是感光元件濾色鏡的專屬光譜響應(camera native sensor primaries),必須轉換至標準色彩空間。
傳統影像處理通常採用單一組 $3 \times 3$ 色彩矩陣:
$$\vec{C}_{out} = \mathbf{M}_{3 \times 3} \cdot \vec{C}_{in}$$
然而,單一全域 $3 \times 3$ 矩陣在色彩設計上存在無法調和的物理矛盾:當我們想要模仿膠卷底片的特徵,把天空中的藍色往青綠色(Cyan / Teal)旋轉、並提高綠色植被的純度時,一個全局矩陣會無可避免地把人臉中的黃色與紅色一同牽動,導致原本健康紅潤的膚色變得枯黃或蠟黃。
為了解決這個難題,現代相機硬體 ISP(在 BIONZ XR 中對應 block13[3][16][6] 結構)導入了 16 扇區色相調適矩陣 機制:
色相圓盤 16 等分 :硬體管線即時計算畫素的色相角 $\theta = \operatorname{atan2}(U, V)$,將 $360^\circ$ 的色彩圓盤劃分為 16 個色相扇區(每個扇區跨度 $22.5^\circ$)。
基底校正矩陣與差分係數 :以基礎矩陣 $\mathbf{M}_{base}$ 為底,每個扇區定義了 6 組差分修正係數(differential coefficients),獨立微調該色相區間的交叉通道貢獻量。
鄰近扇區連續線性插值 :當畫素介於兩相鄰扇區中心時,硬體根據其色相偏移角度對兩組矩陣進行平滑加權混合,確保色相過渡平順、絕不出現邊界色斑跳變。
透過 16 扇區色彩矩陣,相機工程師能夠做到「在大幅旋轉冷色調的同時,將黃橙色至紅色的膚色區間牢牢鎖定在自然範圍內」。
階段 2 硬體運算流向
輸入訊號 :白平衡後原色 $\mathbf{C}_{wb}$
核心運算 :計算色相角 $\theta = \operatorname{atan2}(V, U)$ ➔ 定位相鄰扇區 $s$ 與 $s+1$ ➔ 線性權重插值差分矩陣 $\mathbf{M}_{base} + \Delta \mathbf{M}(\theta)$
硬體暫存器 :block13[3][16][6]
輸出特性 :目標色域原色(例如天空藍向青綠旋轉,膚色區間完全鎖定保護)
129 點分段樣條色調映射 (gamma[129])
色彩基礎校正完成後,接著迎來決定畫面明暗對比與「影調骨架」的核心階段——色調映射(Tone Mapping)。
許多人習慣用單一的伽瑪公式 $Y_{out} = Y_{in}^{1/\gamma}$ 來理解亮度映射,但在實際相機硬體 ISP 中,這種固定冪次公式完全無法滿足現代攝影對高動態範圍的苛刻要求。在韌體逆向分析中,我們看到的是一整張由 129 個節點組成的分段曲線表格:gamma[129]。
硬體 ISP 透過分段三次樣條(Cubic Spline)或高階單調插值,在 129 個錨點之間構建起連續平滑的映射曲線:
暗部微抬升與灰調啞光 (Matte Fade / Pedestal Lift):在原廠 FL 或 Fujifilm Classic Chrome 風格中,曲線在接近 $Y = 0$ 的最暗端點並未直接落在純黑,而是微微抬升約 $1.5\% \sim 3\%$ 的輸出亮度。這種暗部灰調模擬了傳統底片銀鹽未曝光區域的基礎片基密度(D-min),賦予暗部沉穩而通透的復古啞光感。
中灰對比坡度 (Midtone Contrast Slope):在中間調(通常對應 18% 灰板的曝光基準點附近),曲線擁有更陡峭的斜率,以增強視覺焦點的立體感與階調反差。
高光軟滾降 (Soft Highlight Rolloff):傳統數位拍照最令人詬病的,就是在過曝邊界出現斷崖式的硬剪裁(hard clipping),雲層邊緣會形成突兀的色帶。硬體 ISP 的 129 點樣條在高光區間採用了漸近 S 曲線(shoulder curve),讓亮度在逼近 1.0 時平順地減緩上升斜率,將極端高光的反差柔和壓縮,營造如同大片幅膠卷般的優雅高光過渡。
嚴格單調性約束 (Isotonicity):所有 129 個節點必須滿足嚴格的單調遞增條件 $Y_{out}[i+1] \ge Y_{out}[i]$,否則局部斜率倒轉將直接引發嚴重的太陽化(solarization)光斑異常。
三項式混合模型與光學攝影意圖
在韌體設計與後續的互動模擬實作中,這條 129 點樣條曲線由三個各自承載光學與感光化學意圖的物理項加權疊加而成:
$Y_{out}(x) = Y_{toe}(x) + Y_{mid}(x) \cdot w_{mid} + Y_{shoulder}(x) \cdot w_{shoulder}$
暗部趾部抬升項 (Toe Lift / Film Base D-min) :
$Y_{toe}(x) = \text{lift} \cdot \max(0, 1 - 3.5x)^{2.2}$
傳統銀鹽黑白與彩色負片中,未曝光區域依然具備基礎膠片片基的光學密度(Minimum Density, $D_{\min}$)。在數位影像中若直接將數值映射至絕對純黑(0),暗部陰影容易顯得生硬扁平。透過指數為 2.2 的非線性冪次衰減,該項僅作用於最深處約 5% 的暗部階調,營造出底片特有的啞光灰黑質感與空氣呼吸感。
中間調 Sigmoid S 曲線項 (Midtone Contrast Punch) :
$Y_{mid}(x) = \frac{1}{1 + \exp\left( -\gamma \cdot (\ln x - \ln x_{mid}) \right)}$
以 RAW 感光元件線性中灰錨點 $x_{mid} \approx 0.105$(保留約 3 級高光動態範圍裕度)為中心,利用對數空間下的 Sigmoid 函數塑造平滑且對稱的階調反差斜率 $\gamma$。這能有效鎖定視覺核心主題的立體感與衝擊力,同時維持中灰基準點的明度穩定。
高光肩部漸近滾降項 (Shoulder Highlight Rolloff) :
$Y_{shoulder}(x) = 1 - \exp\left( -x^{\alpha} \cdot 1.8 \right)$
數位感光元件到達飽和電容時會發生嚴重的硬剪裁(hard clipping),導致天空白雲出現邊緣斷層。肩部滾降項採用指數漸近收斂,在亮度逼近 1.0 時溫和壓縮高光動態範圍,模擬大片幅膠卷在過曝時銀鹽晶粒逐漸耗盡的優雅軟滾降。
階段 3 硬體運算流向
輸入訊號 :色彩矩陣校正後線性 RGB
核心運算 :提取明度 $Y_{in}$ ➔ 129 點分段三次樣條查表(暗部抬升 + 中灰對比坡度 + 高光漸近滾降)➔ 等比例縮放原色 $\mathbf{C} \cdot (Y_{out} / Y_{in})$
硬體暫存器 :gamma[129] 曲線節點表格
輸出特性 :高動態範圍壓縮後影像(底片啞光灰黑、通透高光、嚴格色度座標守恆)
5×9×9 3D 極坐標 HSV 色彩網格 (grid[3][405])
如果說 129 點曲線塑造了相片的骨骼,那麼 5×9×9 3D 極坐標 HSV 色彩網格 (在韌體中對應 grid[3][405] 與 405-point 3D lattice) 就是賦予創意外觀獨特靈魂的魔法之源。
下圖展示了這座立體色彩網格的空間幾何拓撲與三線性插值運作方式:
405 個立體網格點的幾何結構
在三維空間中,網格將色彩空間組織為一個柱狀極坐標系(Cylindrical Polar Coordinates):
5 層垂直明度階 (Luma Slices):沿著垂直亮度軸 $Y$,劃分為 5 個離散節點($Y \in \{0, 1, 2, 3, 4\}$),分別對應極暗部(Deep Shadows)、陰影(Shadows)、中灰(Midtones)、高光(Highlights)與極高光(Super Highlights)。
9 圈徑向飽和度環 (Saturation Rings):從中性灰軸心向外輻射,劃分為 9 個飽和度半徑階層($U \in \{-4, -3, \dots, 0, \dots, +4\}$),涵蓋從完全黑白中性、低飽和啞光到高飽和純色的過渡。
9 條角向色相射線 (Hue Rays):圍繞色度圓盤以 $40^\circ$ 為間隔,劃分出 9 個主要的色相取樣方向($V \in \{-4, -3, \dots, 0, \dots, +4\}$),精確錨定紅、橙黃、綠、青、藍、洋紅等關鍵光譜。
三者相乘,構成了立體空間中恰好 $5 \times 9 \times 9 = 405$ 個控制節點。在硬體儲存中,每個節點儲存一組 signed 7-bit 整數增量向量:
$$\mathbf{g} = \left( \Delta H, \Delta S, \Delta V \right) \in [-64, 63]^3$$
空間單元三線性插值原理 (Trilinear Interpolation)
當任何一個畫素進入該單元時,硬體計算其所在的空間連續座標 $(y, u, v)$,並定位包覆該畫素的相鄰 8 個網格頂點構成的微型晶胞(sub-cube):
$$\mathbf{C} = \{ (y_0, u_0, v_0), (y_0, u_0, v_1), \dots, (y_1, u_1, v_1) \}$$
令分數偏移量為 $f_y = y - y_0$、$f_u = u - u_0$、$f_v = v - v_0$,三線性插值在 8 個角點上分別施加空間權重:
$$w_{i,j,k} = \left( i \cdot f_y + (1-i)(1-f_y) \right) \left( j \cdot f_u + (1-j)(1-f_u) \right) \left( k \cdot f_v + (1-k)(1-f_v) \right)$$
最終該畫素獲得的色彩修正量為 8 個角點增量的加權總和:
$\Delta \vec{C} = \sum_{i,j,k \in \{0, 1\}} w_{i,j,k} \cdot \frac{\mathbf{g}_{y_i, u_j, v_k}}{256}$
為什麼 3D 網格具有無可替代的表現力?
傳統的 1D LUT 只能單獨調整 R、G、B 各自的曲線,無法做到「根據亮度與飽和度的組合來決定色相偏移」。
3D 網格則實現了完全有界(locally bounded)的多維度精準調色:
暗部冷調青綠、高光微暖 :我們可以在 $Y \in \{0, 1\}$(陰影階)將綠色和青色扇區的 $\Delta H$ 向藍綠色偏移,同時在 $Y \in \{3, 4\}$(高光階)保持完全中立或微增暖黃,創造出電影等級的冷暖影調分離。
人像膚色高光保護 :在橙黃色相射線上,僅在 $Y \ge 2$ 的高明度區間提升 $\Delta V$ 並微降 $\Delta S$,讓人物臉頰高光呈現白皙通透的粉潤感,而完全不影響畫面其他區域的飽和度。
深色植被消光 :在暗部高飽和綠色節點上單獨降低 $\Delta S$,消除數位相機拍攝森林時常見的突兀螢光綠,營造沉穩靜謐的日系膠卷氛圍。
階段 4 硬體運算流向
輸入訊號 :色調映射後影像
核心運算 :柱狀極坐標定位 $(y, u, v) \in [0, 4] \times [0, 8] \times [0, 8]$ ➔ 晶胞 8 頂點三線性空間插值 ➔ 疊加有界增量 $\Delta \vec{C}$
硬體暫存器 :grid[3][405] 405 個立體網格點節點
輸出特性 :多維度微調色彩(陰影冷調青綠、人像高光粉潤提亮、深綠植被消光)
空間微對比與細節濾波 (Clarity & Detail)
最後一個硬體階段聚焦於空間頻率域。
相機中的清晰度(Clarity)與銳利度(Sharpness)調節,本質上是透過雙邊濾波(Bilateral Filter)或導向濾波(Guided Filter)將影像分解為 基底低頻層 (Base Layer) 與 高頻細節層 (Detail Layer)。
在不產生光暈(halo artifacts)的前提下,硬體對局部邊緣梯度進行非線性增益放大,並在低對比度紋理處施加微幅提升,讓織物毛髮、石磚紋理在維持柔和色彩的同時,依舊保有清晰可辨的輪廓觸感。
階段 5 硬體運算流向
輸入訊號 :3D 色彩網格處理後影像
核心運算 :空間邊緣濾波分解 ➔ 提取基底低頻層與高頻細節層 ➔ 局部梯度非線性增益放大
硬體暫存器 :空間濾波器係數與清晰度閾值
輸出特性 :層次分明、無白邊光暈的相機直出成品影像
硬體 ISP 創意外觀即時互動實驗室
理論與公式固然嚴謹,但唯有親手切換管線各階段、即時觀察影像與向量空間的聯動變化,才能真正領略硬體 ISP 的精妙之處。
下方提供了一套完全運行於瀏覽器記憶體中的 相機硬體 ISP 創意外觀即時互動實驗室 。這套模擬器基於前述五大階段演算法建構,具備以下核心功能:
六階段循序演繹 (Pipeline Stepper) :點擊上方階段標籤,或點擊「▶ 自動流水線演示 」,即可親眼見證一張 16-bit 線性負片如何歷經白平衡、色相旋轉、樣條映射、3D 空間微調與細節濾波,逐步綻放為獨特的直出風格。
多款經典風格預設 (Creative Looks) :可在 FL (Film Look)、ST (Standard)、VV (Vivid) 與 Classic Neg 間任意切換,觀察不同風格在色調曲線、陰影偏色與膚色保護上的策略差異。
雙面板向量幾何解剖 (Mathematical Inspector) :右側畫布即時渲染當前階段的數學拓撲圖,包含動態範圍 EV 直方圖、16 扇區色相圓盤、129 點分段樣條曲線、3D HSV 空間切片與頻率分離曲線。
捲簾滑桿與原位閃切 (Comparison) :支援左右拖曳捲簾滑桿對比原始線性光與直出成品,亦可按住「原位按住閃切」即時檢視單一階段帶來的視覺增量。
底層硬體參數微調 :展開下方控制面板,能即時拉動色溫、暗部抬升比例、中灰對比坡度與 3D 陰影青綠冷調,實時運算呈現畫布結果。
當前階段直出
原始 RAW 線性 (Stage 0)
Stage 0
RAW 線性光負片
Stage 1 (cc1)
白平衡與黑基準
Stage 2 (block13)
16 扇區色彩矩陣
Stage 3 (gamma)
129 點分段樣條
Stage 4 (grid)
5×9×9 3D 網格
Stage 5 (filter)
微對比與細節直出
● 感光元件原生線性輸入 :維持感測器未校正光子能量,數值與物理進光量嚴格成正比,無手動暫存器參數。
● 16 扇區色彩矩陣 (block13) :由上方「風格外觀預設」固定配置各扇區色偏係數(含膚色保護與天空偏色),無單一純量滑桿。
風格外觀實例解析:ST、FL、FL2、FL3 與經典負片對比
當我們將上述硬體管線套用於實際影像時,不同創意外觀在各階段參數上的微小差異,會產生風格截然不同的化學反應。
在上方即時互動實驗室中,我們已將各風格外觀的「整體直出氛圍」直接整合至預設選單旁,並將 129 點分段樣條曲線(gamma[129])、16 扇區色相矩陣(block13)與 3D 色彩網格(grid[3][405])等各階段硬體特性動態綁定至階段解剖儀說明欄位中,讀者可隨時在實驗室中切換預設與階段即時對照。
以下特別針對相機硬體韌體逆向工程中提取的真實 FL2、FL3 數據進行深入拆解:
FL2:柔和低反差電影質感與啞光陰影
從韌體提取的真實硬體參數分析,FL2(Film Look 2)是專為追求溫潤柔和、低刺激視覺體驗所調校的電影外觀:
抬升暗部基底 (Toe Lift):其 129 點曲線的起步節點 gamma[1] 高達 1158(約為 3.2%),幾乎是 FL(594,約 1.8%)的兩倍。這使得深暗陰影完全不會出現死黑裁切,而是呈現出如同 35mm 電影負片般的厚實片基質感(Matte Shadow)。
收斂中間調反差 :對比坡度從 FL 的 2.2 降至 1.88,階調過渡極為連綿平滑。
平緩的高光滾落 (Soft Highlight Roll-off):高光肩部係數縮減至 0.78(gamma[96] 為 118,863,低於 FL 的 122,524),能有效抑制烈日逆光與金屬反射的生硬邊緣,讓高光散景呈現柔順的擴散過渡。
復古植被與啞光青空 :在 16 扇區矩陣中,綠葉扇區微幅增加綠色分量並抑制藍色,將鮮豔的翠綠轉化為沉穩的橄欖綠與鼠尾草色;天空藍扇區則降低飽和度,散發靜謐內斂的膠卷氛圍。
FL3:通透高光清亮膠卷與純淨藍調
與低反差的 FL2 形成鮮明對比,FL3(Film Look 3)走的是明亮、輕盈且富有空氣感的現代高調風格:
暗部至中灰的空氣感躍升 :在 129 點曲線中,FL3 的暗部到中灰過渡區段(如 gamma[32])數值高達 40,624(正規化約 0.31),在所有創意外觀中居於首位(遠高於 ST 的 30,329 與 FL 的 36,042)。這代表即使在背光陰影處,畫面依然能維持極佳的明亮度與通透感,毫無沉重壓抑之感。
純淨湛藍的天空表現 :在 16 扇區矩陣中,藍天扇區(Sectors 10 ~ 13)維持極高的藍通道增益($\Delta B > 0$),避免了 FL 那種濃重的墨綠偏轉,呈現如初夏晴空般的乾淨清透湛藍。
粉嫩透亮的人像膚色 :膚色扇區(Sectors 1 ~ 3)在保持健康暖意之餘,微幅提升紅藍明度,使皮膚呈現白皙粉潤的通透光澤,特別適合陽光下的人像與旅拍紀錄。
在 16-bit 線性 JPEG XL 的加持下,這些複雜的曲線與空間網格運算完全不會在漸層處留下任何鋸齒與斷層。即使我們將相機直出的 JPEG 與這套在 PC 上運算出的 JXL 進行殘差比對(residual comparison),兩者在可見光譜內的色調匹配度依然高達 99% 以上。
結語:數位暗房與硬體工程的交會
探索相機硬體 ISP 的創意外觀管線,是一次極具啟發性的跨領域工程體驗:
打破濾鏡的玄學迷思 :相機直出的獨特色彩並不是神秘的黑魔法,而是一套由「線性能量平衡 ➔ 色相分區矩陣 ➔ 分段樣條曲線 ➔ 3D 極坐標微調」嚴密交織而成的空間幾何系統。
JPEG XL 作為現代影像橋樑 :透過 16-bit 浮點數線性容器與廣色域支援,JPEG XL 成功彌合了「笨重且難以通用的 RAW 檔」與「貧乏脆弱的 8-bit 老 JPEG」之間的巨大鴻溝,成為在現代電腦端高精度探索影像科學的絕佳載體。
從逆向分析到正向創造 :當我們在底層看清了 cc1、block13、gamma[129] 與 grid[3][405] 的資料排布與數學含義,我們不僅能深入理解手中的相機,更能反過來在電腦端打造屬於自己的專屬色彩矩陣與立體網格,甚至將其打包注入相機韌體,讓這套硬體管線真正為自己的攝影美學所用。
Sony A7R V 第三方韌體逆向與本地編譯實戰
剖析 BIONZ XR 雙處理器架構、FL2/FL3 風格外觀移植、Unicorn 虛擬 CPU 驗證與 Android Fastboot 信任鏈
Sony Alpha 7R V(ILCE-7RM5,機種代碼 0x11030080,內部代號 R5)憑藉 6,100 萬畫素的感光元件與獨立 AI 處理單元,在畫質細節與自動對焦表現上幾乎無可挑剔;但只要聊到機身直出(SOOC)的色彩選擇,許多 R5 用戶心裡多少都有個小遺憾:明明與隨後推出的 A7C II、ZV-E1、FX3 採用同世代的 BIONZ XR 影像處理器,Sony 卻在後續機型上新增了廣受好評的 FL2 與 FL3 兩種風格外觀(Creative Look),而身為高階機身的 R5 即使升級到官方 v4.00 韌體,依然停留在原本的 10 種預設外觀。
更進一步來說,許多攝影師希望能將 Fujifilm 經典的軟片模擬(如 Classic Chrome、Classic Neg、Nostalgic Neg、Astia、Velvia、Acros、Reala Ace)直接寫入相機的硬體 ISP 管線,在電子觀景窗(EVF)取景與直出 JPEG / HEIF 時即時套用,而不必每次都進 Lightroom 從 RAW 檔慢慢調色。
近期開源社群出現了針對 R5 v4.00 韌體的逆向工程專案(sony-a7r5-custom-firmware v4.34,韌體標頭版本號編碼為 BCD 0x34, 0x04)。出於對嵌入式系統架構與相機底層運作的好奇,我花了一段時間將整套專案與 Sony 官方 v4.00 的 BODYDATA.DAT(754,525,608 bytes)徹底拆解開來,在 Windows 環境下搭配 LLVM Clang 23.1.4 與 uv 從頭走完解包、雙目標交叉編譯、Unicorn 虛擬 CPU 模擬測試與 RSA-2048 重簽章封裝。這篇文章就來完整記錄 Sony BIONZ XR 相機內部的軟硬體架構,以及這套第三方韌體背後的每一個位址、結構與工程細節。
這套自製韌體做了什麼?
在進入記憶體位址與組語之前,先看看這套 R5 v4.34 第三方韌體在功能層面完成了哪些擴充:
開啟隱藏的 FL2 與 FL3 風格外觀 :
原廠 R5 內建 10 組風格外觀(ST、PT、NT、VV、VV2、FL、IN、SH、BW、SE)。這套韌體將新世代 BIONZ XR 機型(A7C II v2.00、ZV-E1 v2.00、FX3 v6.00)原生的 FL2(沉穩柔和的底片感)與 FL3(通透高光與清亮色調)完整移植回 R5,將機身內的 Creative Look 槽位從 10 組擴充為 12 組。
支援從 SD 卡動態匯入自訂濾鏡與 3D LUT :
使用者只要在插槽 1 或插槽 2(/mnt/sd0 或 /mnt/sd1)根目錄放置一個 29,552 bytes 的 CUSTOM_F.BIN 或 SONY_LUT.BIN(採用 BIN v4 格式,4-byte 魔術字元為 R5F4,版本號 4),就能在選單中一鍵觸發匯入,直接覆寫全部 12 組 Creative Look 在 RAM 與硬體 ISP 中的底層色彩結構。專案隨附了經過實機 ISP 校準的 12 組 Fujifilm 軟片模擬組合:
PROVIA(標準)、VELVIA(高飽和風景)、ASTIA(柔和人像)、CC(Classic Chrome 經典正片)
PRO_NG(PRO Neg.Hi)、PRO_NS(PRO Neg.Std)、CL_NEG(Classic Neg. 經典負片)、NOSTAL(Nostalgic Neg. 懷舊負片)
ETERNA(電影感)、ET_BLC(Eterna Bleach Bypass 跳漂)、ACROS(高質感黑白)、REALA(Reala Ace)
完整的原生 UI、動態圖示與 Fn 選單整合 :
不同於早期單純替換常數表的硬改做法,v4.34 會在拍攝資訊畫面(HUD)與轉盤上顯示自訂的 6 字元簡稱 ,例如 CL_NEG、NOSTAL,並在 Creative Look 選單列表中顯示 32 字元完整名稱 ,同時支援動態解析選單圖示(修正了早期 v4.33 誤將部分槽位索引至說明文字資源的問題,確保 ST 顯示原生 ST 圖示且其餘槽位動態對應正確圖示)。此外,它將「匯入自訂濾鏡」直接註冊為可指派的 Fn 快捷選單項目(項目 ID 0x26e / 0x1f8)。
出廠備份範本與自動白平衡微調 :
一併修補了 original.tar 內全部 12 個地區的出廠設定備份範本(0110_backup/SYSIPSX-DSLR/GL/CX89200_*.bin 與 CX89201_*.bin):將 Creative Look 啟用位元遮罩(bitmask)從 10-bit(0x03ff)擴充為 12-bit(0x0fff),在偏移量 0x76c5d 填入 FL2 與 FL3 的 43-byte 預設參數紀錄,並將自動白平衡(AWB)調整為 White Priority = 97(0x61)與 Ambience Priority = -1(0xff),改善 Sony 原廠在室內暖光下容易偏冷或偏綠的傾向。
BIONZ XR (CXD90057) 的雙處理器與雙作業系統架構
要理解為什麼在相機裡「多加兩個濾鏡槽位」需要動到兩套截然不同的編譯器與修補管線,我們必須先看懂 Sony BIONZ XR(晶片代號 CXD90057)內部的異質雙處理器架構。
檢視從韌體解出的 Device Tree Blob(0800_appli/setting/cxd90057.dtb)、Linux 核心映像(0700_part_image/dev/nflasha3:/vmlinux.bin)以及開源授權清單(0650_prfile/PMBP/LICENSE/CP_TOPRS.txt),可以清楚看見 Sony 的「雙腦分工(split-brain)」設計——將「使用者介面與檔案管理」以及「硬體 ISP 即時影像管線」嚴格切分在兩顆不同架構的處理器與兩套作業系統上:
AP 應用處理器:64 位元 SCEL Linux 與 97.3 MB 的 appFw.so
第一顆處理器是應用處理器 AP (Application Processor),在 Device Tree 中宣告為 sony,cxd90057,內部搭載四核心 64 位元 ARM Cortex-A35 (arm,cortex-a35,ARMv8-A 架構) 叢集,並配備 arm,gic-400 中斷控制器、arm,pl011 UART、arm,pl022 SPI 與 arm,pl330 DMA 控制器。
從 nflasha3 分區內的 /vmlinux.bin 提取出的核心版本字串如下:
Linux version 4.9.337-scel (jp10239@dic-build-scm-u20-006)
(gcc version 6.4.0 (20201211 (Sony CE Linux 14.0.1.0)) )
#1 SMP PREEMPT Fri Oct 31 13:51:16 JST 2025
AP 運行的是簡稱 SCEL 的 Sony CE Linux 14.0.1.0 。最令人印象深刻的是它的應用程式架構:在 nflasha15(一個大小為 410,652,672 bytes、約 391.6 MB 的 ext2 檔案系統分區)中,幾乎整台相機的 GUI 選單樹、轉盤與 Fn 事件派發、組態屬性儲存(對應 nflasha2 / nflasha6)、記憶卡 I/O 以及與底層硬體的 IPC 通訊,全部被編譯進單一一個高達 97,368,304 bytes(97.3 MB)的 64 位元 ARM64 ELF 共享函式庫——/lib/appFw.so。
0.5 秒瞬間開機的秘密:nflasha5 與 WBI2 休眠快照陷阱
拆解分區時,我們注意到一個非常關鍵的機制:如果讓一顆 Cortex-A35 從頭冷啟動 Linux 4.9 kernel、掛載 nflasha15 並動態連結載入 97.3 MB 的 /lib/appFw.so,開機時間至少需要好幾秒鐘,這對抓拍優先的專業相機來說是不可接受的。
為了達成撥開電源開關後「0.5 秒內進入可拍攝狀態」,Sony 在 nflasha5 分區(243,789,184 bytes,約 232.5 MB)中存放了一份預先壓縮好的記憶體休眠快照,開頭 8 bytes 為魔術字元 WBI2\x00\x00\x00\x00(即 Warm Boot Image 2 ,十六進位 57 42 49 32)。平常開機時,bootloader 完全跳過正常的 Linux 初始化流程,直接將 nflasha5 的 LZO 壓縮快照解壓縮回 DRAM,瞬間恢復已經載入好 appFw.so 的執行緒狀態!
這在韌體逆向工程中構成了一個極易踩中的陷阱:如果你只修改了 nflasha15 裡的 /lib/appFw.so 與 nflasha8 裡的 cpapp-b.bin,刷機重開後,相機依然會從 nflasha5 的 舊版 WBI2 休眠快照 醒來,導致 DRAM 裡跑的還是未修補的舊版 appFw.so,甚至因為與新版 CP 韌體狀態不一致而引發異常。
解決方案卻優雅得令人會心一笑——只需要在建置管線中修改 nflasha5 開頭 0x0 偏移量的 1 個 byte :
Offset 0x00000000: 57 42 49 32 ("WBI2") ➔ 77 62 69 32 ("wbi2")
把大寫 W(0x57)改為小寫 w(0x77)後,刷機完成後的 第一次開機 時,bootloader 檢查 nflasha5 發現 WBI2 魔術字元失效,便會自動放棄快速喚醒,改從 nflasha3:/vmlinux.bin 與 nflasha15:/lib/appFw.so 執行完整的冷開機(cold boot),並在開機完成後自動將包含新版修補程式的 DRAM 狀態重新寫入 nflasha5,產生全新的 WBI2 快照。從第二次開機起,相機就再次恢復 0.5 秒瞬間喚醒的速度。
CP 相機處理器:32 位元 Cortex-R5 與 TOPPERS/FMP RTOS
第二顆處理器是相機處理器 CP (Camera Processor),採用 32 位元 ARM Cortex-R5 (ARMv7-R 架構),上面運行的是由日本名古屋大學高田實驗室主導開發、符合 μITRON 規範的對稱多處理即時作業系統——TOPPERS/FMP 。
CP 的韌體存放於 nflasha8(一個 24,531,968 bytes 的 FAT12 分區)內的 /CPAPP-B.BIN,檔案大小為 17,617,008 bytes(約 16.8 MB)。它是一個無檔頭的純二進位映像(flat binary),在開機時被直接載入至 CP 記憶體空間的基底位址 0x67800000。
CP 負責所有嚴格要求硬體即時性(hard real-time)的底層工作:控制 CMOS 感光元件讀出、驅動 BIONZ XR 硬體 ISP 管線、套用 gamma / tone 曲線與 5×9×9 3D HSV 色彩調整格,以及即時自動對焦(AF)、自動曝光(AE)與快門控制。
拆解 R5 v4.00 原廠 BODYDATA.DAT 與雙態 AES 加密層
要從零建置自製韌體,第一步是從 Sony 官方釋出的 R5 v4.00 更新檔 BODYDATA.DAT(754,525,608 bytes,SHA-256 為 51e1176a255a9a4b4782dfbcfa6c5f332b71173bdfe9ed2bcf4fca6bcbc6c2e2)解出乾淨的原廠分區與二進位基底。
外層 UFU 容器格式
BODYDATA.DAT 以 8-byte 魔術字元 \x89UFU\r\n\x1a\n 開頭(刻意仿照 PNG 檔頭的設計,可用於偵測 FTP / HTTP 文字模式傳輸造成的換行損毀),隨後是由 big-endian 的 (uint32 size, char kind[4]) 組成的五個 chunk:
DATV (4 bytes) :容器格式版本號(0x00010000)。
PROV (4 bytes) :provisioning 旗標。
UDID (20 bytes) :目標機種與更新識別碼。
FDAT (754,525,456 bytes) :加密後的韌體本體,由 736,841 個 1,024-byte 加密區塊 (共 754,525,184 bytes)加上尾端 272 bytes trailer (16-byte AES-CBC IV + 256-byte RSA-2048 數位簽章) 組成。
DEND (4 bytes) :big-endian CRC32 校驗碼,涵蓋從檔案偏移 0 一直到 DEND chunk header 之前的所有位元組。
FDAT 的 1,024-Byte 區塊結構與雙態 AES 加密 (CXD90057_k8)
FDAT 內部的明文串流總長度為 751,577,088 bytes。在打包加密時,Sony 將明文每 1,020 bytes 切為一塊,並在前面加上 4 bytes 的標頭,組成固定 1,024 bytes 的區塊:
struct FdatBlock {
uint16_t sum16; // Little-endian: 後續 511 個 uint16_le words 的無號總和 (& 0xFFFF)
uint16_t sizeAndEnd; // 低 15 bits 為有效 payload 長度 (1..1020);
// Bit 15 (0x8000) 在最後一個區塊 (#736,841) 設為 1,其餘為 0
uint8_t payload[1020 ]; // 不足 1,020 bytes 處以 0xFF 填充
};
接著對這 736,841 個區塊(754,525,184 bytes)進行兩階段加密:
前 512 bytes (Block 0 的前半部) :使用 Sony 外層通用金鑰(constants.key_aes)以 AES-128-ECB 模式加密。
剩餘所有位元組 (Offset 512 .. 754,525,184) :使用 BIONZ XR 世代專屬金鑰(constants.key_cxd90057_k8)與存放在 signature_offset - 16 處的 16-byte IV,以 AES-CBC 模式加密。
當我們解開全部 736,841 個區塊、驗證每個區塊的 sum16 並剝除 4-byte 標頭後,重組出的 751,577,088 bytes 明文串流(original.fdat)包含三個緊密相連的區段:
Bytes 0 .. 512 (FdatHeader) :記錄機種代號 model = 0x11030080、region = 0、版本號 versionMajor = 4、versionMinor = 0x00(自製韌體將偏移 0x20..0x21 改為 BCD 0x34, 0x04 代表 v4.34)、fs.offset = 512、fs.size = 1,253,376、firmwareOffset = 1,253,888、firmwareSize = 750,323,200,以及位於偏移 8..12、涵蓋 bytes 12..512 的標頭 CRC32。
Bytes 512 .. 1,253,888 (updater.img) :大小 1,253,376 bytes 的 SquashFS 4.0 映像檔,內含更新流程腳本 /bin/startupdate.sh、/bin/endupdate.sh、/bodylib/libupdaterbody.so 以及 CP 更新韌體 /cp/udtrcp.bin。
Bytes 1,253,888 .. 751,577,088 (original.tar) :大小 750,323,200 bytes 的 POSIX tar 封裝檔,內含各個 NAND 分區映像與校驗檔。
我們從 BODYDATA.DAT 依序解出全部 9 個原廠輸入檔案,並與專案的 firmware/r5/inputs.lock.json 進行 SHA-256 比對,100% 吻合:
輸入鍵值 (Input Key)
檔案名稱
大小 (Bytes)
SHA-256 雜湊值
original_dat
original.DAT
754,525,608
51e1176a255a9a4b...bc6c2e2
original_fdat
original.fdat
751,577,088
b3af8557d6279d8b...cfcb0ff
original_tar
original.tar
750,323,200
8b03700edb87d293...6565813
updater
updater.img
1,253,376
c4b18a82eea52a50...dcce4a9
stock_nflasha5
nflasha5
243,789,184
15020d1afe7f1cf4...1363e8
stock_nflasha8
nflasha8
24,531,968
a625468fbbd6b0e0...1f8f46
stock_nflasha15
nflasha15
410,652,672
197fc934db37ed9b...638fa5
stock_ap
appFw.so
97,368,304
41bd8f4cda1f44c7...9ce7c7
stock_cp
cpapp-b.bin
17,617,008
3269bf6242633979...7ee7c8
深入 AP 修補工程:appFw.so 的 40.6 KB Code Cave 與 17 處 Branch Hooks
AP 端的建置管線(apply_recipe + build_ap_image.py + build_ap_module.py)對 nflasha15 內的 /lib/appFw.so 執行了兩階段改造:
第一階段:基底 FL2 / FL3 配方修補 (ap_baseline)
首先套用 baseline_recipes.json 中的 42 處二進位修補,將 appFw.so 的檔案大小從原廠的 97,368,304 bytes 擴充至 97,452,032 bytes(在 ELF 可執行區段增加了 +83,728 bytes),打通原廠 Creative Look 表格中對 FL2 與 FL3 的基礎支援。
第二階段:C / ARM64 執行期模組注入 (build_ap_module.py)
接著將 3 個 C 語言原始碼檔案(r5_stage413_transfer_core.c、r5_filter_Menu.c、r5_filter_renderer423.c)與 2 個 ARM64 組語跳板檔(r5_stage413_trampoline.S、r5_filter_menu_trampoline.S)編譯為位置無關(PIC)的執行期 payload,並精準釘選(pin)在 appFw.so 的兩個 ELF 虛擬位址區段:
RX code cave (可執行程式碼區段) :位於虛擬位址 vaddr = 0x03c97ef8(對應檔案偏移 file_offset = 0x03c97ef8),模組本身佔用 40,600 bytes(連同第一階段基底 cave 合計為 63,096 RX bytes)。
BSS state region (執行期狀態緩衝區) :位於虛擬位址 vaddr = 0x05cf18c0,大小為 136,384 bytes(0x214c0 bytes),用於在 RAM 中存放從 SD 卡解析出來的 .BIN 濾鏡包、當前啟用的 6/32 字元外觀名稱,以及準備傳送給 CP 的 IPC 傳輸緩衝區。
為了讓原本 appFw.so 的生命週期與選單事件流進這塊 code cave,建置腳本在 appFw.so 內植入了 17 處 ARM64 B / BL call-site hooks :
Hook 名稱
檔案偏移量 (File Offset)
原廠指令 (Hex)
修補後跳轉指令 (Hex)
攔截時機與用途
startup
25,170,660 (0x18012a4)
60a26e39
0544da15
系統啟動時跳入初始化跳板
initial
55,422,568 (0x34daae8)
60c21091
69dd6615
Creative Look 子系統初始狀態建立
completed
55,464,256 (0x34e4dc0)
60c21091
fab46615
屬性載入完成後同步自訂濾鏡狀態
media
54,279,380 (0x33c38d4)
a003168b
5b3a6b15
監控 SD 卡掛載狀態(/mnt/sd0、/mnt/sd1)
inbound
55,921,996 (0x35549cc)
20008052
86f66415
攔截並注入跨核心 IPC 濾鏡資料
power
56,306,296 (0x35b26f8)
fd7bbba9
a57f6315
電源關閉或進入休眠前保存狀態
menu / UI hooks (×11)
多處 UI 分派點
原生選單指令
跳轉至 r5_filter_Menu
註冊 Fn 項目、繪製 6/32 字元名稱、查詢 v4.34 圖示 (icon_query434) 等 11 處選單攔截點
原地 Ext2 區塊整合 (integrate())
由於 nflasha15 是一整個 410,652,672 bytes 的 ext2 檔案系統映像,把變大後的 appFw.so(97,452,032 bytes)寫回 nflasha15 時不能隨便重新打包整個檔案系統(否則 inode 順序或時間戳記改變可能引發非預期行為)。
建置腳本透過 nflasha15_layout 先為 /lib/appFw.so 的 inode 擴充對應的資料區塊鏈(block chain),接著 integrate() 直接將新版 appFw.so 的內容覆寫進對應的 ext2 data blocks,並在最後執行嚴格的位元級斷言:確保 nflasha15 分區中除了 /lib/appFw.so 佔用的區塊與對應的 ext2 metadata 之外,其餘每一個 byte 都與原廠映像 100% 完全相同 。
深入 CP 修補工程:cpapp-b.bin 表格重定位與 Unicorn 虛擬 CPU 模擬
看完了 AP 端,接著來看更刺激的 CP 端(nflasha8:/CPAPP-B.BIN,基底位址 0x67800000)。
為什麼必須重定位 12 組 Look 參數表?
在原廠 R5 v4.00 的 cpapp-b.bin 中,CP 將 10 組 Creative Look(ST..SE)的 ISP 參數存放在固定長度為 10 的連續陣列裡,主要包含三大結構:
s_kelvin 與 s_adobekelvin 色溫補償表(每組 0xDC bytes,10 × 0xDC = 2,200 bytes)。
tone / gamma / 色彩矩陣結構(每組 0x16B4 = 5,812 bytes)。
5×9×9 3D HSV 色彩調整格 :每組 184 bytes,由 5 個明度階層 luma levels × 9 個飽和度環 saturation rings × 9 個色相扇區 hue sectors 組成。
問題在於:在 cpapp-b.bin 的記憶體佈局中,這幾個 10-entry 陣列的屁股後面緊接著其他關鍵的 RTOS 全域變數!如果我們直接在原位址把陣列從 10 組硬撐到 12 組(+FL2, +FL3),多出來的 2 組資料就會直接蓋掉後面的 RTOS 全域變數,導致相機一開機就崩潰。
因此,建置管線(build_cp.py)採取了 完整重定位 (table relocation) 策略:
在 cpapp-b.bin 後段找到一塊乾淨的未使用記憶體區段 0x688df740(其中可執行區段位於 RX = 0x68995080,包含 7,308 bytes 的 C 執行期程式碼 cp_transfer.c / cp_stage413_probe.c,以及 522,152 bytes 的擴充表格與狀態空間)。
將擴充為 12 組的完整 look 參數表搬移至 0x688df740。
自動掃描整個 cpapp-b.bin,一舉修補了 167 處 ARM32 指令 ——包含將所有查表函式的 literal-pool 基底位址改指到 0x688df740 新表,以及將所有的陣列邊界檢查從 cmp rX, #10 修改為 cmp rX, #12。
刷機前的虛擬 CPU 守門員:Unicorn Engine (getter_gates.py)
一口氣修改了裸機 RTOS 裡的 167 處 ARM32 指令,要怎麼確定沒有任何一條指令改錯、或者不小心破壞了呼叫慣例(calling convention)?
這套專案設計了一道極為嚴謹的防線:src/gamma/getter_gates.py。在每次打包韌體前,它會使用 Unicorn Engine 建立一顆 32 位元 ARM 虛擬 CPU(UC_ARCH_ARM, UC_MODE_ARM),將 17.6 MB 的 cpapp-b.bin 實際映射載入至虛擬位址 0x67800000:
啟用硬體浮點與向量協同處理器 (VFP / NEON) :
因為 CP 的色彩查表與插值函式內部使用了 ARM VFP 指令,模擬器在啟動時會先透過協同處理器暫存器啟用 CP10 與 CP11:
mrc p15 , 0 , r0 , c1 , c0 , 2
orr r0 , r0 , #0x00f00000 ; 啟用 CP10 / CP11 (VFP/NEON 完全存取權限)
mcr p15 , 0 , r0 , c1 , c0 , 2
mov r0 , #0x40000000 ; EN bit in FPEXC
vmsr fpexc , r0
遍歷模擬 3 種狀態 × 12 組 look 索引 :
模擬器直接設定 PC 呼叫 cpapp-b.bin 內真實的 getter 函式(如 0x684b5148、0x684b5220、0x684b527c 等),針對 look 索引 1..12 依序測試三種執行期狀態:
狀態 A :cold boot 未初始化預設狀態
狀態 B :載入內建 FL2 / FL3 預設值狀態
狀態 C :由 SD 卡 .BIN 濾鏡覆寫全部 12 組槽位狀態
位元級與暫存器完整性斷言 :
針對索引 1..10(原廠 10 種外觀),比對修補後韌體回傳的結構體每一個 byte,必須與未修改的原廠 cpapp-b.bin 100% 逐位元一致 。
針對索引 11..12(FL2、FL3),驗證回傳的指標與內容精準對應 0x688df740 重定位表中的資料。
檢查函式返回時的 callee-saved 暫存器(r4..r11 與 sp),確保堆疊與暫存器零污染。
Windows + LLVM Clang 23.1.4 本地建置調適與跨工具鏈比對
上游專案原本是在 macOS 環境下使用 Apple Clang 21(clang 21.1.x)開發,並在腳本中寫死了 /usr/bin/clang 與 /usr/bin/openssl 等路徑。當我們將建置管線移植到 Windows 11 並使用 LLVM Clang 23.1.4 與 uv(cpython-3.12.11)執行本地編譯時,解決了三個關鍵的跨平台與編譯器差異:
Clang 23 新增的全域變數靜態分析警告 :
LLVM Clang 23 引入了更嚴格的 -Wunused-but-set-global 檢查,在開啟 -Werror 時會攔截 r5_filter_renderer423.c:23 中的診斷計數器 static uint32_t allocation_failures。我們在 build_ap_module.py 與 build_cp.py 的交叉編譯旗標中補上了 -Wno-unused-but-set-variable 與 -Wno-unused-but-set-global。
0701_part_image_sum/part_image.sum 的 454-Byte CRLF 陷阱 :
在打包 original.tar 前,build.py 會重新計算 dev/nflasha5、dev/nflasha8 與 dev/nflasha15 的 CRC32 校驗碼並更新 part_image.sum。因為該建置管線採用原地覆寫 tar 成員的策略,part_image.sum 的檔案大小必須嚴格維持在 454 bytes。
在 Windows 上,Python 的 Path.write_text() 預設會將換行符號 \n 轉換為 \r\n(CRLF),導致這份 6 行校驗檔瞬間膨脹為 460 bytes 並觸發 tar 區塊大小 AssertionError!將寫入方式改為 sums.write_bytes(text.encode('ascii')) 後,便完美保持了 UNIX LF 的 454 bytes 結構。
Apple Clang 21 與 LLVM Clang 23 的產出比對 :
我們將 Windows + LLVM Clang 23.1.4 編譯出的成品與原作者 macOS + Apple Clang 21 的參考輸出進行了逐檔案比對:
所有非編譯型資料產出(包含 nflasha5、12 個地區的 CX8920*.bin 出廠備份範本與 backup.sum)的 SHA-256 雜湊值 100% 相同 ,例如 nflasha5 皆為 eac727efd7af2142ddd9b8ae22387614eaaaba9dc9f04ce0434bf8e7c766d693。
經過 C 編譯器產出的 appFw.so(97,452,032 bytes)與 cpapp-b.bin(17,617,008 bytes)不僅檔案大小完全一致,內部的 RX / BSS 區段尺寸(AP 端為 40,600 B / 136,384 B;CP 端為 7,308 B / 522,152 B)與 17 個 hook 跳板位址也分毫不差,僅在 C 函式內部有 Clang 21 與 Clang 23 之間的微幅指令排程與暫存器分配差異,且全數通過 Unicorn 模擬測試。
密碼學簽章驗證、Partition 1 (nflasha1) 信任鏈與 Android Fastboot
當我們修改完 nflasha5、nflasha8 與 nflasha15,並重新打包、以 CXD90057_k8 加密出 736,841 個 FDAT 區塊後,就來到了整個韌體工程最後、也最關鍵的一道安全防線:數位簽章驗證 。
為什麼不能直接把改好的 BODYDATA.DAT 放進原廠相機更新?
在 BODYDATA.DAT 的 FDAT 區段最尾端(檔案偏移 754,525,332,緊接在 736,841 個加密區塊與 16-byte IV 之後),存放著一段 256 bytes 的 RSA-2048 數位簽章 ,採用 RSASSA-PKCS1-v1_5 填充與 SHA-256 雜湊,簽章範圍涵蓋了前面整整 754,525,200 bytes 的加密串流與 IV。
當你在原廠未改機的 R5 上透過 SD 卡執行韌體更新時,相機其實是重新開機進入 Partition 1 (nflasha1) 的更新環境:
nflasha1 是一個 25,100,288 bytes(約 23.9 MB)的 FAT12 復原分區,裡面放著 /boot/initrd.img(一個以 zlib 壓縮的 SquashFS 4.0 根檔案系統)以及記錄檔案 MD5 的 /p1chksum。
在 /boot/initrd.img 內部,/etc/VerificationKey.pem(451 bytes,SHA-256 為 8281413b5607446939613e07d020b2c7d8ee2371349099de56bc8288f7be0a43)存放著 Sony 官方的 RSA-2048 公鑰。
更嚴密的是,initrd.img 裡負責韌體更新的四個核心共享函式庫:
/usr/lib/libupdaterapi.so
/usr/lib/libsdfileinputupdaterapi.so
/usr/lib/libseninputupdaterapi.so
/usr/lib/libuudfileinputupdaterapi.so
在執行更新時會進行 雙層驗證 :
第一層公鑰指紋驗證 (_ZN7Updater21SignatureVerification9verifyKeyEPKc) :即 Updater::SignatureVerification::verifyKey(char const*),先計算 /etc/VerificationKey.pem 的指紋,檢查它是否等於寫死在 .so 裡的 Sony 官方公鑰雜湊!這意味著就算你只換掉 .pem 公鑰檔,第一層檢查也會直接報錯拒絕。
第二層密碼學簽章驗證 (_ZN7Updater21SignatureVerification15VerifySignatureEPKhjS2_j) :使用 /etc/VerificationKey.pem 對 BODYDATA.DAT 的 FDAT 區段執行標準的 RSA-2048 PKCS#1 v1.5 SHA-256 數位簽章驗證。
一次性硬體引導:RP2040 UART 與隱藏在相機裡的 Android Fastboot v0.5
因為全世界只有 Sony 擁有對應的 RSA-2048 私鑰,任何想要透過 SD 卡刷入自簽章 BODYDATA.DAT 的相機,都必須先透過硬體除錯介面完成一次性的 Partition 1 (nflasha1) 公鑰替換 。
這套硬體引導流程的底層機制最早由安全研究員 David Buchanan 在 DavidBuchanan314/ILCE-7M4-RE 中發掘,完整運作過程如下:
透過 Sony Multi-port UART 進入暫時性 SRAM Fastboot :
使用 Sony Multi-port 轉接板連接至一塊刷有 uart_boot 韌體的 RP2040(接線為 GPIO28 ➔ RX、GPIO29 ➔ TX、GPIO27 ➔ Reset,初始握手鮑率為非標準的 48000 baud),同時將相機 USB-C 接上電腦。在重置瞬間,主機透過 UART 將 Sony 官方簽章過的 BootROM SRAM loader(fastboot.bin)串流寫入相機內部 SRAM 位址 0x92028000。隨後 UART 切換至 115200 baud 並印出:
Sony 的 mira loader 採用標準 Google Android Fastboot v0.5 :
當 mira 啟動 USB 控制器後,電腦端看到的 USB 裝置識別碼竟然是 Google 官方的 Android Fastboot ID :
VID = 0x18d1 (Google Inc.)
PID = 0x4ee0 (Android Fastboot)
bInterfaceClass = 0xFF、bInterfaceSubClass = 0x42、bInterfaceProtocol = 0x03
透過 USB bulk endpoint 發送 getvar:version 會直接回傳 OKAY0.5。它不僅完整支援標準 Android Fastboot 指令(getvar:partition-size:nflasha1、download:<hex_len>、upload、flash:nflasha1、reboot)與標準的 OKAY / FAIL / INFO / DATA%08x 封包協定,甚至原生支援 Android Sparse Image 格式(魔術字元 0xed26ff3a,v1.0,512-byte 磁區區塊,使用 0xCAC1 RAW chunk 寫入有變動的磁區、以 0xCAC3 DONT_CARE chunk 跳過未修改磁區)!
此外,Sony 工程師還在 mira 裡實作了三個專屬的 oem 擴充指令:
oem partition:列出內部 SDM2 eMMC 上全部 17 個分區(nflasha1 .. nflasha17)。
oem partition dump nflasha1(搭配 upload):將 Partition 1 讀取至 RAM 並透過 USB 完整傾印回電腦。
oem dumpbrom:傾印 CXD90057 的晶片內部 BootROM。
原地修補 nflasha1 的 8-Byte verifyKey() 與 Sparse Image 回寫
取得相機傾印出的 nflasha1 映像後,build_r5_partition1.py 與 build_r5_verifykey_skip.py 會在不改變分區與壓縮區塊幾何結構的前提下進行原地修補:
替換公鑰 :將 initrd.img 內的 /etc/VerificationKey.pem(451 bytes)替換為使用者自己生成的 RSA-2048 public.pem(同樣為 451 bytes)。
修補 4 個 updater .so 的 verifyKey() 函式入口 :
將 _ZN7Updater21SignatureVerification9verifyKeyEPKc 開頭的 8 bytes(兩條 ARM64 指令)替換掉:
; 修補前 (Before): fd 7b b2 a9 e0 03 01 aa
stp x29 , x30 , [sp , #- 0x50 ]!
mov x0 , x1
; 修補後 (After): 00 00 80 52 c0 03 5f d6
mov w0 , #0 ; 永遠回傳 0 (代表自訂公鑰指紋合法)
ret
這個 8-byte 修補讓 verifyKey() 認可我們放入的自訂 public.pem,同時 完整保留了 VerifySignature() 的所有密碼學檢查,因此相機在透過 SD 卡刷機時,依然會嚴格驗證 BODYDATA.DAT 的 RSA-2048 數位簽章。
等長重壓縮與 233 磁區 Sparse Image 刷寫 :
腳本只重新壓縮 initrd.img 中受影響的 zlib SquashFS fragments,並精確控制壓縮參數使其與原始 fragment 的 byte 長度完全相同;接著更新 /p1chksum 裡的 initrd.img MD5,最後將 25.1 MB 的 nflasha1 中實際發生變動的 233 個 512-byte 磁區 封裝成一個不到 125 KB 的 Android Sparse Image,透過 flash:nflasha1 瞬間寫回相機。
本地生成自簽章 BODYDATA.DAT 與 100% 解密驗證
在完成所有分析與工具鏈調適後,我們在本地生成了一組全新的 2048-bit RSA 金鑰對(e = 65537):
私鑰 private.pem(1,679 bytes),用於簽署自製 BODYDATA.DAT。
公鑰 public.pem / VerificationKey.pem(451 bytes,SHA-256 為 9b2ab38babd06c8aea18a4064ddc7bbd4dd9014ed14c16db369df8363fc229c8),用於注入 nflasha1。
接著我們執行完整的 --stage all 自動化建置與簽章管線:
uv run python firmware/r5/build.py --inputs firmware/r5/inputs.local.json --output D:/code/r5-build-all --stage all
整條管線依序完成了以下工作並產出完整驗證報告(D:\code\r5-build-all\verification.json):
編譯並整合雙處理器映像 :產出修補後的 appFw.so、cpapp-b.bin、nflasha5(WBI2 ➔ wbi2)、nflasha8、nflasha15、part_image.sum(精確 454 bytes)以及 12 個地區的 CX8920*.bin 備份檔與 backup.sum,並全數通過 Unicorn Engine 虛擬 CPU 模擬閘門。
封裝明文 FDAT 串流 :組裝出 751,577,088 bytes 的明文 FDAT 映像(SHA-256 為 a46c6bb0811064cec9dce08b47deee40116ea5d51f255faf26d8814684361635)。
雙態 AES 區塊加密與 RSA-2048 簽章 :將明文切分為 736,841 個 1,024-byte 區塊(754,525,184 bytes),以 AES-ECB + AES-CBC(CXD90057_k8, iv) 加密,使用 private.pem 對加密內容與 IV 產生 256-byte RSASSA-PKCS1-v1_5 / SHA-256 簽章,最後計算最外層 DEND 的 CRC32,產出最終的 BODYDATA.DAT(754,525,608 bytes,SHA-256 為 b267a02ba29a7c8f84cbd38fa11413c903772f73d64ffd34e2a61129533cd0f6)。
雙向密碼學與解密完整性驗證 :
同時透過 PyCryptodome 與獨立的 openssl pkeyutl -verify 驗證 BODYDATA.DAT 尾端的 256-byte RSA-2048 簽章,確認回傳 Signature Verified Successfully,並測試竄改簽章後能被正確拒絕(tampered_signature_rejected: true、outer_crc_valid: true)。
將產出的 BODYDATA.DAT 重新丟回解密器,獨立解開全部 736,841 個區塊,確認解出的每一顆 byte 都與打包前的明文分區 100% 吻合。
結語
回頭看這次對 Sony A7R V 第三方韌體 v4.34 的拆解與本地建置,最讓我著迷的不只是最終在相機上多出了 FL2、FL3 與 12 組 Fujifilm 軟片模擬,而是得以深入現代高階數位相機內部的系統工程設計:
一顆 CXD90057 SoC 同時跑著四核 64 位元 SCEL Linux(負責 97.3 MB 的 /lib/appFw.so 單體應用)與 32 位元 Cortex-R5 TOPPERS/FMP RTOS(在 0x67800000 負責硬體即時 ISP 管線),並透過 nflasha5 的 WBI2 LZO 記憶體休眠快照達成 0.5 秒瞬間開機。
面對高風險的雙架構二進位修改,透過原地 ext2 區塊替換、重定位 12 組 ISP 參數表至 0x688df740,並結合啟用 VFP/NEON 的 Unicorn Engine 虛擬 CPU 模擬閘門,將傳統嵌入式修改「刷上去才知道會不會磚」的不確定性,轉化為刷機前就能 100% 自動化驗證的工程管線。
而在底層 BootROM 的 mira SRAM loader(0x92028000)中遇見熟悉的 Google Android Fastboot v0.5(0x18d1:0x4ee0)與 Sparse Image 協定,並只用 8 bytes(mov w0, #0; ret)優雅地將自己的 RSA-2048 金鑰對接入 nflasha1 信任鏈,更是跨領域逆向工程中那種「原來底層都是相通的」最大樂趣。
從原始碼編譯與客製化 Ioskeley Mono
解剖 Iosevka 參數化造字管線、Patel 幾何引擎與 Nerd Font 封裝實戰
在上一篇〈Berkeley Mono 與 Ioskeley Mono 〉中,我們深入比較了 U.S. Graphics Company 的商業字體 Berkeley Mono (TX-02) 與開源社群基於 Iosevka 打造的 Ioskeley Mono ,並透過線上互動實驗室對照了兩者在點狀 0、單層 g、平弧括號 () 以及 Normal / SemiCondensed 字寬上的細節表現。
然而,當我把 Ioskeley Mono 換成日常主力字體、在編輯器與 terminal 裡寫了一整天程式碼後,心裡突然冒出一個小小的念頭:如果我想讓數字 7 的斜桿正中央多加一道水平橫槓(crossbar),就像歐洲手寫體或老式工程圖紙上的 7 那樣,有沒有辦法自己動手改?
如果今天面對的是一般用 FontForge 或 Glyphs 手工繪製的傳統字體,這個念頭大概會在五分鐘內被打消——因為一個完整的現代程式字體家族包含 10 種字重(Thin 100 到 Black 900)、2 種字寬(Normal 與 SemiCondensed)以及正體與斜體,光是一個家族就有 40 個 .ttf 字型檔 。手動去改 40 個檔案的貝茲曲線,不僅筆畫粗細與斜率難以對齊,還會破壞原有的 TrueType hinting 與連字表。
但 Ioskeley Mono 最迷人的地方就在這裡:它不是一套靜態的向量圖檔,而是一座由程式碼驅動的參數化造字工廠。 昨晚我在 herdr 的 ioskeley-mono workspace 裡,直接從零複製儲存庫、修改一行設定、一路追蹤到底層的 Patel (.ptl) 幾何語言,並寫腳本用 8 個平行 worker 打上完整的 Nerd Font 圖示補丁。這篇文章就來完整紀錄這趟從原始碼編譯與客製化字體的實戰過程,以及途中踩到的兩個精彩陷阱。
為什麼從原始碼編譯字體比想像中更有趣?
打開 ahatem/IoskeleyMono 的儲存庫會發現,它本身並不儲存成千上萬個字元的曲線座標,核心只有一份不到 400 行的 private-build-plans.toml 藍圖檔,搭配上游的 Iosevka 造字引擎(固定於 v34.4.0)。
當我們從原始碼建置時,整個流程其實是一條橫跨編譯器、計算幾何與二進位封裝的八階段管線:
藍圖結構:四個繼承自同一核心的字體家族
在 private-build-plans.toml 中,作者透過 Iosevka 的繼承機制(inherits = "buildPlans.IoskeleyMono")定義了四個編譯目標(build plans):
標準版 IoskeleyMono :設定 spacing = "normal"、serifs = "sans" 與 noCvSs = true,包含完整的程式碼連字規則(calt)、10 種字重、2 種字寬(Normal = 600、SemiCondensed = 540)與 11.8° 斜體。
終端機版 IoskeleyMonoTerm :繼承自標準版,唯一關鍵差異是設定 spacing = "term"。這會強制將所有箭頭、幾何方塊與 box-drawing 符號嚴格限制在單一 terminal 字元格(character cell)寬度內,避免在 Ghostty、Kitty 或 Alacritty 中破格。
無連字版 IoskeleyMonoNL :設定 noLigation = true,供無法單獨關閉連字的編輯器(如舊版 Xcode)使用。
網頁子集版 IoskeleyMonoWeb :透過 [buildPlans.IoskeleyMonoWeb.subset.include] 僅保留拉丁字母、標點、箭頭、數學運算子與框線符號的 Unicode 區間,將原本完整版每個約 480–550 KB 的 .woff2 透過字元集子集化(subsetting)大幅縮減至約 94 KB。
此外,所有家族都共享一組精心調校的度量參數(private-build-plans.toml 第 201 至 223 行):
[buildPlans .IoskeleyMono .metricOverride ]
xHeight = 520 # 小寫字母高度
cap = 690 # 大寫字母與數字高度 (CAP)
ascender = 740 # 上伸部高度
sb = 85 # 左右邊距 (Sidebearing)
accentWidth = 182 # 變音符號寬度與間距參數
accentClearance = 76
accentHeight = 162
accentStackOffset = 208
leading = 1250 # 預設行距
parenSize = 860 # 括號垂直跨度
dotSize = 'blend(weight, [100, 110], [400, 125], [900, 150])'
periodSize = 140
essRatio = 1.03 # 微調 S / s 與問號中央轉折處的水平加寬比例
essRatioUpper = 1.03
essRatioLower = 1.03
essRatioQuestion = 1.03
archDepth = 152
smallArchDepth = 157
注意看 dotSize 那行:blend(weight, [100, 110], [400, 125], [900, 150]) 是一個隨字重動態插值的函數,確保在極細體(Thin 100)時標點與 i 的方點不會太小而消失,而在極粗體(Black 900)時又不會過度膨脹糊成一團。
本地端建置環境準備
要在 Linux 本地端從頭編譯 Ioskeley Mono,只需要準備 Node.js 20+ (上游 package.json 最低需求為 >=16.0.0,官方 CI 採用 Node.js 20)與 ttfautohint (用於產生標準解析度螢幕所需的 TrueType hinting 指令):
# 1. 複製 Ioskeley Mono 儲存庫
git clone [email protected] :ahatem/IoskeleyMono.git ioskeley-mono
cd ioskeley-mono
# 2. 複製上游 Iosevka 引擎(鎖定 v34.4.0 確保變體名稱與度量完全一致)
git clone --depth 1 --branch v34.4.0 https://github.com/be5invis/Iosevka.git iosevka-src
# 3. 確認系統已安裝 ttfautohint 與 fontforge(後續打 Nerd Font 補丁需要)
sudo pacman -S --needed ttfautohint fontforge python-fonttools
為什麼一定要鎖定 Iosevka v34.4.0?因為上游 Iosevka 的 main 分支演進速度極快,有時會重新命名部分字形變體或調整內部幾何常數;固定在 v34.4.0 可以確保我們編譯出來的結果與官方發行版完全一致。
一行設定的魔法:為數字 7 加上水平橫槓
現在回到我們最初的目標:把數字 7 改成中間帶有水平橫槓的版本。
打開 private-build-plans.toml 的第 15 行,在 [buildPlans.IoskeleyMono.variants.design] 區塊中可以看到上游預設使用的是無襯線直桿 7:
[buildPlans .IoskeleyMono .variants .design ]
one = "no-base"
two = "straight-neck-serifless"
three = "flat-top-serifless"
four = "semi-open-serifless"
five = "oblique-flat-serifless"
six = "open-contour"
seven = "straight-serifless" # <-- 上游預設:無橫槓的俐落直桿 7
eight = "two-circles"
nine = "open-contour"
zero = "dotted"
在 Iosevka 的命名系統中,數字 7 的變體名稱是由三個正交維度組合而成的:[主幹形狀 body]-[左上角襯線 serif]-[中段橫槓 overlay]:
變體名稱 (seven = ...)
主幹形狀 (body)
左上角垂直短襯線 (serif)
中段水平橫槓 (crossbar)
視覺風格建議
straight-serifless(預設)
直線斜桿
無
無
極簡現代,貼近 TX-02 原生設定
straight-serifless-crossbar
直線斜桿
無
有
最推薦:保留幾何直桿,同時強化 7 與 1/T 的辨識度
straight-serifed-crossbar
直線斜桿
有
有
帶有濃厚打字機與古典印刷感
bend-serifless-crossbar
微彎弧形斜桿
無
有
偏柔和手寫風格,較不適合方正的 TX-02 家族
因為 IoskeleyMonoTerm、IoskeleyMonoNL 與 IoskeleyMonoWeb 全部都透過 inherits = "buildPlans.IoskeleyMono" 繼承字形設定,我們只需要改動 private-build-plans.toml 的第 15 行:
seven = "straight-serifless-crossbar"
接著把修改後的設定檔複製進 iosevka-src/,啟動 Verda 建置系統:
cp private-build-plans.toml iosevka-src/
cd iosevka-src
npm install
# 同時編譯標準版 (IDE 用) 與 Term 版 (終端機用)
npm run build -- contents::IoskeleyMono contents::IoskeleyMonoTerm
Verda 是 Iosevka 作者 Belleve Invis 專門開發的增量平行建置引擎(定義於 iosevka-src/verdafile.mjs)。當我們修改 private-build-plans.toml 並針對單一家族執行建置時,Verda 會自動解析相依圖、調動多核心 CPU 平行更新 614 個建置目標 (終端機印出 Finished with 614 targets updated.),短短幾分鐘內就在 iosevka-src/dist/ 下產出全部 40 種樣式的 TTF/(Hinted)、TTF-Unhinted/、WOFF2/ 與 WOFF2-Unhinted/。
深入造字引擎:Patel (.ptl) 如何將一行字串編譯成 TrueType 輪廓?
改一行 TOML 就能讓 40 個字型檔同步長出比例完美的水平橫槓,這背後到底發生了什麼事?我在 ioskeley-mono workspace 中一路追進了 iosevka-src/packages/font-glyphs/src/number/7.ptl,發現 Iosevka 內部的幾何編譯管線設計得極為精妙。
讓我們結合實際從編譯後的 IoskeleyMono-Regular.ttf 抽取出來的 TrueType 頂點座標,一步步拆解 "straight-serifless-crossbar" 是如何變成字型檔裡的二進位曲線(下圖與後文的頂點追蹤圖皆直接擷取自編譯後 TTF 的真實 glyf 輪廓座標):
Patel 語法編譯與 SuffixCfg.weave 變體編織
Iosevka 所有的字形都不是手畫的,而是用一種名為 Patel (.ptl)的領域特定語言(Lisp 方言,採用縮排語法)撰寫的程式。由於 Node.js 無法直接執行 .ptl,建置系統第一步會呼叫 patel-c 編譯器,將 7.ptl 轉譯為標準的 JavaScript ES Module(lib/number/7.mjs):
patel-c --strict --esm packages/font-glyphs/src/number/7.ptl -o packages/font-glyphs/lib/number/7.mjs
打開 packages/font-glyphs/src/number/7.ptl 的第 65 至 93 行,可以看到 SuffixCfg.weave 如何動態編織出所有合法的 7 變體組合:
local SevenConfig : SuffixCfg.weave
object # body
straight StraightBody
bend BendBody
curly CurlyBody
object # serif
serifless nothing
serifed StartSerif
function [body] : object # crossbar
"" nothing
crossbar : match body
[Just "straight" ] Crossbar
[Just "bend" ] BendCrossbar
[Just "curly" ] CurlyCrossbar
foreach { suffix { Body Serif Crossbar } } [Object.entries SevenConfig] : do
create-glyph "seven.lnum.\(suffix)" : glyph-proc
include : MarkSet.capital
if Body : include : Body CAP
if Serif : include : Serif CAP
if Crossbar : include : Crossbar CAP
create-glyph "seven.onum.\(suffix)" : glyph-proc
include : OnumMarks.p
include : refer-glyph "seven.lnum.\(suffix)"
include : ShiftDown
select-variant 'seven.lnum ' [CodeLnum '7 ' ] (follow -- 'seven ' )
select-variant 'seven.onum ' [CodeOnum '7 ' ] (follow -- 'seven ' )
當我們在 TOML 中指定 seven = "straight-serifless-crossbar" 時:
Body 對映到 StraightBody 幾何程序;
Serif 對映到 nothing(不畫左上角垂直襯線);
Crossbar 根據 body == "straight" 自動匹配到 Crossbar 程序;
最後 select-variant 將產生的 seven.lnum.straight-serifless-crossbar 綁定到等高數字(Lining Figures)的 Unicode 碼位 U+0037 ('7') (同時也透過 seven.onum 衍生出供 OpenType onum 特徵使用的老式高低數字版本)。
參數化座標求值:StraightBody 與 Crossbar
當 Node.js 在編譯某個特定字重與字寬(例如 Regular 400,字寬 600、大寫高度 CAP = 690、筆畫粗細 Stroke = 73、左右邊距 SB = 85、右邊界 RightSB = 600 - 85 = 515)時,它會把這些度量常數代入 7.ptl 第 13 至 20 行與第 44 至 49 行的幾何公式中:
define SevenXLeft : mix SB RightSB 0.28
define [StraightBody top] : union [HBar.t SB RightSB top] : intersection
Rect (top - Stroke) 0 SB RightSB
dispiro
widths.rhs
corner RightSB (top - Stroke)
corner [mix RightSB (SevenXLeft + [HSwToV : 0.75 * Stroke]) 2 ] [mix (top - Stroke) 0 2 ]
# ... (第 21 – 43 行定義 BendBody、 CurlyBody 與 StartSerif)
define [Crossbar top] : glyph-proc
local m : mix SevenXLeft (RightSB - [HSwToV HalfStroke]) 0.5
include : HBar.m
m - [HSwToV QuarterStroke] - LongJut * 0.8
m + [HSwToV QuarterStroke] + LongJut * 0.7
mix 0 (top - QuarterStroke) 0.5
這幾行 Patel 程式碼藏著字體設計師滿滿的視覺補償細節:
頂部橫樑 (HBar.t SB RightSB top) :在 X = 85..515、Y = 617..690 畫出一道頂部切齊 CAP (690)、厚度為 Stroke (73) 的水平橫樑。
斜向主幹 (dispiro + Rect 交集裁切) :
如果直接畫一條斜線段,斜線的上下兩端切口會垂直於斜線本身(呈現斜角突出)。因此 Iosevka 故意用 mix ... 2 把 dispiro 斜桿往外延伸兩倍長,再用一個 Rect (top - Stroke) 0 SB RightSB(即 Y = 0..617 的矩形遮罩)做布林交集 (intersection)!這樣斜桿頂端就會完美貼合在 Y = 617(頂部橫樑下緣),底端則在 Y = 0 基線上切出俐落的水平平頭(X = 180..267)。
水平橫槓 (Crossbar) 的不對稱視覺平衡 :
垂直位置 :不是單純取 CAP / 2 = 345,而是 mix 0 (top - QuarterStroke) 0.5 = 336(扣除頂部橫樑視覺重量後的幾何中心,落在 Y = 299..373)。
水平位置 (m) :計算斜桿在半高處的水平中心點 m。
左右突出的視覺微調 :注意橫槓往左延伸了 0.8 * LongJut(到 X = 158),往右卻只延伸 0.7 * LongJut(到 X = 501)!為什麼左邊比右邊多出 0.1 * LongJut?因為 7 的斜桿是從右上往左下傾斜,左上方的字懷空間比右下方開闊;左端稍微多探出一點,在人眼看來才會覺得橫槓「正好位在斜桿正中央」。
Spiro 螺旋曲線展開與 2D CSG 布林聯集 (typo-geom)
在傳統字體設計中,曲線是由三次貝茲曲線(cubic Bézier)的控制點手拉出來的;而在 Iosevka 中,曲線是由 Raph Levien(同時也是 Google Fonts Inconsolata 與 Zed GPUI 部分文字渲染的先驅)提出的 Spiro 螺線演算法 (Euler spiral)計算而成,確保曲率達到 G2 甚至 G4 連續。
經過 spiro-pen-expand.mjs 將一維骨架向兩側展開成二維筆畫多邊形後,重頭戲交給了 typo-geom 幾何引擎:
當我們把頂部橫樑、斜向主幹與水平橫槓透過 union 組合在一起時,三個零件在十字交會處會有內部的重疊邊線。
如果直接把三個重疊的矩形寫進字型檔,在開啟半透明渲染、外框描邊或低解析度光柵化時,交界處會出現破洞(winding rule 衝突)或墨水過深。
typo-geom 會執行嚴謹的 2D 建構實體幾何布林聯集運算 (CSG Union),自動找出所有交點(例如斜桿與橫槓相交的 (300, 299)、(329, 373)、(417, 373)、(388, 299) 四個內角),徹底剔除內部重疊線段,融合成單一封閉的 15 頂點外輪廓 !
我們直接用 Python 的 fontTools 把編譯出來的 IoskeleyMono-Regular.ttf 拆開,印出 seven 字形的 SVG 路徑指令,就能親眼驗證這個結果:
M180 0 300 299H158V373H329L427 617H85V690H515V617L417 373H501V299H388L267 0Z
順著上圖右半邊標出的 [1] 到 [15] 號頂點走一圈,可以看到 typo-geom 如何把原本三個重疊的幾何零件融合成一條順時針方向(TrueType 外輪廓規範)的一筆畫封閉路徑:從基線左下角 [1] (180, 0) 出發,沿斜桿左緣爬升到橫槓下緣交角 [2] (300, 299),往左折出橫槓左翼 [3]➔[4]➔[5],繼續沿斜桿爬升到頂樑下緣交角 [6] (427, 617),繞過頂部橫樑 [7]➔[8]➔[9]➔[10],再沿斜桿右緣一路折出橫槓右翼 [11]➔[12]➔[13]➔[14] 回到基線右下角 [15] (267, 0) 閉合。整顆字元沒有任何多餘的內部節點,而且當 Verda 編譯 Thin (100)、Bold (700)、Black (900) 或 SemiCondensed (90%) 時,這 15 個頂點都會根據各自的 Stroke 與 Width 參數自動重新計算。
二次貝茲曲線擬合 (Quadify) 與 OpenType 封裝 (ot-builder)
最後兩個底層步驟則是跨格式的幾何轉換:
三次轉二次貝茲曲線 (Quadification) :
Iosevka 內部的幾何運算全部採用三次樣條(degree-3 cubic curves),但 TrueType(.ttf)的 glyf 表規範只支援二次貝茲曲線 (degree-2 quadratic Bézier,每段曲線只有一個 off-curve 控制點)。packages/geometry/src/quadify.mjs 會呼叫 TypoGeom.Quadify.auto(arc, 1, 8),將所有曲線自動細分並擬合為誤差小於 1 個字體設計單位(1/1000 em)的二次貝茲曲線。
二進位表格序列化 (ot-builder) 與 ttfautohint :
最後由 ot-builder 將輪廓點座標寫入 glyf 與 loca 表、將 Unicode 映射寫入 cmap 表、將連字與 zero 特徵寫入 GSUB 表,產出 TTF-Unhinted/,再交由 ttfautohint 注入對齊螢幕像素網格的 bytecode 指令產出 TTF/。
為自訂字體打上 Nerd Font 補丁:平行加速與兩個實戰陷阱
在 Linux 桌面環境(尤其是搭配 eza、lazygit、neovim、starship 與 herdr 的 terminal 工作流)中,只有純文字的 .ttf 是不夠的——我們還需要把 FontAwesome、Devicons、Octicons、Material Design Icons 與 Powerline 符號打包進去,做成 Nerd Font 版本。
平行化批次修補腳本 (xargs -P 8)
官方的 Nerd Fonts Patcher 是一個由 FontForge Python 介面驅動的腳本。如果用單執行緒 for 迴圈逐一修補 40 個字型檔,每個檔案約需 15 至 20 秒,跑完一個家族就得花上十幾分鐘。
實務上,我們可以直接下載 FontPatcher.zip(下方範例固定於 v3.5.1 確保可重現性),並利用 xargs -n 2 -P 8 啟動 8 個平行 worker 同時修補:
#!/usr/bin/env bash
set -e
# 1. 下載並解壓縮官方 Nerd Fonts Patcher (v3.5.1)
if [ ! -d "nerd-fonts-patcher" ] ; then
curl -fLO https://github.com/ryanoasis/nerd-fonts/releases/download/v3.5.1/FontPatcher.zip
unzip -q FontPatcher.zip -d nerd-fonts-patcher
fi
DIST_TERM= "iosevka-src/dist/IoskeleyMonoTerm/TTF"
PATCHER= "nerd-fonts-patcher/font-patcher"
mkdir -p patched-fonts-term/Normal patched-fonts-term/SemiCondensed
patch_font() {
local font= " $1"
local outdir= " $2"
fontforge -script " $PATCHER" " $font" \
--complete --careful --mono --outputdir " $outdir" >/dev/null 2>&1
echo "Finished $( basename " $font" ) "
}
export -f patch_font
export PATCHER
# 2. 建立任務佇列(區分 SemiCondensed 與 Normal)
for font in " $DIST_TERM" /IoskeleyMonoTerm-SemiCondensed*.ttf; do
echo " $font" "patched-fonts-term/SemiCondensed"
done > /tmp/fonts-term-to-patch.txt
for font in " $DIST_TERM" /IoskeleyMonoTerm-*.ttf; do
[[ " $font" == *Condensed* ]] && continue
echo " $font" "patched-fonts-term/Normal"
done >> /tmp/fonts-term-to-patch.txt
# 3. 使用 8 個平行 worker 批次修補 40 個字型檔
cat /tmp/fonts-term-to-patch.txt | xargs -n 2 -P 8 bash -c 'patch_font "$@"' _
這裡傳給 font-patcher 的三個參數至關重要:
--complete :注入所有開發者圖示集(FontAwesome、Devicons、Octicons、Codicons、Powerline 等)。
--careful :若圖示碼位與 Ioskeley Mono 原生已有的字元重疊,保留原生字元而不覆蓋。
--mono :將所有注入的圖示嚴格縮放至單一等寬字元格 (1-cell),產出家族名稱帶有 Nerd Font Mono 後綴的字型,確保在 terminal 裡圖示不會溢位蓋住後面的空白或文字。
FontForge 清除 post.isFixedPitch 等寬旗標的副作用
當 font-patcher 透過 FontForge 重新匯出 .ttf 字型檔時,有一個歷史悠久的經典陷阱:FontForge 有時會將 TrueType post 表中的 isFixedPitch 欄位重設為 0 。
一旦 post.isFixedPitch 不等於 1,部分 terminal 模擬器或作業系統的字體選擇器就會誤以為這是一套「比例字體(proportional font)」,進而拒絕在終端機字體清單中顯示它,或是導致字元間距計算異常。
因此在 font-patcher 跑完之後,務必補上一段幾行就能跑完的 Python fontTools 修復腳本,把所有 .ttf 的 isFixedPitch 強制改回 1:
from pathlib import Path
from fontTools.ttLib import TTFont
fixed = 0
for f in sorted(Path("patched-fonts-term" ). rglob("*.ttf" )):
font = TTFont(f)
if "post" in font and font["post" ]. isFixedPitch != 1 :
font["post" ]. isFixedPitch = 1
font. save(f)
fixed += 1
print(f "Restored isFixedPitch on { fixed} fonts" )
為何重刷 fc-cache 後終端機裡的 7 依然沒有橫槓?
這是我昨晚在 ioskeley-mono workspace 裡遇到最有趣的一個實戰插曲。
當我改完 seven = "straight-serifless-crossbar"、編譯完 contents::IoskeleyMono、跑完 40 個檔案的 Nerd Font 修補、複製進 ~/.local/share/fonts/,並執行 fc-cache -fv 重新載入後——我滿懷期待地在 terminal 裡打出 1234567890,結果畫面上的 7 依然是光禿禿的一根直桿,完全沒有水平橫槓 !
第一時間我還以為是 terminal 沒重讀字體快取(在 Kitty 可以按 Ctrl + Shift + F5 熱重載,Ghostty 可按 Ctrl + Shift + , 重載設定,Alacritty 則會自動監聽設定檔變更,VS Code 可執行 Developer: Reload Window)。但就算重開整個 terminal 視窗,7 還是沒變。
一查之下才揪出真正的兇手:
IDE 與 Terminal 使用不同的編譯目標 (IoskeleyMono vs. IoskeleyMonoTerm) :
檢查我的 ~/.config/ghostty/config、~/.config/kitty/kitty.conf、~/.config/alacritty/alacritty.toml、~/.config/foot/foot.ini 與 ~/.config/fontconfig/fonts.conf 後發現,為了讓 terminal 的格線完美對齊,我所有的終端機設定檔指定的都是 IoskeleyMonoTerm Nerd Font Mono (也就是 Term 家族),而不是標準版的 IoskeleyMono Nerd Font Mono。
~/.local/share/fonts/ 裡殘留了舊版的 Term 字型檔 :
因為我第一輪只編譯並安裝了標準版 contents::IoskeleyMono,而 ~/.local/share/fonts/IoskeleyMono/ 資料夾底下還躺著先前安裝的舊版 IoskeleyMonoTermNerdFontMono-*.ttf!於是當 Fontconfig 解析 IoskeleyMonoTerm Nerd Font Mono 時,毫無懸念地繼續指向那批還沒加上橫槓的舊檔案。
找出原因後,解法就非常俐落了:
補跑 npm run build -- contents::IoskeleyMonoTerm 編譯帶橫槓 7 的 Term 家族。
執行 patch-term-fonts.sh 將 40 個 IoskeleyMonoTerm 字型檔打上 Nerd Font 補丁並修復 isFixedPitch = 1。
清理 ~/.local/share/fonts/ 下舊的散落檔案,統一安裝至 ~/.local/share/fonts/IoskeleyMono/ 與 ~/.local/share/fonts/IoskeleyMonoNerdFont/。
重刷快取並用 fc-match 驗證最終解析路徑:
# 重新建立 Fontconfig 快取
fc-cache -fv
# 驗證 Fontconfig 實際匹配到的字型檔路徑
fc-match -v "IoskeleyMonoTerm Nerd Font Mono" | grep -E "file:|family:"
當看到輸出明確指向剛出爐的 ~/.local/share/fonts/IoskeleyMonoNerdFont/IoskeleyMonoTermNerdFontMono-Regular.ttf,再重新載入終端機,整個畫面裡的 7 瞬間長出了那道俐落的水平橫槓!
反思與取捨:為什麼在 Ghostty 環境下,我最終移除了 Nerd Font 補丁?
跑通上面整套 FontForge 平行打補丁與 isFixedPitch 修復流程後,雖然成功得到了自訂的 IoskeleyMonoTerm Nerd Font Mono,但在 Ghostty 終端機裡實際使用了一陣子後,我卻決定回過頭下達另一個指令:把所有 Nerd Font 補丁徹底移除,只安裝純淨未修補的原版 Ioskeley Mono 與 Ioskeley Mono Term 。
為什麼費工夫打完補丁後,反而選擇回歸純淨字型?關鍵在於當今像 Ghostty (以及 WezTerm、Kitty)這類現代終端機的字型渲染架構已經與幾年前截然不同。把 Nerd Font 圖示直接「焊死」進主字型檔裡,實務上會帶來四個明顯的副作用:
屏蔽了 Ghostty 原生內建的智慧圖示縮放引擎
許多人可能沒注意到,Ghostty 出廠就已經原生內建了完整的 Symbols Nerd Font 作為預設後備字型(fallback) ,不需要使用者額外安裝任何打過補丁的字體。更重要的是,Ghostty 在渲染引擎底層實作了極為細膩的動態字元格縮放邏輯(constraint-based glyph scaling) :
當 Ghostty 在你的主字型(例如純淨的 Ioskeley Mono Term)中找不到某個 Private Use Area (PUA) 圖示碼位時,它會自動切換到內建的 Nerd Font 符號表,並動態檢查該圖示右側是否緊鄰空白字元 。
在 eza -l、lazygit、yazi 檔案樹或 starship 提示字元中,圖示後面通常都會跟著一個空白( )。此時 Ghostty 會允許圖示突破單一窄格的限制,以更飽滿、清晰且符合原始長寬比的尺寸渲染。
反之,如果你安裝並指定了預先打上 --mono 補丁的 IoskeleyMonoTerm Nerd Font Mono,因為主字型檔本身已經佔據了所有 PUA 碼位,Ghostty 根本不會觸發內建的 fallback 與動態縮放引擎 ,而是直接畫出字型檔裡被 FontForge 硬生生壓扁塞進單一窄格(Width = 600 或 540)的微型圖示——導致資料夾、Git 分支與語言圖示縮得像郵票一樣小,白白浪費了 Ghostty 原生圖示渲染器的優勢。
FontForge 二次轉檔對 Hinting 與 OpenType 表格的破壞
Iosevka 編譯器透過 ot-builder 與 ttfautohint 產出的原版字型,在 OpenType 表格結構與像素對齊指令上極為嚴謹,並明確區分給標準螢幕用的 TTF/(Hinted)與給高解析度 HiDPI 螢幕用的 TTF-Unhinted/。
然而,一旦把編譯好的 .ttf 丟進 FontForge 執行 font-patcher,FontForge 會將整顆字型反序列化後再重新匯出。這個過程不僅會如前文所述清掉 post.isFixedPitch 旗標,還會改寫 name 表與原有的 hinting 結構(這也是為什麼官方釋出的 Nerd Font 壓縮包無法提供獨立的 Hinted 與 Unhinted 版本)。不經過 FontForge 二次加工,才能保留 Iosevka 100% 原汁原味的曲線與 hinting 品質。
40 倍的圖示重複塞入與 88 MB 體積膨脹
一套完整的 Nerd Fonts 圖示集包含超過一萬個向量圖形。由於圖示本身並沒有「細體 Light」或「粗斜體 Bold Italic」的差別,將圖示直接打進字型檔,等於在 10 種字重 × 2 種字寬 × 正斜體共 40 個 .ttf 檔案裡重複複製了一萬個完全相同的圖示 !
這使得單一家族的字型檔從原本輕巧的十幾 MB 暴增到 88 MB ,而且每次修改一行 private-build-plans.toml 重新編譯時,都得額外花上好幾分鐘讓 8 個 FontForge 處理程序把 CPU 燒滿。
更現代的解耦配置:純淨字體 + 終端機內建或單一符號 Fallback
把 Nerd Font 補丁移除、重新執行乾淨的 Verda 建置後:
# 直接編譯純淨的四大家族,不經過 FontForge 二次加工
cd iosevka-src
npm run build -- contents::IoskeleyMono contents::IoskeleyMonoTerm contents::IoskeleyMonoNL contents::IoskeleyMonoWeb
在 Ghostty (~/.config/ghostty/config)中,只需要直接指定純淨的 Ioskeley Mono Term,圖示就會自動由 Ghostty 內建的 Nerd Font 引擎接手渲染:
font-family = "Ioskeley Mono Term"
font-style = Regular
font-size = 11
而如果同時有使用 Alacritty、Foot 或 Waybar 等沒有內建圖示的工具,也完全不需要回頭打補丁——只要在 Linux 系統中安裝單一一個僅約 2.5 MB 的獨立符號字型套件(例如 Arch Linux 官方套件庫的 ttf-nerd-fonts-symbols-mono),讓 Fontconfig 在遇到 PUA 圖示碼位時自動 fallback 到該符號字型即可。這樣一來,文字排版由最純淨的 Ioskeley Mono Term 負責,開發者圖示由終端機內建引擎或單一符號字型負責,職責徹底解耦,建置速度與視覺品質都達到了最理想的狀態。
結語:把字體當作可編譯的基礎設施
從單純「下載別人打包好的 .zip 字型檔」,跨出一步到「自己擁有字體的建置管線」,帶來的掌控感非常奇妙。
透過 Iosevka 的 Patel 參數化引擎與 private-build-plans.toml,字體不再是一顆無法修改的黑盒子:不管你是想幫數字 7 加上水平橫槓、把預設的點狀 0 換成斜線 0、微調行距 leading,還是針對 Ghostty 的渲染特性選擇最純淨的 Ioskeley Mono Term 建置方案,都只需要改幾行設定檔,就能在幾分鐘內編譯出一套橫跨 10 種字重、2 種字寬的專屬字體家族。如果你也在使用 Ioskeley Mono,強烈推薦把儲存庫 clone 下來親手編譯一次,打造真正百分百貼合自己視覺習慣的開發者字體!
JPEG XL 與 Rust 影像生態解析
對決 Ultra HDR、AVIF、HEIC 與 WebP 的攝影實戰
對於熱愛攝影的朋友來說,硬碟空間與畫質妥協永遠是一場停不下來的拉鋸戰。
每次週末帶著相機出門拍照,回家把記憶卡插上電腦,動輒幾十 GB 的 RAW 檔就這樣倒進硬碟。一張 3,000 萬到 6,000 萬畫素的相機 RAW 原檔(像是 Sony 的 ARW、Nikon 的 NEF 或 Canon 的 CR3,即使開啟無損壓縮),單張體積依然落在 40MB 到 80MB 上下。為了長久保存,我們捨不得刪除 RAW 檔;但如果要把照片整理、出圖、分享到 Instagram 或是放上個人作品集網站,我們又往往得退回有三十多年歷史的老舊 JPEG 格式。
偏偏傳統 JPEG 的限制在現代螢幕上顯得格外刺眼:只有 8-bit 色深、缺乏原生 HDR 支援、在日落晨霧中頻頻出現的階調斷層(color banding),以及將暗部細節壓得七零八落的塊狀偽影(blocking artifacts)。雖然近年來 Google 推出了向後相容的 Ultra HDR 格式(這也是我先前用 Rust 開發圖片切割工具 TileSplit 的起因),但那畢竟是在老舊容器上打補丁的過渡方案。
難道在肥大的 RAW 原檔與打補丁的老 JPEG 之間,就沒有一個從底層重新設計、兼顧極致畫質、原生高動態範圍與合理壓縮率的現代解答嗎?
這正是 JPEG XL 格式(.jxl)當初被寄予厚望的核心使命。更有趣的是,JPEG XL 從 2022 年底一度被 Google Chromium 移除的低谷,一路走到 2026 年獲 Chrome 155 與 Firefox 158 正式採納並預設啟用的戲劇性復活,背後最關鍵的推手之一正是 Rust 語言。今天我們就從業餘攝影玩家與工具開發者的雙重視角出發,深入探討 JPEG XL 的技術特性,將它與 Ultra HDR、AVIF、HEIC、WebP 放在同一個天平上客觀比較,並看看 Rust 如何改變現代影像處理的生態。
攝影人的兩難:RAW 檔太沉重,傳統 JPEG 又太殘缺
只要玩過一陣子攝影,通常都會建立一套固定的照片歸檔流程:
「拍攝與匯入」:相機設定 RAW(或 RAW + JPEG),保留感光元件記錄下的 14-bit 完整寬容度。
「數位暗房修圖」:在 Lightroom、Capture One 或 darktable 中拉亮陰影、壓制高光、校正白平衡並調整色彩風格。
「出圖與典藏」:匯出 JPEG 用於社群分享或相簿檢視,RAW 檔則封存進 NAS 或外接冷儲存硬碟。
問題往往出在第三步。當我們在配備 Liquid Retina XDR 或 OLED 廣色域螢幕的手機與筆電上看圖時,傳統 8-bit JPEG 的致命缺點完全無所遁形:
「8-bit 色深的階調斷層」:每個色彩通道只有 256 階。當拍攝大面積平滑漸層(如無雲藍天、夕陽暮光、棚拍單色背景)時,天空會碎裂成一圈一圈難看的等高線。
「高動態範圍缺席」:現在主流手機與筆電螢幕的峰值亮度動輒衝上 1000 到 2000 nits,但標準 JPEG 規格原生為傳統 SDR(標準動態範圍)設計。即便後製時透過色調映射(tone mapping)壓縮高光以保留階調,在缺乏高動態範圍呈現能力的傳統輸出下,陽光通透感與真實輝度依舊無法被完整還原。
「高頻自然紋理遭到扼殺」:傳統 JPEG 採用固定的 8x8 離散餘弦變換(DCT),對照片中的微細毛髮、鳥類羽毛、織物纖維或底片雜訊非常不友善,容易在邊緣產生蚊狀雜訊(mosquito noise)。
「歷史典藏的儲存重擔」:累積了十幾年的數萬張旅行與生活 JPEG 照片,動輒佔據數百 GB 甚至數 TB 的空間,卻無法在不破壞畫質的前提下重新壓縮。
過渡期的聰明折衷:Ultra HDR 與我開發 TileSplit 的踩坑體悟
在進入次世代格式之前,不得不提目前在 Android 生態與 Instagram 上相當流行的折衷方案,也就是 Ultra HDR 格式(其 gain map 核心技術與中繼資料後續已進一步由 ISO 21496-1 標準化)。
傳統的純 HDR 格式(像 10-bit HEIF 或 AVIF)在舊螢幕或不支援 HDR 的軟體上打開時,常常會因為色調映射(tone mapping)不一致而顯得顏色怪異或整張過暗。Google 在 Android 14 引入的 Ultra HDR 用了一個頗聰明的雙層結構來解決向後相容問題:
「SDR 基礎主圖」:外層本質上就是一張完全合法的 8-bit 傳統 JPEG,任何老舊軟體都能正常打開顯示。
「Gain Map 增益圖」:透過 JPEG 的 MPF(Multi-Picture Format,APP2 區段)在檔案尾端封裝第二張較小的 JPEG(通常是 1/4 解析度),搭配 XMP metadata 或 ISO 21496-1 metadata(記載 hdrgm 相關增益範圍與伽瑪參數),指示系統每個區域對應的亮度增益倍率。
「動態合成」:在支援 HDR 的螢幕上,看圖軟體會把 SDR 主圖乘上 gain map 的指數增益,即時還原出耀眼的高光細節。
從 TileSplit 看雙層修補架構的先天包袱
老實說,Ultra HDR 在社群分享上真的頗好用。我平常很喜歡把 3:2 的橫幅風景照裁切並對半切成符合社群比例的直幅圖(例如切出兩張 3:4 或微幅裁切為 4:5),發到 Instagram 上做「無縫滑動(seamless carousel)」展示。但之前我發現,市面上幾乎所有切圖工具在裁切照片時,根本不知道 gain map 的存在,切完只留下 8-bit SDR 主圖,原本在螢幕上閃閃發光的 HDR 層次感瞬間消失。
為此,我動手用 Rust 寫了 tilesplit (以及可以直接在瀏覽器裡跑的 TileSplit WASM 版 ),專門在裁切圖片時同步剖析、切割並重新組裝 Ultra HDR 容器。但也正是因為親手寫過這套工具,我深刻體會到「在 30 年前的 JPEG 容器上硬塞 HDR」有多麼脆弱與繁瑣:
「雙層解析度不同步的幾何誤差」:因為 gain map 解析度通常只有主圖的 1/4,裁切時不能直接套用主圖座標,必須用無損整數除法與無條件進位(ceiling division)精確映射兩層座標,否則切出來的照片會發生高光位置偏移的靈異現象。
「二進位容器的偏移量陷阱」:MPF APP2 標頭裡的影像偏移量必須相對於內部的 TIFF header(mpf_marker_offset + 8),而不是檔案開頭。當時我使用的第三方 Rust 移植庫 jpegli-rs 的 .add_gainmap() 就把偏移量寫成絕對偏移,害我得自己手刻二進位容器組裝邏輯。
「XMP metadata 的分裂問題」:有些看圖軟體讀主圖的 XMP,有些(像 Android Google Photos 與 Chrome)則堅持從副圖 gain map 自己的 XMP 讀取,而且還挑剔屬性寫法與 rdf:Seq 標籤格式。
「指數放大的量化誤差」:在較大動態範圍增益下,gain map 像素值經過指數公式放大後,JPEG 的微小量化失真可能會在最終高光處被放大。雖然 Android 官方建議 gain map 品質可從 85–90 試起以兼顧容量,但在開發 TileSplit 時,為了確保跨裝置渲染不出現階調瑕疵,我採取了最保守的 Quality 100 高品質編碼策略。
Ultra HDR 確實漂亮地解決了當下社群平台的相容性痛點,但它終究是「用兩張 8-bit JPEG 拼湊出 HDR」的過渡橋樑——底層主圖依然受限於 8-bit 色深與傳統 DCT 架構,每次裁切重編碼也依然會有二次量化失真。
如果我們希望照片從一開始就擁有單一圖層的高色深、無須維護脆弱的 MPF 雙圖偏移量,甚至能無損收納舊有的 JPEG 檔案,那麼 JPEG XL 正是我們真正需要的次世代標準。
JPEG XL 的核心設計與攝影優勢
JPEG XL(ISO/IEC 18181)的前身融合了 Google 的 Pik 專案與 Cloudinary 的 FUIF 演算法(順帶一提,Google 後來發表的 jpegli 高品質 JPEG 編碼技術,正是從 JPEG XL 的心理視覺模型下放而來的副產品)。它從第一天起就不是某個視訊編解碼器的附屬品,而是徹頭徹尾為高階靜態攝影與專業視覺出版量身設計。
先看 codec 的整體資料流:主影像可依需求選擇 VarDCT 或 Modular;即使使用 VarDCT,低頻影像與量化權重等輔助資料仍會透過 Modular 子位元流編碼。既有 JPEG 則可直接沿用 DCT 係數,搭配額外的重建資料保存原始檔案的位元精確還原能力。
圖:JPEG XL codec 高階架構,部分細節省略。圖中容器為選用,也可直接儲存裸 codestream。改繪自 Jon Sneyers 的 JPEG XL codec architecture ,採 CC BY-SA 4.0 授權;調整了繁體中文標示、版面與流程說明。技術細節可參考 libjxl 格式概覽 。
100% 無失真重壓既有 JPEG:平白省下 20% 儲存空間
如果只能挑一個讓攝影玩家最心動的功能,那絕對是 JPEG XL 的無失真 JPEG 重新封裝(Lossless JPEG Recompression)。
前面在寫 tilesplit 時我就提過,傳統 JPEG 只要經過「解碼成像素 ➔ 重新編碼」,就一定會因為 DCT 量化而產生二次失真(generation loss)。但 JPEG XL 走了一條完全不同的路:
它的 VarDCT 模式可以直接讀取傳統 JPEG 檔案中的 DCT 係數。
它不需要將像素解碼重算,而是直接將原始 JPEG 的量化係數與中繼資料(包含 EXIF、XMP 與嵌入的二進位區段,包含未解析的 MPF gain map 資料)透過更高壓縮率的非對稱數值系統(ANS,Asymmetric Numeral Systems)熵編碼重新封裝成 .jxl 容器。需要注意的是,這類直接無損轉碼會完整封存原始檔案位元,但傳統以 MPF 封裝的 gain map 仍需支援該專屬結構的解碼器才能在 JXL 容器中作為 HDR 解析。
轉碼完成後,檔案體積典型縮小約 16% 至 22%(平均約兩成)。
最實用的是,這個過程具備 bit-exact 的完全可逆性!只要你需要,隨時能把 .jxl 還原回與原始檔案完全相同的 JPEG 位元資料(SHA-256 校驗碼完全一致)。
這意味著你可以將硬碟裡的歷史照片整批進行約兩成的無損空間精簡,不損失任何一個像素,同時保留原始檔案的完美復原能力。
VarDCT 與攝影顆粒保真:細緻保留高頻質感
許多攝影同好嘗試用 AVIF 壓縮靜態照片時,常注意到微細紋理在特定設定下容易被平滑化。這是因為 AV1 視訊編解碼器最初專為連續畫面設計,其內建的去區塊濾波器(deblocking filter)在特定 preset 或中低位元率下,傾向將高頻雜訊視為可精簡的失真而進行平滑。在動態影片中這能大幅提升主觀觀感,但在保留底片微粒(film grain)、精緻織物或羽毛紋理的靜態攝影中,若未細緻調校 encoder 參數,常需要更高的編碼位元率或特定參數才能保留自然顆粒。
JPEG XL 的 VarDCT(Variable-block size DCT)專門針對靜態影像特性設計:
支援從 4x8、8x4、8x8、16x16 到 32x32(規格甚至涵蓋至 256x256)以及 AFV 非對稱角頻率變換區塊。
導入了以人類視覺感知模型為基礎的 Butteraugli 距離度量,深度模擬人眼視網膜對高頻訊號與色彩敏感度的生理反應。
在高保真度(high fidelity)下,它能夠精準分配位元給高頻紋理,保留自然的攝影顆粒,而不依賴強制濾波平滑畫面。
原生單層 32-bit 浮點色深與 HDR 廣色域
不同於 Ultra HDR 必須靠「8-bit 主圖 + 8-bit 增益圖」雙層拼裝,JPEG XL 原生單一圖層就支援高達 32-bit 浮點色彩深度:
完整支援 Rec.2100、PQ(Perceptual Quantizer)與 HLG(Hybrid Log-Gamma)等 HDR 標準傳輸曲線,並原生涵蓋 Display P3、Adobe RGB 與 Rec.2020 廣色域。
裁切或縮放照片時,只需要單純操作單一高色深圖層,再也不必擔心主圖與 gain map 座標對不準或 MPF 指標算錯。
同時,如果攝影師希望兼顧 SDR 螢幕顯示並精確掌控色調,JPEG XL 容器標準亦定義了專屬的 jhgm box(包含 ISO 21496-1 gain map metadata、替代色彩資訊與獨立 JXL 編碼流),使單一檔案也能靈活支援雙層動態範圍增益機制。
Modular 模式與響應式漸進載入
除了專為相片打造的 VarDCT,JPEG XL 還內建了 Modular 模式,專門用於完全無失真(Lossless)壓縮、透明度(Alpha)、景深地圖(Depth Map)與局部遮罩。
此外,它的響應式漸進載入(Responsive Progressive Decoding)經過現代化設計:根據 Mozilla 的實測展示,檔案僅需載入約 15% 的資料量即可清楚辨識畫面主體與構圖,載入約 40% 至 50% 時就能呈現接近完整的視覺解析度,相較於常見預設未啟用分層漸進的 AVIF 檔案在一般網頁載入時的體驗,JPEG XL 的漸進解碼對大圖展示非常友善。
五大影像格式全方位比較
為了讓大家在整理與發布照片時有清晰的判斷依據,我們將 JPEG XL、Ultra HDR、AVIF、HEIF 與 WebP 這五大格式拉到業餘攝影的實戰天平上逐項檢視:
評比維度
JPEG XL (.jxl)
Ultra HDR (.jpg)
AVIF (.avif)
HEIC (HEVC 編碼之 HEIF, .heic)
WebP (.webp)
技術根源
專為靜態相片設計 (Pik + FUIF)
傳統 JPEG + MPF gain map 雙層容器
AV1 視訊畫格衍生
HEVC (H.265) 視訊衍生
VP8 視訊衍生
最高色深
高達 32-bit 浮點 (原生單層)
8-bit SDR 主圖 + 8-bit 增益圖
基本 10/12-bit (可透過 transforms 擴展)
10-bit / 12-bit 整數
8-bit (lossless 支援 4:4:4 與 ICC 廣色域)
HDR 實作方式
原生單層 HDR (亦支援 jhgm gain map)
雙層合成 (向後相容性極佳)
原生單層 HDR (PQ / HLG)
原生單層 / 雙層 HDR
無原生 HDR 支援
微細紋理與顆粒
極佳 (VarDCT 人眼感知調校)
普通 (受限傳統 JPEG 8x8 DCT)
需依 encoder 與 preset 細調,否則中低位元率易被平滑
良好 (依硬體/軟體編碼實作而定)
普通 (有失真模式下邊緣區塊效應較明顯)
無損重壓既有 JPEG
支援 (位元精確還原,體積省約 16%–22%)
不支援 (但是本身即為合法 JPEG)
不支援 (解碼為像素後可無損保存,但無法位元精確還原原始 JPEG)
不支援 (解碼為像素後可無損保存,但無法位元精確還原原始 JPEG)
不支援 (解碼為像素後可無損保存,但無法位元精確還原原始 JPEG)
裁切與後製友善度
極高 (單層結構無偏移問題)
低 (一般工具會洗掉 gain map)
高 (單層結構)
中 (視容器封裝而定)
高 (但僅 8-bit)
批次編碼速度
快 (多核心高度平行最佳化)
快 (等同編碼兩張 JPEG)
較慢 (依 encoder、CPU 執行緒與品質 preset 差異極大)
快 (多數平台具硬體編碼加速)
快 (成熟成熟演算法)
專利與授權狀態
免權利金 (Royalty-Free)
免權利金 (ISO 21496-1)
免權利金 (Royalty-Free)
專利池授權複雜 (MPEG LA / HEVC Advance)
免權利金
最佳攝影應用場景
長期典藏、高傳真作品集、舊圖瘦身
Instagram / Threads 社群分享
低頻寬網頁縮圖
手機與相機機身直出暫存
舊裝置網頁相容後備
為什麼 AVIF 與 HEIC 在攝影工作流中各有取捨?
「AVIF 的特質與門檻」:在極低位元率的網頁縮圖上,AVIF 壓縮率相當亮眼。但對高傳真攝影來說,AVIF 在中高畫質設定下若未細緻調整參數,容易平滑微細紋理;此外其軟體編碼器(如 rav1e、libaom)在高畫質多執行緒編碼時相當耗費 CPU 資源,修完數百張照片批次匯出時需花費較長的運算時間。
「HEIC 的授權複雜度」:許多主流相機與智慧型手機雖然都支援拍攝 10-bit HEIC,但因為背後的 HEVC 專利池(MPEG LA、HEVC Advance 等)授權結構複雜,導致非特定生態系的瀏覽器與作業系統支援受限,在跨平台展示與開源工具鏈整合上門檻較高。
為什麼 Rust 是促成 JPEG XL 逆轉與現代工具鏈的關鍵
如果你在 2023 或 2024 年關注過 JPEG XL,一定記得當時令人扼腕的僵局:Google Chromium 團隊在 2022 年底的 M110 版本中,突然移除了實驗性的 JPEG XL 支援;而 Mozilla Firefox 雖然保留了實驗旗標,卻也遲遲不願預設開啟。
為什麼技術指標領先的 JPEG XL,當時在瀏覽器整合上遇到阻礙?2022 年底 Chromium 團隊在評估時,考量了生態系採用度、新增編解碼器的維護成本與既有格式的邊際效益而暫緩;而 Mozilla 則對引入龐大 C/C++ 程式碼至安全敏感的解碼管線抱持審慎態度。要在維持極致效能的同時大幅降低資安防護成本,關鍵鑰匙正是 Rust 語言。
影像解碼管線的資安考量與系統挑戰
從系統軟體工程的角度來看,瀏覽器裡的影像解碼器是極為關鍵的攻擊表面(attack surface):
「直接暴露於不可信輸入」:瀏覽器的渲染行程必須下載並解析完全不可信的外部二進位圖片位元流。
「記憶體安全風險」:傳統參考解碼器 libjxl 是由約 10 萬行高度複雜的多執行緒 C++ 程式碼寫成。解析惡意構造的標頭時,若發生整數溢位或邊界檢查疏漏,即使渲染行程具備沙盒隔離,仍可能成為漏洞利用鏈的第一步。
最顯著的案例之一就是 2023 年的 CVE-2023-4863 漏洞(WebP 堆積緩衝區溢位),凸顯了在複雜二進位多媒體解碼中維持安全性的難度。
這一點我自己開發 tilesplit 時也深有體會:
在 CLI 版本中,我使用了封裝 Google C++ libultrahdr 的 ultrahdr-rs 以及 jpegli-rs。遇到某些格式異常的 JPEG 時,解碼過程可能會觸發 Rust 的 panic。為了防範工具中斷,我不得不使用 catch_unwind 來捕捉 unwinding panic(需要說明的是,catch_unwind 僅能捕捉 Rust 的 panic,無法攔截一般 C/C++ 的未定義行為或程式異常終止)。
更麻煩的是,當我準備開發 TileSplit WASM 版 把切圖工具搬進瀏覽器時,依賴 C/C++ FFI 的 ultrahdr-rs 在 wasm32-unknown-unknown 目標下遇到編譯瓶頸。最後我是換成純 Rust 實作的 image 與 ultrahdr-core crate,才順利做到零外部 C 依賴、安全編譯進瀏覽器直接執行。
主流瀏覽器團隊為此推行了嚴謹的安全邊界策略(例如 Chromium 的「Rule of Two」原則:在不可信任輸入、無沙盒環境與非記憶體安全語言三者中,最多只能同時存在兩者)。這使得以純記憶體安全語言重新實作新解碼器,成為進入主流瀏覽器的最高優先路徑。
Mozilla 的「Rust 挑戰」與 Google Research 的 jxl-rs
正如 Mozilla 在 Intent to Ship: JPEG XL 中所回顧的,當時 Mozilla 團隊直接向 Google Research 的 JPEG XL 團隊拋出了一道明確的戰帖:
“So, we laid down a challenge to the JPEG XL team at Google Research: Build a safe, performant, compact, and compatible JPEG XL decoder in Rust, and we’ll ship it.”
(我們向 Google Research 的 JPEG XL 團隊下了一道戰帖:只要用 Rust 打造出一個安全、高效、精巧且相容的 JPEG XL 解碼器,我們就正式出貨。)
與此同時,Apple 在 2023 年 9 月透過 Safari 17(涵蓋 iOS 17 以及 macOS Sonoma、Ventura、Monterey)率先支援了 JPEG XL(儘管初期屬於部分實作,尚未包含漸進式渲染),加上 PDF 國際標準組織將 JPEG XL 列為 HDR PDF 的首選影像格式,開發者與攝影社群要求恢復支援的呼聲達到頂峰。
為了回應 Mozilla 的戰帖與瀏覽器資安團隊的要求,Google Research 的 JPEG XL 團隊親自出手,啟動了官方純 Rust 解碼器專案:jxl-rs 。
「大幅降低記憶體安全漏洞風險」:jxl-rs 以 Rust 重新打造符合標準規範的 JPEG XL 解碼核心,從語言層級避免緩衝區溢位、野指標與資料競爭等常見漏洞,並透過強大的型別系統提高解碼器容錯能力。
「高度向量化效能」:針對現代瀏覽器環境高度最佳化,兼顧二進位體積與 SIMD 多核心解碼吞吐量。
三大主流瀏覽器引擎全數到位
隨著 jxl-rs 的成熟,瀏覽器版圖在 2025 年底至 2026 年迎來了戲劇性的翻盤:
「Chromium 與 Chrome 155 回歸」:Chrome 團隊於 2025 年底正式接納以記憶體安全為前提的回歸提案,改為直接整合純 Rust 的 jxl-rs 解碼器,並在 2026 年 10 月釋出的 Chrome 155 中正式重返舞台。
「Firefox 158 兌現承諾」:Mozilla 將 jxl-rs 整合進 Firefox 引擎中,並特別針對漸進式渲染深度調校,於 Firefox 158 正式預設啟用靜態圖片解碼(HDR 相關顯示管線項目亦在持續完善中)。
「Safari 持續支援」:加上早已在 iOS、iPadOS 與 macOS 原生支援的 Apple 生態系,三大主流瀏覽器引擎已全數支援 JPEG XL 基本解碼!
這不僅是 JPEG XL 的勝利,更是系統程式設計史上的一座里程碑,證明 Rust 已經正式成為現代網路多媒體標準能否進入主流瀏覽器的安全守門人。
蓬勃發展的純 Rust 社群影像生態:jxl-oxide 與 zune-image
除了瀏覽器內建的官方 jxl-rs 之外,如果你也像我一樣喜歡用 Rust 寫點影像處理小工具,開源 Rust 社群也建立起相當豐富的純 Rust 原生工具鏈:
jxl-oxide :由社群獨立開發的純 Rust JPEG XL 解碼器,完整支援 VarDCT、Modular 與色彩管理(可選用純 Rust 的 moxcms 或 C 依賴的 lcms2),並與 Rust 最核心的 image-rs 生態系深度整合,編譯成 WASM 放上網頁也十分順暢。
zune-image :專注於極致速度的純 Rust 影像處理管線,旗下多個主流編解碼器善用 AVX2 與 ARM NEON 向量指令集,並包含輕量級的 zune-jpegxl 無損 Modular 編碼實作。
需要指出的是,雖然 jxl-rs 是由 JPEG XL 官方專案主導的 Rust 解碼器,但 ISO 標準的官方參考軟體(reference software)仍為 C++ 的 libjxl。不過在個人伺服器、相片管理平台或本地看圖工具等場景中,純 Rust 函式庫能有效防範記憶體損毀漏洞,在維持多核心高效能的同時帶來極佳的系統穩定性。
給業餘攝影玩家的實用工作流程建議
站在 2026 年的時間點,面對 RAW、JPEG XL 與 Ultra HDR,我們在日常拍照與分享時該怎麼搭配最聰明?以下是我目前自己的實戰配置:
原始底片:RAW 檔維持 3-2-1 備份
無論壓縮技術如何進步,千萬不要因為新格式的出現而刪除重要的 RAW 原檔。RAW 檔是感光元件未經烹調的完整紀錄,建議對精選後的 RAW 原檔維持嚴謹的 3-2-1 備份原則(本機工作碟、NAS 陣列、異地冷備份)。
歷史相簿瘦身:用 JPEG XL 無失真重壓精簡儲存空間
如果你硬碟或 NAS 裡躺著過去累積的傳統 JPEG 相片(早期數位相機照片、親友分享照片、或是過去匯出的 JPEG 成品):
使用官方 cjxl 工具執行無失真重壓縮(對 JPEG 輸入而言,無損重壓本來就是 cjxl 的預設行為,加上 --lossless_jpeg=1 則是明確宣示意圖):
# 將傳統 JPEG 無損轉為 JXL,體積典型可節省約 16%–22%
cjxl legacy-photo.jpg archived-photo.jxl --lossless_jpeg= 1
# 若未來需要原始檔案,可 100% 位元精確還原回原本的 JPEG
djxl archived-photo.jxl restored-photo.jpg
建議在批次處理後,透過比對還原前後檔案的 SHA-256 校驗碼確認一致,並確認備份妥當後再行清理來源檔案。
社群平台分享(Instagram / Threads):出圖選 Ultra HDR + TileSplit
如果你的目標是把修好的風景或街拍照發上 Instagram,享受動態牆上閃耀的 HDR 視覺衝擊力:
在 Lightroom 開啟 HDR 編輯模式,匯出時選擇 JPEG 並勾選「包含 HDR 增益圖」(Ultra HDR)。
如果你想把 3:2 橫幅大景切成無縫滑動的直幅多圖貼文,千萬別用一般不認識 Gain Map 的裁切軟體;可以直接使用我寫的 tilesplit CLI 或直接打開線上版 TileSplit WASM 網頁工具 拖拉切圖,把每一格切片的 Ultra HDR 亮度資訊完整保留下來。
個人作品集與長期成品典藏:採用 16-bit HDR JPEG XL
如果是要放進個人作品集網站、部落格藝廊,或是留作高畫質成品典藏:
在 Lightroom 或 darktable 匯出時選擇 JPEG XL 格式(.jxl),設定 16-bit 色深與 Display P3 / Rec.2020 廣色域(需搭配 HDR 輸出特性設定與支援螢幕以呈現高動態範圍)。
隨著 Safari、Chrome 155 與 Firefox 158(透過 jxl-rs)全數到位,網頁端可以透過 <picture> 標籤嘗試優先提供 .jxl,並以 Ultra HDR .jpg 作為向後相容的備案:
<picture >
<!-- 支援 JPEG XL 的瀏覽器 (Safari / Chrome 155+ / Firefox 158+) -->
<source srcset = "landscape-hdr.jxl" type = "image/jxl" >
<!-- 向後相容備案:支援 Ultra HDR 的環境顯示 gain map HDR,舊設備顯示 SDR -->
<img src = "landscape-ultrahdr.jpg" alt = "合歡山日出雲海" loading = "lazy" >
</picture >
需要說明的是,<source type="image/jxl"> 僅會依瀏覽器是否具備 JXL 格式解碼能力來挑選,無法自動偵測螢幕與瀏覽器是否已完整支援該檔案的 HDR 顯示管線(例如部分環境可能先以 SDR 渲染 JXL,而不會跳過它去讀取 fallback 的 Ultra HDR)。在現階段實務應用中,若最看重廣泛設備上的 HDR 亮度衝擊力,Ultra HDR 仍是相容性最高的選擇;而 JPEG XL 則是畫質與典藏的理想基石。
結語
科技的演進,最終都是為了忠實傳遞創作者眼中的感動。
身為業餘攝影愛好者,我們頂著清晨的寒風在山上等待第一道曙光,或是走在異國街頭耐心捕捉光影交錯的瞬間,無非是希望將按下快門那一刻的色彩、空氣感與光線張力完整保存下來。從我們為了發 Instagram 而折騰 Ultra HDR 的雙層 Gain Map 容器,到見證 JPEG XL 憑藉原生高色深、自然顆粒保真與無損轉碼成為真正的次世代主力,這段演進過程實在相當迷人。
更令人欣慰的是,當 JPEG XL 一度因為傳統 C++ 解碼器的資安包袱而在瀏覽器戰場受挫時,是 Rust 語言與 jxl-rs 挺身而出,以記憶體安全為基石打破了僵局,讓攝影愛好者與系統開發者同時迎來了最好的時代。下次整理完記憶卡裡的照片,不妨親自試試這套結合 JPEG XL 與 Ultra HDR 的現代工作流吧 :-)
Berkeley Mono 與 Ioskeley Mono
為 IDE 與終端機打造的等寬字體美學與線上互動對照
每天盯著編輯器與 terminal 超過八小時,字體幾乎是開發者最貼身的視覺介面。回顧過去幾年,我的主力字體一路從微軟經典的 Consolas 、支援連字與手寫斜體的 Cascadia Code 、字腔寬裕且閱讀極為舒適的商業字體 MonoLisa ,一路換到 GitHub Next 主打 texture healing 動態字距與五大超家族的 Monaspace ;這些字體都在不同時期陪伴我寫下無數行程式碼,但當工作流越來越依賴高密度的終端機與多視窗並排時,心裡總覺得想找一款更貼近老派工程手冊、俐落緊湊且毫無拖泥帶水的機械秩序感字體。
直到遇見 U.S. Graphics Company 推出的商業字體 Berkeley Mono (TX-02) ,以及開源社群以此為靈感、基於 Iosevka 參數化引擎打造的 Ioskeley Mono ,那種源自 1970 年代技術手冊與資料終端機的字形比例,才真正讓程式碼與 TUI 介面同時安靜下來。
這篇文章除了整理這兩款字體的設計脈絡、字形解剖差異,以及在 IDE 與 Linux terminal(包含 Omarchy 環境)的最佳實務配置外,我也直接在文內嵌入了一套雙模式線上互動字體實驗室 :你可以直接拖曳捲簾滑桿或原位閃切,對照 Berkeley Mono (TX-02) 與 Ioskeley Mono 的像素級差異,也能即時調整字級、行高、字寬與 OpenType 連字特徵,親手測試它們在程式碼與 terminal 框線下的實際表現。
為什麼開發者開始迷戀 1970 年代技術手冊風格?
過去我非常喜歡的幾款字體各有千秋:Consolas 在 Windows ClearType 時代奠定了極高的小字級易讀性;Cascadia Code 為現代終端機帶來活潑的連字;MonoLisa 擁有稍微加寬的字腔與極佳的長時間閱讀舒適度;而 Monaspace 則透過局部的字寬補償改善了等寬字體的疏密不均。然而當我們的工作流轉向多重 split pane、在左半邊開著編輯器、右半邊同時跑著多個 Ghostty 終端機與 TUI 工具(如 herdr、lazygit、btop 或 AI coding agent 的多工視窗)時,偏寬或偏柔和的字體往往會遇到三個取捨:
水平字距偏寬,分割視窗容納量受限 :在 14 吋或 16 吋筆電螢幕上一旦切出左右兩個 80 列視窗,常見等寬字體很容易被迫折行。
字形情緒過於圓潤,大量符號堆疊時缺乏邊界感 :當畫面充滿 Rust 的泛型與生命週期標記(<'a, T: Into<Cow<'a, str>>>)或巢狀括號時,過於柔和的弧線反而降低了視覺錨點。
終端機框線與文字基線的微妙脫節 :部分針對 IDE 設計的字體在開啟連字後,遇到 terminal 內的 box-drawing 格線與資訊密度極高的 log 輸出時,整體節奏容易顯得鬆散。
Berkeley Mono (TX-02):源自冷戰時期工程圖紙的秩序感
由設計師 Neil Panchal 與 U.S. Graphics Company 主導設計的 Berkeley Mono (TX-02) ,最初是為了自家 CAD 與工程軟體介面量身打造的專屬字體。它的設計哲學非常明確:回到 1970 至 1980 年代的微處理器資料手冊(databooks)、航太儀表與早期 CRT 終端機,追求高辨識度、方正的幾何轉角,以及長時間閱讀不疲勞的灰度節奏。
Berkeley Mono 最吸引人的特質在於它「收斂卻不擁擠」的字腔結構:
方中帶剛的幾何輪廓 :字母的轉角帶有微妙的平切弧度(superellipse),既不像純點陣字體那樣生硬,也沒有多餘的裝飾襯線。
零歧義的字元家族 :針對程式設計最容易混淆的 0 / O、1 / I / l 做了極為鮮明的結構區隔。
緊湊的水平節奏 :在維持充裕字懷(counter)的前提下微調字元推進寬度,讓每一行程式碼像排好隊的積體電路針腳一樣整齊。
不過,Berkeley Mono 是一款採用商業授權的閉源字體(個人開發者授權 FX-102 定價為 $75 美元;官方另有提供代號 FX-050 7 天免費評估試用版 可供下載測試,但 EULA 授權條款禁止將字型檔二次散佈或直接託管於公開網站)。如果你想在多台 Linux 工作站、容器環境、公開的 dotfiles 儲存庫或網頁專案中自由封裝使用,商業授權條款自然有其邊界。這也促成了開源社群最令人驚豔的一次致敬行動。
Ioskeley Mono:站在 Iosevka 肩膀上的開源致敬之作
Ioskeley Mono 是由開發者 Ahmed Hatem(ahatem/IoskeleyMono )發起、採用 SIL Open Font License 1.1 (OFL) 開源授權釋出的等寬字體專案。
它並沒有從零手繪每一筆貝茲曲線,而是巧妙運用了開源字體界最負盛名的參數化造字引擎 Iosevka v34.4.0。透過一份精心調校的 private-build-plans.toml 建置規格檔,Ioskeley Mono 在 Iosevka 數百種字形變體(glyph variants)與度量參數中,逐一挑選出最貼近 Berkeley Mono (TX-02) 視覺精神的組合,並編譯出完整的字體家族。
這套建置管線帶來了幾個非常務實的工程優勢:
完整的字重與字寬矩陣 :原生提供從 Thin (100) 到 Black (900)(額外包含 SemiLight 350)共 10 種字重 ,並同時提供標準寬度 Normal (100%) 與微縮寬度 SemiCondensed (90%) 兩種版本。
針對應用場景拆分的專屬變體 :除了支援連字的標準版 Ioskeley Mono 與無連字版 Ioskeley Mono NL,還專門提供針對 terminal 單格對齊最佳化的 Ioskeley Mono Term ,以及預先打好圖示補丁的 Nerd Font 版本。
完全自由的開源授權 :SIL OFL 1.1 允許你自由安裝在任何機器、打包進 Linux 系統映像檔,甚至直接子集化為 .woff2 嵌入個人技術部落格中。
線上雙模式互動字體對照實驗室
講再多字形理論,都不如直接把兩款字體放在同一塊畫布上親眼比對。由於 Berkeley Mono 屬於商業字體,我們無法直接將其原始字型檔散佈於公開網頁;因此我將下方的互動實驗室設計為兩個互補的模式:
即時互動編輯與終端機實驗室 :直接載入自託管的 Ioskeley Mono(含 Regular、SemiCondensed 90% 與 Italic)、Monaspace Neon 與 Cascadia Code / Mono 網頁字型。為了遵守 U.S. Graphics Company 的 EULA 條款(禁止公開散佈字型檔),我們不將商業版或 FX-050 免費試用版 字型打包進 GitHub 儲存庫;但只要你本機已安裝 Berkeley Mono / Berkeley Mono Trial,或點擊實驗室右上角的 「載入本機 FX-050 字型檔」 按鈕選取從官網免費下載 的 .ttf / .otf / .woff2 檔案(100% 於瀏覽器記憶體內透過 FontFace API 渲染,不經過任何伺服器上傳),就能直接在左右面板即時對照!
像素級對照儀 :將 Berkeley Mono (TX-02) 與 Ioskeley Mono 在完全相同字級(14.2px)、行高(1.54)與推進寬度下渲染的樣本進行零誤差像素對齊。你可以使用 捲簾滑桿 (Curtain Slider)左右拖曳,或點擊 原位閃切 (Blink Comparator)在同一座標瞬間切換,觀察兩者在字腔、筆畫粗細與細節上的異同。
即時互動編輯與終端機實驗室 (Live Webfont)
Berkeley Mono (TX-02) × Ioskeley Mono 像素級對照儀
左側:Ioskeley Mono
右側:Berkeley Mono (TX-02)
IOSKELEY MONO (OPEN SOURCE)
BERKELEY MONO (TX-02)
⇄
實戰程式碼像素對照: 兩款字體在完全相同的字級(14.2px)與字元格線上渲染。拖曳滑桿或切換至「原位一鍵閃切」,可以發現兩者的整體行距與字元落點完全吻合;細看則能察覺 Berkeley Mono (TX-02) 的筆畫略微厚實溫潤,而 Ioskeley Mono 的轉角與小寫 r、g、@ 則展現出更鮮明的幾何線條。
字形解剖:Ioskeley Mono 如何重現經典並走出自己的路?
如果你在上方實驗室切換到「Berkeley Mono (TX-02) × Ioskeley Mono 像素級對照儀」的第三個場景(128px 像素疊影解剖),會看到一個非常有趣的現象:兩款字體在單字元推進寬度(advance width)上達成了 +0.00 px 的零誤差同步,但在個別字元的局部筆畫上,因為 Iosevka 參數化骨架與手工繪製曲線的本質差異,展現了耐人尋味的細節對比。
招牌識別字元:點狀 0、單層 g、Q 與 @
我們可以從 ahatem/IoskeleyMono 的建置設定檔 private-build-plans.toml 中,看出作者是如何一磚一瓦挑選字形零件的:
[buildPlans .IoskeleyMono .variants .design ]
zero = "dotted" # 預設採用中心點狀 0(Iosevka 另自動編譯 GSUB zero 斜線零特徵)
one = "no-base"
two = "straight-neck-serifless"
three = "flat-top-serifless"
six = "open-contour" # 6 與 9 採用開放式弧線缺口
eight = "two-circles" # 由上下兩個圓環組成的 8
nine = "open-contour"
capital-q = "crossing" # Q 的斜向尾筆俐落切過底部邊界
a = "double-storey-serifless" # 雙層無襯線 a
g = "single-storey-serifless" # 單層無襯線 g
k = "symmetric-touching-serifless"
r = "hookless-serifless" # 無鉤無襯線 r
y = "straight-serifless"
tittle = "square" # i 與 j 頂部圓點改為方點
punctuation-dot = "square" # 句號、冒號與分號全面採用方點
paren = "flat-arc" # 近似垂直卻帶微弧的平弧括號 ()
brace = "curly-flat-boundary"
number-sign = "slanted"
at = "fourfold" # 高挑緊湊的四折 @ 符號
dollar = "through" # 豎線貫穿上下的 $ 符號
透過下方的像素疊影與對照表(圖中 象牙白 為共同骨架、琥珀金 為 Ioskeley Mono、珊瑚橘 為 Berkeley Mono TX-02),我們可以更清楚地歸納兩者在關鍵字形上的異同:
字元類別
像素疊影與視覺對照
Berkeley Mono (TX-02) 細節特徵
Ioskeley Mono 細節特徵
數字 0 方圓外框+中心點
中心點略呈圓潤橢圓,外框轉角較柔和
設定 zero = "dotted" 呈現俐落方點,且保留 Iosevka 內建的 OpenType zero 特徵可隨時切換為斜線 0
小寫 a 與 g 雙層 a・單層 g
a 的肚圍較貼近左緣;g 的下伸迴圈弧度較早收尾
採用 double-storey-serifless 與 single-storey-serifless,筆畫轉折更偏幾何直線
大寫 Q 矩形圓角+斜向尾筆
尾筆從字腔內部偏左處起筆向右下延伸
採用 capital-q = "crossing",尾筆切入角度略高且向外探出更鮮明
符號 @ 與 $ 高挑比例・貫穿 $
@ 內圈 a 較窄且外框底部收平
採用 at = "fourfold" 與 dollar = "through",內圈字懷更寬敞易讀
括號 () 與標點 平弧括號・方形句點
括號中段近似垂直,在巢狀呼叫時宛如垂直分隔線
透過 paren = "flat-arc"、tittle = "square" 與 punctuation-dot = "square" 忠實還原相同視覺節奏
為什麼點狀零(Dotted Zero)與平弧括號在程式碼中特別耐看?
許多程式字體為了區分 0 與 O,習慣使用斜線零(Slashed Zero)。然而當程式碼中出現大量十六進位常數(如 0x00FF_0000)或浮點數陣列時,密集的對角斜線容易與大寫 Ø、8 或斜線運算子 / 產生視覺干擾。
Berkeley Mono 與 Ioskeley Mono 預設採用的 點狀零 (Dotted Zero),將識別錨點收斂在字腔正中央的一顆方點上,既能瞬間與內部全空的字母 O 區隔,又不會破壞一整排零的水平安定感。而如果你在特定環境下還是偏愛斜線零,Ioskeley Mono 也貼心地把斜線零封裝在 OpenType 的 zero 特徵槽位中——不需要換字體檔,只要在編輯器或 terminal 設定裡加上一行 zero feature 就能隨時切換。
另一個令人一用就回不去的細節是 平弧括號 (flat-arc parentheses)。當我們在寫 Lisp、Rust 巨集或深層函式呼叫 foo(bar(baz(x))) 時,傳統圓弧括號 ((( 會形成一團向外膨脹的視覺噪音;而平弧括號把中段拉直、僅在上下兩端保留俐落折角,讓連續括號排在一起時就像一排整齊的柵欄,大幅降低了配對括號時的眼球掃描負擔。
如何挑選適合你的 Ioskeley Mono 變體?
第一次點開 Ioskeley Mono 的 GitHub Releases 頁面時,看到多達二十幾個壓縮檔可能會稍微猶豫一下。其實只要順著「使用場景(IDE vs. Terminal)」、「字元寬度(100% vs. 90%)」與「螢幕解析度(Hinted vs. Unhinted)」三個維度,就能在十秒內鎖定最適合的安裝包:
變體家族:Ioskeley Mono vs. NL vs. Term
標準版 Ioskeley Mono (推薦用於 IDE):
內建完整的 Iosevka 程式連字規則(contextual ligatures),包含常見的 !=、===、<=、>=、=>、->、:: 與 <=>。適合在 VS Code、Zed 或 JetBrains IDE 中做為主力字體。
無連字版 Ioskeley Mono NL (No-Ligature):
如果你是「所見即所得」的純粹主義者,不希望兩個獨立的 ASCII 字元被合併成單一圖形,選 NL 版本即可在字型層級徹底關閉連字。
終端機專屬版 Ioskeley Mono Term :
這是專為 terminal emulator 打造的變體。在終端機環境中,部分連字或特殊符號如果跨出單一字元格(character cell),可能會導致游標定位偏移或 TUI 表格錯位。Term 版本透過 spacing = "term" 嚴格鎖定所有符號與框線的格線邊界。
該選預先整合的 Nerd Font,還是原版字體搭配終端機內建圖示?
在 GitHub Releases 頁面上,每個家族除了原版外,都還有一份檔案高達 80 MB 以上的 -NerdFont.zip 版本。過去許多開發者習慣無腦下載預先打好補丁的 IoskeleyMonoTerm Nerd Font Mono,但在 Ghostty 、WezTerm 或 Kitty 等現代終端機環境下,把 Nerd Font 圖示直接焊進字型檔裡其實有四個明顯的缺點 :
會屏蔽 Ghostty 內建的智慧圖示縮放引擎(Built-in Nerd Fonts) :
像 Ghostty 這類現代終端機已經在底層原生內建了完整的 Symbols Nerd Font 作為預設 fallback,並且實作了極為聰明的動態字元格縮放邏輯(constraint-based glyph scaling) ——當終端機遇到 Private Use Area (PUA) 區段的圖示碼位、且右側緊鄰空白字元時(例如 eza -l、lazygit、yazi 檔案樹或 starship 提示字元),Ghostty 會自動允許圖示以更清晰飽滿的比例渲染;若右側緊鄰一般文字,則自動收斂回單一字元格寬度。
然而,一旦你安裝並指定了預先打上 --mono 補丁的 IoskeleyMonoTerm Nerd Font Mono,因為主字型檔本身已經佔據了所有 PUA 碼位,Ghostty 根本不會觸發內建的圖示 fallback 與動態縮放機制 ,而是直接畫出字型檔裡被 FontForge 硬生生壓縮塞進窄小單格(1-cell)的微型圖示,反而讓資料夾、Git 分支與語言圖示縮得又小又難辨識。
FontForge 二次轉檔會破壞原生 Hinting 與 OpenType 表格 :
Iosevka 編譯器透過 ot-builder 與 ttfautohint 產出的原版字型,同時提供了精心微調的 Hinted/ 與保留原始曲線的 Unhinted/ 兩種版本。但當 CI 管線使用 FontForge 執行 font-patcher 注入圖示並重新匯出 .ttf 時,原有的 hinting 結構會被重新改寫(這也是為什麼官方 README 特別註明 Nerd Font 安裝包無法區分 Hinted 與 Unhinted),甚至在轉檔時還會發生清除 post.isFixedPitch 等寬旗標的副作用。
40 倍的圖示重複塞入,造成字型檔體積暴增 :
Nerd Fonts 完整圖示集包含超過一萬個向量圖示。由於圖示本身並沒有「極細體 Thin」或「極粗斜體 Black Italic」的差別,將圖示直接打進字型檔,等於在 10 種字重 × 2 種字寬 × 正斜體共 40 個 .ttf 檔案裡重複複製了一萬個完全相同的圖示 ,導致單一壓縮包膨脹到近 88 MB ,不僅佔用磁碟與字型快取記憶體,也拖慢應用程式啟動時的字型掃描速度。
字體與圖示版本強耦合,維護成本高 :
當未來 Nerd Fonts 釋出新版圖示,或是你想自行微調 Ioskeley Mono 的字形參數重新編譯時,預先打補丁的做法必須重新用 FontForge 跑完 40 個檔案的修補流程;反之,若保持字體純淨,圖示與文字就能徹底解耦。
因此,實務上的最佳選擇策略非常明確:
如果你使用 Ghostty(或 WezTerm 等內建 Nerd Font 的現代終端機) :強烈建議直接安裝未打補丁的原版 Ioskeley Mono Term ,把圖示渲染交給 Ghostty 原生的內建引擎處理,既能保留最純淨的 Hinted / Unhinted 字體品質,又能享受最清晰的圖示比例。
如果你使用的終端機或狀態列工具(如 Alacritty、Foot、Waybar)沒有內建圖示 :比起安裝 40 個預先修補的龐大字型檔,更推薦在系統層級單獨安裝一個僅約 2.5 MB 的符號專用字型(如 Arch Linux 的 ttf-nerd-fonts-symbols-mono),讓 Fontconfig 自動透過 fallback 機制補齊圖示;只有在極少數不支援字型 fallback 的舊型軟體中,才需要考慮安裝預先整合的 IoskeleyMonoTerm Nerd Font Mono。
字寬選擇:Normal (100%) vs. SemiCondensed (90%)
這是 Ioskeley Mono 相比原版商業字體的一大彈性優勢:
Normal (100%) :字元寬高比與 Berkeley Mono (TX-02) 標準版一致,適合外接 27 吋以上 4K 螢幕、或是習慣將視窗維持在兩欄以內的開發環境。
SemiCondensed (90%) :將水平推進寬度微縮 10%。正如你在上方互動實驗室看到的即時統計,在相同的視窗寬度下,SemiCondensed 每一列可以多塞進約 11% 的字元數 ,例如原本只能塞 80 列的分割視窗可以直接擴充到 89 列,而且因為字懷原本就方正飽滿,縮減 10% 後完全不會有壓扁變形的窒息感,非常適合筆電螢幕或重度 tmux / TUI 多視窗使用者。
渲染微調:Hinted vs. Unhinted
macOS 與 Linux HiDPI 外接螢幕 :在 2x 縮放以上的高解析度螢幕上,建議選擇 Unhinted ,讓 FreeType 或 CoreText 直接根據原始向量輪廓進行平滑抗鋸齒渲染,最能保留字體原本的粗細比例。
Windows ClearType 或標準 1080p 螢幕 :在較低像素密度的環境下,建議選擇 Hinted 版本,透過字型內嵌的網格對齊指令(grid-fitting hints)強制將水平與垂直筆畫貼齊實體像素邊界,避免小字級下出現邊緣發虛。
在 IDE 與 Linux 終端機的實戰配置
以下整理我在日常使用的編輯器與 Linux 終端機環境中,讓 Berkeley Mono 與 Ioskeley Mono 發揮最佳視覺效果的設定範例。
VS Code 與 Zed 編輯器設定
在 VS Code 的 settings.json(支援 JSONC 註解語法)中,我們可以將 Ioskeley Mono 與 Berkeley Mono 排進 fallback 鏈,並透過 editor.fontLigatures 同時控制連字與點狀/斜線零:
{
"editor.fontFamily" : "'Ioskeley Mono', 'Berkeley Mono', 'Monaspace Neon', 'Cascadia Code', Consolas, monospace" ,
"editor.fontSize" : 14.5 ,
"editor.lineHeight" : 1.55 ,
"editor.fontWeight" : "400" ,
// 若想維持預設點狀 0 並開啟連字:
"editor.fontLigatures" : "'calt' 1, 'liga' 1, 'zero' 0"
// 若想將 0 切換為斜線 0,改設為: "'calt' 1, 'liga' 1, 'zero' 1"
}
而在以 Rust 與 GPUI 打造的 Zed 編輯器(~/.config/zed/settings.json)中,則可以透過 buffer_font_features 精準指定:
{
"buffer_font_family" : "Ioskeley Mono" ,
"buffer_font_size" : 15 ,
"buffer_line_height" : { "custom" : 1.55 },
"buffer_font_features" : {
"calt" : true ,
"zero" : false
}
}
在 terminal 環境中,建議直接指定原生的 Ioskeley Mono Term ,確保 TUI 框線(┌─┬─┐)嚴格對齊字元格,同時讓 Ghostty 的內建圖示引擎或系統的 Symbols Nerd Font Mono fallback 負責渲染開發者圖示。
Ghostty (~/.config/ghostty/config)
Ghostty 原生內建 Nerd Font 圖示支援,直接指定未打補丁的 Ioskeley Mono Term 即可獲得最飽滿的圖示比例與純淨字形:
font-family = "Ioskeley Mono Term"
font-size = 12
adjust-cell-height = 10%
# 若偏好斜線 0,可取消下一行註解:
# font-feature = +zero
在 Alacritty 與 Foot 這類不含內建圖示的終端機中,只要系統已安裝獨立的 ttf-nerd-fonts-symbols-mono(或透過 Fontconfig 設定 fallback),同樣可以直接指定原生的 Ioskeley Mono Term:
[font ]
size = 12.0
normal = { family = "Ioskeley Mono Term" , style = "Regular" }
bold = { family = "Ioskeley Mono Term" , style = "Bold" }
italic = { family = "Ioskeley Mono Term" , style = "Italic" }
在 Wayland 原生輕量終端機 Foot 中(亦可在逗號後顯式串接 Symbols Nerd Font Mono 作為後備字型):
[main]
font = Ioskeley Mono Term:size=12, Symbols Nerd Font Mono:size=12
line-height = 18px
在 Omarchy Linux 中一鍵完成全系統切換
如果你跟我一樣在桌機或筆電上使用 DHH 主導的 Omarchy 開發者環境(基於 Arch Linux + Hyprland),設定起來又更省事了。將原版字型檔解壓縮放入 ~/.local/share/fonts/IoskeleyMono/ 並更新字型快取後,只需要執行內建的 omarchy-font-set 指令:
# 重新掃描字型快取並確認系統已識別
fc-cache -fv ~/.local/share/fonts/IoskeleyMono
fc-list | grep -i "Ioskeley Mono Term"
# 一鍵同步更新系統所有終端機與預設等寬字體
omarchy-font-set "Ioskeley Mono Term"
# 或在較新版 Omarchy 使用: omarchy font set "Ioskeley Mono Term"
這行指令會自動幫你把 Ghostty、Kitty、Alacritty、Foot、Waybar 狀態列,以及 ~/.config/fontconfig/fonts.conf 中的全域 <family>monospace</family> 對應一次更新到位。當所有視窗、狀態列與 TUI 面板都換上統一的 1970 年代工程手冊字形時,整個桌面環境的視覺一致性真的非常賞心悅目。
結語:商業原創與開源參數化造字的良性共鳴
回頭看 Berkeley Mono (TX-02) 與 Ioskeley Mono 的關係,我認為它們並不是零和替代,而是字體生態系裡一段很有意思的對話:
如果你深深著迷於 Neil Panchal 對每一筆曲線的手工打磨,且日常主力環境是固定的個人工作站,直接購買一套商業版 Berkeley Mono 支持獨立字體設計師,絕對是一筆值得的投資。
如果你需要跨多台 Linux 機器透過 dotfiles 自動部署、偏愛 SemiCondensed (90%) 的高密度分割視窗排版、需要原生的 Nerd Font terminal 支援,或者想先在自己的日常專案裡零成本體驗這種冷冽俐落的工程手冊美學,那麼開源的 Ioskeley Mono 無疑是目前完成度極高的選擇。
不妨現在就捲回上方的互動實驗室,把你最常寫的程式碼貼進去,親眼試試看哪一種字寬與字形組合最對你的胃口吧!