How do I create an 8-bit PNG with transparency from an NSBitmapImageRep?
Asked Answered
H

5

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.

Hoofbound answered 3/3, 2010 at 4:11 Comment(2)
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.netShrink
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.Hoofbound
R
1

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

Rogovy answered 10/3, 2010 at 12:49 Comment(3)
is there an easy way to import pngquant in an iOS app?Dogfight
@Dogfight Yes, add the .c files to your Xcode project. They don't need anything special.Rogovy
The DON'T have BSD-style license! It's either a GPL/3 license which doesn't work with non-pure open source software. Or you can get a (very) expensive commercial license!Coit
S
2

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

Starter answered 3/3, 2010 at 4:17 Comment(2)
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.Hoofbound
@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.Starter
B
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];
Brasilein answered 3/3, 2010 at 15:12 Comment(3)
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.Hoofbound
Sorry, I left that step out. I'll edit my answer to include the two calls needed.Brasilein
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". developer.apple.com/mac/library/DOCUMENTATION/GraphicsImaging/…Hoofbound
R
1

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

Rogovy answered 10/3, 2010 at 12:49 Comment(3)
is there an easy way to import pngquant in an iOS app?Dogfight
@Dogfight Yes, add the .c files to your Xcode project. They don't need anything special.Rogovy
The DON'T have BSD-style license! It's either a GPL/3 license which doesn't work with non-pure open source software. Or you can get a (very) expensive commercial license!Coit
B
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.

Bid answered 3/3, 2010 at 4:16 Comment(1)
Correct. Peter Hosey's suggestion of using pngnq solves this palette creation problem quite nicely, albeit with the need to spawn a task.Hoofbound
P
0

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

Piotr answered 3/3, 2010 at 10:18 Comment(1)
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.Hoofbound

© 2022 - 2024 — McMap. All rights reserved.