2

I'm trying to create an ICNS (including a 1024x1024 image) programmatically. Currently I'm creating an NSImage, then I create CGImageRef objects with the appropriate resolution, finally I'm adding them to an icon by using CGImageDestinationAddImage(). Peter Hosey has helped me create '@2x' images already, but the sizes of the images don't wanna be set.

This is the code (still a bit messy, sourcefile represents the path to the image):

NSSize sizes[10];
sizes[0] = NSMakeSize(1024,1024);
sizes[1] = NSMakeSize(512,512);
sizes[2] = NSMakeSize(512,512);
sizes[3] = NSMakeSize(256,256);
sizes[4] = NSMakeSize(256,256);
sizes[5] = NSMakeSize(128,128);
sizes[6] = NSMakeSize(64,64);
sizes[7] = NSMakeSize(32,32);
sizes[8] = NSMakeSize(32,32);
sizes[9] = NSMakeSize(16,16);
int count = 0;
for (int i=0 ; i<10 ; i++) {
    if ([[NSUserDefaults standardUserDefaults] boolForKey:[NSString stringWithFormat:@"Size%i",i+1]]) count++;
}
NSURL *fileURL = [NSURL fileURLWithPath:aPath];

// Create icns
CGImageDestinationRef dr = CGImageDestinationCreateWithURL((CFURLRef)fileURL, kUTTypeAppleICNS , count, NULL);
NSImage *img = [[NSImage alloc] initWithContentsOfFile:sourcefile];
for (int i=0 ; i<10 ; i++) {
    if ([[NSUserDefaults standardUserDefaults] boolForKey:[NSString stringWithFormat:@"Size%i",i+1]]) {

        // Create dictionary
        BOOL is2X = true;
        if (i == 1 || i == 3 || i == 5 || i == 7 || i == 9) is2X = false;
        int dpi = 144, size = (int)(sizes[i].width/2);
        if (!is2X) {dpi = 72;size = sizes[i].width;}
        [img setSize:NSMakeSize(size,size)];
        for (NSImageRep *rep in [img representations])[rep setSize:NSMakeSize(size,size)];
        const void *keys[2] = {kCGImagePropertyDPIWidth, kCGImagePropertyDPIHeight};
        const void *values[2] = {CFNumberCreate(0, kCFNumberSInt32Type, &dpi), CFNumberCreate(0, kCFNumberSInt32Type, &dpi)};
        CFDictionaryRef imgprops = CFDictionaryCreate(NULL, keys, values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);

        // Add image
        NSRect prect = NSMakeRect(0,0,size,size);
        CGImageRef i1 = [img CGImageForProposedRect:&prect context:nil hints:nil];
        CGImageDestinationAddImage(dr, i1, imgprops);
    }
}
CGImageDestinationFinalize(dr);
CFRelease(dr);

size is the width or height that the current image should be. dpi is 144 if we're making an '@2x' image, otherwise it's 72. These values have been checked with NSLog.

The images in the resulting ICNS file are all the same size as the input image. If the size of the input image is 1024x1024, ImageIO complains:

ImageIO: _CGImagePluginWriteICNS unsupported image size (1024 x 1024) - scaling factor: 1

The above error is displayed every time the dpi is 72 and the size is 1024x1024.

I need to know how to set the size of the CGImage that is to be added to the ICNS file.


EDIT: I logged the images:

2012-12-31 12:48:51.281 Eicon[912:680f] |NSImage 0x101b4caf0 Size={512, 512} Reps=(

"NSBitmapImageRep 0x10380b900 Size={512, 512} ColorSpace=(not yet loaded) BPS=8 BPP=(not yet loaded) Pixels=1024x1024 Alpha=YES

Planar=NO Format=(not yet loaded) CurrentBacking=nil (faulting) CGImageSource=0x10380ae70"

)|

2012-12-31 12:48:52.058 Eicon[912:680f] |NSImage 0x101b4caf0 Size={512, 512} Reps=(

"NSBitmapImageRep 0x10380b900 Size={512, 512} ColorSpace=Generic RGB colorspace BPS=8 BPP=32 Pixels=1024x1024 Alpha=YES Planar=NO

Format=2 CurrentBacking=|CGImageRef: 0x101c14630| CGImageSource=0x10380ae70"

)|

