SkiaSharp 비트맵 파일을 저장 하는 중Saving SkiaSharp bitmaps to files

샘플 다운로드 샘플 다운로드Download Sample Download the sample

SkiaSharp 응용 프로그램을 만들거나 수정 비트맵에, 후 응용 프로그램 사용자의 사진 라이브러리에 비트맵 저장 할 수 있습니다.After a SkiaSharp application has created or modified a bitmap, the application might want to save the bitmap to the user's photo library:

비트맵 저장Saving Bitmaps

이 작업은 두 단계:This task encompasses two steps:

  • SkiaSharp 비트맵 JPEG 또는 PNG와 같은 특정 파일 형식으로 데이터를에서 변환 합니다.Converting the SkiaSharp bitmap to data in a particular file format, such as JPEG or PNG.
  • 플랫폼별 코드를 사용 하 여 사진 라이브러리에 결과 저장 합니다.Saving the result to the photo library using platform-specific code.

파일 형식 및 코덱File formats and codecs

대부분 오늘날 인기 있는 비트맵 파일의 저장소 공간을 줄이기 위해 사용 하 여 압축 형식을 지정 합니다.Most of today's popular bitmap file formats use compression to reduce storage space. 두 가지 범주의 압축 기술 이라고 손실 하 고 _무손실_합니다.The two broad categories of compression techniques are called lossy and lossless. 이러한 용어는 압축 알고리즘을 데이터 손실에서 발생 여부를 나타냅니다.These terms indicate whether or not the compression algorithm results in the loss of data.

가장 인기 있는 손실 Joint Photographic Experts Group에서 개발한 형식과 JPEG 라고 합니다.The most popular lossy format was developed by the Joint Photographic Experts Group and is called JPEG. JPEG 압축 알고리즘 이라고 하는 수학 도구를 사용 하 여 이미지를 분석 하는 불연속 코사인 변환, 이미지의 시각적 품질을 유지 하는 데 중요 하지 않은 데이터를 제거 하려고 합니다.The JPEG compression algorithm analyzes the image using a mathematical tool called the discrete cosine transform, and attempts to remove data that is not crucial to preserving the image's visual fidelity. 일반적으로 라고 하는 설정을 사용 하 여 압축 수준을 제어할 수 있습니다 _품질_합니다.The degree of compression can be controlled with a setting generally referred to as quality. 더 높은 품질 설정으로 인해 더 큰 파일입니다.Higher quality settings result in larger files.

반면, 손실 없는 압축 알고리즘을 반복 하 고 픽셀 데이터를 줄이지만의 모든 정보가 손실 되지 않습니다 하는 방식으로 인코딩할 수 있는 패턴에 대 한 이미지를 분석 합니다.In contrast, a lossless compression algorithm analyzes the image for repetition and patterns of pixels that can be encoded in a way that reduces the data but does not result in the loss of any information. 원래 비트맵 데이터를 압축된 파일에서 복원할 수 있습니다.The original bitmap data can be restored entirely from the compressed file. 주 손실 없는 압축 된 파일 형식을 사용 하 여 PNG 이동식 네트워크 그래픽 () 임.The primary lossless compressed file format in use today is Portable Network Graphics (PNG).

일반적으로 알고리즘 방식으로 또는 수동으로 생성 된 이미지에 대 한 PNG 사용 하는 동안 JPEG 사진에 사용 됩니다.Generally, JPEG is used for photographs, while PNG is used for images that have been manually or algorithmically generated. 일부 파일의 크기를 줄이는 손실 없는 압축 알고리즘 반드시 다른 크기를 늘려야 합니다.Any lossless compression algorithm that reduces the size of some files must necessarily increase the size of others. 다행 스럽게도이 증가 크기에만 발생 임의 (또는 임의의) 정보가 많이 포함 된 데이터에 대 한 합니다.Fortunately, this increase in size generally only occurs for data that contains a lot of random (or seemingly random) information.

