Share via


SkiaSharp ビットマップの作成と描画

ここまでの記事では、Web から、アプリケーション リソースから、またはユーザーのフォト ライブラリから、アプリケーションにビットマップを読み込む方法を見てきました。 次は、アプリケーション内に新しいビットマップを作成する方法を説明します。 最も単純なアプローチは、次のようにしていずれかの SKBitmap コンストラクタを使用する方法です。

SKBitmap bitmap = new SKBitmap(width, height);

widthheight は、ビットマップの寸法をピクセル単位で指定する整数パラメーターです。 このコンストラクターは、ピクセルあたり 4 バイト (赤、緑、青、アルファ (不透明度) の各成分に 1 バイトずつ) のフルカラー ビットマップを作成します。

新しいビットマップを作成した後は、ビットマップのサーフェイスに何らかのコンテンツを配置する必要があります。 これを行う方法には、大きく分けて以下の 2 つがあります。

  • 標準の Canvas 描画メソッドを使用してビットマップにグラフィックを描画する。
  • ピクセルのビット群に直接アクセスする。

この記事では、第 1 の方法について説明します。

描画のサンプル

第 2 の方法については、記事「SkiaSharp ビットマップ ピクセルへのアクセス」を参照してください。

ビットマップへの描画

ビットマップのサーフェイスに描画する方法は、ビデオ ディスプレイに描画する方法と同様です。 ビデオ ディスプレイに描画するときは、PaintSurface イベントの引数から SKCanvas オブジェクトを取得します。 ビットマップに描画するときは、SKCanvas コンストラクタを使用して SKCanvas オブジェクトを作成します。

SKCanvas canvas = new SKCanvas(bitmap);

ビットマップへの描画操作が終了した後は、SKCanvas オブジェクトは破棄して問題ありません。 この理由から、多くの場合、SKCanvas コンストラクターは次のように using ステートメント内で呼び出されます。

using (SKCanvas canvas = new SKCanvas(bitmap))
{
    ··· // call drawing function
}

以上を実行すると、ビットマップは表示可能になります。 これ以降、プログラムで同じビットマップ上に新しい SKCanvas オブジェクトを作成し、さらなる描画操作を実行することもできます。

サンプル アプリケーションの [Hello Bitmap] ページでは、ビットマップに "Hello, Bitmap!" というテキストが書き込まれ、そのビットマップが複数回表示されます。

HelloBitmapPage のコンストラクターは、まず、テキストを表示するための SKPaint オブジェクトを作成します。 テキスト文字列の寸法を調べ、その寸法でビットマップを作成します。 次に、そのビットマップを基にして SKCanvas オブジェクトを作成し、Clear を呼び出してから、DrawText を呼び出します。 新しく作成されたビットマップにはランダムなデータが含まれている可能性があるため、ビットマップを作成したら必ず Clear を呼び出すことをお勧めします。

コンストラクターは、ビットマップを表示するための SKCanvasView オブジェクトを作成して終了します。

public partial class HelloBitmapPage : ContentPage
{
    const string TEXT = "Hello, Bitmap!";
    SKBitmap helloBitmap;

    public HelloBitmapPage()
    {
        Title = TEXT;

        // Create bitmap and draw on it
        using (SKPaint textPaint = new SKPaint { TextSize = 48 })
        {
            SKRect bounds = new SKRect();
            textPaint.MeasureText(TEXT, ref bounds);

            helloBitmap = new SKBitmap((int)bounds.Right,
                                       (int)bounds.Height);

            using (SKCanvas bitmapCanvas = new SKCanvas(helloBitmap))
            {
                bitmapCanvas.Clear();
                bitmapCanvas.DrawText(TEXT, 0, -bounds.Top, textPaint);
            }
        }

        // Create SKCanvasView to view result
        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear(SKColors.Aqua);

        for (float y = 0; y < info.Height; y += helloBitmap.Height)
            for (float x = 0; x < info.Width; x += helloBitmap.Width)
            {
                canvas.DrawBitmap(helloBitmap, x, y);
            }
    }
}

