SkiaSharp ビットマップをファイルに保存する

SkiaSharp アプリケーションでビットマップを作成または変更した後、アプリケーションでビットマップをユーザーのフォト ライブラリに保存できます。

ビットマップの保存

このタスクには、次の 2 つの手順が含まれます。

  • SkiaSharp ビットマップを JPEG や PNG などの特定のファイル形式のデータに変換する。
  • プラットフォーム固有のコードを使用して、結果をフォト ライブラリに保存する。

ファイル形式とコーデック

今日の一般的なビットマップ ファイル形式のほとんどは、圧縮を使用してストレージ領域を削減します。 圧縮手法の 2 つの広範なカテゴリは、"非可逆" と "無損失" と呼ばれます。 これらの用語は、圧縮アルゴリズムによってデータが失われる結果になるかどうかを示します。

最も人気のある非可逆形式は、Joint Photographic Experts Group によって開発されたもので、JPEG と呼ばれています。 JPEG 圧縮アルゴリズムは、"離散コサイン変換" と呼ばれる数学的ツールを使用して画像を分析し、画像の視覚的忠実性を維持するために重要ではないデータの削除を試みます。 圧縮の度合いは、一般に "品質" と呼ばれる設定によって制御できます。 品質設定が高いほど、ファイルが大きくなります。

これに対し、無損失圧縮アルゴリズムは、データを減らすが情報を失う結果にならない方法でエンコードできる、ピクセルの繰り返しとパターンを見つけるために画像を分析します。 元のビットマップ データを圧縮ファイルから完全に復元できます。 現在使用されている主な無損失圧縮ファイル形式は、ポータブル ネットワーク グラフィックス (PNG) です。

一般に、JPEG は写真に使用され、PNG は手動またはアルゴリズムによって生成された画像に使用されます。 一部のファイルのサイズを小さくする無損失圧縮アルゴリズムでは、必然的に他のファイルのサイズを大きくする必要があります。 さいわい、このサイズの増加は、一般にランダムな (またはランダムに見える) 情報が多く含まれるデータに対してのみ発生します。

圧縮アルゴリズムは、圧縮と展開のプロセスを表す 2 つの用語が必要になるほど複雑であると言えます。

  • decode — ビットマップ ファイル形式を読み取って展開します
  • encode — ビットマップを圧縮し、ビットマップ ファイル形式に書き込みます

SKBitmap クラスには、圧縮されたソースから SKBitmap を作成する、Decode という名前のメソッドがいくつか含まれています。 必要なのは、ファイル名、ストリーム、またはバイト配列を指定することだけです。 デコーダーは、ファイル形式を決定し、適切な内部デコード関数に渡すことができます。

さらに、SKCodec クラスには、圧縮されたソースから SKCodec オブジェクトを作成し、デコード プロセスへのアプリケーションの関与を高める、Create という名前の 2 つのメソッドがあります。 (SKCodec クラスは、アニメーション GIF ファイルのデコードに関連した「SkiaSharp ビットマップのアニメーション化」の記事に示されています。)

ビットマップをエンコードする場合は、より多くの情報が必要です。エンコーダーは、アプリケーションで使用する特定のファイル形式 (JPEG または PNG など) を認識している必要があります。 非可逆形式が必要な場合は、エンコードで必要な品質レベルもわかっている必要があります。

SKBitmap クラスでは、次の構文を使用して 1 つの Encode メソッドを定義します。

public Boolean Encode (SKWStream dst, SKEncodedImageFormat format, Int32 quality)

この方法については、後ほど詳しく説明します。 エンコードされたビットマップは、書き込み可能なストリームに書き込まれます。 (SKWStream の 'W' は "writable (書き込み可能)" を表します。)2 番目と 3 番目の引数は、ファイル形式と、(非可逆形式の場合は) 0 から 100 の範囲の目的の品質を指定します。

さらに、SKImage および SKPixmap クラスでは、Encode メソッドも定義されます。やや汎用性が高いので、こちらを優先することもできます。 静的 SKImage.FromBitmap を使用して、SKBitmap オブジェクトから SKImage オブジェクトを簡単に作成できます。 PeekPixels メソッドを使用して SKBitmap オブジェクトから SKPixmap オブジェクトを取得できます。

