Photo by 🇸🇮 Janko Ferlič on Unsplash

mPDF 是一個基於 PHP 用來產出 pdf 檔的函式套件;我們公司導入的 InvoicePlan 在輸出 pdf 這一塊也是用 mPDF 處理,然後就很理所當然地遇到了中文亂碼的問題。

拿關鍵字 mPDF 中文 亂碼 餵 Google,查到的大多都寫的很簡略,像是這樣:

$mpdf = new\Mpdf\Mpdf ([
  "autoScriptToLang" => true,
  "autoLangToFont" => true,
]);

按建議試了一下,的確可以輸出中文,但字體不是我要的;而設定字體部份繼續 Google 也查不太到有用的資料;索性直接看原始碼,搭配官方文件來瞭解裡面實際處理的方式。

lang 和 font-family

mPDF 所做的事,基本上就是將 HTML code 解析,並組合輸出成一份 pdf document。在輸出過程中,mPDF 會按 HTML langCSS font-family 兩個屬性判斷要使用哪種字體。

參考文件 Fonts & Languages / lang 6.xlang 會影響 OTL 外字集的設定,或也可以做為 CSS lang selector 使用; font-family 則是指定 HTML block 內容要輸出為哪種字體,所支援的 HTML tag 詳細清單可見 CSS & Stylesheets / Supported CSS

一般網路上建議啟用 autoScriptToLangautoLangToFont ,就是省略掉上面兩個設定,讓系統去暴力拆你的內容判斷該用什麼 langfont-family,具體可以看 Mpdf.php 中的 markScriptToLangLanguage / LanguageToFont.phpLanguage / ScriptToLang.php 這幾支檔案…是真的滿暴力的。一般如果內文含有簡中的話,語系會被判定為 lang="und-Hans",繁中的話語系則是 lang="und-Hant",字體不分繁簡會自動選用 font-family: sun-exta(宋體)。

Free Adobe CJK Asian fonts

在一些舊的資訊裡,會告訴你要讓中文正常顯示還要啟用 useAdobeCJK,這選項某方面來說完全是個雷。useAdobeCJK 的意思是,假設你的電腦裡有安裝 Adobe Acrobat 或任何相關套件,理論上也該會裝有 Adobe 的 CJK Asian fonts;啟用這個選項,可以直接透過電腦內已安裝的字體來顯示,就不需要另外把字體塞進 PDF,進而有效減少檔案大小。

按 PDF 和 Adobe Acrobat 的普及率來說這個設計是沒錯,問題是時代在前進,useAdobeCJK 所採用的 Adobe 標準繁中字體是 MSungStd-Light-Acro — 見 AddBig5Font,已經是老古董的字體了,現在的較新的電腦內很少有安裝,啟用這個選項反而會造成顯示上的問題。

官方文件 mPDF Variables / useAdobeCJK 上, useAdobeCJK 是從 5.0 版開始支援的;我們從 Github 可以查到的最小版本號是 2011/09/14 發佈的 v5.3.0…可想而知這個功能有多古老。此外雖然官方標注 useAdobeCJK 預設是啟用,但我目前用的 mPDF v8.0.1 已經編成預設關閉了。

回到自訂字體

所以最穩妥的中文顯示方式,除了 mPDF 內附的 sun-exta 外,就是自己增加字體了,幸好這個需求已經被 mPDF 做到非常簡單,只有三個步驟:

  1. 佈署字體檔案
  2. 指定字體檔案目錄
  3. 連結 font-family 和字體檔

接下來我們會用目前最潮的 台北黑體 來做範例。

佈署字體檔案

簡單來說就是把字體檔放到你 mPDF 可存取的目錄下。台北黑體 目前共有 Light、Regular、Bold 三種,我們需要用到 Regular 和 Bold,字體檔下載後直接放到 mpdf/ttfonts/ 目錄。

指定字體檔案目錄

如果你的自訂字體的檔案目錄不是 mpdf/ttfonts/ (例如 mPDF 只是以一個 vendor 存在),那就要額外指定字體目錄,參考 官方範例

// 預設字體目錄
$defaultConfig = (new \Mpdf\Config\ConfigVariables())
  ->getDefaults();

$fontDirs = $defaultConfig["fontDir"];

$mpdf = new \Mpdf\Mpdf ([
  "fontDir" => array_merge ($fontDirs, [
    __DIR__ . '/custom/font/directory',
  ])
]);

連結 font-family 和字體檔

因為在 HTML 當中,字體至少會有標準、粗體、斜體、粗斜體這四種變化,所以 font-family 設定上也要按需求對應到各種不同的字體檔,以下是 mPDF 附帶字體 freesans 的 fontdata 設定:

// font-family 名稱
'freesans' => [

  // Regular 標準字體
  'R' => 'FreeSans.ttf',

  // Bold 粗體
  'B' => 'FreeSansBold.ttf',

  // Italic 斜體
  'I' => 'FreeSansBoldOblique.ttf',

  // Bold Italic 粗斜體
  'BI' => 'FreeSansOblique.ttf',
],

以台北黑體來說,因為沒有斜體和粗斜體,所以 fontdata 設定是:

// font-family 名稱
'taipei' => [

  // Regular 標準字體
  'R' => 'TaipeiSansTCBeta-Regular.ttf',

  // Bold 粗體
  'B' => 'TaipeiSansTCBeta-Bold.ttf',
],

除了上面的 R B I,字體設定還有另外許多參數,例如 SIP-extensionuseOTL …等,詳細可參考 Fonts & Languages / Fonts in mPDF v7+

整合 fontDir、fontdata 設定

將上面 2、3 步驟整理起來:

// 預設字體目錄
$defaultConfig = (new \Mpdf\Config\ConfigVariables())
  ->getDefaults ();

$fontDirs = $defaultConfig["fontDir"];

// 新增指定字體目錄
$fontDirs = array_merge ($fontDirs, [
  __DIR__ . '/custom/font/directory'
]);

// 預設字體設定
$defaultFontConfig = (new \Mpdf\Config\FontVariables ())
  ->getDefaults ();

$fontData = $defaultFontConfig["fontdata"];

// 新增字體 font-family
$fontData = [
  'taipei' => [

    // Regular 標準字體
    'R' => 'TaipeiSansTCBeta-Regular.ttf',

    // Bold 粗體
    'B' => 'TaipeiSansTCBeta-Bold.ttf'
  ]
] + $fontData; // 要記得加上原本的 $fontData,
               // 否則 mPDF 內建字集會被清空而失效

$mpdf = new \Mpdf\Mpdf ([
  "fontDir" => $fontDirs,
  "fontdata" => $fontData
]);

然後要輸出成 PDF 的 HTML:

<div style="font-size: 30px;">
  <p style="font-family: taipei;">台北黑體 Regular:覺形創意有限公司</p>
  <p style="font-family: taipei; font-weight: bold;">
   台北黑體 Bold:覺形創意有限公司
  </p>
  <p style="font-family: sun-exta;">宋體 Sun-ExtA:覺形創意有限公司</p>
</div>

輸出結果:

噹噹~(鞠躬音效),可以用自訂的字體來正常顯示中文了。