PaintSurface ハンドラーは、ディスプレイ内の異なる行位置と列位置にビットマップを複数回レンダリングします。 PaintSurface ハンドラー内の Clear メソッド呼び出しに、表示サーフェイスの背景色を指定する引数 SKColors.Aqua が付いていることに注目してください。

Hello, Bitmap!

水色の背景が見えているのは、このビットマップが、テキストの黒い部分以外は透明だからです。

クリアと透過性

Hello Bitmap ページの表示を見ると、プログラムで作成したビットマップが、テキストの黒い部分以外は透明であることがわかります。 表示サーフェイスが水色なのは、透明部分から背景が透けているからです。

SKCanvasClear メソッドに関するドキュメントには、"キャンバスの現在のクリップに含まれるすべてのピクセルを置き換える" と記述されています。"置き換える" という表現からは、SKCanvas のすべての描画メソッドは既存の表示サーフェイスに何かを追加するという、重要な性質を読み取ることができます。 Clear メソッドは、既にそこに配置されていた何かを "置き換える" のです。

Clear には 2 つの異なるバージョンがあります。

  • SKColor パラメーター 1 個を取る Clear メソッドは、表示サーフェイスのピクセルをその色のピクセルで置き換えます。

  • パラメーターを取らない Clear メソッドは、ピクセルを SKColors.Empty カラー (赤、緑、青、アルファの各成分がすべて 0) で置き換えます。 このカラーは "透明ブラック" とも呼ばれることがあります。

新しいビットマップに対して、引数なしの Clear を呼び出すと、そのビットマップ全体が完全に透明な状態に初期化されます。 その後でビットマップ内に描画されるコンテンツは、通常、不透明または半透明です。

ここでお勧めの実験: Hello Bitmap ページで bitmapCanvas に適用されている Clear メソッド呼び出しを、次のコードに変更してみましょう。

bitmapCanvas.Clear(new SKColor(255, 0, 0, 128));

この SKColor コンストラクターのパラメーターは、先頭から順に赤、緑、青、アルファで、値の範囲はそれぞれ 0 から 255 です。 アルファ値は 0 が透明、255 が不透明を意味することを覚えておいてください。

値 (255, 0, 0, 128) は、ビットマップのピクセル群を、不透明度 50% 赤いピクセルでクリアする指定です。 つまり、ビットマップの背景は半透明になります。 ビットマップ背景を半透明の赤にすると、表示サーフェイスの水色の背景と混じり合ってグレーの背景に見えるようになります。

SKPaint の初期化子に次の代入文を挿入することで、テキストのカラーを透明ブラックに設定してみましょう。

Color = new SKColor(0, 0, 0, 0)

一見すると、この透明なテキストはビットマップに完全な透明の領域を作り出し、表示サーフェイスの水色の背景が透けるようになると思えるかもしれません。 しかし、実際の結果は異なります。 ビットマップ上に既に存在するものの上にテキストが描画されるため、 この透明なテキストはまったく見えません。

どの Draw メソッドにも、ビットマップの透明度を高める作用はありません。 そのような効果を持つメソッドは Clear だけです。

ビットマップの各種カラー タイプ

最もシンプルな SKBitmap コンストラクターは、ビットマップの幅と高さをピクセル単位で指定する整数パラメーターを取ります。 その他の SKBitmap コンストラクターは、これよりも複雑で、 SKColorType および SKAlphaType という 2 つの列挙型パラメーターが必須です。 また、これらの情報を統合した SKImageInfo 構造体を使用するコンストラクターもあります。

SKColorType 列挙型には 9 つのメンバーがあります。 各メンバーは、ビットマップのピクセルに関する特定の格納方法を表しています。

  • Unknown
  • Alpha8 — 各ピクセルは 8 ビットで、アルファ値 (完全な透明の 0 から完全な不透明まで) を表す
  • Rgb565 — 各ピクセルは 16 ビットで、赤と青をそれぞれ 5ビット、緑を 6 ビットで表す
  • Argb4444 — 各ピクセルは 16 ビットで、アルファ、赤、緑、青をそれぞれ 4 ビットで表す
  • Rgba8888 — 各ピクセルは 32 ビットで、赤、緑、青、アルファをそれぞれ 8 ビットで表す
  • Bgra8888 — 各ピクセルは 32 ビットで、青、緑、赤、アルファをそれぞれ 8 ビットで表す
  • Index8 — 各ピクセルは 8 ビットで、SKColorTable 内を指すインデックスを表す
  • Gray8 — 各ピクセルは 8 ビットで、グレーの濃淡 (黒から白まで) を表す
  • RgbaF16 — 各ピクセルは 64 ビットで、赤、緑、青、アルファをそれぞれ 16 ビット浮動小数点形式で表す

各ピクセルが 32 びっと (4 バイト) である 2 つの形式は、多くの場合に "フルカラー" 形式と呼ばれます。 その他多くの形式は、ビデオ ディスプレイ装置にフル カラー表示能力がなかった時代から使用されてきたものです。 限定的なカラー形式のビットマップは、そのようなディスプレイに適しており、メモリ内のビットマップ格納領域が小さくて済むという利点があります。

最近では、プログラマーはフルカラーのビットマップ形式を使用する場合がほとんどであり、その他の形式を扱う必要性はまずありません。 ただし、RgbaF16 形式は例外で、フル カラー形式を超えるカラー解像度も表現することができます。 とはいえ、この形式は医療用画像処理などの特殊用途向けであり、一般的なフル カラー ディスプレイで使用してもあまり意味はありません。

この一連の記事では、SKColorType メンバーを指定しない場合に既定で使用される各種 SKBitmap カラー形式だけを取り上げます。 既定の形式は、基礎になるプラットフォームに由来するものです。 Xamarin.Forms でサポートされているプラットフォームの場合、既定のカラー タイプは以下のとおりです。

  • Rgba8888 — iOS と Android
  • Bgra8888 — UWP

両者の違いは、4 つのバイト値がメモリに格納される順番だけであり、ピクセル内のビット データに直接アクセスしない限り問題になりません。 記事「SkiaSharp ビットマップ ピクセルへのアクセス」の内容に立ち入るまでは、これが重要な意味を持つことはありません。

SKAlphaType 列挙には 4 つの要素があります。

  • Unknown
  • Opaque — ビットマップの透過性なし
  • Premul — カラー成分はアルファ成分をあらかじめ乗算済み
  • Unpremul — カラー成分はアルファ成分を乗算済みではない

赤色で透明度 50% の 4 バイト ビットマップ ピクセルは、赤、緑、青、アルファの順に格納されたバイト列では次のように表現されます。

0xFF 0x00 0x00 0x80

半透明のピクセルを含んだビットマップを表示サーフェイスにレンダリングする際は、各ビットマップ ピクセルのカラー成分にそのピクセルのアルファ値を乗算し、さらに、表示サーフェイス上の対応するピクセルのカラー成分には、255 からアルファ値を引いた値を乗算する必要があります。 それら 2 つのピクセルは、この計算を経たうえで組み合わせ可能になります。 ビットマップ ピクセルのカラー成分にアルファ値があらかじめ乗算されている場合は、ビットマップのレンダリングを高速に実行できます。 先ほど示した赤色ピクセルは、アルファ成分乗算済みで格納されている場合、次のバイト列で表現されます。

0x80 0x00 0x00 0x80

SkiaSharp のビットマップ作成時の既定形式が Premul になっているのは、このパフォーマンス上の利点があるからです。 とはいえ、これについても、ピクセル内のビット データに対するアクセスや操作をしない限り意識する必要はありません。

既存のビットマップへの描画

ビットマップへの描画操作の対象は、新しく作成したビットマップである必要はありません。 既存のビットマップに対して描画を実行することもできます。

Monkey Moustache ページの場合は、コンストラクター内で MonkeyFace.png 画像を読み込んだ後、 そのビットマップを基にして SKCanvas オブジェクトを作成し、SKPaint および SKPath オブジェクトを使用して、ビットマップにヒゲを描画します。

