Creación y dibujo en mapas de bits de SkiaSharp

Download SampleDescargar el ejemplo

Ha visto cómo una aplicación puede cargar mapas de bits desde la Web, desde los recursos de la aplicación y desde la biblioteca de fotos del usuario. También se pueden crear nuevos mapas de bits dentro de la aplicación. El enfoque más sencillo implica uno de los constructores de SKBitmap:

SKBitmap bitmap = new SKBitmap(width, height);

Los parámetros width y height son enteros y especifican las dimensiones de píxel del mapa de bits. Este constructor crea un mapa de bits de color completo de cuatro bytes por píxel: un byte cada uno para los componentes rojo, verde, azul y alfa (opacidad).

Después de crear un mapa de bits nuevo, necesita obtener algo en la superficie del mapa de bits. Por lo general, se hace de una de estas dos maneras:

  • Dibujar en el mapa de bits mediante métodos de dibujo estándar Canvas.
  • Acceda directamente a los bits de píxel.

En este artículo se muestra el primer enfoque:

Drawing Sample

El segundo enfoque se describe en el artículo Acceso a los píxeles de mapa de bits SkiaSharp.

Dibujar en el mapa de bits

Dibujar en la superficie de un mapa de bits es igual que dibujar en una pantalla de vídeo. Para dibujar en una pantalla de vídeo, se obtiene un objeto SKCanvas de los argumentos del evento PaintSurface. Para dibujar en un mapa de bits, cree un objeto SKCanvas mediante el constructor SKCanvas:

SKCanvas canvas = new SKCanvas(bitmap);

Cuando haya terminado de dibujar en el mapa de bits, puede eliminar el objeto SKCanvas. Por este motivo, el constructor SKCanvas se suele llamar en una instrucción using:

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

Se puede mostrar el mapa de bits. Más adelante, el programa puede crear un nuevo objeto SKCanvas basado en ese mismo mapa de bits y dibujarlo más.

La página Hello Bitmap de la aplicación SkiaSharpFormsDemos escribe el texto «Hello, Bitmap!» en un mapa de bits y, a continuación, muestra ese mapa de bits varias veces.

El constructor de HelloBitmapPage comienza creando un objeto SKPaint para mostrar texto. Determina las dimensiones de una cadena de texto y crea un mapa de bits con esas dimensiones. A continuación, crea un objeto SKCanvas basado en ese mapa de bits, llama a Clear y después llama a DrawText. Siempre es una buena idea llamar a Clear con un mapa de bits nuevo porque un mapa de bits recién creado podría contener datos aleatorios.

El constructor concluye creando un objeto SKCanvasView para mostrar el mapa de bits:

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

El controlador PaintSurface representa el mapa de bits varias veces en filas y columnas de la pantalla. Observe que el método Clear del controlador PaintSurface tiene un argumento de SKColors.Aqua, que colorea el fondo de la superficie de presentación:

Hello, Bitmap!

La apariencia del fondo turquesa revela que el mapa de bits es transparente, excepto para el texto.

Borrar y transparencia

La presentación de la página Hello Bitmap muestra que el mapa de bits creado por el programa es transparente, excepto el texto negro. Por eso se muestra el color turquesa de la superficie de visualización.

La documentación de los métodos Clear de SKCanvas los describe con la instrucción : «Reemplaza todos los píxeles del clip actual del lienzo». El uso de la palabra «replaces» revela una característica importante de estos métodos: todos los métodos de dibujo de SKCanvas agregan algo a la superficie de visualización existente. Los métodos Clearreemplazan lo que ya está ahí.

Clear existe en dos versiones diferentes:

  • El método Clear con un parámetro SKColor reemplaza los píxeles de la superficie de visualización por píxeles de ese color.

  • El método sin parámetros Clear reemplaza los píxeles por el color SKColors.Empty, que es un color en el que todos los componentes (rojo, verde, azul y alfa) se establecen en cero. Este color se conoce a veces como «negro transparente».

Al llamar Clear sin argumentos en un mapa de bits nuevo, se inicializa todo el mapa de bits para que sea totalmente transparente. Cualquier cosa que se dibuje posteriormente en el mapa de bits será normalmente opaca o parcialmente opaca.

Esto es algo que se debe probar: en la página Hello Bitmap, reemplace el método Clear aplicado a bitmapCanvas por este:

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

