7
votes

I have a 32-bit NSBitmapImageRep which has an alpha channel with essentially 1-bit values (the pixels are either on or off).

I want to save this bitmap to an 8-bit PNG file with transparency. If I use the -representationUsingType:properties: method of NSBitmapImageRep and pass in NSPNGFileType, a 32-bit PNG is created, which is not what I want.

I know that 8-bit PNGs can be read, they open in Preview with no problems, but is it possible to write this type of PNG file using any built-in Mac OS X APIs? I'm happy to drop down to Core Image or even QuickTime if necessary. A cursory examination of the CGImage docs didn't reveal anything obvious.

EDIT: I've started a bounty on this question, if someone can provide working source code that takes a 32-bit NSBitmapImageRep and writes a 256-color PNG with 1-bit transparency, it's yours.

5
8-bit as in 256-color? I hope you have no more than 256 colors in the image. If you may have more than 256 colors, you may want to use pngnq (bundling it in your app and running it with NSTask) instead: pngnq.sourceforge.netPeter Hosey
Yeah, 256 colors. I'm looking for something like the output using -representationUsingType:properties with NSGIFFileType except with an 8-bit PNG as output. pngnq is an option (thanks) but I'm hoping to handle it without spawning tasks if at all possible.Rob Keniger

5 Answers

2
votes

How about pnglib? It's really lightweight and easy to use.

1
votes

A great reference for working with lower level APIs is Programming With Quartz

Some of the code below is based on examples from that book.

Note: This is un-tested code meant to be a starting point only....

- (NSBitmapImageRep*)convertImageRep:(NSBitmapImageRep*)startingImage{

    CGImageRef anImage = [startingImage CGImage];

    CGContextRef    bitmapContext;
    CGRect ctxRect;
    size_t  bytesPerRow, width, height;

    width = CGImageGetWidth(anImage);
    height = CGImageGetHeight(anImage);
    ctxRect = CGRectMake(0.0, 0.0, width, height);
    bytesPerRow = (width * 4 + 63) & ~63;
    bitmapData = calloc(bytesPerRow * height, 1);
    bitmapContext = createRGBBitmapContext(width, height, TRUE);
    CGContextDrawImage (bitmapContext, ctxRect, anImage);

    //Now extract the image from the context
    CGImageRef      bitmapImage = nil;
    bitmapImage = CGBitmapContextCreateImage(bitmapContext);
    if(!bitmapImage){
        fprintf(stderr, "Couldn't create the image!\n");
        return nil;
    }

    NSBitmapImageRep *newImage = [[NSBitmapImageRep alloc] initWithCGImage:bitmapImage];
    return newImage;
}

Context Creation Function:

CGContextRef createRGBBitmapContext(size_t width, size_t height, Boolean needsTransparentBitmap)
{
    CGContextRef context;
    size_t bytesPerRow;
    unsigned char *rasterData;

    //minimum bytes per row is 4 bytes per sample * number of samples
    bytesPerRow = width*4;
    //round up to nearest multiple of 16.
    bytesPerRow = COMPUTE_BEST_BYTES_PER_ROW(bytesPerRow);

    int bitsPerComponent = 2;  // to get 256 colors (2xRGBA)

    //use function 'calloc' so memory is initialized to 0.
    rasterData = calloc(1, bytesPerRow * height);
    if(rasterData == NULL){
        fprintf(stderr, "Couldn't allocate the needed amount of memory!\n");
        return NULL;
    }

    // uses the generic calibrated RGB color space.
    context = CGBitmapContextCreate(rasterData, width, height, bitsPerComponent, bytesPerRow,
                                    CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB),
                                    (needsTransparentBitmap ? kCGImageAlphaPremultipliedFirst :
                                     kCGImageAlphaNoneSkipFirst)
                                    );
    if(context == NULL){
        free(rasterData);
        fprintf(stderr, "Couldn't create the context!\n");
        return NULL;
    }

    //Either clear the rect or paint with opaque white,
    if(needsTransparentBitmap){
        CGContextClearRect(context, CGRectMake(0, 0, width, height));
    }else{
        CGContextSaveGState(context);
        CGContextSetFillColorWithColor(context, getRGBOpaqueWhiteColor());
        CGContextFillRect(context, CGRectMake(0, 0, width, height));
        CGContextRestoreGState(context);
    }
    return context;
}

Usage would be:

NSBitmapImageRep *startingImage;  // assumed to be previously set.
NSBitmapImageRep *endingImageRep = [self convertImageRep:startingImage];
// Write out as data
NSData *outputData = [endingImageRep representationUsingType:NSPNGFileType properties:nil];
// somePath is set elsewhere
[outputData writeToFile:somePath atomically:YES];
1
votes

pngnq (and new pngquant which achieves higher quality) has BSD-style license, so you can just include it in your program. No need to spawn as separate task.

0
votes

One thing to try would be creating a NSBitmapImageRep with 8 bits, then copying the data to it.

This would actually be a lot of work, as you would have to compute the color index table yourself.

0
votes

CGImageDestination is your man for low-level image writing, but I don't know if it supports that specific ability.