Bitmap Basics in SkiaSharp

Load bitmaps from various sources and display them.

The support of bitmaps in SkiaSharp is quite extensive. This article covers only the basics — how to load bitmaps and how to display them:

A much deeper exploration of bitmaps can be found in the section SkiaSharp Bitmaps.

A SkiaSharp bitmap is an object of type SKBitmap. There are many ways to create a bitmap but this article restricts itself to the SKBitmap.Decode method, which loads the bitmap from a .NET Stream object.

The Basic Bitmaps page in the SkiaSharpFormsDemos program demonstrates how to load bitmaps from three different sources:

  • From over the Internet
  • From a resource embedded in the executable
  • From the user's photo library

Three SKBitmap objects for these three sources are defined as fields in the BasicBitmapsPage class:

public class BasicBitmapsPage : ContentPage
{
    SKCanvasView canvasView;
    SKBitmap webBitmap;
    SKBitmap resourceBitmap;
    SKBitmap libraryBitmap;

    public BasicBitmapsPage()
    {
        Title = "Basic Bitmaps";

        canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
        ...
    }
    ...
}

Loading a Bitmap from the Web

To load a bitmap based on a URL, you can use the HttpClient class. You should instantiate only one instance of HttpClient and reuse it, so store it as a field:

HttpClient httpClient = new HttpClient();

When using HttpClient with iOS and Android applications, you'll want to set project properties as described in the documents on Transport Layer Security (TLS) 1.2.

Because it's most convenient to use the await operator with HttpClient, the code can't be executed in the BasicBitmapsPage constructor. Instead, it's part of the OnAppearing override. The URL here points to an area on the Xamarin web site with some sample bitmaps. A package on the web site allows appending a specification for resizing the bitmap to a particular width:

protected override async void OnAppearing()
{
    base.OnAppearing();

    // Load web bitmap.
    string url = "https://developer.xamarin.com/demo/IMG_3256.JPG?width=480";

    try
    {
        using (Stream stream = await httpClient.GetStreamAsync(url))
        using (MemoryStream memStream = new MemoryStream())
        {
            await stream.CopyToAsync(memStream);
            memStream.Seek(0, SeekOrigin.Begin);

            webBitmap = SKBitmap.Decode(stream);
            canvasView.InvalidateSurface();
        };
    }
    catch
    {
    }
}

The Android operating system raises an exception when using the Stream returned from GetStreamAsync in the SKBitmap.Decode method because it's performing a lengthy operation on a main thread. For this reason, the contents of the bitmap file are copied to a MemoryStream object using CopyToAsync.

The static SKBitmap.Decode method is responsible for decoding bitmap files. It works with JPEG, PNG, and GIF bitmap formats, and stores the results in an internal SkiaSharp format. At this point, the SKCanvasView needs to be invalidated to allow the PaintSurface handler to update the display.

Loading a Bitmap Resource

In terms of code, the easiest approach to loading bitmaps is including a bitmap resource directly in your application. The SkiaSharpFormsDemos program includes a folder named Media containing several bitmap files, including one named monkey.png. For bitmaps stored as program resources, you must use the Properties dialog to give the file a Build Action of Embedded Resource!

Each embedded resource has a resource ID that consists of the project name, the folder, and the filename, all connected by periods: SkiaSharpFormsDemos.Media.monkey.png. You can get access to this resource by specifying that resource ID as an argument to the GetManifestResourceStream method of the Assembly class:

string resourceID = "SkiaSharpFormsDemos.Media.monkey.png";
Assembly assembly = GetType().GetTypeInfo().Assembly;

using (Stream stream = assembly.GetManifestResourceStream(resourceID))
{
    resourceBitmap = SKBitmap.Decode(stream);
}

This Stream object can be passed directly to the SKBitmap.Decode method.

Loading a Bitmap from the Photo Library

It's also possible for the user to load a photo from the device's picture library. This facility is not provided by Xamarin.Forms itself. The job requires a dependency service, such as the one described in the article Picking a Photo from the Picture Library.

The IPhotoLibrary.cs file in the SkiaSharpFormsDemos project and the three PhotoLibrary.cs files in the platform projects have been adapted from that article. In addition, the Android MainActivity.cs file has been modified as described in the article, and the iOS project has been given permission to access the photo library with two lines towards the bottom of the info.plist file.

The BasicBitmapsPage constructor adds a TapGestureRecognizer to the SKCanvasView to be notified of taps. On receipt of a tap, the Tapped handler gets access to the picture-picker dependency service and calls PickPhotoAsync. If a Stream object is returned, then it is passed to the SKBitmap.Decode method:

// Add tap gesture recognizer
TapGestureRecognizer tapRecognizer = new TapGestureRecognizer();
tapRecognizer.Tapped += async (sender, args) =>
{
    // Load bitmap from photo library
    IPhotoLibrary photoLibrary = DependencyService.Get<IPhotoLibrary>();

    using (Stream stream = await photoLibrary.PickPhotoAsync())
    {
        if (stream != null)
        {
            libraryBitmap = SKBitmap.Decode(stream);
            canvasView.InvalidateSurface();
        }
    }
};
canvasView.GestureRecognizers.Add(tapRecognizer);

Notice that the Tapped handler also calls the InvalidateSurface method of the SKCanvasView object. This generates a new call to the PaintSurface handler.

Displaying the Bitmaps

The PaintSurface handler needs to display three bitmaps. The handler assumes that the phone is in portrait mode and divides the canvas vertically into three equal parts.

The first bitmap is displayed with the simplest DrawBitmap method. All you need to specify are the X and Y coordinates where the upper-left corner of the bitmap is to be positioned:

public void DrawBitmap (SKBitmap bitmap, Single x, Single y, SKPaint paint = null)

Although an SKPaint parameter is defined, it has a default value of null and you can ignore it. The pixels of the bitmap are simply transferred to the pixels of the display surface with a one-to-one mapping. You'll see an application for this SKPaint argument in the next section on SkiaSharp Transparency.

A program can obtain the pixel dimensions of a bitmap with the Width and Height properties. These properties allow the program to calculate coordinates to position the bitmap in the center of the upper-third of the canvas:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKSurface surface = args.Surface;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();

    if (webBitmap != null)
    {
        float x = (info.Width - webBitmap.Width) / 2;
        float y = (info.Height / 3 - webBitmap.Height) / 2;
        canvas.DrawBitmap(webBitmap, x, y);
    }
    ...
}

The other two bitmaps are displayed with a version of DrawBitmap with an SKRect parameter:

public void DrawBitmap (SKBitmap bitmap, SKRect dest, SKPaint paint = null)

A third version of DrawBitmap has two SKRect arguments for specifying a rectangular subset of the bitmap to display, but that version isn't used in this article.

Here's the code to display the bitmap loaded from an embedded resource bitmap:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    ...
    if (resourceBitmap != null)
    {
        canvas.DrawBitmap(resourceBitmap,
            new SKRect(0, info.Height / 3, info.Width, 2 * info.Height / 3));
    }
    ...
}

The bitmap is stretched to the dimensions of the rectangle, which is why the monkey is horizontally stretched in these screenshots:

The third image — which you can only see if you run the program and load a photo from your own picture library — is also displayed within a rectangle, but the rectangle's position and size are adjusted to maintain the bitmap's aspect ratio. This calculation is a little more involved because it requires calculating a scaling factor based on the size of the bitmap and the destination rectangle, and centering the rectangle in that area:

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    ...
    if (libraryBitmap != null)
    {
        float scale = Math.Min((float)info.Width / libraryBitmap.Width,
                               info.Height / 3f / libraryBitmap.Height);

        float left = (info.Width - scale * libraryBitmap.Width) / 2;
        float top = (info.Height / 3 - scale * libraryBitmap.Height) / 2;
        float right = left + scale * libraryBitmap.Width;
        float bottom = top + scale * libraryBitmap.Height;
        SKRect rect = new SKRect(left, top, right, bottom);
        rect.Offset(0, 2 * info.Height / 3);

        canvas.DrawBitmap(libraryBitmap, rect);
    }
    else
    {
        using (SKPaint paint = new SKPaint())
        {
            paint.Color = SKColors.Blue;
            paint.TextAlign = SKTextAlign.Center;
            paint.TextSize = 48;

            canvas.DrawText("Tap to load bitmap",
                info.Width / 2, 5 * info.Height / 6, paint);
        }
    }
}

If no bitmap has yet been loaded from the picture library, then the else block displays some text to prompt the user to tap the screen.

You can display bitmaps with various degrees of transparency, and the next article on SkiaSharp Transparency describes how.