Partilhar via


Criando e desenhando em bitmaps SkiaSharp

Você viu como um aplicativo pode carregar bitmaps da Web, dos recursos do aplicativo e da biblioteca de fotos do usuário. Também é possível criar novos bitmaps em seu aplicativo. A abordagem mais simples envolve um dos construtores de SKBitmap:

SKBitmap bitmap = new SKBitmap(width, height);

Os width parâmetros e height são inteiros e especificam as dimensões em pixels do bitmap. Este construtor cria um bitmap colorido com quatro bytes por pixel: um byte cada para os componentes vermelho, verde, azul e alfa (opacidade).

Depois de criar um novo bitmap, você precisa obter algo na superfície do bitmap. Você geralmente faz isso de duas maneiras:

  • Desenhe no bitmap usando métodos de desenho padrão Canvas .
  • Acesse os bits de pixel diretamente.

Este artigo demonstra a primeira abordagem:

Amostra de desenho

A segunda abordagem é discutida no artigo Acessando pixels de bitmap SkiaSharp.

Desenhando no bitmap

Desenhar na superfície de um bitmap é o mesmo que desenhar em uma exibição de vídeo. Para desenhar em uma exibição de vídeo, você obtém um SKCanvas objeto dos PaintSurface argumentos de evento. Para desenhar em um bitmap, crie um SKCanvas objeto usando o SKCanvas construtor:

SKCanvas canvas = new SKCanvas(bitmap);

Quando terminar de desenhar no bitmap, você poderá descartar o SKCanvas objeto. Por esta razão, o SKCanvas construtor é geralmente chamado em uma using declaração:

using (SKCanvas canvas = new SKCanvas(bitmap))
{
    ··· // call drawing function
}

O bitmap pode então ser exibido. Posteriormente, o programa pode criar um novo SKCanvas objeto com base nesse mesmo bitmap e desenhar nele um pouco mais.

A página Hello Bitmap no aplicativo de exemplo grava o texto "Hello, Bitmap!" em um bitmap e, em seguida, exibe esse bitmap várias vezes.

O construtor do HelloBitmapPage começa criando um SKPaint objeto para exibir texto. Ele determina as dimensões de uma cadeia de caracteres de texto e cria um bitmap com essas dimensões. Em seguida, ele cria um SKCanvas objeto com base nesse bitmap, chama e Clear, em seguida, chama DrawText. É sempre uma boa ideia chamar Clear com um novo bitmap porque um bitmap recém-criado pode conter dados aleatórios.

O construtor conclui criando um SKCanvasView objeto para exibir o bitmap:

public partial class HelloBitmapPage : ContentPage
{
    const string TEXT = "Hello, Bitmap!";
    SKBitmap helloBitmap;

    public HelloBitmapPage()
    {
        Title = TEXT;

        // Create bitmap and draw on it
        using (SKPaint textPaint = new SKPaint { TextSize = 48 })
        {
            SKRect bounds = new SKRect();
            textPaint.MeasureText(TEXT, ref bounds);

            helloBitmap = new SKBitmap((int)bounds.Right,
                                       (int)bounds.Height);

            using (SKCanvas bitmapCanvas = new SKCanvas(helloBitmap))
            {
                bitmapCanvas.Clear();
                bitmapCanvas.DrawText(TEXT, 0, -bounds.Top, textPaint);
            }
        }

        // Create SKCanvasView to view result
        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(SKColors.Aqua);

        for (float y = 0; y < info.Height; y += helloBitmap.Height)
            for (float x = 0; x < info.Width; x += helloBitmap.Width)
            {
                canvas.DrawBitmap(helloBitmap, x, y);
            }
    }
}

O PaintSurface manipulador renderiza o bitmap várias vezes em linhas e colunas da exibição. Observe que o Clear método no PaintSurface manipulador tem um argumento de SKColors.Aqua, que colore o plano de fundo da superfície de exibição:

Olá, Bitmap!

A aparência do fundo aqua revela que o bitmap é transparente, exceto para o texto.

Compensação e transparência

A exibição da página Hello Bitmap demonstra que o bitmap que o programa criou é transparente, exceto para o texto preto. É por isso que a cor aqua da superfície da tela aparece.

