Opentypeフォントの構造

先日、むすめとしまじろうを見に行ってきました。トリッピーが予想以上に丸っこくて、足元見えないよねぇ〜なんて思ってましたが、やはり階段を踏み外して転びかけてました。それもそうなんですが、トリッピーの右足の裏になんだかテープが貼ってあってそれが気になって気になって、踊っているオネーサン見るどころではありませんでしたw
まあ、そんな事を書きたかったのではありません。先日MITの中の人がやっちゃったのでどうしようか迷っていましたが、ある程度プロジェクトが進んでいるので放棄するのももったいないですから取りあえずやろうと思う次第です。
オープンタイプっていうのは色々な規格のデータを寄せ集めてまとめただけあって構造的にもそういった雰囲気を醸し出すものです。おおざっぱに構造について言うと幾つかの必須テーブルとtrueTypeやCFFに関連するテーブル等が定義されています。この各種テーブルを参照しつつ各種データにアクセスしながらグリフを拾う感じですが、まずはその辺を適当に解説していこうと思います。最終的に何につながるかは見てのお楽しみw

まずOpentypeFontをいじり回す為にはある程度の知識が必要となりますが、これらの情報は全て無料で読めます。但し、日本語ではありません。まず読むべき物はこちらhttp://www.microsoft.com/typography/otspec/otff.htmとっかかりとしてはここら辺がよろしそうです。

構造について
まずはファイル先頭から12byte目までのOffset Tableから始まります。これはフォントの種類や内包するテーブルの数といった基本となる情報が記述されています。

Offset Table
sfnt version (Fixed(32bit))
truetypeは0x10000(versionNum)Postscript系はOTTOで始まる。

number of tables (unsigned short(16bit))
16ビットでテーブル数が記述される。

search range (unsigned short(16bit))
entry selector (unsigned short(16bit))
range shift (unsigned short(16bit))
こちらの3種類も16bitで記述される。

ではバイナリエディタを利用して見てみます。
Truetype outline (Arial.ttf)

オフセット0から12byte抜き出したのがこちら。

00 01 00 00 00 17 01 00 00 04 00 70

0x00010000 version 1.0
0x0017 number of tables = 23
0x0100 search range
0x0004 entry selector
0x0070 range shift

もう一つ見てみましょう。
CFF outline(Avenir-Roman)

こちらも冒頭12byteを見てみましょう。

4F 54 54 4F 00 0A 00 80 00 03 00 20

4F 54 54 4F OTTO
0x00A number of tables = 10
0x0080 search range
0x0003 entry selector
0x0020 range shift

こちらはOTTOとなります。テーブルの数がけっこう違うのがおわかり頂けると思います。含まれるグリフの数等のからみで単純に比較は出来ませんが、基本的に要求されるテーブル数はTruetype/5、CFF/2とPostscript系の方が少なくなります。
searchRang、entrySlelector及びrangeShiftはテーブル数から計算されます。詳細は割愛しますが、以下のJavascript計算ファンクションを参考にして下さい。

function getOTinf(num/*number of tables*/){
    var result = [0,0,0];
    var n = 2;
    var ctr = 0;
    while (n<num){
        n *= 2;
        ctr++;
        }
    result[0] = n / 2*16;
    result[1] = ctr–;
    result[2] = num * 16 – result[0];
    return result;
    }<num){ n="" *="2;" ctr++;="" }="" result[0]="n" 2*16;="" result[1]="ctr–;" result[2]="num" 16="" -="" result[0];="" return="" result;="" }

テーブル数を引数に渡すと、上記3つの情報がArrayで返されます。

続いてtableDirectoryを見てみます。テーブルディレクトリは13byte目から始まり、一つのテーブルについて4byte×4=16byteで構成されます。

tag unsigned long (32bit identifier)
冒頭4byteはテーブル名で4文字のascii形式の識別子です。

checkSum unsigned long(32bit)
次はチェックサム。これは後日解説します。

offset unsigned long(32bit)
テーブルの開始位置

length unsigned long(32bit)
テーブルデータの長さ

となっております。

以下はArial.ttfの13byte目から28byte目までを抜き出した物です。

44 53 49 47 24 3D F9 E7 00 05 7F 8C 00 00 1A 7C

