views:

2685

answers:

5

Basically I am trying to render a simple image in an ASP.NET handler:

public void ProcessRequest (HttpContext context)
{
    Bitmap image = new Bitmap(16, 16);
    Graphics graph = Graphics.FromImage(image);

    graph.FillEllipse(Brushes.Green, 0, 0, 16, 16);

    context.Response.ContentType = "image/png";
    image.Save(context.Response.OutputStream, ImageFormat.Png);
}

But I get the following exception:

System.Runtime.InteropServices.ExternalException: A generic error
occurred in GDI+.
    at System.Drawing.Image.Save(Stream stream, ImageCodecInfo encoder,
    EncoderParameters encoderParams)

The solution is to use this instead of having image write to OutputStream:

MemoryStream temp = new MemoryStream();
image.Save(temp, ImageFormat.Png);
byte[] buffer = temp.GetBuffer();
context.Response.OutputStream.Write(buffer, 0, buffer.Length);

So I'm just curious as to why the first variant is problematic?

Edit: The HRESULT is 80004005 which is just "generic".

+1  A: 

I believe the problem is that the Response.OutputStream does not support seeking. In order to save a PNG (or JPEG), the image object needs to be able to write the output non-sequentially. If I remember correctly, it would have worked if you saved the image as a BMP since that image format can be written without seeking the stream.

David
I tried that actually but the result turned out the same.
Serguei
+1  A: 

From the look of it, it appears that you are trying to create a graph of some kind. Is this just to be rendered on a page within a div? If so, I would think you might be able to do something like this. Basically take the image object that is created and add that as a control to an existing div or placeholder in the code...

Bitmap image = new Bitmap(16, 16);
Graphics graph = Graphics.FromImage(image);
graph.FillEllipse(Brushes.Green, 0, 0, 16, 16);
this.myGraphPlaceholder.Controls.Add(graph);
RSolberg
Not really. This is an implementation of IHttpHandler and the consumer is a non-ASP.NET app.
Serguei
A: 

Ok I used a wrapper for Stream (implements Stream and passes calls to an underlying stream) to determine that Image.Save() calls Position and Length properties without checking CanSeek which returns false. It also tries to set Position to 0.

So it seems an intermediate buffer is required.

Serguei
+2  A: 

The writer indeed needs to seek to write in the stream properly.

But in your last source code, make sure that you do use either MemoryStream.ToArray() to get the proper data or, if you do not want to copy the data, use MemoryStream.GetBuffer() with MemoryStream.Length and not the length of the returned array.

GetBuffer will return the internal buffer used by the MemoryStream, and its length generally greater than the length of the data that has been written to the stream.

This will avoid you to send garbage at the end of the stream, and not mess up some strict image decoder that would not tolerate trailing garbage. (And transfer less data...)

Jerome Laban
Good catch, thanks! MSDN says pretty much the same thing about GetBuffer(): http://msdn.microsoft.com/en-us/library/system.io.memorystream.getbuffer.aspx
Serguei
A: 

Image.Save(MemoryStream stream) does require a MemoryStream object that can be seeked upon. The context.Response.OutputStream is forward-only and doesn't support seeking, so you need an intermediate stream. However, you don't need the byte array buffer. You can write directly from the temporary memory stream into the context.Response.OutputStream:

/// <summary>
/// Sends a given image to the client browser as a PNG encoded image.
/// </summary>
/// <param name="image">The image object to send.</param>
private void SendImage(Image image)
{
    // Get the PNG image codec
    ImageCodecInfo codec = GetCodec("image/png");

    // Configure to encode at high quality
    using (EncoderParameters ep = new EncoderParameters())
    {
        ep.Param[0] = new EncoderParameter(Encoder.Quality, 100L);

        // Encode the image
        using (MemoryStream ms = new MemoryStream())
        {
            image.Save(ms, codec, ep);

            // Send the encoded image to the browser
            HttpContext.Current.Response.Clear();
            HttpContext.Current.Response.ContentType = "image/png";
            ms.WriteTo(HttpContext.Current.Response.OutputStream);
        }
    }
}

A fully functional code sample is available here:

Auto-Generate Anti-Aliased Text Images with ASP.NET

Jason Carter