Criar, editar e salvar imagens de bitmap

Este artigo explica como carregar e salvar arquivos de imagem usando o BitmapDecoder e o 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 fontes, incluindo arquivos de imagem, objetos WriteableBitmap, superfícies Direct3D e código. SoftwareBitmap permite converter facilmente entre diferentes formatos de pixel e modos alfa, e permite acesso de baixo nível a dados de pixel. Além disso, SoftwareBitmap é uma interface comum usada por vários recursos do 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 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 a partir de um arquivo de imagem com BitmapDecoder

Para criar um SoftwareBitmap a partir de um arquivo, obtenha uma instância de StorageFile contendo 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 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.

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 para cada codec suportado pelo 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 sem suporte que será lançado se as miniaturas não forem suportadas.

Chame FlushAsync para fazer com que o codificador grave os dados da 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 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 Referência de opções do 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 Imagem, primeiro defina um controle Imagem em sua página XAML.

<Image x:Name="imageControl"/>

Atualmente, o controle Imagem só oferece suporte a imagens que usam codificação BGRA8 e canal alfa multiplicado previamente ou nenhum canal alfa. Antes de tentar exibir uma imagem, teste para verificar se ela tem o formato correto e, se não, use o método Convert estático do SoftwareBitmap para converter a imagem para o formato suportado.

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

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

Criar um SoftwareBitmap a partir de um WriteableBitmap

Você pode criar um SoftwareBitmap a partir de um WriteableBitmap existente chamando SoftwareBitmap.CreateCopyFromBuffer e fornecer 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 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.

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

Para usar a 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 dentro do 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 formato e tamanho de pixel desejado. Ou, use um SoftwareBitmap existente para o 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 o BitmapBuffer para a interface COM IMemoryBufferByteAccess e, em seguida, chame IMemoryBufferByteAccess.GetBuffer para preencher uma matriz de bytes com dados. Use o método BitmapBuffer.GetPlaneDescription para obter um objeto BitmapPlaneDescription que o ajudará a calcular o deslocamento no buffer para 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 abrindo a página Propriedades do projeto, clicando na página de propriedades Build e marcando a caixa de seleção Permitir código não seguro.

Criar um SoftwareBitmap a partir de uma superfície Direct3D

Para criar um objeto SoftwareBitmap a partir de uma superfície 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 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 qualquer efeito na superfície 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, Convertque 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 da 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 a partir do arquivo a ser transcodificado. Crie um novo BitmapDecoder a partir do fluxo de entrada. Crie um novo InMemoryRandomAccessStream para o codificador gravar e chamar BitmapEncoder.CreateForTranscodingAsync, passando o fluxo na memória e o objeto decodificador. As opções de codificação não são suportadas durante a transcodificação; em vez disso, você deve usar CreateAsync. Quaisquer 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 para o fluxo na memória. Finalmente, procure o fluxo de arquivos e o fluxo na memória até o início e chame CopyAsync para gravar o fluxo na memória no fluxo de arquivos.

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