A documentação dos Clear métodos de SKCanvas descreve-os com a instrução: "Substitui todos os pixels no clipe atual da tela". O uso da palavra "substitui" revela uma característica importante desses métodos: todos os métodos de desenho de SKCanvas adicionar algo à superfície de exibição existente. Os Clear métodos substituem o que já existe.

Clear existe em duas versões diferentes:

  • O Clear método com um SKColor parâmetro substitui os pixels da superfície de exibição por pixels dessa cor.

  • O Clear método sem parâmetros substitui os pixels pela SKColors.Empty cor, que é uma cor na qual todos os componentes (vermelho, verde, azul e alfa) são definidos como zero. Essa cor às vezes é chamada de "preto transparente".

Chamar Clear sem argumentos em um novo bitmap inicializa o bitmap inteiro para ser totalmente transparente. Qualquer coisa desenhada posteriormente no bitmap geralmente será opaca ou parcialmente opaca.

Aqui está algo para tentar: Na página Hello Bitmap , substitua o Clear método aplicado ao bitmapCanvas por este:

bitmapCanvas.Clear(new SKColor(255, 0, 0, 128));

A ordem dos SKColor parâmetros do construtor é vermelho, verde, azul e alfa, onde cada valor pode variar de 0 a 255. Lembre-se de que um valor alfa de 0 é transparente, enquanto um valor alfa de 255 é opaco.

O valor (255, 0, 0, 128) limpa os pixels de bitmap para pixels vermelhos com uma opacidade de 50%. Isso significa que o plano de fundo do bitmap é semitransparente. O fundo vermelho semitransparente do bitmap combina com o plano de fundo aquático da superfície de exibição para criar um plano de fundo cinza.

Tente definir a cor do texto como preto transparente colocando a SKPaint seguinte atribuição no inicializador:

Color = new SKColor(0, 0, 0, 0)

Você pode pensar que esse texto transparente criaria áreas totalmente transparentes do bitmap através das quais você veria o plano de fundo aquático da superfície de exibição. Mas não é assim. O texto é desenhado sobre o que já está no bitmap. O texto transparente não será visível.

Nenhum Draw método torna um bitmap mais transparente. Só Clear pode fazer isso.

Tipos de cores de bitmap

O construtor mais SKBitmap simples permite que você especifique uma largura e altura de pixel inteiro para o bitmap. Outras SKBitmap construtoras são mais complexas. Esses construtores exigem argumentos de dois tipos de enumeração: SKColorType e SKAlphaType. Outros construtores utilizam a SKImageInfo estrutura, que consolida essas informações.

A SKColorType enumeração tem 9 membros. Cada um desses membros descreve uma maneira particular de armazenar os pixels de bitmap:

  • Unknown
  • Alpha8 — cada pixel é de 8 bits, representando um valor alfa de totalmente transparente para totalmente opaco
  • Rgb565 — cada pixel é de 16 bits, 5 bits para vermelho e azul, e 6 para verde
  • Argb4444 — cada pixel é de 16 bits, 4 cada para alfa, vermelho, verde e azul
  • Rgba8888 — cada pixel é de 32 bits, 8 cada para vermelho, verde, azul e alfa
  • Bgra8888 — cada pixel é de 32 bits, 8 cada para azul, verde, vermelho e alfa
  • Index8 — cada pixel é de 8 bits e representa um índice em um SKColorTable
  • Gray8 — cada pixel é de 8 bits representando um tom de cinza do preto ao branco
  • RgbaF16 — cada pixel é de 64 bits, com vermelho, verde, azul e alfa em um formato de ponto flutuante de 16 bits

Os dois formatos em que cada pixel é de 32 pixels (4 bytes) são frequentemente chamados de formatos coloridos . Muitos dos outros formatos datam de uma época em que as próprias exibições de vídeo não eram capazes de colorir. Bitmaps de cores limitadas eram adequados para essas exibições e permitiam que bitmaps ocupassem menos espaço na memória.

Hoje em dia, os programadores quase sempre usam bitmaps coloridos e não se preocupam com outros formatos. A exceção é o RgbaF16 formato, que permite maior resolução de cores do que até mesmo os formatos coloridos. No entanto, esse formato é usado para fins especializados, como imagens médicas, e não faz muito sentido quando usado com monitores coloridos padrão.

