POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

FeedlyRSSTwitterFacebook
Leah Hanson

本記事は、原著者の許諾のもとに翻訳・掲載しております。

Haiku では、オリジナルのベクター画像形式でアイコンを保存しています。これは「大半のOSでは、アイコンはビットマップで表示すれば十分だと考えられている」「ベクター画像にはSVGのような形式が他にもたくさん存在している」という2つの理由から考えて、何とも驚くべきことです。Haikuのベクターアイコン形式(HVIF)は、ベクターアイコンのファイルサイズをできる限り縮小する目的で開発されたものです。そのため、Haiku ではファイルサイズをinode(ファイルのメタデータの内部)に十分収まる容量に抑えつつ、複数のサイズでアイコンを表示することができます。さらにアイコンをメタデータ内で保持することで、フォルダ表示に必要なディスク読み出し回数が減り、各ファイルを一度のディスク読み出しで表示できるようになっています。

この記事ではバイナリエディタとカノニカルパーサーのソースコードを使用して、HVIF形式について詳しく調査します。また、サンプルのアイコンを分析する過程で、アイコン画像エディタにおける最適化の問題にも触れます。

アイコンにまつわる問題

HVIFはフォルダ内に表示されるファイルアイコンに特化した作りになっています。例えばデスクトップ(これも一種の特殊なフォルダ)に存在するファイルなどが対象です。各ファイルには、ファイル種別に応じたアイコンが紐づけられています。OSによっては、PDFのサムネイルや一般的な画像タイプを使用しているものもありますが、HVIFではファイルのプレビューではなく、象徴的で抽象的なアイコンのみを対象にしています。

一般的に、アイコンはビットマップで表示されます。BeOSのアプローチはいたって標準的で、2種類のアイコンサイズ( 16x16 32x32 ピクセル)を使用して、各アイコンを2つビットマップで表示していました(1つのアイコンに対して各サイズのビットマップが存在)。ビットマップはピクセル色の配列であり、特定のピクセルの「意味」を表すメタデータは含まれていません。よってサイズを変更すると、無条件に形状が崩れてしまいます。一般的には歪みが発生する、画像がぼやける、ブロックノイズが発生するなどの問題が生じます。アイコンの汎用サイズは数が限られているため、各サイズは独自の画像として個別に作成され、別々に保存されます。

2つのビットマップはファイルサイズが非常に小さく、 16x16 のビットマップは256バイト、 32x32 のビットマップは1024バイトなので、1つのアイコンの合計サイズは1280バイトでした。90年代以降は(BeOSの登場により)アイコンが次第に大きくなり、最新のOSでは 64x64 128x128 のアイコンが使用されています。 64x64 のアイコンを追加しただけでも、アイコン1つあたりの合計バイト数は1キロバイト強から5キロバイト強に増加します。各アイコンサイズが一気に大きくなると(各辺の長さが2倍になると)、ピクセル数や画像に必要なストレージ領域は4倍になります。