SKImage で定義された Encode メソッドの 1 つにはパラメーターがなく、自動的に PNG 形式で保存されます。 そのパラメーターなしのメソッドはとても使いやすいものです。

ビットマップ ファイルを保存するためのプラットフォーム固有のコード

SKBitmap オブジェクトを特定のファイル形式にエンコードすると、通常は、何らかの種類のストリーム オブジェクトまたはデータの配列が残ります。 一部の Encode メソッド (パラメーターが SKImage で定義されていないものを含む) は、SKData オブジェクトを返します。これは、ToArray メソッドを使用してバイト配列に変換できます。 その後、このデータをファイルに保存する必要があります。

このタスクには標準の System.IO のクラスとメソッドを使用できるため、アプリケーションのローカル ストレージ内のファイルへの保存はとても簡単です。 この手法は、Mandelbrot セットの一連のビットマップのアニメーション化に関連した「SkiaSharp ビットマップのアニメーション化」の記事で説明されています。

ファイルを他のアプリケーションで共有する場合は、ユーザーのフォト ライブラリに保存する必要があります。 このタスクには、プラットフォーム固有のコードと Xamarin.FormsDependencyService を使用する必要があります。

サンプル アプリケーションの SkiaSharpFormsDemo プロジェクトは、DependencyService クラスで使用される IPhotoLibrary インターフェイスを定義します。 これにより、SavePhotoAsync メソッドの構文が定義されます。

public interface IPhotoLibrary
{
    Task<Stream> PickPhotoAsync();

    Task<bool> SavePhotoAsync(byte[] data, string folder, string filename);
}

このインターフェイスでは、デバイスのフォト ライブラリに対してプラットフォーム固有のファイル ピッカーを開くために使用される、PickPhotoAsync メソッドも定義します。

SavePhotoAsync の最初の引数は、JPEG や PNG などの特定のファイル形式に既にエンコードされているビットマップを含むバイト配列です。 アプリケーションで作成するすべてのビットマップを特定のフォルダーに分離することが必要になる場合があります。このフォルダーは、次のパラメーターで指定され、その後にファイル名が続きます。 このメソッドは、成功したかどうかを示すブール値を返します。

以降のセクションでは、各プラットフォームでの SavePhotoAsync の実装方法について説明します。

iOS での実装

iOS での SavePhotoAsync の実装には、UIImageSaveToPhotosAlbum メソッドを使用します。

public class PhotoLibrary : IPhotoLibrary
{
    ···
    public Task<bool> SavePhotoAsync(byte[] data, string folder, string filename)
    {
        NSData nsData = NSData.FromArray(data);
        UIImage image = new UIImage(nsData);
        TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>();

        image.SaveToPhotosAlbum((UIImage img, NSError error) =>
        {
            taskCompletionSource.SetResult(error == null);
        });

        return taskCompletionSource.Task;
    }
}

残念ながら、画像のファイル名またはフォルダーを指定する方法はありません。

iOS プロジェクトの Info.plist ファイルには、フォト ライブラリに画像を追加することを示すキーが必要です。

<key>NSPhotoLibraryAddUsageDescription</key>
<string>SkiaSharp Forms Demos adds images to your photo library</string>

注意してください。 単にフォト ライブラリにアクセスするためのアクセス許可キーは、よく似ていますが、同じものではありません。

<key>NSPhotoLibraryUsageDescription</key>
<string>SkiaSharp Forms Demos accesses your photo library</string>

Android での実装

Android での SavePhotoAsync の実装では、folder 引数が null または空の文字列かどうかを最初にチェックします。 該当する場合、ビットマップはフォト ライブラリのルート ディレクトリに保存されます。 該当しない場合は、フォルダーが取得され、存在しない場合は作成されます。

