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
の実装には、UIImage
の SaveToPhotosAlbum
メソッドを使用します。
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 セクションには、画像ライブラリが必要です。
画像形式の調査
SKImage
の Encode
メソッドをもう一度示します。
public Boolean Encode (SKWStream dst, SKEncodedImageFormat format, Int32 quality)
SKEncodedImageFormat
は、11 個のビットマップ ファイル形式を参照するメンバーを含む列挙体であり、その一部はかなり不明瞭です。
Astc
— アダプティブ スケーラブル テクスチャ圧縮Bmp
— Windows ビットマップDng
— Adobe Digital NegativeGif
— グラフィックス交換形式Ico
— Windows アイコンの画像Jpeg
— Joint Photographic Experts GroupKtx
— OpenGL 用の Khronos テクスチャ形式Pkm
— GrafX2 のカスタム形式Png
— ポータブル ネットワーク グラフィックス (PNG)Wbmp
— ワイヤレス アプリケーション プロトコル ビットマップ形式 (ピクセルあたり 1 ビット)Webp
— Google WebP 形式
後ほど説明しますが、これらのファイル形式のうち、3 つ (Jpeg
、Png
、Webp
) だけが 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
が含まれていますが、ページの残りの部分には、アプリケーションが SKBitmap
の Encode
メソッドを呼び出すために必要なすべてのものが含まれています。 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
を使用して表示します。 そのビットマップは変更されません。 Picker
の SelectedIndexChanged
ハンドラーは、列挙メンバーと同じ拡張子を使用してファイル名を変更します。
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!";
}
}
}
}
}
Button
の Clicked
ハンドラーは、すべての実際の作業を行います。 Picker
と Slider
から 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>
分離コード ファイルには、種類が SKBitmap
で saveBitmap
という名前のフィールドが含まれています。 このビットマップは、表示サーフェイスのサイズが変更されるたびに 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 つのコレクション inProgressPaths
と completedPaths
があり、最後に表示がクリアされてからユーザーが描画したすべてのものが含まれます。 タッチ イベントごとに、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 ファイルが含まれています。
SKData
の ToArray
メソッドはバイト配列を取得します。 これは、固定フォルダー名と、現在の日時から構成された一意のファイル名と共に、SavePhotoAsync
メソッドに渡されます。
動作中のプログラムを次に示します。
サンプルでもよく似た手法が使用されています。 これもフィンガー ペイント プログラムですが、ユーザーが回転するディスク上にペイントし、そのデザインが他の 4 つの象限に再現される点が異なります。 ディスクが回転すると、フィンガー ペイントの色が変わります。
SpinPaint
クラスの Save ボタンは、固定フォルダー名 (SpinPaint) と、日時から構成されたファイル名で画像を保存するという点で、Finger Paint に似ています。