1

I need to display .pgm images in my Universal Windows App. XAML Image control does not directly support .pgm images so I need to work around it.

There are many examples on the internet of opening .pgm files in c#, but all of these rely on using the Bitmap object which is not supported in Universal Windows Platform (the System.Drawing and System.Windows.Media libraries cannot be used).

I've got the code that reads the image width and height and reads the pixels in a byte array (containing values 0-255 representing gray shades).

The next step would be to draw the image from the byte[] array using any object that can eventually be passed to the XAML Image.Source (and to do it in a reasonable amount of time).

The best I managed to do was to display this but the actual picture is supposed to look like this (for some reason it shows the image 4x and the colors are wrong).

The code i used:

    public int width;
    public int height;
    public int maxVal; //255
    public byte[] pixels;

    public async Task<WriteableBitmap> ToWriteableBitmap()
    {
        WriteableBitmap writeableBitmap = new WriteableBitmap(width, height);
        using (Stream stream = writeableBitmap.PixelBuffer.AsStream())
        {
            await stream.WriteAsync(pixels, 0, pixels.Length);
        }
        return writeableBitmap;
    }

Should it matter, I'm also providing the code I use to read the .pgm file to the PgmImage object, but I'm pretty sure this works fine:

    public static async Task<PgmImage> LoadFromFile(string file)
    {
        FileStream ifs = null;
        await Task.Run( () =>
        {
            Task.Yield();
            ifs = new FileStream(file, FileMode.Open, FileAccess.Read);
        });
        BinaryReader br = new BinaryReader(ifs);

        string magic = NextNonCommentLine(br);
        //if (magic != "P5")
        //    throw new Exception("Unknown magic number: " + magic);

        string widthHeight = NextNonCommentLine(br);
        string[] tokens = widthHeight.Split(' ');
        int width = int.Parse(tokens[0]);
        int height = int.Parse(tokens[1]);

        string sMaxVal = NextNonCommentLine(br);
        int maxVal = int.Parse(sMaxVal);

        byte[] pixels = new byte[height * width];
        for (int i = 0; i < height * width; i++)
        {
            pixels[i] = br.ReadByte();
        }
        return new PgmImage(width, height, maxVal, pixels);
    }

    static string NextAnyLine(BinaryReader br)
    {
        string s = "";
        byte b = 0; // dummy
        while (b != 10) // newline
        {
            b = br.ReadByte();
            char c = (char)b;
            s += c;
        }
        return s.Trim();
    }

    static string NextNonCommentLine(BinaryReader br)
    {
        string s = NextAnyLine(br);
        while (s.StartsWith("#") || s == "")
            s = NextAnyLine(br);
        return s;
    }

(it's a slightly edited version of this: jamesmccaffrey.wordpress.com/2014/10/21/a-pgm-image-viewer-using-c). I should mention that I would prefer a solution that does not depend on any third-party libraries or NuGet packages, but I am desperate and therefore open to any solutions.

Community
  • 1
  • 1
zvjeverica
  • 21
  • 5

2 Answers2

1

The WritableBitmap.PixelBuffer uses a RGBA color space which means every pixel is described with four bytes in the byte array, but the array generated from the PGM image uses only one byte to describe a pixel. By simply expanding the array 4 times (and setting the alpha value for each pixel to the max value of 255) I had managed to get the right display.

public async Task<WriteableBitmap> ToWriteableBitmap()
    {
        WriteableBitmap writeableBitmap = new WriteableBitmap(width, height);

        byte[] expanded = new byte[pixels.Length * 4];
        int j = 0;
        for (int i = 0; i< pixels.Length; i++)
        {
            expanded[j++] = pixels[i];
            expanded[j++]= pixels[i];
            expanded[j++] = pixels[i];
            expanded[j++] = 255; //Alpha
        }

        using (Stream stream = writeableBitmap.PixelBuffer.AsStream())
        {
            await stream.WriteAsync(expanded, 0, expanded.Length);
        }
        return writeableBitmap;
    }
zvjeverica
  • 21
  • 5
0

I have tested your code and reproduced your issue. The problem is WriteableBitmap could not be perfectly loaded by xmal control.

I have tried to use SoftwareBitmap to store PgmByteData that load from pgm file. And it Works fine.

Your pixels in a byte array containing values 0-255 representing gray shades.So you could use BitmapPixelFormat.Gray8 as BitmapPixelFormat. You cloud create SoftwareBitmap as the following code.

SoftwareBitmap softwareBitmap = new SoftwareBitmap(BitmapPixelFormat.Gray8, (int)width, (int)height);
            softwareBitmap.CopyFromBuffer(pgmByteData.AsBuffer());

Currently, the Image control only supports images that use BGRA8 encoding and pre-multiplied or no alpha channel. Before attempting to display an image, test to make sure it has the correct format, and if not, use the SoftwareBitmap static Convert method to convert the image to the supported format.

For more information please reference Use SoftwareBitmap with a XAML Image control.

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;

The code sample has upload. Please check.

Nico Zhu
  • 32,367
  • 2
  • 15
  • 36
  • Thank you Nico for your fast and detailed answer. I had also found my own solution by expanding the byte[] to fit the BGRA encoding. I've posted the code as an answer; maybe somebody will find it useful. – zvjeverica Apr 05 '17 at 18:43