Ukládání rastrových obrázků SkiaSharp do souborů

Download Sample Stažení ukázky

Jakmile aplikace SkiaSharp vytvoří nebo upraví rastrový obrázek, může aplikace chtít uložit rastrový obrázek do knihovny fotografií uživatele:

Saving Bitmaps

Tento úkol zahrnuje dva kroky:

  • Převod rastrového obrázku SkiaSharp na data v určitém formátu souboru, například JPEG nebo PNG.
  • Uložení výsledku do knihovny fotografií pomocí kódu specifického pro platformu

Formáty souborů a kodeky

Většina dnešních oblíbených rastrových formátů souborů používá kompresi ke snížení úložného prostoru. Dvě široké kategorie technik komprese se nazývají ztrátové a bezeztrátové. Tyto termíny označují, zda algoritmus komprese vede ke ztrátě dat.

Nejoblíbenější ztrátový formát byl vyvinut skupinou Joint Photographic Experts Group a nazývá se JPEG. Algoritmus komprese JPEG analyzuje obrázek pomocí matematického nástroje označovaného jako diskrétní kosinusová transformace a pokusí se odebrat data, která nejsou zásadní pro zachování vizuální věrnosti obrázku. Stupeň komprese lze řídit nastavením obecně označovaným jako kvalita. Nastavení vyšší kvality vede k větším souborům.

Naproti tomu algoritmus komprese bezeztrátové komprese analyzuje obrázek pro opakování a vzory pixelů, které lze kódovat způsobem, který snižuje data, ale nezpůsobí ztrátu informací. Původní rastrová data lze obnovit zcela z komprimovaného souboru. Primární bezeztrátový komprimovaný formát souboru, který se dnes používá, je Portable Network Graphics (PNG).

Obecně platí, že jpeg se používá pro fotografie, zatímco PNG se používá pro obrázky, které byly ručně nebo algoritmicky generovány. Jakýkoli bezeztrátový algoritmus komprese, který zmenší velikost některých souborů, musí nutně zvětšit velikost ostatních. Naštěstí k tomuto zvýšení velikosti obvykle dochází pouze u dat, která obsahují velké množství náhodných (nebo zdánlivě náhodných) informací.

Algoritmy komprese jsou dostatečně složité, aby bylo zaručeno dva termíny, které popisují procesy komprese a dekomprese:

  • dekódování – čtení formátu rastrového souboru a dekomprimace
  • kódování – komprimace rastrového obrázku a zápis do formátu rastrového souboru

Třída SKBitmap obsahuje několik metod pojmenovaných Decode , které vytvoří SKBitmap z komprimovaného zdroje. Stačí zadat název souboru, datový proud nebo pole bajtů. Dekodér může určit formát souboru a předat ho správné vnitřní dekódovací funkci.

Kromě toho třída má dvě metody pojmenovanéCreate, SKCodec které mohou vytvořit SKCodec objekt z komprimovaného zdroje a umožnit aplikaci, aby se více zapojila do procesu dekódování. (Třída SKCodec je zobrazena v článku Animace SkiaSharp Bitmaps v souvislosti s dekódováním animovaného souboru GIF.)

Při kódování rastrového obrázku se vyžadují další informace: Kodér musí znát konkrétní formát souboru, který chce aplikace použít (JPEG nebo PNG nebo něco jiného). Pokud je požadovaný formát ztráty, kódování musí také znát požadovanou úroveň kvality.

Třída SKBitmap definuje jednu Encode metodu s následující syntaxí:

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

Tato metoda je podrobněji popsána krátce. Zakódovaný rastrový obrázek se zapíše do zapisovatelného datového proudu. ("W" SKWStream znamená "zapisovatelné".) Druhý a třetí argument určují formát souboru a (pro ztrátové formáty) požadovanou kvalitu v rozsahu od 0 do 100.

Kromě toho a SKImageSKPixmap třídy také definují Encode metody, které jsou poněkud všestrannější, a které můžete preferovat. Pomocí statické SKImage.FromBitmap metody můžete snadno vytvořit SKImage objekt z SKBitmap objektu. Objekt můžete získat SKPixmap z objektu SKBitmapPeekPixels pomocí metody.

Jedna z metod definovaných EncodeSKImage pomocí žádných parametrů a automaticky se uloží do formátu PNG. Tato metoda bez parametrů je velmi snadná.

Kód specifický pro platformu pro ukládání rastrových souborů

Když objekt zakódujete SKBitmap do konkrétního formátu souboru, obvykle zůstanete s objektem datového proudu nějakého druhu nebo polem dat. Některé metody Encode (včetně metody bez parametrů definovaných SKImage) vrací SKData objekt, který lze pomocí metody převést na pole bajtů ToArray . Tato data se pak musí uložit do souboru.

Ukládání do souboru v místním úložišti aplikace je poměrně snadné, protože pro tuto úlohu můžete použít standardní System.IO třídy a metody. Tato technika je ukázaná v článku Animating SkiaSharp Bitmaps in connection with animating a series bitmaps of the Mandelbrot set.

Pokud chcete soubor sdílet jinými aplikacemi, musíte ho uložit do knihovny fotek uživatele. Tato úloha vyžaduje kód specifický pro platformu a použití Xamarin.FormsDependencyService.

Projekt SkiaSharpFormsDemo v aplikaci SkiaSharpFormsDemos definuje IPhotoLibrary rozhraní používané s DependencyService třídou. Tím se definuje syntaxe SavePhotoAsync metody:

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

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

Toto rozhraní také definuje metodu PickPhotoAsync , která se používá k otevření nástroje pro výběr souborů specifických pro platformu pro knihovnu fotografií zařízení.

První SavePhotoAsyncargument je pole bajtů, které obsahuje rastrový obrázek již zakódovaný do určitého formátu souboru, například JPEG nebo PNG. Je možné, že aplikace může chtít izolovat všechny rastrové obrázky, které vytvoří, do konkrétní složky, která je zadaná v dalším parametru a za ním název souboru. Metoda vrátí logickou hodnotu označující úspěch nebo ne.

V následujících částech se dozvíte, jak SavePhotoAsync se implementuje na jednotlivých platformách.

Implementace iOS

Implementace SavePhotoAsync iOS používá metodu SaveToPhotosAlbumUIImage:

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

Bohužel neexistuje způsob, jak zadat název souboru nebo složku obrázku.

Soubor Info.plist v projektu iOS vyžaduje klíč označující, že přidává obrázky do knihovny fotografií:

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

Dávejte pozor! Klíč oprávnění pro jednoduchý přístup ke knihovně fotek je velmi podobný, ale ne stejný:

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

Implementace Androidu

Implementace Androidu SavePhotoAsync nejprve zkontroluje, jestli folder je null argument nebo prázdný řetězec. Pokud ano, rastrový obrázek se uloží do kořenového adresáře knihovny fotek. Jinak se složka získá a pokud neexistuje, vytvoří se:

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

Volání MediaScannerConnection.ScanFile není nezbytně nutné, ale pokud program testujete okamžitou kontrolou knihovny fotek, pomůže to hodně aktualizací zobrazení galerie knihoven.

Soubor AndroidManifest.xml vyžaduje následující značku oprávnění:

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

Implementace UPW

Implementace SavePhotoAsync UPW je velmi podobná struktuře jako implementace Androidu:

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

Část Schopnosti souboru Package.appxmanifest vyžaduje knihovnu obrázků.

Zkoumání formátů obrázků

Tady je Encode metoda SKImage znovu:

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

SKEncodedImageFormat je výčet se členy, které odkazují na jedenáct rastrových formátů souborů, z nichž některé jsou spíše nejasné:

  • Astc — Adaptivní škálovatelná komprese textury
  • Bmp — Windows Bitmap
  • Dng — Adobe Digital Negative
  • Gif — Formát výměny grafiky
  • Ico — Obrázky ikon Windows
  • Jpeg — Společná fotografická expertská skupina
  • Ktx — Formát textury Khronosu pro OpenGL
  • Pkm — Vlastní formát pro GrafX2
  • Png — Přenosná síťová grafika
  • Wbmp — Formát rastrového obrázku bezdrátové aplikace (1 bit na pixel)
  • Webp — Formát Google WebP

Jak uvidíte krátce, skiaSharp ve skutečnosti podporuje pouze tři z těchto formátů souborů (JpegPngaWebp) .

Pokud chcete uložit objekt pojmenovaný SKBitmapbitmap do knihovny fotografií uživatele, potřebujete také člena výčtu SKEncodedImageFormat s názvem imageFormat a (pro ztrátové formáty) celočíselnou quality proměnnou. Pomocí následujícího kódu můžete tento rastrový obrázek uložit do souboru s názvem filename ve folder složce:

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

Třída SKManagedWStream je odvozena od SKWStream (což je zkratka pro "zapisovatelný datový proud"). Metoda Encode zapíše do daného datového proudu zakódovaný rastrový soubor. Komentáře v tomto kódu odkazují na určitou kontrolu chyb, kterou možná budete muset provést.

Stránka Uložit formáty souborů v aplikaci SkiaSharpFormsDemos používá podobný kód, který vám umožní experimentovat s uložením rastrového obrázku v různých formátech.

Soubor XAML obsahuje SKCanvasView rastrový obrázek, zatímco zbytek stránky obsahuje vše, co aplikace potřebuje k volání Encode metody SKBitmap. Má Picker pro člena výčtu SKEncodedImageFormat , Slider pro argument kvality pro ztrátové rastrové formáty, dvě Entry zobrazení pro název souboru a název složky a Button pro uložení souboru.

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

Soubor s kódem načte rastrový zdroj a použije ho SKCanvasView k jeho zobrazení. Tento rastrový obrázek se nikdy nezmění. Obslužná rutina SelectedIndexChanged pro Picker modifikuje název souboru s příponou, která je stejná jako člen výčtu:

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

Obslužná rutina Clicked pro veškerou skutečnou Button práci. Získá dva argumenty pro Encode z Picker a Slidera pak použije kód zobrazený dříve k vytvoření SKManagedWStream metody Encode . Entry Dvě zobrazení zařízená složka a názvy souborů pro metoduSavePhotoAsync.

Většina této metody se věnuje řešení problémů nebo chyb. Pokud Encode vytvoří prázdné pole, znamená to, že konkrétní formát souboru není podporovaný. Pokud SavePhotoAsync se vrátí false, soubor nebyl úspěšně uložen.

Tady je spuštěný program:

Save File Formats

Tento snímek obrazovky ukazuje pouze tři formáty podporované na těchto platformách:

  • JPEG
  • PNG
  • WebP

Pro všechny ostatní formáty Encode metoda zapíše do datového proudu nic a výsledné bajtové pole je prázdné.

Rastrový obrázek, který stránka Uložit formáty souborů uloží, je čtvercový 600 pixelů. S 4 bajty na pixel je to celkem 1 440 000 bajtů v paměti. Následující tabulka ukazuje velikost souboru pro různé kombinace formátu a kvality souboru:

Formát Kvalita Velikost
PNG 492K
JPEG 0 2,95 K
50 22.1K
100 206K
WebP 0 2.71K
50 11,9K
100 101K

Můžete experimentovat s různými nastaveními kvality a zkoumat výsledky.

Ukládání malování prstem

Jedním z běžných použití rastrového obrázku je kreslení programů, kde funguje jako něco, co se nazývá stínový rastr. Veškerý výkres se zachová na rastrovém obrázku, který pak program zobrazí. Rastrový obrázek je také užitečný pro uložení výkresu.

V článku SkiaSharp se Malování Prst ukazuje, jak pomocí dotykového sledování implementovat primitivní program pro malování prstem. Program podporoval pouze jednu barvu a pouze jednu šířku tahu, ale zachoval celý výkres v kolekci SKPath objektů.

Prst Malování se stránkou Uložit v ukázce SkiaSharpFormsDemos zachová také celý výkres v kolekci SKPath objektů, ale také vykreslí výkres na rastrovém obrázku, který může uložit do knihovny fotografií.

Většina tohoto programu je podobná původnímu programu Finger Malování. Jedním z vylepšení je, že soubor XAML teď vytvoří instance tlačítek označených jako Vymazat a Uložit:

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

Soubor s kódem udržuje pole typu SKBitmap s názvem saveBitmap. Tento rastrový obrázek se vytvoří nebo znovu vytvoří v obslužné rutině PaintSurface při každé změně velikosti plochy zobrazení. Pokud je potřeba rastrový obrázek znovu vytvořit, obsah existujícího rastrového obrázku se zkopíruje do nového rastrového obrázku, aby se zachovalo vše bez ohledu na to, jak se plocha zobrazení změní ve velikosti:

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

Výkres provedený obslužnou rutinou PaintSurface se vyskytuje na samém konci a skládá se výhradně z vykreslení rastrového obrázku.

Dotykové zpracování je podobné dřívějšímu programu. Program udržuje dvě kolekce a completedPaths, které obsahují vše, inProgressPaths co uživatel nakreslil od posledního vymazání zobrazení. Pro každou událost dotyku volá UpdateBitmapobslužná rutinaOnTouchEffectAction:

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

Metoda UpdateBitmap překresluje saveBitmap vytvořením nové SKCanvas, vymazání a následné vykreslení všech cest na rastrovém obrázku. Končí tím, že zneplatní, canvasView aby rastrový obrázek mohl být nakreslen na displeji.

Tady jsou obslužné rutiny pro tato dvě tlačítka. Tlačítko Vymazat vymaže obě kolekce cest, aktualizace saveBitmap (což vede k vymazání rastrového obrázku) a zneplatní 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");
            }
        }
    }
}

Obslužná rutina tlačítka Uložit používá zjednodušenou Encode metodu z SKImage. Tato metoda kóduje pomocí formátu PNG. Objekt SKImage je vytvořen na saveBitmapzákladě a SKData objekt obsahuje kódovaný soubor PNG.

Metoda ToArraySKData získá pole bajtů. Toto je to, co se předává SavePhotoAsync metodě spolu s pevným názvem složky a jedinečným názvem souboru vytvořeným z aktuálního data a času.

Tady je program v akci:

Finger Paint Save

Ve vzorci Spin Malování se používá velmi podobná technika. Jedná se také o program malování prstem s tím rozdílem, že uživatel maluje na otáčejícím se disku, který pak reprodukuje návrhy na dalších čtyřech kvadrantech. Barva prstu se změní při otáčení disku:

Spin Paint

Tlačítko Uložit třídy SpinPaint je podobné prstu Malování v tom, že uloží obrázek do pevného názvu složky (Španělsko Malování) a název souboru vytvořeného z data a času.