Criar, editar e salvar imagens bitmap

Este artigo explica como carregar e guardar ficheiros de imagem usando BitmapDecoder e BitmapEncoder e como usar o objeto SoftwareBitmap para representar imagens bitmap.

A classe SoftwareBitmap é uma API versátil que pode ser criada a partir de múltiplas fontes, incluindo ficheiros de imagem, objetos WriteableBitmap , superfícies Direct3D e código. O SoftwareBitmap permite converter facilmente entre diferentes formatos de píxeis e modos alfa, e permite o acesso de baixo nível aos dados dos píxeis. Além disso, SoftwareBitmap é uma interface comum usada por múltiplas funcionalidades do Windows, incluindo:

  • CapturedFrame permite obter fotogramas captados pela câmara como SoftwareBitmap.

  • VideoFrame permite obter uma representação SoftwareBitmap de um VideoFrame.

  • FaceDetector permite detetar rostos num SoftwareBitmap.

O código de exemplo neste artigo utiliza APIs dos seguintes espaços de nomes.

using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.Storage.Streams;
using Windows.Graphics.Imaging;
using Microsoft.UI.Xaml.Media.Imaging;
using System.Runtime.InteropServices.WindowsRuntime;

Crie um SoftwareBitmap a partir de um ficheiro de imagem com BitmapDecoder

Para criar um SoftwareBitmap a partir de um ficheiro, obtenha uma instância de StorageFile contendo os dados da imagem. Este exemplo utiliza um FileOpenPicker para permitir ao utilizador selecionar um ficheiro de imagem.

private async Task<StorageFile?> PickInputFileAsync()
{
    var picker = new FileOpenPicker();

    // Initialize the picker with the window handle (required for WinUI 3).
    var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
    WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);

    picker.ViewMode = PickerViewMode.Thumbnail;
    picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
    picker.FileTypeFilter.Add(".jpg");
    picker.FileTypeFilter.Add(".jpeg");
    picker.FileTypeFilter.Add(".png");
    picker.FileTypeFilter.Add(".bmp");

    return await picker.PickSingleFileAsync();
}

Chame o método OpenAsync do objeto StorageFile para obter um fluxo de acesso aleatório contendo os dados da imagem. Chame o método estático BitmapDecoder.CreateAsync para obter uma instância da classe BitmapDecoder para o fluxo especificado. Chame GetSoftwareBitmapAsync para obter um objeto SoftwareBitmap que contém a imagem.

private async Task<SoftwareBitmap> CreateSoftwareBitmapFromFileAsync(StorageFile file)
{
    using IRandomAccessStream stream = await file.OpenAsync(FileAccessMode.Read);

    // Create a decoder from the image file.
    BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream);

    // Get the SoftwareBitmap representation of the file.
    SoftwareBitmap softwareBitmap = await decoder.GetSoftwareBitmapAsync();

    return softwareBitmap;
}

Guardar um SoftwareBitmap num ficheiro com o BitmapEncoder

Para guardar um SoftwareBitmap num ficheiro, obtenha uma instância de StorageFile onde a imagem será guardada. Este exemplo utiliza um FileSavePicker para permitir ao utilizador selecionar um ficheiro de saída.

private async Task<StorageFile?> PickOutputFileAsync()
{
    var picker = new FileSavePicker();

    // Initialize the picker with the window handle (required for WinUI 3).
    var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
    WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);

    picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
    picker.SuggestedFileName = "output";
    picker.FileTypeChoices.Add("JPEG Image", new List<string> { ".jpg" });
    picker.FileTypeChoices.Add("PNG Image", new List<string> { ".png" });

    return await picker.PickSaveFileAsync();
}

Chame o método OpenAsync do objeto StorageFile para obter um fluxo de acesso aleatório onde a imagem será escrita. Chame o método estático BitmapEncoder.CreateAsync para obter uma instância da classe BitmapEncoder para o fluxo especificado. O primeiro parâmetro do CreateAsync é um GUID que representa o codec que deve ser usado para codificar a imagem. A classe BitmapEncoder expõe uma propriedade contendo o ID de cada codec suportado pelo codificador, como JpegEncoderId.

