Partilhar via


Salvando bitmaps do SkiaSharp em arquivos

Depois que um aplicativo SkiaSharp tiver criado ou modificado um bitmap, o aplicativo pode querer salvar o bitmap na biblioteca de fotos do usuário:

Salvando bitmaps

Essa tarefa abrange duas etapas:

  • Convertendo o bitmap SkiaSharp em dados em um formato de arquivo específico, como JPEG ou PNG.
  • Salvar o resultado na biblioteca de fotos usando código específico da plataforma.

Formatos de arquivo e codecs

A maioria dos formatos de arquivo bitmap populares de hoje usa compactação para reduzir o espaço de armazenamento. As duas grandes categorias de técnicas de compressão são chamadas de lossy e lossless. Esses termos indicam se o algoritmo de compactação resulta ou não na perda de dados.

O formato com perdas mais popular foi desenvolvido pelo Joint Photographic Experts Group e é chamado de JPEG. O algoritmo de compactação JPEG analisa a imagem usando uma ferramenta matemática chamada transformação discreta de cosseno e tenta remover dados que não são cruciais para preservar a fidelidade visual da imagem. O grau de compressão pode ser controlado com uma configuração geralmente chamada de qualidade. Configurações de qualidade mais alta resultam em arquivos maiores.

Em contraste, um algoritmo de compactação sem perdas analisa a imagem quanto à repetição e padrões de pixels que podem ser codificados de uma forma que reduz os dados, mas não resulta na perda de nenhuma informação. Os dados de bitmap originais podem ser restaurados inteiramente a partir do arquivo compactado. O principal formato de arquivo compactado sem perdas em uso atualmente é o PNG (Portable Network Graphics).

Geralmente, o JPEG é usado para fotografias, enquanto o PNG é usado para imagens que foram geradas manualmente ou por algoritmos. Qualquer algoritmo de compactação sem perdas que reduza o tamanho de alguns arquivos deve necessariamente aumentar o tamanho de outros. Felizmente, esse aumento de tamanho geralmente ocorre apenas para dados que contêm muitas informações aleatórias (ou aparentemente aleatórias).

Os algoritmos de compactação são complexos o suficiente para justificar dois termos que descrevem os processos de compactação e descompactação:

  • decodificar — ler um formato de arquivo bitmap e descompactá-lo
  • codificar — compactar o bitmap e gravar em um formato de arquivo bitmap

A SKBitmap classe contém vários métodos nomeados Decode que criam um SKBitmap a partir de uma fonte compactada. Tudo o que é necessário é fornecer um nome de arquivo, fluxo ou matriz de bytes. O decodificador pode determinar o formato do arquivo e entregá-lo à função de decodificação interna adequada.

Além disso, a SKCodec classe tem dois métodos nomeados Create que podem criar um SKCodec objeto a partir de uma fonte compactada e permitir que um aplicativo se envolva mais no processo de decodificação. (A SKCodec classe é mostrada no artigo Animando Bitmaps SkiaSharp em conexão com a decodificação de um arquivo GIF animado.)

Ao codificar um bitmap, mais informações são necessárias: O codificador deve saber o formato de arquivo específico que o aplicativo deseja usar (JPEG ou PNG ou qualquer outra coisa). Se um formato com perdas for desejado, a codificação também deverá saber o nível de qualidade desejado.

A SKBitmap classe define um Encode método com a seguinte sintaxe:

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

Este método é descrito com mais detalhes em breve. O bitmap codificado é gravado em um fluxo gravável. (O 'W' significa SKWStream "gravável".) O segundo e o terceiro argumentos especificam o formato do arquivo e (para formatos com perdas) a qualidade desejada variando de 0 a 100.

Além disso, as SKImage classes and SKPixmap também definem Encode métodos que são um pouco mais versáteis e que você pode preferir. Você pode criar facilmente um SKImage objeto a partir de um SKBitmap objeto usando o método estático SKImage.FromBitmap . Você pode obter um SKPixmap objeto de um SKBitmap objeto usando o PeekPixels método.

Um dos Encode métodos definidos por SKImage não tem parâmetros e é salvo automaticamente em um formato PNG. Esse método sem parâmetros é muito fácil de usar.

Código específico da plataforma para salvar arquivos bitmap

Quando você codifica um SKBitmap objeto em um formato de arquivo específico, geralmente você fica com um objeto de fluxo de algum tipo ou uma matriz de dados. Alguns dos Encode métodos (incluindo aquele sem parâmetros definidos por SKImage) retornam um SKData objeto, que pode ser convertido em uma matriz de bytes usando o ToArray método. Esses dados devem ser salvos em um arquivo.

