8

I'm developing a quick app in which I have a method that should rescale a @2x image to a regular one. The problem is that it doesn't :(

Why?

-(BOOL)createNormalImage:(NSString*)inputRetinaImagePath {

    NSImage *inputRetinaImage = [[NSImage alloc] initWithContentsOfFile:inputRetinaImagePath];



    NSSize size = NSZeroSize;
    size.width = inputRetinaImage.size.width*0.5;
    size.height = inputRetinaImage.size.height*0.5;

    [inputRetinaImage setSize:size];


    NSLog(@"%f",inputRetinaImage.size.height);


    NSBitmapImageRep *imgRep = [[inputRetinaImage representations] objectAtIndex: 0];

    NSData *data = [imgRep representationUsingType: NSPNGFileType properties: nil];

    NSString *outputFilePath = [[inputRetinaImagePath substringToIndex:inputRetinaImagePath.length - 7] stringByAppendingString:@".png"];

    NSLog([@"Normal version file path: " stringByAppendingString:outputFilePath]);
    [data writeToFile:outputFilePath atomically: NO];
    return true;
}
Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
Tobias Timpe
  • 720
  • 2
  • 13
  • 27
  • Here's a solution that *won't* work: `setScalesWhenResized:`. That used to be how you did this, but it's deprecated since Snow Leopard and doesn't work as of Lion. – Peter Hosey Dec 20 '12 at 18:47
  • Can't you just draw it in a smaller rect? Or pass a NSAffineTransform as hint? – Ramy Al Zuhouri Dec 23 '12 at 01:57
  • @RamyAlZuhouri: It sounds like the application is meant to lower the resolution of the image and save the result, for creating assets at 2x and producing 1x assets from the same. – Peter Hosey Dec 23 '12 at 04:16
  • But do you explicitly draw the image or jut assign it to an image well? – Ramy Al Zuhouri Dec 23 '12 at 13:27
  • @RamyAlZuhouri: Neither one, according to the code in the question. The resized image never makes it outside of the method, neither to a property nor to an ivar nor to another object. – Peter Hosey Dec 24 '12 at 01:25

2 Answers2

11

You have to be very wary of the size attribute of an NSImage. It doesn't necessarily refer to the bitmapRepresentation's pixel dimensions, it could refer to the displayed size for example. An NSImage may have a number of bitmapRepresentations for use at different output sizes.

Likewise, changing the size attribute of an NSImage does nothing to alter the bitmapRepresentations

So what you need to do is work out the size you want your output image to be, and then draw a new image at that size using a bitmapRepresentation from the source NSImage.

Getting that size depends on how you have obtained your input image and what you know about it. For example, if you are confident that your input image has only one bitmapImageRep you can use this type of thing (as a category on NSImage)

  - (NSSize) pixelSize
{
    NSBitmapImageRep* bitmap = [[self representations] objectAtIndex:0];
    return NSMakeSize(bitmap.pixelsWide,bitmap.pixelsHigh);
}

Even if you have a number of bitmapImageReps, the first one should be the largest one, and if that is the size that your Retina image was created at, it should be the Retina size you are after.

When you have worked out your final size, you can make the image:

- (NSImage*) resizeImage:(NSImage*)sourceImage size:(NSSize)size
{

    NSRect targetFrame = NSMakeRect(0, 0, size.width, size.height);     
    NSImage* targetImage = nil;
    NSImageRep *sourceImageRep =
    [sourceImage bestRepresentationForRect:targetFrame
                                   context:nil
                                     hints:nil];

    targetImage = [[NSImage alloc] initWithSize:size];

    [targetImage lockFocus];
    [sourceImageRep drawInRect: targetFrame];
    [targetImage unlockFocus];

return targetImage; 

}

update

Here is a more elaborate version of a pixel-size-getting category on NSImage... let's assume nothing about the image, how many imageReps it has, whether it has any bitmapImageReps... this will return the largest pixel dimensions it can find. If it can't find bitMapImageRep pixel dimensions it will use whatever else it can get, which will most likely be bounding box dimensions (used by eps and pdfs).

NSImage+PixelSize.h

#import <Cocoa/Cocoa.h>
#import <QuartzCore/QuartzCore.h>

@interface NSImage (PixelSize)

- (NSInteger) pixelsWide;
- (NSInteger) pixelsHigh;
- (NSSize) pixelSize;

@end

NSImage+PixelSize.m

#import "NSImage+PixelSize.h"

@implementation NSImage (Extensions)

- (NSInteger) pixelsWide
{
    /*
     returns the pixel width of NSImage.
     Selects the largest bitmapRep by preference
     If there is no bitmapRep returns largest size reported by any imageRep.
     */
    NSInteger result = 0;
    NSInteger bitmapResult = 0;

    for (NSImageRep* imageRep in [self representations]) {
        if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) {
            if (imageRep.pixelsWide > bitmapResult)
                bitmapResult = imageRep.pixelsWide;
        } else {
            if (imageRep.pixelsWide > result)
                result = imageRep.pixelsWide;
        }
    }
    if (bitmapResult) result = bitmapResult;
    return result;

}