public partial class MonkeyMoustachePage : ContentPage
{
    SKBitmap monkeyBitmap;

    public MonkeyMoustachePage()
    {
        Title = "Monkey Moustache";

        monkeyBitmap = BitmapExtensions.LoadBitmapResource(GetType(),
            "SkiaSharpFormsDemos.Media.MonkeyFace.png");

        // Create canvas based on bitmap
        using (SKCanvas canvas = new SKCanvas(monkeyBitmap))
        {
            using (SKPaint paint = new SKPaint())
            {
                paint.Style = SKPaintStyle.Stroke;
                paint.Color = SKColors.Black;
                paint.StrokeWidth = 24;
                paint.StrokeCap = SKStrokeCap.Round;

                using (SKPath path = new SKPath())
                {
                    path.MoveTo(380, 390);
                    path.CubicTo(560, 390, 560, 280, 500, 280);

                    path.MoveTo(320, 390);
                    path.CubicTo(140, 390, 140, 280, 200, 280);

                    canvas.DrawPath(path, paint);
                }
            }
        }

        // Create SKCanvasView to view result
        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();
        canvas.DrawBitmap(monkeyBitmap, info.Rect, BitmapStretch.Uniform);
    }
}

コンストラクターの最後では SKCanvasView を作成しています。このオブジェクトの PaintSurface ハンドラーは、描画結果を単に表示します。

Monkey Moustache

ビットマップのコピーと変更

SKCanvas には、ビットマップへの描画に使用できるメソッドの 1 つとして DrawBitmap が用意されています。 これは、あるビットマップを別のビットマップ上に (通常、何らかの変更を伴って) 描画できることを意味します。

ビットマップに変更を加える手法として最も幅広い用途に対応できるのは、ピクセル内の実際のビット データにアクセスする手法です (詳しくは、記事「SkiaSharp ビットマップ ピクセルへのアクセス」を参照してください)。 ただし、ピクセル内のビットに直接アクセスすることなくビットマップに変更を加える手法も多数用意されています。

サンプル アプリケーションに含まれている次のビットマップは、幅 360 ピクセル、高さ 480 ピクセルです。

Mountain Climbers

ここで、たとえば左側のサルから写真の公開許可が得られなかった場合の対処を考えてみましょう。 1 つの方法は、ピクセル化 (モザイク処理) でサルの顔を隠すことです。 顔の部分を色のブロックから成るモザイクに置き換えて特徴をぼかせば、そのサルを特定できなくなります。 色のブロックは、元の画像から、該当する部分にあるピクセル群のカラーを抽出し、平均化することで作成されるのが普通です。 このとき、平均化処理のロジックを自力で記述する必要はありません。 ビットマップを小さいピクセル寸法の中にコピーするだけで自動的に平均化処理が発生するからです。

左側のサルの顔は、(112, 238) の位置を左上隅とする約 72 ピクセル四方の領域にあります。 この 72 ピクセル四方の領域を、大きさ 8 x 8 ピクセルの正方形の色付きブロックを 9 x 9 個並べたモザイクに置き換えましょう。

Pixelize Image ページでは、ビットマップを読み込んだ後で、まず、faceBitmap という 9 ピクセル四方の小さいビットマップを作成します。 これが、サルの顔のコピー先となるビットマップです。 コピー先は 9 ピクセル四方の正方形ですが、コピー元には 72 ピクセル四方の大きさがあります。 したがって、8 x 8 ピクセルのブロックがコピー先では 1 個のピクセルに集約され、カラーは平均化されます。

その次の手順では、元のビットマップを、pixelizedBitmap という同サイズの新しいビットマップにコピーします。 さらに、小さい faceBitmap を、72 ピクセル四方の大きさがあるコピー先の四角形領域に上書きします。すると、faceBitmap に含まれる個々のピクセルが 8 倍に拡大されます。

public class PixelizedImagePage : ContentPage
{
    SKBitmap pixelizedBitmap;