public class PhotoLibrary : IPhotoLibrary
{
    ···
    public async Task<bool> SavePhotoAsync(byte[] data, string folder, string filename)
    {
        try
        {
            File picturesDirectory = Environment.GetExternalStoragePublicDirectory(Environment.DirectoryPictures);
            File folderDirectory = picturesDirectory;

            if (!string.IsNullOrEmpty(folder))
            {
                folderDirectory = new File(picturesDirectory, folder);
                folderDirectory.Mkdirs();
            }

            using (File bitmapFile = new File(folderDirectory, filename))
            {
                bitmapFile.CreateNewFile();

                using (FileOutputStream outputStream = new FileOutputStream(bitmapFile))
                {
                    await outputStream.WriteAsync(data);
                }

                // Make sure it shows up in the Photos gallery promptly.
                MediaScannerConnection.ScanFile(MainActivity.Instance,
                                                new string[] { bitmapFile.Path },
                                                new string[] { "image/png", "image/jpeg" }, null);
            }
        }
        catch
        {
            return false;
        }

        return true;
    }
}

MediaScannerConnection.ScanFile の呼び出しは厳密には必須ではありませんが、フォト ライブラリをすぐにチェックしてプログラムをテストする場合は、ライブラリ ギャラリー ビューを更新すると大きく役立ちます。

AndroidManifest.xml ファイルには、次のアクセス許可タグが必要です。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

UWP での実装

UWP での SavePhotoAsync の実装は、Android での実装と構造がよく似ています。

public class PhotoLibrary : IPhotoLibrary
{
    ···
    public async Task<bool> SavePhotoAsync(byte[] data, string folder, string filename)
    {
        StorageFolder picturesDirectory = KnownFolders.PicturesLibrary;
        StorageFolder folderDirectory = picturesDirectory;

        // Get the folder or create it if necessary
        if (!string.IsNullOrEmpty(folder))
        {
            try
            {
                folderDirectory = await picturesDirectory.GetFolderAsync(folder);
            }
            catch
            { }

            if (folderDirectory == null)
            {
                try
                {
                    folderDirectory = await picturesDirectory.CreateFolderAsync(folder);
                }
                catch
                {
                    return false;
                }
            }
        }

        try
        {
            // Create the file.
            StorageFile storageFile = await folderDirectory.CreateFileAsync(filename,
                                                CreationCollisionOption.GenerateUniqueName);

            // Convert byte[] to Windows buffer and write it out.
            IBuffer buffer = WindowsRuntimeBuffer.Create(data, 0, data.Length, data.Length);
            await FileIO.WriteBufferAsync(storageFile, buffer);
        }
        catch
        {
            return false;
        }

        return true;
    }
}

Package.appxmanifest ファイルの Capabilities セクションには、画像ライブラリが必要です。

画像形式の調査

SKImageEncode メソッドをもう一度示します。

public Boolean Encode (SKWStream dst, SKEncodedImageFormat format, Int32 quality)

SKEncodedImageFormat は、11 個のビットマップ ファイル形式を参照するメンバーを含む列挙体であり、その一部はかなり不明瞭です。

  • Astc — アダプティブ スケーラブル テクスチャ圧縮
  • Bmp — Windows ビットマップ
  • Dng — Adobe Digital Negative
  • Gif — グラフィックス交換形式
  • Ico — Windows アイコンの画像
  • Jpeg — Joint Photographic Experts Group
  • Ktx — OpenGL 用の Khronos テクスチャ形式
  • Pkm — GrafX2 のカスタム形式
  • Png — ポータブル ネットワーク グラフィックス (PNG)
  • Wbmp — ワイヤレス アプリケーション プロトコル ビットマップ形式 (ピクセルあたり 1 ビット)
  • Webp — Google WebP 形式

後ほど説明しますが、これらのファイル形式のうち、3 つ (JpegPngWebp) だけが SkiaSharp で実際にサポートされています。

ユーザーのフォト ライブラリに bitmap という名前の SKBitmap オブジェクトを保存するには、SKEncodedImageFormat 列挙体の imageFormat という名前のメンバーと、(不可逆形式の場合は) 整数の quality 変数も必要です。 次のコードを使用して、そのビットマップを folder フォルダーの filename という名前のファイルに保存できます。