El orden de los parámetros SKColor del constructor es rojo, verde, azul y alfa, donde cada valor puede oscilar entre 0 y 255. Tenga en cuenta que un valor alfa de 0 es transparente, mientras que un valor alfa de 255 es opaco.

El valor (255, 0, 0, 128) borra los píxeles del mapa de bits en píxeles rojos con una opacidad del 50 %. Esto significa que el fondo del mapa de bits es semitransparente. El fondo rojo semitransparente del mapa de bits se combina con el fondo acuático de la superficie de visualización para crear un fondo gris.

Intente establecer el color del texto en negro transparente colocando la siguiente asignación en el inicializador SKPaint:

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

Puede pensar que este texto transparente crearía áreas totalmente transparentes del mapa de bits a través del cual vería el fondo celeste de la superficie de visualización. Pero no es así. El texto se dibuja sobre lo que ya está en el mapa de bits. El texto transparente no será visible en absoluto.

Ningún método Draw nunca hace que un mapa de bits sea más transparente. Sólo puede hacer eso Clear.

Tipos de color de mapa de bits

El constructor más sencillo SKBitmap permite especificar un ancho y alto de píxeles enteros para el mapa de bits. Otros constructores SKBitmap son más complejos. Estos constructores requieren argumentos de dos tipos de enumeración: SKColorType y SKAlphaType. Otros constructores usan la estructura SKImageInfo, que consolida esta información.

La enumeraciónSKColorType tiene 9 miembros. Cada uno de estos miembros describe una manera determinada de almacenar los píxeles del mapa de bits:

  • Unknown
  • Alpha8: cada píxel es de 8 bits, que representa un valor alfa de totalmente transparente a totalmente opaco
  • Rgb565: cada píxel es de 16 bits, 5 bits para rojo y azul, y 6 para verde
  • Argb4444: cada píxel es de 16 bits, 4 para alfa, rojo, verde y azul
  • Rgba8888: cada píxel es de 32 bits, 8 para rojo, verde, azul y alfa
  • Bgra8888: cada píxel es de 32 bits, 8 para azul, verde, rojo y alfa
  • Index8: cada píxel es de 8 bits y representa un índice en un SKColorTable
  • Gray8: cada píxel es de 8 bits que representan un tono gris de negro a blanco
  • RgbaF16: cada píxel es de 64 bits, con rojo, verde, azul y alfa en un formato de punto flotante de 16 bits

Los dos formatos en los que cada píxel es de 32 píxeles (4 bytes) a menudo se denominan formatos de color completo. Muchos de los otros formatos datan de una época en la que las propias pantallas de vídeo no eran capaces de mostrar todo el color. Los mapas de bits de color limitado eran adecuados para estas pantallas y permitían que los mapas de bits ocuparan menos espacio en la memoria.

Hoy en día los programadores casi siempre usan mapas de bits de color completo y no prueban con otros formatos. La excepción es el formato RgbaF16, que permite una resolución de color mayor que incluso los formatos de color completo. Sin embargo, este formato se usa para fines especializados, como la creación de imágenes médicas, y no tiene mucho sentido cuando se usa con pantallas de color completo estándar.

Esta serie de artículos se restringirá a los formatos de color SKBitmap que se usan de forma predeterminada cuando no se especifica ningún miembro SKColorType. Este formato predeterminado se basa en la plataforma subyacente. Para las plataformas compatibles con Xamarin.Forms, el tipo de color predeterminado es:

  • Rgba8888 de iOS y Android
  • Bgra8888 para la UWP

La única diferencia es el orden de los 4 bytes en memoria y esto solo se convierte en un problema cuando se accede directamente a los bits de píxel. Esto no será importante hasta que llegue al artículo Acceso a los píxeles de mapa de bits de SkiaSharp.

La enumeración SKAlphaType tiene cuatro miembros:

  • Unknown
  • Opaque: el mapa de bits no tiene transparencia
  • Premul: los componentes de color se multiplican previamente por el componente alfa
  • Unpremul: los componentes de color no se multiplican previamente por el componente alfa

Este es un píxel de mapa de bits rojo de 4 bytes con transparencia del 50 %, con los bytes mostrados en el orden rojo, verde, azul, alfa:

