Image File Formats: Image Input and Output

2010, Dr. Lawlor, CS 481/681, CS, UAF

Generally, there are two classes of file formats out there: simple but huge, and complicated but small.  The simple formats don't do any real data compression, so they're really easy for programs to read and write, but they don't do any data compression, so they take up a lot of space on disk.  The complicated formats do data compression, so they're really unreasonably difficult to read without a dedicated library, but on the plus side they take much less space on disk.

Typical compressed image formats:
Typical simple image formats:
It's pretty easy to write code that reads these simple image formats from disk, since they're all just some sort of small binary header followed by RGB pixel data; see the examples below.  I can heartily recommend that you never attempt to read or write a real file format like JPEG or PNG without using a library--life is just too short!


Name
Stores
Compression
Advantages
Disadvantages
jpg
JPEG
RGB
DCT+zip (lossy)
Amazingly tight compression, especially for photos.
libjpeg is big, and no alpha channel.  Can cause artifacts on sharp edges.
png
Portable Network Graphics
RGBA
or
color table
zip (lossless)
Good compression for repeating patterns or flat colors, and can represent alpha channel properly.
libpng is big, and not present by default on Windows machines.
gif
Graphics Interchange Format
color table
zip (lossless)
Supported even by ancient code.  Allows animation (like blinking banner ads!)
Can't store 24-bit color images (only 8-bit table at most).
bmp
Windows Bitmap
RGB
None (usually)
Builtin editors on Windows.  Simple format.  Also known as "pcx", which is the same format.
Files are big.  No alpha channel.
tga
TARGA
RGBA
Runlength
Simple format--easy to read.  See C++ ltga library and docs.  Unlike every other format, most tga files store the data from bottom to top, which nicely matches OpenGL's default Y axis.
Not completely standardized.  For example, some targas store un-premultiplied alpha (normal R, G, B, A); others store premultiplied alpha (RA,GA,BA,A).  Files are big.
ppm
Portable Pixel Map
RGB
None
Very simple ASCII header followed by binary data.
ASCII header can include comments.  Files are big.  No alpha channel.

Any decent image editing program, like the GIMP, can read or write all these formats, including the alpha channel or the indexed data formats.

Example Image Output Code

The simplest format, and my personal favorite, is PPM.  The "P6" flavor has a very short ASCII header giving the image size, and then binary RGB data.  NetRun directly supports PPM output (just write a file named "out.ppm"), so this code will show an image in your web browser:
	int wid=200, ht=100;
std::ofstream out("out.ppm",std::ios_base::binary);
out<<"P6\n" // <- ASCII header, then binary RGB data
<<wid<<" "<<ht<<"\n"
<<"255\n"; // maximum channel value is 255 (so 8-bit channels)
for (int y=0;y<ht;y++)
for (int x=0;x<wid;x++) {
unsigned char r=0xff,g=0,b=0; // red
float radius=sqrt(x*x+y*y);
if (radius<60) b=0xff; // purple
out<<r<<g<<b;
}

(Try this in NetRun now!)

From OpenGL, you can dump out a PPM screenshot using glReadPixels anytime before glutSwapBuffers.  This code is also in "ogl/ppm.h":
void savePPM(int start_x,int start_y,int w,int h,char *fname)
{
FILE *f=fopen(fname,"wb");
if (!f) return;
std::vector<unsigned char> out(3*w*h);
glPixelStorei(GL_PACK_ALIGNMENT,1); /* byte aligned output */
glReadPixels(start_x,start_y,w,h, GL_RGB,GL_UNSIGNED_BYTE,&out[0]);
fprintf(f,"P6\n%d %d\n255\n",w,h);
for (int y=0;y<h;y++) { /* flip image bottom-to-top on output */
fwrite(&out[3*(h-1-y)*w],1,3*w,f);
}
fclose(f);
}

Example Image Input Code

Reading images is always harder than writing them--you need to support all the various weird sizes and options that may exist in your input files.

One annoying thing about old graphics hardware (2006 or before): sometimes old hardware only works with power-of-two texture sizes.  This is trivial if you're generating the image, but it's a real pain if you're reading an image off disk: your options are to scale the image, or pad the image and scale your texture coordinates.  Both options are annoyingly tricky to get right, so it's better to just pre-scale your texture image files, or else use modern hardware.

From Jonathan Dummer's SOIL, I extracted "soil/SOIL.h" and friends.  This can read or write BMP, TGA, and DirectX DDS; and read (only) JPEG or PNG.  SOIL includes a tiny stripped-down JPEG decompressor library inside of it; sadly this decompressor is somewhat slower than libjpeg.  SOIL also prefers to clamp texture coordinates by default, rather than wrap like OpenGL's default.  SOIL's interface, though, is very simple, returning you a texture handle; this code works with all the above formats:
#include "soil/SOIL.h" /* Simple OpenGL Image Library, www.lonesock.net/soil.html (plus Dr. Lawlor
's modifications) */
#include "soil/SOIL.c" /* just slap in implementation files here, for easier linking */
#include "soil/stb_image_aug.c"
GLuint load_soil_texture(const char *filename)
{
GLuint tex = SOIL_load_OGL_texture
(
filename,
SOIL_LOAD_AUTO,
SOIL_CREATE_NEW_ID,
SOIL_FLAG_MIPMAPS | SOIL_FLAG_INVERT_Y,
GL_RGBA8
);
glBindTexture(GL_TEXTURE_2D,tex);
return tex;
}
From Nigel Stewart's GLT library, I extracted "ogl/image.h" and "ogl/image.cpp", and added a compact "readTextureFromFile" interface.  It can read or write PPM, BMP, or TGA; and it has some #ifdef GLT_PNG code to read or write PNG via libpng.
#include "ogl/image.h"
#include "ogl/image.cpp"
...
glBindTexture(GL_TEXTURE_2D,sometex);
readTextureFromFile("cat.ppm");

From libjpeg, you need a bunch of steps to set up a JPEG decompressor:
#include <jpeglib.h> /* needs -ljpeg, see http://www.ijg.org/ */

// Read this JPEG file into the current OpenGL texture.
bool read_jpeg(const char *filename)
{
// Open file and set up decompressor:
FILE *f=fopen(filename,"rb");
if (f==NULL) return false; // couldn't open file.
struct jpeg_error_mgr jerr;
struct jpeg_decompress_struct cinfo;
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_decompress(&cinfo);
jpeg_stdio_src(&cinfo, f);
jpeg_read_header(&cinfo, TRUE);

// Now we know the image size: allocate buffer
int w=cinfo.image_width, h=cinfo.image_height;
int bands=cinfo.num_components;
std::vector<unsigned char> buf(3*w*h); /* whole RGB buffer of data */

// Copy data out of JPEG file, row-by-row:
jpeg_start_decompress(&cinfo);
for (int y=0;y<h;y++) {
JSAMPLE *rows[1]; rows[0]=&buf[3*w*(h-1-y)];
jpeg_read_scanlines(&cinfo,rows,1);
}
jpeg_destroy_decompress(&cinfo);
fclose(f);

// Finally upload the read-in data
gluBuild2DMipmaps(GL_TEXTURE_2D,GL_RGBA8,
w,h, GL_RGB,GL_UNSIGNED_BYTE,&buf[0]);
return true;
}
Yes, this is a pain, but trust me--reading JPEG yourself is a whole lot worse!