using (MemoryStream memStream = new MemoryStream())
using (SKManagedWStream wstream = new SKManagedWStream(memStream))
{
    bitmap.Encode(wstream, imageFormat, quality);
    byte[] data = memStream.ToArray();

    // Check the data array for content!

    bool success = await DependencyService.Get<IPhotoLibrary>().SavePhotoAsync(data, folder, filename);

    // Check return value for success!
}

SKManagedWStream クラスは SKWStream ("writable stream (書き込み可能なストリーム)" を表します) から派生します。 Encode メソッドは、エンコードされたビットマップ ファイルをそのストリームに書き込みます。 そのコード内のコメントは、実行が必要になる可能性のあるエラー チェックについて言及しています。

サンプル アプリケーションの [ファイル保存形式] ページでは、同様のコードを使用して、さまざまな形式でビットマップを保存する実験を行うことができます。

XAML ファイルにはビットマップを表示する SKCanvasView が含まれていますが、ページの残りの部分には、アプリケーションが SKBitmapEncode メソッドを呼び出すために必要なすべてのものが含まれています。 SKEncodedImageFormat 列挙型のメンバー用の Picker、損失の多いビットマップ形式の品質引数に使用する Slider、ファイル名とフォルダー名のための 2 つの Entry ビュー、ファイルを保存するための Button があります。

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp;assembly=SkiaSharp"
             xmlns:skiaforms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Bitmaps.SaveFileFormatsPage"
             Title="Save Bitmap Formats">

    <StackLayout Margin="10">
        <skiaforms:SKCanvasView PaintSurface="OnCanvasViewPaintSurface"
                                VerticalOptions="FillAndExpand" />

        <Picker x:Name="formatPicker"
                Title="image format"
                SelectedIndexChanged="OnFormatPickerChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type skia:SKEncodedImageFormat}">
                    <x:Static Member="skia:SKEncodedImageFormat.Astc" />
                    <x:Static Member="skia:SKEncodedImageFormat.Bmp" />
                    <x:Static Member="skia:SKEncodedImageFormat.Dng" />
                    <x:Static Member="skia:SKEncodedImageFormat.Gif" />
                    <x:Static Member="skia:SKEncodedImageFormat.Ico" />
                    <x:Static Member="skia:SKEncodedImageFormat.Jpeg" />
                    <x:Static Member="skia:SKEncodedImageFormat.Ktx" />
                    <x:Static Member="skia:SKEncodedImageFormat.Pkm" />
                    <x:Static Member="skia:SKEncodedImageFormat.Png" />
                    <x:Static Member="skia:SKEncodedImageFormat.Wbmp" />
                    <x:Static Member="skia:SKEncodedImageFormat.Webp" />
                </x:Array>
            </Picker.ItemsSource>
        </Picker>

        <Slider x:Name="qualitySlider"
                Maximum="100"
                Value="50" />

        <Label Text="{Binding Source={x:Reference qualitySlider},
                              Path=Value,
                              StringFormat='Quality = {0:F0}'}"
               HorizontalTextAlignment="Center" />

        <StackLayout Orientation="Horizontal">
            <Label Text="Folder Name: "
                   VerticalOptions="Center" />

            <Entry x:Name="folderNameEntry"
                   Text="SaveFileFormats"
                   HorizontalOptions="FillAndExpand" />
        </StackLayout>

        <StackLayout Orientation="Horizontal">
            <Label Text="File Name: "
                   VerticalOptions="Center" />

            <Entry x:Name="fileNameEntry"
                   Text="Sample.xxx"
                   HorizontalOptions="FillAndExpand" />
        </StackLayout>

        <Button Text="Save"
                Clicked="OnButtonClicked">
            <Button.Triggers>
                <DataTrigger TargetType="Button"
                             Binding="{Binding Source={x:Reference formatPicker},
                                               Path=SelectedIndex}"
                             Value="-1">
                    <Setter Property="IsEnabled" Value="False" />
                </DataTrigger>

                <DataTrigger TargetType="Button"
                             Binding="{Binding Source={x:Reference fileNameEntry},
                                               Path=Text.Length}"
                             Value="0">
                    <Setter Property="IsEnabled" Value="False" />
                </DataTrigger>
            </Button.Triggers>
        </Button>

        <Label x:Name="statusLabel"
               Text="OK"
               Margin="10, 0" />
    </StackLayout>
