MS ゴシックの文字幅

こんな場末の日記をわざわざ見に来る方は UAX #11: East Asian Width なんかは当たり前に読み込んでいると思うんですが、読み込んだ人はきっと気づくと思うんです、このドキュメントはあてにならないことに。じゃあどうすればいいかってなるんですが、端的には融通の効かない奴に合わせるわけですな。それはアプリ側のこともフォント側のこともあるんですが、今回はフォント側の事を考えることにします。フォント側のことを考えるってことは、要するにフォントは不動と考えるってことで、そういうフォントって要するにMS ゴシックのことなので、これを測ることにするわけです。
さて、どうやって測るかというと、実際に描画して測るわけです。TextRenderer.MeasureTextとかでも行けそうに思えるんだけど、グリフがない場合に fallback しちゃうんでうまくいかない。

using System;
using System.Drawing;
using System.Windows.Forms;
using System.Windows;

class Program
{
	[STAThread]
	static void Main(string[] args)
	{
		Application.Run(new FormA());
	}
}

class FormA : Form
{
	protected override void OnPaint(PaintEventArgs e)
	{
		for (int c = 0; c <= 0x10FFFF; c++) {
			if (0xD800 <= c && c <= 0xDFFF) continue;
			measureChar(e, c);
		}
		//measureChar(e, 0x20bb7);
		//measureChar(e, 0x20b9f);
		//measureChar(e, 0x216b4);
		Application.Exit();
	}

	private void measureChar(PaintEventArgs e, int c) {
    Font stringFont = new Font("VL Gothic", 16.0F);
    RectangleF layoutRect = new RectangleF(0F, 0F, 300F, 300F);
		Pen pen = new Pen(Color.Red, 1);
    StringFormat stringFormat = new StringFormat();
    stringFormat.FormatFlags = StringFormatFlags.NoFontFallback;

		string str = Char.ConvertFromUtf32(c);
    CharacterRange[] ranges = {new CharacterRange(0, str.Length)};
    stringFormat.SetMeasurableCharacterRanges(ranges);


    Region[] stringRegions = e.Graphics.MeasureCharacterRanges(str, stringFont, layoutRect, stringFormat);
    RectangleF measureRect1 = stringRegions[0].GetBounds(e.Graphics);
		Console.WriteLine("{0,0:X6}: {1}", c, measureRect1.Width);

    //e.Graphics.DrawString(str, stringFont, Brushes.Black, 0, 0, stringFormat);
    //e.Graphics.DrawRectangle(pen, Rectangle.Round(measureRect1));
	}
}

で、これで測れたわけですがこのままだとまともに扱えないので、以下のようなスクリプトを使って区間にします。つまり、連続してる奴はまとめるわけです。

class CC
  def initialize(encoding=nil)
    @encoding = encoding
    @cc = []
  end

  def <<(v)
    case v
    when Fixnum
      _bsearch_add2(v, v)
    when Range
      _bsearch_add2(v.first, v.last)
    when CC
      v.to_a.each_slice(2) do |from, to|
        _bsearch_add2(from, to)
      end
    else
      raise TypeError, "invalid value to add a charclass"
    end
    self
  end

  def to_s
    return to_char(@cc.first) if @encoding && @cc.size == 2 && @cc.first == @cc.last
    buf = '['
    n = @encoding ? nil : 0
    @cc.each_slice(2) do |from, to|
      buf << ',' if n && 1 < n += 1
      buf << to_char(from)
      if @encoding.nil? ||  from != to
        buf << '-' if to - from > 1
        buf << to_char(to)
      end
    end
    buf << ']'
  end

  def inspect
    to_s.inspect[1..-1]
  end

  def empty?
    @cc.empty?
  end

  private
  def to_char(v)
    #Regexp.quote(@encoding ? v.chr(@encoding) : v.to_s)
    @encoding ? '\u%04x' % v : v.to_s
  end

  def _bsearch_add2(from, to)
    pos1 = _bsearch(from)
    pos2 = from == to ? pos1 : _bsearch(to)
    if pos1.odd?
      pos1 -= 1
      from = @cc[pos1]
    elsif pos1 > 0 && @cc[pos1-1] == from - 1
      pos1 -= 2
      from = @cc[pos1]
    end
    if pos2.odd?
      to = @cc[pos2]
    elsif to + 1 == @cc[pos2] || to == @cc[pos2]
      pos2 += 1
      to = @cc[pos2]
    else
      pos2 -= 1
    end
    @cc[pos1, pos2-pos1+1] = from, to
  end
