dreamlax
dreamlax

Reputation: 95315

Create monochrome CGImageRef (1-bit per pixel bitmap)

I'm trying to show a preview of an image in 1-bit monochrome, as in, not grayscale, but bitonal black and white. It's supposed to be an indication of how the image will look if it were faxed. Formats as low as 1-bit per pixel aren't available on OS X, only 8-bit grayscale. Is there any way to achieve this effect using Core Graphics or another framework (ideally with dithering)?

I know there's a filter called CIColorMonochrome but this only converts the image to grayscale.

Upvotes: 4

Views: 2318

Answers (1)

Heinrich Giesen
Heinrich Giesen

Reputation: 1835

The creation of a 1 bit deep NSImageRep (and also in the CG-world) is AFAIK not supported, So we have to do it manually. It might be useful to use CIImage for this task. Here I go the classical (you may call it old-fashioned) way. Here is a code that shows how we can do it. First a gray image is created from an NSImageRep so we have a well defined and simple format whatever the source image will be formatted (could also be a PDF file). The resulting gray image is the source for the bitonal image. Here is the code for creating a gray image: (without respecting the size / resolution of the source image, only the pixels count!):

- (NSBitmapImageRep *) grayRepresentationOf:(NSImageRep *)aRep
{
    NSBitmapImageRep *newRep =
     [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
                                        pixelsWide:[aRep pixelsWide]
                                        pixelsHigh:[aRep pixelsHigh]
                                     bitsPerSample:8
                                   samplesPerPixel:1
                                          hasAlpha:NO   //must be NO !
                                          isPlanar:NO
                                    colorSpaceName:NSCalibratedWhiteColorSpace
                                       bytesPerRow:0
                                      bitsPerPixel:0 ];
    // this new imagerep has (as default) a resolution of 72 dpi

    [NSGraphicsContext saveGraphicsState];
    NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithBitmapImageRep:newRep];
    if( context==nil ){
        NSLog( @"***  %s context is nil", __FUNCTION__ );
        return nil;
    }

    [NSGraphicsContext setCurrentContext:context];
    [aRep drawInRect:NSMakeRect( 0, 0, [newRep pixelsWide], [newRep pixelsHigh] )];

    [NSGraphicsContext restoreGraphicsState];

    return [newRep autorelease];
}

In the next method we create an NXBitmapImageRep (bits per pixel=1, samples per pixel=1) from a given NSImageRep (one of it's subclasses) and will use the method just given:

- (NSBitmapImageRep *) binaryRepresentationOf:(NSImageRep *)aRep
{
    NSBitmapImageRep *grayRep = [aRep grayRepresentation];
    if( grayRep==nil ) return nil;

    NSInteger numberOfRows = [grayRep pixelsHigh];
    NSInteger numberOfCols = [grayRep pixelsWide];

    NSBitmapImageRep *newRep =
      [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
                                        pixelsWide:numberOfCols
                                        pixelsHigh:numberOfRows
                                     bitsPerSample:1
                                   samplesPerPixel:1
                                          hasAlpha:NO
                                          isPlanar:NO
                                    colorSpaceName:NSCalibratedWhiteColorSpace
                                      bitmapFormat:0 
                                       bytesPerRow:0
                                      bitsPerPixel:0 ];

    unsigned char *bitmapDataSource = [grayRep bitmapData];
    unsigned char *bitmapDataDest = [newRep bitmapData];

    // here is the place to use dithering or error diffusion (code below)

    // iterate over all pixels
    NSInteger grayBPR = [grayRep bytesPerRow];
    NSInteger binBPR = [newRep bytesPerRow];
    NSInteger pWide = [newRep pixelsWide];

    for( NSInteger row=0; row<numberOfRows; row++ ){
      unsigned char *rowDataSource = bitmapDataSource + row*grayBPR;
      unsigned char *rowDataDest = bitmapDataDest + row*binBPR;

      NSInteger destCol = 0;
      unsigned char bw = 0;
      for( NSInteger col = 0; col<pWide; ){
        unsigned char gray = rowDataSource[col];
        if( gray>127 ) {bw |= (1<<(7-col%8)); };
        col++;
        if( (col%8 == 0) || (col==pWide) ){
            rowDataDest[destCol] = bw;
            bw = 0;
            destCol++;
        }
    }
}

// save as PNG for testing and return
    [[newRep representationUsingType:NSPNGFileType properties:nil] writeToFile:@"/tmp/bin_1.png" atomically:YES];
    return [newRep autorelease];
}

For error diffusion I used the following code which changes directly the bitmap of the gray image. This is allowed because the gray image itself is no longer used.

// change bitmapDataSource : use Error-Diffusion
for( NSInteger row=0; row<numberOfRows-1; row++ ){
    unsigned char *currentRowData = bitmapDataSource + row*grayBPR;
    unsigned char *nextRowData = currentRowData + grayBPR;
    for( NSInteger col = 1; col<numberOfCols; col++ ){
        NSInteger origValue = currentRowData[col];
        NSInteger newValue = (origValue>127) ? 255 : 0;
        NSInteger error = -(newValue - origValue);
        currentRowData[col] = newValue;

        currentRowData[col+1] = clamp(currentRowData[col+1] + (7*error/16));
        nextRowData[col-1] = clamp( nextRowData[col-1] + (3*error/16) );
        nextRowData[col] = clamp( nextRowData[col] + (5*error/16) );
        nextRowData[col+1] = clamp( nextRowData[col+1] + (error/16) );
    }
}

clamp is a macro defined before the method

#define clamp(z) ( (z>255)?255 : ((z<0)?0:z) ) 

This makes the unsigned char bytes to have valid values (0<=z<=255)

Upvotes: 3

Related Questions