</ContentPage>

分離コード ファイルはビットマップ リソースを読み込み、SKCanvasView を使用して表示します。 そのビットマップは変更されません。 PickerSelectedIndexChanged ハンドラーは、列挙メンバーと同じ拡張子を使用してファイル名を変更します。

public partial class SaveFileFormatsPage : ContentPage
{
    SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(typeof(SaveFileFormatsPage),
        "SkiaSharpFormsDemos.Media.MonkeyFace.png");

    public SaveFileFormatsPage ()
    {
        InitializeComponent ();
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        args.Surface.Canvas.DrawBitmap(bitmap, args.Info.Rect, BitmapStretch.Uniform);
    }

    void OnFormatPickerChanged(object sender, EventArgs args)
    {
        if (formatPicker.SelectedIndex != -1)
        {
            SKEncodedImageFormat imageFormat = (SKEncodedImageFormat)formatPicker.SelectedItem;
            fileNameEntry.Text = Path.ChangeExtension(fileNameEntry.Text, imageFormat.ToString());
            statusLabel.Text = "OK";
        }
    }

    async void OnButtonClicked(object sender, EventArgs args)
    {
        SKEncodedImageFormat imageFormat = (SKEncodedImageFormat)formatPicker.SelectedItem;
        int quality = (int)qualitySlider.Value;

        using (MemoryStream memStream = new MemoryStream())
        using (SKManagedWStream wstream = new SKManagedWStream(memStream))
        {
            bitmap.Encode(wstream, imageFormat, quality);
            byte[] data = memStream.ToArray();

            if (data == null)
            {
                statusLabel.Text = "Encode returned null";
            }
            else if (data.Length == 0)
            {
                statusLabel.Text = "Encode returned empty array";
            }
            else
            {
                bool success = await DependencyService.Get<IPhotoLibrary>().
                    SavePhotoAsync(data, folderNameEntry.Text, fileNameEntry.Text);

                if (!success)
                {
                    statusLabel.Text = "SavePhotoAsync return false";
                }
                else
                {
                    statusLabel.Text = "Success!";
                }
            }
        }
    }
}

ButtonClicked ハンドラーは、すべての実際の作業を行います。 PickerSlider から Encode の 2 つの引数を取得し、前に示したコードを使用して Encode メソッドの SKManagedWStream を作成します。 2 つの Entry ビューは、SavePhotoAsync メソッドのフォルダー名とファイル名を提供します。

このメソッドの大部分は、問題やエラーの処理に当てられます。 Encode で空の配列が作成された場合は、特定のファイル形式がサポートされていないことを意味します。 SavePhotoAsync から false が返された場合、ファイルは正常に保存されませんでした。

実行中のプログラムを次に示します。

ファイル形式を保存する

このスクリーンショットは、これらのプラットフォームでサポートされている 3 つの形式のみを示しています。

  • JPEG
  • PNG
  • WebP

他のすべての形式では、Encode メソッドはストリームに何も書き込まず、結果のバイト配列は空です。

[ファイル保存形式] ページで保存されるビットマップは、600 ピクセルの正方形です。 ピクセルあたり 4 バイトの場合、メモリ内には合計 1,440,000 バイトです。 次の表は、ファイル形式と品質のさまざまな組み合わせについて、ファイル サイズを示しています。

形式 Quality サイズ
PNG 該当なし 492K
JPEG 0 2.95K
50 22.1K
100 206K
WebP 0 2.71K
50 11.9K
100 101K

さまざまな品質設定を試し、結果を調べることができます。

フィンガーペイント アートを保存する

ビットマップの一般的な用途の 1 つに描画プログラムがあり、"シャドウ ビットマップ" と呼ばれる機能を果たします。 すべての描画はビットマップで保持され、プログラムによって表示されます。 ビットマップは描画を保存するのにも便利です。

SkiaSharp での指による描画」の記事では、タッチ追跡を使用して素朴なフィンガー ペイント プログラムを実装する方法について説明しています。 プログラムでサポートされていたのは 1 つの色と 1 つのストローク幅のみでしたが、SKPath オブジェクトのコレクション内の描画全体が保持されています。