    public PixelizedImagePage ()
    {
        Title = "Pixelize Image";

        SKBitmap originalBitmap = BitmapExtensions.LoadBitmapResource(GetType(),
            "SkiaSharpFormsDemos.Media.MountainClimbers.jpg");

        // Create tiny bitmap for pixelized face
        SKBitmap faceBitmap = new SKBitmap(9, 9);

        // Copy subset of original bitmap to that
        using (SKCanvas canvas = new SKCanvas(faceBitmap))
        {
            canvas.Clear();
            canvas.DrawBitmap(originalBitmap,
                              new SKRect(112, 238, 184, 310),   // source
                              new SKRect(0, 0, 9, 9));          // destination

        }

        // Create full-sized bitmap for copy
        pixelizedBitmap = new SKBitmap(originalBitmap.Width, originalBitmap.Height);

        using (SKCanvas canvas = new SKCanvas(pixelizedBitmap))
        {
            canvas.Clear();

            // Draw original in full size
            canvas.DrawBitmap(originalBitmap, new SKPoint());

            // Draw tiny bitmap to cover face
            canvas.DrawBitmap(faceBitmap,
                              new SKRect(112, 238, 184, 310));  // destination
        }

        // Create SKCanvasView to view result
        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();
        canvas.DrawBitmap(pixelizedBitmap, info.Rect, BitmapStretch.Uniform);
    }
}

コンストラクターの最後では、結果を表示するための SKCanvasView を作成しています。

Pixelize Image

ビットマップの回転

ビットマップの回転も、よく行われるタスクの 1 つです。 iPhone や iPad の写真ライブラリからビットマップを取得して回転させれば、非常に有用な使い方ができます。 デバイスを望ましくない向きに構えて撮影した場合、上下が逆さまの写真や横倒しの写真が撮れてしまうことがあります。

ビットマップの上下を逆さまにするには、元のビットマップと同じサイズのビットマップをもう 1 つ作成し、180 度の回転変換を適用してビットマップ間のコピーを実行する必要があります。 このセクションのコード例では、回転を必要とする元の SKBitmap オブジェクトを常に bitmap という名前で示すことにします。

SKBitmap rotatedBitmap = new SKBitmap(bitmap.Width, bitmap.Height);

using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
{
    canvas.Clear();
    canvas.RotateDegrees(180, bitmap.Width / 2, bitmap.Height / 2);
    canvas.DrawBitmap(bitmap, new SKPoint());
}

90 度の回転を適用する場合は、元のサイズとは異なる、高さと幅を入れ替えた寸法のビットマップをもう 1 つ作成する必要があります。 たとえば、元のビットマップが幅 1200 ピクセル、高さ 800 ピクセルの場合、回転後のビットマップは幅 800 ピクセル、高さ 1200 ピクセルになります。 まず、ビットマップの左上隅を中心とする回転変換を適用し、次に、表示領域に収めるための平行移動を適用します (Translate メソッドと RotateDegrees メソッドは、適用する順とは逆の順序で呼び出す必要があることに注意してください)。右回りの 90 度回転をかける場合のコードは次のとおりです。

SKBitmap rotatedBitmap = new SKBitmap(bitmap.Height, bitmap.Width);

using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
{
    canvas.Clear();
    canvas.Translate(bitmap.Height, 0);
    canvas.RotateDegrees(90);
    canvas.DrawBitmap(bitmap, new SKPoint());
}

同様に、左回りの 90 度回転をかける場合のコードは次のとおりです。

SKBitmap rotatedBitmap = new SKBitmap(bitmap.Height, bitmap.Width);

using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
{
    canvas.Clear();
    canvas.Translate(0, bitmap.Width);
    canvas.RotateDegrees(-90);
    canvas.DrawBitmap(bitmap, new SKPoint());
}

これら 2 つのメソッドは Photo Puzzle ページで使用されています。説明は、記事「SkiaSharp ビットマップのトリミング」を参照してください。