Use o método SetSoftwareBitmap para definir a imagem que será codificada. Podes definir valores da propriedade BitmapTransform para aplicar transformações básicas à imagem enquanto esta está a ser codificada. A propriedade IsThumbnailGenerated determina se uma miniatura é gerada pelo codificador. Note que nem todos os formatos de ficheiro suportam miniaturas, por isso, se usar esta funcionalidade, deverá detetar o erro de operação não suportado que será lançado se as miniaturas não forem suportadas.

Chame o FlushAsync para fazer com que o codificador escreva os dados da imagem no ficheiro especificado.

private async Task SaveSoftwareBitmapToFileAsync(SoftwareBitmap softwareBitmap, StorageFile outputFile)
{
    using IRandomAccessStream stream = await outputFile.OpenAsync(FileAccessMode.ReadWrite);

    // Create an encoder with the desired format.
    BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream);

    // Set the software bitmap.
    encoder.SetSoftwareBitmap(softwareBitmap);

    // Set additional encoding parameters (optional).
    encoder.BitmapTransform.ScaledWidth = (uint)softwareBitmap.PixelWidth;
    encoder.BitmapTransform.ScaledHeight = (uint)softwareBitmap.PixelHeight;
    encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;
    encoder.IsThumbnailGenerated = true;

    try
    {
        await encoder.FlushAsync();
    }
    catch (Exception ex)
    {
        const int WINCODEC_ERR_UNSUPPORTEDOPERATION = unchecked((int)0x88982F81);
        switch (ex.HResult)
        {
            case WINCODEC_ERR_UNSUPPORTEDOPERATION:
                // If the encoder does not support thumbnail generation,
                // disable it and try again.
                encoder.IsThumbnailGenerated = false;
                break;

            default:
                throw;
        }
    }

    if (!encoder.IsThumbnailGenerated)
    {
        await encoder.FlushAsync();
    }
}

Pode especificar opções adicionais de codificação ao criar o BitmapEncoder criando um novo objeto BitmapPropertySet e preenchendo-o com um ou mais objetos BitmapTypedValue que representem as definições do codificador. Para uma lista de opções de codificador suportadas, consulte a referência de opções BitmapEncoder.

private async Task SaveWithEncodingOptionsAsync(SoftwareBitmap softwareBitmap, StorageFile outputFile)
{
    using IRandomAccessStream stream = await outputFile.OpenAsync(FileAccessMode.ReadWrite);

    // Create encoding options with a specific image quality.
    var propertySet = new BitmapPropertySet();
    var qualityValue = new BitmapTypedValue(0.9, Windows.Foundation.PropertyType.Single);
    propertySet.Add("ImageQuality", qualityValue);

    // Create the encoder with the encoding options.
    BitmapEncoder encoder = await BitmapEncoder.CreateAsync(
        BitmapEncoder.JpegEncoderId, stream, propertySet);

    encoder.SetSoftwareBitmap(softwareBitmap);
    await encoder.FlushAsync();
}

Utilize o SoftwareBitmap com um controlo Image em XAML

Para mostrar uma imagem dentro de uma página XAML usando o controlo Imagem , defina primeiro um controlo Image na sua página XAML.

<Image x:Name="imageControl"
       Grid.Row="2"
       Stretch="Uniform"/>

Atualmente, o controlo Image só suporta imagens com codificação BGRA8 e canal alfa pré-multiplicado ou sem canal alfa. Antes de tentar mostrar uma imagem, teste se tem o formato correto e, caso contrário, utilize o método SoftwareBitmapstatic convert para converter a imagem para o formato suportado.

Crie um novo objeto SoftwareBitmapSource . Defina o conteúdo do objeto de origem chamando SetBitmapAsync, passando um SoftwareBitmap. Depois podes definir a propriedade Source do controlo Image para o recém-criado SoftwareBitmapSource.

