파일에 SkiaSharp 비트맵 저장

Download Sample 샘플 다운로드

SkiaSharp 애플리케이션이 비트맵을 만들거나 수정한 후 애플리케이션은 사용자의 사진 라이브러리에 비트맵을 저장할 수 있습니다.

Saving Bitmaps

이 작업에는 다음 두 단계가 포함됩니다.

  • SkiaSharp 비트맵을 JPEG 또는 PNG와 같은 특정 파일 형식의 데이터로 변환합니다.
  • 플랫폼별 코드를 사용하여 결과를 사진 라이브러리에 저장합니다.

파일 형식 및 코덱

오늘날 널리 사용되는 대부분의 비트맵 파일 형식은 압축을 사용하여 스토리지 공간을 줄입니다. 압축 기술의 두 가지 광범위한 범주를 손실 및 무손이라고 합니다. 이러한 용어는 압축 알고리즘으로 인해 데이터가 손실되는지 여부를 나타냅니다.

가장 인기있는 손실 형식은 공동 사진 전문가 그룹에 의해 개발되었으며 JPEG라고합니다. JPEG 압축 알고리즘은 불연속 코사인 변환이라는 수학 도구를 사용하여 이미지를 분석하고 이미지의 시각적 충실도를 유지하는 데 중요하지 않은 데이터를 제거하려고 시도합니다. 일반적으로 품질이라고 하는 설정을 사용하여 압축 수준을 제어할 수 있습니다. 품질 설정이 높아질수록 파일이 커집니다.

반면 무손실 압축 알고리즘은 데이터를 줄이지만 정보가 손실되지 않는 방식으로 인코딩할 수 있는 픽셀의 반복 및 패턴을 위해 이미지를 분석합니다. 원래 비트맵 데이터는 압축된 파일에서 완전히 복원할 수 있습니다. 현재 사용 중인 기본 무손실 압축 파일 형식은 PNG(이식 가능한 네트워크 그래픽)입니다.

일반적으로 JPEG는 사진에 사용되고 PNG는 수동으로 또는 알고리즘으로 생성된 이미지에 사용됩니다. 일부 파일의 크기를 줄이는 무손실 압축 알고리즘은 반드시 다른 파일의 크기를 늘려야 합니다. 다행히 이러한 크기 증가는 일반적으로 많은 임의(또는 임의로 보이는) 정보를 포함하는 데이터에 대해서만 발생합니다.

압축 알고리즘은 압축 및 압축 해제 프로세스를 설명하는 두 가지 용어를 보증할 만큼 복잡합니다.

  • decode — 비트맵 파일 형식을 읽고 압축을 풉니다.
  • encode — 비트맵 압축 및 비트맵 파일 형식에 쓰기

SKBitmap 클래스에는 압축된 소스에서 만드는 SKBitmap 여러 메서드가 포함되어 Decode 있습니다. 파일 이름, 스트림 또는 바이트 배열을 제공하는 것만 있으면 됩니다. 디코더는 파일 형식을 결정하고 적절한 내부 디코딩 함수에 전달할 수 있습니다.

또한 SKCodec 클래스에는 압축된 소스에서 개체를 SKCodec 만들고 애플리케이션이 디코딩 프로세스에 더 많이 참여할 수 있도록 하는 두 가지 메서드가 있습니다Create. (이 SKCodec 클래스는 애니메이션 GIF 파일 디코딩과 관련하여 SkiaSharp 비트맵에 애니메이션 효과를 주는 문서에 나와 있습니다.)

비트맵을 인코딩할 때 추가 정보가 필요합니다. 인코더는 애플리케이션에서 사용하려는 특정 파일 형식(JPEG 또는 PNG 또는 다른 파일)을 알고 있어야 합니다. 손실 형식이 필요한 경우 인코딩은 원하는 품질 수준도 알고 있어야 합니다.

클래스는 SKBitmap 다음 구문을 사용하여 하나의 Encode 메서드를 정의합니다.

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

이 메서드는 곧 더 자세히 설명되어 있습니다. 인코딩된 비트맵은 쓰기 가능한 스트림에 기록됩니다. ('W'는 SKWStream "쓰기 가능"을 의미합니다.) 두 번째 및 세 번째 인수는 파일 형식과 (손실 형식의 경우) 0에서 100 사이의 원하는 품질을 지정합니다.

또한 SKImage 클래스 및 SKPixmap 클래스는 좀 더 다양하고 선호하는 메서드를 정의 Encode 합니다. 정적 메서드를 SKImage 사용하여 개체에서 개체를 SKBitmap 쉽게 만들 수 SKImage.FromBitmap 있습니다. 메서드를 SKPixmap 사용하여 개체에서 개체를 SKBitmapPeekPixels 가져올 수 있습니다.