Salvar em um arquivo no armazenamento local do aplicativo é bastante fácil porque você pode usar classes e métodos padrão System.IO para essa tarefa. Essa técnica é demonstrada no artigo Animando Bitmaps SkiaSharp em conexão com a animação de uma série de bitmaps do conjunto de Mandelbrot.

Se você quiser que o arquivo seja compartilhado por outros aplicativos, ele deve ser salvo na biblioteca de fotos do usuário. Essa tarefa requer código específico da plataforma e o uso do Xamarin.FormsDependencyService.

O projeto SkiaSharpFormsDemo no aplicativo de exemplo define uma IPhotoLibrary interface usada com a DependencyService classe. Isso define a sintaxe de um SavePhotoAsync método:

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

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

Essa interface também define o PickPhotoAsync método, que é usado para abrir o seletor de arquivos específico da plataforma para a biblioteca de fotos do dispositivo.

Para SavePhotoAsync, o primeiro argumento é uma matriz de bytes que contém o bitmap já codificado em um formato de arquivo específico, como JPEG ou PNG. É possível que um aplicativo queira isolar todos os bitmaps criados em uma pasta específica, que é especificada no próximo parâmetro, seguido pelo nome do arquivo. O método retorna um booleano indicando sucesso ou não.

As seções a seguir discutem como SavePhotoAsync é implementado em cada plataforma.

A implementação do iOS

A implementação do iOS usa SavePhotoAsync o SaveToPhotosAlbum método de 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;
    }
}

Infelizmente, não há como especificar um nome de arquivo ou pasta para a imagem.

O arquivo Info.plist no projeto iOS requer uma chave indicando que ele adiciona imagens à biblioteca de fotos:

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

Tomar cuidado! A chave de permissão para simplesmente acessar a biblioteca de fotos é muito semelhante, mas não a mesma:

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

A implementação do Android

A implementação do Android de first verifica se o folder argumento é null ou uma cadeia de SavePhotoAsync caracteres vazia. Nesse caso, o bitmap é salvo no diretório raiz da biblioteca de fotos. Caso contrário, a pasta é obtida e, se não existir, é criada:

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

A chamada para MediaScannerConnection.ScanFile não é estritamente necessária, mas se você estiver testando seu programa verificando imediatamente a biblioteca de fotos, ela ajuda muito atualizando a visualização da galeria da biblioteca.

O arquivo AndroidManifest.xml requer a seguinte tag de permissão:

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

A implementação da UWP

A implementação UWP de é muito semelhante em estrutura à implementação do 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;
    }
}

A seção Recursos do arquivo Package.appxmanifest requer a Biblioteca de Imagens.

Explorando os formatos de imagem

Aqui está o Encode método de SKImage novamente:

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

SKEncodedImageFormat é uma enumeração com membros que se referem a onze formatos de arquivo bitmap, alguns dos quais são bastante obscuros:

  • Astc — Compressão de textura escalável adaptável
  • Bmp — Bitmap do Windows
  • Dng — Negativo digital da Adobe
  • Gif — Formato de intercâmbio gráfico
  • Ico — Imagens de ícones do Windows
  • Jpeg — Grupo Conjunto de Peritos Fotográficos
  • Ktx — Formato de textura Khronos para OpenGL
  • Pkm — Formato personalizado para GrafX2
  • Png — Gráficos de rede portáteis
  • Wbmp — Formato de bitmap do protocolo de aplicativo sem fio (1 bit por pixel)
  • Webp — Formato Google WebP

Como você verá em breve, apenas três desses formatos de arquivo (Jpeg, Pnge Webp) são realmente suportados pelo SkiaSharp.

Para salvar um SKBitmap objeto nomeado bitmap na biblioteca de fotos do usuário, você também precisa de um membro da SKEncodedImageFormat enumeração chamado imageFormat e (para formatos com perdas) uma variável inteira quality . Você pode usar o seguinte código para salvar esse bitmap em um arquivo com o nome filename na folder pasta:

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

A SKManagedWStream classe deriva de SKWStream (que significa "fluxo gravável"). O Encode método grava o arquivo de bitmap codificado nesse fluxo. Os comentários nesse código referem-se a alguma verificação de erro que você pode precisar executar.

A página Salvar Formatos de Arquivo no aplicativo de exemplo usa código semelhante para permitir que você experimente salvar um bitmap nos vários formatos.

O arquivo XAML contém um SKCanvasView que exibe um bitmap, enquanto o restante da página contém tudo o que o aplicativo precisa para chamar o Encode método de SKBitmap. Ele tem um Picker para um membro da enumeração, um Slider para o argumento de qualidade para formatos de bitmap com perdas, duas Entry exibições para um nome de arquivo e um nome de SKEncodedImageFormat pasta e um Button para salvar o arquivo.

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

