Partager via


Enregistrement des bitmaps SkiaSharp dans des fichiers

Une fois qu’une application SkiaSharp a créé ou modifié une bitmap, l’application peut souhaiter enregistrer la bitmap dans la bibliothèque de photos de l’utilisateur :

Enregistrement de bitmaps

Cette tâche englobe deux étapes :

  • Conversion de la bitmap SkiaSharp en données dans un format de fichier particulier, tel que JPEG ou PNG.
  • Enregistrement du résultat dans la bibliothèque de photos à l’aide du code spécifique à la plateforme.

Formats de fichiers et codecs

La plupart des formats de fichiers bitmap populaires d’aujourd’hui utilisent la compression pour réduire l’espace de stockage. Les deux grandes catégories de techniques de compression sont appelées perte et sans perte. Ces termes indiquent si l’algorithme de compression entraîne ou non la perte de données.

Le format de perte le plus populaire a été développé par le Joint Photographic Experts Group et est appelé JPEG. L’algorithme de compression JPEG analyse l’image à l’aide d’un outil mathématique appelé transformation cosinus discrète et tente de supprimer des données qui ne sont pas cruciales pour préserver la fidélité visuelle de l’image. Le degré de compression peut être contrôlé avec un paramètre généralement appelé qualité. Les paramètres de qualité supérieure entraînent des fichiers plus volumineux.

En revanche, un algorithme de compression sans perte analyse l’image pour la répétition et les modèles de pixels qui peuvent être encodés d’une manière qui réduit les données, mais n’entraîne pas de perte d’informations. Les données bitmap d’origine peuvent être restaurées entièrement à partir du fichier compressé. Le format de fichier compressé sans perte principal utilisé aujourd’hui est Portable Network Graphics (PNG).

En règle générale, JPEG est utilisé pour les photographies, tandis que PNG est utilisé pour les images qui ont été générées manuellement ou de manière algorithmique. Tout algorithme de compression sans perte qui réduit la taille de certains fichiers doit nécessairement augmenter la taille des autres. Heureusement, cette augmentation de la taille se produit généralement uniquement pour les données qui contiennent beaucoup d’informations aléatoires (ou apparemment aléatoires).

Les algorithmes de compression sont suffisamment complexes pour justifier deux termes qui décrivent les processus de compression et de décompression :

  • décoder : lire un format de fichier bitmap et le décompresser
  • encode : compresser la bitmap et écrire dans un format de fichier bitmap

La SKBitmap classe contient plusieurs méthodes nommées Decode qui créent une SKBitmap source compressée. Tout ce qui est nécessaire consiste à fournir un nom de fichier, un flux ou un tableau d’octets. Le décodeur peut déterminer le format de fichier et le remettre à la fonction de décodage interne appropriée.

En outre, la SKCodec classe a deux méthodes nommées Create qui peuvent créer un objet à partir d’une SKCodec source compressée et permettre à une application d’être plus impliquée dans le processus de décodage. (La SKCodec classe est affichée dans l’article Animating SkiaSharp Bitmaps en connexion avec le décodage d’un fichier GIF animé.)

Lors de l’encodage d’une bitmap, plus d’informations sont requises : l’encodeur doit connaître le format de fichier particulier que l’application souhaite utiliser (JPEG ou PNG ou autre chose). Si un format de perte est souhaité, l’encode doit également connaître le niveau de qualité souhaité.

La SKBitmap classe définit une Encode méthode avec la syntaxe suivante :

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

Cette méthode est décrite plus en détail sous peu. La bitmap encodée est écrite dans un flux accessible en écriture. (Le « W » SKWStream signifie « accessible en écriture ». Les deuxième et troisième arguments spécifient le format de fichier et (pour les formats perdus) la qualité souhaitée comprise entre 0 et 100.

En outre, les classes et SKPixmap les SKImage classes définissent Encode également des méthodes quelque peu plus polyvalentes, et que vous pouvez préférer. Vous pouvez facilement créer un SKImage objet à partir d’un SKBitmap objet à l’aide de la méthode statique SKImage.FromBitmap . Vous pouvez obtenir un SKPixmap objet à partir d’un SKBitmap objet à l’aide de la PeekPixels méthode.

L’une des Encode méthodes définies par SKImage aucun paramètre n’est enregistrée automatiquement dans un format PNG. Cette méthode sans paramètre est très facile à utiliser.

Code spécifique à la plateforme pour l’enregistrement de fichiers bitmap

Lorsque vous encodez un SKBitmap objet dans un format de fichier particulier, vous êtes généralement laissé avec un objet de flux de quelque sorte, ou un tableau de données. Certaines des Encode méthodes (y compris celles sans paramètres définis par SKImage) retournent un SKData objet, qui peut être converti en tableau d’octets à l’aide de la ToArray méthode. Ces données doivent ensuite être enregistrées dans un fichier.

L’enregistrement dans un fichier dans le stockage local de l’application est assez facile, car vous pouvez utiliser des classes et méthodes standard System.IO pour cette tâche. Cette technique est illustrée dans l’article Animating SkiaSharp Bitmaps en lien avec l’animation d’une série de bitmaps de l’ensemble De Mandelbrot.

Si vous souhaitez que le fichier soit partagé par d’autres applications, il doit être enregistré dans la bibliothèque de photos de l’utilisateur. Cette tâche nécessite du code spécifique à la plateforme et l’utilisation du Xamarin.FormsDependencyService.

Le projet SkiaSharpFormsDemo dans l’exemple d’application définit une IPhotoLibrary interface utilisée avec la DependencyService classe. Cela définit la syntaxe d’une SavePhotoAsync méthode :

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

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

Cette interface définit également la PickPhotoAsync méthode utilisée pour ouvrir le sélecteur de fichiers spécifique à la plateforme pour la bibliothèque de photos de l’appareil.

Pour SavePhotoAsync, le premier argument est un tableau d’octets qui contient l’image bitmap déjà encodée dans un format de fichier particulier, tel que JPEG ou PNG. Il est possible qu’une application souhaite isoler toutes les bitmaps qu’elle crée dans un dossier particulier, qui est spécifié dans le paramètre suivant, suivi du nom de fichier. La méthode retourne une valeur booléenne indiquant la réussite ou non.

Les sections suivantes expliquent comment SavePhotoAsync est implémentée sur chaque plateforme.

Implémentation iOS

Implémentation iOS d’utilisation de SavePhotoAsync la SaveToPhotosAlbum méthode 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;
    }
}

Malheureusement, il n’existe aucun moyen de spécifier un nom de fichier ou un dossier pour l’image.

Le fichier Info.plist dans le projet iOS nécessite une clé indiquant qu’il ajoute des images à la bibliothèque de photos :

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

Fais gaffe! La clé d’autorisation pour accéder simplement à la bibliothèque de photos est très similaire, mais pas la même :

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

Implémentation Android

Implémentation Android de SavePhotoAsync première case activée s si l’argument folder est null ou une chaîne vide. Dans ce cas, la bitmap est enregistrée dans le répertoire racine de la bibliothèque de photos. Sinon, le dossier est obtenu et, s’il n’existe pas, il est créé :

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

L’appel à MediaScannerConnection.ScanFile n’est pas strictement obligatoire, mais si vous testez votre programme immédiatement case activée la bibliothèque de photos, il vous aide beaucoup en mettant à jour l’affichage de la galerie de bibliothèques.

Le fichier AndroidManifest.xml nécessite la balise d’autorisation suivante :

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

Implémentation UWP

L’implémentation UWP est SavePhotoAsync très similaire à l’implémentation 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 section Fonctionnalités du fichier Package.appxmanifest nécessite la bibliothèque d’images.

Exploration des formats d’image

Voici la Encode méthode de SKImage nouveau :

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

SKEncodedImageFormat est une énumération avec des membres qui font référence à onze formats de fichier bitmap, dont certains sont plutôt obscurs :

  • Astc — Compression de texture évolutive adaptative
  • Bmp — Bitmap Windows
  • Dng — Adobe Digital Negative
  • Gif — Format d’échange graphique
  • Ico — Images d’icône Windows
  • Jpeg — Groupe d’experts photographiques conjoints
  • Ktx — Format de texture Khronos pour OpenGL
  • Pkm — Format personnalisé pour GrafX2
  • Png — Graphiques réseau portables
  • Wbmp — Format bitmap du protocole d’application sans fil (1 bit par pixel)
  • Webp — Format Google WebP

Comme vous le verrez sous peu, seuls trois de ces formats de fichier (Jpeg, Pnget Webp) sont réellement pris en charge par SkiaSharp.

Pour enregistrer un SKBitmap objet nommé bitmap dans la bibliothèque de photos de l’utilisateur, vous avez également besoin d’un membre de l’énumération SKEncodedImageFormat nommée imageFormat et (pour les formats de perte) d’une variable entière quality . Vous pouvez utiliser le code suivant pour enregistrer cette bitmap dans un fichier portant le nom filename dans le folder dossier :

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 SKManagedWStream classe dérive de SKWStream (qui signifie « flux accessible en écriture »). La Encode méthode écrit le fichier bitmap encodé dans ce flux. Les commentaires de ce code font référence à une erreur case activée vous devrez peut-être effectuer.

La page Enregistrer des formats de fichier dans l’exemple d’application utilise du code similaire pour vous permettre d’expérimenter l’enregistrement d’une bitmap dans les différents formats.

Le fichier XAML contient une SKCanvasView image bitmap, tandis que le reste de la page contient tout ce dont l’application a besoin pour appeler la Encode méthode .SKBitmap Il a un Picker pour un membre de l’énumération SKEncodedImageFormat , un Slider argument de qualité pour les formats bitmap perdus, deux Entry vues pour un nom de fichier et un nom de dossier, et un Button pour enregistrer le fichier.

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

Le fichier code-behind charge une ressource bitmap et l’utilise pour l’afficher SKCanvasView . Cette bitmap ne change jamais. Le SelectedIndexChanged gestionnaire pour les Picker modifications du nom de fichier avec une extension identique au membre d’énumération :

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