サンプルの [Finger Paint with Save] ページでは、SKPath オブジェクトのコレクション内に描画全体を保持することに加え、図面をビットマップにレンダリングして、フォト ライブラリに保存することもできます。

このプログラムの大部分は、元の Finger Paint プログラムに似ています。 1 つの機能強化として、XAML ファイルで [Clear][Save] というラベルのボタンがインスタンス化されるようになりました。

<?xml version="1.0" encoding="utf-8" ?>
<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"
             xmlns:tt="clr-namespace:TouchTracking"
             x:Class="SkiaSharpFormsDemos.Bitmaps.FingerPaintSavePage"
             Title="Finger Paint Save">

    <StackLayout>
        <Grid BackgroundColor="White"
              VerticalOptions="FillAndExpand">
            <skia:SKCanvasView x:Name="canvasView"
                               PaintSurface="OnCanvasViewPaintSurface" />
            <Grid.Effects>
                <tt:TouchEffect Capture="True"
                                TouchAction="OnTouchEffectAction" />
            </Grid.Effects>
        </Grid>

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

        <Button Text="Clear"
                Grid.Row="0"
                Margin="50, 5"
                Clicked="OnClearButtonClicked" />

        <Button Text="Save"
                Grid.Row="1"
                Margin="50, 5"
                Clicked="OnSaveButtonClicked" />

    </StackLayout>
</ContentPage>

分離コード ファイルには、種類が SKBitmapsaveBitmap という名前のフィールドが含まれています。 このビットマップは、表示サーフェイスのサイズが変更されるたびに PaintSurface ハンドラーで作成または再作成されます。 ビットマップを再作成する必要がある場合は、既存のビットマップの内容が新しいビットマップにコピーされ、表示サーフェイスのサイズがどのように変化してもすべてが保持されます。

public partial class FingerPaintSavePage : ContentPage
{
    ···
    SKBitmap saveBitmap;

    public FingerPaintSavePage ()
    {
        InitializeComponent ();
    }

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

        // Create bitmap the size of the display surface
        if (saveBitmap == null)
        {
            saveBitmap = new SKBitmap(info.Width, info.Height);
        }
        // Or create new bitmap for a new size of display surface
        else if (saveBitmap.Width < info.Width || saveBitmap.Height < info.Height)
        {
            SKBitmap newBitmap = new SKBitmap(Math.Max(saveBitmap.Width, info.Width),
                                              Math.Max(saveBitmap.Height, info.Height));

            using (SKCanvas newCanvas = new SKCanvas(newBitmap))
            {
                newCanvas.Clear();
                newCanvas.DrawBitmap(saveBitmap, 0, 0);
            }

            saveBitmap = newBitmap;
        }

        // Render the bitmap
        canvas.Clear();
        canvas.DrawBitmap(saveBitmap, 0, 0);
    }
    ···
}

PaintSurface ハンドラーによって実行される描画は、最後に行われ、ビットマップのレンダリングのみで構成されます。

タッチ処理は、以前のプログラムに似ています。 プログラムには 2 つのコレクション inProgressPathscompletedPaths があり、最後に表示がクリアされてからユーザーが描画したすべてのものが含まれます。 タッチ イベントごとに、OnTouchEffectAction ハンドラーは UpdateBitmap を呼び出します。

public partial class FingerPaintSavePage : ContentPage
{
    Dictionary<long, SKPath> inProgressPaths = new Dictionary<long, SKPath>();
    List<SKPath> completedPaths = new List<SKPath>();