0xFF 0x00 0x00 0x80

Cuando un mapa de bits que contiene píxeles semitransparentes se representa en una superficie de visualización, los componentes de color de cada píxel del mapa de bits deben multiplicarse por el valor alfa de ese píxel y los componentes de color del píxel correspondiente de la superficie de visualización deben multiplicarse por 255 menos el valor alfa. A continuación, se pueden combinar los dos píxeles. El mapa de bits se puede representar más rápido si los componentes de color de los píxeles del mapa de bits ya han sido mulitplicados previamente por el valor alfa. Ese mismo píxel rojo se almacenaría como este en un formato multiplicado previamente:

0x80 0x00 0x00 0x80

Esta mejora del rendimiento es la razón por la que se crean mapas de bits SkiaSharp de forma predeterminada con un formato Premul. Pero es necesario saber esto solo cuando se accede a los bits de píxel y se manipulan.

Dibujar en mapas de bits existentes

No es necesario crear un mapa de bits nuevo para dibujar en él. También puede dibujar en un mapa de bits existente.

La página Monkey Moustache usa su constructor para cargar la imagen MonkeyFace.png. A continuación, crea un objeto SKCanvas basado en ese mapa de bits y usa objetos SKPaint y SKPath para dibujar un bigote en él:

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

El constructor concluye creando un controlador SKCanvasView cuyo controlador PaintSurface simplemente muestra el resultado:

Monkey Moustache

Copiar y modificar mapas de bits

Los métodos de SKCanvas que puede usar para dibujar en un mapa de bits incluyen DrawBitmap. Esto significa que puede dibujar un mapa de bits en otro, normalmente modificándolo de alguna manera.

La manera más versátil de modificar un mapa de bits es acceder a los bits de píxeles reales, un tema tratado en el artículo Acceso a los píxeles de mapa de bits SkiaSharp. Pero hay muchas otras técnicas para modificar mapas de bits que no requieren acceso a los bits de píxel.

El siguiente mapa de bits incluido con la aplicación SkiaSharpFormsDemos es de 360 píxeles de ancho y 480 píxeles de altura:

Mountain Climbers

Supongamos que no ha recibido permiso del mono de la izquierda para publicar esta fotografía. Una solución es ocultar la cara del mono mediante una técnica denominada pixelización. Los píxeles de la cara se reemplazan por bloques de color, por lo que no se pueden ver las características. Normalmente, los bloques de color derivan de la imagen original promediando los colores de los píxeles correspondientes a estos bloques. Pero no es necesario que usted realice esta media. Se produce automáticamente cuando se copia un mapa de bits en una dimensión de píxel más pequeña.

La cara del mono izquierdo ocupa aproximadamente un área cuadrada de 72 píxeles con una esquina superior izquierda en el punto (112, 238). Vamos a reemplazar ese área cuadrada de 72 píxeles por una matriz de 9x9 de bloques coloreados, cada una de las cuales tiene un cuadrado de 8x8 píxeles.

La página Pixelize Image se carga en ese mapa de bits y primero crea un mapa de bits cuadrado de 9 píxeles pequeño denominado faceBitmap. Este es un destino para copiar solo la cara del mono. El rectángulo de destino es solo de 9 píxeles cuadrados, pero el rectángulo de origen es cuadrado de 72 píxeles. Cada bloque de 8x8 píxeles de origen se consolida hasta un solo píxel al promediar los colores.

El siguiente paso consiste en copiar el mapa de bits original en un nuevo mapa de bits del mismo tamaño denominado pixelizedBitmap. A continuación, el diminuto faceBitmap se copia encima de eso con un rectángulo de destino cuadrado de 72 píxeles para que cada píxel de faceBitmap se expanda a 8 veces su tamaño:

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

El constructor concluye creando un objeto SKCanvasView para mostrar el resultado:

Pixelize Image

Rotación de mapas de bits

Otra tarea común es rotar mapas de bits. Esto es especialmente útil al recuperar mapas de bits de una fototeca de iPhone o iPad. A menos que el dispositivo se mantenga en una orientación determinada cuando se tomó la foto, es probable que la imagen esté al revés o de lado.

