PHPとfontforgeで実装するWebフォント変換サーバー
はじめに
Webサイトで好きなフォントを使いたい時、「Webフォント」は非常に強力な選択肢です。しかし、フォント形式にはOTF, TTF, WOFF, WOFF2, EOT, SVGなど様々な種類があり、クロスブラウザ対応を考えると複数の形式を用意する必要があって面倒ですよね。
また、フォントファイルをサーバーにアップロードして変換ツールにかける、という作業も手間がかかります。
この記事では、そんな手間を解消するために、PHPのプログラムからサーバーサイドでフォント形式を自動変換する仕組みを実装した際の知見を共有します。
やりたいこと
- ユーザーがフォームから
OTFまたはTTF形式のフォントファイルをアップロードする。 - サーバー側で、アップロードされたフォントを主要なWebフォント形式(
WOFF2,WOFF,EOT,SVG)に一括変換する。 - 変換したフォントファイルをZIPファイルにまとめて、ユーザーにダウンロードさせる。
中核を担う技術:FontForgeと各種コンバーター
今回の仕組みの心臓部となるのが、CUI(コマンドライン)で動作するフォント編集・変換ツールです。これらをPHPのexec()関数から呼び出して利用します。
-
FontForge:
オープンソースの高機能フォントエディタ。コマンドラインからスクリプトを実行でき、OTFからTTF、TTFからSVGなど、主要な変換を担います。 -
ttf2eot, ttf2woff, woff2_compress:
それぞれTTFを各Webフォント形式に変換するための専用コンバーターツールです。事前にサーバーにインストールしておく必要があります。
# 事前に必要なツールをインストールしておく
sudo apt-get install fontforge woff-tools woff2 ...
実装のポイント (CakePHPでの例)
今回はCakePHPのコントローラー内に実装した例を元に、処理の流れを解説します。
1. ファイルのアップロードと前処理
まず、フォームから送信されたフォントファイルを受け取ります。ファイル名に日本語などが含まれているとサーバーで扱いにくいため、Inflector::slug()などを使って英数字の安全なファイル名に変換しておきます。
1. ファイルのアップロードと安全なファイル名の生成
まず、フォームから送信されたフォントファイルを受け取ります。この時、セキュリティ上の重要なポイントは、ユーザーがアップロードしたファイル名をそのまま信用しないことです。悪意のあるファイル名によって予期せぬ問題が起きる可能性を避けるため、ファイル名はサーバー側で完全に新しく生成します。
time()(現在時刻のタイムスタンプ)とrandom_bytes()(暗号学的に安全な乱数)を組み合わせることで、他のファイルと衝突しない、推測困難なファイル名を生成しています。
Zennのコメントで、ファイル名をサーバー側で命名する方が安全であるとのご指摘をいただきました。ありがとうございます!より堅牢な実装に修正しました。
$data = $this->request->getData();
$tmp_path = TMP . 'web_font_files/';
- $original_pathinfo = pathinfo($data['WebFontFile']['ttf']['name']);
- // ファイル名をASCII文字のみに変換
- $file_name = \Cake\Utility\Inflector::slug($original_pathinfo['filename']);
- $ttf_file_name = $file_name . '.ttf';
- $ttf_file_path = $tmp_path . $ttf_file_name;
- // アップロードされたファイルを一時ディレクトリにコピー
- exec("cp {$data['WebFontFile']['ttf']['tmp_name']} " . $ttf_file_path);
+ $uploaded_file = $data['WebFontFile']['ttf'];
+ // 1. 安全性の検証:本当にアップロードされたファイルか確認
+ if (!is_uploaded_file($uploaded_file['tmp_name'])) {
+ // エラー処理
+ throw new \Exception('Invalid file upload.');
+ }
+ // 2. サーバー側でユニークなファイル名を生成
+ // 形式: タイムスタンプ + 8バイトのランダムな16進数文字列
+ $file_name = time() . '_' . bin2hex(random_bytes(8));
+ // 3. 拡張子を取得して、新しいファイルパスを決定
+ $extension = pathinfo($uploaded_file['name'], PATHINFO_EXTENSION);
+ if (strtolower($extension) !== 'ttf') {
+ // TTF以外のファイルはエラーにするなど
+ throw new \Exception('Invalid file type.');
+ }
+ $ttf_file_name = $file_name . '.ttf';
+ $ttf_file_path = $tmp_path . $ttf_file_name;
+ // 4. アップロードされたファイルを、新しい名前で一時ディレクトリに移動
+ if (!move_uploaded_file($uploaded_file['tmp_name'], $ttf_file_path)) {
+ // エラー処理
+ throw new \Exception('Failed to move uploaded file.');
+ }
2. OTFからTTFへの変換
アップロードされたのがOTFファイルだった場合、まずFontForgeを使ってTTFに変換します。TTFの方が他のWebフォント形式への変換元として扱いやすいためです。
if (!empty($data['WebFontFile']['otf']['name'])) {
// ...前処理...
// FontForgeのスクリプト機能を使ってOTFをTTFに変換
exec('fontforge -lang=ff -c \'Open($1); Generate($1:r + ".ttf")\' ' . $otf_file_path);
// ...後処理...
}
fontforge -lang=ff -c '...'というコマンドで、FontForgeのスクリプトを直接実行しています。
3. TTFから各種Webフォントへの変換
ベースとなるTTFファイルが用意できたら、そこから各種コンバーターを使ってWOFF2, WOFF, EOT, SVGを生成していきます。
// ttf => svg に変換
exec('fontforge -lang=ff -c \'Open($1); Generate($1:r + ".svg")\' ' . $ttf_file_path);
// ttf => eot に変換
$eot_file_path = $tmp_path . $file_name . '.eot';
exec('ttf2eot ' . $ttf_file_path . ' ' . $eot_file_path);
// ttf => woff に変換
exec('ttf2woff ' . $ttf_file_path);
// ttf => woff2 に変換
exec('woff2_compress ' . $ttf_file_path);
exec()で外部コマンドを順番に実行しているだけですが、これで一時ディレクトリに各形式のフォントファイルが生成されます。
4. ZIP圧縮とダウンロード
最後に、生成された全てのフォントファイルをzipコマンドで一つのファイルにまとめ、一時ファイルを削除してから、ユーザーをダウンロード用のURLにリダイレクトさせます。
$zip_files = '';
// ... 生成されたファイルのパスを$zip_filesにまとめる ...
$dl_file_path = WWW_ROOT . 'files/';
$zip_file = $file_name . '.zip';
// -j オプションでディレクトリ構造を無視して圧縮
exec('zip -j ' . $dl_file_path . $zip_file . $zip_files);
// ... 一時ファイルを削除 ...
// ダウンロードアクションにリダイレクト
return $this->redirect(['action's => 'downloadWebFont', '?' => ['file' => $zip_file]]);
注意点
-
セキュリティ:
exec()関数は、ユーザーからの入力をそのまま渡すと非常に危険です(OSコマンドインジェクション)。ファイル名などをescapeshellarg()でエスケープ処理するか、今回の例のようにInflector::slug()で安全な文字列に変換する処理が必須です。 - サーバー負荷: フォントの変換処理はCPUに負荷がかかります。利用頻度が高いサービスにする場合は、処理をキューイングするなどの非同期化を検討する必要があります。
-
エラーハンドリング:
exec()の実行結果をしっかりハンドリングし、変換に失敗した場合の処理を実装することが重要です(コード例では省略しています)。
まとめ
PHPからFontForgeなどのコマンドラインツールを呼び出すことで、Webフォントの変換処理を自動化する仕組みを実装しました。
Webサービスとして公開すれば、デザイナーやフロントエンドエンジニアの面倒なフォント変換作業を効率化できる、価値あるツールになるかもしれません。PHPの可能性とLinuxコマンドの強力さを再認識できた、楽しい開発でした。
この記事で紹介した内容以外にも、技術情報をブログで発信しています。
MEANTECH
Discussion
Inflector::slug()は3.2.7で廃止されたので、Text::slug()を使いましょう。
というかURL用だからファイル名に使っていいんだっけ?サーバ側命名のほうが安全な気がする。
的確なご指摘、誠にありがとうございます!
Inflector::slug()がCakePHP 3.2.7で廃止されていた件、完全に失念しておりました。
「サーバ側命名のほうが安全」というご指摘、まさにおっしゃる通りですね。slug化するとしても、元がユーザー入力である以上、意図しない挙動やファイル名の衝突といったリスクはゼロではないと改めて気付かされました。
いただいたアドバイスを元に、ユーザーのファイル名には依存せず、サーバー側でタイムスタンプと乱数を元にユニークなファイル名を生成する方式に、早速コードを修正いたしました。
貴重なフィードバックをいただき、本当にありがとうございました!