44 53 49 47 DSIG
24 3D F9 E7 check sum
00 05 7F 8C offset
00 00 1A 7C length

このようにDISGテーブルについて記述されているわけです。
しかしながら、バイナリエディタに頼るのも少々面倒な話ですので身近なツールでこねくり回す事を考えないと、なかなか前に進みません。そこでESTKを利用します。

var ot = new Array();
var dat = new Array();
var bt = new Object();

var fn = File.openDialog (“target”);
var f = new File(fn);
f.encoding = ‘BINARY’;
if (f.open(“r”)){
    ot = readOfstTbl();
    var w = new Window(‘palette’, ‘Offset Table’, [100,100,300,235]);
    w.add(‘statictext’,[5,5,180,20],’sfnt              ‘+ot[0]);
    w.add(‘statictext’,[5,25,180,40],’table length  ‘+ot[1]);
    w.add(‘statictext’,[5,45,180,60],’search range  ‘+ot[2]);
    w.add(‘statictext’,[5,65,180,80],’entry selector  ‘+ot[3]);
    w.add(‘statictext’,[5,85,180,100],’range shift  ‘+ot[4]);
    var clsBtn = w.add(‘button’,[130,105,190,120],’close’,{name:’cancel’});
    clsBtn.onClick = function(){w.close()};
    w.show();
    var tw = new Window(‘palette’,’Table Directory’,[150,120,500,24*ot[1]+160]);
    var clstw = tw.add(‘button’,[10,ot[1]*24+10,80,ot[1]*24+25],’close’,{name:’cancel’});
    readTD (ot[1]);
    clstw.onClick = function(){tw.close()};
    tw.show();
    }

function readTD(num){
    var a=0;
    for (j=0;j<num;j++){
        dat[j] = [“”,0,0,0,””,””,””];
        for (i=0;i<4;i++) dat[j][0] += f.readch();
        $.write(dat[j][0]+”  “);
        dat[j][1] += f.readch().charCodeAt (0) *16777216;
        dat[j][1] += f.readch().charCodeAt (0) *65536;
        dat[j][1] += f.readch().charCodeAt (0) *256;
        dat[j][1] += f.readch().charCodeAt (0) ;
        dat[j][4] = “0x”+dat[j][1].toString(16);
        dat[j][2] += f.readch().charCodeAt (0) *16777216;
        dat[j][2] += f.readch().charCodeAt (0) *65536;
        dat[j][2] += f.readch().charCodeAt (0) *256;
        dat[j][2] += f.readch().charCodeAt (0) ;
        dat[j][5] = “0x”+dat[j][2].toString(16);
        dat[j][3] += f.readch().charCodeAt (0) *16777216;
        dat[j][3] += f.readch().charCodeAt (0) *65536;
        dat[j][3] += f.readch().charCodeAt (0) *256;
        dat[j][3] += f.readch().charCodeAt (0) ;
        dat[j][6] = “0x”+dat[j][3].toString(16);
        $.write(dat[j][4]+”  “+dat[j][5]+”  “+dat[j][6]+”\n”);
        tw.add(‘statictext’,[5,5+24*j,300,5+24*(j+1)],dat[j][0]+”  “+dat[j][4]+”  “+dat[j][5]+”  “+dat[j][6]);
        bt[j] = tw.add(‘button’,[250,24*j,300,5+24*(j+1)-15],dat[j][0],{name:dat[j][0]});
        bt[j].dat = [dat[j][0],dat[j][4],dat[j][5],dat[j][6]];
        bt[j].onClick = function(){tbFnc(this.dat)};
        }
    return dat;
    }

function tbFnc(td){
    alert(td[0]+” “+td[1]+” “+td[2]+” “+td[3]);
    }

function readOfstTbl(){
    var dat = [“”,0,0,0,0];
    var d;
    var a = f.readch();
    if (a==”O”){
        dat[0] = a;
        for (i=0;i<3;i++) {
            dat[0] += f.readch();
            }
        } else {
            dat[0] = “0x0” + a.charCodeAt(0).toString (16);
            for (i=0;i<3;i++) {
                a = f.readch();
                d = a.charCodeAt(0).toString (16);
                if (d.length==1) d = “0” + d;
                dat[0] += d;
                }
            }
    dat[1] = readBytes();
    dat[2] = readBytes();
    dat[3] = readBytes();
    dat[4] = readBytes();
    return dat;
    }

