Apple Color Emojiフォントを見本になにかする その1
何気なくつぶやいたら思いの外反響のあった絵文字フォントの件なのですが、一足飛びにsbixだけを解説してお茶を濁すのも面白くありません。ので、全体構造から始まり、適宜必要なツールを作りながらフォント構造を紐解いていく事にします。
まず、オープンタイプっていうのは色々な規格のデータを寄せ集めてまとめたものです。構造を見てみると本当にひどいものです。旧来あったものを大企業が主に利害をぶつけ合った結果出来上がったもの(暴言w)です。ですから、OpenTypeの構造は旧来のTrueTypeとPostscriptのフォント構造をラップするような仕様になっています。
こんな状況変だよね〜って思ったのかどうかはわかりませんが、ここへ来てOpenTypeが内包する無駄な部分を調整する動きが出てきています。これは主に新しいカラーフォントやバリアブルフォントを効率良く実装する為の変更だったりします。まあ、当然従来からのフォントに対しても利用可能です。しかし、フォーマットの定義としては正しいものができるでしょうが既存のアプリケーションが正しく扱えるかどうかはまた別のお話です。そこらへんが今ひとつよくわからなかったりするのですが、そのへんは追々ということでよろしいかと思うのです。興味深いのはCFFまわりというかCFFの後継フォーマットのCFF2テーブルというのが追加されたことです。CFFというフォーマットはAdobeが作ったもので、Type1フォントの3次のベジェ曲線をサポートするためのコマンド群を含みます。当然AppleやMSは全くノータッチで、ドキュメントすら「Adobeの読んだら?」って状態でした。ところが、このCFF2テーブルに関してはMSからドキュメントがリリースされています。そして、中身はというと従来のType2Stringコマンドをばっさりと切り捨ててコマンドがスカスカの状態になっています。なぜこうまでしてCFFを使うのかと言いますと、このテーブルはかなり圧縮が効きます。Pr6などのOTFフォントがあの容量で収まるのはこのCFFのご利益といえるでしょう。そして、展開もそう複雑な処理になりませんので負荷が軽くて済みます。このへんのところというのは1990年代の非力なマシンでの利用を想定していたPostscriptフォントの特性を残しつつといったところでしょう。でも、パース処理は煩雑になります。やんなっちゃうw この辺に関しては、そのうちご説明できる機会があるかもしれません。
大幅に脱線しました。絵文字フォントに関してですが、高解像度ビットマップをグリフとして扱うものとSVGを利用したベクターベースのものがあります。今回はApple Color Emojiフォントを紐解く為のものですからビットマップベースのフォントを見ていくことになります。
まずフォントの基本構造です。以下を一読してからお戻りください。
http://www.microsoft.com/typography/otspec/otff.htm
フォントというものはテーブルと呼ばれる幾つかのデータの塊に分けられて保持されています。アウトラインを入れてあるテーブルだとか文字コードとアウトラインデータを紐付けする為のテーブルだとかいう具合です。それを順番に並べるだけでは何を読めば良いのかわかりません。当然どのテーブルはどこからどこまでという感じでアドレスが書かれています。そして、フォントファイル自体は純粋なバイナリデータの塊です。これをフォーマットに応じて1〜4バイトで読み出して適切な処理を行う必要があります。
まず一番最初に読み出す必要のあるテーブル「Offset Table」を読み出してみます。このテーブルは特殊なテーブルで、どの様なフォントであれ一番最初に置かれているテーブルです。大きさも12バイトの固定長で、フォントの種類や内包するテーブル数といった重要な情報が記述されています。
こちらはApple Color Emojiフォントをバイナリエディタでのぞいてみたものです。先頭の12バイトを選択しハイライトさせています。これをお約束に従って読み出したものが以下です。
Offset Table sfnt version(fixed32bit):0x00010000 Number of Tables(unsigned16bit):0x0010 search range (unsigned short(16bit)):0x0100 entry selector (unsigned short(16bit)):0x0004 range shift (unsigned short(16bit)):0x00F0
sfntはフォントの種類を識別する為のものです。これは4バイトで構成され、fixedデータで記述されたバージョン情報である場合はTruTypeフォント(上記の場合はメジャーバージョンが上位2バイトマイナーバージョンが下位2バイトとなるので1.0となります。)、この4バイトが4文字のアスキー文字列である「OTTO(0x4F54544F)」ならCFFテーブルを持つOpenTypeフォントとなります。この事からApple Color EmojiフォントはTrueTypeフォントだと言えます。
続く2バイトはフォントに含まれる総テーブル数で、もちろん16進数表記ですからこの場合のテーブル数は16コです。
SearchRange、EntrySelector、RangeShiftはそれぞれ2バイトの整数となっております。でも……とりあえずこの3つは今必要ありません。今必要なのはこのフォントがどの様な種類のフォントでテーブルをいくつ持つかということです。
さて、オフセットテーブルに続くのはテーブル・レコード・エントリと呼ばれる1セットが4バイト×4個で構成されたエントリがテーブルの数だけ並べられています。
このエントリの構造は
Tag( 4バイト unsigned) identifier(識別子) check sum(4バイト unsigned)テーブル・チェックサム offset(4バイト unsigned)フォントファイル先頭からのオフセットアドレス。規格上はoffset32と表記される。 length(4バイト unsigned)テーブル長
この様になっています。Tagが各テーブルにつけられた名前でAscii4文字で構成されます。チェックサムは後日解説します。
このフォントは16個のテーブルを持ちますからこのテーブル・レコード・エントリは16個並べられているという事になります。
はい、本日の解説はここまでです。
このApple Color EmojiフォントはTruTypeフォーマットで16個のテーブルを持つ事がわかったところで本日のコーディングです。基本的にDTP界隈の方が多いし、皆さんAdobeのアプリケーション使っていると思うのでプログラミングにはExtendscriptを利用します。要するにESTKを使ってフォントを操作していこうということです。
では、先に目を通してきてねって言ったページ覚えていますか? フォントファイルというのはお作法に則って読み出す必要がありますので、その為の関数群を準備します。
//readBin.jsxinc
function readLongDt(targetFile){
var result=0;
var a = 72057594037927936;
for(var k=0;k<8;k++){
result += targetFile.readch().charCodeAt(0) * a;
a /= 256;
}
return result;
}
function readULong(targetFile){
var result=0;
var a = 16777216;
for(var k=0;k<4;k++){
result += targetFile.readch().charCodeAt(0) * a;
a /= 256;
}
return result;
}
function readLong(targetFile){
var result=0;
var a = 16777216;
for(var k=0;k<4;k++){
result += targetFile.readch().charCodeAt(0) * a;
a /= 256;
}
if (result>2147483648) result -= 4294967296;
return result;
}
function readUShort(targetFile){
var result = targetFile.readch().charCodeAt(0) * 256;
result += targetFile.readch().charCodeAt(0);
return result;
}
function readShort(targetFile){
var d = targetFile.readch().charCodeAt(0);
var result = d << 8;
result += targetFile.readch().charCodeAt(0);
if (result>32768) result -= 65536;
return result;
}
function readFixed(targetFile){
var result = "0x";
var d;
for (var i=0;i<4;i++){
d = targetFile.readch().charCodeAt(0).toString (16);
if (d.length==1) d = "0" + d;
result += d;
}
return result;
}
function readFxd16(targetFile){
var result = readShort(targetFile) + ".";
result += readShort(targetFile);
return Number(result);
}
function readFxd2d14(targetFile){
var result = "";
var d = readShort(targetFile);
result = d >> 14;
if (result>2) result -= 4;
result += (d & 0x3fff) / 16384;
return result;
}
function readUnit24(targetFile){
var result = targetFile.readch().charCodeAt(0) * 65536;
result += targetFile.readch().charCodeAt(0) * 256;
result += targetFile.readch().charCodeAt(0);
return result
}
function winBMP(tg,ofst,len){
var byt = 0;
var st = "";
for (var i=0;i<len/2;i++){
byt = tg.readch().charCodeAt(0) * 256;
st += String.fromCharCode(byt+tg.readch().charCodeAt(0));
}
return st;
}
function readBytes(targetFile){
var a = targetFile.readch().charCodeAt (0);
var dat = a << 8;
a = targetFile.readch().charCodeAt (0);
dat += a;
return dat
}
それぞれの関数は与えられたファイルオブジェクトのポインタ位置から特定長のデータを読み込み必要ならば加工して返すという単純なものです。この関数群でフォントのバイナリデータは全て読み込むことができます。
掲載するだけというのもアレなのでひとつだけご説明を
function readUShort(targetFile){
var result = targetFile.readch().charCodeAt(0) * 256;
result += targetFile.readch().charCodeAt(0);
return result;
}
Extendscriptのファイルオブジェクトはバイナリデータをそのまま扱うのに都合の良いバイナリモードが用意されています。これらの関数群はすべて「encodingプロパティを”BINARY’に設定した状態での利用を前提」としている事にご注意ください。このバイナリモードでは1バイトのデータを1文字として読み込む事が可能です。ということでreadchメソッドで1文字読み込むと1バイト取り込むことができるのです。しかし、読み込みそのものは文字として行われますのでそれを正しい数値として取り込む為にキャラクタコードに変換する必要があります。ということで上位1バイト読み込んでコードに変換し8ビット分の位合わせに256を掛けます。そこへ下位バイトを読み込んで足してやると2バイトの符号なしデータの出来上がりとなります。
さて、ここからが本番です。
//font_cutter.jsx
#include readBin.jsxinc
var ot = []; //Offset Table
var dat = [];
var bt = [];
var nwflag = false;
var fn = File.openDialog ("Select target FONT.");
var fnm = String(fn).match(/\/([^\/]+?)\.[a-zA-Z]+$/);
var currentPath = "~/desktop/" + RegExp.$1;
var currentFolder = new Folder (currentPath);
if (!currentFolder.exists) {
currentFolder.create();
nwflag = true;
}
var f = new File(fn); //Global, Font File Instance.
f.encoding = 'BINARY';
if (f.open("r")){
ot = readOfstTbl();
var w = new Window('palette', 'Offset Table', undefined);
w.add('statictext', undefined, 'sfnt ' + ot[0]);
w.add('statictext', undefined, 'table length ' + ot[1]);
w.add('statictext', undefined, 'search range ' + ot[2]);
w.add('statictext', undefined, 'entry selector ' + ot[3]);
w.add('statictext', undefined, 'range shift ' + ot[4]);
var clsBtn = w.add('button', undefined, 'close',{name:'cancel'});
clsBtn.onClick = function(){w.close()};
w.show();
var tw = new Window('palette', 'Table Directory', undefined);
var clstw = tw.add('button', undefined, 'close', {name:'cancel'});
readTD (ot[1]);
clstw.onClick = function(){tw.close()};
tw.show();
if (nwflag)
for (j=0;j<dat.length;j++)
saveTables(dat[j][0], dat[j][2], dat[j][3]);
}
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] = readULong(f);
dat[j][4] = "0x"+dat[j][1].toString(16);
dat[j][2] = readULong(f);
dat[j][5] = "0x"+dat[j][2].toString(16);
dat[j][3] = readULong(f);
dat[j][6] = "0x"+dat[j][3].toString(16);
$.writeln(dat[j][4] + " " + dat[j][5] + " " + dat[j][6]);
tw.add('statictext',undefined,
dat[j][0]+" "+dat[j][4]+" "+dat[j][5]+" "+dat[j][6]);
if (dat[j][0]=="head"&&!nwflag) nwflag = verifyVersion(dat[j]);
}
return dat;
}
function verifyVersion(dat){
var currentPoint = f.tell();
var existFile = new File(currentFolder + "/head.table");
if (existFile.open('r')){
var eVer = readFixed(existFile);
var eRev = readFixed(existFile);
}
else {
return true;
}
existFile.close();
f.seek(dat[2]);
var targetVer = readFixed(f);
if (targetVer!=eVer){
f.seek(currentPoint);
return true;
}
var targetRev = readFixed(f);
if (targetRev!=eRev){
f.seek(currentPoint);
return true;
}
f.seek(currentPoint);
return false;
}
function readOfstTbl(){
var dt = ["",0,0,0,0];
var d;
var a = f.readch();
if (a=="O"){
dt[0] = a;
for (i=0;i<3;i++) {
dt[0] += f.readch();
}
}
else {
dt[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;
dt[0] += d;
}
}
dt[1] = readBytes(f);
dt[2] = readBytes(f);
dt[3] = readBytes(f);
dt[4] = readBytes(f);
return dt;
}
function readFClass(targetFile){
var rslt = new Array();
rslt[0] = targetFile.readch().charCodeAt(0);
rslt[1] = targetFile.readch().charCodeAt(0);
return rslt;
}
function getURArray(ur,range){
var rslt = new Array;
var d = 2147483648;
for (var i=31;i>-1;i--){
if (ur>=d){
rslt.unshift(range[i]);
ur -= d;
}
d /= 2;
}
return rslt.join ("\n");
}
function getOTinf(num){
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;
}
function saveTables(nam, offset, len) {
if (nam=="OS/2") nam = "OS_2";
var fn = new File(currentPath + '/' + nam + ".table");
fn.encoding = 'BINARY';
f.seek(offset);
fn.open("w");
var chr = f.readch().charCodeAt(0);
fn.write(String.fromCharCode (chr));
fn.close();
fn.open("a");
for (var i=1;i<len;i++){
chr = f.readch().charCodeAt(0);
fn.write (String.fromCharCode(chr));
}
fn.close();
}
これは指定したフォントのオフセットテーブルとテーブル・レコード・エントリを解析してくれるスクリプトです。更に今後扱いやすいようにフォントから各テーブルを抜き出して保存してくれるという親切設計です。保存先を変更したい場合はcurrentPathを書き換えてください。絵文字フォントの場合容量が大きいので処理に時間がかかりますのでご注意を。ダウンロードは下記から。両方共同じフォルダに入れておいてください。
実行すると…
このようなオフセットテーブル情報と
テーブル・レコード・エントリの値を列記したパレットが表示されます。そのバックグランドではせっせとフォントを分解する作業が進行しているわけですが…
コード自体はコピペでも良いのですが以下のgitからも落とせます。
https://github.com/ten-A/Extend_Script_experimentals/blob/master/font_cutter.jsx
https://github.com/ten-A/Extend_Script_experimentals/blob/master/readBin.jsxinc
ではまたの機会まで。アディオ〜ス(^-^)/