2005.08.25 04:03 PM

Sparklines 2.2 (or GIF transparency with .NET)

[Updated 8/31/05: Now version 2.3.]

[Updated 8/27/05: Oops. Forgot that the specially styled sparkline table with the non-white background would appear on the main page with the earlier posts and would override their styles. Fixed. Also, I forgot to mention another article that helped improve my understanding of color table manipulation in .NET: Optimizing Color Quantization for ASP.NET Images by Morgan Skinner. It's a great read, highly recommended.]

I woke up today wanting to add background transparency to my sparklines generator. I previously punted transparency because Joe Gregorio's Python implementation, on which my code was originally based, didn't include it. More importantly, though, I had no idea how to do it and didn't have time to figure it out. I still don't have time, but figured a little study would do me some good.

After some searching, I wound up on a thread started by Nathan Sokalski asking how to make a color transparent in a dynamically generated GIF in ASP.NET. After some initial difficulty getting folks to understand the question, Mr. Sokalski finally got some relevant advice from Microsoft MVP Kevin Spencer. Mr. Spencer pointed Mr. Sokalski to KB 319061, How to save a .gif file with a new color table by using Visual C# .NET, which includes a link to KB 318343, GDI+ GIF files are saved using the 8-bpp format. When combined with the thread, these articles do a pretty good job of explaining .NET GDI+ image handling and its impact on GIF color palettes and transparency. However, I still needed some concrete examples (or at least some I could understand) to pull it all together, so I kept searching and eventually found Bob Powell's excellent GDI+ FAQ post, Creating Transparent GIF Images.

Finally, I understood.

Dynamically generated images are assembled in Bitmap objects using Graphics objects and turned into GIFs using the Bitmap.Save() method. When this method is called, the GIF codec used by GDI+ version 1.0 encodes the image in an 8 bit per pixel (bpp) format, which means it always produces GIFs with 256-entry color tables. When the dynamically generated image starts out with a 32 bpp format, as is the case with my sparkline generator, the GIF encoder sets the palette to the default halftone palette (this involves Alpha component adjustments, using intensity as a substitute for color in order to reduce the number of colors required). When the dynamically generated image starts out with other indexed pixel formats (like 1 bpp or 4 bpp), the Bitmap.Save() method promotes the format to 32 bpp before giving the image to the GIF encoder, which then color-reduces it to a 256-entry color table. And, when the dynamically generated image format starts out as 8 bpp, the codec simply encodes its palette entries into the smallest GIF color table possible containing all the palette entries without exceeding 256 (per the GIF specification).

In short, except in certain cases involving images that start out with an indexed 8 bpp format and specific colors, the GIF color palette resulting from the passing of a dynamically generated image through these conversions using Bitmap.Save() will not be the same as the color palette created (implicitly or explicitly) for the original dynamic image. Therefore, if the original color palette was given a transparent color entry, it will likely be gone.

So, how does one add a transparent color to a dynamically generated GIF?

Here's the trick:

  1. Pass the dynamically generated image through the GIF codec once using Bitmap.Save(), letting it convert the color table as described above (creating an indexed 8 bpp image).
  2. Create a new indexed 8 bpp image and fill its 256 color entries with the colors from the converted image.
  3. Set one of the new image's color entries to transparent by setting its Alpha component to 0 (this will require finding the right color entry first).
  4. Copy the pixels from the converted image one byte at a time to the new image.

When the new image is then passed through Bitmap.Save() to make a GIF, there will be no full scale color table conversion/replacement and the transparency will be preserved.

Here's the code I added to spark.ashx to do this. It reflects some spark.ashx-only decisions, but it wouldn't be hard to generalize. For instance, it takes and returns a MemoryStream, because that's what the calling routines are dealing with, but it could take and return just Stream objects. Also, the routine is only concerned with making white transparent, but it could easily take a Color type parameter describing the color to make transparent.

 1 using System;
 2 using System.Drawing;
 3 using System.Drawing.Imaging;
 4 using System.Runtime.InteropServices;
 5 
 6 MemoryStream MakeTransparent(MemoryStream origBitmapMemoryStream) {
 7 
 8   Color transparentColor = GetColor("White");
 9   int transparentArgb = transparentColor.ToArgb();
10 
11   using (Bitmap origBitmap = new Bitmap(origBitmapMemoryStream)) {
12     using (Bitmap newBitmap = new Bitmap(origBitmap.Width, origBitmap.Height, origBitmap.PixelFormat)) {
13 
14       ColorPalette origPalette = origBitmap.Palette;
15       ColorPalette newPalette = newBitmap.Palette;
16 
17       int index = 0;
18       int transparentIndex = -1;
19 
20       foreach (Color origColor in origPalette.Entries) {
21         newPalette.Entries[index] = Color.FromArgb(255, origColor);
22         if (origColor.ToArgb() == transparentArgb) transparentIndex = index;
23         index += 1;
24       }
25 
26       if (-1 == transparentIndex) {
27         return origBitmapMemoryStream;
28       }
29 
30       newPalette.Entries[transparentIndex] = Color.FromArgb(0, transparentColor);
31       newBitmap.Palette = newPalette;
32       
33       Rectangle rect = new Rectangle(0, 0, origBitmap.Width, origBitmap.Height);
34       
35       BitmapData origBitmapData = origBitmap.LockBits(rect, ImageLockMode.ReadOnly, origBitmap.PixelFormat);
36       BitmapData newBitmapData = newBitmap.LockBits(rect, ImageLockMode.WriteOnly, newBitmap.PixelFormat);    
37 
38       for (int y = 0; y < origBitmap.Height; y++) {
39         for (int x = 0; x < origBitmap.Width; x++) {
40           byte origBitmapByte = Marshal.ReadByte(origBitmapData.Scan0, origBitmapData.Stride * y + x);
41           Marshal.WriteByte(newBitmapData.Scan0, newBitmapData.Stride * y + x, origBitmapByte);
42         }    
43       }
44 
45       newBitmap.UnlockBits(newBitmapData);
46       origBitmap.UnlockBits(origBitmapData);
47 
48       MemoryStream m = new MemoryStream();
49       newBitmap.Save(m, ImageFormat.Gif);
50       return m;
51       
52     }
53   }
54   
55 }

Some notes regarding the code:

  • In spark.ashx, the MemoryStream received by this function is always the result of a call to Bitmap.Save() using a ImageFormat.Gif parameter, so I know it represents the end result of a GDI+ GIF codec encoding of an image to an indexed 8 bpp format. So, line 12 could have used PixelFormat.Format8bppIndexed instead of origBitmap.PixelFormat.
  • Note the color comparison on line 22 uses the 32-bit ARGB value, retrieved using ToArgb(), because "...the [Color].Equals and == operators determine equivalency using more than just the ARGB value of the colors."
  • If the color targeted for transparency isn't found in the palette, line 26 short-circuits the routine, as there's no point in building a new image that exactly matches the original.
  • There's another faster way to copy the image bytes besides using the System.Runtime.InteropServices.Marshal class' static ReadByte() and WriteByte() methods (lines 40 and 41), but it requires unsafe code. See Bob Powell's GDI+ FAQ post for details.

After adding this code to spark.ashx, I really wanted to make transparent backgrounds the default behavior. I decided against it in order to remain compatible with earlier versions. (For all I know, someone actually counts on the GIFs it produces having white backgrounds.) The one exception is the bad/missing parameters "X" graphic, which will now always return a transparent background.

To get a sparkline having a transparent background, just add a transparent=true parameter to the querystring. With this parameter on, all whites appearing anywhere in the resulting GIF will be transparent.

Here's the source. And here are some IMG elements pointing at the updated spark.ashx handler with transparent backgrounds, along with their querystrings.

sparkline querystring
type=smooth&d=86,66,82,44,64,66,88,96,26,14,0,0,26,8,6,24,52,36,6,10,30&
height=20&min-color=red&max-color=blue&last-color=green&step=2&
last-m=true&max-m=true&min-m=true&transparent=true
type=discrete&d=40,55,32,65,33,34,65,80,50,66,82,44,64,66,78,86,78,68,80,54,45,48,32&
height=20&above-color=red&below-color=blue&upper=60&transparent=true
type=bars&d=76,48,100,25&width=50&bar-height=3&
bar-colors=teal,teal,maroon,teal&
transparent=true
(missing or bad parameters)

Let me know if you have any questions.


Comments

Hey there :). remember me from version 1? lol Just wondering if you had a chance to think about the data normalization. i.e. converting (1,4,5,3,1,4,9) to (0,40,60,20,0,40,100)