ユーザーの操作に従ってビットマップを 90 度ずつ回転させるプログラムを作成する場合は、90 度回転の関数を 1 つ実装するだけで済みます。 ユーザーに 90 度回転の操作を繰り返し実行させれば、90 度の倍数にあたる任意の回転を適用できるからです。

また、自由な角度の回転をビットマップに適用するプログラムも作成できます。 シンプルな実現方法としては、180 度回転を実行する関数を改造し、次のように、"180" の指定から一般化された変数 angle への変更を加えることが考えられます。

SKBitmap rotatedBitmap = new SKBitmap(bitmap.Width, bitmap.Height);

using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
{
    canvas.Clear();
    canvas.RotateDegrees(angle, bitmap.Width / 2, bitmap.Height / 2);
    canvas.DrawBitmap(bitmap, new SKPoint());
}

ただし、このロジックをそのまま一般化すると、回転結果は元のビットマップから四隅を裁ち落としたものになります。 より良い方法としては、四隅を含めた全体が収まるよう、回転後のビットマップのサイズを三角法で計算することが考えられます。

Bitmap Rotator ページは、この三角法を採用したプログラムの例です。 XAML ファイルで、SKCanvasView と、0 度から 360 度の範囲で角度を指定できる Slider、現在の値を表示する Label をインスタンス化しています。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Bitmaps.BitmapRotatorPage"
             Title="Bitmap Rotator">
    <StackLayout>
        <skia:SKCanvasView x:Name="canvasView"
                           VerticalOptions="FillAndExpand"
                           PaintSurface="OnCanvasViewPaintSurface" />

        <Slider x:Name="slider"
                Maximum="360"
                Margin="10, 0"
                ValueChanged="OnSliderValueChanged" />

        <Label Text="{Binding Source={x:Reference slider},
                              Path=Value,
                              StringFormat='Rotate by {0:F0}&#x00B0;'}"
               HorizontalTextAlignment="Center" />

    </StackLayout>
</ContentPage>

分離コード ファイルでは、ビットマップ リソースを読み込み、originalBitmap という静的な読み取り専用フィールドに保存します。 PaintSurface ハンドラー内で表示されるビットマップ rotatedBitmap には、初期状態として originalBitmap と同じ内容を設定します。

public partial class BitmapRotatorPage : ContentPage
{
    static readonly SKBitmap originalBitmap =
        BitmapExtensions.LoadBitmapResource(typeof(BitmapRotatorPage),
            "SkiaSharpFormsDemos.Media.Banana.jpg");

    SKBitmap rotatedBitmap = originalBitmap;

    public BitmapRotatorPage ()
    {
        InitializeComponent ();
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();
        canvas.DrawBitmap(rotatedBitmap, info.Rect, BitmapStretch.Uniform);
    }

    void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
    {
        double angle = args.NewValue;
        double radians = Math.PI * angle / 180;
        float sine = (float)Math.Abs(Math.Sin(radians));
        float cosine = (float)Math.Abs(Math.Cos(radians));
        int originalWidth = originalBitmap.Width;
        int originalHeight = originalBitmap.Height;
        int rotatedWidth = (int)(cosine * originalWidth + sine * originalHeight);
        int rotatedHeight = (int)(cosine * originalHeight + sine * originalWidth);

        rotatedBitmap = new SKBitmap(rotatedWidth, rotatedHeight);

        using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
        {
            canvas.Clear(SKColors.LightPink);
            canvas.Translate(rotatedWidth / 2, rotatedHeight / 2);
            canvas.RotateDegrees((float)angle);
            canvas.Translate(-originalWidth / 2, -originalHeight / 2);
            canvas.DrawBitmap(originalBitmap, new SKPoint());
        }

        canvasView.InvalidateSurface();
    }
}

SliderValueChanged ハンドラーでは、回転の角度に基づいて新しい rotatedBitmap を作成する操作を実行します。 回転結果ビットマップの幅と高さは、元の幅と高さから計算した正弦と余弦の絶対値に基づいて決まります。 回転結果ビットマップ内に元のビットマップを描画する際には、元のビットマップの中央を原点にするための並行移動、指定された角度の回転、そして、回転されたコンテンツを回転結果ビットマップの中央に配置するための並行移動を適用します (Translate メソッドと RotateDegrees メソッドは、適用する順とは逆の順序で呼び出します)。