정의된 SKImage 메서드 중 Encode 하나에는 매개 변수가 없으며 PNG 형식으로 자동으로 저장됩니다. 매개 변수가 없는 메서드는 사용하기가 매우 쉽습니다.

비트맵 파일을 저장하기 위한 플랫폼별 코드

개체를 SKBitmap 특정 파일 형식으로 인코딩하는 경우 일반적으로 일종의 스트림 개체 또는 데이터 배열이 남게 됩니다. Encode 일부 메서드(매개 변수가 정의SKImage되지 않은 메서드 포함)는 메서드를 사용하여 바이트 배열로 변환할 수 있는 개체를 ToArray 반환 SKData 합니다. 그런 다음 이 데이터를 파일에 저장해야 합니다.

이 작업에 표준 System.IO 클래스와 메서드를 사용할 수 있으므로 애플리케이션 로컬 스토리지의 파일에 저장하는 것은 매우 쉽습니다. 이 기술은 일련의 만델브로트 세트 비트맵에 애니메이션 효과를 주는 것과 관련하여 SkiaSharp 비트맵에 애니메이션 효과를 주는 문서에서 설명합니다.

파일을 다른 응용 프로그램에서 공유하려면 사용자의 사진 라이브러리에 저장해야 합니다. 이 작업에는 플랫폼별 코드와 .Xamarin.FormsDependencyService

SkiaSharpFormsDemos 애플리케이션의 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 구현

인수가 SavePhotoAsync 빈 문자열인 경우 folder 첫 번째 검사 Android 구현입니다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 파일의 기능 섹션에는 그림 라이브러리가 필요합니다.

이미지 형식 탐색

다시 메서드는 EncodeSKImage 다음과 같습니다.

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

SKEncodedImageFormat 는 11개의 비트맵 파일 형식을 참조하는 멤버를 포함하는 열거형이며, 그 중 일부는 다소 모호합니다.

  • Astc — 적응형 확장 가능한 텍스처 압축
  • Bmp — Windows 비트맵
  • Dng — Adobe Digital Negative
  • Gif — 그래픽 교환 형식
  • Ico — Windows 아이콘 이미지
  • Jpeg - 공동 사진 전문가 그룹
  • Ktx — OpenGL의 Khronos 텍스처 형식
  • Pkm — GrafX2에 대한 사용자 지정 형식
  • Png — 이식 가능한 네트워크 그래픽
  • Wbmp — 무선 애플리케이션 프로토콜 비트맵 형식(픽셀당 1비트)
  • Webp — Google WebP 형식

곧 볼 수 있듯이 이러한 파일 형식(JpegPngWebp) 중 3개만 SkiaSharp에서 실제로 지원됩니다.

사용자의 사진 라이브러리에 명명된 bitmap 개체를 저장 SKBitmap 하려면 명명 imageFormat 된 열거형의 SKEncodedImageFormat 멤버와 (손실 형식의 경우) 정 quality 수 변수도 필요합니다. 다음 코드를 사용하여 해당 비트맵을 폴더에 이름이 filenamefolder 있는 파일에 저장할 수 있습니다.

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 파생됩니다. 이 메서드는 Encode 인코딩된 비트맵 파일을 해당 스트림에 씁니다. 해당 코드의 주석은 수행해야 할 수 있는 몇 가지 오류 검사 참조합니다.

SkiaSharpFormsDemos 애플리케이션의 파일 형식 저장 페이지에서 비슷한 코드를 사용하여 다양한 형식으로 비트맵을 저장하는 실험을 수행할 수 있습니다.

XAML 파일에는 비트맵을 표시하는 파일이 포함되어 SKCanvasView 있으며, 나머지 페이지에는 애플리케이션이 메서드SKBitmap를 호출하는 데 필요한 모든 항목이 Encode 포함되어 있습니다. Picker 열거형의 SKEncodedImageFormat 멤버, Slider 손실된 비트맵 형식에 대한 품질 인수, 파일 이름 및 폴더 이름에 대한 두 개의 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 표시합니다. 해당 비트맵은 변경되지 않습니다. SelectedIndexChanged 해당 Picker 처리기는 열거형 멤버와 동일한 확장명으로 파일 이름을 수정합니다.

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!";
                }
            }
        }
    }
}

Clicked 처리기는 Button 모든 실제 작업을 수행합니다. andSlider에서 두 개의 인수를 EncodePicker 가져온 다음, 앞에서 보여 준 코드를 사용하여 메서드에 대한 인수를 SKManagedWStreamEncode 만듭니다. 두 Entry 뷰는 메서드의 SavePhotoAsync 폴더와 파일 이름을 제공합니다.