File Sizes: Bitmaps vs Vectors
(画像引用元: Icon-O-Maticのドキュメント

ベクター形式は、ビットマップに並ぶ主要な画像表示方法です。Haikuや IRIX 、さらにGnomeやKDEのような主流のLinuxデスクトップでもベクターアイコンが使用されています。ベクター画像は、線やグラデーションなどの要素に分解できる画像に最適です。つまりアイコンにおいて一般的な、抽象的なスタイルに適しています。ベクター形式の最大の特長の1つは、容易にサイズ変更ができることです。本来の形状(円形など)が分かっているため、極端に縮小や拡大をしても歪みが生じることはありません。そのため、1つのアイコンに対してファイルが1つあれば、アイコンを多様なサイズにレンダリングできます。上の画像は、同一のアイコンを3つの異なるファイル形式(ビットマップと2つのベクター形式)でレンダリングしたものです。ビットマップのサイズは 16x16 32x32 で、より大きなサイズを使用すると、それに応じてファイルサイズも非常に大きくなります。一方ベクター画像は、新しいサイズを導入してもファイルサイズは変わりません。また、ビットマップでは表示サイズごとに異なるファイルが必要になります。その理由は、 32x32 などのビットマップを拡大すると大量のブロックノイズが発生するためです。一方のベクター画像は、拡大しても元のサイズと見た目は変わりません。ただし、ベクター画像を使用するにあたっては、ビットマップに比べてレンダリング時間が長いという難点があります。基本的に、画面に表示したいサイズのビットマップにベクター画像を変換する必要があるためです。

Scaling: Bitmaps vs Vectors
(画像引用元: Icon-O-Maticのドキュメント

アイコンにベクター形式を使用すると、1つのファイルですべてのサイズを表示でき、鮮明さも保たれます。ファイル数が(3つから)1つに減れば、アイコンデザイナーの作業負荷は軽減され、ファイルサイズも削減されるでしょう。ただし、ご存じの方もいると思いますが、Web画像においては、必ずしもSVG(ベクターファイル形式)のサイズが、同じ画像をPNGやJPGなどのビットマップ形式にしたものよりも小さくなるとは限りません。ビットマップでは、画像のピクセル数やピクセルで使用可能な色数に基づいて、サイズを非常に正確に予想できます。しかしベクター画像では、ファイルサイズは画像の複雑さに左右されます。線とグラデーションをそれぞれ保存する領域が必要なので、線が複雑であるほど必要な保存領域も増えます。

サイズに関しては、HVIF画像はBeOSのビットマップに引けを取りません。実際に使用されているアイコンは、およそ500から700バイトですが、250バイト以下のものや1000バイト以上のものを作成することも可能です。このサイズは 64x64 のビットマップ画像を1枚だけ保存するよりもはるかに小さく、SVGなどの他のベクター形式に比べても小さくなっています。このようにサイズを縮小できるのは、(SVGのプレーンテキストXML形式とは異なり)サイズ縮小を目的に設計されたバイナリファイル形式を使用しているためです。このサイズ縮小設計においては、ファイルの複雑さに(256パスまでなどの)上限が設けられています。

Haikuの開発者がアイコンファイルのサイズにこだわる理由

最近のハードウェアやRAMにおいて、1キロバイトというメモリ容量は微々たるものでしょう。また1つのOSに何百万もの(または何万もの)アイコンが含まれていることは、まずありません。Haikuの開発者がサイズにこだわるのは、アイコンファイルのサイズを極力縮小することで、フォルダ内のファイルがより高速で表示されるように最適化されるからです。標準的なビットマップアイコン形式を使用する際は、それらを独自のファイルに保存し、それを使用するファイルとは分けておくのが一般的です。OSはフォルダ内の各ファイルを表示するために、ファイルのメタデータ(ファイル名と形式)を読み込んでから、そのファイル形式に対応するアイコンファイルを読み込まなくてはなりません。アイコンファイルが非常に小さければ、ファイルのメタデータと同じ場所に格納できます。そうすることで、ハードドライブからの読み出しが1回減り、メタデータとアイコンを1回の読み出しで取得できるようになります。

各ファイルで読み出しが1回減る程度では、たいした効果はないと思うかもしれません。しかし、ディスクからの読み出しは時間のかかる処理です。CPUサイクルが0.25ナノ秒なのに対し、RAMからの読み出しには約60ナノ秒かかります。比較的高速な最新のSSDでも20マイクロ秒(2万ナノ秒)、低速のディスクの場合は10ミリ秒(1000万ナノ秒)もかかります。フォルダ内のファイル表示に要する時間の内訳は、ディスクからの読み出しが大半を占めています。つまり、ビットマップに比べてベクター画像はレンダリング時間が長いとはいえ、ディスク読み出しの回数が半分になれば、パフォーマンスは格段に上がるはずなのです。

実装の詳細

ファイル形式においては、どこまでサイズを縮小できるかという点が重要であり、私はその仕組みを詳しく学びたいと思いました。Haikuのファイル形式を生み出したStephan Assmusは いくつかの記事 を書いており、その中の1つで、HVIF形式でサイズを縮小する方法の要点を詳しく説明しています。ただ、残念ながら、この記事には私の好奇心が満たされるほど詳細な内容は書かれていませんでした。そこで私は、より詳しく学ぶために、パーサーのソースコードとサンプルのHVIFファイルを(バイナリエディタで)使用することにしました。

サンプルファイル

HVIFファイルを作成編集するためのアプリケーション、Icon-O-Maticの中にファイルを1つ作りました。ぼんやりした形の上にHの字がのっています。2つのシェイプがあります。Hは白で、ブロブ(ぼんやりした部分 )の方は青から赤へのグラデーションです。 ^(1) 背景部分(64×64)は空白で透明です。Hは規定のサイズから拡大され、ほとんどのスペースを占めています。

Example Logo
このようにシンプルな検証用ロゴの利点は、サイズがとても小さいということです(128バイト)。バイナリエディタでたったの8行です。

Example Logo's Hex Source
上の画像はHaikuのバイナリエディタです。右側は、バイト配列をASCII文字に変換したものです。非表示文字や不当な文字は . で表されています。中央にファイルのバイト配列が16進数で表示されています。文字のペア(例: 6e )が1バイトで、各行は16バイトです。左には16進数で文字数を表しています(0x00、0x10、0x20はそれぞれ、10進数では0、16、32)。これらの文字数は行番号のような役割を果たし、文字数/行番号の前に来る文字をカウントしています。

普通のテキストフォーマットに比べ、人間がバイナリフォーマットを読むのは大変困難です。私はこれらのファイルをパースしたコードを読んでこのファイルを解読しました。このファイルは、各セクションにスタイル、パス、そしてシェイプを持っています。シェイプは、スタイルと1つ以上のパスの組み合わせなので、最後になっています。以下に掲載したのは、これらのバイト配列の意味の概略です。

Diagram of what the bytes mean
最初の4バイト( 6e 64 69 66 )は、HVIFファイルであることを特定するマジックナンバーです。 ficn という文字は「フラットアイコン」を表現しています。それに続く3つのセクション(黄色、青、緑)は、すべてのHVIFファイルにあるもので、先頭に、そのセクションのオブジェクトの数(このファイルでは全て 02 )があります。黄色いセクションは2つのスタイル、つまりフラットな白と、赤から青へのグラデーションです。青いセクションは2つのパス、つまりHのアウトラインと、ブロブのアウトラインです。緑のセクションは2つのシェイプで、各々がスタイルとパスを統合して白のHと、カラフルなブロブを作ります。Hのシェイプは、サイズを大きくするための変換行列も持っています。

マジックナンバー

ファイルをパースする時、期待するタイプかどうかがすぐ分かると助かりますよね。それで最初の4バイトがこの目的に充てられています。バイナリファイルの形式としては一般的です。文字は ficn ですが、バイナリエディタでは ncif と表示されます。なぜならマジックナンバーはバイトオーダーを決定する リトルエンディアン の順序で書かれたシングルの int32 だからです。リトルエンディアンでは、最下位バイト( n )が先に来て最上位バイト( f )が最後に来ます。

スタイル

スタイルにはグラデーションとフラットカラーがあります。各シェイプは1つのスタイルを持ち、色/グラデーションのパターンでシェイプを満たすのに使われます。色とグラデーション両方の例が得られるよう、フラットの白でHを、グラデーションでブロブを作りました。このセクションの最初のバイト 02 は、2つのスタイルがあることを示していて、最初のスタイルはすぐそのあとから始まります。各スタイルはスタイルの種類(1バイト)から始まり、これによって残りのスタイルをどうパースすればいいかがわかります。すべてのスタイルには最低でも1つの色が含まれています。グラデーションはフラットカラーより多くの色とより多くの属性を持っています。HVIFでは色をaRGBとして表現します。各色には4つのチャネル、すなわちアルファ(不透明)、赤、緑、青があります。各チャネルの表現に1バイト使います( unit8 )。

最初のスタイルはフラットな白で、 05 ff で表現されます。皆さんにファイルの説明をする前にパースしておいたので、スタイルの長さがわかっていました。そうでなければ、最初のバイト(スタイルの種類、 05 )を見て、そこからすべてを判読しなければならなかったでしょう。HVIFのオブジェクト(スタイル/パス/シェイプ)はすべて可変長で、シンプルまたは一般的な(これのような)特殊な形式のオブジェクトをコンパクトに保存できます。最初のバイトはスタイルの種類で、パーサーのコードのenumに相当します。(C/C++のenumは数値で名前をつけられます。例えばここでのスタイルの種類のように、関連する一連の名前に異なる値をアサインする方法としてよく使われています。)。

スタイルには5種類あります。

STYLE_TYPE_SOLID_COLOR = 1,
STYLE_TYPE_GRADIENT = 2,
STYLE_TYPE_SOLID_COLOR_NO_ALPHA = 3,
STYLE_TYPE_SOLID_GRAY = 4,
STYLE_TYPE_SOLID_GRAY_NO_ALPHA = 5,

フラットな白は STYLE_TYPE_SOLID_GRAY_NO_ALPHA です。ここから分かるのは、これがフラットカラーであってグラデーションではないということ、そしてグレー、つまり赤、緑、ブルーのチャネルがすべて等しくかつ完全に不透明なので、アルファの値は255になるということです。1ビットにずいぶんたくさんの情報が入っていますよね。共通なケースを数ビットに詰め込むために特殊なタイプを利用するのは、ファイルサイズを小さくしておくために大事なことです。

これはフラットカラーなので、1つのaRGB色によって定義されます。アルファの値は(NO_ALPHA)というタイプによって与えられます。赤/緑/青の値は2番目のバイト ff によって与えられます。3つの色チャネルはすべて255にセットされます。( 255, 255, 255, 255 )というのが、aRGBでピュアな白を表現します。

最初のスタイルは終わりました。HVIFにはスペースを多く占めてしまうパディングやデリミタはありません。その代わり、2番目のスタイルに直接行きましょう。ブロブ向けのグラデーションです。

2番目のスタイルのバイト列は 02 03 04 02 00 ff 00 00 86 00 00 ff です。enumによれば、 02 STYLE_TYPE_GRADIENT です。タイプの値が人間にも読みやすいのはいいですね。 2 は単に 02 です。グラデーションは種類、フラグ、ストップカウントの順になっていて、それぞれ1バイトです。グラデーションの種類はここでは 03 です。パーサーの他のenumを使って解読します。

enum gradients_type {
GRADIENT_LINEAR = 0,
GRADIENT_CIRCULAR,
GRADIENT_DIAMOND,
GRADIENT_CONIC,
GRADIENT_XY,
GRADIENT_SQRT_XY
};

enumはゼロでスタートし、新しい名前ごとに1ずつ増えます。ですから 03 は4番目の名前、 GRADIENT_CONIC に合致します。スタイルをパースするのにこれを理解する必要はありません。グラデーションの種類は、ファイルの構造ではなく、レンダリングに影響するからです。各グラデーションは違う場所でスタートしストップします(例えば、aRGB色がそうであるように不透明で始まり、最終的に透明になる)。グラデーションの種類はレンダラーがどのように色から色へフェードするのかを決定します。

次のバイトはグラデーションフラグ( 04 )で、パースに影響します。多くのHVIFオブジェクトは、先頭近くにフラグを表すバイトを持っています。これによって様々なフラットカラーの種類と似たような方法でスペースを節約できます。フラグバイトの各ビットには意味があり、別のenumで決定されます。

GRADIENT_FLAG_TRANSFORM     = 1 << 1,
GRADIENT_FLAG_NO_ALPHA      = 1 << 2,
GRADIENT_FLAG_16_BIT_COLORS   = 1 << 3, // not yet used
GRADIENT_FLAG_GRAYS     = 1 << 4,

1バイトは8ビットですから、 04 0000 0100 です。 1 であるビットが1つしかない場合、フラグが1つだけセットされていることを意味します。フラグは gradientFlags & GRADIENT_FLAG_TRANSFORM のような命令文を使ってチェックできます。結果の値がゼロより大きいものに真のフラグがセットされます。それぞれのフラグの値は1つの 1 だけを持っているので、セットされているのは、値が4のものだけです。これは、 GRADIENT_FLAG_NO_ALPHA だけがセットされているという意味です。なぜなら 1 << 2 (1を左に2回シフト)は 0000 0100 だからです。このフラグが意味するのはグラデーション色のアルファの値が 255 (完全に不透明)ということです。

お気づきかもしれませんが、フラグの値は 0000 0010 から始まり、 0001 0000 まで続きます。(十分なフラグがないという理由で)すべてのビットを使わなくても問題はありませんが、 0000 0001 がないのはおかしい気がします。理由はよくわかりませんが。

これは色がアルファ値を含まないグラデーション(すべてが不透明)なので、次のステップはストップカウントを見て何色あるか判断することです。今回は 02 ですから、 2 色だけです。これら2色はそれぞれ4バイトです。ストップオフセット、赤、緑、青です。最初の色のバイトは 00 ff 00 00 で、これはストップオフセットがゼロで、色は100%赤だということです。2番目の色は 86 00 00 ff で、ストップオフセットは86、100%青です。これがグラデーションの終わりになり(2つのスタイルをパースしたので)スタイルセクションの終わりでもあります。


  1. これは実際には2つめのサンプルファイルです。1つめは大きすぎました。「Hello」という言葉を使ったので、パスが4つ、セグメントも大量に増えてしまいました。次に作ったファイルでは、スタイルとパスを2つに絞り、できる限り特徴空間を網羅できるようにしました。1つのスタイルはフラットカラーで、もう1つはグラデーションです。色(もっとも彩度の高い青)を選び、バイナリを手動パースするのが楽なようにしました。一方のパスはずっと直線(H)で、他方は曲線と直線のミックスです(コマンドパスとしてエンコードされるようにするため)。パスに文字を使ったのは、Icon-O-Maticにテキストをペーストできるからで、これで自動的にパスを作ってくれます。つまり、パスを作るのに最も簡単な方法です。

監修者
監修者_古川陽介
古川陽介
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。