I\'m trying to write a UIImage out as a tiff using libtiff. The problem is that even though I\'m writing it as 1 bit per pixel, the files are still coming out in the 2-5MB rang
I have the need to generate a TIFF image in the iPhone and send it to a remote server which is expecting TIFF files. I can't use the accepted answer which converts to 1bpp PNG and I have been working in a solution to convert to TIFF, 1bpp CCITT Group 4 format, using libTIFF.
After debugging the method I have found where the errors are and I finally got the correct solution.
The following block of code is the solution. Read after the code to found the explanation to the errors in the OP method.
- (void) convertUIImage:(UIImage *)uiImage toTiff:(NSString *)file withThreshold:(float)threshold {
CGImageRef srcCGImage = [uiImage CGImage];
CFDataRef pixelData = CGDataProviderCopyData(CGImageGetDataProvider(srcCGImage));
unsigned char *pixelDataPtr = (unsigned char *)CFDataGetBytePtr(pixelData);
TIFF *tiff;
if ((tiff = TIFFOpen([file UTF8String], "w")) == NULL) {
[[[UIAlertView alloc] initWithTitle:@"Error" message:[NSString stringWithFormat:@"Unable to write to file %@.", file] delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil] show];
return;
}
size_t width = CGImageGetWidth(srcCGImage);
size_t height = CGImageGetHeight(srcCGImage);
TIFFSetField(tiff, TIFFTAG_IMAGEWIDTH, width);
TIFFSetField(tiff, TIFFTAG_IMAGELENGTH, height);
TIFFSetField(tiff, TIFFTAG_BITSPERSAMPLE, 1);
TIFFSetField(tiff, TIFFTAG_SAMPLESPERPIXEL, 1);
TIFFSetField(tiff, TIFFTAG_ROWSPERSTRIP, 1);
TIFFSetField(tiff, TIFFTAG_COMPRESSION, COMPRESSION_CCITTFAX4);
TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISWHITE);
TIFFSetField(tiff, TIFFTAG_FILLORDER, FILLORDER_MSB2LSB);
TIFFSetField(tiff, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
TIFFSetField(tiff, TIFFTAG_XRESOLUTION, 200.0);
TIFFSetField(tiff, TIFFTAG_YRESOLUTION, 200.0);
TIFFSetField(tiff, TIFFTAG_RESOLUTIONUNIT, RESUNIT_INCH);
unsigned char *ptr = pixelDataPtr; // initialize pointer to the first byte of the image buffer
unsigned char red, green, blue, gray, eightPixels;
tmsize_t bytesPerStrip = ceil(width/8.0);
unsigned char *strip = (unsigned char *)_TIFFmalloc(bytesPerStrip);
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
red = *ptr++; green = *ptr++; blue = *ptr++;
ptr++; // discard fourth byte by advancing the pointer 1 more byte
gray = .3 * red + .59 * green + .11 * blue; // http://answers.yahoo.com/question/index?qid=20100608031814AAeBHPU
eightPixels = strip[x/8];
eightPixels = eightPixels << 1;
if (gray < threshold) eightPixels = eightPixels | 1; // black=1 in tiff image without TIFFTAG_PHOTOMETRIC header
strip[x/8] = eightPixels;
}
TIFFWriteEncodedStrip(tiff, y, strip, bytesPerStrip);
}
TIFFClose(tiff);
if (strip) _TIFFfree(strip);
if (pixelData) CFRelease(pixelData);
}
Here are the errors and the explanation of what is wrong.
1) the allocation of memory for one scan line is 1 byte short if the width of the image is not a multiple of 8.
unsigned char *line = (unsigned char *)_TIFFmalloc(width/8);
should be replaced by
tmsize_t bytesPerStrip = ceil(width/8.0);
unsigned char *line = (unsigned char *)_TIFFmalloc(bytesPerStrip);
The explanation is that we have to take the ceiling of the division by 8 in order to get the number of bytes for a strip. For example a strip of 83 pixels needs 11 bytes, not 10, or we could loose the 3 last pixels. Note also we have to divide by 8.0 in order to get a floating point number and pass it to the ceil function. Integer division in C looses the decimal part and rounds to the floor, which is wrong in our case.
2) the last argument passed to the function TIFFWriteEncodedStrip
is wrong. We can't pass the number of pixels in a strip, we have to pass the number of bytes per strip.
So replace:
TIFFWriteEncodedStrip(tiff, y, line, width);
by
TIFFWriteEncodedStrip(tiff, y, line, bytesPerStrip);
3) A last error difficult to detect is related to the convention on whether a bit with 0 value represents white or black in the bi-tonal image. Thanks to the TIFF header TIFFTAG_PHOTOMETRIC
we can safely indicate this. However I have found than some older software ignores this header. What happens if the header is not present or ignored is that a 0
bit gets interpreted as white
and a 1
bit gets interpreted as black
.
For this reason I recommend to replace the line
TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);
by
TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISWHITE);
and then invert the threshold comparison, replace line
if (gray > threshold) bite = bite | 1;
by
if (gray < threshold) bite = bite | 1;
In my method I use C-pointer arithmetic instead of an index to access the bitmap in memory.
Finally, a couple of improvements:
a) detect the encoding of the original UIImage (RGBA, ABGR, etc.) and get the correct RGB values for each pixel
b) the algorithm to convert from a grayscale image to a bi-tonal image could be improved by using an adaptive-threshold algorithm instead of a pure binary conditional.
I ended up going with GPUImage and libpng. If anyone wants to know how to write a png in iOS outside of the UIPNGRepresentation, here goes:
- (void) writeUIImage:(UIImage *)uiImage toPNG:(NSString *)file {
FILE *fp = fopen([file UTF8String], "wb");
if (!fp) return [self reportError:[NSString stringWithFormat:@"Unable to open file %@", file]];
CGImageRef image = [uiImage CGImage];
CGDataProviderRef provider = CGImageGetDataProvider(image);
CFDataRef pixelData = CGDataProviderCopyData(provider);
unsigned char *buffer = (unsigned char *)CFDataGetBytePtr(pixelData);
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(image);
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(image);
size_t compBits = CGImageGetBitsPerComponent(image);
size_t pixelBits = CGImageGetBitsPerPixel(image);
size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);
NSLog(@"bitmapInfo=%d, alphaInfo=%d, pixelBits=%lu, compBits=%lu, width=%lu, height=%lu", bitmapInfo, alphaInfo, pixelBits, compBits, width, height);
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png_ptr) [self reportError:@"Unable to create write struct."];
png_infop info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr) {
png_destroy_write_struct(&png_ptr, (png_infopp)NULL);
return [self reportError:@"Unable to create info struct."];
}
if (setjmp(png_jmpbuf(png_ptr))) {
png_destroy_write_struct(&png_ptr, &info_ptr);
fclose(fp);
return [self reportError:@"Got error callback."];
}
png_init_io(png_ptr, fp);
png_set_IHDR(png_ptr, info_ptr, (png_uint_32)width, (png_uint_32)height, 1, PNG_COLOR_TYPE_GRAY, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png_ptr, info_ptr);
png_set_packing(png_ptr);
png_bytep line = (png_bytep)png_malloc(png_ptr, width);
unsigned long pos;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
pos = y * width * 4 + x * 4; // multiplying by four because each pixel is represented by four bytes
line[x] = buffer[ pos ]; // just use the first byte (red) since r=g=b in grayscale
}
png_write_row(png_ptr, line);
}
png_write_end(png_ptr, info_ptr);
png_destroy_write_struct(&png_ptr, &info_ptr);
if (pixelData) CFRelease(pixelData);
fclose(fp);
}
Why would you want to do this? UIPNGRepresentation is RGBA with 8 bits per component. That's 32 bits per pixel. Since I wanted a monochrome 1728x2304 image, I only need 1 bit per pixel and I end up with images as small as 40k. The same image with UIPNGRepresentation is 130k. Thankfully compression helps that 32 bit version a lot, but changing the bit depth to 1 really gets it down to very small file sizes.