Hi everyone! It is my first post here.
I am quite new in objective-c so maybe my question is not so hard but It is a problem for me. I've searched the web and didn't find any useful hints for me... I am writing a program in Objective-c in Xcode. I need to read and display a pgm file (P5 with two bytes per pixel). To do that i'm trying to subclass NSImageRep
but I don't know how to create a proper bitmap for this file and how to draw it. Below is my code so far:
header:
@interface CTPWTPGMImageRep : NSImageRep
@property (readonly)NSInteger width;
@property (readonly)NSInteger height;
@property (readonly)NSInteger maxValue;
+ (void)load;
+ (NSArray *)imageUnfilteredTypes;
+ (NSArray *)imageUnfilteredFileTypes;
+ (BOOL)canInitWithData:(NSData *)data;
+ (id)imageRepWithContentsOfFile:(NSString*)file;
+ (id)imageRepWithData:(NSData*)pgmData;
- (id)initWithData:(NSData *)data;
- (BOOL)draw;
@end
and implementation:
#import "CTPWTPGMImageRep.h"
@implementation CTPWTPGMImageRep
@synthesize width;
@synthesize height;
@synthesize maxValue;
#pragma mark - class methods
+(void) load
{
NSLog(@"Called 'load' method for CTPWTPGMImageRep");
[NSImageRep registerImageRepClass:[CTPWTPGMImageRep class]];
}
+ (NSArray *)imageUnfilteredTypes
{
// This is a UTI
NSLog(@"imageUnfilteredTypes called");
static NSArray *types = nil;
if (!types) {
types = [[NSArray alloc] initWithObjects:@"public.unix-executable", @"public.data", @"public.item", @"public.executable", nil];
}
return types;
}
+ (NSArray *)imageUnfilteredFileTypes
{
// This is a filename suffix
NSLog(@"imageUnfilteredFileTypes called");
static NSArray *types = nil;
if (!types)
types = [[NSArray alloc] initWithObjects:@"pgm", @"PGM", nil];
return types;
}
+ (BOOL)canInitWithData:(NSData *)data;
{
// FIX IT
NSLog(@"canInitWithData called");
if ([data length] >= 2) // First two bytes for magic number magic number
{
NSString *magicNumber = @"P5";
const unsigned char *mNum = (const unsigned char *)[magicNumber UTF8String];
unsigned char aBuffer[2];
[data getBytes:aBuffer length:2];
if(memcmp(mNum, aBuffer, 2) == 0)
{
NSLog(@"canInitWithData: YES");
return YES;
}
}
NSLog(@"canInitWithData: NO");
// end
return NO;
}
+ (id)imageRepWithContentsOfFile:(NSString*)file {
NSLog(@"imageRepWithContentsOfFile called");
NSData* data = [NSData dataWithContentsOfFile:file];
if (data)
return [CTPWTPGMImageRep imageRepWithData:data];
return nil;
}
+ (id)imageRepWithData:(NSData*)pgmData {
NSLog(@"imageRepWithData called");
return [[self alloc] initWithData:pgmData];
}
#pragma mark - instance methods
- (id)initWithData:(NSData *)data;
{
NSLog(@"initWithData called");
self = [super init];
if (!self)
{
return nil;
}
if ([data length] >= 2) {
NSString *magicNumberP5 = @"P5";
const unsigned char *mnP5 = (const unsigned char *)[magicNumberP5 UTF8String];
unsigned char headerBuffer[20];
[data getBytes:headerBuffer length:2];
if(memcmp(mnP5, headerBuffer, 2) == 0)
{
NSArray *pgmParameters = [self calculatePgmParameters:data beginingByte:3];
width = [[pgmParameters objectAtIndex:0] integerValue];
height = [[pgmParameters objectAtIndex:1] integerValue];
maxValue = [[pgmParameters objectAtIndex:2] integerValue];
if (width <= 0 || height <= 0)
{
NSLog(@"Invalid image size: Both width and height must be > 0");
return nil;
}
[self setPixelsWide:width];
[self setPixelsHigh:height];
[self setSize:NSMakeSize(width, height)];
[self setColorSpaceName:NSDeviceWhiteColorSpace];
[self setBitsPerSample:16];
[self setAlpha:NO];
[self setOpaque:NO];
//What to do here?
//CTPWTPGMImageRep *imageRep =
[NSBitmapImageRep alloc] initWithBitmapDataPlanes:];
//if (imageRep) {
/* code to populate the pixel map */
//}
}
else
{
NSLog(@"It is not supported pgm file format.");
}
}
return self;
//return imageRep;
}
- (BOOL)draw
{
NSLog(@"draw method1 called");
return NO;
}
It is interesting that my canInitWithData:
method is never called. Can you give me a hint how to smart read the Bitmap? I think that I need to use initWithBitmapDataPlanes:pixelsWide:pixelsHigh:bitsPerSample:samplesPerPixel:hasAlpha:isPlanar:colorSpaceName:bytesPerRow:bitsPerPixel:
but I don't know how to use it. How to create smart create (unsigned char **)planes from my NSData
object? Do I need it?
May I use NSDeviceWhiteColorSpace
which has white and alpha components (I don't need alpha)
[self setColorSpaceName:NSDeviceWhiteColorSpace];
and the most dificult part - I've completely no idea how to implement draw method. Any sugestions or hints?
Thank you in advance for your help.
EDIT:
Ok. Now I have implementation according to NSGod directions:
@implementation CTPWTPGMImageRep
//@synthesize width;
//@synthesize height;
#pragma mark - class methods
+(void) load
{
NSLog(@"Called 'load' method for CTPWTPGMImageRep");
[NSBitmapImageRep registerImageRepClass:[CTPWTPGMImageRep class]];
}
+ (NSArray *)imageUnfilteredTypes
{
// This is a UTI
NSLog(@"imageUnfilteredTypes called");
static NSArray *types = nil;
if (!types) {
types = [[NSArray alloc] initWithObjects:@"public.unix-executable", @"public.data", @"public.item", @"public.executable", nil];
}
return types;
}
+ (NSArray *)imageUnfilteredFileTypes
{
// This is a filename suffix
NSLog(@"imageUnfilteredFileTypes called");
static NSArray *types = nil;
if (!types)
types = [[NSArray alloc] initWithObjects:@"pgm", nil];
return types;
}
+ (NSArray *)imageRepsWithData:(NSData *)data {
NSLog(@"imageRepsWithData called");
id imageRep = [[self class] imageRepWithData:data];
return [NSArray arrayWithObject:imageRep];
}
- (id)initWithData:(NSData *)data {
NSLog(@"initWithData called");
CTPWTPGMImageRep *imageRep = [[self class] imageRepWithData:data];
if (imageRep == nil) {
return nil;
}
return self;
}
#pragma mark - instance methods
+ (id)imageRepWithData:(NSData *)data {
NSLog(@"imageRepWithData called");
if (data.length < 2) return nil;
NSString *magicNumberP5 = @"P5";
const unsigned char *mnP5 = (const unsigned char *)[magicNumberP5 UTF8String];
unsigned char headerBuffer[2];
[data getBytes:headerBuffer length:2];
if (memcmp(mnP5, headerBuffer, 2) != 0) {
NSLog(@"It is not supported pgm file format.");
return nil;
}
NSArray *pgmParameters = [self calculatePgmParameters:data beginingByte:3];
NSInteger width = [[pgmParameters objectAtIndex:0] integerValue]; // width in pixels
NSInteger height = [[pgmParameters objectAtIndex:1] integerValue]; // height in pixels
NSUInteger imageLength = width * height * 2; // two bytes per pixel
// imageData contains bytes of Bitmap only. Without header
NSData *imageData = [data subdataWithRange:
NSMakeRange(data.length - imageLength, imageLength)];
CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef)CFBridgingRetain(imageData));
CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericGray); // kCGColorSpaceGenericGrayGamma2_2
CGImageRef imageRef = CGImageCreate(width,
height,
16,
16,
width * 2,
colorSpace,
kCGImageAlphaNone,
provider,
NULL,
false,
kCGRenderingIntentDefault);
CGColorSpaceRelease(colorSpace);
CGDataProviderRelease(provider);
if (imageRef == NULL) {
NSLog(@"CGImageCreate() failed!");
}
CTPWTPGMImageRep *imageRep = [[CTPWTPGMImageRep alloc] initWithCGImage:imageRef];
return imageRep;
}
as you can see I left the part with extending values between 0-255 because my pixels has values between 0-65535.
But It doesn't work. When I am choosing a pgm file from panel nothing happens. Bellow is my openPanel code:
- (IBAction)showOpenPanel:(id)sender
{
NSLog(@"showPanel method called");
__block NSOpenPanel *panel = [NSOpenPanel openPanel];
[panel setAllowedFileTypes:[NSImage imageFileTypes]];
[panel beginSheetModalForWindow:[pgmImageView window] completionHandler:^ (NSInteger result) {
if (result == NSOKButton) {
CTPWTPGMImageRep *pgmImage = [[CTPWTPGMImageRep alloc] initWithData:[NSData dataWithContentsOfURL:[panel URL]]];
// NSLog(@"Bits per pixel: %ld",[pgmImage bitsPerPixel]); // BUG HERE!
NSImage *image = [[NSImage alloc] init];
[image addRepresentation:pgmImage];
[pgmImageView setImage:image];
}
panel = nil; // prevent strong ref cycle
}];
}
Moreover, when i uncomment line with code // NSLog(@"Bits per pixel: %ld",[pgmImage bitsPerPixel]); // BUG HERE!
just for check my Xcode freezes for a moment and I get EXC_BAD_ACCESS in:
AppKit`__75-[NSBitmapImageRep _withoutChangingBackingPerformBlockUsingBackingCGImage:]_block_invoke_0:
0x7fff8b4823e8: pushq %rbp
0x7fff8b4823e9: movq %rsp, %rbp
0x7fff8b4823ec: pushq %r15
0x7fff8b4823ee: pushq %r14
0x7fff8b4823f0: pushq %r13
0x7fff8b4823f2: pushq %r12
0x7fff8b4823f4: pushq %rbx
0x7fff8b4823f5: subq $312, %rsp
0x7fff8b4823fc: movq %rsi, %rbx
0x7fff8b4823ff: movq %rdi, %r15
0x7fff8b482402: movq 10625679(%rip), %rax
0x7fff8b482409: movq (%rax), %rax
0x7fff8b48240c: movq %rax, -48(%rbp)
0x7fff8b482410: movq %rbx, %rdi
0x7fff8b482413: callq 0x7fff8b383148 ; BIRBackingType //EXC_BAD_ACCESS (code=2, adress=...)
any help??? I have no idea what is wrong...
It's actually much easier than you're thinking.
First of all, I would recommend making CTPWTPGMImageRep
a subclass of NSBitmapImageRep
rather than NSImageRep
. That will take care of the "hardest" problem, since there will be no need to implement a custom draw
method, as NSBitmapImageRep
s already know how to draw themselves. (In OS X 10.5 and later, NSBitmapImageRep
is basically a direct wrapper around CoreGraphics CGImageRef
s).
I'm not that familiar with the PGM format, but what you'll basically be doing is creating a representation of the image in the closest destination format that matches the source format. To use a specific example, we'll take the PGM example FEEP image from Wikipedia.
P2
# Shows the word "FEEP" (example from Netpbm main page on PGM)
24 7
15
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 3 3 3 3 0 0 7 7 7 7 0 0 11 11 11 11 0 0 15 15 15 15 0
0 3 0 0 0 0 0 7 0 0 0 0 0 11 0 0 0 0 0 15 0 0 15 0
0 3 3 3 0 0 0 7 7 7 0 0 0 11 11 11 0 0 0 15 15 15 15 0
0 3 0 0 0 0 0 7 0 0 0 0 0 11 0 0 0 0 0 15 0 0 0 0
0 3 0 0 0 0 0 7 7 7 7 0 0 11 11 11 11 0 0 15 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
The closest native image to be able to portray the values in the example image would be a single channel grayscale image, 8 bits per pixel, no alpha. The plan then is to create a CGImageRef
with the specified settings, and then use NSBitmapImageRep
's initWithCGImage:
method to initialize the custom subclass.
I would first override the following two methods to both rely on the singular +imageRepWithData:
:
+ (NSArray *)imageRepsWithData:(NSData *)data {
id imageRep = [[self class] imageRepWithData:data];
return [NSArray arrayWithObject:imageRep];
}
- (id)initWithData:(NSData *)data {
CTPWTPGMImageRep *imageRep = [[self class] imageRepWithData:data];
if (imageRep == nil) {
[self release];
return nil;
}
self = [imageRep retain];
return self;
}
For me, it was necessary to implement the +imageRepsWithData:
method to call the singular method before the image was able to be loaded properly.
I would then change the singular +imageRepWithData:
method be as follows:
+ (id)imageRepWithData:(NSData *)data {
if (data.length < 2) return nil;
NSString *magicNumberP5 = @"P5";
const unsigned char *mnP5 = (const unsigned char *)[magicNumberP5 UTF8String];
unsigned char headerBuffer[20];
[data getBytes:headerBuffer length:2];
if (memcmp(mnP5, headerBuffer, 2) != 0) {
NSLog(@"It is not supported pgm file format.");
return nil;
}
NSArray *pgmParameters = [self calculatePgmParameters:data beginingByte:3];
NSUInteger width = [[pgmParameters objectAtIndex:0] integerValue];
NSUInteger height = [[pgmParameters objectAtIndex:1] integerValue];
NSUInteger maxValue = [[pgmParameters objectAtIndex:2] integerValue];
NSUInteger imageLength = width * height * 1;
NSData *imageData = [data subdataWithRange:
NSMakeRange(data.length - imageLength, imageLength)];
const UInt8 *imageDataBytes = [imageData bytes];
UInt8 *expandedImageDataBytes = malloc(imageLength);
for (NSUInteger i = 0; i < imageLength; i++) {
expandedImageDataBytes[i] = 255 * (imageDataBytes[i] / (CGFloat)maxValue);
}
NSData *expandedImageData = [NSData dataWithBytes:expandedImageDataBytes
length:imageLength];
free(expandedImageDataBytes);
CGDataProviderRef provider = CGDataProviderCreateWithCFData(
(CFDataRef)expandedImageData);
CGColorSpaceRef colorSpace =
CGColorSpaceCreateWithName(kCGColorSpaceGenericGrayGamma2_2);
CGImageRef imageRef = CGImageCreate(width,
height,
8,
8,
width * 1,
colorSpace,
kCGImageAlphaNone,
provider,
NULL,
false,
kCGRenderingIntentDefault);
CGColorSpaceRelease(colorSpace);
CGDataProviderRelease(provider);
if (imageRef == NULL) {
NSLog(@"CGImageCreate() failed!");
}
CTPWTPGMImageRep *imageRep = [[[CTPWTPGMImageRep alloc]
initWithCGImage:imageRef] autorelease];
CGImageRelease(imageRef);
return imageRep;
}
As you can see, we need to loop through the bytes in the original image and create a second copy of these bytes with the full expanded range between 0 and 255.
To make use of this image rep, you can call it like the following (being sure to use NSImage
's initWithData:
method):
// if it hasn't been done already:
[NSImageRep registerImageRepClass:[CTPWTPGMImageRep class]];
NSString *path = [[NSBundle mainBundle] pathForResource:@"feep" ofType:@"pgm"];
NSData *data = [NSData dataWithContentsOfFile:path];
NSImage *image = [[[NSImage alloc] initWithData:data] autorelease];
[self.imageView setImage:image];
One more note about your +imageUnfilteredFileTypes
method: filename extensions are case insensitive, so there's no need to specify both lowercase and uppercase @"PGM"
, you can just do lowercase:
+ (NSArray *)imageUnfilteredFileTypes {
static NSArray *types = nil;
if (!types) types = [[NSArray alloc] initWithObjects:@"pgm", nil];
return types;
}