Google 開発の 2D グラフィックライブラリ Skia の紹介とはじめかた

Skia

みなさん、こんにちは。今回は絵文字ジェネレーターの肝となる、絵文字の画像生成についての記事です。

絵文字ジェネレーター のサーバーサイドは Python で書かれています。そして、絵文字画像の生成部分は、独自のライブラリを C++ で開発し、それを Cython 経由で呼び出しています。

画像生成部分は、Skia という 2D グラフィックライブラリを内部では呼び出しています。Skia は Google が中心となって開発している C++ のライブラリで、Google Chrome や Android などの大きなプロダクトでも採用されている実績があります。

この記事では、2D グラフィックライブラリである Skia の概要と、簡単な使い方を具体的なコマンドとソースコードを交えて紹介します。次に、絵文字ジェネレーターでの Skia の利用例を解説して行こうと思います。

Skia とは?

Skia とは、Google が OSS として開発している 2D グラフィックライブラリです。C++11 で書かれており、スマートポインタを活用した使いやすいモダンなインターフェイスが特徴です。Skia は Google Chrome や Android など、Google のプロダクトで主に利用されています。

サポートしている動作環境も幅広く、x86_64 アーキテクチャが主流な Windows や macOS、Linux はもちろん、Android や iOS もターゲットです。Skia は比較的多機能なライブラリであり、レポジトリも巨大です。

Skia の簡単な使い方

はじめに

Skia は高機能で綺麗な C++ インターフェイスを持ったライブラリなのですが、ドキュメントがほとんど用意されていません。日本語のドキュメントに至っては、ほぼ皆無です。そのため、Skia を使って本格的に開発する場合は、ある程度の覚悟が必要です。

オススメは、Skia 本体のソースコードと、Skia を使った何らかのプロジェクトを読むことです。私は Android と SkiaSharp のソースコードを参考にしました。

この記事では、Skia を使う際にはじめに躓くであろうライブラリのビルドと、簡単なサンプルを紹介していきます。この記事をきっかけに、Skia を使う方が増えていただけると嬉しいです。

ライブラリのビルド

Skia は C++ で書かれた巨大なライブラリであり、予めビルドが必要です。環境にもよりますが、キャッシュがない状況では 15〜30 分はかかると考えておいてください。

※ この記事では macOS (10.14 Mojave)、及び bash 上で動作確認しています。Skia 自体は他の環境にも対応していますが、実行する環境に応じてコマンドを適宜書き換える必要があります。

まず、必要なソースコードを Git を用いて取得します。

$ mkdir -p ~/tmp/
$ cd ~/tmp/
$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
$ git clone https://skia.googlesource.com/skia.git

depot_tools には、Skia をビルドする際に必要な各種ツールが含まれています。これらのツールが Python 2.7 へ依存しているため、ビルドする環境には Python 2.7 が必要です(※ 2019年2月4日現在)。

次に、depot_tools へパスを通した後、ビルドするコマンドを実行します。今回はビルドの成果物として、静的ライブラリ libskia.a を生成することを目標とします。

$ python --version
Python 2.7.15

$ cd ~/tmp/skia
$ PATH="$HOME/tmp/depot_tools:$PATH"

$ python tools/git-sync-deps
$ gn gen out/Static --args='is_debug=false target_cpu="x64" is_official_build=true skia_use_system_libjpeg_turbo=false skia_use_system_libpng=false skia_use_system_libwebp=false skia_use_system_icu=false'
$ ninja -C out/Static

git-sync-deps コマンドは、Skia が依存しているライブラリを Git で取得してくるラッパースクリプトです。

gn コマンドは Generate Ninja の略で、ビルドツール ninja でビルドする為に必要なファイルを生成するためのメタビルドシステムです。gnmake に対する cmake のような位置づけです。gn へ渡している引数では、システムのライブラリではなく、サブモジュールのライブラリをコンパイルしリンクするように指示をしています。