Esta série de artigos se restringirá aos SKBitmap formatos de cores usados por padrão quando nenhum SKColorType membro for especificado. Esse formato padrão é baseado na plataforma subjacente. Para as plataformas suportadas pelo Xamarin.Forms, o tipo de cor padrão é:

  • Rgba8888 para iOS e Android
  • Bgra8888 para a UWP

A única diferença é a ordem dos 4 bytes na memória, e isso só se torna um problema quando você acessa diretamente os bits de pixel. Isso não se tornará importante até que você chegue ao artigo Acessando pixels de bitmap do SkiaSharp.

A SKAlphaType enumeração tem quatro membros:

  • Unknown
  • Opaque — o bitmap não tem transparência
  • Premul — os componentes de cor são pré-multiplicados pelo componente alfa
  • Unpremul — os componentes de cor não são pré-multiplicados pelo componente alfa

Aqui está um pixel de bitmap vermelho de 4 bytes com 50% de transparência, com os bytes mostrados na ordem vermelha, verde, azul, alfa:

0xFF 0x00 0x00 0x80

Quando um bitmap contendo pixels semitransparentes é renderizado em uma superfície de exibição, os componentes de cor de cada pixel de bitmap devem ser multiplicados pelo valor alfa desse pixel, e os componentes de cor do pixel correspondente da superfície de exibição devem ser multiplicados por 255 menos o valor alfa. Os dois pixels podem então ser combinados. O bitmap pode ser renderizado mais rapidamente se os componentes de cor nos pixels de bitmap já tiverem sido pré-multiplicados pelo valor alfa. Esse mesmo pixel vermelho seria armazenado assim em um formato pré-multiplicado:

0x80 0x00 0x00 0x80

Essa melhoria de desempenho é o motivo pelo qual SkiaSharp os bitmaps por padrão são criados com um Premul formato. Mas, novamente, torna-se necessário saber disso apenas quando você acessa e manipula pixel bits.

Desenhando em bitmaps existentes

Não é necessário criar um novo bitmap para desenhar nele. Você também pode desenhar em um bitmap existente.

A página Bigode do Macaco usa seu construtor para carregar a imagem MonkeyFace.png. Em seguida, ele cria um SKCanvas objeto com base nesse bitmap e usa SKPaint e SKPath objetos para desenhar um bigode nele:

public partial class MonkeyMoustachePage : ContentPage
{
    SKBitmap monkeyBitmap;

    public MonkeyMoustachePage()
    {
        Title = "Monkey Moustache";

        monkeyBitmap = BitmapExtensions.LoadBitmapResource(GetType(),
            "SkiaSharpFormsDemos.Media.MonkeyFace.png");

        // Create canvas based on bitmap
        using (SKCanvas canvas = new SKCanvas(monkeyBitmap))
        {
            using (SKPaint paint = new SKPaint())
            {
                paint.Style = SKPaintStyle.Stroke;
                paint.Color = SKColors.Black;
                paint.StrokeWidth = 24;
                paint.StrokeCap = SKStrokeCap.Round;

                using (SKPath path = new SKPath())
                {
                    path.MoveTo(380, 390);
                    path.CubicTo(560, 390, 560, 280, 500, 280);

                    path.MoveTo(320, 390);
                    path.CubicTo(140, 390, 140, 280, 200, 280);

                    canvas.DrawPath(path, paint);
                }
            }
        }

        // Create SKCanvasView to view result
        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(monkeyBitmap, info.Rect, BitmapStretch.Uniform);
    }
}

O construtor conclui criando um SKCanvasView manipulador cujo PaintSurface simplesmente exibe o resultado:

Bigode de Macaco

Copiando e modificando bitmaps

Os métodos SKCanvas que você pode usar para desenhar em um bitmap incluem DrawBitmap. Isso significa que você pode desenhar um bitmap em outro, geralmente modificando-o de alguma forma.

A maneira mais versátil de modificar um bitmap é acessando os bits de pixel reais, um assunto abordado no artigo Acessando pixels de bitmap SkiaSharp. Mas existem muitas outras técnicas para modificar bitmaps que não exigem o acesso aos bits de pixel.

O seguinte bitmap incluído com o aplicativo de exemplo tem 360 pixels de largura e 480 pixels de altura:

Alpinistas

