Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WinRT image handling

A friend and I spent the better part of last night nearly tearing our hair out trying to work with some images in a metro app. We got images into the app with the share charm, and then I wanted to do some other work with them, cropping the images and saving them back into the appdata folder. This proved extremely frustrating.

My question, at the end of all this, is going to be "What's the proper way of doing this, without feeling like I'm hammering together a bunch of mismatched jigsaw puzzle pieces?"

When sharing multiple images with the app, they come in as a list of Windows.Storage.StorageFiles. Here's some code used to handle that.

var storageItems = await _shareOperation.Data.GetStorageItemsAsync();

foreach (StorageFile item in storageItems)
{
    var stream = await item.OpenReadAsync();
    var properties = await item.Properties.GetImagePropertiesAsync();

    var image = new WriteableBitmap((Int32)properties.Width, (Int32)properties.Height);
    image.SetSource(stream);

    images.Add(image);
}

Some searching online has indicated that currently, a Windows.UI.Xaml.Media.Imaging.WriteableBitmap is the only thing capable of letting you access the pixel data in the image. This question includes a helpful answer full of extension methods for saving images to a file, so we used those.

Our problems were the worst when I tried opening the files again later. I did something similar to before:

var files = await ApplicationData.Current.LocalFolder.GetFilesAsync();

foreach (var file in files)
{
    var fileStream = await file.OpenReadAsync();
    var properties = await file.Properties.GetImagePropertiesAsync();
    var bitmap = new WriteableBitmap((Int32)properties.Width, (Int32)properties.Height);
    bitmap.SetSource(fileStream);

    System.IO.Stream stream = bitmap.PixelBuffer.AsStream();

Here comes a problem. How long is this stream, if I want the bytes out of it?

    // CRASH! Length isn't supported on an IRandomAccessStream.
    var pixels = new byte[fileStream.Length];

Ok try again.

    var pixels = new byte[stream.Length];

This works, except... if the image is compressed, the stream is shorter than you would expect, so you will eventually get an out of bounds exception. For now pretend it's an uncompressed bitmap.

    await _stream.ReadAsync(pixels, 0, pixels.Length);

Well guess what. Even though I said bitmap.SetSource(fileStream); in order to read in the data, my byte array is still full of zeroes. I have no idea why. If I pass this same bitmap into a my UI through the sample data group, the image shows up just fine. So it has clearly got the pixel data in that bitmap somewhere, but I can't read it out of bitmap.PixelBuffer? Why not?

Finally, here's what ended up actually working.

    var decoder = await BitmapDecoder.CreateAsync(BitmapDecoder.PngDecoderId, fileStream);
    var data = await decoder.GetPixelDataAsync();
    var bytes = data.DetachPixelData();

    /* process my data, finally */

} // end of that foreach I started a while ago

So now I have by image data, but I still have a big problem. In order to do anything with it, I have to make assumptions about its format. I have no idea whether it's rgba, rgb, abgr, bgra, whatever they can be. If I guess wrong my processing just fails. I've had dozens of test runs spit out zero byte and corrupted images, upside down images (???), wrong colors, etc. I would have expected to find some of this info in the properties that I got from calling await file.Properties.GetImagePropertiesAsync();, but no luck. That only contains the image width and height, plus some other useless things. Minimal documentation here.

So, why is this process so painful? Is this just reflecting the immaturity of the libraries right now, and can I expect it to get better? Or is there already some standard way of doing this? I wish it were as easy as in System.Drawing. That gave you all the data you ever needed, and happily loaded any image type correctly, without making you deal with streams yourself.

like image 251
Tesserex Avatar asked Apr 04 '12 15:04

Tesserex


1 Answers

  1. From what I have seen - when you are planning on loading the WriteableBitmap with a stream - you don't need to check the image dimensions - just do new WriteableBitmap(1,1), then call SetSource().
  2. Not sure why you were thinking var pixels = new byte[fileStream.Length]; would work, since the fileStream has the compressed image bytes and not a pixel array.
  3. You might need to seek to the beginning of the stream to get the pixels array:

    var pixelStream = pixelBuffer.AsStream();
    var bytes = new byte[this.pixelStream.Length];
    this.pixelStream.Seek(0, SeekOrigin.Begin);
    this.pixelStream.Read(bytes, 0, Bytes.Length);
    
  4. I had started working on a WinRT port of WriteableBitmapEx - maybe it could help you: http://bit.ly/WriteableBitmapExWinRT. I have not tested it well and it is based on an older version of WBX, but it is fairly complete in terms of feature support. Might be a tad slower than it is possible too.

like image 98
Filip Skakun Avatar answered Sep 23 '22 02:09

Filip Skakun