views:

1703

answers:

1

Greetings!

I'm creating a web form prototype (ImageLaoder.aspx) that will return an image so that it may be used like this simple example other Web Forms/web pages:

<img src="http://www.mydomain.com/ImageLoader.aspx?i=http://images.mydomain.com/img/a.jpg" />

So far, it loads JPGs with no problems, however GIFs look "grainy" compared to the orignals and BMPs and PNGs result in the following exception:

System.Runtime.InteropServices.ExternalException: A generic error occurred in GDI+

My code thus far looks like this:

protected void Page_Load(object sender, EventArgs e)
{
    string l_filePath = Request.QueryString["i"];

    System.Drawing.Image l_image = GetImage(l_filePath);
    if (l_image != null)
    {
        System.Drawing.Imaging.ImageFormat l_imageFormat = DetermineImageFormat(l_filePath);
        WriteImageAsReponse(l_image, l_imageFormat);
    }
}

private System.Drawing.Image GetImage(string filePath)
{
    WebClient l_WebClient = new WebClient();
    byte[] l_imageBytes = l_WebClient.DownloadData(filePath);

    System.Drawing.Image l_image = null;
    using (MemoryStream l_MemStream = new MemoryStream(l_imageBytes, 0, l_imageBytes.Length))
    {
        l_MemStream.Write(l_imageBytes, 0, l_imageBytes.Length);
        l_image = System.Drawing.Image.FromStream(l_MemStream, true);
        l_MemStream.Close();
    }

    return l_image;
}

private System.Drawing.Imaging.ImageFormat DetermineImageFormat(string filePath)
{
    if (filePath.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase))
        return System.Drawing.Imaging.ImageFormat.Jpeg;
    else if (filePath.EndsWith(".gif", StringComparison.OrdinalIgnoreCase))
        return System.Drawing.Imaging.ImageFormat.Gif;
    else if (filePath.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
        return System.Drawing.Imaging.ImageFormat.Png;
    else
        return System.Drawing.Imaging.ImageFormat.Bmp;
}

private void WriteImageAsReponse(System.Drawing.Image image, System.Drawing.Imaging.ImageFormat imageFormat)
{
    if (image == null)
        return;

    System.Drawing.Bitmap l_outputBitMap = new Bitmap(image);

    if (imageFormat == System.Drawing.Imaging.ImageFormat.Jpeg)
        Response.ContentType = "image/jpg";
    else if (imageFormat == System.Drawing.Imaging.ImageFormat.Gif)
        Response.ContentType = "image/gif";
    else if (imageFormat == System.Drawing.Imaging.ImageFormat.Png)
        Response.ContentType = "image/png";
    else
        Response.ContentType = "image/bmp";

    l_outputBitMap.Save(Response.OutputStream, imageFormat);
}

Any ideas why GIFs are grainy and PNGs and BMPs cause exceptions?

+7  A: 

A few points about your GetImage method:

  • When you use Image.FromStream you shouldn't close (or dispose) the stream
  • If you're calling Dispose on a stream (with the using statement) you don't need to call Close
  • You're writing to the stream, but then not "rewinding it" so l_image doesn't actually get any data as far as I can see (unless Image.FromStream resets the position itself). (It could be that the gif/jpg decoders rewind the stream but bmp/png don't, hence the error.)
  • Why don't you just use the MemoryStream constructor which takes a byte array?

In short, I believe your GetImage method can be replaced with:

private Image GetImage(string filePath)
{
    WebClient l_WebClient = new WebClient();
    byte[] l_imageBytes = l_WebClient.DownloadData(filePath);
    MemoryStream l_stream = new MemoryStream(l_imageBytes);
    return Image.FromStream(l_stream);
}

Now, more importantly - why are you loading the image at all? Why don't you just serve the file itself as a response, setting the content type as you're already doing - or possibly just based on the extension? In other words, all of your code would become:

protected void Page_Load(object sender, EventArgs e)
{
    string filePath = Request.QueryString["i"];
    string extension = l_filePath.Substring(l_filePath.LastIndexOf('.') + 1);
    Response.ContentType = "image/" + extension;
    byte[] data = new WebClient.DownloadData(filePath);
    Response.OutputStream.Write(data, 0, data.Length);
    Response.End();
}

A bit more error handling (including "is this a reasonable extension?") would be nice, but other than that I think it's okay. The only benefit of actually loading the image yourself is that you get to validate that it really is an image rather than a virus or something like that.

EDIT: Just out of interest, do you have a good reason why you'd want image requests to go through your server? Why would the web page author write:

<img src="http://www.mydomain.com/ImageLoader.aspx?i=http://images.mydomain.com/img/a.jpg" />

instead of

<img src="http://images.mydomain.com/img/a.jpg" />

There are some reasons why it might be useful, but in many cases it's just a waste.

Jon Skeet
As per usual, Jon, you're right on the money - it works like a charm :) I know the reason for this page seems pointless, but there will be more logic to determine which image is served, rather than a simple path on a query string - I just wanted to keep the example simple.
Bullines
Re the second bullet (don't call Close) - if you are *writing* a stream, this can ensure the last few bytes are flushed, so is important. I've seen the zip streams, in particular, lose data without an explicit Close(). But good answer ;-p
Marc Gravell
@Marc: You mean close doesn't flush it? I'd argue that such a stream is truly broken. (I do seem to remember this happening with a CryptoStream in .NET 1.1 though.)
Jon Skeet
@Jon: more accurately; for compression streams Flush() doesn't actually Flush()... it keeps a few bytes back as part of the compression cycle. The *only* way to write the last few bytes is to Close()
Marc Gravell
Hmmm... unable to reproduce now, but I remember discussing this (with repro code) on usenet. Maybe they fixed it in an SP?
Marc Gravell
Any reason why you couldn't make a reference to the `WebClient`, use it, then set `Response.ContentType = wclient.ResponseHeaders["Content-Type"]`
Adam Nofsinger
@Adam: Not particularly... if you trust the content type, of course :)
Jon Skeet