Greatly simplify and (hopefully improve) image rescaling using Core Image API.
authorMichael J. Rubinsky <mrubinsk@horde.org>
Mon, 10 Aug 2009 22:57:52 +0000 (18:57 -0400)
committerMichael J. Rubinsky <mrubinsk@horde.org>
Mon, 10 Aug 2009 23:00:29 +0000 (19:00 -0400)
This gets rid of the complex QuickTime code I "borrowed" from Gallery iPhoto exporter
and does it more efficiently - while also allowing us to both preserve and add to
metadata other than EXIF.

iPhoto2Ansel/AnselExportController.m
iPhoto2Ansel/iPhoto2Ansel.xcodeproj/project.pbxproj

index 73fec55..b66aa3d 100644 (file)
@@ -548,24 +548,26 @@ NSString * const TURAnselServerPasswordKey = @"password";
         
         /*** Pull out (and generate) all desired metadata before rescaling the image ***/
         // The CGImageSource for getting the image INTO Quartz
+        // TODO: All the metadata stuff needs to be pulled out into it's own class
         CGImageSourceRef source;
         
         // Dictionary to hold all metadata
         NSMutableDictionary *metadata;
         
-        // Read the image data into Quartz
-        NSURL *url = [NSURL fileURLWithPath: [mExportMgr imagePathAtIndex:i]];
-               source = CGImageSourceCreateWithURL((CFURLRef)url, NULL);
-        
-        // Prepare to get the data OUT of Quartz
-        NSData *data = [NSMutableData data];
-        CGImageDestinationRef destination = CGImageDestinationCreateWithData((CFMutableDataRef)data, (CFStringRef)@"public.jpeg", 1, NULL);
+        // Read the image into ImageIO (the only API that supports more then just EXIF metadata)
+        // Read it into a NSData object first, since we'll need that later on anyway...
+        NSData *theImageData = [[NSData alloc] initWithContentsOfFile: [mExportMgr imagePathAtIndex:i]];
+        source = CGImageSourceCreateWithData((CFDataRef)theImageData, NULL);
         
         // Get the metadata dictionary, cast it to NSDictionary the get a mutable copy of it
         CFDictionaryRef metadataRef = CGImageSourceCopyPropertiesAtIndex(source, 0, NULL);
         NSDictionary *immutableMetadata = (NSDictionary *)metadataRef;
         metadata = [immutableMetadata mutableCopy];
+        
+        // Clean up some stuff we own that we don't need anymore
+        immutableMetadata = nil;
         CFRelease(metadataRef);
+        CFRelease(source);
         
         // Get a mutable copy of the IPTC Dictionary for the image...create a 
         // new one if one doesn't exist in the image.
@@ -574,31 +576,31 @@ NSString * const TURAnselServerPasswordKey = @"password";
         if (!iptcDict) {
             iptcDict = [[NSMutableDictionary alloc] init];
         }
+        iptcData = nil;
         
-        // Get the keywords from the image and put it into the dictionary...should we check for them first?
+        // Get the keywords from the image and put it into the dictionary...
+        // TODO: should we check for any existing keywords first?
         NSArray *keywords = [mExportMgr imageKeywordsAtIndex: i];
-        [iptcDict setObject:keywords  forKey:(NSString *)kCGImagePropertyIPTCKeywords];
+        if (keywords) {
+            [iptcDict setObject:keywords  forKey:(NSString *)kCGImagePropertyIPTCKeywords];
+        }    
         
-        // Put the IPTC Dictionary (back?) into the metadata dictionary
-        [metadata setObject:iptcDict forKey:(NSString *)kCGImagePropertyIPTCDictionary];
-
-        // Get the data out of quartz (image data is in *data now.
-        CGImageDestinationAddImageFromSource(destination, source, 0, (CFDictionaryRef)metadata);
-        BOOL success = CGImageDestinationFinalize(destination);        
+        // Add the title to the ObjectName field
+        NSString *imageDescription = [mExportMgr imageTitleAtIndex:i];
+        [iptcDict setObject:imageDescription forKey:(NSString *)kCGImagePropertyIPTCObjectName];
         
-        CFRelease(source);
-        CFRelease(destination);
+        // Add any ratings...not sure what Ansel will do with them yet, but no harm in including them
+        // eh...seems like quartz mistakenly puts this value into the keywords field???
+        //NSNumber *imageRating = [NSNumber numberWithInt: [mExportMgr imageRatingAtIndex:i]];
+        //[iptcDict setObject:imageRating forKey:(NSString *)kCGImagePropertyIPTCStarRating];
         
-        if (!success) {
-           // ??
-        }
+        // Add the IPTC Dictionary back into the metadata dictionary....we use this
+        // after the image is scaled.
+        [metadata setObject:iptcDict forKey:(NSString *)kCGImagePropertyIPTCDictionary];
         
