TTXを使用してTrueTypeフォントのcmapテーブルを修正

IPAゴシック」フォントの「饅」(U+9945) 「頭」(U+982D)を、「○」(U+25CB)で表示するためのTTX使用方法を説明。

[話者] 『さいきん「饅頭(まんじゅう)」の「饅」「頭」の2文字を見るだけで、顔が青ざめて‥‥』という人がいるとする。

[合いの手] それは大変だね(棒)

[話者]IPAゴシック」というフォントの「饅」(U+9945) 「頭」(U+982D)を、「○」(U+25CB)で表示されるようにしよう。

FontForgeIPAゴシックの個々の字形を修正してもよいのだが http://itouhiro.hatenablog.com/entry/20140910/font で書いたようにFontForgeで読み書きするとフォントがおかしくなることがある。

ここはFontForgeでフォントを書き出すのではなく、TTX/FontToolsでフォントのcmapテーブルを修正する方法を説明する。

[合いの手] 「饅」はわかるけど、 U+9945 は何? どこで調べるの?

[話者] U+9945 というのはUnicodeのコードポイントだよ。文字番号。

Unicodeでは、文字集合中の文字をあらわす符号位置(コードポイント、符号点を参照)に、「Unicodeスカラ値」という非負整数値が割り振られている。Unicodeスカラ値は "U+" の後に十六進法でその値を続けることで表す。

引用元: http://ja.wikipedia.org/wiki/Unicode

テキストエディターのMery なら文字の前にカーソルもってくればUnicode値を表示する。

f:id:itouhiro:20141004145225p:plain

それ以外でも、「饅 Unicode」でGoogle検索で検索すれば分かる。

グリフ名

まずグリフ(字形)名を見てみよう。この「IPAゴシック」の場合 「饅」(U+9945)のグリフ名は aj7220 だな。FontForgeだと[エレメント-グリフ情報-Glyph Name]で確認できるぞ。

f:id:itouhiro:20141004112014p:plain

同様に「頭」(U+982D)のグリフ名は aj3204

f:id:itouhiro:20141004112144p:plain

「○」(U+25CB)のグリフ名は aj723

f:id:itouhiro:20141004112945p:plain

[合いの手] グリフ(字形)ごとに名前がついているのか。

[話者] このグリフ名はフォントごとに異なる。 たとえばNasuフォントだと「饅」(U+9945)のグリフ名は cid44662 だったりする。

cmapテーブルを取り出し書き換える

TTXを使い、cmapテーブルを取り出す。 TTXの導入方法は http://itouhiro.hatenablog.com/entry/20140910/font 参照。

$ ttx -t cmap ipag.ttf
Dumping "ipag.ttf" to "ipag.ttx"...
Dumping 'cmap' table...
$

ipag.ttxの中身はこんな感じ。

      ...
      <map code="0x25cb" name="aj723"/><!-- WHITE CIRCLE -->
      ...
      <map code="0x9945" name="aj7220"/><!-- CJK UNIFIED IDEOGRAPH-9945 -->
      ...
      <map code="0x982d" name="aj3204"/><!-- CJK UNIFIED IDEOGRAPH-982D -->
      ...

f:id:itouhiro:20141004114039p:plain

[合いの手] 確かに U+9945 のnameは aj7220 になっているね。

[話者] このcmapテーブル定義を以下のように書き換える。

      ...
      <map code="0x25cb" name="aj723"/><!-- WHITE CIRCLE -->
      ...
      <map code="0x9945" name="aj723"/><!-- CJK UNIFIED IDEOGRAPH-9945 -->
      ...
      <map code="0x982d" name="aj723"/><!-- CJK UNIFIED IDEOGRAPH-982D -->
      ...

[合いの手] 「饅」(U+9945) 「頭」(U+982D)のnameを aj723 に変更したのか。

[話者] ここで注意だ。

      <map code="0x9945" name="aj7220"/><!-- CJK UNIFIED IDEOGRAPH-9945 -->

の行、

      <map code="0x982d" name="aj3204"/><!-- CJK UNIFIED IDEOGRAPH-982D -->

の行は「複数」ある。すべて書き換えなくてはならないので、テキストエディターの「すべて置換」などを使おう。

[合いの手] 一つじゃないんだ?

[話者] このフォントの場合だと、cmapのformatとplatformIDで以下の3つがあって、

    <cmap_format_4 platformID="0" platEncID="3" language="0"> ..  </cmap_format_4>

    <cmap_format_4 platformID="3" platEncID="1" language="0"> ..  </cmap_format_4>

    <cmap_format_12 platformID="3" platEncID="10" format="12" reserved="0" length="124000" language="0" nGroups="10332"> .. </cmap_format_4>