Suponha que você não tenha recebido permissão do macaco à esquerda para publicar esta fotografia. Uma solução é obscurecer o rosto do macaco usando uma técnica chamada pixelização. Os pixels do rosto são substituídos por blocos de cor para que você não possa distinguir as características. Os blocos de cor são geralmente derivados da imagem original pela média das cores dos pixels correspondentes a esses blocos. Mas você não precisa fazer essa média sozinho. Isso acontece automaticamente quando você copia um bitmap em uma dimensão de pixel menor.

A face do macaco esquerdo ocupa aproximadamente uma área quadrada de 72 pixels com um canto superior esquerdo no ponto (112, 238). Vamos substituir essa área quadrada de 72 pixels por uma matriz de 9 por 9 de blocos coloridos, cada um dos quais é quadrado de 8 por 8 pixels.

A página Pixelizar imagem é carregada nesse bitmap e primeiro cria um pequeno bitmap quadrado de 9 pixels chamado faceBitmap. Este é um destino para copiar apenas o rosto do macaco. O retângulo de destino é apenas quadrado de 9 pixels, mas o retângulo de origem é quadrado de 72 pixels. Cada bloco 8 por 8 de pixels de origem é consolidado em apenas um pixel, calculando a média das cores.

A próxima etapa é copiar o bitmap original em um novo bitmap do mesmo tamanho chamado pixelizedBitmap. O minúsculo faceBitmap é então copiado em cima disso com um retângulo de destino quadrado de 72 pixels para que cada pixel de faceBitmap seja expandido para 8 vezes seu tamanho:

public class PixelizedImagePage : ContentPage
{
    SKBitmap pixelizedBitmap;

    public PixelizedImagePage ()
    {
        Title = "Pixelize Image";

        SKBitmap originalBitmap = BitmapExtensions.LoadBitmapResource(GetType(),
            "SkiaSharpFormsDemos.Media.MountainClimbers.jpg");

        // Create tiny bitmap for pixelized face
        SKBitmap faceBitmap = new SKBitmap(9, 9);

        // Copy subset of original bitmap to that
        using (SKCanvas canvas = new SKCanvas(faceBitmap))
        {
            canvas.Clear();
            canvas.DrawBitmap(originalBitmap,
                              new SKRect(112, 238, 184, 310),   // source
                              new SKRect(0, 0, 9, 9));          // destination

        }

        // Create full-sized bitmap for copy
        pixelizedBitmap = new SKBitmap(originalBitmap.Width, originalBitmap.Height);

        using (SKCanvas canvas = new SKCanvas(pixelizedBitmap))
        {
            canvas.Clear();

            // Draw original in full size
            canvas.DrawBitmap(originalBitmap, new SKPoint());

            // Draw tiny bitmap to cover face
            canvas.DrawBitmap(faceBitmap,
                              new SKRect(112, 238, 184, 310));  // destination
        }

        // Create SKCanvasView to view result
        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(pixelizedBitmap, info.Rect, BitmapStretch.Uniform);
    }
}

O construtor conclui criando um SKCanvasView para exibir o resultado:

Pixelizar imagem

Rotação de bitmaps

Outra tarefa comum é girar bitmaps. Isso é particularmente útil ao recuperar bitmaps de uma biblioteca de fotos do iPhone ou iPad. A menos que o dispositivo tenha sido mantido em uma orientação específica quando a foto foi tirada, é provável que a foto esteja de cabeça para baixo ou de lado.

Virar um bitmap de cabeça para baixo requer a criação de outro bitmap do mesmo tamanho do primeiro e, em seguida, definir uma transformação para girar em 180 graus enquanto copia o primeiro para o segundo. Em todos os exemplos desta seção, bitmap é o SKBitmap objeto que você precisa girar:

SKBitmap rotatedBitmap = new SKBitmap(bitmap.Width, bitmap.Height);

using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
{
    canvas.Clear();
    canvas.RotateDegrees(180, bitmap.Width / 2, bitmap.Height / 2);
    canvas.DrawBitmap(bitmap, new SKPoint());
}