이 방법의 대부분은 문제 또는 오류를 처리하는 데 전념합니다. 빈 배열을 만드는 경우 Encode 특정 파일 형식이 지원되지 않음을 의미합니다. 반환false하는 경우 SavePhotoAsync 파일이 성공적으로 저장되지 않았습니다.

실행 중인 프로그램은 다음과 같습니다.

Save File Formats

이 스크린샷은 이러한 플랫폼에서 지원되는 세 가지 형식만 보여줍니다.

  • JPEG
  • PNG
  • WebP

다른 모든 형식의 경우 메서드는 Encode 스트림에 아무 것도 쓰지 않으며 결과 바이트 배열은 비어 있습니다.

파일 형식 저장 페이지에서 저장하는 비트맵은 600픽셀 정사각형입니다. 픽셀당 4바이트를 사용하는 경우 메모리의 총 1,440,000바이트입니다. 다음 표에서는 파일 형식과 품질의 다양한 조합에 대한 파일 크기를 보여 줍니다.

형식 품질 크기
PNG 해당 없음 492K
JPEG 0 2.95K
50 22.1K
100 206K
WebP 0 2.71K
50 11.9K
100 101K

다양한 품질 설정을 실험하고 결과를 검사할 수 있습니다.

손가락 페인트 아트 저장

비트맵의 일반적인 용도 중 하나는 그리기 프로그램에서 섀도 비트맵이라고 하는 함수입니다. 모든 드로잉은 비트맵에 유지된 다음 프로그램에 의해 표시됩니다. 비트맵은 드로잉을 저장하는 데에도 편리합니다.

SkiaSharp 문서의 핑거 그림판 터치 추적을 사용하여 기본 손가락 그리기 프로그램을 구현하는 방법을 보여 줍니다. 이 프로그램은 하나의 색과 하나의 스트로크 너비만 지원했지만 개체 컬렉션 SKPath 에 전체 드로잉을 유지했습니다.

SkiaSharpFormsDemos 샘플의 저장 페이지가 있는 Finger 그림판 개체 컬렉션 SKPath 에 전체 드로잉을 유지하지만, 드로잉을 비트맵에 렌더링하여 사진 라이브러리에 저장할 수도 있습니다.

이 프로그램의 대부분은 원래 손가락 그림판 프로그램과 유사합니다. 향상된 기능 중 하나는 XAML 파일에서 지우기 및 저장이라는 레이블이 지정된 단추를 인스턴스화한다는 것입니다.

<?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>

코드 숨김 파일은 라는 saveBitmap형식 SKBitmap 의 필드를 기본. 이 비트맵은 표시 화면의 크기가 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 수행하는 그리기는 맨 끝에서 발생하며 비트맵 렌더링으로만 구성됩니다.

터치 처리는 이전 프로그램과 비슷합니다. 프로그램은 기본 두 개의 컬렉션을 포함하고 있으며completedPaths, inProgressPaths 이 컬렉션에는 디스플레이가 마지막으로 지워진 이후 사용자가 그린 모든 항목이 포함됩니다. 각 터치 이벤트에 대해 처리기는 다음을 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();
    }
    ···
}

메서드는 UpdateBitmapSKCanvas메서드를 saveBitmap 만들고 지운 다음 비트맵의 모든 경로를 렌더링하여 다시 그렸습니다. 비트맵을 디스플레이에 그릴 수 있도록 무효화하여 canvasView 마무리합니다.

다음은 두 단추에 대한 처리기입니다. 지우기 단추는 두 경로 컬렉션을 모두 지우고, 업데이트 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 파일이 포함됩니다.

ToArray 바이트 배열을 가져오는 메서드 SKData 입니다. 이는 고정 폴더 SavePhotoAsync 이름 및 현재 날짜 및 시간에서 생성된 고유한 파일 이름과 함께 메서드에 전달됩니다.

실행 중인 프로그램은 다음과 같습니다.

Finger Paint Save

Spin그림판 샘플에서 매우 유사한 기술이 사용됩니다. 사용자가 회전 디스크에 페인트한 다음 다른 4개의 사분면에서 디자인을 재현한다는 점을 제외하고 손가락 그리기 프로그램이기도 합니다. 디스크가 회전할 때 손가락 페인트의 색이 변경됩니다.

Spin Paint

클래스의 SpinPaint 저장 단추는 이미지를 고정 폴더 이름(스페인그림판) 및 날짜 및 시간에서 생성된 파일 이름에 저장한다는 측면에서 Finger 그림판 유사합니다.