Al activar un mapa de bits, es necesario crear otro mapa de bits con el mismo tamaño que el primero y luego establecer una transformación para girar en 180 grados mientras se copia el primero en el segundo. En todos los ejemplos de esta sección, bitmap es el objeto SKBitmap que necesita 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());
}

Al girar en 90 grados, debe crear un mapa de bits distinto del original intercambiando el alto y el ancho. Por ejemplo, si el mapa de bits original tiene un ancho de 1200 píxeles y 800 píxeles de alto, el mapa de bits girado tiene un ancho de 800 píxeles y 1200 píxeles de ancho. Establezca la traducción y la rotación para que el mapa de bits se gire alrededor de su esquina superior izquierda y luego, cambie a la vista. (Tenga en cuenta que se llama a los métodos Translate y RotateDegrees en el orden opuesto de la forma en que se aplican). Este es el código para girar 90 grados en el sentido de las agujas del reloj:

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

Y esta es una función similar para girar 90 grados en sentido contrario a las agujas del reloj:

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

Estos dos métodos se usan en las páginas del Rompecabezas de fotos que se describen en el artículo Recortar mapas de bits SkiaSharp.

Un programa que permite al usuario girar un mapa de bits en incrementos de 90 grados solo necesita implementar una función para girar a 90 grados. El usuario puede entonces girar en cualquier incremento de 90 grados mediante la ejecución repetida de esta función.

Un programa también puede girar un mapa de bits de cualquier cantidad. Un enfoque sencillo consiste en modificar la función que gira en 180 grados reemplazando 180 por una variable 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());
}

Sin embargo, en el caso normal, esta lógica recortará las esquinas del mapa de bits girado. Un mejor enfoque es calcular el tamaño del mapa de bits girado mediante trigonometría para incluir esas esquinas.

Esta trigonometría se muestra en la página Rotator de mapa de bits. El archivo XAML crea una instancia de SKCanvasView y Slider que pueden oscilar entre 0 y 360 grados con un Label que muestra el valor actual:

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

El archivo de código subyacente carga un recurso de mapa de bits y lo guarda como un campo estático de solo lectura denominado originalBitmap. El mapa de bits que se muestra en el controlador PaintSurface es rotatedBitmap, que se establece inicialmente en 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();
    }
}

El controlador ValueChanged de Slider realiza las operaciones que crean un nuevo rotatedBitmap en función del ángulo de rotación. El nuevo ancho y alto se basan en valores absolutos de senos y cosenos de los anchos y alturas originales. Las transformaciones usadas para dibujar el mapa de bits original en el mapa de bits girado mueven el centro de mapa de bits original al origen, después lo giran por el número especificado de grados y después traducen ese centro al centro del mapa de bits girado. (Se llama a los métodos Translate y RotateDegrees en el orden opuesto al modo en que se aplican).

Observe el uso del método Clear para convertir el fondo de rotatedBitmap un rosa claro. Esto es únicamente para ilustrar el tamaño de rotatedBitmap en la pantalla:

Bitmap Rotator

El mapa de bits girado es lo suficientemente grande como para incluir todo el mapa de bits original, pero no más grande.

Voltear mapas de bits

Otra operación que se realiza normalmente en mapas de bits se denomina voltear. Conceptualmente, el mapa de bits se gira en tres dimensiones alrededor de un eje vertical o un eje horizontal a través del centro del mapa de bits. El volteo vertical crea una imagen reflejada.

La página Volteador de mapa de bits de la aplicación SkiaSharpFormsDemos muestra estos procesos. El archivo XAML contiene un SKCanvasView y dos botones para voltear vertical y 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>

El archivo de código subyacente implementa estas dos operaciones en los controladoresClicked para los botones:

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

El volteo vertical se realiza mediante una transformación de escalado con un factor de escalado horizontal de –1. El centro de escalado es el centro vertical del mapa de bits. El volteo horizontal es una transformación de escalado con un factor de escalado vertical de –1.

Como puede ver en las letras invertidas en la camisa del mono, voltear no es lo mismo que rotar. Pero como muestra la captura de pantalla de la UWP a la derecha, voltear horizontal y verticalmente es lo mismo que girar 180 grados:

Bitmap Flipper

Otra tarea común que se puede controlar mediante técnicas similares es recortar un mapa de bits a un subconjunto rectangular. Esto se describe en el siguiente artículo Recortar mapas de bits SkiaSharp.