7

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.

Rob Keniger
  • 45,830
  • 6
  • 101
  • 134
  • 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: http://pngnq.sourceforge.net/ – Peter Hosey Mar 03 '10 at 04:31
  • 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 Mar 03 '10 at 09:43

5 Answers5

2

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

Carl Norum
  • 219,201
  • 40
  • 422
  • 469
  • This is definitely an option but since I've not worked with `pnglib` before there's a lot of learning involved, I was really hoping for something higher-level. – Rob Keniger Mar 08 '10 at 01:37
  • @Rob, there's not a lot of learning for pnglib, it's really pretty straightforward as far as C libraries go. You might be stuck for something else, most of the higher-level APIs assume more general cases, which usually means 24 or 32 bpp images. – Carl Norum Mar 08 '10 at 06:47
1

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];
Chip Coons
  • 3,201
  • 1
  • 24
  • 16
  • Thanks, this will work nicely to create the bitmap but it doesn't actually solve the problem of writing a 256-color 8-bit PNG file. – Rob Keniger Mar 03 '10 at 23:51
  • Sorry, I left that step out. I'll edit my answer to include the two calls needed. – Chip Coons Mar 04 '10 at 16:55
  • I've looked into this, and according to the "Supported Pixel Formats" in the Quartz 2D Programming Guide, it's not possible to create an 8-bit RGB context, so this code can't work. If you try running it, it fails to create the context due to an "invalid parameter combination". http://developer.apple.com/mac/library/DOCUMENTATION/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB – Rob Keniger Mar 08 '10 at 01:35
1

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.

Kornel
  • 97,764
  • 37
  • 219
  • 309
0

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.

cobbal
  • 69,903
  • 20
  • 143
  • 156
  • Correct. Peter Hosey's suggestion of using **pngnq** solves this palette creation problem quite nicely, albeit with the need to spawn a task. – Rob Keniger Mar 03 '10 at 23:53
0

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

Mike Abdullah
  • 14,933
  • 2
  • 50
  • 75
  • Yeah, it looks like it should be the answer but so far I've been unable to find a way to specify the type of PNG to create, everything I try writes a 32-bit PNG. – Rob Keniger Mar 03 '10 at 23:52