8
\$\begingroup\$

A Texture2D in XNA can only hold so much. Depending on which profile I use that limit is either 2048 or 4096 pixels wide. As far as I could tell from my research (someone correct me if I'm wrong) GDI Images do not have any limitation other than not exceeding the user's memory.

I'm looking for the most efficient way to take a GDI Image and a source rectangle (within XNA's size limitations) and create a new Texture2D from that data at runtime. The process must also take into account XNA 4.0's pre-multiplied alpha!

OR...

A way to take Texture2D.FromStream and adapt it so that it takes a source rectangle too, as that would be even better for my needs.

The requirements are:

  • No content pipeline, I need to be able to load them at runtime.
  • Using only Reach profile if possible!
  • Only care about Windows. Don't worry about portability.

TL;DR:

I need a method such as:

Texture2D Texture2DHelper.FromStream(GraphicsDevice, Stream, SourceRectangle);

Or

Texture2D Texture2DHelper.FromImage(Image, SourceRectangle)

Preferably the first.


Big picture:

Ignoring the source rectangle for a moment (to make things easier to visualize), I've tried loading a whole texture in GDI and passing the data to a Texture2D using a completely a brute-force approach:

using (System.Drawing.Image image = System.Drawing.Image.FromFile(path))
{
    int w = image.Width;
    int h = image.Height;
    System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(image);
    uint[] data = new uint[w * h];
    for (int i=0; i!=bitmap.Width; ++i)
    {
        for (int j=0; j!=bitmap.Height; ++j)
        {
            System.Drawing.Color pixel = bitmap.GetPixel(i, j);
            Microsoft.Xna.Framework.Color color = Color.FromNonPremultiplied(pixel.R, pixel.G, pixel.B, pixel.A);
            data[i + j * w] = color.PackedValue;
        }
    }
    _texture = new Texture2D(device, w, h);
    _texture.SetData(data);
}

And the result was really slow. It took many many times longer than simply doing:

Texture2D.FromStream(GraphicsDevice, new FileStream(path, FileMode.Open));

I also thought about using Bitmap.LockBits and a Block Copy but doing that would leave out the pre-multiplied alpha correction, and I don't know how to apply it after the copy.

I'm tempted to think that a better solution would be to work around Texture2D.FromStream directly... I'm not too familiar with how Stream works but I've seen some methods in .NET that take a Stream and a Rectangle. How would I achieve something like that? Either an entirely new FromStream method or a wrapper around any Stream that provides "Rectangle" functionality.

\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

OK, managed to find the solution to this, GDI-style! I've wrapped it up in a static helper class:

public static class TextureLoader
{
    public static Texture2D FromFile(GraphicsDevice device, string path, Microsoft.Xna.Framework.Rectangle? sourceRectangle = null)
    {
        // XNA 4.0 removed FromFile in favor of FromStream
        // So if we don't pass a source rectangle just delegate to FromStream
        if(sourceRectangle == null)
        {
            return Texture2D.FromStream(device, new FileStream(path, FileMode.Open));
        }
        else
        {
            // If we passed in a source rectangle convert it to System.Drawing.Rectangle
            Microsoft.Xna.Framework.Rectangle xnaRectangle = sourceRectangle.Value;
            System.Drawing.Rectangle gdiRectangle = new System.Drawing.Rectangle(xnaRectangle.X, xnaRectangle.Y, xnaRectangle.Width, xnaRectangle.Height);

            // Start by loading the entire image
            Image image = Image.FromFile(path);

            // Create an empty bitmap to contain our crop
            Bitmap bitmap = new Bitmap(gdiRectangle.Width, gdiRectangle.Height, image.PixelFormat);

            // Draw the cropped image region to the bitmap
            Graphics graphics = Graphics.FromImage(bitmap);
            graphics.DrawImage(image, new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height), gdiRectangle.X, gdiRectangle.Y, gdiRectangle.Width, gdiRectangle.Height, GraphicsUnit.Pixel);

            // Save the bitmap into a memory stream
            MemoryStream stream = new MemoryStream();
            bitmap.Save(stream, ImageFormat.Png);

            // Finally create the Texture2D from stream
            Texture2D texture = Texture2D.FromStream(device, stream);

            // Clean up
            stream.Dispose();
            graphics.Dispose();
            bitmap.Dispose();
            image.Dispose();

            return texture;
        }
    }
}

In case you didn't notice Texture2D.FromFile was removed from XNA 4.0 so I took the chance to replicate the old behavior when not passed a source rectangle.

And the best part is that since at the end it still relies on Texture2D.FromStream, the whole premultiplied alpha phase is automated! Runs relatively fast too. Okay this is not true! You still have to do the premultiply alpha calculations. I just didn't remember well since I already had code to do that in place.

But still wondering if there's a way to do this directly from the Stream without having to pass through GDI. Mind wandered towards decorating the Stream in some way but didn't arrive anywhere. :)

And from browsing through my old code I found a comment about Texture2D.FromStream being bugged and not being able to read all of the formats the documentation says it does, so I've been using GDI anyway. So I'll be staying with this method for now.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .