Criar, editar e salvar imagens de bitmap

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

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

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

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

  • FaceDetector permite que você detecte 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 Windows.UI.Xaml.Media.Imaging;

Criar um SoftwareBitmap de um arquivo de imagem com BitmapDecoder

Para criar um SoftwareBitmap de um arquivo, obtenha uma instância de StorageFile que contenha os dados da imagem. Este exemplo usa um FileOpenPicker para permitir que o usuário selecione um arquivo de imagem.

FileOpenPicker fileOpenPicker = new FileOpenPicker();
fileOpenPicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
fileOpenPicker.FileTypeFilter.Add(".jpg");
fileOpenPicker.ViewMode = PickerViewMode.Thumbnail;

var inputFile = await fileOpenPicker.PickSingleFileAsync();

if (inputFile == null)
{
    // The user cancelled the picking operation
    return;
}

Chame o método OpenAsync do objeto StorageFile para obter um fluxo de acesso aleatório que contém 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.

SoftwareBitmap softwareBitmap;

using (IRandomAccessStream stream = await inputFile.OpenAsync(FileAccessMode.Read))
{
    // Create the decoder from the stream
    BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream);

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

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.

FileSavePicker fileSavePicker = new FileSavePicker();
fileSavePicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
fileSavePicker.FileTypeChoices.Add("JPEG files", new List<string>() { ".jpg" });
fileSavePicker.SuggestedFileName = "image";

var outputFile = await fileSavePicker.PickSaveFileAsync();

if (outputFile == null)
{
    // The user cancelled the picking operation
    return;
}

Chame o método OpenAsync do objeto StorageFile para obter um fluxo de acesso aleatório no 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 suportam miniaturas. Portanto, se você usar esse recurso, deverá capturar o erro de operação não suportada que será gerado se não houver suporte para miniaturas.

Chame FlushAsync para fazer com que o codificador grave os dados de imagem no arquivo especificado.

private async void SaveSoftwareBitmapToFile(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, if needed
        encoder.BitmapTransform.ScaledWidth = 320;
        encoder.BitmapTransform.ScaledHeight = 240;
        encoder.BitmapTransform.Rotation = Windows.Graphics.Imaging.BitmapRotation.Clockwise90Degrees;
        encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;
        encoder.IsThumbnailGenerated = true;

        try
        {
            await encoder.FlushAsync();
        }
        catch (Exception err)
        {
            const int WINCODEC_ERR_UNSUPPORTEDOPERATION = unchecked((int)0x88982F81);
            switch (err.HResult)
            {
                case WINCODEC_ERR_UNSUPPORTEDOPERATION: 
                    // If the encoder does not support writing a thumbnail, then try again
                    // but disable thumbnail generation.
                    encoder.IsThumbnailGenerated = false;
                    break;
                default:
                    throw;
            }
        }

        if (encoder.IsThumbnailGenerated == false)
        {
            await encoder.FlushAsync();
        }


    }
}

Você pode especificar opções de codificação adicionais ao criar o BitmapEncoder, criando um novo objeto BitmapPropertySet e populando-o com um ou mais objetos BitmapTypedValue que representam as configurações de codificador. Para obter uma lista de opções de codificador com suporte, consulte Referência de opções de BitmapEncoder.

var propertySet = new Windows.Graphics.Imaging.BitmapPropertySet();
var qualityValue = new Windows.Graphics.Imaging.BitmapTypedValue(
    1.0, // Maximum quality
    Windows.Foundation.PropertyType.Single
    );

propertySet.Add("ImageQuality", qualityValue);

await Windows.Graphics.Imaging.BitmapEncoder.CreateAsync(
    Windows.Graphics.Imaging.BitmapEncoder.JpegEncoderId,
    stream,
    propertySet
);

Usar SoftwareBitmap com um controle de imagem XAML

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

<Image x:Name="imageControl"/>

Atualmente, o controle Image só dá suporte a imagens que usam a codificação BGRA8 e canal alfa pré-multiplicado ou nenhum. Antes de tentar exibir uma imagem, teste para verificar se ele tem o formato correto e, se não tiver, use o método Convert estático de SoftwareBitmap para converter a imagem no formato com suporte.

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

if (softwareBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 ||
    softwareBitmap.BitmapAlphaMode == BitmapAlphaMode.Straight)
{
    softwareBitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
}

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 o ImageSource para um ImageBrush.

Criar um SoftwareBitmap 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 que você solicite um formato de pixel para o WriteableBitmap recém-criado. Você pode usar as propriedades PixelWidth e PixelHeight do WriteableBitmap para especificar as dimensões da nova imagem.

