如何在iOS上用libtiff写一个1bpp tiff?

时间:2014-02-20 18:09:57

标签: ios uiimage png gpuimage libtiff

我正在尝试使用libtiff将UIImage写成tiff。问题是,即使我将它写成每像素1位,当我期待更像100k或更少的东西时,文件仍然在2-5MB范围内出现。

这就是我所拥有的。

- (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);

}

第一条NSLog专线说:

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

我还有一个使用GPUImage的项目版本。有了这个,我可以将相同的图像作为8位PNG得到大约130k。如果我将PNG发送到PNG优化器站点,他们可以将其降低到大约25k。如果有人可以告诉我如何编写从我的GPUImage过滤器生成的1位PNG,我将放弃tiff。

谢谢!

2 个答案:

答案 0 :(得分:4)

我需要在iPhone中生成TIFF图像并将其发送到期望TIFF文件的远程服务器。我不能使用已转换为1bpp PNG的已接受答案,我一直在使用libTIFF转换为TIFF,1bpp CCITT Group 4格式的解决方案。

调试方法后,我找到了错误的位置,最后我得到了正确的解决方案。

以下代码块是解决方案。在代码之后读取以找到OP方法中的错误的解释。

- (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);
}

以下是错误和对错误的解释。

1)如果图像的宽度不是8的倍数,则一条扫描线的内存分配短1个字节。

  

unsigned char *line = (unsigned char *)_TIFFmalloc(width/8);

应替换为

tmsize_t bytesPerStrip = ceil(width/8.0); unsigned char *line = (unsigned char *)_TIFFmalloc(bytesPerStrip);

解释是我们必须将除法的上限乘以8才能得到条带的字节数。例如,83像素的条带需要11个字节,而不是10个,或者我们可以松开最后3个像素。另请注意,我们必须除以8.0才能获得浮点数并将其传递给ceil函数。 C中的整数除法会丢失小数部分并舍入到底线,这在我们的情况下是错误的。

2)传递给函数TIFFWriteEncodedStrip的最后一个参数是错误的。我们不能传递一个条带中的像素数,我们必须传递每个条带的字节数。

所以替换:

  

TIFFWriteEncodedStrip(tiff, y, line, width);

通过

TIFFWriteEncodedStrip(tiff, y, line, bytesPerStrip);

3)难以检测的最后一个错误与关于0值的位是否在双色调图像中表示白色或黑色的约定有关。感谢TIFF标题TIFFTAG_PHOTOMETRIC,我们可以安全地指出这一点。但是我发现有些旧软件忽略了这个标题。如果标头不存在或被忽略,会发生什么情况:0位被解释为white1位被解释为black

出于这个原因,我建议更换行

  

TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISBLACK);

通过

TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_MINISWHITE);

然后反转阈值比较,替换行

  

if (gray > threshold) bite = bite | 1;

通过

if (gray < threshold) bite = bite | 1;

在我的方法中,我使用C指针算法而不是索引来访问内存中的位图。

最后,进行了一些改进:

a)检测原始UIImage(RGBA,ABGR等)的编码并为每个像素获取正确的RGB值

b)通过使用自适应阈值算法而不是纯二元条件,可以改善从灰度图像转换为双色调图像的算法。

答案 1 :(得分:1)

我最终选择了GPUImage和libpng。如果有人想知道如何在UIPNGRepresentation之外的iOS中编写png,请点击这里:

- (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);
}

你为什么要这样做? UIPNGRepresentation是RGBA,每个组件8位。这是每像素32位。由于我想要一个单色的1728x2304图像,我每个像素只需要1位,最终得到的图像只有40k。与UIPNGRepresentation相同的图像是130k。值得庆幸的是,压缩有助于32位版本,但将位深度更改为1确实可以将其缩小到非常小的文件大小。