
Creating Transparent GIF Images
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, 8); 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, 8) 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. Copyright Robert W Powell 2003. All rights reserved.
|