Dan Wood: The Eponymous Weblog (Archives)

Dan Wood Dan Wood is co-owner of Karelia Software, creating programs for the Macintosh computer. He is the father of two kids, lives in the Bay Area of California USA, and prefers bicycles to cars. This site is his older weblog, which mostly covers geeky topics like Macs and Mac Programming. Go visit the current blog here.

Useful Tidbits and Egotistical Musings from Dan Wood

Categories: Business · Mac OS X · Cocoa Programming · General · All Categories

Wed, 28 Feb 2007

File Caching, take 2: using NSURLCache directly

Recently I blogged about a home-spun cache technique. After some discussion in the comments, Robert Blum sent me a snippet of code which uses NSURLCache and related classes. I had no idea these classes could really be used for anything outside of the URL loading system, so I explored them a bit further, and with the help of some other code I found on CocoaBuilder, I got something working.

I've defined two category methods on NSURLCache. My code calls cachedDataForPath: to look and see if the data have already been cached; if not, it is generated and then saved for next time using cacheData:forPath:. Note that this code generates a file:// URL; I'll explain why after the code.

See more ...

Q: When is an connection error not an error?

A: When it's a status code.

If you are using NSURLConnection for accessing data over HTTP, as I like to do from time to time, you may find yourself scratching your head wondering why you didn't receive the connection:didFailWithError: callback in your delegate when clearly there was an error retrieving the resource you were looking for.

The reason is that connections that return an error as its status code (such as a 404 "Not Found" or 500 "Internal Server Error") don't result in a failure callback.

What you need to do is check for status codes in your implementation of connection:didReceiveResponse:. If you are fetching a resource over HTTP, then your NSURLResponse in the callback will actually be NSHTTPURLResponse, and you can query the status code. Any value greater than or equal to 400 is generally an error.

My general pattern is to cancel the connection, package up an NSError object based on that status code, and call the connection:didFailWithError: callback myself. This way I can treat status-code errors the same as I would another kind of error. (I have appropriated the string constant NSHTTPPropertyStatusCodeKey, normally used by NSURLHandle, as the error domain string.)

if ([response respondsToSelector:@selector(statusCode)])
{
  int statusCode = [((NSHTTPURLResponse *)response) statusCode];
  if (statusCode >= 400)
  {
    [connection cancel];  // stop connecting; no more delegate messages
    NSDictionary *errorInfo
      = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:
          NSLocalizedString(@"Server returned status code %d",@""),
            statusCode]
                                    forKey:NSLocalizedDescriptionKey];
    NSError *statusError
      = [NSError errorWithDomain:NSHTTPPropertyStatusCodeKey
                            code:statusCode
                        userInfo:errorInfo];
    [self connection:connection didFailWithError:statusError];
  }
}

Side note: Don't forget that you may recieve the connection:didReceiveResponse: callback multiple times, in the case of redirects!

Wed, 07 Feb 2007

CGImageRef in an NSImage

I recently blogged the praises of CGImageRep. David Catmull suggested in the comments that I make an NSImageRep subclass that draws a CGImageRef, so I can continue to use my NSImage-based code.

Well, it turns out that there is a private class in Cocoa, NSCGImageRep. It's exactly what the doctor ordered. There's a class-dump of its functions here; it's very straightforward.

I put in some code to check that this class exists and responds to the initWithCGImage: method. If the class is available, my code constructs the image by creating and adding this representation instead of performing the conversion I mentioned earlier. It's much cleaner and I assume it will be faster.

It's annoying that NSCGImageRep is a private class. I do hope that Apple will see how useful this class will be do developers and make it part of Leopard. I've filed my bug report (4981955) and I hope you will file a bug report too!

Update: Anybody wishing to file a "me-too" bug report can reference the original report, which is 3880068.

Tue, 06 Feb 2007

Leak Detection Tests

I was hearing about some memory problems with the CIImage to NSImage conversion I was doing. Since I was looking for leaks in just a small area, I came up with a basic technique that I wanted to share.

Essentially what I thought I'd do is to see how a particular method invocation grew memory over repeated use. That way, leaks would be easy to spot with ObjectAlloc (or probably with OmniObjectMeter).

So the basic technique was to just launch the application and then, in an infinite loop, do my processing. Each invocation was wrapped with an NSAutoreleasePool so that the expected behavior is that at the end of each loop, no extra memory has been allocated. I find that this technique, of repeating the process and watching the memory grow, is much easier to deal with than just trying to do an operation once and try to determine what changed.

while (YES)
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  ...
  [pool release];
}

Then you run the test from ObjectAlloc, and set a mark once the loop enters. If you see memory growing, then you have a leak.

Interestingly, I did find some leakage in my core image routines. It wasn't in my code, though! At some point, I need to isolate this and report it as a bug to Apple.

For fellow Cocoa developer bloggers: What are some debugging techniques you have come up with?

Sun, 04 Feb 2007

Preserve Metadata when resizing JPEG images

One thing that Sandvox does is to make resized copies of image files. There are often a lot of useful metadata in a JPEG file, so I decided that it would be a good idea to preserve these as much as possible if scaling down a JPEG file to another size. Many but not all of these are supported by NSImage, so it's not hard to extract from the original image and put in the other.

To do this, you just need to extract the metadata from the source and apply to the new image. You can get the NSImageEXIFData and NSImageColorSyncProfileData and apply to the new image.

Here's a category method on NSImage that will do the trick. You pass in the original bitmap that the scaled image (self) will use for its source metadata.

- (NSData *)JPEGRepresentationWithQuality:(float)aQuality
    originalBitmap:(NSBitmapImageRep *)parentImage;
{
    NSMutableDictionary *props;
    if (nil != parentImage)
    {
        NSSet *propsToExtract = [NSSet setWithObjects: NSImageEXIFData,
            NSImageColorSyncProfileData, nil];
        props = [parentImage dictionaryOfPropertiesWithSetOfKeys:
                    propsToExtract];
        
        // Fix up dictionary to take out ISOSpeedRatings since that
        // doesn't appear to be settable
        NSDictionary *exifDict = [props objectForKey:NSImageEXIFData];
        if (nil != exifDict)
        {
            NSMutableDictionary *dict
              = [NSMutableDictionary dictionaryWithDictionary:exifDict];
            [dict removeObjectForKey:@"ISOSpeedRatings"];
            [props setObject:dict forKey:NSImageEXIFData];
        }
    }
    else
    {
        props = [NSMutableDictionary dictionary];
    }
    // Now set our desired compression property, and make the image NOT
    // progressive for the benefit of the flash-based viewers who care
    // about that kind of thing
    [props setObject:[NSNumber numberWithFloat:aQuality] 
        forKey:NSImageCompressionFactor];
    [props setObject:[NSNumber numberWithBool:NO]
        forKey:NSImageProgressive];
    
    NSData *result = [[self bitmap]	// method to get images's bitmap
        representationUsingType:NSJPEGFileType
                     properties:props];
    
    return result;
}

PNG files have a bit of metadata too; this technique could also apply for PNG to PNG conversion as well.