最後に ninja コマンドを使いビルドを実行します。ここで結構時間がかかります。ninja 自体は depot_tools に含まれているため、別途インストールする必要はありません。

ビルドが成功すると、out/Static ディレクトリに libskia.a が完成しているはずです。これを使って、Skia を使った簡単なサンプルを動かしてみます。

サンプルの作成

はじめに、簡単な矩形を書くプログラムを作成してみます。Skia を直接で扱うため、言語は C++ で記述します。

#include <string>
#include <fstream>

#include "SkCanvas.h"
#include "SkData.h"
#include "SkEncodedImageFormat.h"
#include "SkImage.h"
#include "SkPaint.h"
#include "SkSurface.h"

int main() {
  // 描画対象キャンバスの準備
  sk_sp<SkSurface> surface = SkSurface::MakeRasterN32Premul(300, 200);
  SkCanvas *canvas = surface->getCanvas();
  canvas->clear(SK_ColorGRAY); // 背景色: グレー

  // 矩形の描写
  SkPaint paint;
  paint.setStyle(SkPaint::kFill_Style);
  paint.setAntiAlias(true);
  paint.setColor(SK_ColorBLUE);

  SkRect rect = SkRect::MakeXYWH(20, 20, 100, 100);
  canvas->drawRect(rect, paint);

  // 画像の保存
  sk_sp<SkImage> image(surface->makeImageSnapshot());
  sk_sp<SkData> data(image->encodeToData(SkEncodedImageFormat::kPNG, 100));

  std::ofstream ofs("sample1.png", std::ios::binary);
  ofs.write(reinterpret_cast<const char*>(data->data()), data->size());
  ofs.close();

  return 0;
}

SkSurface は描画対象のメモリを管理します。Skia は CPU と GPU のどちらで描画するか選べます。上記は、CPU で描画する為のメモリを割当ています。

SkCanvas は実際の描画に必要なインターフェイスを提供しています。SkSurface から getCanvas() メソッドで取得し、各種操作をしていきます。

次に、矩形を描画するために SkPaint SkRect を使います。この辺りのクラス構成は、Android のクラス android.graphics.CanvasPaintRect の関係に酷似しているため、Android の知識がある人は理解しやすいかもしれません。

座標は左上が (0, 0) になります。

座標系

最後に、PNG 画像としてフォーマットし、保存したら完了です。Skia は PNG 以外にも GIF、JPEG、WebP など複数のフォーマットに対応しています。

※ 対応しているフォーマットの一覧は、SkEncodedImageFormat.h を参照してください。

プログラムが ~/tmp/sample/sample1.cpp へ保存されている場合、以下のコマンドでビルドできます。ビルドするには、Skia のソースコードが ~/tmp/skia に配置されており、ビルド済みの静的ライブラリ libskia.a~/tmp/skia/out/Static に存在する必要があります。

$ clang++ --version
Apple LLVM version 10.0.0 (clang-1000.11.45.5)
Target: x86_64-apple-darwin18.2.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

$ cd ~/tmp/sample
$ clang++ --std=c++14 \
    -I ~/tmp/skia/include/core \
    -I ~/tmp/skia/include/config \
    -I ~/tmp/skia/include/utils \
    -I ~/tmp/skia/include/gpu \
    -L ~/tmp/skia/out/Static \
    -framework CoreFoundation \
    -framework CoreGraphics \
    -framework CoreText \
    -framework CoreServices \
    -lskia -lz sample1.cpp
$ ./a.out

※ Linux 環境でビルドする場合、リンクが必要なライブラリが macOS と大きく異なります。libdl、libfontconfig、libfreetype、libGL、libGLU への追加のリンクが必要です。

ビルドしたプログラムを実行すると、以下のような画像が得られます。

実行結果

このように Skia を使ったプログラムを作るとができます。矩形以外の描画をする場合も、同様に SkCanvas に対して描画指示を行います。

