C#

OCRBというフォント

OCRBというフォントは、機械での読み取りに適しているらしく、印刷でときどき使うことがある。でも、枠の中央に数字を描画しようとすると、印刷プレビューのときだけちょっとした調整が必要だ。いつも適当な感じで、「だいたいこのぐらいずらせばOKさ」みたいにしていたけど、暇を持て余したので、自分なりに調べてみた。

private void Document_PrintPage(object sender, PrintPageEventArgs e)
{
  e.Graphics.PageUnit = GraphicsUnit.Point;

  RectangleF _Rect = new RectangleF(72.0f, 72.0f, 96.0f, 144.0f);
  e.Graphics.DrawRectangle(Pens.Black, _Rect.X, _Rect.Y, _Rect.Width, _Rect.Height);

  using (StringFormat _Format = new StringFormat(StringFormat.GenericTypographic))
  {
    _Format.Alignment = StringAlignment.Center;
    _Format.LineAlignment = StringAlignment.Center;

    using (Font _Font = new Font("OCRB", 80.0f))
    {
      e.Graphics.DrawString("0", _Font, Brushes.Black, _Rect, _Format);
    }
  }
}
fig.1
fig.1: 本当は枠の真ん中に描画したいのに、上にずれてしまう。

いろいろ調べてみたら、フォントにはAscent, Descent, InternalLeading, ExternalLeadingといったプロパティがあるらしい。C#ではFontFamilyにGetCellAscent()などのメソッドがあって、CellAscent, CellDescent, EmHeight, LineSpacingを取得でき、あとは計算でCellHeight, InternalLeading, ExternalLeadingを求めるようだ。これらの値はデザイン単位ということになっている。要するに各部の比率を表していると思う。

int CellAscent = _Font.FontFamily.GetCellAscent(FontStyle.Regular);
int CellDescent = _Font.FontFamily.GetCellDescent(FontStyle.Regular);
int EmHeight = _Font.FontFamily.GetEmHeight(FontStyle.Regular);
int LineSpacing = _Font.FontFamily.GetLineSpacing(FontStyle.Regular);
int CellHeight = CellAscent + CellDescent;
int InternalLeading = CellHeight - EmHeight;
int ExternalLeading = LineSpacing - CellHeight;
fig.2
fig.2: Lathaというフォントの例。これらは大まかな情報で、いろいろなフォントを実際に見てみると、すべてのグリフがちゃんとEmHeightの中に収まっているわけではない。

OCRBはどうなっているかというと、他のフォントと比べてExternalLeadingがやけに大きい。InternalLeadingがマイナスなのも気になるが、そのようなフォントは他にもあり、あまり問題ではないようだ。

fig.3
fig.3: OCRBの場合。

試行錯誤して、CellDescent + ExternalLeadingの分だけ、枠を下に拡げればだいたい合うとわかった。上下中央寄せにしても、下寄せにしても、2行にしても、3行にしても、だいたいうまくいく。だいたいうまくいくんだけど、どうしてもちょっと合わない。よく見ると「0」はグリフがCellAscentに収まってなくて、わずかに下にずれているような感じだ。

ビットマップに描画して調べた。EmHeight = 2048に対して「0」の高さは1445、上の隙間は16だった。枠を上に16ずらし、高さを2920 - 1445 = 1475拡げれば、ぴったり収まる。印刷のときはEmHeightだけ考えるようで、上に16ずらし、高さを16 + CellDescent = 16 + 337 = 353拡げれば良い。

private void Document_PrintPage(object sender, PrintPageEventArgs e)
{
  e.Graphics.PageUnit = GraphicsUnit.Point;

  RectangleF _Rect = new RectangleF(72.0f, 72.0f, 96.0f, 144.0f);
  e.Graphics.DrawRectangle(Pens.Black, _Rect.X, _Rect.Y, _Rect.Width, _Rect.Height);

  using (StringFormat _Format = new StringFormat(StringFormat.GenericTypographic))
  {
    _Format.Alignment = StringAlignment.Center;
    _Format.LineAlignment = StringAlignment.Center;

    using (Font _Font = new Font("OCRB", 80.0f))
    {
      if (((PrintDocument)sender).PrintController.IsPreview)
      {
        _Rect.Y -= (_Font.SizeInPoints * 16.0f) / 2048.0f;
        _Rect.Height += (_Font.SizeInPoints * 1475.0f) / 2048.0f;
      }
      else
      {
        _Rect.Y -= (_Font.SizeInPoints * 16.0f) / 2048.0f;
        _Rect.Height += (_Font.SizeInPoints * 353.0f) / 2048.0f;
      }

      e.Graphics.DrawString("0", _Font, Brushes.Black, _Rect, _Format);
    }
  }
}
fig.4
fig.4: 正確に中央に描画

OCRBはこれで良いけど、それじゃ他のフォントはどうなっているんだろう。どうしてこんなことしなくても真ん中に出るんだろうな。自分のマシンにインストールされているフォントを見た限りだけど、多くのフォントはInternalLeadingやExternalLeadingをうまく使ってLineSpacingの中央辺りにグリフを配置しているみたいだ。それで“何となくうまくいってる”らしい。実際、中央に描画しても、少し上にずれていたり、少し下にずれていたりすることがある。みなみな個性豊かで、いろいろなバリエーションがあり、ぴたりと位置を合わせようとすれば、OCRBよりもっと苦労するだろう。君子危うきに近寄らず。

その他のメモ

  • EmHeightがフォントのサイズにあたる。10ptを指定すれば、EmHeightが10ptになるようにフォントが作られる。
  • Graphics.MeasureString()が返す値は、正確なサイズよりも少し大きい。幅はフォントサイズの0.333007813倍大きく、高さはフォントサイズの0.125倍大きい。ただし、この余白は多分、あったほうが良いのだろうと思う。
    float PaddingW = _Font.SizeInPoints * 0.333007813f;
    float PaddingH = _Font.SizeInPoints * 0.125f;
    
  • OCRBで円記号を使おうとするとバックスラッシュになってしまうけど、あれは '\\' じゃなくて '\xa5' を使えば大丈夫。

(2017/12/02 更新)
(2017/11/24 更新)
(2017/11/22 初稿)