압축 알고리즘은 두 용어는 보증 하는 복잡 한 압축 및 압축 풀기 프로세스에 설명 합니다.The compression algorithms are complex enough to warrant two terms that describe the compression and decompression processes:

  • 디코딩 — 비트맵 파일 형식을 읽고 압축을 풀어decode — read a bitmap file format and decompress it
  • 인코딩할 — 비트맵을 압축 하 고 비트맵 파일 형식으로 작성 합니다.encode — compress the bitmap and write to a bitmap file format

합니다 SKBitmap 메서드가 여러 개를 포함 하는 클래스 Decode 생성 하는 SKBitmap 압축 된 원본에서 합니다.The SKBitmap class contains several methods named Decode that create an SKBitmap from a compressed source. 필요한 모든 파일 이름, 스트림 또는 바이트 배열을 제공 하는 것입니다.All that's required is to supply a filename, stream, or array of bytes. 디코더는 파일 형식을 결정 하 고 적절 한 내부 디코딩 함수에 전달할 수 있습니다.The decoder can determine the file format and hand it off to the proper internal decoding function.

또한 합니다 SKCodec 클래스 라는 두 가지 방법에 Create 을 만들 수는 SKCodec 압축 된 원본에서 개체 및 응용 프로그램이 디코딩 프로세스에서 더 참여 하도록 허용 합니다.In addition, the SKCodec class has two methods named Create that can create an SKCodec object from a compressed source and allow an application to get more involved in the decoding process. (합니다 SKCodec 클래스는 문서에 표시 됩니다 SkiaSharp 비트맵 애니메이션 애니메이션된 GIF 파일 디코딩와 관련 하 여.)(The SKCodec class is shown in the article Animating SkiaSharp Bitmaps in connection with decoding an animated GIF file.)

비트맵을 인코딩할 때 자세한 정보는 필요 합니다. 인코더는 특정 파일 형식으로 응용 프로그램 (JPEG 또는 PNG 또는 다른)를 사용 하려는 알고 있어야 합니다.When encoding a bitmap, more information is required: The encoder must know the particular file format the application wants to use (JPEG or PNG or something else). 손실 형식으로 필요한 경우 인코딩 원하는 수준의 품질도 알아야 합니다.If a lossy format is desired, the encode must also know the desired level of quality.

합니다 SKBitmap 클래스를 정의 Encode 다음 구문 사용 하 여 메서드:The SKBitmap class defines one Encode method with the following syntax:

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

이 메서드는 곧 자세히 설명 되어 있습니다.This method is described in more detail shortly. 인코드된 비트맵 쓰기 가능한 스트림을에 기록 됩니다.The encoded bitmap is written to a writable stream. ('W'에서 SKWStream "쓰기"는 의미입니다.) 두 번째와 세 번째 인수는 파일 형식을 지정 하 고 (손실 형식)에 대 한 0에서 100 사이의 원하는 품질입니다.(The 'W' in SKWStream stands for "writable".) The second and third arguments specify the file format and (for lossy formats) the desired quality ranging from 0 to 100.

또한 합니다 SKImage SKPixmap 클래스를 정의할 수도 Encode 방법과 약간 더 나쁠 수 있는 것을 선호할 수 있는 합니다.In addition, the SKImage and SKPixmap classes also define Encode methods that are somewhat more versatile, and which you might prefer. 쉽게 만들 수 있습니다는 SKImage 에서 개체를 SKBitmap 사용 하는 정적 개체 SKImage.FromBitmap 메서드.You can easily create an SKImage object from an SKBitmap object using the static SKImage.FromBitmap method. 가져올 수 있습니다는 SKPixmap 에서 개체를 SKBitmap 사용 하 여 개체를 PeekPixels 메서드.You can obtain an SKPixmap object from an SKBitmap object using the PeekPixels method.

중 하나는 Encode 정의한 메서드 SKImage 매개 변수가 없는 및 PNG 형식으로 자동으로 저장 합니다.One of the Encode methods defined by SKImage has no parameters and automatically saves to a PNG format. 해당 매개 변수가 없는 메서드는 사용 하기가 매우 간편 합니다.That parameterless method is very easy to use.

비트맵 파일을 저장 하기 위한 플랫폼 특정 코드Platform-specific code for saving bitmap files

인코딩할 때는 SKBitmap 개체로 특정 파일 형식에 일반적으로 됩니다 수 둔 일종의 스트림 개체와 데이터의 배열입니다.When you encode an SKBitmap object into a particular file format, generally you'll be left with a stream object of some sort, or an array of data. 일부를 Encode 메서드 (정의한 매개 변수가 없는 것을 포함 하 여 SKImage) 반환을 SKData 를 사용 하 여 바이트 배열로 변환 될 수 있는 개체를 ToArray 메서드.Some of the Encode methods (including the one with no parameters defined by SKImage) return an SKData object, which can be converted to an array of bytes using the ToArray method. 이 데이터 파일에 저장 해야 합니다.This data must then be saved to a file.

응용 프로그램 로컬 저장소에서 파일에 저장 하는 표준 사용할 수 있으므로 쉽게 System.IO 클래스 및이 태스크에 대 한 메서드.Saving to a file in application local storage is quite easy because you can use standard System.IO classes and methods for this task. 이 기술 문서에 설명 되어 SkiaSharp 비트맵 애니메이션 관련 하 여 일련의 비트맵 Mandelbrot 집합의 애니메이션.This technique is demonstrated in the article Animating SkiaSharp Bitmaps in connection with animating a series of bitmaps of the Mandelbrot set.

다른 응용 프로그램에서 공유 하도록 파일을 원하는 경우 사용자의 사진 라이브러리에 저장 해야 합니다.If you want the file to be shared by other applications, it must be saved to the user's photo library. 이 작업을 수행 하려면 Xamarin.Forms 사용 하 여 플랫폼 특정 코드 DependencyService 합니다.This task requires platform-specific code and the use of the Xamarin.Forms DependencyService.

SkiaSharpFormsDemo 프로젝트를 SkiaSharpFormsDemos 응용 프로그램 정의 IPhotoLibrary 사용 하는 인터페이스를 DependencyService 클래스.The SkiaSharpFormsDemo project in the SkiaSharpFormsDemos application defines an IPhotoLibrary interface used with the DependencyService class. 이 구문을 정의 SavePhotoAsync 메서드:This defines the syntax of a SavePhotoAsync method:

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

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

또한이 인터페이스를 정의 합니다 PickPhotoAsync 메서드를 사용 하는 장치의 사진 라이브러리에 대 한 플랫폼별 파일 선택기를 엽니다.This interface also defines the PickPhotoAsync method, which is used to open the platform-specific file-picker for the device's photo library.

에 대 한 SavePhotoAsync, 첫 번째 인수는 비트맵, JPEG 또는 PNG와 같은 특정 파일 형식으로 인코딩된 이미 포함 하는 바이트 배열입니다.For SavePhotoAsync, the first argument is an array of bytes that contains the bitmap already encoded into a particular file format, such as JPEG or PNG. 응용 프로그램에 파일 이름 뒤에 다음 매개 변수에 지정 된 특정 폴더를 만들면 모든 비트맵을 격리 하기를 원하는 가능성이 있습니다.It's possible that an application might want to isolate all the bitmaps it creates into a particular folder, which is specified in the next parameter, followed by the file name. 메서드 또는 성공 여부를 나타내는 부울 값을 반환 합니다.The method returns a Boolean indicating success or not.

다음 섹션에서는 설명 하는 방법을 SavePhotoAsync 각 플랫폼에서 구현 됩니다.The following sections discuss how SavePhotoAsync is implemented on each platform.

IOS 구현The iOS implementation