    SKPaint paint = new SKPaint
    {
        Style = SKPaintStyle.Stroke,
        Color = SKColors.Blue,
        StrokeWidth = 10,
        StrokeCap = SKStrokeCap.Round,
        StrokeJoin = SKStrokeJoin.Round
    };
    ···
    void OnTouchEffectAction(object sender, TouchActionEventArgs args)
    {
        switch (args.Type)
        {
            case TouchActionType.Pressed:
                if (!inProgressPaths.ContainsKey(args.Id))
                {
                    SKPath path = new SKPath();
                    path.MoveTo(ConvertToPixel(args.Location));
                    inProgressPaths.Add(args.Id, path);
                    UpdateBitmap();
                }
                break;

            case TouchActionType.Moved:
                if (inProgressPaths.ContainsKey(args.Id))
                {
                    SKPath path = inProgressPaths[args.Id];
                    path.LineTo(ConvertToPixel(args.Location));
                    UpdateBitmap();
                }
                break;

            case TouchActionType.Released:
                if (inProgressPaths.ContainsKey(args.Id))
                {
                    completedPaths.Add(inProgressPaths[args.Id]);
                    inProgressPaths.Remove(args.Id);
                    UpdateBitmap();
                }
                break;

            case TouchActionType.Cancelled:
                if (inProgressPaths.ContainsKey(args.Id))
                {
                    inProgressPaths.Remove(args.Id);
                    UpdateBitmap();
                }
                break;
        }
    }

    SKPoint ConvertToPixel(Point pt)
    {
        return new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
                            (float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
    }

    void UpdateBitmap()
    {
        using (SKCanvas saveBitmapCanvas = new SKCanvas(saveBitmap))
        {
            saveBitmapCanvas.Clear();

            foreach (SKPath path in completedPaths)
            {
                saveBitmapCanvas.DrawPath(path, paint);
            }

            foreach (SKPath path in inProgressPaths.Values)
            {
                saveBitmapCanvas.DrawPath(path, paint);
            }
        }

        canvasView.InvalidateSurface();
    }
    ···
}

UpdateBitmap メソッドは、新しい SKCanvas を作成してクリアし、ビットマップ上のすべてのパスをレンダリングすることによって saveBitmap を再描画します。 最後に、ビットマップをディスプレイに描画できるように canvasView を無効にします。

2 つのボタンのハンドラーを次に示します。 [クリア] ボタンは、両方のパス コレクションをクリアし、saveBitmap を更新し (結果としてビットマップをクリアする)、SKCanvasView を無効にします。

public partial class FingerPaintSavePage : ContentPage
{
    ···
    void OnClearButtonClicked(object sender, EventArgs args)
    {
        completedPaths.Clear();
        inProgressPaths.Clear();
        UpdateBitmap();
        canvasView.InvalidateSurface();
    }

    async void OnSaveButtonClicked(object sender, EventArgs args)
    {
        using (SKImage image = SKImage.FromBitmap(saveBitmap))
        {
            SKData data = image.Encode();
            DateTime dt = DateTime.Now;
            string filename = String.Format("FingerPaint-{0:D4}{1:D2}{2:D2}-{3:D2}{4:D2}{5:D2}{6:D3}.png",
                                            dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.Millisecond);

            IPhotoLibrary photoLibrary = DependencyService.Get<IPhotoLibrary>();
            bool result = await photoLibrary.SavePhotoAsync(data.ToArray(), "FingerPaint", filename);

            if (!result)
            {
                await DisplayAlert("FingerPaint", "Artwork could not be saved. Sorry!", "OK");
            }
        }
    }
}

[保存] ボタン ハンドラーは、SKImage の簡略化された Encode メソッドを使用します。 このメソッドは、PNG 形式を使用してエンコードします。 SKImage オブジェクトが saveBitmap に基づいて作成され、SKData オブジェクトにはエンコードされた PNG ファイルが含まれています。

SKDataToArray メソッドはバイト配列を取得します。 これは、固定フォルダー名と、現在の日時から構成された一意のファイル名と共に、SavePhotoAsync メソッドに渡されます。

動作中のプログラムを次に示します。

Finger Paint Save

サンプルでもよく似た手法が使用されています。 これもフィンガー ペイント プログラムですが、ユーザーが回転するディスク上にペイントし、そのデザインが他の 4 つの象限に再現される点が異なります。 ディスクが回転すると、フィンガー ペイントの色が変わります。

Spin Paint

SpinPaint クラスの Save ボタンは、固定フォルダー名 (SpinPaint) と、日時から構成されたファイル名で画像を保存するという点で、Finger Paint に似ています。