Ao girar em 90 graus, você precisa criar um bitmap de tamanho diferente do original, trocando a altura e a largura. Por exemplo, se o bitmap original tiver 1200 pixels de largura e 800 pixels de altura, o bitmap girado terá 800 pixels de largura e 1200 pixels de largura. Defina a conversão e a rotação para que o bitmap seja girado em torno de seu canto superior esquerdo e, em seguida, deslocado para a exibição. (Lembre-se de que os Translate métodos e RotateDegrees são chamados na ordem oposta da maneira como são aplicados.) Aqui está o código para girar 90 graus no sentido horário:

SKBitmap rotatedBitmap = new SKBitmap(bitmap.Height, bitmap.Width);

using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
{
    canvas.Clear();
    canvas.Translate(bitmap.Height, 0);
    canvas.RotateDegrees(90);
    canvas.DrawBitmap(bitmap, new SKPoint());
}

E aqui está uma função semelhante para girar 90 graus no sentido anti-horário:

SKBitmap rotatedBitmap = new SKBitmap(bitmap.Height, bitmap.Width);

using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
{
    canvas.Clear();
    canvas.Translate(0, bitmap.Width);
    canvas.RotateDegrees(-90);
    canvas.DrawBitmap(bitmap, new SKPoint());
}

Esses dois métodos são usados nas páginas do Photo Puzzle descritas no artigo Recortando bitmaps SkiaSharp.

Um programa que permite ao usuário girar um bitmap em incrementos de 90 graus precisa implementar apenas uma função para girar em 90 graus. O usuário pode então girar em qualquer incremento de 90 graus pela execução repetida desta função.

Um programa também pode girar um bitmap em qualquer quantidade. Uma abordagem simples é modificar a função que gira em 180 graus, substituindo 180 por uma variável generalizada angle :

SKBitmap rotatedBitmap = new SKBitmap(bitmap.Width, bitmap.Height);

using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
{
    canvas.Clear();
    canvas.RotateDegrees(angle, bitmap.Width / 2, bitmap.Height / 2);
    canvas.DrawBitmap(bitmap, new SKPoint());
}

No entanto, no caso geral, essa lógica cortará os cantos do bitmap rotacionado. Uma abordagem melhor é calcular o tamanho do bitmap girado usando trigonometria para incluir esses cantos.

Essa trigonometria é mostrada na página Bitmap Rotator . O arquivo XAML instancia um SKCanvasView e um Slider que pode variar de 0 a 360 graus com um Label mostrando o valor atual:

<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"
             x:Class="SkiaSharpFormsDemos.Bitmaps.BitmapRotatorPage"
             Title="Bitmap Rotator">
    <StackLayout>
        <skia:SKCanvasView x:Name="canvasView"
                           VerticalOptions="FillAndExpand"
                           PaintSurface="OnCanvasViewPaintSurface" />

        <Slider x:Name="slider"
                Maximum="360"
                Margin="10, 0"
                ValueChanged="OnSliderValueChanged" />

        <Label Text="{Binding Source={x:Reference slider},
                              Path=Value,
                              StringFormat='Rotate by {0:F0}&#x00B0;'}"
               HorizontalTextAlignment="Center" />

    </StackLayout>
</ContentPage>

O arquivo code-behind carrega um recurso de bitmap e o salva como um campo estático somente leitura chamado originalBitmap. O bitmap exibido no PaintSurface manipulador é rotatedBitmap, que é inicialmente definido como originalBitmap:

public partial class BitmapRotatorPage : ContentPage
{
    static readonly SKBitmap originalBitmap =
        BitmapExtensions.LoadBitmapResource(typeof(BitmapRotatorPage),
            "SkiaSharpFormsDemos.Media.Banana.jpg");

    SKBitmap rotatedBitmap = originalBitmap;

    public BitmapRotatorPage ()
    {
        InitializeComponent ();
    }

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

        canvas.Clear();
        canvas.DrawBitmap(rotatedBitmap, info.Rect, BitmapStretch.Uniform);
    }

    void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
    {
        double angle = args.NewValue;
        double radians = Math.PI * angle / 180;
        float sine = (float)Math.Abs(Math.Sin(radians));
        float cosine = (float)Math.Abs(Math.Cos(radians));
        int originalWidth = originalBitmap.Width;
        int originalHeight = originalBitmap.Height;
        int rotatedWidth = (int)(cosine * originalWidth + sine * originalHeight);
        int rotatedHeight = (int)(cosine * originalHeight + sine * originalWidth);

        rotatedBitmap = new SKBitmap(rotatedWidth, rotatedHeight);

        using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
        {
            canvas.Clear(SKColors.LightPink);
            canvas.Translate(rotatedWidth / 2, rotatedHeight / 2);
            canvas.RotateDegrees((float)angle);
            canvas.Translate(-originalWidth / 2, -originalHeight / 2);
            canvas.DrawBitmap(originalBitmap, new SKPoint());
        }

        canvasView.InvalidateSurface();
    }
}

