Compartir a través de


Almacenamiento de mapas de bits de SkiaSharp en archivos

Después de que una aplicación SkiaSharp haya creado o modificado un mapa de bits, es posible que quiera guardar el mapa de bits en la biblioteca de fotos del usuario:

Guardar mapas de bits

Esta tarea abarca dos pasos:

  • Convertir el mapa de bits SkiaSharp en datos en un formato de archivo determinado, como JPEG o PNG.
  • Guardar el resultado en la biblioteca de fotos mediante código específico de la plataforma.

Formatos de archivo y códecs

La mayoría de los formatos de archivo de mapa de bits más populares de hoy usan compresión para reducir el espacio de almacenamiento. Las dos amplias categorías de técnicas de compresión se denominan pérdida y sin pérdida. Estos términos indican si el algoritmo de compresión da como resultado la pérdida de datos.

El formato de pérdida más popular fue desarrollado por el Joint Photographic Experts Group y se llama JPEG. El algoritmo de compresión JPEG analiza la imagen mediante una herramienta matemática denominada transformación coseno discreta e intenta quitar los datos que no son fundamentales para conservar la fidelidad visual de la imagen. El grado de compresión se puede controlar con un valor que generalmente se conoce como calidad. La configuración de mayor calidad da como resultado archivos más grandes.

Por el contrario, un algoritmo de compresión sin pérdida analiza la imagen para la repetición y los patrones de píxeles que se pueden codificar de una manera que reduce los datos, pero no produce la pérdida de ninguna información. Los datos originales del mapa de bits se pueden restaurar completamente desde el archivo comprimido. El formato de archivo comprimido sin pérdida principal en uso hoy es Portable Network Graphics (PNG).

Por lo general, JPEG se usa para fotografías, mientras que PNG se usa para imágenes que se han generado manual o algorítmicamente. Cualquier algoritmo de compresión sin pérdida que reduzca el tamaño de algunos archivos debe aumentar necesariamente el tamaño de otros. Afortunadamente, este aumento en el tamaño generalmente solo se produce para los datos que contienen una gran cantidad de información aleatoria (o aparentemente aleatoria).

Los algoritmos de compresión son lo suficientemente complejos como para garantizar dos términos que describen los procesos de compresión y descompresión:

  • Descodificación: leer un formato de archivo de mapa de bits y descomprimirlo
  • Codificación: comprima el mapa de bits y escriba en un formato de archivo de mapa de bits

La clase SKBitmap contiene varios métodos denominados Decode que crean un SKBitmap a partir de un origen comprimido. Todo lo necesario es proporcionar un nombre de archivo, una secuencia o una matriz de bytes. El descodificador puede determinar el formato de archivo y entregarlo a la función de descodificación interna adecuada.

Además, la clase SKCodec tiene dos métodos denominados Create que pueden crear un objeto SKCodec a partir de un origen comprimido y permitir que una aplicación participe más en el proceso de descodificación. (La clase SKCodec se muestra en el artículo Animar mapa de bits SkiaSharp en conexión con la descodificación de un archivo GIF animado).

Al codificar un mapa de bits, se requiere más información: el codificador debe conocer el formato de archivo determinado que la aplicación quiere usar (JPEG o PNG o algo más). Si se desea un formato de pérdida, el código también debe conocer el nivel de calidad deseado.

La clase SKBitmap define un método Encode con la sintaxis siguiente:

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

Este método se describe con más detalle en breve. El mapa de bits codificado se escribe en una secuencia grabable. ("W" en SKWStream significa "grabable"). Los argumentos segundo y tercero especifican el formato de archivo y (para formatos de pérdida) la calidad deseada que va de 0 a 100.

Además, las clases SKImage y SKPixmap también definen métodos Encode que son algo más versátiles y que puede preferir. Puede crear fácilmente un objeto SKImage a partir de un objeto SKBitmap mediante el método estático SKImage.FromBitmap. Puede obtener un objeto SKPixmap de un objeto SKBitmap mediante el método PeekPixels.

Uno de los métodos Encode definidos por SKImage no tiene parámetros y guarda automáticamente en un formato PNG. Ese método sin parámetros es muy fácil de usar.

Código específico de la plataforma para guardar archivos de mapa de bits

Al codificar un objeto SKBitmap en un formato de archivo determinado, normalmente se le deja un objeto de secuencia de algún tipo o una matriz de datos. Algunos de los métodos Encode (incluidos los que no tienen parámetros definidos por SKImage) devuelven un objeto SKData, que se puede convertir en una matriz de bytes mediante el método ToArray. Estos datos deben guardarse en un archivo.

Guardar en un archivo en el almacenamiento local de la aplicación es bastante fácil porque puede usar clases y métodos estándar System.IO para esta tarea. Esta técnica se muestra en el artículo Animando mapas de bits SkiaSharp en conexión con la animación de una serie de mapas de bits del conjunto Mandelbrot.