Clear メソッドでは、rotatedBitmap の背景を薄いピンク色に塗っています。 これは、rotatedBitmap のサイズをピンクの領域として画面上に示すためです。

Bitmap Rotator

回転結果ビットマップのサイズは、元のビットマップのコンテンツ全体がちょうど収まる最低限の大きさです。

ビットマップの反転

反転操作も、ビットマップに対してよく実行されます。 概念的には、ビットマップの真ん中に原点がある三次元空間内で、縦軸または横軸の周りでビットマップを回転させることにより反転が行われます。 縦軸での反転結果は鏡像になります。

サンプル アプリケーションの [Bitmap Flipper] ページは、これらのプロセスを示しています。 XAML ファイルには、SKCanvasView と、縦軸または横軸での反転を実行する 2 つのボタンが含まれています。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Bitmaps.BitmapFlipperPage"
             Title="Bitmap Flipper">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <skia:SKCanvasView x:Name="canvasView"
                           Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
                           PaintSurface="OnCanvasViewPaintSurface" />

        <Button Text="Flip Vertical"
                Grid.Row="1" Grid.Column="0"
                Margin="0, 10"
                Clicked="OnFlipVerticalClicked" />

        <Button Text="Flip Horizontal"
                Grid.Row="1" Grid.Column="1"
                Margin="0, 10"
                Clicked="OnFlipHorizontalClicked" />
    </Grid>
</ContentPage>

分離コード ファイルでは、それら 2 つの反転操作をボタンの Clicked ハンドラー内に実装しています。

public partial class BitmapFlipperPage : ContentPage
{
    SKBitmap bitmap =
        BitmapExtensions.LoadBitmapResource(typeof(BitmapRotatorPage),
            "SkiaSharpFormsDemos.Media.SeatedMonkey.jpg");

    public BitmapFlipperPage()
    {
        InitializeComponent();
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();
        canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform);
    }

    void OnFlipVerticalClicked(object sender, ValueChangedEventArgs args)
    {
        SKBitmap flippedBitmap = new SKBitmap(bitmap.Width, bitmap.Height);

        using (SKCanvas canvas = new SKCanvas(flippedBitmap))
        {
            canvas.Clear();
            canvas.Scale(-1, 1, bitmap.Width / 2, 0);
            canvas.DrawBitmap(bitmap, new SKPoint());
        }

        bitmap = flippedBitmap;
        canvasView.InvalidateSurface();
    }

    void OnFlipHorizontalClicked(object sender, ValueChangedEventArgs args)
    {
        SKBitmap flippedBitmap = new SKBitmap(bitmap.Width, bitmap.Height);

        using (SKCanvas canvas = new SKCanvas(flippedBitmap))
        {
            canvas.Clear();
            canvas.Scale(1, -1, 0, bitmap.Height / 2);
            canvas.DrawBitmap(bitmap, new SKPoint());
        }

        bitmap = flippedBitmap;
        canvasView.InvalidateSurface();
    }
}

縦軸での反転は、水平方向のスケーリング係数を -1 にするスケーリング変換によって実現されます。 スケーリングの中心は、ビットマップの左右中央に配置した縦軸です。 横軸での反転は、垂直方向のスケーリング係数を -1 にするスケーリング変換です。

サルのシャツに書かれた文字が鏡文字になっていることでわかるように、反転は回転とは異なります。 ただし、横軸での反転と縦軸での反転を両方とも実行すると、180 度回転と同じ結果になります (右の UWP のスクリーンショット)。

Bitmap Flipper

似た手法で実現できる他の一般的なタスクとしては、小さい四角形でビットマップの一部だけを切り出すトリミング処理があります。 これについては、次の記事「SkiaSharp ビットマップのトリミング」で説明します。