Le Clicked gestionnaire pour le Button fait tout le travail réel. Il obtient deux arguments à Encode partir du Picker code et Sliderutilise ensuite le code indiqué précédemment pour créer une SKManagedWStream méthode Encode . Les deux Entry vues fournissent des noms de dossiers et de fichiers pour la SavePhotoAsync méthode.

La plupart de cette méthode est consacrée à la gestion des problèmes ou des erreurs. Si Encode vous créez un tableau vide, cela signifie que le format de fichier particulier n’est pas pris en charge. Si SavePhotoAsync cette propriété est retournée false, le fichier n’a pas été correctement enregistré.

Voici le programme en cours d’exécution :

Enregistrer les formats de fichier

Cette capture d’écran montre les trois seuls formats pris en charge sur ces plateformes :

  • JPEG
  • PNG
  • WebP

Pour tous les autres formats, la Encode méthode écrit rien dans le flux et le tableau d’octets résultant est vide.

La bitmap que la page Enregistrer les formats de fichier enregistre est carrée de 600 pixels. Avec 4 octets par pixel, il s’agit d’un total de 1 440 000 octets en mémoire. Le tableau suivant présente la taille de fichier pour différentes combinaisons de format et de qualité de fichier :

Format Contrôle Size
PNG S/O 492 Ko
JPEG 0 2,95 Ko
50 22 100
100 206K
WebP 0 2.71K
50 11,9 Ko
100 101 Ko

Vous pouvez expérimenter différents paramètres de qualité et examiner les résultats.

Enregistrement de l’art de peinture au doigt

L’une des utilisations courantes d’une bitmap se trouve dans les programmes de dessin, où elle fonctionne comme quelque chose appelé bitmap d’ombre. Tout le dessin est conservé sur la bitmap, qui est ensuite affichée par le programme. La bitmap est également utile pour enregistrer le dessin.

L’article Sur la peinture des doigts dans SkiaSharp a montré comment utiliser le suivi tactile pour implémenter un programme primitif de peinture des doigts. Le programme n’a pris en charge qu’une seule couleur et une seule largeur de trait, mais il a conservé l’intégralité du dessin dans une collection d’objets SKPath .

La peinture doigt avec la page Enregistrer dans l’exemple conserve également l’intégralité du dessin dans une collection d’objets SKPath , mais affiche également le dessin sur une bitmap, qu’il peut enregistrer dans votre bibliothèque de photos.

Une grande partie de ce programme est similaire au programme De peinture de doigt d’origine. Une amélioration est que le fichier XAML instancie désormais les boutons étiquetés Effacer et Enregistrer :

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

Le fichier code-behind gère un champ de type SKBitmap nommé saveBitmap. Cette bitmap est créée ou recréée dans le PaintSurface gestionnaire chaque fois que la taille de l’aire d’affichage change. Si la bitmap doit être recréée, le contenu de la bitmap existante est copié dans la nouvelle bitmap afin que tout soit conservé quelle que soit la taille de l’aire d’affichage :

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

Le dessin effectué par le PaintSurface gestionnaire se produit à la fin et se compose uniquement du rendu de l’image bitmap.

Le traitement tactile est similaire au programme précédent. Le programme gère deux collections et inProgressPathscompletedPaths, qui contiennent tout ce que l’utilisateur a dessiné depuis la dernière fois que l’affichage a été effacé. Pour chaque événement tactile, le OnTouchEffectAction gestionnaire appelle 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();
    }
    ···
}

La UpdateBitmap méthode redessine saveBitmap en créant une nouvelle SKCanvasméthode, en la désactivant, puis en affichant tous les chemins d’accès sur l’image bitmap. Elle conclut en invalidant canvasView afin que la bitmap puisse être dessinée sur l’affichage.

Voici les gestionnaires des deux boutons. Le bouton Effacer efface les deux collections de chemins d’accès, met à jour saveBitmap (ce qui entraîne l’effacement de la bitmap) et invalide les SKCanvasViewéléments suivants :

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

Le gestionnaire de boutons Enregistrer utilise la méthode simplifiée Encode à partir de SKImage. Cette méthode encode à l’aide du format PNG. L’objet SKImage est créé en fonction saveBitmapde , et l’objet SKData contient le fichier PNG encodé.

Méthode ToArray d’obtention SKData d’un tableau d’octets. Il s’agit de ce qui est passé à la SavePhotoAsync méthode, ainsi qu’un nom de dossier fixe et un nom de fichier unique construit à partir de la date et de l’heure actuelles.

Voici le programme en action :

Enregistrer la peinture des doigts

Une technique très similaire est utilisée dans l’exemple. Il s’agit également d’un programme de peinture de doigts, sauf que l’utilisateur peint sur un disque de rotation qui reproduit ensuite les conceptions sur ses quatre autres quadrants. La couleur de la peinture du doigt change à mesure que le disque tourne :

Spin Paint

Le bouton Enregistrer de la classe est similaire à Finger Paint dans lequel il enregistre l’image dans un nom de SpinPaint dossier fixe (SpainPaint) et un nom de fichier construit à partir de la date et de l’heure.