ひtoりgoと

NSImageRep って何者?

macOS 開発

AppKit のクラスって普段 UIKit を触る人々から誤解を受けやすいが、中でも NSImage はその筆頭。「UIImage なら cgImage プロパティで簡単に CGImage を取り出せるのになぜできない?」「representations ってなんだよ」「洗練された UIImage に比べて無駄に複雑」「これだから過去の遺物は...」などなど、言われてない被害妄想も簡単にできる。

だが実際にはよくできた設計の柔軟性のあるクラスで、正しい知識で使いこなすと UIImage は NSImage のほんの一部の機能しか提供していないのがわかってくる1

Representation(NSImageRep)とは

まず重要な点として、NSImage は UIImage とか CGImage みたいに 1 枚のビットマップを表すクラスではない。あくまでも開発者がイメージを描画するときに触る 1 オブジェクトという単位でしかないのである。

...これだけだと意味不明なので続けると、NSImage の核となるのが representations という謎のプロパティ。型は [NSImageRep] という配列だ。NSImageRep って何者...?

これは表示内容を定義するクラスであり、実際に使われるのは生の NSImageRep ではなくそのサブクラスのインスタンスだ。「ビットマップなら NSBitmapImageRep」といったように種類に応じたものが用意されている。ベクタイメージも NSPDFImageRep があるからビットマップに変換されずに扱えるし、NSCustomImageRep があればたとえ描画関数であってもイメージとして扱える。

配列であるのを見てわかるように NSImage はそれを複数持てるのだが、描画するときに使われるのはその中の 1 つであり重なって表示されたりするわけではない。それでも複数持てることには意味があって、描画に使われる Representation というのは描画するタイミングで描画先に応じた最適なものが自動で選択される。考慮されるのは描画先の出力方法(スクリーン、プリント)や HiDPI 倍率、描画サイズなどだ。

例えば .icns ファイルみたいなアイコンだったら 16、32、128 など異なるサイズの正方形を用意していて「小さいサイズではデフォルメしてくっきり」「大きいサイズでは間延びするので質感にこだわる」みたいな描き分けがされているのが昔からの伝統だ。これらの各サイズに該当するのが representations であり、すべてを内包した 1 つの NSImage で表すことができるから扱いやすい。 NSImageView に貼り付ければ表示サイズに追随して最適なものを表示してくれる。

表示サイズに追随

昔はディスプレイの解像度が低く最終出力は紙だったので「スクリーン表示にはくっきりしたドット絵」「プリントするときにはベクタイメージ」みたいなそれぞれの長所を活かした異種混合だってできる仕組みになっている。

また、この仕組みは Retina ディスプレイの導入にも大きく貢献している。NSImage を生成するときに HiDPI 用の @2x イメージとそうでないイメージをそれぞれ Representation(NSBitmapImageRep)として読み込む仕組みが追加された。開発者は描画側のコードには何も変更を加えず従来通りの 1 つの NSImage オブジェクトを扱うだけでディスプレイ性能に合わせた対応ができる。前述のアイコンみたいにもともと複数サイズを持っていても関係なく 16x16、16x16@2x みたいに内部の Representation のバリエーションが増えるだけだ。

試しに Safari のアイコンに含まれる Representation を一覧表示してみよう。

let workspace = NSWorkspace.shared
let representations = workspace.icon(
    forFile: workspace.urlForApplication(withBundleIdentifier: "com.apple.Safari")!.path
).representations
print(representations.map { "(\(Int($0.size.width))pt, \($0.pixelsWide)px)" })
アイコンの Representation を一覧表示
[
    "(32pt, 32px)", "(32pt, 32px)", "(32pt, 64px)", "(32pt, 64px)",
    "(16pt, 16px)", "(16pt, 16px)", "(16pt, 32px)", "(16pt, 32px)",
    "(18pt, 18px)", "(18pt, 18px)", "(18pt, 36px)", "(18pt, 36px)",
    "(24pt, 24px)", "(24pt, 24px)", "(24pt, 48px)", "(24pt, 48px)",
    "(128pt, 128px)", "(128pt, 128px)", "(128pt, 256px)", "(128pt, 256px)",
    "(256pt, 256px)", "(256pt, 256px)", "(256pt, 512px)", "(256pt, 512px)",
    "(512pt, 512px)", "(512pt, 512px)", "(512pt, 1024px)", "(512pt, 1024px)",
    "(1024pt, 1024px)", "(1024pt, 1024px)", "(1024pt, 2048px)", "(1024pt, 2048px)"
]
Result

こんなにある。ここでサイズを二種類表示しているのだけど、pt というのは NSView の座標にも使われる物理的な長さを表す単位。px はピクセル数であり、HiDPI だと高密度になるからこちらが大きくなる。Mac 上のスクリーンに表示することだけを考えれば「1 pt = 1 px」なのが HiDPI なしで「1 pt = 2 px」なのが 200 % HiDPI だ。各サイズにそれぞれピクセル数が倍の HiDPI バージョンが用意されているのがわかると思う。

一見メモリの無駄にも見えるが Mac だと好きなディスプレイを複数枚繋げられるので HiDPI とそうでないディスプレイが混合する環境が普通にある。ウインドウをドラッグしてその境界を跨ぐとその瞬間に再描画されて適切な Representation が選択されるのだ。Windows の世界だといまだに HiDPI 倍率が異なるディスプレイの混合に対応できないアプリケーションが多いのに Cocoa では最初から予定していたかのように NeXT 時代の仕組みがそのまま有効に働いている事実は評価されるべき。

iPhone 4 の Retina ディスプレイとともにいち早く HiDPI 対応した UIKit の UIImage も端末に応じて得られるビットマップの倍率が違うものの、1 つの UIImage が 1 つの cgImage と対になる設計だからこういう動的な変化への対応を表現しにくい。内部に仕組みがあっても直接さわれないと何かと不便だ。もっともあちらの世界は 300 % HiDPI まで高解像度化しており、それぞれの倍率に用意したビットマップでピクセルパーフェクトを目指すよりは SF Symbols のようなやり方にシフトしているのでもう必要ないかもしれないのだけれども。


  1. 最近 AppKit 用に作っていたフレームワークを UIKit でも使えるように変更作業をしたのだけど、UIImage だと該当する機能が存在しなくて大変だった。 ↩︎

Share

リンクも共有もお気軽に。記事を書くモチベーションの向上に役立てます。

© 2005-2021 zumuya