end

cc = CC.new(Encoding::UTF_8)
IO.read('msgothic.txt').scan(/(\w+): (\d+)/) do |x, y|
  a << x.to_i(16) if y.to_i == 22
end
p cc

で、出来た全角文字にマッチする正規表現は以下の通りになります。うーん、なんか違う気がするのが混じってますね。やっぱり OpenType 直接読みに行かないとダメかなぁ。

/[\u0001-\u0008\u000b\u000c\u000e-\u001b\u007f-\u0083\u0086-\u009f\u00a7\u00a8
  \u00b0\u00b1\u00b4\u00b6\u00d7\u00f7\u0180-\u0191\u0194-\u01c1\u01c3-\u01f7
  \u0200-\u024f\u02a9-\u02c5\u02ca\u02cb\u02cd-\u02cf\u02d2-\u02d7\u02df-\u02e4
  \u02ea-\u02ff\u0305\u0307\u0309\u030a\u030d\u030e\u0310-\u0317\u031b
  \u0321-\u0323\u0326-\u0328\u032b\u032d\u032e\u0331-\u0333\u0335-\u0338
  \u033e-\u0360\u0362-\u0373\u0376-\u0379\u037b-\u037d\u037f-\u0383\u038b\u038d
  \u0391-\u03a9\u03b1-\u03c1\u03c3-\u03c9\u03cf-\u0401\u040d\u0410-\u0451\u045d
  \u0487-\u048f\u04c5\u04c6\u04c9\u04ca\u04cd-\u04cf\u04ec\u04ed\u04f6\u04f7
  \u04fa-\u070e\u0710-\u09f1\u09f4-\u0e3e\u0e40-\u17da\u17dc-\u180a\u1810-\u1e3d
  \u1e40-\u1e7f\u1e86-\u1ef1\u1ef4-\u1f6f\u1f74-\u1fff\u2001\u2010\u2011\u2015\u2016
  \u2018\u2019\u201c\u201d\u2020\u2021\u2025\u2026\u202f\u2030\u2032\u2033\u203b
  \u204a-\u2069\u2071-\u2073\u208f-\u209f\u20b2-\u20ff\u2103\u2116\u2121\u212b
  \u2139-\u2152\u2160-\u2193\u21d2\u21d4\u21eb-\u2200\u2202\u2203\u2207\u2208
  \u220b\u2211\u221a\u221d-\u2220\u2225\u2227-\u222c\u222e\u2234\u2235\u223d
  \u2252\u2260\u2261\u2266\u2267\u226a\u226b\u2282\u2283\u2286\u2287\u22a5\u22bf
  \u22f2-\u2301\u2303\u2304\u2307-\u230f\u2311-\u231f\u2322-\u2503\u250c\u250f
  \u2510\u2513\u2514\u2517\u2518\u251b-\u251d\u2520\u2523-\u2525\u2528\u252b\u252c
  \u252f\u2530\u2533\u2534\u2537\u2538\u253b\u253c\u253f\u2542\u254b\u2596-\u25a1
  \u25b2\u25b3\u25bc\u25bd\u25c6\u25c7\u25cb\u25ce\u25cf\u25ef-\u2638\u263d-\u265f
  \u2668\u266a\u266d\u266f-\u2933\u2936-\u2984\u2987-\u29f9\u29fc-\u2fff\u3001-\ud7ff
  \ue000-\ufb00\ufb03-\ufefe\uff00-\uff60\uffa0-\uffe7\uffef-\ufffe]/x