2012-12-31 12:48:52.111 Eicon[912:680f] |NSImage 0x101b4caf0 Size={256, 256} Reps=(

"NSBitmapImageRep 0x10380b900 Size={256, 256} ColorSpace=Generic RGB colorspace BPS=8 BPP=32 Pixels=1024x1024 Alpha=YES Planar=NO

Format=2 CurrentBacking=|CGImageRef: 0x101c14630| CGImageSource=0x10380ae70"

)|

2012-12-31 12:48:52.238 Eicon[912:680f] |NSImage 0x101b4caf0 Size={256, 256} Reps=(

"NSBitmapImageRep 0x10380b900 Size={256, 256} ColorSpace=Generic RGB colorspace BPS=8 BPP=32 Pixels=1024x1024 Alpha=YES Planar=NO

Format=2 CurrentBacking=|CGImageRef: 0x101c14630| CGImageSource=0x10380ae70"

)|

2012-12-31 12:48:52.309 Eicon[912:680f] |NSImage 0x101b4caf0 Size={128, 128} Reps=(

"NSBitmapImageRep 0x10380b900 Size={128, 128} ColorSpace=Generic RGB colorspace BPS=8 BPP=32 Pixels=1024x1024 Alpha=YES Planar=NO

Format=2 CurrentBacking=|CGImageRef: 0x101c14630| CGImageSource=0x10380ae70"

)|

2012-12-31 12:48:52.409 Eicon[912:680f] |NSImage 0x101b4caf0 Size={128, 128} Reps=(

"NSBitmapImageRep 0x10380b900 Size={128, 128} ColorSpace=Generic RGB colorspace BPS=8 BPP=32 Pixels=1024x1024 Alpha=YES Planar=NO

Format=2 CurrentBacking=|CGImageRef: 0x101c14630| CGImageSource=0x10380ae70"

)|

2012-12-31 12:48:52.534 Eicon[912:680f] |NSImage 0x101b4caf0 Size={32, 32} Reps=(

"NSBitmapImageRep 0x10380b900 Size={32, 32} ColorSpace=Generic RGB colorspace BPS=8 BPP=32 Pixels=1024x1024 Alpha=YES Planar=NO Format=2

CurrentBacking=|CGImageRef: 0x101c14630| CGImageSource=0x10380ae70"

)|

2012-12-31 12:48:52.616 Eicon[912:680f] |NSImage 0x101b4caf0 Size={32, 32} Reps=(

"NSBitmapImageRep 0x10380b900 Size={32, 32} ColorSpace=Generic RGB colorspace BPS=8 BPP=32 Pixels=1024x1024 Alpha=YES Planar=NO Format=2

CurrentBacking=|CGImageRef: 0x101c14630| CGImageSource=0x10380ae70"

)|

2012-12-31 12:48:52.729 Eicon[912:680f] |NSImage 0x101b4caf0 Size={16, 16} Reps=(

"NSBitmapImageRep 0x10380b900 Size={16, 16} ColorSpace=Generic RGB colorspace BPS=8 BPP=32 Pixels=1024x1024 Alpha=YES Planar=NO Format=2

CurrentBacking=|CGImageRef: 0x101c14630| CGImageSource=0x10380ae70"

)|

2012-12-31 12:48:52.864 Eicon[912:680f] |NSImage 0x101b4caf0 Size={16, 16} Reps=(

"NSBitmapImageRep 0x10380b900 Size={16, 16} ColorSpace=Generic RGB colorspace BPS=8 BPP=32 Pixels=1024x1024 Alpha=YES Planar=NO Format=2

CurrentBacking=|CGImageRef: 0x101c14630| CGImageSource=0x10380ae70"

)|

Cœur
  • 37,241
  • 25
  • 195
  • 267
Fatso
  • 1,278
  • 16
  • 46
  • Be careful with that 64×64 element. 64×64 points isn't valid in IconFamily; you can have 64×64 pixels, but only as 32 @ 2x. The size in between 32 points and 128 points is 48 points. – Peter Hosey Dec 30 '12 at 02:03
  • untrue, apple's icns files have 1024^2 and 64^2 too – Daij-Djan Dec 30 '12 at 02:38
  • Incidentally, is there a reason you're apparently trying to set the same image (sourced from `aFile`) in all of the different representations? Anything that draws an IconFamily will automatically scale the nearest available element, so you could just add the image *once* in its natural size (if it's a valid element size) or the next smaller valid element size. – Peter Hosey Dec 30 '12 at 08:28
  • So I'd make one `NSImage`, then add it repeatedly, changing its size while doing so? – Fatso Dec 30 '12 at 09:14
  • 1
    @PeterHosey /Applications/Preview.app/Contents/Resources/Preview.icns – Daij-Djan Dec 30 '12 at 10:15
  • @Daij-Djan do you have an idea on how to add a 1024x1024 image to an ICNS programmatically? – Fatso Dec 30 '12 at 18:31
  • @Daij-Djan: As I mentioned in my comment on your answer, Preview.icns does not have a 1024×1024 point element. It has a 512×512 point element @ 2x, which is 1024×1024 pixels. – Peter Hosey Dec 30 '12 at 21:31
  • @Korion: No, you don't need to add it repeatedly at all. I doubt you'll get anywhere with an image whose point size is not exactly 1.0 or 2.0 times its pixel size (on each axis). You only need to add it once, in whichever pixel size you have. – Peter Hosey Dec 30 '12 at 21:44
  • @Daij-Djan: Oh, and (also from the same comment), no 64 @ 2x, either. There is a 64-pixel element, which is 32 *points* @ 2x. Read the properties in Preview's Inspector carefully, and keep in mind the difference between points and pixels. – Peter Hosey Dec 30 '12 at 21:46
  • If you're going to use `kCFNumberSInt32Type` when creating the CFNumbers, you should declare the type of `dpi` as `SInt32` to match. – Peter Hosey Dec 31 '12 at 17:43

1 Answers1

1

The error message is correct. You're putting in images of a size that is not supported by the IconFamily format. Specifically, from your output:

2012-12-26 13:48:57.682 Eicon[1131:1b0f] |NSImage 0x1025233b0 Size={11.52, 11.52} Reps=( "NSBitmapImageRep 0x10421fc30 Size={11.52, 11.52} ColorSpace=(not yet loaded) BPS=8 BPP=(not yet loaded) Pixels=1024x1024 Alpha=NO Planar=NO Format=(not yet loaded) CurrentBacking=nil (faulting) CGImageSource=0x104221170"

11.52 points is not a valid size for any element of an IconFamily. You need to find out why this image and rep have that size.

A couple of other things:

  1. As I told you on that other answer, you don't need to change the pixel size of the representation. Leave the pixel size alone. Set the size (point size) of the rep and image (preferably to something valid).
  2. The -[NSImage initWithSize:] documentation says:

    It is permissible to initialize the receiver by passing a size of (0.0, 0.0); however, the receiver’s size must be set to a non-zero value before the NSImage object is used or an exception will be raised.

    You are not setting either object's size, which is what you need to do. (I'm surprised you're not getting an exception about this like the documentation promises.)

As I mentioned on your other question, there is no 1024-point element anymore; the correct specification for a 1024-by-1024-pixel element is as 512 points @ 2x. That's a size (of both image and rep) of (NSSize){ 512.0, 512.0 } (points), with the rep being 1024 pixelsWide and 1024 pixelsHigh.


Looks like I was missing one key ingredient before. Here it is.

The CGImage that you give to the CGImageDestination doesn't have a point size associated with it—only NSImages and NSImageReps have that. The CGImage only has a pixel size; nothing to indicate the image's physical size or resolution.

To tell the CGImageDestination whether a given CGImage is meant to be @ 1x or @ 2x, you need to create a dictionary that gives the image's DPI:

NSDictionary *imageProps1x = @{
    (__bridge NSString *)kCGImagePropertyDPIWidth: @72.0,
    (__bridge NSString *)kCGImagePropertyDPIHeight: @72.0,
};
NSDictionary *imageProps2x = @{
    (__bridge NSString *)kCGImagePropertyDPIWidth: @144.0,
    (__bridge NSString *)kCGImagePropertyDPIHeight: @144.0,
};

Pass the correct dictionary as the last argument to CGImageDestinationAddImage.

Peter Hosey
  • 95,783
  • 15
  • 211
  • 370
  • I should have updated the code. Even if I don't set pixel size and the resulting size of the images are valid, it still results in errors. My image is correct, based on your last paragraph. What I don't understand either is that the same image gets added 10 times (resulting in 10 errors), but when I log the images, they all have a different size. – Fatso Dec 30 '12 at 07:27
  • 1
    I tried setting only the size of the `NSImage` instances, setting the size of the `NSImage`instances and `NSBitmapImageRep`instances, setting the size of both and setting the pixel size. Also tried making `NSImage` instances directly (not by adding a representation to an empty image) and setting their size, still that error. – Fatso Dec 30 '12 at 07:31
  • 1
    @Korion: Please edit your question to include the code that sets the images' and their reps' `size` (but not `pixelsWide` and `pixelsHigh`, since you should *not* be trying to change that) and the log output, including both `size`s *and* the reps' `pixelsWide` and `pixelsHigh`, from it. – Peter Hosey Dec 30 '12 at 08:22
  • Whoever downvoted my answer, please explain what was wrong with it so I can improve it. – Peter Hosey Dec 30 '12 at 21:42
  • Say, if `aPath` is a valid path to a `1024x1024` image. Should this code work then, Peter? `NSURL *fileURL = [NSURL fileURLWithPath:aPath]; CGImageDestinationRef dr = CGImageDestinationCreateWithURL((CFURLRef)fileURL, kUTTypeAppleICNS , source.count, NULL); NSImage *img = [[NSImage alloc] initWithContentsOfFile:sourcefile]; [img setSize:NSMakeSize(512,512)]; CGImageRef i1 = [img CGImageForProposedRect:nil context:nil hints:nil]; CGImageDestinationAddImage(dr, i1, NULL); CGImageDestinationFinalize(dr); CFRelease(dr);` – Fatso Dec 31 '12 at 08:34
  • I'll put a bounty on this question and will award it to you so you get your points back :) I don't think down-voting your answer was necessary at all, it's a good answer, it just doesn't seem to be working ATM. – Fatso Dec 31 '12 at 08:36
  • @Korion: Eh, don't worry about my reputation points. I've got enough of them that I do a bunch of bounties every year for the holidays. – Peter Hosey Dec 31 '12 at 08:55
  • @Korion: In my opinion, it *should*, but it currently does not. I tried it in CodeRunner and it produces the error you saw. In retrospect, this makes sense—the NSImage's and NSImageReps' `size` is not associated with the CGImage. You need to pass the resolution in in the image properties dictionary, which you give to CGImageDestinationAddImage. Use 144.0 DPI (in each dimension) for @ 2x; 72.0 DPI for @ 1x. – Peter Hosey Dec 31 '12 at 09:02
  • That works. I'll add an answer with the solution when I'm completely done but will pick your answer as best, obviously! Thanks!!!! – Fatso Dec 31 '12 at 10:26
  • Also, thanks for not giving me a code answer, it's fun to have to look around yourself a bit. – Fatso Dec 31 '12 at 10:27
  • 1
    @Korion: You're welcome. I'm happy to have helped solve this mystery. – Peter Hosey Dec 31 '12 at 10:31
  • Just one last question : now the DPI does change, and this allows me to create 512x512@2x images. But for some reason, as the input image (located at `aPath`) is 1024x1024@1x, ImageIO complains again when the dpi is 72. Altering the size of the image, altering the size of its representations, altering the pixel size of the `CGImage`, ... nothing works. Any ideas on how to resize the NSImage for real? – Fatso Dec 31 '12 at 11:16
  • There's one thing that seems strange : the 'CurrentBacking' of each image and the CGImageSource is always the same object. Doesn't seem right. Looks like the source image is always added, with its original pixel size, but with different resolutions (depending on whether I set the dpi or not). I've tried everything (changing image size, pixel size of rep, size of rep, setting proposedRect, setting pixel size of CGImage, ...) to alter the size of the image, but it won't work. Maybe I'm missing something silly here. – Fatso Dec 31 '12 at 15:27
  • @Korion: I tried your updated code above with an original 1024×1024 72-dpi PNG as input, with i = 0, and it worked correctly without complaint. The dpi (if any) of the input file shouldn't matter anyway, since that doesn't come along as part of the CGImage (same reason why setting the `size` properties had no effect). – Peter Hosey Dec 31 '12 at 17:55
  • @Korion: Faulting behavior (note that `CurrentBacking` is initially `nil`) notwithstanding, why would the `CurrentBacking` need to change? The source image doesn't change at all. – Peter Hosey Dec 31 '12 at 17:58
  • When i=0 the dpi is set to 144. That works for me too. On the other hand, as soon as i is uneven, the dpi remains 72. Then ImageIO complains. Does it complain when you try that too? – Fatso Dec 31 '12 at 18:00
  • @Korion: Yup. Same problem: Changing the NSImage's `size` has no effect on the CGImage, so the CGImage is still 1024×1024 pixels, no matter what you set the NSImage's `size` to. You could scale it, but it's more work than it's worth—as I said, whatever uses your output later will scale the nearest available resolution automatically anyway, so if you're starting with a single source image, just put that one in. – Peter Hosey Dec 31 '12 at 18:31
  • So how would I add all the necessary images to the icon? Scaling the image? Cuz right now all I can add is the original image, in different resolutions... – Fatso Dec 31 '12 at 18:51
  • @Korion: If all you have to start with is one image whose dimensions match one of the valid element sizes, then you're done. You don't need to add all of the elements; you only need to add at least one. – Peter Hosey Dec 31 '12 at 19:24
  • Oh... But if I do, and it opens in Preview, it doesn't have the other sizes... It's still an icon, but would it be a valid icon? The docs about creating icons list 10 sizes (the ones I coded above). – Fatso Dec 31 '12 at 19:29
  • @Korion: It's perfectly valid. Preview will only show you the elements that the icon contains. You *should* only add elements that you have original artwork for. Synthesizing others is effectively no different from what will happen anyway when the image is eventually drawn; it's just more work. – Peter Hosey Dec 31 '12 at 19:30
  • Okay. Uploading an icon to the app store with only 512x512@2x should thus be okay? Cuz then this question is closed :) – Fatso Dec 31 '12 at 19:40
  • @Korion: I've never published to the App Store, so I don't know. [This](http://developer.apple.com/library/mac/documentation/ToolsLanguages/Conceptual/OSXWorkflowGuide/ConfiguringApplications/ConfiguringApplications.html#//apple_ref/doc/uid/TP40011201-CH3-SW6) says that it must “contain a 512 x 512 image” (doesn't specify points or pixels); [this](https://developer.apple.com/devcenter/mac/checklist/) says it “must include a large app icon with a minimum of 1024 x 1024 pixels”. You'll have to find out for yourself whether an icon containing only 512×512 @ 2x will fly. – Peter Hosey Dec 31 '12 at 19:55
  • I was making a free app to create icons because it's become such a hassle lately since they require 1024x1024 icons and the only way I know that does this is using Terminal or buying an app on the app store. I though, for such a stupid thing, I'd make a simple app so devs don't have to use the console all the time. I'll upload the app with an icon it made to see if it works :). Thanks, now everything works as I wanted it to (currently I *am* rescaling the image and that works, and it isn't that slow). – Fatso Dec 31 '12 at 20:04
  • Correction they require 512x512@2x icons :) – Fatso Dec 31 '12 at 20:04
  • @Korion: Are you talking about scaling the image *up*, or just producing the IconFamily file? Xcode can produce IconFamily files all by itself from an icon set: http://developer.apple.com/library/mac/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Optimizing/Optimizing.html#//apple_ref/doc/uid/TP40012302-CH7-SW4 – Peter Hosey Dec 31 '12 at 20:11