-        /*** TODO: This is wasteful, but do it this way for now to just test if this works as expected ***/
-        // Need to resize the image, but all metadata is lost...ideally we should only read the image in once though...
-        NSData *theImage = [[NSData alloc] initWithContentsOfFile: [mExportMgr imagePathAtIndex:i]];
-        //NSData *theImage = (NSData *)data;
         
-        CGFloat imageSize;
+        // Prepare to scale the image now that we have the metadata out of it
+        float imageSize;
         switch([mSizePopUp selectedTag])
         {
             case 0:
@@ -618,41 +620,82 @@ NSString * const TURAnselServerPasswordKey = @"password";
                 break;
         }
         
-        
         [self postProgressStatus: [NSString stringWithFormat: @"Resizing image %d out of %d", (i+1), count]];
         
-        // Don't resize if we want original image...it will lose some metadata needlessly.
+        // Don't even touch this code if we are uploading the original image
         NSData *scaledData;
         if ([mSizePopUp selectedTag] != 3) {
-           scaledData = [ImageResizer getScaledImageFromData: theImage
-                                                      toSize: NSMakeSize(imageSize, imageSize)];
+            
+            // Put the image data into CIImage
+            CIImage *im = [CIImage imageWithData: theImageData];
+            
+            // Calculate the scale factor and the actual dimensions.
+            float yscale;
+            if([im extent].size.height > [im extent].size.width) {
+                yscale = imageSize / [im extent].size.height;
+            }  else {
+                yscale = imageSize / [im extent].size.width;
+            }
+            float finalW = ceilf(yscale * [im extent].size.width);
+            float finalH = ceilf(yscale * [im extent].size.height);
+            
+            // Do an affine clamp (This essentially make the image extent
+            // infinite but removes problems with certain image sizes causing
+            // edge artifacts.
+            CIFilter *clamp = [CIFilter filterWithName:@"CIAffineClamp"];
+            [clamp setValue:[NSAffineTransform transform] forKey:@"inputTransform"];
+            [clamp setValue:im forKey:@"inputImage"];
+            im = [clamp valueForKey:@"outputImage"];
+            
+            // Now perform the scale
+            CIFilter *f = [CIFilter filterWithName: @"CILanczosScaleTransform"];
+            [f setDefaults];
+            [f setValue:[NSNumber numberWithFloat:yscale]
+                 forKey:@"inputScale"];
+            [f setValue:[NSNumber numberWithFloat:1.0]
+                 forKey:@"inputAspectRatio"];
+            [f setValue:im forKey:@"inputImage"];
+            im = [f valueForKey:@"outputImage"];
+            
+            // Crop back to finite dimensions
+            CIFilter *crop = [CIFilter filterWithName:@"CICrop"];
+            [crop setValue:[CIVector vectorWithX:0.0
+                                               Y:0.0                                               
+                                               Z: finalW
+                                               W: finalH]
+                    forKey:@"inputRectangle"];
+            
+            [crop setValue: im forKey:@"inputImage"];
+            im = [crop valueForKey:@"outputImage"];
+   
+            // Now get the image back out into a NSData object
+            NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithCIImage: im];
+            NSDictionary *properties = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithFloat: 0.9], NSImageCompressionFactor,
+            [NSNumber numberWithInt: 0], NSImageCompressionMethod, nil];
+            scaledData = [bitmap representationUsingType:NSJPEG2000FileType properties:properties];    
+
         } else {
-            scaledData = theImage;
+            scaledData = theImageData;
         }
         
         // Now we have resized image data, put back the metadata...
         source = CGImageSourceCreateWithData((CFDataRef)scaledData, NULL);
+        NSData *newData = [[NSMutableData alloc] init];
+        CGImageDestinationRef destination = CGImageDestinationCreateWithData((CFMutableDataRef)newData, (CFStringRef)@"public.jpeg", 1, NULL);
         
-        // Should we release, or clear or use a new data object?
-        NSData *newData = [NSMutableData data];
-        destination = CGImageDestinationCreateWithData((CFMutableDataRef)newData, (CFStringRef)@"public.jpeg", 1, NULL);
-        
-        // Get the data out of quartz (image data is in the NSData *data object now.
-        CGImageDestinationAddImageFromSource(destination, source, 0, (CFDictionaryRef)metadata);
-        success = CGImageDestinationFinalize(destination); // write metadata into the data object
-        
+        // Get the data out of quartz (image data is in the NSData *newData object now.
+         CGImageDestinationAddImageFromSource(destination, source, 0, (CFDictionaryRef)metadata);
+         CGImageDestinationFinalize(destination);
         [self postProgressStatus: [NSString stringWithFormat: @"Encoding image %d out of %d", (i+1), count]];
-//        NSString *base64ImageData = [NSString base64StringFromData: scaledData  
-//                                                            length: [scaledData length]];
         NSString *base64ImageData = [NSString base64StringFromData: newData  
                                                             length: [newData length]];
+        [newData release];
+        [theImageData release];
         
         // Get the filename/path for this image. This returns either the most
         // recent version of the image, the original, or (if RAW) the jpeg 
         // version of the original.
         NSString *filename = [mExportMgr imageFileNameAtIndex:i];
-        NSString *imageDescription = [mExportMgr imageTitleAtIndex:i];
-       // NSArray *keywords = [mExportMgr imageKeywordsAtIndex: i];
         
         NSArray *keys = [[NSArray alloc] initWithObjects:
                          @"filename", @"description", @"data", @"type", @"tags", nil];
@@ -666,10 +709,10 @@ NSString * const TURAnselServerPasswordKey = @"password";
                            keywords,
                            nil];
         
-        NSDictionary *imageData = [[NSDictionary alloc] initWithObjects:values
-                                                                forKeys:keys];
+        NSDictionary *imageDataDict = [[NSDictionary alloc] initWithObjects:values
+                                                                    forKeys:keys];
         NSDictionary *params = [[NSDictionary alloc] initWithObjectsAndKeys:
-                                imageData, @"data", 
+                                imageDataDict, @"data", 
                                 [NSNumber numberWithBool:NO], @"default",
                                 nil];
         
@@ -678,7 +721,7 @@ NSString * const TURAnselServerPasswordKey = @"password";
         [currentGallery uploadImageObject: params];
         [keys release];
         [values release];
-        [imageData release];
+        [imageDataDict release];
         [params release];
         [iptcDict release];
         [pool release];
index 554bdce..0c591b3 100644 (file)
@@ -11,9 +11,7 @@
                8D5B49B4048680CD000E48DA /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */; };
                B00EF55A0EF5DD5900A9D71C /* AnselServers.nib in Resources */ = {isa = PBXBuildFile; fileRef = B00EF5580EF5DD5900A9D71C /* AnselServers.nib */; };
                B00EF5670EF5E22900A9D71C /* TURAnselServersPanelController.m in Sources */ = {isa = PBXBuildFile; fileRef = B00EF5660EF5E22900A9D71C /* TURAnselServersPanelController.m */; };
-               B03B92BC0FB5F43600F628AF /* TURAnselGalleryNode.m in Sources */ = {isa = PBXBuildFile; fileRef = B03B92BB0FB5F43600F628AF /* TURAnselGalleryNode.m */; };
                B04FC1A90EEB4A2B008EEB0E /* AnselGalleryPanel.nib in Resources */ = {isa = PBXBuildFile; fileRef = B04FC1A70EEB4A2B008EEB0E /* AnselGalleryPanel.nib */; };
-               B05C4A060EE9E001005B4B28 /* ImageResizer.m in Sources */ = {isa = PBXBuildFile; fileRef = B05C4A050EE9E001005B4B28 /* ImageResizer.m */; };
                B06C1E030EB1644600BFAFCB /* AnselExportPluginBox.m in Sources */ = {isa = PBXBuildFile; fileRef = B06C1E020EB1644600BFAFCB /* AnselExportPluginBox.m */; };
                B06C1E060EB164D900BFAFCB /* AnselExportController.m in Sources */ = {isa = PBXBuildFile; fileRef = B06C1E050EB164D900BFAFCB /* AnselExportController.m */; };
                B06C1E3D0EB17E3700BFAFCB /* Panel.nib in Resources */ = {isa = PBXBuildFile; fileRef = B06C1E3C0EB17E3700BFAFCB /* Panel.nib */; };
@@ -47,7 +45,6 @@
                B0C888490ED85DEA000E19FB /* ProgressSheet.nib in Resources */ = {isa = PBXBuildFile; fileRef = B0C888480ED85DEA000E19FB /* ProgressSheet.nib */; };
                B0C8884E0ED85E02000E19FB /* FBProgressController.m in Sources */ = {isa = PBXBuildFile; fileRef = B0C8884D0ED85E02000E19FB /* FBProgressController.m */; };
                B0CCED420EEC6E810012D3D3 /* TURAnselGalleryPanelController.m in Sources */ = {isa = PBXBuildFile; fileRef = B0CCED410EEC6E810012D3D3 /* TURAnselGalleryPanelController.m */; };
-               B0DF843C0EE9E6EB000DAA9E /* QuickTime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B0DF843B0EE9E6EB000DAA9E /* QuickTime.framework */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
                B00EF5590EF5DD5900A9D71C /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/AnselServers.nib; sourceTree = "<group>"; };
                B00EF5650EF5E22900A9D71C /* TURAnselServersPanelController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TURAnselServersPanelController.h; sourceTree = "<group>"; };
                B00EF5660EF5E22900A9D71C /* TURAnselServersPanelController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TURAnselServersPanelController.m; sourceTree = "<group>"; };
-               B03B92BA0FB5F43600F628AF /* TURAnselGalleryNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TURAnselGalleryNode.h; sourceTree = "<group>"; };
-               B03B92BB0FB5F43600F628AF /* TURAnselGalleryNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TURAnselGalleryNode.m; sourceTree = "<group>"; };
                B03D3B590ED5BB3800CF5B92 /* XMLRPC-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "XMLRPC-Info.plist"; sourceTree = SOURCE_ROOT; };
                B04FC1A80EEB4A2B008EEB0E /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/AnselGalleryPanel.nib; sourceTree = "<group>"; };
-               B05C4A040EE9E001005B4B28 /* ImageResizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ImageResizer.h; sourceTree = "<group>"; };
-               B05C4A050EE9E001005B4B28 /* ImageResizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ImageResizer.m; sourceTree = "<group>"; };
                B06C1E010EB1644600BFAFCB /* AnselExportPluginBox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AnselExportPluginBox.h; sourceTree = "<group>"; };
                B06C1E020EB1644600BFAFCB /* AnselExportPluginBox.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AnselExportPluginBox.m; sourceTree = "<group>"; };
                B06C1E040EB164D900BFAFCB /* AnselExportController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AnselExportController.h; sourceTree = "<group>"; };
                B0C8884D0ED85E02000E19FB /* FBProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBProgressController.m; sourceTree = "<group>"; };
                B0CCED400EEC6E810012D3D3 /* TURAnselGalleryPanelController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TURAnselGalleryPanelController.h; sourceTree = "<group>"; };
                B0CCED410EEC6E810012D3D3 /* TURAnselGalleryPanelController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TURAnselGalleryPanelController.m; sourceTree = "<group>"; };
-               B0DF843B0EE9E6EB000DAA9E /* QuickTime.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickTime.framework; path = /System/Library/Frameworks/QuickTime.framework; sourceTree = "<absolute>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
                        files = (
                                B0BFBC780ED5B2AB006581A5 /* XMLRPC.framework in Frameworks */,
                                8D5B49B4048680CD000E48DA /* Cocoa.framework in Frameworks */,
-                               B0DF843C0EE9E6EB000DAA9E /* QuickTime.framework in Frameworks */,
                                B0B666750FB34604009459D5 /* Quartz.framework in Frameworks */,
                                B0B666760FB34604009459D5 /* QuartzCore.framework in Frameworks */,
                        );
                        children = (
                                B0B666730FB34604009459D5 /* Quartz.framework */,
                                B0B666740FB34604009459D5 /* QuartzCore.framework */,
-                               B0DF843B0EE9E6EB000DAA9E /* QuickTime.framework */,
                                089C1672FE841209C02AAC07 /* Foundation.framework */,
                                1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */,
                        );
                32C88E010371C26100C91783 /* Other Sources */ = {
                        isa = PBXGroup;
                        children = (
-                               B05C4A040EE9E001005B4B28 /* ImageResizer.h */,
-                               B05C4A050EE9E001005B4B28 /* ImageResizer.m */,
                                B0C8884D0ED85E02000E19FB /* FBProgressController.m */,
                                B0C8884C0ED85E02000E19FB /* FBProgressController.h */,
                                B07D44F50EC2AEC700B59765 /* TURXMLConnection.h */,
                                B07D426F0EC230B100B59765 /* TURAnselGallery.m */,
                                B00EF5650EF5E22900A9D71C /* TURAnselServersPanelController.h */,
                                B00EF5660EF5E22900A9D71C /* TURAnselServersPanelController.m */,
-                               B03B92BA0FB5F43600F628AF /* TURAnselGalleryNode.h */,
-                               B03B92BB0FB5F43600F628AF /* TURAnselGalleryNode.m */,
                        );
                        name = AnselToolkit;
                        sourceTree = "<group>";
                                B07D42710EC230B100B59765 /* TURAnselGallery.m in Sources */,
                                B07D44F70EC2AEC700B59765 /* TURXMLConnection.m in Sources */,
                                B0C8884E0ED85E02000E19FB /* FBProgressController.m in Sources */,
-                               B05C4A060EE9E001005B4B28 /* ImageResizer.m in Sources */,
                                B0CCED420EEC6E810012D3D3 /* TURAnselGalleryPanelController.m in Sources */,
                                B00EF5670EF5E22900A9D71C /* TURAnselServersPanelController.m in Sources */,
                                B0B6669B0FB357B3009459D5 /* AnselGalleryViewItem.m in Sources */,
-                               B03B92BC0FB5F43600F628AF /* TURAnselGalleryNode.m in Sources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };