Compartilhar via


Exibindo bitmaps do SkiaSharp

O assunto dos bitmaps do SkiaSharp foi introduzido no artigo Noções básicas de bitmap no SkiaSharp. Esse artigo mostrou três maneiras de carregar bitmaps e três maneiras de exibir bitmaps. Este artigo analisa as técnicas para carregar bitmaps e se aprofunda no uso dos DrawBitmap métodos de SKCanvas.

Exibindo amostra

Os DrawBitmapLattice métodos e DrawBitmapNinePatch são discutidos no artigo Exibição segmentada de bitmaps SkiaSharp.

Os exemplos nesta página são do aplicativo de exemplo. Na página inicial desse aplicativo, escolha SkiaSharp Bitmaps e vá para a seção Exibindo Bitmaps .

Carregando um bitmap

Um bitmap usado por um aplicativo SkiaSharp geralmente vem de uma das três fontes diferentes:

  • Pela Internet
  • De um recurso inserido no executável
  • Da biblioteca de fotos do usuário

Também é possível que um aplicativo SkiaSharp crie um novo bitmap e, em seguida, desenhe nele ou defina os bits de bitmap algoritmicamente. Essas técnicas são discutidas nos artigos Criando e desenhando em bitmaps do SkiaSharp e Acessando pixels de bitmap do SkiaSharp.

Nos três exemplos de código a seguir de carregamento de um bitmap, supõe-se que a classe contenha um campo do tipo SKBitmap:

SKBitmap bitmap;

Como afirmou o artigo Noções básicas de bitmap no SkiaSharp , a melhor maneira de carregar um bitmap pela Internet é com a HttpClient classe. Uma única instância da classe pode ser definida como um campo:

HttpClient httpClient = new HttpClient();

Ao usar HttpClient com aplicativos iOS e Android, você desejará definir as propriedades do projeto conforme descrito nos documentos sobre Transport Layer Security (TLS) 1.2.

O código que usa HttpClient geralmente envolve o await operador, portanto, ele deve residir em um async método:

try
{
    using (Stream stream = await httpClient.GetStreamAsync("https:// ··· "))
    using (MemoryStream memStream = new MemoryStream())
    {
        await stream.CopyToAsync(memStream);
        memStream.Seek(0, SeekOrigin.Begin);

        bitmap = SKBitmap.Decode(memStream);
        ···
    };
}
catch
{
    ···
}

Observe que o Stream objeto obtido de é copiado GetStreamAsync para um MemoryStreamarquivo . O Android não permite que o Stream from HttpClient seja processado pelo thread principal, exceto em métodos assíncronos.

O SKBitmap.Decode faz muito trabalho: o Stream objeto passado para ele faz referência a um bloco de memória contendo um bitmap inteiro em um dos formatos de arquivo bitmap comuns, geralmente JPEG, PNG ou GIF. O Decode método deve determinar o formato e, em seguida, decodificar o arquivo de bitmap no próprio formato de bitmap interno do SkiaSharp.

Depois que o código chamar SKBitmap.Decode, ele provavelmente invalidará o CanvasView para que o PaintSurface manipulador possa exibir o bitmap recém-carregado.

A segunda maneira de carregar um bitmap é incluindo o bitmap como um recurso inserido na biblioteca .NET Standard referenciada pelos projetos de plataforma individuais. Uma ID de recurso é passada para o GetManifestResourceStream método. Essa ID de recurso consiste no nome do assembly, no nome da pasta e no nome do arquivo do recurso separados por pontos:

string resourceID = "assemblyName.folderName.fileName";
Assembly assembly = GetType().GetTypeInfo().Assembly;

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

Os arquivos bitmap também podem ser armazenados como recursos no projeto de plataforma individual para iOS, Android e UWP (Plataforma Universal do Windows). No entanto, carregar esses bitmaps requer código localizado no projeto da plataforma.

Uma terceira abordagem para obter um bitmap é da biblioteca de imagens do usuário. O código a seguir usa um serviço de dependência incluído no aplicativo de exemplo. A Biblioteca Padrão do .NET SkiaSharpFormsDemo inclui a IPhotoLibrary interface, enquanto cada um dos projetos de plataforma contém uma PhotoLibrary classe que implementa essa interface.

IPhotoicturePicker picturePicker = DependencyService.Get<IPhotoLibrary>();

using (Stream stream = await picturePicker.GetImageStreamAsync())
{
    if (stream != null)
    {
        bitmap = SKBitmap.Decode(stream);
        ···
    }
}

Geralmente, esse código também invalida o CanvasView para que o PaintSurface manipulador possa exibir o novo bitmap.

A SKBitmap classe define várias propriedades úteis, incluindo Width e Height, que revelam as dimensões em pixels do bitmap, bem como muitos métodos, incluindo métodos para criar bitmaps, copiá-los e expor os bits de pixel.

Exibição em dimensões de pixel

A classe SkiaSharp Canvas define quatro DrawBitmap métodos. Esses métodos permitem que os bitmaps sejam exibidos de duas maneiras fundamentalmente diferentes:

  • Especificar um SKPoint valor (ou separar x e y valores) exibe o bitmap em suas dimensões de pixel. Os pixels do bitmap são mapeados diretamente para os pixels da exibição de vídeo.
  • Especificar um retângulo faz com que o bitmap seja esticado para o tamanho e a forma do retângulo.

Você exibe um bitmap em suas dimensões de pixel usando DrawBitmap com um SKPoint parâmetro ou DrawBitmap com parâmetros e y separadosx:

DrawBitmap(SKBitmap bitmap, SKPoint pt, SKPaint paint = null)

DrawBitmap(SKBitmap bitmap, float x, float y, SKPaint paint = null)

Esses dois métodos são funcionalmente idênticos. O ponto especificado indica o local do canto superior esquerdo do bitmap em relação à tela. Como a resolução de pixels dos dispositivos móveis é tão alta, bitmaps menores geralmente aparecem bem pequenos nesses dispositivos.

O parâmetro opcional SKPaint permite exibir o bitmap usando transparência. Para fazer isso, crie um SKPaint objeto e defina a Color propriedade como qualquer SKColor valor com um canal alfa menor que 1. Por exemplo:

paint.Color = new SKColor(0, 0, 0, 0x80);

O 0x80 aprovado como o último argumento indica 50% de transparência. Você também pode definir um canal alfa em uma das cores predefinidas:

paint.Color = SKColors.Red.WithAlpha(0x80);

No entanto, a cor em si é irrelevante. Somente o canal alfa é examinado quando você usa o SKPaint objeto em uma DrawBitmap chamada.

O SKPaint objeto também desempenha um papel ao exibir bitmaps usando modos de mesclagem ou efeitos de filtro. Estes são demonstrados nos artigos Modos de composição e mistura SkiaSharp e filtros de imagem SkiaSharp.

A página Dimensões em Pixels no programa de exemplo exibe um recurso de bitmap com 320 pixels de largura por 240 pixels de altura:

public class PixelDimensionsPage : ContentPage
{
    SKBitmap bitmap;

    public PixelDimensionsPage()
    {
        Title = "Pixel Dimensions";

        // Load the bitmap from a resource
        string resourceID = "SkiaSharpFormsDemos.Media.Banana.jpg";
        Assembly assembly = GetType().GetTypeInfo().Assembly;

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

        // Create the SKCanvasView and set the PaintSurface handler
        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;
        Content = canvasView;
    }

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

        canvas.Clear();

        float x = (info.Width - bitmap.Width) / 2;
        float y = (info.Height - bitmap.Height) / 2;

        canvas.DrawBitmap(bitmap, x, y);
    }
}

O PaintSurface manipulador centraliza o bitmap calculando x valores com y base nas dimensões em pixels da superfície de exibição e nas dimensões em pixels do bitmap:

Dimensões em pixels

Se o aplicativo quiser exibir o bitmap em seu canto superior esquerdo, ele simplesmente passará coordenadas de (0, 0).

Um método para carregar bitmaps de recursos

Muitos dos exemplos que estão chegando precisarão carregar recursos de bitmap. A classe estática BitmapExtensions na solução de exemplo contém um método para ajudar:

static class BitmapExtensions
{
    public static SKBitmap LoadBitmapResource(Type type, string resourceID)
    {
        Assembly assembly = type.GetTypeInfo().Assembly;

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

Observe o Type parâmetro. Esse pode ser o Type objeto associado a qualquer tipo no assembly que armazena o recurso de bitmap.

Esse LoadBitmapResource método será usado em todos os exemplos subsequentes que exigem recursos de bitmap.

Esticar para preencher um retângulo

A SKCanvas classe também define um DrawBitmap método que renderiza o bitmap em um retângulo e outro DrawBitmap método que renderiza um subconjunto retangular do bitmap em um retângulo:

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

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

Em ambos os casos, o bitmap é esticado para preencher o retângulo chamado dest. No segundo método, o source retângulo permite selecionar um subconjunto do bitmap. O dest retângulo é relativo ao dispositivo de saída; o source retângulo é relativo ao bitmap.

A página Preencher Retângulo demonstra o primeiro desses dois métodos exibindo o mesmo bitmap usado no exemplo anterior em um retângulo do mesmo tamanho da tela:

public class FillRectanglePage : ContentPage
{
    SKBitmap bitmap =
        BitmapExtensions.LoadBitmapResource(typeof(FillRectanglePage),
                                            "SkiaSharpFormsDemos.Media.Banana.jpg");
    public FillRectanglePage ()
    {
        Title = "Fill Rectangle";

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

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

        canvas.Clear();

        canvas.DrawBitmap(bitmap, info.Rect);
    }
}

Observe o uso do novo BitmapExtensions.LoadBitmapResource método para definir o SKBitmap campo. O retângulo de destino é obtido a Rect partir da propriedade de SKImageInfo, que descreve o tamanho da superfície de exibição:

Preencher retângulo

Isso geralmente não é o que você quer. A imagem é distorcida ao ser esticada de forma diferente nas direções horizontal e vertical. Ao exibir um bitmap em algo diferente do tamanho do pixel, geralmente você deseja preservar a taxa de proporção original do bitmap.

Alongamento preservando a proporção

Esticar um bitmap preservando a taxa de proporção é um processo também conhecido como dimensionamento uniforme. Esse termo sugere uma abordagem algorítmica. Uma solução possível é mostrada na página Dimensionamento uniforme :

public class UniformScalingPage : ContentPage
{
    SKBitmap bitmap =
        BitmapExtensions.LoadBitmapResource(typeof(UniformScalingPage),
                                            "SkiaSharpFormsDemos.Media.Banana.jpg");
    public UniformScalingPage()
    {
        Title = "Uniform Scaling";

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

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

        canvas.Clear();

        float scale = Math.Min((float)info.Width / bitmap.Width,
                               (float)info.Height / bitmap.Height);
        float x = (info.Width - scale * bitmap.Width) / 2;
        float y = (info.Height - scale * bitmap.Height) / 2;
        SKRect destRect = new SKRect(x, y, x + scale * bitmap.Width,
                                           y + scale * bitmap.Height);

        canvas.DrawBitmap(bitmap, destRect);
    }
}

O PaintSurface manipulador calcula um scale fator que é o mínimo da proporção entre a largura e a altura da exibição e a largura e a altura do bitmap. Os x valores and y podem ser calculados para centralizar o bitmap dimensionado dentro da largura e altura da exibição. O retângulo de destino tem um canto superior esquerdo de x e y um canto inferior direito desses valores, além da largura e altura dimensionadas do bitmap:

Escala uniforme

Vire o telefone de lado para ver o bitmap esticado para essa área:

Cenário de dimensionamento uniforme

A vantagem de usar esse scale fator torna-se óbvia quando você deseja implementar um algoritmo ligeiramente diferente. Suponha que você queira preservar a taxa de proporção do bitmap, mas também preencher o retângulo de destino. A única maneira de isso ser possível é cortando parte da imagem, mas você pode implementar esse algoritmo simplesmente alterando Math.Min para Math.Max no código acima. Eis o resultado:

Alternativa de dimensionamento uniforme

A taxa de proporção do bitmap é preservada, mas as áreas à esquerda e à direita do bitmap são cortadas.

Uma função de exibição de bitmap versátil

Ambientes de programação baseados em XAML (como UWP e Xamarin.Forms) têm a facilidade de expandir ou reduzir o tamanho dos bitmaps, preservando suas taxas de proporção. Embora o SkiaSharp não inclua esse recurso, você mesmo pode implementá-lo.

A BitmapExtensions classe incluída no aplicativo de exemplo mostra como. A classe define dois novos DrawBitmap métodos que executam o cálculo da taxa de proporção. Esses novos métodos são métodos de extensão do SKCanvas.

Os novos DrawBitmap métodos incluem um parâmetro do tipo BitmapStretch, uma enumeração definida no arquivo BitmapExtensions.cs :

public enum BitmapStretch
{
    None,
    Fill,
    Uniform,
    UniformToFill,
    AspectFit = Uniform,
    AspectFill = UniformToFill
}

Os Nonemembros , Fill, Uniforme UniformToFill são os mesmos da enumeração UWP Stretch . A enumeração semelhante Xamarin.FormsAspect define os membros Fill, AspectFit, e AspectFill.

A página Dimensionamento Uniforme mostrada acima centraliza o bitmap dentro do retângulo, mas talvez você queira outras opções, como posicionar o bitmap no lado esquerdo ou direito do retângulo ou na parte superior ou inferior. Esse é o objetivo da BitmapAlignment enumeração:

public enum BitmapAlignment
{
    Start,
    Center,
    End
}

As configurações de alinhamento não têm efeito quando usadas com BitmapStretch.Fill.

A primeira DrawBitmap função de extensão contém um retângulo de destino, mas nenhum retângulo de origem. Os padrões são definidos para que, se você quiser que o bitmap seja centralizado, você só precisa especificar um BitmapStretch membro:

static class BitmapExtensions
{
    ···
    public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, SKRect dest,
                                  BitmapStretch stretch,
                                  BitmapAlignment horizontal = BitmapAlignment.Center,
                                  BitmapAlignment vertical = BitmapAlignment.Center,
                                  SKPaint paint = null)
    {
        if (stretch == BitmapStretch.Fill)
        {
            canvas.DrawBitmap(bitmap, dest, paint);
        }
        else
        {
            float scale = 1;

            switch (stretch)
            {
                case BitmapStretch.None:
                    break;

                case BitmapStretch.Uniform:
                    scale = Math.Min(dest.Width / bitmap.Width, dest.Height / bitmap.Height);
                    break;

                case BitmapStretch.UniformToFill:
                    scale = Math.Max(dest.Width / bitmap.Width, dest.Height / bitmap.Height);
                    break;
            }

            SKRect display = CalculateDisplayRect(dest, scale * bitmap.Width, scale * bitmap.Height,
                                                  horizontal, vertical);

            canvas.DrawBitmap(bitmap, display, paint);
        }
    }
    ···
}

A principal finalidade desse método é calcular um fator de dimensionamento chamado scale que é aplicado à largura e altura do bitmap ao chamar o CalculateDisplayRect método. Este é o método que calcula um retângulo para exibir o bitmap com base no alinhamento horizontal e vertical:

static class BitmapExtensions
{
    ···
    static SKRect CalculateDisplayRect(SKRect dest, float bmpWidth, float bmpHeight,
                                       BitmapAlignment horizontal, BitmapAlignment vertical)
    {
        float x = 0;
        float y = 0;

        switch (horizontal)
        {
            case BitmapAlignment.Center:
                x = (dest.Width - bmpWidth) / 2;
                break;

            case BitmapAlignment.Start:
                break;

            case BitmapAlignment.End:
                x = dest.Width - bmpWidth;
                break;
        }

        switch (vertical)
        {
            case BitmapAlignment.Center:
                y = (dest.Height - bmpHeight) / 2;
                break;

            case BitmapAlignment.Start:
                break;

            case BitmapAlignment.End:
                y = dest.Height - bmpHeight;
                break;
        }

        x += dest.Left;
        y += dest.Top;

        return new SKRect(x, y, x + bmpWidth, y + bmpHeight);
    }
}

A BitmapExtensions classe contém um método adicional DrawBitmap com um retângulo de origem para especificar um subconjunto do bitmap. Esse método é semelhante ao primeiro, exceto que o fator de escala é calculado com base no source retângulo e, em seguida, aplicado ao source retângulo na chamada para CalculateDisplayRect:

static class BitmapExtensions
{
    ···
    public static void DrawBitmap(this SKCanvas canvas, SKBitmap bitmap, SKRect source, SKRect dest,
                                  BitmapStretch stretch,
                                  BitmapAlignment horizontal = BitmapAlignment.Center,
                                  BitmapAlignment vertical = BitmapAlignment.Center,
                                  SKPaint paint = null)
    {
        if (stretch == BitmapStretch.Fill)
        {
            canvas.DrawBitmap(bitmap, source, dest, paint);
        }
        else
        {
            float scale = 1;

            switch (stretch)
            {
                case BitmapStretch.None:
                    break;

                case BitmapStretch.Uniform:
                    scale = Math.Min(dest.Width / source.Width, dest.Height / source.Height);
                    break;

                case BitmapStretch.UniformToFill:
                    scale = Math.Max(dest.Width / source.Width, dest.Height / source.Height);
                    break;
            }

            SKRect display = CalculateDisplayRect(dest, scale * source.Width, scale * source.Height,
                                                  horizontal, vertical);

            canvas.DrawBitmap(bitmap, source, display, paint);
        }
    }
    ···
}

O primeiro desses dois novos DrawBitmap métodos é demonstrado na página Modos de dimensionamento. O arquivo XAML contém três Picker elementos que permitem selecionar membros das BitmapStretch enumerações e BitmapAlignment :

<?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:local="clr-namespace:SkiaSharpFormsDemos"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Bitmaps.ScalingModesPage"
             Title="Scaling Modes">

    <Grid Padding="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <skia:SKCanvasView x:Name="canvasView"
                           Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
                           PaintSurface="OnCanvasViewPaintSurface" />

        <Label Text="Stretch:"
               Grid.Row="1" Grid.Column="0"
               VerticalOptions="Center" />

        <Picker x:Name="stretchPicker"
                Grid.Row="1" Grid.Column="1"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type local:BitmapStretch}">
                    <x:Static Member="local:BitmapStretch.None" />
                    <x:Static Member="local:BitmapStretch.Fill" />
                    <x:Static Member="local:BitmapStretch.Uniform" />
                    <x:Static Member="local:BitmapStretch.UniformToFill" />
                </x:Array>
            </Picker.ItemsSource>

            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>

        <Label Text="Horizontal Alignment:"
               Grid.Row="2" Grid.Column="0"
               VerticalOptions="Center" />

        <Picker x:Name="horizontalPicker"
                Grid.Row="2" Grid.Column="1"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type local:BitmapAlignment}">
                    <x:Static Member="local:BitmapAlignment.Start" />
                    <x:Static Member="local:BitmapAlignment.Center" />
                    <x:Static Member="local:BitmapAlignment.End" />
                </x:Array>
            </Picker.ItemsSource>

            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>

        <Label Text="Vertical Alignment:"
               Grid.Row="3" Grid.Column="0"
               VerticalOptions="Center" />

        <Picker x:Name="verticalPicker"
                Grid.Row="3" Grid.Column="1"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type local:BitmapAlignment}">
                    <x:Static Member="local:BitmapAlignment.Start" />
                    <x:Static Member="local:BitmapAlignment.Center" />
                    <x:Static Member="local:BitmapAlignment.End" />
                </x:Array>
            </Picker.ItemsSource>

            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>
    </Grid>