Matt | 2005.08.27 08:26 AM

Matt,

It hasn't crept to the top of my list, yet, as it can be easily done by the client/caller. Now that I've scratched my transparency itch, though, I'll look again at adding it.

ewbi.develops | 2005.08.27 09:14 AM

Hi there,
I want to dynamically display the image that create by
GDI+ and save it to web server folder(temp.gif),so I want to display using imag1.imageUrl(server.mappath & "/temp.gif"),it works in localhost,but I can not get the dynamically image if i post the application to the web server.
could u tell me what the problem is?

Thanks,

Yang | 2005.10.10 02:12 PM

Hi Yang,

Sorry for the long delay (I've been moving cross country for the last few weeks). Sounds to me like you might be experiencing some permissions issues on the web server, owing perhaps to the account under which your ASP.NET service is running. Rather than try to dive into all the possibilities, I thought I'd point you at a couple of good MSDN posts that might help (assuming you still need help).

The first is a great Cutting Edge article by Dino Esposito from the April 2004 issue that describes dynamic/cached image generation in ASP.NET 1.1:

http://msdn.microsoft.com/msdnmag/issues/04/04/CuttingEdge/default.aspx

The second is from the MSDN Library's Visual Basic and C# Concepts help that describes Security Considerations for ASP.NET Web Applications:

http://msdn.microsoft.com/library/en-us/vbcon/html/vbconAccessPermissionsForWebApplications.asp

Hope those can help. If not, please feel free to come back and let me know.

ewbi.develops | 2005.11.08 03:40 PM

Hi,

I use your code to generate the Image on the fly . My application is simple. It consist of Textbox and a button, Once I write a Text in textbox and click Button it should generate the Image of it..

I try to make the Transparent Image using Imageformat.Png and save it , But this code works fine on mozilla but not in IE.
so, I use your code to make Transparent GIF, First i write the code in VB.Net It works fine and Make Images that have transparent Background, but soon it start crashing my application, as some Images Leave there reference in memory, So I try to rewrite it in C# (also I copy paste your function) for the Same, But now it doesn't Make the Transparent Image at all,

Do you have any idea ..Please help me

Thanks

Sumit Gupta

Sumit Gupta | 2005.11.26 02:05 AM

Sumit,

As per my email, please checkout this post which attempts to address your questions:

http://ewbi.blogs.com/develops/2005/11/aspnet_dynamic_.html

ewbi.develops | 2005.12.02 12:31 AM

Thanks! I needed that. BTW, here it is in VB.net
Private Function MakeTransparent(ByRef origBitmapMemoryStream As MemoryStream) As MemoryStream
Dim transparentColor As Color = Color.White
Dim transparentArgb As Int32 = transparentColor.ToArgb()
Dim origBitmap As Bitmap = New Bitmap(origBitmapMemoryStream)
Dim newBitmap As Bitmap = New Bitmap(origBitmap.Width, origBitmap.Height, origBitmap.PixelFormat)
Dim origPalette As ColorPalette = origBitmap.Palette
Dim newPalette As ColorPalette = newBitmap.Palette
Dim index As Int32 = 0
Dim transparentIndex As Int32 = -1
For Each origColor As Color In origPalette.Entries
newPalette.Entries(index) = Color.FromArgb(255, origColor)
If (origColor.ToArgb.Equals(transparentArgb)) Then
transparentIndex = index
End If
index += 1

Next

If transparentIndex.Equals(-1) Then Return origBitmapMemoryStream

newPalette.Entries(transparentIndex) = Color.FromArgb(0, transparentColor)
newBitmap.Palette = newPalette

Dim rect As Rectangle = New Rectangle(0, 0, origBitmap.Width, origBitmap.Height)
Dim origBitmapData As BitmapData = origBitmap.LockBits(rect, ImageLockMode.ReadOnly, origBitmap.PixelFormat)
Dim newBitmapData As BitmapData = newBitmap.LockBits(rect, ImageLockMode.WriteOnly, newBitmap.PixelFormat)

For y As Integer = 0 To origBitmap.Height - 1
For x As Int32 = 0 To origBitmap.Width - 1
Dim origBitmapByte As Byte = Marshal.ReadByte(origBitmapData.Scan0, origBitmapData.Stride * y + x)
Marshal.WriteByte(newBitmapData.Scan0, newBitmapData.Stride * y + x, origBitmapByte)
Next
Next

newBitmap.UnlockBits(newBitmapData)
origBitmap.UnlockBits(origBitmapData)

Dim m As MemoryStream = New MemoryStream()
newBitmap.Save(m, ImageFormat.Gif)

newBitmap.Dispose()
origBitmap.Dispose()

Return m
End Function

Joe | 2007.03.05 03:26 PM

Wow, thanks Joe, I'm sure someone's going to find that to be very helpful.

ewbi.develops | 2007.03.05 04:07 PM



Post a Comment

 
  (optional)
  (no html)
 
   


TrackBack

TrackBack URL:  http://www.typepad.com/services/trackback/6a00d8341c7bd453ef00d83467120d53ef

Listed below are links to weblogs that reference Sparklines 2.2 (or GIF transparency with .NET):