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

2019-03-22 08:55发布

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.

- (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条回答
走好不送
2楼-- · 2019-03-22 09:37

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.

查看更多
乱世女痞
3楼-- · 2019-03-22 09:54

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.

查看更多
登录 后发表回答