O ValueChanged manipulador do executa as operações que criam um novo rotatedBitmap com base no ângulo de Slider rotação. A nova largura e altura são baseadas em valores absolutos de senos e cossenos das larguras e alturas originais. As transformações usadas para desenhar o bitmap original no bitmap girado movem o centro do bitmap original para a origem, depois o giram pelo número especificado de graus e, em seguida, convertem esse centro para o centro do bitmap rotacionado. (Os Translate métodos e RotateDegrees são chamados na ordem oposta à forma como são aplicados.)

Observe o Clear uso do método para fazer o fundo de rotatedBitmap um rosa claro. Isso é apenas para ilustrar o tamanho da rotatedBitmap tela:

Rotador de bitmap

O bitmap girado é grande o suficiente para incluir todo o bitmap original, mas não maior.

Invertendo bitmaps

Outra operação comumente executada em bitmaps é chamada de inversão. Conceitualmente, o bitmap é girado em três dimensões em torno de um eixo vertical ou eixo horizontal através do centro do bitmap. A inversão vertical cria uma imagem espelhada.

A página Bitmap Flipper no aplicativo de exemplo demonstra esses processos. O arquivo XAML contém um SKCanvasView e dois botões para inverter vertical e horizontalmente:

<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"
             x:Class="SkiaSharpFormsDemos.Bitmaps.BitmapFlipperPage"
             Title="Bitmap Flipper">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

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

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

        <Button Text="Flip Vertical"
                Grid.Row="1" Grid.Column="0"
                Margin="0, 10"
                Clicked="OnFlipVerticalClicked" />

        <Button Text="Flip Horizontal"
                Grid.Row="1" Grid.Column="1"
                Margin="0, 10"
                Clicked="OnFlipHorizontalClicked" />
    </Grid>
</ContentPage>

O arquivo code-behind implementa essas duas operações nos Clicked manipuladores dos botões:

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

    public BitmapFlipperPage()
    {
        InitializeComponent();
    }

    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, BitmapStretch.Uniform);
    }

    void OnFlipVerticalClicked(object sender, ValueChangedEventArgs args)
    {
        SKBitmap flippedBitmap = new SKBitmap(bitmap.Width, bitmap.Height);

        using (SKCanvas canvas = new SKCanvas(flippedBitmap))
        {
            canvas.Clear();
            canvas.Scale(-1, 1, bitmap.Width / 2, 0);
            canvas.DrawBitmap(bitmap, new SKPoint());
        }

        bitmap = flippedBitmap;
        canvasView.InvalidateSurface();
    }

    void OnFlipHorizontalClicked(object sender, ValueChangedEventArgs args)
    {
        SKBitmap flippedBitmap = new SKBitmap(bitmap.Width, bitmap.Height);

        using (SKCanvas canvas = new SKCanvas(flippedBitmap))
        {
            canvas.Clear();
            canvas.Scale(1, -1, 0, bitmap.Height / 2);
            canvas.DrawBitmap(bitmap, new SKPoint());
        }

        bitmap = flippedBitmap;
        canvasView.InvalidateSurface();
    }
}

A inversão vertical é realizada por uma transformação de escala com um fator de escala horizontal de –1. O centro de dimensionamento é o centro vertical do bitmap. A inversão horizontal é uma transformada de escala com um fator de escala vertical de –1.

Como você pode ver pelas letras invertidas na camisa do macaco, virar não é o mesmo que girar. Mas, como a captura de tela UWP à direita demonstra, inverter horizontal e verticalmente é o mesmo que girar 180 graus:

Bitmap Flipper

Outra tarefa comum que pode ser manipulada usando técnicas semelhantes é cortar um bitmap para um subconjunto retangular. Isso é descrito no próximo artigo Recortando bitmaps SkiaSharp.