</ContentPage>

O arquivo code-behind simplesmente invalida o CanvasView quando qualquer Picker item é alterado. O PaintSurface manipulador acessa as três Picker exibições para chamar o DrawBitmap método de extensão:

public partial class ScalingModesPage : ContentPage
{
    SKBitmap bitmap =
        BitmapExtensions.LoadBitmapResource(typeof(ScalingModesPage),
                                            "SkiaSharpFormsDemos.Media.Banana.jpg");
    public ScalingModesPage()
    {
        InitializeComponent();
    }

    private void OnPickerSelectedIndexChanged(object sender, EventArgs args)
    {
        canvasView.InvalidateSurface();
    }

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

        canvas.Clear();

        SKRect dest = new SKRect(0, 0, info.Width, info.Height);

        BitmapStretch stretch = (BitmapStretch)stretchPicker.SelectedItem;
        BitmapAlignment horizontal = (BitmapAlignment)horizontalPicker.SelectedItem;
        BitmapAlignment vertical = (BitmapAlignment)verticalPicker.SelectedItem;

        canvas.DrawBitmap(bitmap, dest, stretch, horizontal, vertical);
    }
}

Aqui estão algumas combinações de opções:

Modos de dimensionamento

A página Subconjunto Retangular tem praticamente o mesmo arquivo XAML que os Modos de Dimensionamento, mas o arquivo code-behind define um subconjunto retangular do bitmap fornecido pelo SOURCE campo:

public partial class ScalingModesPage : ContentPage
{
    SKBitmap bitmap =
        BitmapExtensions.LoadBitmapResource(typeof(ScalingModesPage),
                                            "SkiaSharpFormsDemos.Media.Banana.jpg");

    static readonly SKRect SOURCE = new SKRect(94, 12, 212, 118);

    public RectangleSubsetPage()
    {
        InitializeComponent();
    }

    private void OnPickerSelectedIndexChanged(object sender, EventArgs args)
    {
        canvasView.InvalidateSurface();
    }

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

        canvas.Clear();

        SKRect dest = new SKRect(0, 0, info.Width, info.Height);

        BitmapStretch stretch = (BitmapStretch)stretchPicker.SelectedItem;
        BitmapAlignment horizontal = (BitmapAlignment)horizontalPicker.SelectedItem;
        BitmapAlignment vertical = (BitmapAlignment)verticalPicker.SelectedItem;

        canvas.DrawBitmap(bitmap, SOURCE, dest, stretch, horizontal, vertical);
    }
}

Essa origem retangular isola a cabeça do macaco, conforme mostrado nestas capturas de tela:

Subconjunto de retângulo