O arquivo code-behind carrega um recurso de bitmap e usa o SKCanvasView para exibi-lo. Esse bitmap nunca muda. O SelectedIndexChanged manipulador do Picker modifica o nome do arquivo com uma extensão que é igual ao membro da enumeração:

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

O Clicked manipulador do Button faz todo o trabalho real. Ele obtém dois argumentos para Encode from the Picker e Slider, e usa o código mostrado anteriormente para criar um SKManagedWStream para o Encode método. As duas Entry visualizações fornecem nomes de pastas e arquivos para o SavePhotoAsync método.

A maior parte desse método é dedicada ao tratamento de problemas ou erros. Se Encode criar uma matriz vazia, isso significa que o formato de arquivo específico não é suportado. Se SavePhotoAsync retornar false, o arquivo não foi salvo com êxito.

Aqui está o programa em execução:

Salvar formatos de arquivo

Essa captura de tela mostra os únicos três formatos com suporte nessas plataformas:

  • JPEG
  • PNG
  • Redes

Para todos os outros formatos, o método não grava Encode nada no fluxo e a matriz de bytes resultante está vazia.

O bitmap que a página Salvar Formatos de Arquivo salva tem 600 pixels quadrados. Com 4 bytes por pixel, isso é um total de 1.440.000 bytes na memória. A tabela a seguir mostra o tamanho do arquivo para várias combinações de formato e qualidade de arquivo:

Formatar Quality Tamanho
PNG N/D 492 mil
JPEG 0 2,95 mil
50 22,1 mil
100 206 mil
Redes 0 2,71 mil
50 11,9 mil
100 101 mil

Você pode experimentar várias configurações de qualidade e examinar os resultados.

Salvando a arte da pintura a dedo

Um uso comum de um bitmap é em programas de desenho, onde ele funciona como algo chamado bitmap de sombra. Todo o desenho é retido no bitmap, que é exibido pelo programa. O bitmap também é útil para salvar o desenho.

O artigo Pintura a dedo no SkiaSharp demonstrou como usar o rastreamento de toque para implementar um programa primitivo de pintura com os dedos. O programa suportava apenas uma cor e apenas uma largura de traçado, mas mantinha todo o desenho em uma coleção de SKPath objetos.

A página Pintura a dedo com salvar no exemplo também retém o desenho inteiro em uma coleção de objetos, mas também renderiza o desenho em um bitmap, que pode ser salvo em sua biblioteca de SKPath fotos.

Grande parte deste programa é semelhante ao programa Finger Paint original. Um aprimoramento é que o arquivo XAML agora instancia botões rotulados como Limpar e Salvar:

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

O arquivo code-behind mantém um campo do tipo SKBitmap chamado saveBitmap. Esse bitmap é criado ou recriado no PaintSurface manipulador sempre que o tamanho da superfície de exibição é alterado. Se o bitmap precisar ser recriado, o conteúdo do bitmap existente será copiado para o novo bitmap para que tudo seja retido, independentemente de como a superfície de exibição muda de tamanho:

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

O desenho feito pelo PaintSurface manipulador ocorre no final e consiste apenas em renderizar o bitmap.

O processamento de toque é semelhante ao programa anterior. O programa mantém duas coleções, inProgressPaths e completedPaths, que contêm tudo o que o usuário desenhou desde a última vez que a tela foi apagada. Para cada evento de toque, o OnTouchEffectAction manipulador chama 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();
    }
    ···
}

O UpdateBitmap método redesenha saveBitmap criando um novo SKCanvas, limpando-o e, em seguida, renderizando todos os caminhos no bitmap. Ele conclui invalidando canvasView para que o bitmap possa ser desenhado na tela.

Aqui estão os manipuladores para os dois botões. O botão Limpar limpa as coleções de caminhos, atualiza saveBitmap (o que resulta na limpeza do bitmap) e invalida o 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");
            }
        }
    }
}

O manipulador de botões Salvar usa o método simplificado Encode do SKImage. Esse método codifica usando o formato PNG. O SKImage objeto é criado com base em saveBitmap, e o SKData objeto contém o arquivo PNG codificado.

O ToArray método de SKData obtém uma matriz de bytes. Isso é o que é passado para o SavePhotoAsync método, juntamente com um nome de pasta fixo e um nome de arquivo exclusivo construído a partir da data e hora atuais.

Veja o programa em ação:

Pintura a dedo Salvar

Uma técnica muito semelhante é usada na amostra. Este também é um programa de pintura a dedo, exceto que o usuário pinta em um disco giratório que reproduz os desenhos em seus outros quatro quadrantes. A cor da pintura a dedo muda conforme o disco está girando:

Spin Paint

O botão Salvar da SpinPaint classe é semelhante ao Finger Paint , pois salva a imagem em um nome de pasta fixo (SpainPaint) e um nome de arquivo construído a partir da data e hora.