How do I write a 1bpp tiff with libtiff on iOS?

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 range when I’m expecting something more like 100k or less.

Here’s what I’ve got.

  • Is there way to get metadata of the image file in iOS?
  • Detecting iPhone camera orientation
  • UIImageView missing images in Launch Screen on device
  • How to add multiple UIImageViews to paging UIScrollView?
  • change UIImage from MKAnnotation in the MKMapView
  • How to implement highlighting on UIImage like UIButton does when tapped?
  • - (void) convertUIImage:(UIImage *)uiImage toTiff:(NSString *)file withThreshold:(float)threshold {
    
        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;
        }
    
        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);
    
    
        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_FAXMODE, FAXMODE_CLASSF);
        TIFFSetField(tiff, TIFFTAG_COMPRESSION, COMPRESSION_CCITTFAX4);
        TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);
        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 red, green, blue, gray, bite;
        unsigned char *line = (unsigned char *)_TIFFmalloc(width/8);
        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
                red = buffer[ pos ];
                green = buffer[ pos + 1 ];
                blue = buffer[ pos + 2 ];
                gray = .3 * red + .59 * green + .11 * blue; // http://answers.yahoo.com/question/index?qid=20100608031814AAeBHPU
    
    
                bite = line[x / 8];
                bite = bite << 1;
                if (gray > threshold) bite = bite | 1;
    //            NSLog(@"y=%d, x=%d, byte=%d, red=%d, green=%d, blue=%d, gray=%d, before=%@, after=%@", y, x, x/8, red, green, blue, gray, [self bitStringForChar:line[x / 8]], [self bitStringForChar:bite]);
                line[x / 8] = bite;
            }
            TIFFWriteEncodedStrip(tiff, y, line, width);
        }
    
        // Close the file and free buffer
        TIFFClose(tiff);
        if (line) _TIFFfree(line);
        if (pixelData) CFRelease(pixelData);
    
    }
    

    The first NSLog line says:

    bitmapInfo=5, alphaInfo=5, pixelBits=32, compBits=8, width=3264, height=2448
    

    I’ve also got a version of this project that uses GPUImage instead. With that I can get the same image down to about 130k as an 8-bit PNG. If I send that PNG to a PNG optimizer site, they can get it down to about 25k. If someone can show me how to write a 1 bit PNG generated from my GPUImage filters, I’ll forego the tiff.

    Thanks!

    2 Solutions Collect From Internet About “How do I write a 1bpp tiff with libtiff on iOS?”

    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 1728×2304 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.