This article is a follow up to my previous article on black and white graphics programming with libtiff. This article covers grayscale and color imaging, but it assumes that you have read and understand the code from the black and white article. It might be helpful to refer to that article before going too far here.
Here I'll discuss some of the theory required to understand how the image data is stored for color and grayscale. This theory applies to all imaging formats. I'll then discuss some of the specifics of using libtiff.
For color images, we need to store even more information. For each pixel we will need to store a red, green, and blue value. Each of these values is stored in a separate "sample." Therefore, we will need to define TIFFTAG_SAMPLESPERPIXEL. This will be 1 for black and white or grayscale, but will normally be 3 for color images. We also need to define the size of each sample, so we'll still need to set a value for TIFFTAG_BITSPERSAMPLE.
Thankfully, there is a more efficient way to store image data. Imagine a simple four color, 24 bit per pixel image. If we build a lookup table of the four color values (the 24 bit values which represent those colors), then we just need to store the relevant entry number of the color in the image strip itself. This can be done in only two bits, instead of the full 24.
The math looks something like this: A 24 bit color image that is 1,000 by 1,000 pixels will take 24 million bits to store. The same image, if it was a four color image, would take 4 million bits for the strip data, and 98 bits for the color table. Neither of these numbers includes header and footer information for the file format, and the numbers are for uncompressed bitmaps. The advantages of the lookup table should be obvious. There is a name for this style of lookup table: it is called a palette, probably because of those things painters carry around.
This concept works for grayscale images as well. The only difference is that the "colors" in the palette are just shades of gray.
Why does the loss in lossy compression algorithms such as JPEG accumulate? Imagine that you compress an image using JPEG. You then need to add, say, a barcode to the image, so you uncompress the image, add the barcode, and recompress it. When the recompression occurs, a new set of loss is introduced. You can imagine that if you do this enough, then you'll end up with an image which is a big blob.
Whether this is a problem depends on the type of your data. To test how much of a problem this is, I wrote a simple libtiff program that repeatedly uncompresses and recompresses an image. What I found was that with pictures, the data is much more resilient to repeated compression.
The code I used had a "quality" rating of 25% on the JPEG compression, which is a way of tweaking the loss of the compression algorithm. The lower the quality, the higher the compression ratio. The default is 75%.
Writing a color image
#include <tiffio.h>
#include <stdio.h>
int main(int argc, char *argv[]){
TIFF *output;
uint32 width, height;
char *raster;
// Open the output image
if((output = TIFFOpen("output.tif", "w")) == NULL){
fprintf(stderr, "Could not open outgoing image\n");
exit(42);
}
// We need to know the width and the height before we can malloc
width = 42;
height = 42;
if((raster = (char *) malloc(sizeof(char) * width * height * 3)) == NULL){
fprintf(stderr, "Could not allocate enough memory\n");
exit(42);
}
// Magical stuff for creating the image
// ...
// Write the tiff tags to the file
TIFFSetField(output, TIFFTAG_IMAGEWIDTH, width);
TIFFSetField(output, TIFFTAG_IMAGELENGTH, height);
TIFFSetField(output, TIFFTAG_COMPRESSION, COMPRESSION_DEFLATE);
TIFFSetField(output, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
TIFFSetField(output, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB);
TIFFSetField(output, TIFFTAG_BITSPERSAMPLE, 8);
TIFFSetField(output, TIFFTAG_SAMPLESPERPIXEL, 3);
// Actually write the image
if(TIFFWriteEncodedStrip(output, 0, raster, width * height * 3) == 0){
fprintf(stderr, "Could not write image\n");
exit(42);
}
TIFFClose(output);
}
You can see from this code some of the things we've discussed in theory. The image has three samples per pixel, each of eight bits. This means that the image is a 24 bit RGB image. If this was a black and white or grayscale image, then this value would be one. The tag PHOTOMETRIC_RGB says that the image data is stored within the strips themselves (as opposed to being paletted) -- more about this in a minute.
In my example, I have three samples per pixel. If this was a black and white image, or a grayscale image, then we would have one sample per pixel. There are other valid values as well; for instance, sometimes people will store a transparency value for a given pixel, an alpha channel. This would result in having four samples per pixel. It is possible to have an arbitary number of samples per pixel, which is good if you need to pack in extra information about a pixel. Note that doing this can break image viewers that make silly assumptions -- I once had to write code for a former employer to strip out alpha channels and the like so that their PDF generator wouldn't crash.
The other interesting thing to discuss here is the planar configuration of the image. Here I've specifed PLANARCONFIG_CONTIG, which means that the red green and blue information for a given pixel is grouped together in the strips of image data. The other option is PLANARCONFIG_SEPARATE, where the red samples for the image are stored together, then the blue samples, and finally the green samples.
TIFFReadRGBAStrip reads a single strip of a strip-based image into memory,
storing the result in the user supplied RGBA raster. The raster is
assumed to be an array of width times rowsperstrip 32-bit entries,
where width is the width of the image (TIFFTAG_IMAGEWIDTH) and
rowsperstrip is the maximum lines in a strip (TIFFTAG_ROWSPERSTRIP).
The strip value should be the strip number (strip zero is the
first) as returned by the TIFFComputeStrip function, but always for sample 0.
Note that the raster is assume to be organized such that the pixel
at location (x,y) is raster[y*width+x]; with the raster origin in the
lower-left hand corner of the strip. That is bottom to top organization.
When reading a partial last strip in the file the last line of the image
will begin at the beginning of the buffer.
Raster pixels are 8-bit packed red, green, blue, alpha samples. The
macros TIFFGetR, TIFFGetG, TIFFGetB, and TIFFGetA should be used to
access individual samples. Images without Associated Alpha matting
information have a constant Alpha of 1.0 (255).
See the TIFFRGBAImage(3T) page for more details on how various image types
are converted to RGBA values.
NOTES
Samples must be either 1, 2, 4, 8, or 16 bits. Colorimetric samples/pixel
must be either 1, 3, or 4 (i.e. SamplesPerPixel minus ExtraSamples).
Palette image colormaps that appear to be incorrectly written as 8-bit
values are automatically scaled to 16-bits.
TIFFReadRGBAStrip is just a wrapper around the more general
TIFFRGBAImage(3T) facilities. It's main advantage over the similar
TIFFReadRGBAImage() function is that for large images a single buffer
capable of holding the whole image doesn't need to be allocated, only
enough for one strip. The TIFFReadRGBATile() function does a similar
operation for tiled images.
There are a couple of odd things about this function. First, it defines (0, 0) to be in a different location than all the other code that we have been writing. In the previous code, the (0, 0) point has been in the top left of the image. This call defines (0, 0) to be in the bottom left. The other limitation to be aware of is that not all valid values for bits per sample are supported. If you find these quirks unacceptable, then remember that you can still use TIFFReadEncodedStrip() in the same manner that I did for the black and white images in the previous article.
Reading a color image with TIFFReadEncodedStrip()
#include <stdio.h>
#include <tiffio.h>
int main(int argc, char *argv[]){
TIFF *image;
uint32 width, height, *raster;
tsize_t stripSize;
unsigned long imagesize, c, d, e;
// Open the TIFF image
if((image = TIFFOpen(argv[1], "r")) == NULL){
fprintf(stderr, "Could not open incoming image\n");
exit(42);
}
// Find the width and height of the image
TIFFGetField(image, TIFFTAG_IMAGEWIDTH, &width);
TIFFGetField(image, TIFFTAG_IMAGELENGTH, &height);
imagesize = height * width + 1;
if((raster = (uint32 *) malloc(sizeof(uint32) * imagesize)) == NULL){
fprintf(stderr, "Could not allocate enough memory\n");
exit(42);
}
// Read the image into the memory buffer
if(TIFFReadRGBAStrip(image, 0, raster) == 0){
fprintf(stderr, "Could not read image\n");
exit(42);
}
// Here I fix the reversal of the image (vertically) and show you how to get the color values from each pixel
d = 0;
for(e = height - 1; e != -1; e--){
for(c = 0; c < width; c++){
// Red = TIFFGetR(raster[e * width + c]);
// Green = TIFFGetG(raster[e * width + c]);
// Blue = TIFFGetB(raster[e * width + c]);
}
}
free(raster);
TIFFClose(image);
}
The example I am most familiar with is PDF documents. In PDF files, you can embed images into the document. These images can be in a subset of TIFF if desired, and TIFF is quite clearly the choice for black and white images.
If you need more information about hooking the file input and output functions within libtiff, have a look at the images.c file in Panda, my PDF library. The Web pages for Panda can be found in Resources.
Libtiff allows you to replace the file input and output functions in the library with your own. This is done with the TIFFClientOpen() method. Here's an example (please note this code won't compile, and is shown only to describe the main concepts:
Using TIFFClientOpen
#include <tiffio.h>
#include <pthread.h>
// Function prototypes
static tsize_t libtiffDummyReadProc (thandle_t fd, tdata_t buf, tsize_t size);
static tsize_t libtiffDummyWriteProc (thandle_t fd, tdata_t buf, tsize_t size);
static toff_t libtiffDummySeekProc (thandle_t fd, toff_t off, int i);
static int libtiffDummyCloseProc (thandle_t fd);
// We need globals because of the callbacks (they don't allow us to pass state)
char *globalImageBuffer;
unsigned long globalImageBufferOffset;
// This mutex keeps the globals safe by ensuring only one user at a time
pthread_mutex_t convMutex = PTHREAD_MUTEX_INITIALIZER;
...
TIFF *conv;
// Lock the mutex
pthread_mutex_lock (&convMutex);
globalImageBuffer = NULL;
globalImageBufferOffset = 0;
// Open the dummy document (which actually only exists in memory)
conv = TIFFClientOpen ("dummy", "w", (thandle_t) - 1, libtiffDummyReadProc,
libtiffDummyWriteProc, libtiffDummySeekProc,
libtiffDummyCloseProc, NULL, NULL, NULL);
// Setup the image as if it was any other tiff image here, including setting tags
...
// Actually do the client open
TIFFWriteEncodedStrip (conv, 0, stripBuffer, imageOffset);
// Unlock the mutex
pthread_mutex_unlock (&convMutex);
...
/////////////////// Callbacks to libtiff
...
static tsize_t
libtiffDummyReadProc (thandle_t fd, tdata_t buf, tsize_t size)
{
// Return the amount of data read, which we will always set as 0 because
// we only need to be able to write to these in-memory tiffs
return 0;
}
static tsize_t
libtiffDummyWriteProc (thandle_t fd, tdata_t buf, tsize_t size)
{
// libtiff will try to write an 8 byte header into the tiff file. We need
// to ignore this because PDF does not use it...
if ((size == 8) && (((char *) buf)[0] == 'I') && (((char *) buf)[1] == 'I')
&& (((char *) buf)[2] == 42))
{
// Skip the header -- little endian
}
else if ((size == 8) && (((char *) buf)[0] == 'M') &&
(((char *) buf)[1] == 'M') && (((char *) buf)[2] == 42))
{
// Skip the header -- big endian
}
else
{
// Have we done anything yet?
if (globalImageBuffer == NULL)
if((globalImageBuffer = (char *) malloc (size * sizeof (char))) == NULL)
{
fprintf(stderr, "Memory allocation error\n");
exit(42);
}
// Otherwise, we need to grow the memory buffer
else
{
if ((globalImageBuffer = (char *) realloc (globalImageBuffer,
(size * sizeof (char)) +
globalImageBufferOffset)) == NULL)
fprintf(stderr, "Could not grow the tiff conversion memory buffer\n");
exit(42);
}
// Now move the image data into the buffer
memcpy (globalImageBuffer + globalImageBufferOffset, buf, size);
globalImageBufferOffset += size;
}
return (size);
}
static toff_t
libtiffDummySeekProc (thandle_t fd, toff_t off, int i)
{
// This appears to return the location that it went to
return off;
}
static int
libtiffDummyCloseProc (thandle_t fd)
{
// Return a zero meaning all is well
return 0;
}
- Download the source files for performing the tasks mentioned in this article:
- read.c, a reading example
- write.c, a writing example
- client.c: a client example
- recompress.c: repeated compression source
- recompress.sh: Repeated compression script
- For more information about hooking the file input and output functions within libtiff, take a look at the images.c file on Michael's Panda page.
- Check out Poynton's Color FAQ for a discussion of converting to grayscale.
- The libtiff Web site is a good place to download the libtiff source and perhaps find a binary package for your operating system of choice.