IOS 구현의 SavePhotoAsync 사용 하 여 SaveToPhotosAlbum 메서드의 UIImage:The iOS implementation of SavePhotoAsync uses the SaveToPhotosAlbum method of UIImage:

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

아쉽게도 없기 파일 이름 또는 이미지에 대 한 폴더를 지정 합니다.Unfortunately, there is no way to specify a file name or folder for the image.

합니다 Info.plist iOS 프로젝트의 파일에는 사진 라이브러리에 이미지를 추가한 것입니다 나타내는 키가 필요 합니다.The Info.plist file in the iOS project requires a key indicating that it adds images to the photo library:

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

조심하세요!Watch out! 단순히 사진 라이브러리에 액세스 하기 위한 권한을 키는 매우 유사 하지만 동일 하지는 않습니다.The permission key for simply accessing the photo library is very similar but not the same:

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

Android 구현The Android implementation

Android 구현의 SavePhotoAsync 있는지 먼저 확인 합니다 folder 인수가 null 또는 빈 문자열입니다.The Android implementation of SavePhotoAsync first checks if the folder argument is null or an empty string. 그렇다면 비트맵 사진 라이브러리의 루트 디렉터리에 저장 됩니다.If so, then the bitmap is saved in the root directory of the photo library. 이 고, 그렇지 폴더를 가져오고 존재 하지 않는 경우 만들어집니다.Otherwise, the folder is obtained, and if it doesn't exist, it is created:

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 엄격 하 게 필요 하지 않습니다 하지만 라이브러리 갤러리 보기를 업데이트 하 여 많은 통해 즉시 사진 라이브러리를 확인 하 여 프로그램을 테스트 하는 경우.The call to MediaScannerConnection.ScanFile isn't strictly required, but if you're testing your program by immediately checking the photo library, it helps a lot by updating the library gallery view.

합니다 AndroidManifest.xml 파일에 다음 사용 권한 태그가 필요 합니다.The AndroidManifest.xml file requires the following permission tag:

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

UWP 구현The UWP implementation

UWP 구현의 SavePhotoAsync Android 구현 구조의 매우 유사 합니다.The UWP implementation of SavePhotoAsync is very similar in structure to the Android implementation:

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 파일에 필요한 사진 라이브러리합니다.The Capabilities section of the Package.appxmanifest file requires Pictures Library.

이미지 형식 탐색Exploring the image formats

다음은 Encode 메서드의 SKImage 다시 합니다.Here's the Encode method of SKImage again:

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

SKEncodedImageFormat 일부는 다소 모호한 11 비트맵 파일 형식을 참조 하는 멤버로 구성 된 열거형이 같습니다.SKEncodedImageFormat is an enumeration with members that refer to eleven bitmap file formats, some of which are rather obscure:

  • Astc — 적응 확장성 있는 질감 압축Astc — Adaptive Scalable Texture Compression
  • Bmp — Windows 비트맵Bmp — Windows Bitmap
  • Dng — Adobe 디지털 네거티브Dng — Adobe Digital Negative
  • Gif — Graphics Interchange FormatGif — Graphics Interchange Format
  • Ico — Windows 아이콘 이미지Ico — Windows icon images
  • Jpeg — Joint Photographic Experts GroupJpeg — Joint Photographic Experts Group
  • Ktx — OpenGL Khronos 질감 형식Ktx — Khronos texture format for OpenGL
  • Pkm — Pokémon 파일 저장Pkm — Pokémon save file
  • Png — 이동식 네트워크 그래픽Png — Portable Network Graphics
  • Wbmp — 무선 응용 프로그램 프로토콜 비트맵 형식 (픽셀당 1 비트)Wbmp — Wireless Application Protocol Bitmap Format (1 bit per pixel)
  • Webp — Google WebP 형식Webp — Google WebP format

곧 확인 하겠지만 이러한 3 개만 파일 형식 (Jpeg, Png, 및 Webp) SkiaSharp 실제로 지 합니다.As you'll see shortly, only three of these file formats (Jpeg, Png, and Webp) are actually supported by SkiaSharp.