Si desea que otras aplicaciones compartan el archivo, debe guardarse en la biblioteca de fotos del usuario. Esta tarea requiere código específico de la plataforma y el uso de Xamarin.FormsDependencyService.

El proyecto SkiaSharpFormsDemo de la aplicación de ejemplo define una interfaz IPhotoLibrary usada con la clase DependencyService. Esto define la sintaxis de un método SavePhotoAsync:

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

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

Esta interfaz también define el método PickPhotoAsync, que se usa para abrir el selector de archivos específico de la plataforma para la biblioteca de fotos del dispositivo.

Para SavePhotoAsync, el primer argumento es una matriz de bytes que contiene el mapa de bits ya codificado en un formato de archivo determinado, como JPEG o PNG. Es posible que una aplicación quiera aislar todos los mapas de bits que crea en una carpeta determinada, que se especifica en el parámetro siguiente, seguido del nombre de archivo. El método devuelve un valor booleano que indica que se ha realizado correctamente o no.

En las secciones siguientes se habla de cómo se implementa SavePhotoAsync en cada plataforma.

La implementación de iOS

La implementación de iOS de SavePhotoAsync usa el método SaveToPhotosAlbum 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;
    }
}

Desafortunadamente, no hay ninguna manera de especificar un nombre de archivo o carpeta para la imagen.

El archivo Info.plist del proyecto de iOS requiere una clave que indique que agrega imágenes a la biblioteca de fotos:

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

¡Cuidado! La clave de permiso para acceder simplemente a la biblioteca de fotos es muy similar, pero no la misma:

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

La implementación de Android

La implementación de Android de SavePhotoAsync primero comprueba si el argumento folder es null o una cadena vacía. Si es así, el mapa de bits se guarda en el directorio raíz de la biblioteca de fotos. De lo contrario, se obtiene la carpeta y, si no existe, se crea:

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

La llamada a MediaScannerConnection.ScanFile no es estrictamente necesaria, pero si estás probando tu programa comprobando inmediatamente la biblioteca de fotos, ayuda mucho actualizando la vista de la galería de bibliotecas.

El archivo AndroidManifest.xml requiere la siguiente etiqueta de permiso:

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

La implementación de UWP

La implementación de UWP de SavePhotoAsync es muy similar en estructura a la implementación de 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;
    }
}

La sección Funcionalidades del archivo Package.appxmanifest requiere biblioteca de imágenes.

Exploración de los formatos de imagen

Este es el método Encode de SKImage de nuevo:

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

SKEncodedImageFormat es una enumeración con miembros que hacen referencia a once formatos de archivo de mapa de bits, algunos de los cuales son bastante oscuros:

  • Astc: Compresión de textura escalable adaptable
  • Bmp: Mapa de bits de Windows
  • Dng: Adobe Digital Negative
  • Gif: Formato de intercambio de gráficos (GIF)
  • Ico: Imágenes de icono de Windows
  • Jpeg: JPEG (Joint Photographic Experts Group)
  • Ktx: Formato de textura khronos para OpenGL
  • Pkm: Formato personalizado para GrafX2
  • Png: Portable Network Graphics (PNG)
  • Wbmp: Formato de mapa de bits del protocolo de aplicación inalámbrica (1 bit por píxel)
  • Webp: Formato WebP de Google

Como verá en breve, solo tres de estos formatos de archivo (Jpeg, Png y Webp) son realmente compatibles con SkiaSharp.

Para guardar un objeto SKBitmap denominado bitmap en la biblioteca de fotos del usuario, también necesita un miembro de la enumeración SKEncodedImageFormat denominada imageFormat y (para formatos de pérdida) una variable de entero quality. Puede usar el código siguiente para guardar ese mapa de bits en un archivo con el nombre filename en la carpeta 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!
}

La clase SKManagedWStream se deriva de SKWStream (que significa "secuencia grabable"). El método Encode escribe el archivo de mapa de bits codificado en esa secuencia. Los comentarios de ese código hacen referencia a alguna comprobación de errores que puede que necesite realizar.

La página Guardar formatos de archivo de la aplicación de ejemplo usa código similar para permitirle experimentar con el guardado de un mapa de bits en los distintos formatos.

El archivo XAML contiene un objeto SKCanvasView que muestra un mapa de bits, mientras que el resto de la página contiene todo lo que la aplicación necesita para llamar al método Encode de SKBitmap. Tiene un Picker para un miembro de la enumeración SKEncodedImageFormat, un Slider para el argumento de calidad para los formatos de mapa de bits de pérdida, dos vistas Entry para un nombre de archivo y carpeta, y un Button para guardar el archivo.

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

El archivo de código subyacente carga un recurso de mapa de bits y usa SKCanvasView para mostrarlo. Ese mapa de bits nunca cambia. El controlador SelectedIndexChanged del Picker modifica el nombre de archivo con una extensión que es la misma que el miembro de enumeración:

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

