Criar, editar e salvar imagens de bitmap

Este artigo explica como carregar e salvar arquivos de imagem usando BitmapDecoder e BitmapEncode e como usar o objeto SoftwareBitmap para representar imagens bitmap.

A classe SoftwareBitmap é uma API versátil que pode ser criada a partir de várias fontes, incluindo arquivos de imagem, objetos WriteableBitmap , superfícies Direct3D e código. O SoftwareBitmap permite que você converta facilmente entre diferentes formatos de pixel e modos alfa e permite acesso de baixo nível aos dados de pixel. Além disso, SoftwareBitmap é uma interface comum usada por vários recursos de Windows, incluindo:

  • CapturedFrame permite que você obtenha quadros capturados pela câmera como um SoftwareBitmap.

  • VideoFrame permite que você obtenha uma representação SoftwareBitmap de um VideoFrame.

  • FaceDetector permite detectar rostos em um SoftwareBitmap.

O código de exemplo neste artigo usa APIs dos namespaces a seguir.

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;

Criar um SoftwareBitmap de um arquivo de imagem com BitmapDecoder

Para criar um SoftwareBitmap de um arquivo, obtenha uma instância de StorageFile que contém os dados da imagem. Este exemplo usa um FileOpenPicker para permitir que o usuário selecione um arquivo 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;
}

Salvar um SoftwareBitmap em um arquivo com BitmapEncoder

Para salvar um SoftwareBitmap em um arquivo, obtenha uma instância de StorageFile na qual a imagem será salva. Este exemplo usa um FileSavePicker para permitir que o usuário selecione um arquivo 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 ao qual a imagem será gravada. Chame o método estático BitmapEncoder.CreateAsync para obter uma instância da classe BitmapEncoder para o fluxo especificado. O primeiro parâmetro para CreateAsync é um GUID que representa o codec que deve ser usado para codificar a imagem. A classe BitmapEncoder expõe uma propriedade que contém a ID de cada codec compatível com o codificador, como JpegEncoderId.

Use o método SetSoftwareBitmap para definir a imagem que será codificada. Você pode definir valores da propriedade BitmapTransform para aplicar transformações básicas à imagem enquanto ela está sendo codificada. A propriedade IsThumbnailGenerated determina se uma miniatura é gerada pelo codificador. Observe que nem todos os formatos de arquivo dão suporte a miniaturas, portanto, se você usar esse recurso, deverá capturar o erro de operação sem suporte que será gerado se não houver suporte para miniaturas.

Chame FlushAsync para fazer com que o codificador escreva os dados da imagem no arquivo 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();
    }
}

Você 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 representam as configurações do codificador. Para obter uma lista de opções de codificador com suporte, consulte a referência de opções do 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();
}

Usar SoftwareBitmap com um controle de imagem XAML

Para exibir uma imagem em uma página XAML usando o controle Imagem , primeiro defina um controle de imagem em sua página XAML.

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

Atualmente, o controle Imagem dá suporte apenas a imagens que usam codificação BGRA8 e pré-multiplicadas ou nenhum canal alfa. Antes de tentar exibir uma imagem, teste para verificar se ela tem o formato correto e, caso contrário, use o método Converter estático SoftwareBitmap para converter a imagem no formato com suporte.

Crie um novo objeto SoftwareBitmapSource . Defina o conteúdo do objeto de origem chamando SetBitmapAsync, passando um SoftwareBitmap. Em seguida, você pode definir a propriedade Source do controle Image para o SoftwareBitmapSource recém-criado.

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

Você também pode usar SoftwareBitmapSource para definir um SoftwareBitmap como ImageSource para um ImageBrush.

Criar um SoftwareBitmap a partir de um WriteableBitmap

Você pode criar um SoftwareBitmap de um WriteableBitmap existente chamando SoftwareBitmap.CreateCopyFromBuffer e fornecendo a propriedade PixelBuffer do WriteableBitmap para definir os dados de pixel. O segundo argumento permite especificar o formato de pixel para o SoftwareBitmap recém-criado. Você 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 tópico abordou o trabalho com arquivos de imagem. Você também pode criar um novo SoftwareBitmap programaticamente no código e usar a mesma técnica para acessar e modificar os dados de pixel do SoftwareBitmap.

Use o método CopyFromBuffer para preencher um SoftwareBitmap de uma matriz de bytes e CopyToBuffer para copiar dados de pixel para uma matriz de bytes para leitura ou modificação. Para usar o método de extensão AsBuffer para encapsular uma matriz de bytes como um IBuffer, inclua o namespace System.Runtime.InteropServices.WindowsRuntime (isso está incluído no SnippetNamespaces usando instruções na parte superior deste artigo).

Crie um novo SoftwareBitmap com o formato de pixel e o tamanho desejados. Aloque uma matriz de bytes grande o suficiente para manter os dados de pixel, preenchê-los com os valores desejados e, em seguida, chamar CopyFromBuffer para gravar 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 de uma superfície Direct3D, você deve incluir o Windows. Graphics.DirectX.Direct3D11 namespace em 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 no SoftwareBitmap não terão nenhum 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 em um formato de pixel diferente

A classe SoftwareBitmap fornece o método estático , Convert, que permite criar facilmente um novo SoftwareBitmap que usa o formato pixel e o modo alfa especificados de um SoftwareBitmap existente. Observe 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 arquivo de imagem

Você pode transcodificar um arquivo de imagem diretamente de um BitmapDecoder para um BitmapEncoder. Crie um IRandomAccessStream a partir do arquivo a ser transcodificado. Crie um novo BitmapDecoder a partir do fluxo de entrada. Crie um novo InMemoryRandomAccessStream para que o codificador grave nele e chame BitmapEncoder.CreateForTranscodingAsync, passando o fluxo na memória e o objeto do decodificador. Não há suporte para opções de codificação ao transcodificar; em vez disso, você deve usar CreateAsync. Todas as propriedades no arquivo de imagem de entrada que você não definir especificamente no codificador serão gravadas no arquivo de saída inalteradas. Chame FlushAsync para fazer com que o codificador codifique no fluxo em memória. Por fim, procure o fluxo de arquivos e o fluxo na memória para o início e chame CopyAsync para gravar o fluxo na memória no fluxo de arquivos.

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