Graphics2Dで100ピクセルの文字を描くためのFontポイント数は?

これをやろうとしてはたと困ってしまった。Graphics2Dで何か文字を描く時にはFontを作成しなければならないのだが、その大きさはポイント数指定なのである。例えばBufferedImageを作成してcreateGraphics()にてGraphics2Dを取得し、高さ100ピクセルの文字を書きたいとすれば、Fontに与える「ポイント数」はいくつにすれば良いのか?

結局のところ、まともな解は無いようだ。次のような方法で取得する以外無いように思える。

  • 極めて適当なポイント数でフォントを作成する
  • このフォントで対象文字列を「描画」してみて、その境界を求める
  • 境界が小さすぎればポイント数を大きく、大きすぎればポイント数を小さくする
  • 以上を繰り返す。

import java.awt.*; import java.awt.font.*; import java.awt.geom.*; /** * 任意のサイズのフォントを作成する。 * * <h2>問題点</h2> * <p> * Javaではフォントサイズはポイントで指定するようになっている。つまり、「物理的大きさ」を指定しな * ければならない。しかし、任意の{@link Graphics2D}が与えられ、その中で高さ100の文字を * 描画したい場合には、この100という数値は「物理的大きさ」ではない。 * 例えば、単純に100ピクセルの高さに描画した場合、その100ピクセルという数字は、 * ユーザへの表示時には実際に何mmで表示されるのかはわからない。 * </p> * <p> * したがって、この状況で実際のポイントサイズを指定してフォントを生成することは不可能である。 * </p> * * <h2>取得方法</h2> * <p> * しかし、適当なポイント数でフォントを作成した後、そのフォントで描画した結果のピクセル数を取得する * ことはできる。 * * このように最初に極めて適当な値でフォントを作成しておき、これで描画を行ってみて、 * 実際のピクセル数を取得し、 * これが大きすぎる場合はポイント数を小さくしていき、小さすぎる場合はその逆を行い、 * おおよそ所望のサイズを得ることができる。 * </p> * <p> * このやり方でも、フォントに与える初期値のポイント数が可能な限り所望のものに近い方が * 計算が速いことは当然である。 * このため、とりあえずJavaツールキットから取得される「スクリーン解像度」からピクセル数/ポイント数を取得し、 * この値を使い初期ポイント数を決定する。 * </p> * <p> * なお、描画される文字の大きさを計測するためのサンプルは適当でよい。ここでは適当な漢字と * アルファベットの組み合わせとしている。 * </p> * @see <a href="https://stackoverflow.com/questions/17551537/how-to-fit-font-into-pixel-size-in-java-how-to-convert-pixels-to-points"> * How to fit font into pixel size in Java? How to convert pixels to points?</a> * @see <a href="https://stackoverflow.com/questions/5829703/java-getting-a-font-with-a-specific-height-in-pixels"> * Java: Getting a font with a specific height in pixels</a> */ public class FontCreator { /** スクリーン解像度をツールキットから取得する */ static final int SCREEN_PIXELS_PER_INCH = Toolkit.getDefaultToolkit().getScreenResolution(); /** スクリーンの1ピクセルあたりのポイント数を計算 */ static final double SCREEN_POINTS_PER_PIXEL = 1 / (SCREEN_PIXELS_PER_INCH * Consts.INCH_PER_POINT); /** 文字大きさを測定するためのサンプル文字列 */ static final String SAMPLE_STRING = "漢字fgijklqy"; /** * 指定フォント名称で指定サイズのフォントを取得する。 * * @param g2d {@link Graphics2D} * @param fontName フォント名称 * @param height 所望のフォントサイズ * @return フォント */ public static FontInfo create(Graphics2D g2d, String fontName, double height) { FontRenderContext frc = g2d.getFontRenderContext(); // フォントポイント数初期値を計算し、初期フォントを作成する int fontPoints = (int)Math.round(SCREEN_POINTS_PER_PIXEL * height); Font font = new Font(fontName, 0, fontPoints); // サンプル文字列の高さを取得する double fontHeight = getFontHeight(frc, font); // 所望高さとサンプル文字列描画高さを比較する if (fontHeight < height) { // 所望の高さより小さすぎる場合、フォントのポイント数を大きくしていく。 while (true) { fontPoints++; font = font.deriveFont((float)fontPoints); if (getFontHeight(frc, font) > height) { break; } } } else { // 所望の高さより大きすぎる場合、フォントのポイント数を小さくしていく while (true) { fontPoints--; font = font.deriveFont((float)fontPoints); if (getFontHeight(frc, font) < height) { break; } } } Rectangle2D bounds = getFontBounds(frc, font); return new FontInfo(font, -bounds.getY(), bounds.getHeight() + bounds.getY()); } /** * 指定されたフォントレンダリングコンテキストの下で、指定フォントでサンプル文字列を描画した * 場合の高さを取得する。 * @param frc フォントレンダリングコンテキスト * @param font フォント * @return 描画結果 */ static double getFontHeight(FontRenderContext frc, Font font) { double fontHeight = getFontBounds(frc, font).getHeight(); return fontHeight; } static Rectangle2D getFontBounds(FontRenderContext frc, Font font) { // サンプル文字列についてTextLayoutを作成する TextLayout layout = new TextLayout(SAMPLE_STRING, font, frc); // それを取り囲む境界を取得し、高さを取得する Rectangle2D bounds = layout.getBounds(); return bounds; } }
import java.awt.*;

/**
 * フォントと票樹文字列で計測したアセント・ディセントの情報
 * <p>
 * あるフォントで描画を行う場合、TextLayoutでその描画境界を取得することはできるが、
 * しかし、その高さは常に同じとは限らない。与える文字の種類によって変化してしまう。
 * 例えば"漢字"を与えた時と"ex"を与えた時は異なる。
 * </p>
 * <p>
 * この情報は、「標準的」な計測用文字列で計測したアセント・ディセントであり、
 * 常にこの値を使用することにより、同じフォントを使って文字を横並びにした場合に「凸凹」
 * ができないようにすることができる。
 * </p>
 */
public class FontInfo {

  /** フォント */
  public final Font font;

  /** アセント */
  public final double ascent;

  /** ディセント */
  public final double descent;

  public FontInfo(Font font, double ascent, double descent) {
    this.font = font;
    this.ascent = ascent;
    this.descent = descent;
  }

  @Override
  public String toString() {
    return ascent + "," + descent;
  }
}