function readBytes(){
    var a = f.readch().charCodeAt (0);
    var dat = a << 8;
    a = f.readch().charCodeAt (0);
    dat += a;
    return dat
    }<num;j++){ dat[j]="["",0,0,0,"","",""];" for="" (i="0;i<4;i++)" dat[j][0]="" +="f.readch();" $.write(dat[j][0]+"="" ");="" dat[j][1]="" (0)="" *16777216;="" *65536;="" *256;="" ;="" dat[j][4]="0x" +dat[j][1].tostring(16);="" dat[j][2]="" dat[j][5]="0x" +dat[j][2].tostring(16);="" dat[j][3]="" dat[j][6]="0x" +dat[j][3].tostring(16);="" $.write(dat[j][4]+"="" "+dat[j][5]+"="" "+dat[j][6]+"\n");="" tw.add('statictext',[5,5+24*j,300,5+24*(j+1)],dat[j][0]+"="" "+dat[j][4]+"="" "+dat[j][6]);="" bt[j]="tw.add('button',[250,24*j,300,5+24*(j+1)-15],dat[j][0],{name:dat[j][0]});" bt[j].dat="[dat[j][0],dat[j][4],dat[j][5],dat[j][6]];" bt[j].onclick="function(){tbFnc(this.dat)};" }="" return="" dat;="" function="" tbfnc(td){="" alert(td[0]+"="" "+td[1]+"="" "+td[2]+"="" "+td[3]);="" readofsttbl(){="" var="" dat="["",0,0,0,0];" d;="" a="f.readch();" if="" (a="="O"){" dat[0]="a;" {="" else="" a.charcodeat(0).tostring="" (16);="" d="a.charCodeAt(0).toString" (d.length="=1)" dat[1]="readBytes();" dat[2]="readBytes();" dat[3]="readBytes();" dat[4]="readBytes();" readbytes(){="" (0);="" <<="" 8;="" }

こちらがヘッダ解析ツールになります。まあ、フォント解析ツールになる予定の物ですから今後色々実装する事になります。
動作見本はこちら。

参考までに、こちらはその他のフォントにて解析した結果です。

*GaramondPremrPro.otf
BASE 0x3f624fba 0x563e4 0x34
CFF 0xcf1ad5e6 0x1e74 0x3bb09
DSIG 0x5bfb36ba 0x56418 0x1668
GPOS 0xac926f21 0x4516c 0x11278
GSUB 0xe4b32a9d 0x3fe94 0x52d6
OS/2 0x43b68239 0x140 0x60
cmap 0xc7b852e2 0x758 0x16fa
head 0xe0eaa61f 0xdc 0x36
hhea 0x8480b21 0x114 0x24
hmtx 0x725e0f43 0x3d980 0x2514
maxp 0x9455000 0x138 0x6
name 0x57246ce0 0x1a0 0x5b5
post 0xffb80032 0x1e54 0x20

ついでに日本語フォント…

*KozMinPr6N-Light
BASE 0x1b8e18d8 0x7c04d0 0xe4
CFF 0x694dcbbb 0x4093c 0x713b47
DSIG 0x99f9285d 0x7c0d5c 0x1b84
GPOS 0x25479c46 0x7b2118 0xe3b8
GSUB 0x38d14556 0x77f128 0x32fee
OS/2 0x784fa043 0x170 0x60
VORG 0x4182269d 0x7c05b4 0x7a8
cmap 0x12394338 0x9e0 0x3ff3c
head 0xf55c46d3 0x10c 0x36
hhea 0xa1c52ac 0x144 0x24
hmtx 0x10e17078 0x754484 0x153bc
maxp 0x5a125000 0x168 0x6
name 0x3daf0d05 0x1d0 0x80d
post 0xffb80032 0x4091c 0x20
vhea 0x8f263ac 0x769840 0x24
vmtx 0x71b19578 0x769864 0x158c4

もちろんCJKフォントにおいても構成は同じですのでこのように調査が可能です。
次回からこの読み込んでみたテーブルについて必須の物から順番に解説してみたいと思います。

ten_a

Graphic Designer, Scripter and Coder. Adobe Community Professional.

シェアする