From fbe13be77a59162c5175e449406c7d351e0aa350 Mon Sep 17 00:00:00 2001 From: "Michael J. Rubinsky" Date: Mon, 10 Aug 2009 18:57:52 -0400 Subject: [PATCH] Greatly simplify and (hopefully improve) image rescaling using Core Image API. 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 | 135 ++++++++++++++------- .../iPhoto2Ansel.xcodeproj/project.pbxproj | 16 --- 2 files changed, 89 insertions(+), 62 deletions(-) diff --git a/iPhoto2Ansel/AnselExportController.m b/iPhoto2Ansel/AnselExportController.m index 73fec5559..b66aa3d83 100644 --- a/iPhoto2Ansel/AnselExportController.m +++ b/iPhoto2Ansel/AnselExportController.m @@ -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]; diff --git a/iPhoto2Ansel/iPhoto2Ansel.xcodeproj/project.pbxproj b/iPhoto2Ansel/iPhoto2Ansel.xcodeproj/project.pbxproj index 554bdcea7..0c591b34c 100644 --- a/iPhoto2Ansel/iPhoto2Ansel.xcodeproj/project.pbxproj +++ b/iPhoto2Ansel/iPhoto2Ansel.xcodeproj/project.pbxproj @@ -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 */ @@ -83,12 +80,8 @@ B00EF5590EF5DD5900A9D71C /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/AnselServers.nib; sourceTree = ""; }; B00EF5650EF5E22900A9D71C /* TURAnselServersPanelController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TURAnselServersPanelController.h; sourceTree = ""; }; B00EF5660EF5E22900A9D71C /* TURAnselServersPanelController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TURAnselServersPanelController.m; sourceTree = ""; }; - B03B92BA0FB5F43600F628AF /* TURAnselGalleryNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TURAnselGalleryNode.h; sourceTree = ""; }; - B03B92BB0FB5F43600F628AF /* TURAnselGalleryNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TURAnselGalleryNode.m; sourceTree = ""; }; 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 = ""; }; - B05C4A040EE9E001005B4B28 /* ImageResizer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ImageResizer.h; sourceTree = ""; }; - B05C4A050EE9E001005B4B28 /* ImageResizer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ImageResizer.m; sourceTree = ""; }; B06C1E010EB1644600BFAFCB /* AnselExportPluginBox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AnselExportPluginBox.h; sourceTree = ""; }; B06C1E020EB1644600BFAFCB /* AnselExportPluginBox.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AnselExportPluginBox.m; sourceTree = ""; }; B06C1E040EB164D900BFAFCB /* AnselExportController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AnselExportController.h; sourceTree = ""; }; @@ -132,7 +125,6 @@ B0C8884D0ED85E02000E19FB /* FBProgressController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBProgressController.m; sourceTree = ""; }; B0CCED400EEC6E810012D3D3 /* TURAnselGalleryPanelController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TURAnselGalleryPanelController.h; sourceTree = ""; }; B0CCED410EEC6E810012D3D3 /* TURAnselGalleryPanelController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TURAnselGalleryPanelController.m; sourceTree = ""; }; - B0DF843B0EE9E6EB000DAA9E /* QuickTime.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickTime.framework; path = /System/Library/Frameworks/QuickTime.framework; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -142,7 +134,6 @@ 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 */, ); @@ -213,7 +204,6 @@ children = ( B0B666730FB34604009459D5 /* Quartz.framework */, B0B666740FB34604009459D5 /* QuartzCore.framework */, - B0DF843B0EE9E6EB000DAA9E /* QuickTime.framework */, 089C1672FE841209C02AAC07 /* Foundation.framework */, 1058C7ADFEA557BF11CA2CBB /* Cocoa.framework */, ); @@ -239,8 +229,6 @@ 32C88E010371C26100C91783 /* Other Sources */ = { isa = PBXGroup; children = ( - B05C4A040EE9E001005B4B28 /* ImageResizer.h */, - B05C4A050EE9E001005B4B28 /* ImageResizer.m */, B0C8884D0ED85E02000E19FB /* FBProgressController.m */, B0C8884C0ED85E02000E19FB /* FBProgressController.h */, B07D44F50EC2AEC700B59765 /* TURXMLConnection.h */, @@ -271,8 +259,6 @@ B07D426F0EC230B100B59765 /* TURAnselGallery.m */, B00EF5650EF5E22900A9D71C /* TURAnselServersPanelController.h */, B00EF5660EF5E22900A9D71C /* TURAnselServersPanelController.m */, - B03B92BA0FB5F43600F628AF /* TURAnselGalleryNode.h */, - B03B92BB0FB5F43600F628AF /* TURAnselGalleryNode.m */, ); name = AnselToolkit; sourceTree = ""; @@ -434,11 +420,9 @@ 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; }; -- 2.11.0