次は、絵文字ジェネレーターでどのように Skia を使っているか紹介します。

絵文字ジェネレーターでの採用事例

プログラムの構成

絵文字ジェネレーター では、絵文字生成の部分に Skia を利用しています。リリース当初は Pillow というグラフィックライブラリライブラリを用いていましたが、2018年3月から Skia に切り替えています。

絵文字ジェネレーターでは、リリース当初から画像の生成はクライアントサイドでは行わず、全てサーバーサイドで行っています。これは環境による差異を無くし、環境問わず綺麗にテキストを描画するためです。

実際の絵文字の生成は、C++ で書かれた libemoji というライブラリに分離し、開発しています。このライブラリは、外部インターフェイスとして C++ のクラスをラップした C の関数と構造体を提供しています (参考: emoji.h)。Python から利用する際は、これをさらに Cython でラップしています。

生成部分

絵文字の生成は、大きく2つのステップに分かれます。

  1. 行ごとのフォントサイズ・位置・倍率の計算
  2. 行ごとのテキストの描画

生成フロー

描画は行単位で行います。まず初めに、行ごとのフォントサイズと描画開始位置を決定します。SkPaint クラスのメソッド measureText を使うと、そのテキストを描画した際に必要とするサイズ情報が取得できます。下記はその部分のコードの一部です。

for (SkScalar i = minTextSize; i < maxTextSize;
     i += SkDoubleToScalar(0.5)) {
    paint.setTextSize(i);
    paint.measureText(fText.c_str(), fText.length(), &bounds);

    if (bounds.height() > fLineHeight) break;
    if (fDisableStretch && bounds.width() > fWidth) break;

    prevTextSize = i;
    prevBounds = bounds;
}

フォントサイズと実際に描画されるテキストのサイズは一致しないため、適切なフォントサイズを都度求める必要があります。Android では android.graphics.Paint クラスの getTextBounds メソッドが同様の働きをします。

行の高さ算出が終わったら、実際のテキストの描画を行います。オプションに応じてテキストの描画開始座標を決定し、描画します。下記は、テキストの描画コードの一部です。

// for X-axis
SkScalar x;
switch (fTextAlign) {
case SkTextUtils::kLeft_Align:
    x = -spec.fBounds.fLeft;
    break;
case SkTextUtils::kCenter_Align:
    if (spec.fTextScaleX < SkIntToScalar(1)) {
        x = -spec.fBounds.fLeft;
    } else {
        x = (fWidth - spec.fBounds.width()) / SkIntToScalar(2) -
            spec.fBounds.fLeft;
    }
    break;
case SkTextUtils::kRight_Align:
    if (spec.fTextScaleX < SkIntToScalar(1)) {
        x = -spec.fBounds.fLeft;
    } else {
        x = fWidth - spec.fBounds.width() - spec.fBounds.fLeft;
    }
    break;
}

// for Y-axis
SkScalar offsetY = (fLineHeight - spec.fBounds.height()) / SkIntToScalar(2);

paint.setTextScaleX(spec.fTextScaleX);
SkTextUtils::DrawString(canvas, fText.c_str(), x,
                        y - spec.fBounds.fTop + offsetY, paint);

ソースコードは GitHub で libemoji として公開しているので、興味のある方はぜひそちらをご覧ください。

まとめ

この記事では、グラフィックライブラリ Skia の紹介・簡単な使い方と、絵文字ジェネレーターでの採用事例を紹介しました。

Skia は巨大なライブラリであり、使い始めるまでが大変です。そのかわり、Skia は C++11 を全面的に使って綺麗に設計されたインターフェイスを持ち、使い心地は抜群です。Google Chrome に採用されている関係上、現在も活発に開発が進められています。機会があったら、ぜひ Skia を使ってみてください。

今後も、絵文字ジェネレーター をよろしくお願いします!