Home » Community » Forums » » How to Load a Bitmap
  Intel sponsors gamedev.net search:   
[Control Panel] [Register] [Bookmarks] [Who's Online] [Active Topics] [Stats] [FAQ] [Search]

Add Forum to Favorites |  Send Topic To a Friend | View Forum FAQ | Track this topic

Page:   1 2 »»

 Last Thread Next Thread 
 How to Load a Bitmap
Post Reply 
Nice reference, but I'd like to see it more x-platform. For example, knock out the windows.h and substitute the appropriate types instead of DWORD etc

Good work!

:D

[edited by - voodoo_john on July 23, 2003 1:23:21 PM]

 User Rating: 1027   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

Nice article indeed. Just when I needed one Thanks.

.lick


 User Rating: 1469   |  Rate This User  Send Private MessageView ProfileView Journal Report this Post to a Moderator | Link

it isn't hard to look up what a DWORD really is... i wrote my own bitmap-loading structs in about 2 minutes (from specs i found on the internet), to avoid platform dependencies ("windows.h").

 User Rating: 1214   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

quote:
Original post by krez
it isn't hard to look up what a DWORD really is... i wrote my own bitmap-loading structs in about 2 minutes (from specs i found on the internet), to avoid platform dependencies ("windows.h").


Well good for you. I was meaning as a reference it would be nice to see it platform neutral.

 User Rating: 1027   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

quote:
Original post by voodoo_john
Nice reference, but I'd like to see it more x-platform. For example, knock out the windows.h and substitute the appropriate types instead of DWORD etc



More x-platform ?

#include <SDL/SDL-image.h>

And things will be MUCH easier for you !

BTW, who needs another BMP loader ?

 User Rating: 1022   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

Those that want to know how things work rather than use something blindly.

SDL_image is crap, code your own routines and learn something.

 User Rating: 1027   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

Many bitmap files are RLE compressed, but this loader doesn't deal with it.

Also, the structs have no packing information; depending on compiler vendor, or even compiler version or settings/switches, those structs may or may not work without extra code (i e, __attrib__(align) or #pragma pack or whatever).

Also, if I want a quick-and-dirty loader, I use TGA: it's 9 "short" values, and item 6 and 7 are width/height. If you define that you only support 32-bit data, it's all cake from there.

unsigned short header[9];
FILE * f = fopen("file.tga", "rb";
fread( header, 2, 9, f );
unsigned long * data = new unsigned long[ header[6]*header[7] ];
fread( data, 4*header[6], header[7], f );
fclose(f);
glTexImage2D( blah, header[6], header[7], blah, data );
delete[] data;
 


Of course, well factored code doesn't do the texture upload and the file reading in the same spot -- you'd want to be able to load DDS files into OpenGL, and to load TGA files into DirectX, without re-writing all of it, you know.

 User Rating: 1015    Report this Post to a Moderator | Link

Excellent...this helped me alot. Although im still not sure of how to make my own "Loading Image" code but since im in the need of one for testing things this will do...I'm not doing anything great but if I do i'll be sure to add Mark Bernard (aka Captain Jester) to the credits.

 User Rating: 1015   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

quote:
Original post by voodoo_john
Those that want to know how things work rather than use something blindly.



OK, if I wanna know how things work, I'll just find the specifications for the BMP file format and try to write my own loader. You can stare at someone else's code and think "Hey, cool !", but that will not improve your skills.

And if I wanna things to be done, I'll reuse existing code whenever possible.

quote:
Original post by voodoo_john
SDL_image is crap, code your own routines and learn something.


Could you find at least one reason why this piece of code is better than the BMP loader included in the SDL library (no need for SDL_image for BMP, but SDL_image provides readers for other formats) ?
- SDL can read all flavours of BMP, this piece of code doesn't.
- SDL separates correctly the "read" and "decode" parts so you can read a BMP structure from a file or from memory (or network, ZIP files if you write the appropriate SDL_RWops wrappers). This loader doesn't.

 User Rating: 1022   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

Thanks for all the feedback guys.

To answer some of the comments:

This was intended as an article for beginners. I see it asked hundreds times by beginners on how to load a bitmap and a lot of people say search the forums. Well, to make it easier, they can just look at the article in the reference section instead.

To all the people that said "I'll just look at the specs and write my own." That is what I did and I applaud you. However, it doesn't come that easily to some people, especially beginners, so it is nice to have some code to see how it works.

To the anonymous poster, while RLE is in the spec, I have never seen a bitmap that has actually used it. So I decided not to put it in the article to keep it simple. That is why I put in the conclusion that I left the less used parts of the spec out.

To the same anonymous poster, you are right on the struct alignment issue. To be honest, I didn't even think about that. However, if you include the windows.h header outside any pack instruction, then there should be no problem.

About the cross platform issure. I agree, I should have taken the time to break down the structures into a more generic form. But since this is my first article, I will chalk that up to experience.

Thanks again for all the comments. Keep them coming.



First make it work,
then make it fast.

--Brian Kernighan

"I’m happy to share what I can, because I’m in it for the love of programming. The Ferraris are just gravy, honest!" --John Carmack: Forward to Graphics Programming Black Book

 User Rating: 1279   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

Very good,but I'd like to something which are base on X platform.

 User Rating: 1015    Report this Post to a Moderator | Link

For those looking for a more cross platform solution, remove #include <windows.h> and add the following to the front of the header file:

//File information header

//provides general information about the file

typedef struct tagBITMAPFILEHEADER { 
	unsigned short	bfType; 
	unsigned long	bfSize; 
	unsigned short	bfReserved1; 
	unsigned short	bfReserved2; 
	unsigned long	bfOffBits; 
} BITMAPFILEHEADER, *PBITMAPFILEHEADER;

//Bitmap information header

//provides information specific to the image data

typedef struct tagBITMAPINFOHEADER{
	unsigned long	biSize; 
	long		biWidth; 
	long		biHeight; 
	unsigned short	biPlanes; 
	unsigned short	biBitCount; 
	unsigned long	biCompression; 
	unsigned long	biSizeImage; 
	long		biXPelsPerMeter; 
	long		biYPelsPerMeter; 
	unsigned long	biClrUsed; 
	unsigned long	biClrImportant; 
} BITMAPINFOHEADER, *PBITMAPINFOHEADER;

//Colour palette

typedef struct tagRGBQUAD {
	unsigned char	rgbBlue; 
	unsigned char	rgbGreen; 
	unsigned char	rgbRed; 
	unsigned char	rgbReserved; 
} RGBQUAD;




First make it work,
then make it fast.

--Brian Kernighan

"I’m happy to share what I can, because I’m in it for the love of programming. The Ferraris are just gravy, honest!" --John Carmack: Forward to Graphics Programming Black Book

 User Rating: 1279   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

Those structs have exactly the same alignment problems -- only worse, as they won't even align right on Windows, much less on GCC/Linux, or MacOS.

The best way to parse a header you read from disk, is to read it as a block of bytes, and doing explicit bit shifting yourself. That way, you get size right on all compilers, and you get byte order right on all architectures!

// assume little-endian storage on disk
struct NotAligned {
  unsigned short a;
  unsigned long b;
};

char buf[ sizeof( NotAligned ) ];
fread( buf, 1, sizeof( NotAligned ), f );
NotAligned out;
out.a = buf[0] | (buf[1]<<8);
out.b = buf[2] | (buf[3]<<8) | (buf[4]<<16) | (buf[5]<<24);
 



 User Rating: 1015    Report this Post to a Moderator | Link

I can't argue with on the alignment issue. As I said earlier, I didn't even think of that. However, they are not worse that the original ones, because they are the original ones with the typedefs removed for BYTE, WORD, DWORD and LONG.



First make it work,
then make it fast.

--Brian Kernighan

"I’m happy to share what I can, because I’m in it for the love of programming. The Ferraris are just gravy, honest!" --John Carmack: Forward to Graphics Programming Black Book

 User Rating: 1279   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

Thanks for the reference. Very handy!

 User Rating: 1023   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

I'm working on a bitmap library, and I wrote a save bitmap function. The problem is the save function doesn't work. Any ideas, code, ect?

 User Rating: 1015   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

int j==size-3;
offset=padWidth-byteWidth;
for(int i=0;i<size;i+=3) {
   if((i+1)%padWidth==0) {
        i+=offset;
   }

   *(pixelData+j+2)=*(tempPixelData+i);
   *(pixelData+j+1)=*(tempPixelData+i+1);
   *(pixelData+j)=*(tempPixelData+i+2);
   j++;
}
 

Isn't there something wrong with the j++? You will go past the boundaries of pixelData[size]. Also don't you want (i + 1) % byteWidth == 0?


 User Rating: 1015    Report this Post to a Moderator | Link

This all looks good, and is an excellent starting point for someone like me. But I have a few questions:

diff=width*height*RGB_BYTE_SIZE;
//allocate the buffer for the final image data
data=new char[diff];

The height might be negative right? So couldn't this produce a negative result, which you're then using to allocate memory, and wouldn't this be bad?

Finally, what's all the talk about struct alignment? Is that making sure the data you read from the disk is put into the appropriate members of the struct? For instance we are assuming that a DWORD (unsigned long int) is 4 bytes.

Thanks for the help.

[edited by - random_acts on August 12, 2003 12:20:50 PM]

 User Rating: 1029   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

AP: Yes, that does not look right. I wrote the article when and then found the code didn't work right, so I fixed the code and then tried to correct the article. That is something that I may have missed.

random_acts: That slipped through the cracks. In my testing, I didn't run across a bitmap file with a negative height. The struct alignment issue has to do with how the compiler packs the data. It may pack variables it on a 1 byte boundary, or 2 byte or 4 byte. Try this little test program and you will see what I mean.
//change pack(1) to 2 and 4 to see the difference

//different architectures may react differently as well.

#pragma pack(1)

#include <iostream>
using namespace std;

struct test {
	int a;
	int b;
	char c;
};

int main() {
	test t;

	t.a=0;
	cout << "size=" << sizeof(t) << endl;
	return 0;
}


Thanks guys, I will make those corrections.



First make it work,
then make it fast.

--Brian Kernighan

"I’m happy to share what I can, because I’m in it for the love of programming. The Ferraris are just gravy, honest!" --John Carmack: Forward to Graphics Programming Black Book

 User Rating: 1279   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

Hi there, really great tutorial, I did appreciate the code. I spent a lot of time checking out the BMP format, and made some changes to your code. I thought I would put it up for further comments, and hopefully help some others out.

I expect that everyone uses a kind of BMP 'manager' object, when loading files. Thus, the Bitmap class as it stands is only good for one file at a time, particularly with the cout statements. I changed it so that you / manager object pass an ostream& into the object, with the filename, so that if you choose, you can save all the information to a .txt file of your BMP loads, instead of couting to the screen. I also changed the reset() function to init(), and added a reset() of my own, which is just re-organizing what was already written. But I think it's more intuitive. I also changed the log output to follow the BMP spec order. I also did a lot of re-formatting and / tidying up of identifyiers / comments. Hope it's useful, and let me know what you think. And thanks again Mark for starting me off.

Here is the header :


#ifndef _BITMAP_H
#define _BITMAP_H

// Original code by Mark Bernard ( e-mail: mark.bernard@rogers.com )

// modified by David Henry ( e-mail: strad1238@hotmail.com )

// Copyright 2003



#include <iostream>
#include <cstdio>
#include <string>

using namespace std;

const short BITMAP_MAGIC_NUMBER = 19778;
const int RGB_BYTE_SIZE = 3;

#pragma pack(push, bitmap_data, 1)

// these structures make the class less platform dependent

//------------------------------------------

typedef struct tagRGBQuad 
{
	char rgbBlue;
	char rgbGreen;
	char rgbRed;
	char rgbReserved;
} RGBQuad;


//------------------------------------------

typedef struct tagBitmapFileHeader 
{
	unsigned short bfType;
	unsigned int bfSize;
	unsigned short bfReserved1;
	unsigned short bfReserved2;
	unsigned int bfOffBits;
} BitmapFileHeader;


//------------------------------------------

typedef struct tagBitmapInfoHeader 
{
	unsigned int biSize;
	int biWidth;
	int biHeight;
	unsigned short biPlanes;
	unsigned short biBitCount;
	unsigned int biCompression;
	unsigned int biSizeImage;
	int biXPelsPerMeter;
	int biYPelsPerMeter;
	unsigned int biClrUsed;
	unsigned int biClrImportant;
} BitmapInfoHeader;


//------------------------------------------


#pragma pack(pop, bitmap_data)

//--------------------------------------------------------------------------------------

class Bitmap 
{
public:
    Bitmap();
    Bitmap(char *fileName, ostream& logFile);
    ~Bitmap();

    bool loadBMP(char * fileName, ostream& logFile);

    RGBQuad *colours;						// the color pallete for 8-bit bitmaps

    char *data;								// Here is the BMP data

    bool loaded;							// loaded or not

    int width;								// width and height

	int height;	
    unsigned short bpp;						// bits per pixel

    string error;							// a field for error message


private:
	void init();							// initializes the main data fields

    void reset();							// resets all data in object

    bool convert24(char *tempData);			// convert to 24bit RGB bottom up data

    bool convert8(char *tempData);			// convert to 24bit RGB bottom up data

	void writeLog(ostream& OS, char *fileName);		// write log to file


    BitmapFileHeader bmfh;					// file header

    BitmapInfoHeader bmih;					// information header

    int byteWidth;							// the width in bytes of the image

    int padWidth;							// the width in bytes of the padded image

    unsigned int dataSize;					// size of the data in the file

};

//--------------------------------------------------------------------------------------



#endif //_BITMAP_H




and here is the .cpp :


// Original code by Mark Bernard ( e-mail: mark.bernard@rogers.com )

// modified by David Henry ( e-mail: strad1238@hotmail.com )

// Copyright 2003


#include "Bitmap.h"


////////////////////////////////////////////////////////////////////////////////////

Bitmap::Bitmap()					// basic constructor

{
    init();
}


////////////////////////////////////////////////////////////////////////////////////

Bitmap::Bitmap(char *file, ostream& logFile)	// constructor loads the bitmap

{
	init();
    loadBMP(file, logFile);
}


////////////////////////////////////////////////////////////////////////////////////

Bitmap::~Bitmap()					// destructor

{
    reset();
}


////////////////////////////////////////////////////////////////////////////////////

bool Bitmap::loadBMP(char *fileName, ostream& logFile) 	// loads a bitmap into memory

{
	FILE *in;					    // file stream for reading

    char *tempData;					// temp storage for image data

    int numColours;					// total available colours


    loaded = false;					// bitmap is not loaded yet

    
    in = fopen(fileName, "rb");			// open the file for reading in binary mode


    if(in == NULL) 					// if the file does not exist return in error

    {
		error = "File not found";
        fclose(in);
        return false;
    }


	//==========	read in the BITMAPFILEHEADER      ==========//


    fread(&bmfh, sizeof(BitmapFileHeader), 1, in);
  
    if(bmfh.bfType != BITMAP_MAGIC_NUMBER) 	// check magic number that says this is a bitmap

	{
		error = "File is not in DIB format";
        fclose(in);
        return false;
    }

	//==========     read in the BITMAPINFOHEADER     =========//


    fread(&bmih, sizeof(BitmapInfoHeader), 1, in);

	writeLog(logFile, fileName);			// write data to file



    width = bmih.biWidth;					// set object data fields

    height = bmih.biHeight;
    bpp = bmih.biBitCount;

    if(bpp < 8) 							// test for correct bit-per-pixel depth

    {
		error = "File is not 8 or 24 bits per pixel";
        fclose(in);
        return false;
    }
											// calculate the size of image data + padding

    dataSize = (width * height * (unsigned int)(bpp / 8.0) );

    if(bpp == 8) 							// load the color palette for 8 bits per pixel

	{
		numColours = 1 << bpp;				// calculate the number of available colours

		colours = new RGBQuad[numColours];					
    	fread(colours, sizeof(RGBQuad), numColours, in);	// read in the color palette

    }

    tempData = new char[dataSize];			// set up the temporary buffer for the image data


    if(tempData == NULL) 					// exit if there is not enough memory

	{
		error = "Not enough memory to allocate a temporary buffer";
        fclose(in);
        return false;
    }

    fread(tempData, sizeof(char), dataSize, in);	// read in the entire image


    fclose(in);								// close the file now that we have all the info


							// set both values to the byte total of the file, without padding

    byteWidth = padWidth = (int)( (float)width * (float)bpp / 8.0 );

    while(padWidth % 4 != 0) 				// adjust the width for padding as necessary

	{
		padWidth++;
    }

    if(bpp == 8) 							// change format from GBR to RGB

    {
		loaded = convert8(tempData);
   	}
    else if(bpp == 24) 
	{
    	loaded = convert24(tempData);
   	}

    delete [] tempData;					// clean up memory


    error = "Bitmap loaded";			// bitmap is now loaded


    return loaded;						// return success

}


////////////////////////////////////////////////////////////////////////////////////

void Bitmap::init(void) 				// function to set the inital values

{
	loaded = false;
    colours = 0;
    data = 0;
    error = "";
}


////////////////////////////////////////////////////////////////////////////////////

void Bitmap::reset(void)
{
    if(colours)							// clear the color pallete if required

	{	
        delete [] colours;
    }

    if(data)							// clear the data if required

	{
        delete [] data;
    }

	loaded = false;
    error = "";
}


////////////////////////////////////////////////////////////////////////////////////

bool Bitmap::convert24(char* tempData) 
{
	int offset;							// padding between data

	int dataSize;						// the size of the bitmap data


	dataSize = abs(width * height * RGB_BYTE_SIZE);
    
    data = new char[dataSize];			// allocate the buffer for the final image data


    if(data == NULL) 					// exit if there is not enough memory

    {
		error = "Not enough memory to allocate an image buffer";
        delete [] data;
        return false;
    }

    if(height > 0) 
	{
        offset = padWidth - byteWidth;			// find the difference

        
        for(int i = 0; i < dataSize; i += 3)	// count backwards until at front of image

        {
            if((i+1) % padWidth == 0)			// jump over the padding

			{
				i += offset;
            }
    
            *(data+i+2) = *(tempData+i);        // transfer the data in reverse order

            *(data+i+1) = *(tempData+i+1);		// in 3-color blocks of RGB

            *(data+i) = *(tempData+i+2);
        }
    }

    else										// image parser for a forward image

	{
        offset = padWidth - byteWidth;
        int j = dataSize - 3;

        for(int i = 0; i < dataSize; i += 3, j -= 3) 
		{
            if((i+1) % padWidth == 0)			// jump over padding

			{
                i += offset;
            }

            *(data+j+2) = *(tempData+i);		// transfer the data ||NOTE||

            *(data+j+1) = *(tempData+i+1);		// that final data is stored backwards now

            *(data+j) = *(tempData+i+2);		// due to the difference of i and j

        }
    }

    return true;
}


////////////////////////////////////////////////////////////////////////////////////

bool Bitmap::convert8(char* tempData) 
{
	int offset;							// padding between data

	int dataSize;						// the size of the bitmap data


	dataSize = abs(width * height * RGB_BYTE_SIZE);		// total memory required

    
    data = new char[dataSize];			// allocate the buffer for the final image data


    if(data == NULL)					// exit if there is not enough memory

	{
        error = "Not enough memory to allocate an image buffer";
        delete [] data;
        return false;
    }

    if(height > 0) 
	{
        offset = padWidth - byteWidth;
        int j = 0;
										//count backwards so you start at the front of the image

        for(int i = 0; i < dataSize * RGB_BYTE_SIZE; i += 3, j++) 
		{
            if((i+1) % padWidth == 0)						// jump over padding

			{
                i += offset;
            }

            *(data+i) = colours[*(tempData+j)].rgbRed;		// transfer the data

            *(data+i+1) = colours[*(tempData+j)].rgbGreen;
            *(data+i+2) = colours[*(tempData+j)].rgbBlue;
        }
    }

    else										// image parser for a forward image

	{
        offset = padWidth - byteWidth;
        int j = dataSize - 1;

										//count backwards until start at front of image

        for(int i = 0; i < dataSize * RGB_BYTE_SIZE; i += 3, j--) 
		{ 
            if((i+1) % padWidth == 0)						// jump over padding

			{
                i += offset;
            }

            *(data+i) = colours[*(tempData+j)].rgbRed;		// transfer the data,

            *(data+i+1) = colours[*(tempData+j)].rgbGreen;	// this time getting it

            *(data+i+2) = colours[*(tempData+j)].rgbBlue;	// from the color table

        }
    }
    return true;
}


////////////////////////////////////////////////////////////////////////////////////

void Bitmap::writeLog(ostream& OS, char* fileName)				// write log to file

{
	OS << "name of file    = " << fileName << endl;
	OS << "FileHeader Size = " << sizeof(BitmapFileHeader) << endl;
	OS << "InfoHeader Size = " << sizeof(BitmapInfoHeader) << endl;
	OS << "Size            = " << bmih.biSize << endl;
	OS << "Width           = " << bmih.biWidth << endl;
	OS << "Height          = " << bmih.biHeight << endl;
	OS << "Planes          = " << bmih.biPlanes << endl;
	OS << "BitCount        = " << bmih.biBitCount << endl;
	OS << "Compression     = " << bmih.biCompression << endl;
	OS << "ClrImportant    = " << bmih.biClrImportant << endl;
	OS << "ClrUsed         = " << bmih.biClrUsed << endl;
	OS << "SizeImage       = " << bmih.biSizeImage << endl;
	OS << "PixelsPerMeter  = " << bmih.biXPelsPerMeter << endl;
	OS << "PixelsPerMeter  = " << bmih.biYPelsPerMeter << endl;
	OS << endl;
}



cheers all.

 User Rating: 1078   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link

--------------------Configuration: Bitmap - Win32 Debug--------------------
Linking...
LIBCD.lib(crt0.obj) : error LNK2001: unresolved external symbol _main
Debug/Bitmap.exe : fatal error LNK1120: 1 unresolved externals
Error executing link.exe.

Bitmap.exe - 2 error(s), 0 warning(s)



------> Why the program has two errors ??when i build the program.(How to Load a Bitmap )-_-;

 User Rating: 1015    Report this Post to a Moderator | Link

--------------------Configuration: Bitmap - Win32 Debug--------------------
Linking...
LIBCD.lib(crt0.obj) : error LNK2001: unresolved external symbol _main
Debug/Bitmap.exe : fatal error LNK1120: 1 unresolved externals
Error executing link.exe.

Bitmap.exe - 2 error(s), 0 warning(s)



------> Why the program has two errors ??when i build the program.(How to Load a Bitmap )-_-;

 User Rating: 1015    Report this Post to a Moderator | Link

It doesn't compile like that because there is no main function/method. Add this to the Bitmap.cpp file:
int main(int argc, char* argv[]) {
	
	if(argc < 2) {

    		printf("Usage: %s bmpInput.bmp\n", argv[0]);

    		exit(0);

  	}	


	//pull the file name off the command line
	char *fileName = argv[1];
	//re-direct ostream to cout
	std::ostream& my_cout = cout;
	//read the given bmp
	Bitmap(fileName,my_cout);
}


It runs just fine for me now on Linux Whitebox using the g++ compiler.
g++ -c Bitmap.cpp
g++ -o fileName Bitmap.o
./fileName imageFileName.bmp

 User Rating: 1015    Report this Post to a Moderator | Link

HELP

I've been struglin for 2 days now on how to get the results of this article into a HBITMAP to load into a DX Surface.

I cannot even get the char* bitmap data into a HBITMAP to be used with BitBlt.

 User Rating: 1070   |  Rate This User  Send Private MessageView Profile Report this Post to a Moderator | Link
Page:   1 2 »»
All times are ET (US)

Post Reply
 Last Thread Next Thread 
Forum Rules:
You may not post new threads
You may post replies
You may not edit your posts
You may not use HTML in your posts
Jump To:
Administrative Options: