この記事は はてなエンジニア Advent Calendar 2024 の 1/18 (49 日目) の記事です。 何故か二回書くことになりました。あと、期限に間に合いませんでした。
背景
前回 で、imageproc + ab_glyph crate を使った文字合成で x 座標および、y 座標を位置がずれる話とその修正方法を書きました。
その中の余談で
実は上記の方法は x, y が固定の場合に成り立つ方法で、ベースラインに沿った文字列だとちょっとうまくいきません。これに関しては一工夫ですぐ解決できるので別記事で解説したいところですね。
と書いてたのでその解説です。
環境は前回と同じ環境で、フォントも前回と同じ Dejavu Sans を利用しています。
ベースラインに沿わせる
結論から先に言いますと以下のコードで解決します:
const BASELINE_Y_POS: f32 = 200.0; // y = 200 の位置にベースラインがある想定 fn calc_y<F: Font>(px_scale_font: &PxScaleFont<F>) -> f32 { let result = BASELINE_Y_POS - px_scale_font.ascent(); result }
なんと、今まで入っていた text 引数を使わずに求めることができます。
結果
ベースラインがわかりやすいように、ベースラインに緑色*1 の線を加えて表示してみます。

ちゃんとベースラインに沿って表示してくれてますね。ピクセル単位で細かく見るとラスタライズの関係上、ずれているように見えますがほぼほぼ正確です。
今回使ったコードはこちらから確認できます:
https://github.com/KashEight/hatena-advent-2024/blob/33242360dfa1f36274a31e69c1e38530e99b0997/src/bin/processing_baseline.rs
解説
グリフの描画、ascent と descent
本筋の前にまず前提となるグリフ (Glyph) について述べます。今回の肝となる話です。 以下にグリフを描画した際に、どのようになるかを図にした概念図を示します。

黒い線で囲まれているのが、描画したときの大きさ、ab_glyph crate でいう ScaleFont::glyph_bounds*2 に当てはまる部分です。
一方、青い線で囲まれているのが実際の大きさ、OutlineGlyph::px_bounds*3 となります。
また、図中にある および
はそれぞれ、上記のメソッドで返される
Rect 構造体*4の min.x、min.y に対応します。
上図でベースラインとなる部分は青線の下部分、Rect 構造体でいう max.y がベースラインとなります。
ab_glyph crate はベースラインを基準として上部分を ascent、下部分を descent と定めています*5。この ascent および descent は描画したときの大きさ、つまりこの記事中では ScaleFont::glyph_bounds がもとになっており、ascent の空白部分 (bearing) である や descent の bearing も含んでしまいます。
上図は descent がほぼないパターンなので、わかりやすいようにもう一つ図を用意しました。

こちらはベースラインをわかりやすくするように赤線でベースラインを描画してます。 前図と比べ、ascent と descent が明確になっているかと思います (コードで出した描画結果と見比べてみてもいいかもしれません)。 ここでの ascent はベースラインから黒枠の上部分まで、descent は黒枠の下部分までになります。
描画位置を計算する
さて、前回は描画位置を固定している状態でした、一方こちらはベースラインに依存するため、描画位置を固定できません (文字の大きさによって描画位置が変動する)。
答えはコードに書いてありますが、描画位置を頑張って計算する必要があります。
とりあえず、計算のために色々変数にしましょう (全て単位は px です)。
: ベースラインより上のグリフの大きさ
: ベースラインより上の bearing の大きさ
: ascent
: ベースラインの y 座標
: 描画位置の y 座標
まず、描画位置の y 座標を求める式は となります。
一方で ascent は、以下の式
と表すことができます。
これを描画位置を求める式に代入すると と表すことができ、ベースラインの位置と ascent のみの式、つまりはコードのような式になります*6。
なので、今までやっていた文字一つ一つから求めるより圧倒的に早く、楽に描画位置を計算することができます*7。
*1:R = 0, G = 255, B = 0 | Hex: #00FF00
*2:これの詳細は前回の記事を参照してください
*3:同上
*4:https://docs.rs/ab_glyph/0.2.29/ab_glyph/struct.Rect.html
*5:https://docs.rs/ab_glyph/0.2.29/ab_glyph/trait.Font.html#glyph-layout-concepts
*6:コードでは ascent は足しているのではなく引いているのですが、これは描画では原点が左上となるためです。今回の立式では原点を左下に置いた座標で計算しているので y 座標が描画でと立式とで反転します
*7:正確には、この計算は文字一つにしか対応しない計算なので文字列 (文字集合) に対しては証明できているかと言われたらできてないんですが、そこらへん書くの面倒なのと実際に動いているところから省いています。もし、暇があれば自身で文字集合に対する証明はやってみてはいかがでしょうか。