PHP GD の文字描画で日本語だけ位置がずれる|原点と文字の高さの見落とし

  • URLをコピーしました!

WordPress のプラグイン「Coverpeek」の番外編、その4です。
Coverpeek は投稿タイトルを背景画像の上に描いて、アイキャッチを自動で作るプラグインです。
画像の描画には PHP の GD を使っています。

Coverpeek はこちらから入れます。

GD での画像生成が PHP 8.5 で壊れたときの話は↓こちらです。

今回はタイトルの文字が余白に食い込んでいた不具合の話です。
余白を色で表示する機能を作ったら、日本語のタイトルだけ文字が上の余白にはみ出しているのが見えました。

原因は文字の高さを「フォントサイズと同じ」と見積もっていたことでした。
imagettfbbox() で、実際に描く文字列の大きさを測るように直しています。


目次

余白を色で表示する機能を作った理由

Coverpeek の設定画面では、タイトルの周りの余白(上下・左右)を px で指定できます。
余白の入力欄を選ぶと、プレビュー画像の上に余白の範囲が色付きで表示されるようにしました。
横の余白は赤、縦の余白は青です。

この機能を付けたのは、私自身が画像のバランスが悪いと気になってしまうからです。
同じように思う人もいるだろうなと。

日本語か英語か、行間をどうするかでも、余白の見え方は変わります。
今はそこまで必要な機能ではないかもしれません。
後々、記事ごとに画像の設定を変えられるようにしたときなど、こういう細かい機能が必要になると思って入れておきました。


日本語だけ余白に食い込んでいた

できあがった機能を実際に使ってみるとこうなっていました。

修正前。日本語のタイトルの上端が、上の余白(青)に食い込んでいる
修正前の画像に余白の範囲を重ねたもの。文字の上端が青い帯に入り込んでいます

タイトルの上端が上の余白(青い帯)に入り込んでいます…!

最初に疑ったのは余白の設定がうまく効いていないのか、文字の折り返しがおかしくなっているのかの2つでした。

英語のタイトルでも試してみると、こちらは問題なさそうに見えました。
英語で大丈夫なら余白の設定そのものは効いているはず。
そこで余白の設定は原因から外しましたが、折り返しはまだ疑っていました。

ただ、普段は日本語のサイトばかり触っているので、日本語と英語で何が違うのかはすぐには見当がつきませんでした。


原因:文字の高さを「フォントサイズ」で見積もっていた

y 座標は文字の上端ではなくベースライン

GD で文字を描く imagettftext() は、x と y の座標を指定して文字を描きます。
この y 座標は文字の上端の位置ではありません。
文字が乗っている線、ベースラインの位置です。

なので、上の余白を100pxにしたいなら「100px + 文字の上端からベースラインまでの高さ」を y に渡す必要があります。
修正前のコードはこの高さをフォントサイズと同じとみなしていました。

// 修正前:上端からフォントサイズ分下がった位置をベースラインにしていた
return $margin_y + $font_size;

太字の日本語はフォントサイズより背が高い

実際に測ってみるとフォントサイズ56pxで描いたとき、ベースラインから文字の上端までの高さは以下のようになっていました。

文字上端までの高さ
日本語(Noto Sans JP Bold)63px
英語(同じフォント)55px

これが原因でした。
日本語はフォントサイズより7px高く、そのぶん上の余白にはみ出していました。
デフォルトの40pxでも実際の高さは46pxあったので、6pxはみ出していたことになります。

英語は55pxでフォントサイズとほぼ同じなので、食い込みが小さく、目立ちにくかったようです。

記事を書くにあたって、白い背景で描き直してピクセル単位で測ってみました。
修正前は日本語で約9px、英語でも約5px、上の余白に食い込んでいました。
英語も実は少しはみ出していたのですが、日本語の約半分なのでパッと見では気づかなかったんですね。

修正前の英語のタイトル。上の余白への食い込みは日本語より小さい
英語も少し食い込んでいますが、日本語ほど目立ちません

横方向も「原点」の考え方は同じ

x 座標も同じで、文字の見た目の左端ではなく、描画の原点を指定しています。
文字の左側には少し空きがあり、その幅は文字によって違います。

こちらも原点からのずれを差し引くように直しました。
ただ、今回の条件で測ると差は数px程度で、目で見てはっきりわかったのは上の余白のほうでした。


直し方:実際に描く1行目を測る

文字の大きさは imagettfbbox() で測れます。
描く文字列を渡すと、その文字列を囲む四角形の4隅を座標で返してくれます。
座標の基準はベースラインです。

返ってくる配列の [7] は、左上の角の y 座標です。
ベースラインより上はマイナスで返ってくるので、符号を反転すれば「ベースラインから上端までの高さ」になります。

// 実際に描く文字列で、ベースラインから上端までの高さを測る
private static function measure_ascent( $font_size, $font_path, $text ) {
	$bbox = imagettfbbox( $font_size, 0, $font_path, $text );
	return -$bbox[7];
}

y 座標はフォントサイズの代わりにこの値を使います。

// 修正後:実測した高さを使う
$ascent = self::measure_ascent( $font_size, $font_path, $lines[0] );
return $margin_y + $ascent;

測る文字列には実際に描く1行目をそのまま渡しています。
最初は「文字ABg」のような決まったサンプルで測ることも考えたのですが、実際の文字と字形が違うと1px前後のずれが残るそうなのでやめました。
実際のデータで測るのが確実です。

横方向も同じように、imagettfbbox() の [0](原点から左端までのずれ)を差し引いています。

// 修正前
return $margin_x;

// 修正後:原点から見た実際の左端のずれを差し引く
$bbox = imagettfbbox( $font_size, 0, $font_path, $line );
return $margin_x - $bbox[0];

直した結果がこちらです。

日本語のタイトルの修正前と修正後の比較
上が修正前、下が修正後。修正後は上の余白の境目にそろっています

修正後は文字の上端が余白の境目にそろいました。
スッキリです。


まとめ

今回の不具合は上の余白に数px食い込むだけのものでした。
余白を色で表示する機能がなかったら、たぶん気づかないままだったと思います。
確認するための仕組みを作ったことで、ずれが目に見えるようになりました。

GD で日本語を描くときは、文字の高さをフォントサイズで見積もらず、imagettfbbox() で実際に描く文字列を測るのがよさそうです。
特に太字の日本語フォントは英語よりも上下を大きく使うので、ずれが目立ちやすいようです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

Web制作会社にてフロントエンド業務に携わる一方、個人でブログを書いたりサイト制作やシステム構築にいそしむ。元家具職人。
https://rcwas.com/

目次