저장 하는 SKBitmap 라는 개체 bitmap 사용자의 사진 라이브러리도 하려면의 구성원을 SKEncodedImageFormat 라는 열거형 imageFormat 한 (손실 형식) 정수 quality 변수입니다.To save an SKBitmap object named bitmap to the user's photo library, you also need a member of the SKEncodedImageFormat enumeration named imageFormat and (for lossy formats) an integer quality variable. 다음 코드를 사용 하 여 해당 비트맵 이름 사용 하 여 파일 저장 filenamefolder 폴더:You can use the following code to save that bitmap to a file with the name filename in the folder folder:

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 (나타내는 "쓰기 가능 스트림").The SKManagedWStream class derives from SKWStream (which stands for "writable stream"). Encode 메서드는 스트림으로 인코딩된 비트맵 파일을 씁니다.The Encode method writes the encoded bitmap file into that stream. 일부 오류 검사를 수행 해야를 해당 코드의 주석을 참조 하십시오.The comments in that code refer to some error checking you might need to perform.

저장 파일 형식 페이지에 SkiaSharpFormsDemos 응용 프로그램에서 유사한 코드를 사용 하 여 다양 한 형식에서 비트맵을 저장 하는 실험을 수행할 수 있도록 합니다.The Save File Formats page in the SkiaSharpFormsDemos application uses similar code to allow you to experiment with saving a bitmap in the various formats.

XAML 파일에 포함 되어는 SKCanvasView 비트맵을 표시 하는, 응용 프로그램을 호출 해야 하지만 페이지의 나머지 부분에 있는 모든 항목을 Encode 메서드의 SKBitmap합니다.The XAML file contains an SKCanvasView that displays a bitmap, while the rest of the page contains everything the application needs to call the Encode method of SKBitmap. 있기를 Picker 의 멤버에 대 한는 SKEncodedImageFormat 열거형을 Slider 손실 비트맵 형식에 대 한 품질 인수에 대 한 두 Entry 파일 이름 및 폴더 이름에 대 한 뷰 및 Button 파일을 저장 하는 것에 대 한 합니다.It has a Picker for a member of the SKEncodedImageFormat enumeration, a Slider for the quality argument for lossy bitmap formats, two Entry views for a filename and folder name, and a Button for saving the file.

<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 표시 합니다.The code-behind file loads a bitmap resource and uses the SKCanvasView to display it. 비트맵 변경 되지 않습니다.That bitmap never changes. SelectedIndexChanged 에 대 한 처리기를 Picker 열거형 멤버와 같은 확장명을 가진 파일을 수정 합니다.The SelectedIndexChanged handler for the Picker modifies the filename with an extension that is the same as the enumeration member:

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 모든 실제 작동 합니다.The Clicked handler for the Button does all the real work. 에 대 한 두 개의 인수를 가져와서 Encode 에서 합니다 PickerSlider, 다음 만들려면 앞서 살펴본 코드를 사용 하 여는 SKManagedWStream 에 대 한는 Encode 메서드.It obtains two arguments for Encode from the Picker and Slider, and then uses the code shown earlier to create an SKManagedWStream for the Encode method. 두 개의 Entry 보기에 대 한 폴더와 파일 이름을 제공 합니다 SavePhotoAsync 메서드.The two Entry views furnish folder and file names for the SavePhotoAsync method.

이 메서드는 대부분 할애 되는데 문제 또는 오류를 처리 합니다.Most of this method is devoted to handling problems or errors. 경우 Encode 빈 배열을 만듭니다. 즉, 특정 파일 형식은 지원 되지 않습니다.If Encode creates an empty array, it means that the particular file format isn't supported. 하는 경우 SavePhotoAsync 반환 false, 다음 파일을 성공적으로 저장 되지 않았습니다.If SavePhotoAsync returns false, then the file wasn't successfully saved.

실행 중인 프로그램이 다음과 같습니다.Here is the program running:

파일 형식 저장Save File Formats

스크린샷에 이러한 플랫폼에서 지원 되는 세 개의 형식을 보여 줍니다.That screenshot shows the only three formats that are supported on these platforms:

  • JPEGJPEG
  • PNGPNG
  • WebPWebP

다른 모든 형식에 대해는 Encode 메서드 nothing 스트림에 쓰고 결과 바이트 배열이 비어 있는 것입니다.For all the other formats, the Encode method writes nothing into the stream and the resultant byte array is empty.

비트맵은를 파일 형식은 저장 페이지 저장은 600 픽셀 사각형이 됩니다.The bitmap that the Save File Formats page saves is 600-pixels square. 픽셀당 4 바이트를 사용 하 여 총 메모리에 1,440,000 바이트입니다.With 4 bytes per pixel, that's a total of 1,440,000 bytes in memory. 다음 표에서 파일 형식 및 품질의 다양 한 조합에 대 한 파일 크기를 보여 줍니다.The following table shows the file size for various combinations of file format and quality:

형식Format 품질Quality 크기Size
PNGPNG N/AN/A 492 K492K
JPEGJPEG 00 2.95 K2.95K
5050 22.1 K22.1K
100100 206 K206K
WebPWebP 00 2.71 K2.71K
5050 11.9 K11.9K
100100 101 K101K

다양 한 품질 설정으로 실험 하는 결과 검토할 수 있습니다.You can experiment with various quality settings and examine the results.

저장 finger-paint 아트Saving finger-paint art

비트맵의 일반적 용도 중 하나는 그리기 프로그램 라는 대로 작동 하는지는 _섀도 비트맵_합니다.One common use of a bitmap is in drawing programs, where it functions as something called a shadow bitmap. 모든 그리기 프로그램에 의해 표시 되는 비트맵에 유지 됩니다.All the drawing is retained on the bitmap, which is then displayed by the program. 비트맵도 유용 그리기를 저장 합니다.The bitmap also comes in handy for saving the drawing.

합니다 SkiaSharp에서 손가락 페인팅 문서 추적 기본 손가락 프로그램 구현에서 터치를 사용 하는 방법을 설명 합니다.The Finger Painting in SkiaSharp article demonstrated how to use touch tracking to implement a primitive finger-painting program. 하나의 색 및 하나의 스트로크 너비 프로그램 지원 되지만 컬렉션의 전체 그리기 유지 SKPath 개체입니다.The program supported only one color and only one stroke width, but it retained the entire drawing in a collection of SKPath objects.

합니다 손가락으로 그리기와 저장 페이지에 SkiaSharpFormsDemos 샘플은 또한 컬렉션의 전체 그리기 유지 SKPath 개체 하지만 또한 사진 라이브러리에 저장할 수 있는 비트맵에 드로잉을 렌더링 합니다.The Finger Paint with Save page in the SkiaSharpFormsDemos sample also retains the entire drawing in a collection of SKPath objects, but it also renders the drawing on a bitmap, which it can save to your photo library.

이 프로그램의 대부분은 원래 비슷합니다 손가락으로 그리기 프로그램입니다.Much of this program is similar to the original Finger Paint program. 개선 사항 중 하나는 XAML 파일에 이제 라는 레이블이 있는 단추가 인스턴스화합니다 명확한 하 고 저장:One enhancement is that the XAML file now instantiates buttons labeled Clear and 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합니다.The code-behind file maintains a field of type SKBitmap named saveBitmap. 이 비트맵을 생성 또는 다시는 PaintSurface 디스플레이의 크기를 화면 변경 될 때마다 처리기입니다.This bitmap is created or recreated in the PaintSurface handler whenever the size of the display surface changes. 비트맵을 만들어야 하는 경우 모든 화면 크기에서 변경 하는 방법에 관계 없이 유지 됩니다 있도록 기존 비트맵 이미지의 내용은 새 비트맵에 복사 됩니다.If the bitmap needs to be recreated, the contents of the existing bitmap are copied to the new bitmap so that everything is retained no matter how the display surface changes in size:

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 처리기 맨 끝에서 발생 하 고 으로만 이루어져 비트맵을 렌더링 합니다.The drawing done by the PaintSurface handler occurs at the very end, and consists solely of rendering the bitmap.

