Сохранение растровых карт SkiaSharp в файлы

После создания или изменения растрового изображения приложение SkiaSharp может потребоваться сохранить растровое изображение в библиотеке фотографий пользователя:

Сохранение растровых карт

Эта задача включает два шага.

  • Преобразование растрового изображения SkiaSharp в данные в определенном формате файла, например JPEG или PNG.
  • Сохранение результата в библиотеке фотографий с помощью кода для конкретной платформы.

Форматы файлов и кодеки

Большинство популярных форматов файлов растрового изображения используют сжатие для уменьшения места хранения. Две широкие категории методов сжатия называются потерей и потерей. Эти термины указывают, приводит ли алгоритм сжатия к потере данных.

Самый популярный формат потери был разработан Группой совместных фотографических экспертов и называется JPEG. Алгоритм сжатия JPEG анализирует изображение с помощью математического средства, называемого дискретным преобразованием косинуса, и пытается удалить данные, которые не имеют решающего значения для сохранения визуальной точности изображения. Степень сжатия можно контролировать с помощью параметра, который обычно называется качеством. Более высокие параметры качества приводят к более крупным файлам.

В отличие от этого, алгоритм сжатия без потери анализирует изображение для повторения и шаблонов пикселей, которые могут быть закодированы таким образом, чтобы уменьшить данные, но не приводит к потере любой информации. Исходные растровые данные можно восстановить полностью из сжатого файла. Основной формат сжатых сжатых файлов без потери, используемый сегодня, — переносимая сетевая графика (PNG).

Как правило, JPEG используется для фотографий, а PNG используется для изображений, которые были созданы вручную или алгоритмически. Любой алгоритм сжатия без потери, уменьшающий размер некоторых файлов, должен обязательно увеличить размер других файлов. К счастью, это увеличение размера обычно происходит только для данных, содержащих много случайных (или, казалось бы, случайных) сведений.

Алгоритмы сжатия достаточно сложны, чтобы гарантировать два термина, описывающие процессы сжатия и декомпрессии:

  • декодирование — чтение формата растрового файла и распаковка его
  • кодирование — сжатие растрового изображения и запись в формат файла растрового изображения

Класс SKBitmap содержит несколько методов, которые Decode создаются SKBitmap из сжатого источника. Все, что необходимо, — указать имя файла, поток или массив байтов. Декодатор может определить формат файла и передать его в соответствующую внутреннюю функцию декодирования.

Кроме того, SKCodec класс имеет два метода с именем Create , которые могут создать SKCodec объект из сжатого источника и разрешить приложению более активно участвовать в процессе декодирования. (Класс SKCodec показан в статье Animating SkiaSharp Bitmaps в связи с декодированием анимированного GIF-файла.)

При кодировании растрового изображения требуется дополнительная информация: кодировщик должен знать конкретный формат файла, который приложение хочет использовать (JPEG или PNG или что-то другое). Если требуется формат потери, код должен также знать требуемый уровень качества.

Класс SKBitmap определяет один Encode метод со следующим синтаксисом:

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

Этот метод подробно описан в ближайшее время. Закодированное растровое изображение записывается в записываемый поток. (W в SKWStream означает "записываемый".) Второй и третий аргументы указывают формат файла и (для форматов потери) требуемое качество от 0 до 100.

Кроме того, классы SKImageSKPixmap также определяют Encode методы, которые являются несколько более универсальными и которые можно предпочесть. Вы можете легко создать SKImage объект из SKBitmap объекта с помощью статического SKImage.FromBitmap метода. Объект можно получить SKPixmap из SKBitmap объекта с помощью PeekPixels метода.

Один из методов, определенных Encode без SKImage параметров, и автоматически сохраняется в формате PNG. Этот метод без параметров очень прост в использовании.

Код для конкретной платформы для сохранения растровых файлов

При кодировании SKBitmap объекта в определенном формате файла обычно вы останетесь с объектом потока определенного типа или массивом данных. Encode Некоторые методы (включая один без параметров, определенных параметромSKImage) возвращают SKData объект, который можно преобразовать в массив байтов с помощью ToArray метода. Затем эти данные должны быть сохранены в файле.

Сохранение файла в локальном хранилище приложения довольно легко, так как для этой задачи можно использовать стандартные System.IO классы и методы. Этот метод демонстрируется в статье Animating SkiaSharp Bitmaps в связи с анимацией ряда растровых изображений набора Mandelbrot.

Если вы хотите, чтобы файл был предоставлен другим приложениям, его необходимо сохранить в библиотеке фотографий пользователя. Для этой задачи требуется код для конкретной Xamarin.FormsDependencyServiceплатформы и использование .

Проект SkiaSharpFormsDemo в примере приложения определяет интерфейс, используемый IPhotoLibrary с классом DependencyService . Это определяет синтаксис SavePhotoAsync метода:

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

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

Этот интерфейс также определяет PickPhotoAsync метод, который используется для открытия средства выбора файлов для конкретной платформы для фототеки устройства.

Для SavePhotoAsyncэтого первым аргументом является массив байтов, содержащий растровое изображение, которое уже закодировано в определенный формат файла, например JPEG или PNG. Возможно, что приложению может потребоваться изолировать все точечные изображения, создаваемые в определенную папку, которая указана в следующем параметре, за которым следует имя файла. Метод возвращает логическое значение, указывающее на успех или нет.

В следующих разделах описывается SavePhotoAsync реализация на каждой платформе.

Реализация iOS

Реализация iOS SavePhotoAsync использует SaveToPhotosAlbum метод 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;
    }
}