private async Task DisplaySoftwareBitmapAsync(SoftwareBitmap softwareBitmap)
{
    // SoftwareBitmap must be Bgra8 with premultiplied or no alpha
    // to display in a XAML Image control.
    if (softwareBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 ||
        softwareBitmap.BitmapAlphaMode == BitmapAlphaMode.Straight)
    {
        softwareBitmap = SoftwareBitmap.Convert(
            softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
    }

    // In WinUI 3, SoftwareBitmapSource is in Microsoft.UI.Xaml.Media.Imaging.
    var source = new SoftwareBitmapSource();
    await source.SetBitmapAsync(softwareBitmap);

    // Set the source of the Image control.
    imageControl.Source = source;
}

Também pode usar o SoftwareBitmapSource para definir um SoftwareBitmap como ImageSource para um ImageBrush.

Crie um SoftwareBitmap a partir de um WriteableBitmap

Pode criar um SoftwareBitmap a partir de um WriteableBitmap existente chamando o SoftwareBitmap.CreateCopyFromBuffer e fornecendo a propriedade PixelBuffer do WriteableBitmap para definir os dados do pixel. O segundo argumento permite-te especificar o formato de píxel para o recém-criado SoftwareBitmap. Pode usar as propriedades PixelWidth e PixelHeight do WriteableBitmap para especificar as dimensões da nova imagem.

private SoftwareBitmap ConvertWriteableBitmapToSoftwareBitmap(WriteableBitmap writeableBitmap)
{
    // Create a SoftwareBitmap from the WriteableBitmap's pixel buffer.
    SoftwareBitmap softwareBitmap = SoftwareBitmap.CreateCopyFromBuffer(
        writeableBitmap.PixelBuffer,
        BitmapPixelFormat.Bgra8,
        writeableBitmap.PixelWidth,
        writeableBitmap.PixelHeight);

    return softwareBitmap;
}

Criar ou editar um SoftwareBitmap programaticamente

Até agora, este tema abordou o trabalho com ficheiros de imagem. Também pode criar um novo SoftwareBitmap programaticamente em código e usar a mesma técnica para aceder e modificar os dados de píxeis do SoftwareBitmap.

Use o método CopyFromBuffer para preencher um SoftwareBitmap a partir de um array de bytes, e o CopyToBuffer para copiar dados de píxeis para um array de bytes para leitura ou modificação. Para utilizar o método de extensão AsBuffer para encapsular uma matriz de bytes como um IBuffer, inclua o espaço de nomes System.Runtime.InteropServices.WindowsRuntime (este já está incluído nas diretivas SnippetNamespaces no início deste artigo).

Cria um novo SoftwareBitmap com o formato de píxel e tamanho que queres. Aloque um array de bytes grande o suficiente para armazenar os dados do pixel, preencha-o com os valores desejados e depois chame o CopyFromBuffer para escrever os dados no bitmap.

private SoftwareBitmap CreateGradientBitmap(int width, int height)
{
    // Create a new SoftwareBitmap programmatically.
    var softwareBitmap = new SoftwareBitmap(
        BitmapPixelFormat.Bgra8, width, height, BitmapAlphaMode.Premultiplied);

    // Allocate a byte array for the pixel data.
    int bytesPerPixel = 4; // BGRA8
    byte[] pixelData = new byte[width * height * bytesPerPixel];

    // Fill the bitmap with a gradient pattern.
    for (int row = 0; row < height; row++)
    {
        for (int col = 0; col < width; col++)
        {
            int pixelIndex = (row * width + col) * bytesPerPixel;

            // Blue channel: gradient left to right.
            pixelData[pixelIndex + 0] = (byte)((double)col / width * 255);
            // Green channel: gradient top to bottom.
            pixelData[pixelIndex + 1] = (byte)((double)row / height * 255);
            // Red channel: inverse diagonal gradient.
            pixelData[pixelIndex + 2] = (byte)(255 - (((double)(col + row)
                / (width + height)) * 255));
            // Alpha channel: fully opaque.
            pixelData[pixelIndex + 3] = 255;
        }
    }

    // Copy the pixel data into the SoftwareBitmap.
    softwareBitmap.CopyFromBuffer(pixelData.AsBuffer());

    return softwareBitmap;
}

Criar um SoftwareBitmap a partir de uma superfície Direct3D

Para criar um objeto SoftwareBitmap a partir de uma superfície Direct3D, deve incluir o espaço de nomes Windows.Graphics.DirectX.Direct3D11 no seu projeto.

using Windows.Graphics.DirectX.Direct3D11;

Chame CreateCopyFromSurfaceAsync para criar um novo SoftwareBitmap a partir da superfície. Como o nome indica, o novo SoftwareBitmap tem uma cópia separada dos dados da imagem. As modificações ao SoftwareBitmap não terão qualquer efeito na superfície do Direct3D.

private async Task<SoftwareBitmap> CreateBitmapFromSurfaceAsync(IDirect3DSurface surface)
{
    // Create a SoftwareBitmap from a Direct3D surface.
    SoftwareBitmap softwareBitmap =
        await SoftwareBitmap.CreateCopyFromSurfaceAsync(surface);

    return softwareBitmap;
}

Converter um SoftwareBitmap para um formato de píxel diferente

A classe SoftwareBitmap fornece o método estático, Convert, que permite criar facilmente um novo SoftwareBitmap que utiliza o formato de píxeis e o modo alfa que especifica a partir de um SoftwareBitmap existente. Note que o bitmap recém-criado tem uma cópia separada dos dados da imagem. As modificações no novo bitmap não afetarão o bitmap de origem.

private SoftwareBitmap ConvertBitmapPixelFormat(SoftwareBitmap softwareBitmap)
{
    // Convert the pixel format and alpha mode of the SoftwareBitmap.
    SoftwareBitmap convertedBitmap = SoftwareBitmap.Convert(
        softwareBitmap,
        BitmapPixelFormat.Bgra8,
        BitmapAlphaMode.Premultiplied);

    return convertedBitmap;
}

Transcodificar um ficheiro de imagem

Pode transcodificar um ficheiro de imagem diretamente de um BitmapDecoder para um BitmapEncoder. Crie um IRandomAccessStream a partir do ficheiro a transcodificar. Crie um novo BitmapDecoder a partir do fluxo de entrada. Crie um novo InMemoryRandomAccessStream para o codificador escrever nele e chame BitmapEncoder.CreateForTranscodingAsync, passando o fluxo na memória e o objeto descodificador. As opções de codificação não são suportadas durante a transcodificação; em vez disso, deves usar o CreateAsync. Quaisquer propriedades no ficheiro de imagem de entrada que não definas especificamente no codificador serão escritas no ficheiro de saída sem alterações. Chame FlushAsync para que o codificador codifique para o fluxo na memória. Finalmente, procure o fluxo de ficheiros e o fluxo em memória no início e chame o CopyAsync para escrever o fluxo em memória para o fluxo de ficheiros.

private async Task TranscodeImageFileAsync(StorageFile inputFile, StorageFile outputFile)
{
    using IRandomAccessStream inputStream =
        await inputFile.OpenAsync(FileAccessMode.Read);
    using IRandomAccessStream outputStream =
        await outputFile.OpenAsync(FileAccessMode.ReadWrite);

    // Create a decoder for the input file.
    BitmapDecoder decoder = await BitmapDecoder.CreateAsync(inputStream);

    // Create an encoder for transcoding to the output file.
    BitmapEncoder encoder =
        await BitmapEncoder.CreateForTranscodingAsync(outputStream, decoder);

    // Optionally apply transforms during transcoding.
    encoder.BitmapTransform.ScaledWidth = decoder.PixelWidth / 2;
    encoder.BitmapTransform.ScaledHeight = decoder.PixelHeight / 2;
    encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;

    await encoder.FlushAsync();
}