터치를 처리 하는 이전 프로그램와 비슷합니다.The touch processing is similar to the earlier program. 프로그램에는 두 개의 컬렉션 유지 관리 inProgressPathscompletedPaths를 포함 하는 모든 표시의 선택이 취소 된 마지막 시간 이후 사용자가 그린 합니다.The program maintains two collections, inProgressPaths and completedPaths, that contain everything the user has drawn since the last time the display was cleared. 각 터치 이벤트에 대 한 합니다 OnTouchEffectAction 처리기 호출 UpdateBitmap:For each touch event, the OnTouchEffectAction handler calls 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 메서드 다시 그리기 횟수 saveBitmapSKCanvas를 지운 다음 비트맵의 모든 경로 렌더링 합니다.The UpdateBitmap method redraws saveBitmap by creating a new SKCanvas, clearing it, and then rendering all the paths on the bitmap. 무효화 하 여 마지막으로 canvasView 디스플레이에서 비트맵을 그릴 수 있도록 합니다.It concludes by invalidating canvasView so that the bitmap can be drawn on the display.

두 개의 단추에 대 한 처리기는 다음과 같습니다.Here are the handlers for the two buttons. 합니다 지우기 단추 두 경로 컬렉션을 업데이트를 지운 saveBitmap (결과 비트맵을 선택 취소)을 무효화 하 고는 SKCanvasView:The Clear button clears both path collections, updates saveBitmap (which results in clearing the bitmap), and invalidates the 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");
            }
        }
    }
}

합니다 저장 단추 처리기를 사용 하 여 단순한 Encode 메서드에서 SKImage합니다.The Save button handler uses the simplified Encode method from SKImage. 이 메서드는 PNG 형식으로 인코딩합니다.This method encodes using the PNG format. 합니다 SKImage 개체를 기반으로 생성 됩니다 saveBitmap, 및 SKData 인코딩된 PNG 파일을 포함 하는 개체입니다.The SKImage object is created based on saveBitmap, and the SKData object contains the encoded PNG file.

합니다 ToArray 메서드의 SKData 바이트의 배열을 가져옵니다.The ToArray method of SKData obtains an array of bytes. 에 전달 되는이 SavePhotoAsync 고정된 폴더 이름 및 현재 날짜 및 시간에서 생성 된 고유한 파일 이름을 함께 메서드.This is what is passed to the SavePhotoAsync method, along with a fixed folder name, and a unique filename constructed from the current date and time.

작업에서 프로그램이 다음과 같습니다.Here's the program in action:

저장 그리기 손가락Finger Paint Save

매우 유사한 기술을 합니다 SpinPaint 샘플입니다.A very similar technique is used in the SpinPaint sample. 그런 다음 해당 다른 4 개의 사분면에 디자인을 재현 하는 회전 디스크에 그리는 사용자 한다는 손가락 프로그램 이기도 합니다.This is also a finger-painting program except that the user paints on a spinning disk that then reproduces the designs on its other four quadrants. 디스크로 손가락 그리기 변경의 색이 회전 합니다.The color of the finger paint changes as the disk is spinning:

그리기를 스핀업Spin Paint

합니다 저장 단추의 SpinPaint 와 비슷하지만 손가락으로 그리기 고정된 폴더 이름에 해당 이미지를 저장 하는 (SpainPaint)에서 생성 된 파일 이름 및 날짜 및 시간입니다.The Save button of SpinPaint class is similar to Finger Paint in that it saves the image to a fixed folder name (SpainPaint) and a filename constructed from the date and time.