Creating Transparent GIF Images
Now includes a VB version of the listing.
This process is easy enough when you know how but is nevertheless a little more complex than it should be.
To save a GIF with a transparency key you need to modify the colour palette of the image. There are a few problems associated with this. If you're creating an image, you'll be using a true colour format such as 24 or 32 bit per pixel non indexed. This is because you cannot obtain a Graphics object using Graphics.FromImage for any images with an indexed pixel format. Saving such an image as a GIF file will create a standard spread palette for you with the range of colours seen in figure 1.
Figure 1: The standard spread palette provided by GDI+
Unfortunately, at no point during the save process are you given the opportunity to choose a transparent colour so you need to take the saved image and re-save it with a modified palette.
This in itself presents a problem because once a GIF image has been created, even though you can get hold of and manipulate the palette using the Bitmap.Palette property, GDI+ refuses to save the image with anything other than its original palette.
To work around these limitations it's necessary to create a new, blank 8 bit per pixel, indexed palette image, modify it's bitmap to be the same as the original images, copy all the pixel data from the original to the new and then save the new image.
As a demonstration of this process, and to provide a useful tool, the code in listing 1 is an application that enables you to load a GIF image, choose a transparent colour and save the GIF. panel1_Click is the method with the actual GIF manipulation.
Listing 1: TransparentGifCreator.cs
using System;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
namespace TransparentGifCreator
{
/// <summary>
/// Summary description for Form1.
/// </summary>
public class Form1 : System.Windows.Forms.Form
{
private System.Windows.Forms.PictureBox pictureBox1;
private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.Button button1;
private System.Windows.Forms.Button button2;
private System.Windows.Forms.Button button3;
private System.ComponentModel.IContainer components;
Image _gifImage;
private System.Windows.Forms.Timer timer1;
ColorPalette cp;
int CurrentEntry;
public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
//
// TODO: Add any constructor code after InitializeComponent call
//
}
/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.pictureBox1 = new System.Windows.Forms.PictureBox();
this.panel1 = new System.Windows.Forms.Panel();
this.button1 = new System.Windows.Forms.Button();
this.button2 = new System.Windows.Forms.Button();
this.button3 = new System.Windows.Forms.Button();
this.timer1 = new System.Windows.Forms.Timer(this.components);
this.SuspendLayout();
//
// pictureBox1
//
this.pictureBox1.Location = new System.Drawing.Point(96, ;
this.pictureBox1.Name = "pictureBox1";
this.pictureBox1.Size = new System.Drawing.Size(144, 144);
this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
this.pictureBox1.TabIndex = 0;
this.pictureBox1.TabStop = false;
//
// panel1
//
this.panel1.Location = new System.Drawing.Point(8, 168);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(144, 144);
this.panel1.TabIndex = 1;
this.panel1.Click += new System.EventHandler(this.panel1_Click);
this.panel1.Paint += new System.Windows.Forms.PaintEventHandler(this.panel1_Paint);
this.panel1.MouseMove += new System.Windows.Forms.MouseEventHandler(this.panel1_MouseMove);
//
// button1
//
this.button1.Location = new System.Drawing.Point(200, 176);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(88, 24);
this.button1.TabIndex = 2;
this.button1.Text = "Open";
this.button1.Click += new System.EventHandler(this.button1_Click);
//
// button2
//
this.button2.Location = new System.Drawing.Point(200, 216);
this.button2.Name = "button2";
this.button2.Size = new System.Drawing.Size(88, 24);
this.button2.TabIndex = 2;
this.button2.Text = "Save";
this.button2.Click += new System.EventHandler(this.button2_Click);
//
// button3
//
this.button3.Location = new System.Drawing.Point(200, 256);
this.button3.Name = "button3";
this.button3.Size = new System.Drawing.Size(88, 24);
this.button3.TabIndex = 2;
this.button3.Text = "Exit";
this.button3.Click += new System.EventHandler(this.button3_Click);
//
// timer1
//
this.timer1.Enabled = true;
this.timer1.Interval = 250;
this.timer1.Tick += new System.EventHandler(this.timer1_Tick);
//
// Form1
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(328, 325);
this.Controls.Add(this.button1);
this.Controls.Add(this.panel1);
this.Controls.Add(this.pictureBox1);
this.Controls.Add(this.button2);
this.Controls.Add(this.button3);
this.Name = "Form1";
this.Text = "Form1";
this.ResumeLayout(false);
}
#endregion
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.Run(new Form1());
}
private void panel1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
if(cp==null)
return;
for(float y=0;y<16;y++)
for(float x=0;x<16;x++)
{
Color c=Color.Black;
if( ((16*y) + x)<cp.Entries.Length)
c=cp.Entries[(int)((16*y)+x)];
SolidBrush sb=new SolidBrush(Color.FromArgb(255,c));
float w=((float)this.panel1.Width)/16;
float h=((float)this.panel1.Height)/16;
e.Graphics.FillRectangle(sb,w*x,h*y,w,h);
if(c.A!=255)
{
if(showTrans)
e.Graphics.DrawRectangle(Pens.Black,w*x,h*y,w-1,h-1);
else
e.Graphics.DrawRectangle(Pens.White,w*x,h*y,w-1,h-1);
}
sb.Dispose();
}
}
private void panel1_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
{
int y=(int)(((float)e.Y)/(((float)this.panel1.Width)/16f));
int x=(int)(((float)e.X)/(((float)this.panel1.Height)/16f));
CurrentEntry=(int)((16*y)+x);
if(cp!=null)
{
if(CurrentEntry>=cp.Entries.Length)
CurrentEntry=cp.Entries.Length-1;
//Little bit of diagnostic for the palette chooser below
//System.Diagnostics.Trace.WriteLine(string.Format("{0},{1}, adjusted={4},{5} entry={2} Colour={3}",e.X,e.Y,CurrentEntry,cp.Entries[CurrentEntry].ToString(),x,y));
}
}
private void panel1_Click(object sender, System.EventArgs e)
{
//Creates a new GIF image with a modified colour palette
if(cp!=null)
{
//Create a new 8 bit per pixel image
Bitmap bm=new Bitmap(_gifImage.Width,_gifImage.Height,PixelFormat.Format8bppIndexed);
//get it's palette
ColorPalette ncp=bm.Palette;
//copy all the entries from the old palette removing any transparency
int n=0;
foreach(Color c in cp.Entries)
ncp.Entries[n++]=Color.FromArgb(255,c);
//Set the newly selected transparency
ncp.Entries[CurrentEntry]=Color.FromArgb(0,cp.Entries[CurrentEntry]);
//re-insert the palette
bm.Palette=ncp;
//now to copy the actual bitmap data
//lock the source and destination bits
BitmapData src=((Bitmap)_gifImage).LockBits(new Rectangle(0,0,_gifImage.Width,_gifImage.Height),ImageLockMode.ReadOnly,_gifImage.PixelFormat);
BitmapData dst=bm.LockBits(new Rectangle(0,0,bm.Width,bm.Height),ImageLockMode.WriteOnly,bm.PixelFormat);
//uses pointers so we need unsafe code.
//the project is also compiled with /unsafe
unsafe
{
//steps through each pixel
for(int y=0;y<_gifImage.Height;y++)
for(int x=0;x<_gifImage.Width;x++)
{
//transferring the bytes
((byte *)dst.Scan0.ToPointer())[(dst.Stride*y)+x]=((byte *)src.Scan0.ToPointer())[(src.Stride*y)+x];
}
}
//all done, unlock the bitmaps
((Bitmap)_gifImage).UnlockBits(src);
bm.UnlockBits(dst);
//clear out the picturebox
this.pictureBox1.Image=null;
_gifImage.Dispose();
//set the new image in place
_gifImage=bm;
cp=_gifImage.Palette;
this.pictureBox1.Image=_gifImage;
}
}
private void button1_Click(object sender, System.EventArgs e)
{
OpenFileDialog dlg=new OpenFileDialog();
dlg.Filter="GIF files|*.GIF";
if(dlg.ShowDialog()==DialogResult.OK)
{
_gifImage=Image.FromFile(dlg.FileName);
this.pictureBox1.Image=_gifImage;
cp=_gifImage.Palette;
this.panel1.Invalidate();
}
}
private void button2_Click(object sender, System.EventArgs e)
{
SaveFileDialog dlg=new SaveFileDialog();
dlg.Filter="GIF files|*.gif";
dlg.DefaultExt=".gif";
dlg.AddExtension=true;
if(dlg.ShowDialog()==DialogResult.OK)
{
_gifImage.Save(dlg.FileName,ImageFormat.Gif);
}
}
private void button3_Click(object sender, System.EventArgs e)
{
Application.Exit();
}
bool showTrans;
private void timer1_Tick(object sender, System.EventArgs e)
{
showTrans^=true;
Graphics g=this.panel1.CreateGraphics();
//I do this rather than invalidate the panel because
//the panel draws its background ans so flickers horribly.
PaintEventArgs pe=new PaintEventArgs(g,new Rectangle(0,0,this.panel1.Width,this.panel1.Height));
this.panel1_Paint(this,pe);
g.Dispose();
}
}
}
To perform the same task in VB the Marshal class can be used to access the image byte array as shown in the following listing.
Imports System
Imports System.IO
Imports System.Drawing
Imports System.Drawing.Imaging
Imports System.Collections
Imports System.ComponentModel
Imports System.Windows.Forms
Imports System.Data
Imports System.Runtime.InteropServices
Listing 2. TransparentGifCreator.VB
Namespace TransparentGifCreator
'/ <summary>
'/ Summary description for Form1.
'/ </summary>
Public Class Form1
Inherits System.Windows.Forms.Form
Private pictureBox1 As System.Windows.Forms.PictureBox
Private WithEvents panel1 As System.Windows.Forms.Panel
Private WithEvents button1 As System.Windows.Forms.Button
Private WithEvents button2 As System.Windows.Forms.Button
Private WithEvents button3 As System.Windows.Forms.Button
Private components As System.ComponentModel.IContainer
Private _gifImage As Image
Private WithEvents timer1 As System.Windows.Forms.Timer
Private cp As ColorPalette
Private CurrentEntry As Integer
Public Sub New()
'
' Required for Windows Form Designer support
'
InitializeComponent()
End Sub 'New
'
' TODO: Add any constructor code after InitializeComponent call
'
'/ <summary>
'/ Clean up any resources being used.
'/ </summary>
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing Then
If Not (components Is Nothing) Then
components.Dispose()
End If
End If
MyBase.Dispose(disposing)
End Sub 'Dispose
#Region "Windows Form Designer generated code"
'/ <summary>
'/ Required method for Designer support - do not modify
'/ the contents of this method with the code editor.
'/ </summary>
Private Sub InitializeComponent()
Me.components = New System.ComponentModel.Container
Me.pictureBox1 = New System.Windows.Forms.PictureBox
Me.panel1 = New System.Windows.Forms.Panel
Me.button1 = New System.Windows.Forms.Button
Me.button2 = New System.Windows.Forms.Button
Me.button3 = New System.Windows.Forms.Button
Me.timer1 = New System.Windows.Forms.Timer(Me.components)
Me.SuspendLayout()
'
' pictureBox1
'
Me.pictureBox1.Location = New System.Drawing.Point(96, 
Me.pictureBox1.Name = "pictureBox1"
Me.pictureBox1.Size = New System.Drawing.Size(144, 144)
Me.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage
Me.pictureBox1.TabIndex = 0
Me.pictureBox1.TabStop = False
'
' panel1
'
Me.panel1.Location = New System.Drawing.Point(8, 168)
Me.panel1.Name = "panel1"
Me.panel1.Size = New System.Drawing.Size(144, 144)
Me.panel1.TabIndex = 1
'
' button1
'
Me.button1.Location = New System.Drawing.Point(200, 176)
Me.button1.Name = "button1"
Me.button1.Size = New System.Drawing.Size(88, 24)
Me.button1.TabIndex = 2
Me.button1.Text = "Open"
'
' button2
'
Me.button2.Location = New System.Drawing.Point(200, 216)
Me.button2.Name = "button2"
Me.button2.Size = New System.Drawing.Size(88, 24)
Me.button2.TabIndex = 2
Me.button2.Text = "Save"
'
' button3
'
Me.button3.Location = New System.Drawing.Point(200, 256)
Me.button3.Name = "button3"
Me.button3.Size = New System.Drawing.Size(88, 24)
Me.button3.TabIndex = 2
Me.button3.Text = "Exit"
'
' timer1
'
Me.timer1.Enabled = True
Me.timer1.Interval = 250
'
' Form1
'
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Me.ClientSize = New System.Drawing.Size(328, 325)
Me.Controls.Add(button1)
Me.Controls.Add(panel1)
Me.Controls.Add(pictureBox1)
Me.Controls.Add(button2)
Me.Controls.Add(button3)
Me.Name = "Form1"
Me.Text = "Form1"
Me.ResumeLayout(False)
End Sub 'InitializeComponent
#End Region
'/ <summary>
'/ The main entry point for the application.
'/ </summary>
<STAThread()> _
Shared Sub Main()
Application.Run(New Form1)
End Sub 'Main
Private Sub panel1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles panel1.Paint
If cp Is Nothing Then
Return
End If
Dim y As Single
For y = 0 To 15
Dim x As Single
For x = 0 To 15
Dim c As Color = Color.Black
If 16 * y + x < cp.Entries.Length Then
c = cp.Entries(CInt(16 * y + x))
End If
Dim sb As New SolidBrush(Color.FromArgb(255, c))
Dim w As Single = CSng(Me.panel1.Width) / 16
Dim h As Single = CSng(Me.panel1.Height) / 16
e.Graphics.FillRectangle(sb, w * x, h * y, w, h)
If c.A <> 255 Then
If showTrans Then
e.Graphics.DrawRectangle(Pens.Black, w * x, h * y, w - 1, h - 1)
Else
e.Graphics.DrawRectangle(Pens.White, w * x, h * y, w - 1, h - 1)
End If
End If
sb.Dispose()
Next x
Next y
End Sub 'panel1_Paint
Private Sub panel1_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles panel1.MouseMove
Dim y As Integer = CInt(CSng(e.Y) / (CSng(Me.panel1.Width) / 16.0F))
Dim x As Integer = CInt(CSng(e.X) / (CSng(Me.panel1.Height) / 16.0F))
CurrentEntry = CInt(16 * y + x)
If Not (cp Is Nothing) Then
If CurrentEntry >= cp.Entries.Length Then
CurrentEntry = cp.Entries.Length - 1
End If 'Little bit of diagnostic for the palette chooser below
End If 'System.Diagnostics.Trace.WriteLine(string.Format("{0},{1}, adjusted={4},{5} entry={2} Colour={3}",e.X,e.Y,CurrentEntry,cp.Entries[CurrentEntry].ToString(),x,y));
End Sub 'panel1_MouseMove
Private Sub panel1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles panel1.Click
'Creates a new GIF image with a modified colour palette
If Not (cp Is Nothing) Then
'Create a new 8 bit per pixel image
Dim bm As New Bitmap(_gifImage.Width, _gifImage.Height, PixelFormat.Format8bppIndexed)
'get it's palette
Dim ncp As ColorPalette = bm.Palette
'copy all the entries from the old palette removing any transparency
Dim n As Integer = 0
Dim c As Color
For Each c In cp.Entries
ncp.Entries(n) = Color.FromArgb(255, c)
n += 1
Next c
'Set the newly selected transparency
ncp.Entries(CurrentEntry) = Color.FromArgb(0, cp.Entries(CurrentEntry))
're-insert the palette
bm.Palette = ncp
'now to copy the actual bitmap data
'lock the source and destination bits
Dim src As BitmapData = CType(_gifImage, Bitmap).LockBits(New Rectangle(0, 0, _gifImage.Width, _gifImage.Height), ImageLockMode.ReadOnly, _gifImage.PixelFormat)
Dim dst As BitmapData = bm.LockBits(New Rectangle(0, 0, bm.Width, bm.Height), ImageLockMode.WriteOnly, bm.PixelFormat)
If (True) Then
'steps through each pixel
Dim y As Integer
For y = 0 To _gifImage.Height - 1
Dim x As Integer
For x = 0 To _gifImage.Width - 1
'transferring the bytes
Marshal.WriteByte(dst.Scan0, dst.Stride * y + x, Marshal.ReadByte(src.Scan0, src.Stride * y + x))
Next x
Next y
End If
'all done, unlock the bitmaps
CType(_gifImage, Bitmap).UnlockBits(src)
bm.UnlockBits(dst)
'clear out the picturebox
Me.pictureBox1.Image = Nothing
_gifImage.Dispose()
'set the new image in place
_gifImage = bm
cp = _gifImage.Palette
Me.pictureBox1.Image = _gifImage
End If
End Sub 'panel1_Click
Private Sub button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles button1.Click
Dim dlg As New OpenFileDialog
dlg.Filter = "GIF files|*.GIF"
If dlg.ShowDialog() = DialogResult.OK Then
_gifImage = Image.FromFile(dlg.FileName)
Me.pictureBox1.Image = _gifImage
cp = _gifImage.Palette
Me.panel1.Invalidate()
End If
End Sub 'button1_Click
Private Sub button2_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles button2.Click
Dim dlg As New SaveFileDialog
dlg.Filter = "GIF files|*.gif"
dlg.DefaultExt = ".gif"
dlg.AddExtension = True
If dlg.ShowDialog() = DialogResult.OK Then
_gifImage.Save(dlg.FileName, ImageFormat.Gif)
End If
End Sub 'button2_Click
Private Sub button3_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles button3.Click
Application.Exit()
End Sub 'button3_Click
Private showTrans As Boolean
Private Sub timer1_Tick(ByVal sender As Object, ByVal e As System.EventArgs) Handles timer1.Tick
showTrans ^= True
Dim g As Graphics = Me.panel1.CreateGraphics()
'I do this rather than invalidate the panel because
'the panel draws its background ans so flickers horribly.
Dim pe As New PaintEventArgs(g, New Rectangle(0, 0, Me.panel1.Width, Me.panel1.Height))
Me.panel1_Paint(Me, pe)
g.Dispose()
End Sub 'timer1_Tick
End Class 'Form1
End Namespace 'TransparentGifCreator
Figure 2 shows the application in action.
Figure 2: Before and after changing the transparent colour. |