- (NSInteger) pixelsHigh
{
    /*
     returns the pixel height of NSImage.
     Selects the largest bitmapRep by preference
     If there is no bitmapRep returns largest size reported by any imageRep.
     */
    NSInteger result = 0;
    NSInteger bitmapResult = 0;

    for (NSImageRep* imageRep in [self representations]) {
        if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) {
            if (imageRep.pixelsHigh > bitmapResult)
                bitmapResult = imageRep.pixelsHigh;
        } else {
            if (imageRep.pixelsHigh > result)
                result = imageRep.pixelsHigh;
        }
    }
    if (bitmapResult) result = bitmapResult;
    return result;
}

- (NSSize) pixelSize
{
    return NSMakeSize(self.pixelsWide,self.pixelsHigh);
}

@end

You would #import "NSImage+PixelSize.h" in your current file to make it accessible.

With this image category and the resize: method, you would modify your method thus:

//size.width = inputRetinaImage.size.width*0.5;
//size.height = inputRetinaImage.size.height*0.5;
size.width  = inputRetinaImage.pixelsWide*0.5;
size.height = inputRetinaImage.pixelsHigh*0.5;

//[inputRetinaImage setSize:size];
NSImage* outputImage = [self resizeImage:inputRetinaImage size:size];

//NSBitmapImageRep *imgRep = [[inputRetinaImage representations] objectAtIndex: 0];
NSBitmapImageRep *imgRep = [[outputImage representations] objectAtIndex: 0];

That should fix things for you (proviso: I haven't tested it on your code)

foundry
  • 31,615
  • 9
  • 90
  • 125
  • Why not use `bestRepresentationForRect:context:hints:` in all three places? – Peter Hosey Dec 27 '12 at 16:25
  • 1
    I think it might be begging the question... BestRepresentationForRect: suggests we know the final size we are after - which in this case we may not, we are looking to halve the size of the retina... bitmap. Also as an exercise in learning how NSImage hangs together, it's not a bad idea to have a grasp on how the various imageReps actually relate to each other? – foundry Dec 27 '12 at 19:44
  • It's not circular since the rect is in the source image's user space. If you want the whole image, then the rect is `(NSRect){ NSZeroPoint, image.size }`, regardless of the pixel resolutions (or lack thereof) of any of the representations in the image, or of the desired scaled size or resolution. I do agree that understanding the nature of reps is good; however, grabbing the first rep and assuming that it will be what you want is not a good means to that end. – Peter Hosey Dec 28 '12 at 02:49
  • @Peter - firstly can i thank you for awarding me the bonus, that is much appreciated, i am sure my answer could still do with some improvement. Regarding image.size, I agree with your point, but it does skirt around the issue here, which is that we are trying to get at pixel dimensions (as I interpret the question anyway) ... Whereas references to rect/size are getting at display dimensions - and specifically, image.size does not necessarily bear any relation to the 'original' pixel size of the image. I think this is a point worth labouring as it can be the source of so much confusion. – foundry Dec 28 '12 at 03:40
  • @peterhosey, on your last point - that grabbing the first imageReps is a shoddy solution - I do agree, it makes way too many assumptions about the source image, which is why I expanded my answer with a more elaborate pixel size category... I shouldn't have been so hasty! – foundry Dec 28 '12 at 03:44
2

I modified the script i use to downscale my images for you :)

-(BOOL)createNormalImage:(NSString*)inputRetinaImagePath {

    NSImage *inputRetinaImage = [[NSImage alloc] initWithContentsOfFile:inputRetinaImagePath];

    //determine new size
    NSBitmapImageRep* bitmapImageRep = [[inputRetinaImage representations] objectAtIndex:0];
    NSSize size = NSMakeSize(bitmapImageRep.pixelsWide * 0.5,bitmapImageRep.pixelsHigh * 0.5);

    NSLog(@"size = %@", NSStringFromSize(size));

    //get CGImageRef
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)[inputRetinaImage TIFFRepresentation], NULL);
    CGImageRef oldImageRef =  CGImageSourceCreateImageAtIndex(source, 0, NULL);
    CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(oldImageRef);
    if (alphaInfo == kCGImageAlphaNone) alphaInfo = kCGImageAlphaNoneSkipLast;

    // Build a bitmap context
    CGContextRef bitmap = CGBitmapContextCreate(NULL, size.width, size.height, 8, 4 * size.width, CGImageGetColorSpace(oldImageRef), alphaInfo);

    // Draw into the context, this scales the image
    CGContextDrawImage(bitmap, CGRectMake(0, 0, size.width, size.height), oldImageRef);

    // Get an image from the context
    CGImageRef newImageRef = CGBitmapContextCreateImage(bitmap);

    //this does not work in my test.
    NSString *outputFilePath = [[inputRetinaImagePath substringToIndex:inputRetinaImagePath.length - 7] stringByAppendingString:@".png"];

    //but this does!
    NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString* docsDirectory = [paths objectAtIndex:0];
    NSString *newfileName = [docsDirectory stringByAppendingFormat:@"/%@", [outputFilePath lastPathComponent]];

    CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:newfileName];
    CGImageDestinationRef destination = CGImageDestinationCreateWithURL(url, kUTTypePNG, 1, NULL);
    CGImageDestinationAddImage(destination, newImageRef, nil);

    if (!CGImageDestinationFinalize(destination)) {
        NSLog(@"Failed to write image to %@", newfileName);
    }

    CFRelease(destination);

    return true;
}
Tieme
  • 62,602
  • 20
  • 102
  • 156