SoftwareBitmap outputBitmap = SoftwareBitmap.CreateCopyFromBuffer(
    writeableBitmap.PixelBuffer,
    BitmapPixelFormat.Bgra8,
    writeableBitmap.PixelWidth,
    writeableBitmap.PixelHeight
);

Criar ou editar um SoftwareBitmap programaticamente

Até agora este tópico mostrou como trabalhar 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.

SoftwareBitmap usa interoperabilidade COM para expor o buffer bruto que contém os dados de pixel.

Para usar interoperabilidade COM, você deve incluir uma referência ao namespace System.Runtime.InteropServices em seu projeto.

using System.Runtime.InteropServices;

Inicialize a interface COM IMemoryBufferByteAccess adicionando o seguinte código em seu namespace.

[ComImport]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
unsafe interface IMemoryBufferByteAccess
{
    void GetBuffer(out byte* buffer, out uint capacity);
}

Crie um novo SoftwareBitmap com o formato de pixel e o tamanho desejados. Ou use um SoftwareBitmap existente do qual você deseja editar os dados de pixel. Chame SoftwareBitmap.LockBuffer para obter uma instância da classe BitmapBuffer que representa o buffer de dados de pixel. Converta BitmapBuffer em IMemoryBufferByteAccess na interface COM e chame IMemoryBufferByteAccess.GetBuffer para popular uma matriz de bytes com os dados. Use o método BitmapBuffer.GetPlaneDescription para obter um objeto BitmapPlaneDescription que ajudará você a calcular o deslocamento para o buffer de cada pixel.

softwareBitmap = new SoftwareBitmap(BitmapPixelFormat.Bgra8, 100, 100, BitmapAlphaMode.Premultiplied);

using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Write))
{
    using (var reference = buffer.CreateReference())
    {
        byte* dataInBytes;
        uint capacity;
        ((IMemoryBufferByteAccess)reference).GetBuffer(out dataInBytes, out capacity);

        // Fill-in the BGRA plane
        BitmapPlaneDescription bufferLayout = buffer.GetPlaneDescription(0);
        for (int i = 0; i < bufferLayout.Height; i++)
        {
            for (int j = 0; j < bufferLayout.Width; j++)
            {

                byte value = (byte)((float)j / bufferLayout.Width * 255);
                dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 0] = value;
                dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 1] = value;
                dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 2] = value;
                dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 3] = (byte)255;
            }
        }
    }
}

Como esse método acessa o buffer bruto subjacente aos tipos do Windows Runtime, ele deve ser declarado usando a palavra-chave unsafe. Você também deve configurar seu projeto no Microsoft Visual Studio para permitir a compilação de código não seguro. Para fazer isso, abra a página Propriedades do projeto, clique na página de propriedade Build e selecione a caixa de seleção Permitir Código Não Seguro.

Criar um SoftwareBitmap de uma superfície de Direct3D

Para criar um objeto SoftwareBitmap de uma superfície de Direct3D, você deve incluir o namespace Windows.Graphics.DirectX.Direct3D11 em seu projeto.

using Windows.Graphics.DirectX.Direct3D11;

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

private async void CreateSoftwareBitmapFromSurface(IDirect3DSurface surface)
{
    softwareBitmap = await SoftwareBitmap.CreateCopyFromSurfaceAsync(surface);
}

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 de pixel e o modo alfa especificados de um SoftwareBitmap existente. Observe que o bitmap recém-criado tem uma cópia separada dos dados de imagem. As modificações no novo bitmap não afetarão o bitmap de origem.

SoftwareBitmap bitmapBGRA8 = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);

Transcodificar um arquivo de imagem

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

private async void TranscodeImageFile(StorageFile imageFile)
{


    using (IRandomAccessStream fileStream = await imageFile.OpenAsync(FileAccessMode.ReadWrite))
    {
        BitmapDecoder decoder = await BitmapDecoder.CreateAsync(fileStream);

        var memStream = new Windows.Storage.Streams.InMemoryRandomAccessStream();
        BitmapEncoder encoder = await BitmapEncoder.CreateForTranscodingAsync(memStream, decoder);

        encoder.BitmapTransform.ScaledWidth = 320;
        encoder.BitmapTransform.ScaledHeight = 240;

        await encoder.FlushAsync();

        memStream.Seek(0);
        fileStream.Seek(0);
        fileStream.Size = 0;
        await RandomAccessStream.CopyAsync(memStream, fileStream);

        memStream.Dispose();
    }
}