К сожалению, невозможно указать имя файла или папку для образа.

Файл Info.plist в проекте iOS требует ключа, указывающего, что он добавляет изображения в библиотеку фотографий:

<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 требуется библиотека изображений.

Изучение форматов изображений

Вот метод повтора EncodeSKImage :

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

SKEncodedImageFormat — это перечисление с элементами, ссылающимися на одиннадцать форматов растровых файлов, некоторые из которых являются довольно неясными:

  • Astc — адаптивное сжатие текстуры с возможностью масштабирования
  • Bmp — Растровое изображение Windows
  • Dng — Adobe Digital Negative
  • Gif — формат обмена графикой
  • Ico — изображения значков Windows
  • Jpeg — Совместная группа фотографических экспертов
  • Ktx — формат текстуры Khronos для OpenGL
  • Pkm — настраиваемый формат для GrafX2
  • Png — переносимая сетевая графика
  • Wbmp — формат растрового изображения протокола беспроводного приложения (1 бит на пиксель)
  • Webp — формат Google WebP

Как вы увидите вскоре, только три из этих форматов файлов (Jpeg, Pngи Webp) на самом деле поддерживаются SkiaSharp.

Чтобы сохранить SKBitmap объект с именем bitmap в библиотеке фотографий пользователя, также требуется элемент SKEncodedImageFormat перечисления с именем imageFormat и (для форматов потери) целочисленной quality переменной. Для сохранения растрового рисунка в файле с именем filename в папке 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 (который обозначает "записываемый поток"). Метод Encode записывает кодированный растровый файл в этот поток. Примечания в этом коде ссылаются на некоторые ошибки, проверка может потребоваться выполнить.

Страница "Сохранить форматы файлов" в примере приложения использует аналогичный код, чтобы можно было экспериментировать с сохранением растрового изображения в различных форматах.

XAML-файл содержит SKCanvasView растровое изображение, а остальная часть страницы содержит все, что приложение должно вызывать Encode метод SKBitmap. Он имеет 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 выполнения всех реальных работ. Он получает два аргумента Encode из Picker и Sliderзатем использует код, показанный ранее, Encode для создания SKManagedWStream метода. Два Entry представления предоставляет имена папок и файлов для SavePhotoAsync метода.

Большая часть этого метода посвящена обработке проблем или ошибок. Если Encode создается пустой массив, это означает, что конкретный формат файла не поддерживается. Если SavePhotoAsync возвращается false, файл не был успешно сохранен.

Ниже приведена программа:

Сохранение форматов файлов

Снимок экрана: только три формата, поддерживаемые на этих платформах:

  • JPEG
  • PNG
  • Webp

Для всех других форматов Encode метод ничего не записывает в поток, а результирующий массив байтов пуст.

Растровое изображение, которое сохраняет страница "Форматы файлов", составляет 600 пикселей. С 4 байтами на пиксель, это в общей сложности 1440 000 байт в памяти. В следующей таблице показан размер файла для различных сочетаний формата файла и качества:

Формат Качество Размер
PNG Н/П 492K
JPEG 0 2.95K
50 22.1K
100 206K
Webp 0 2.71K
50 11.9K
100 101K

Вы можете поэкспериментировать с различными параметрами качества и проверить результаты.

Сохранение искусства пальцем

Одним из распространенных вариантов использования растрового изображения является рисование программ, в которых она функционирует как то, что называется теневым растровым изображением. Все рисунки сохраняются на растровом рисунке, который затем отображается программой. Растровое изображение также удобно для сохранения рисунка.

Картина пальцев в статье SkiaSharp демонстрирует, как использовать отслеживание сенсорного ввода для реализации примитивной программы рисования пальцев. Программа поддерживает только один цвет и только одну ширину штриха SKPath , но она сохранила весь рисунок в коллекции объектов.

На странице " Сохранить пальцем" в примере также сохраняется весь рисунок в коллекции SKPath объектов, но он также отображает рисунок на растровом рисунке, который он может сохранить в вашей библиотеке фотографий.

Большая часть этой программы похожа на исходную программу Пальцем Краска . Одно из улучшений заключается в том, что XAML-файл теперь создает экземпляр кнопок с меткой 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. Эта растровая карта создается или воссоздается в 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 обработчиком, происходит в самом конце и состоит исключительно из отрисовки растрового изображения.

Обработка касания аналогична предыдущей программе. Программа поддерживает две коллекции и 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 перерисовывается saveBitmap путем создания нового SKCanvas, очистки и отрисовки всех путей на растровом рисунке. Он завершается недействительным 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");
            }
        }
    }
}

Обработчик кнопки "Сохранить " использует упрощенный Encode метод из SKImage. Этот метод кодирует формат PNG. Объект SKImage создается на основе saveBitmap, и SKData объект содержит закодированный PNG-файл.

Метод ToArraySKData получения массива байтов. Это то, что передается SavePhotoAsync методу, а также фиксированное имя папки и уникальное имя файла, созданное из текущей даты и времени.

Вот программа в действии:

Сохранение пальцев

В примере используется очень похожий метод. Это также программа рисования пальцев, за исключением того, что пользователь рисует на спиннинг-диске, который затем воспроизводит конструкции на других четырех квадрантах. Цвет пальца изменяется по мере вращения диска:

Спин краска

Кнопка SpinPaint "Сохранить" класса аналогична кнопке "Палец" в том, что изображение сохраняется в фиксированном имени папки (SpainPaint) и имени файла, созданного с даты и времени.