それぞれが

      <map code="0x9945" name="aj7220"/><!-- CJK UNIFIED IDEOGRAPH-9945 -->

を含んでいるんだ。 だからそれを全部書き換える必要がある。

cmapのformatについては http://vanillasky-room.cocolog-nifty.com/blog/2008/02/opentype6cmap-d.html に説明あり。

フォーマット4は2バイトエンコーディングフォーマットである。フォーマット8、10、12は4バイトエンコーディングフォーマットである。MicrosoftWindows向けのサロゲートペアに対応したUnicodeフォントを作成する時、フォーマット4とフォーマット12を組み合わせて使うように推奨している。

cmapのplatformIDについては http://vanillasky-room.cocolog-nifty.com/blog/2008/02/opentype3name-a.html に説明あり。

プラットフォームIDプラットフォーム名
0Unicode
1Macintosh
2ISO
3Microsoft
4カスタム

[合いの手] ふうん。

cmapテーブルを置き換えたTrueTypeフォントを生成

[話者] そしてttxファイル名をipag.ttxからipag_new.ttxにでも変えておく。 そして新しいttfファイルを生成。

$ ttx -m ipag.ttf ipag_new.ttx
Compiling "ipag_new.ttx" to "ipag_new.ttf"...
Parsing 'cmap' table...
$

f:id:itouhiro:20141004122046p:plain

f:id:itouhiro:20141004122055p:plain

[話者] 「○」(U+25CB)の表示も「○」のままだ。 つまり、以下の3つの文字は、同じ字形で表示されることになる。

  • 「饅」(U+9945)
  • 「頭」(U+982D)
  • 「○」(U+25CB)

[合いの手] 確かに表示は変わったね。しかし特定の文字を見たくないという個人の些細な感情をこんな技術で満たしてよいのだろうか。

スクリプト

[話者] node.jsのスクリプトにした。

使い方は以下。WindowsのPortableGit(msysgit) https://github.com/msysgit/msysgit/releases 環境で実行している。

rm *.ttx *_new*; node cmapReplace.js foo.ttf

cmapReplace.js

var fs = require('fs');
var exec = require('child_process').exec;

if (process.argv.length != 3){
  console.log('usage: rm *.ttx *_new*; node cmapReplace.js foo.ttf');
  process.exit(0);
}

var orgFontFileName = process.argv[2];
exec('ttx -t cmap '+ orgFontFileName, function(err, stdoutTxt, stderrTxt){
  console.log('ttx -t cmap '+ orgFontFileName);
  console.log(stdoutTxt+stderrTxt);
  var orgTtxName = orgFontFileName.replace(/\.ttf/i, '.ttx');

  var newTtxName = replaceCmap(orgTtxName);

  exec('ttx -m '+ orgFontFileName + ' ' + newTtxName, function(err, stdoutTxt, stderrTxt){
    console.log('ttx -m '+ orgFontFileName + ' ' + newTtxName);
    console.log(stdoutTxt+stderrTxt);
  });
});


function replaceCmap(orgFileName){
  var fileContent = fs.readFileSync(orgFileName) + '';
  fileContent = fileContent.replace(/\r/g, '').replace(/\uFEFF/g, '').replace(/\n*$/, '');
  var lines = fileContent.split(/\n/);

  var ngChars = '0x9945 0x982d'.split(' '); //「饅」「頭」
  var toBeReplaced = '0x25cb'; //「○」

  var toBeGlyphName = getGlyphName(toBeReplaced, lines);

  for(var i=0; i<ngChars.length; i++){
    var ngGlyphName = getGlyphName(ngChars[i], lines);
    fileContent = fileContent.replace(new RegExp(ngGlyphName+'"','g'), toBeGlyphName+'"')
  }

  var newFileName = orgFileName.replace('\.ttx','_new.ttx');
  fs.writeFileSync(newFileName, fileContent);
  console.log('(replace): ' + orgFileName + ' -> ' + newFileName);
  return newFileName;
}

function getGlyphName(theGlyph, lines){
  var m;
  var n;
  for (var i=0; i<lines.length; i++){
    if (lines[i].match(theGlyph)){
      if (m = lines[i].match('name="([^\"]+)"/>')){
        return m[1];
      }
    }
  }
  console.log('error: '+theGlyph+' not found.');
  process.exit(-1);
}