El controlador Clicked de Button realiza todo el trabajo real. Obtiene dos argumentos para Encode de Picker y Slider y, a continuación, usa el código mostrado anteriormente para crear un SKManagedWStream para el método Encode. Las dos vistas Entry presentan nombres de carpeta y archivo para el método SavePhotoAsync.

La mayoría de este método se dedica a controlar problemas o errores. Si Encode crea una matriz vacía, significa que no se admite el formato de archivo determinado. Si SavePhotoAsync devuelve false, el archivo no se guardó correctamente.

Esta es la ejecución del programa:

Guardar formatos de archivo

Esa captura de pantalla muestra los tres únicos formatos que se admiten en estas plataformas:

  • JPEG
  • PNG
  • WebP

Para todos los demás formatos, el método Encode no escribe nada en la secuencia y la matriz de bytes resultante está vacía.

El mapa de bits que guarda la página Guardar formatos de archivo es de 600 píxeles cuadrados. Con 4 bytes por píxel, es un total de 1 440 000 bytes en memoria. En la tabla siguiente se muestra el tamaño del archivo para varias combinaciones de formato y calidad de archivo:

Format Control de Size
PNG N/D 492 K
JPEG 0 2,95 K
50 22.1K
100 206 K
WebP 0 2,71 K
50 11,9 K
100 101 K

Puede experimentar con varias configuraciones de calidad y examinar los resultados.

Guardar arte de pintura de dedo

Un uso común de un mapa de bits es en los programas de dibujo, donde funciona como algo llamado mapa de bits de sombra. Todo el dibujo se conserva en el mapa de bits, que el programa muestra a continuación. El mapa de bits también resulta útil para guardar el dibujo.

El artículo Pintura de dedos en SkiaSharp mostró cómo usar el seguimiento táctil para implementar un programa primitivo de pintura de dedos. El programa solo admite un color y solo un ancho de trazo, pero conserva todo el dibujo en una colección de objetos SKPath.

La página Pintura de dedos con Guardar en el ejemplo también conserva todo el dibujo en una colección de objetos, pero también representa el dibujo en un mapa de bits, que puede guardar en la biblioteca de fotos SKPath.

Gran parte de este programa es similar al programa original Pintura de dedos. Una mejora es que el archivo XAML ahora crea instancias de los botones con la etiqueta Borrar y Guardar:

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

El archivo de código subyacente mantiene un campo de tipo SKBitmap denominado saveBitmap. Este mapa de bits se crea o vuelve a crear en el controlador PaintSurface cada vez que cambia el tamaño de la superficie de visualización. Si es necesario volver a crear el mapa de bits, el contenido del mapa de bits existente se copia en el nuevo mapa de bits para que todo se conserve independientemente de cómo cambia la superficie de visualización en tamaño:

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

El dibujo realizado por el controlador PaintSurface se produce al final y consiste únicamente en representar el mapa de bits.

El procesamiento táctil es similar al programa anterior. El programa mantiene dos colecciones, inProgressPaths y completedPaths, que contienen todo lo que el usuario ha dibujado desde la última vez que se ha borrado la pantalla. Para cada evento táctil, el controlador OnTouchEffectAction llama a 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();
    }
    ···
}

El método UpdateBitmap vuelve a dibujar saveBitmap mediante la creación de un nuevo SKCanvas, lo despeja y, a continuación, representa todas las rutas de acceso en el mapa de bits. Concluye invalidando canvasView para que el mapa de bits se pueda dibujar en la pantalla.

Estos son los controladores de los dos botones. El botón Borrar borra ambas colecciones de rutas de acceso, actualizaciones saveBitmap (lo que da como resultado borrar el mapa de bits) y invalida el 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");
            }
        }
    }
}

El controlador del botón Guardar usa el método simplificado Encode de SKImage. Este método codifica con el formato PNG. El objeto SKImage se crea en función de saveBitmap y el objeto SKData contiene el archivo PNG codificado.

El método ToArray de SKData obtiene una matriz de bytes. Esto es lo que se pasa al método SavePhotoAsync, junto con un nombre de carpeta fijo y un nombre de archivo único construido a partir de la fecha y hora actuales.

Este es el programa en acción:

Guardar pintura con los dedos

Se usa una técnica muy similar en el ejemplo. También es un programa de pintura de dedos, excepto que el usuario pinta en un disco giratorio que luego reproduce los diseños en sus otros cuatro cuadrantes. El color de la Pintura de dedos cambia a medida que el disco gira:

Spin Paint

El botón Guardar de clase SpinPaint es similar a Pintura de dedos en que guarda la imagen en un nombre de carpeta fijo (SpainPaint) y un nombre de archivo construido a partir de la fecha y hora.