Working with images (CGImage), exif data, and file icons - cocoa

What I am trying to do (under 10.6)....
I have an image (jpeg) that includes an icon in the image file (that is you see an icon based on the image in the file, as opposed to a generic jpeg icon in file open dialogs in a program). I wish to edit the exif metadata, save it back to the image in a new file. Ideally I would like to save this back to an exact copy of the file (i.e. preserving any custom embedded icons created etc.), however, in my hands the icon is lost.
My code (some bits removed for ease of reading):
// set up source ref I THINK THE PROBLEM IS HERE - NOT GRABBING THE INITIAL DATA
CGImageSourceRef source = CGImageSourceCreateWithURL( (CFURLRef) URL,NULL);
// snag metadata
NSDictionary *metadata = (NSDictionary *) CGImageSourceCopyPropertiesAtIndex(source,0,NULL);
// make metadata mutable
NSMutableDictionary *metadataAsMutable = [[metadata mutableCopy] autorelease];
// grab exif
NSMutableDictionary *EXIFDictionary = [[[metadata objectForKey:(NSString *)kCGImagePropertyExifDictionary] mutableCopy] autorelease];
<< edit exif >>
// add back edited exif
[metadataAsMutable setObject:EXIFDictionary forKey:(NSString *)kCGImagePropertyExifDictionary];
// get source type
CFStringRef UTI = CGImageSourceGetType(source);
// set up write data
NSMutableData *data = [NSMutableData data];
CGImageDestinationRef destination = CGImageDestinationCreateWithData((CFMutableDataRef)data,UTI,1,NULL);
//add the image plus modified metadata PROBLEM HERE? NOT ADDING THE ICON
CGImageDestinationAddImageFromSource(destination,source,0, (CFDictionaryRef) metadataAsMutable);
// write to data
BOOL success = NO;
success = CGImageDestinationFinalize(destination);
// save data to disk
[data writeToURL:saveURL atomically:YES];
//cleanup
CFRelease(destination);
CFRelease(source);
I don't know if this is really a question of image handling, file handing, post-save processing (I could use sip), or me just being think (I suspect the last).
Nick

Don't you just hate it when you post something and then find the answer....
The way to deal with this is to use:
// grab the original unique icon
NSImage *theicon = [[NSWorkspace sharedWorkspace] iconForFile:full_file_path_of_original]];
// add it to the file after you have saved
BOOL done = [[NSWorkspace sharedWorkspace] setIcon:theicon forFile:full_file_path_to_new_file options:NSExcludeQuickDrawElementsIconCreationOption];
Doh!

Related

Convert data to an UIImageView

I try to convert data from parse.com into my UIImageView.
for (PFObject *object in objects)
{
PFFile *file = [object objectForKey:#"imageData"];
[file getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
[contentImageArray addObject: [UIImage imageWithData:data]];
}];
}
I load my data in a NSMutableArray, this still works, but then I want my data in an ImageView.
So my code:
cell.contentImage.image = [UIImage imageWithData:[contentImageArray objectAtIndex:(contentImageArray.count - indexPath.row - 1)]];
But it does not work, I just get an error message, please help!
First, you need to check the error and that the data isn't nil because this isn't safe:
[file getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
[contentImageArray addObject: [UIImage imageWithData:data]];
}];
if parse has an issue returning the data your app will crash.
Also, in that code you are already creating the image from the data, so you don't need to do it again:
cell.contentImage.image = [contentImageArray objectAtIndex:(contentImageArray.count - indexPath.row - 1)];
Finally, you're making assumptions about what index each image has in the array and you have very specific indexing logic. That's unlikely to work well / properly in the future, even if it does now. You load images in the background and put them into an array - you have no idea what order the images are going to load in... You should apply logic to deal with that. If you load the table when you don't have any images they you'll be trying to get the (0 - 0 - 1)'th image from the array, and that isn't going to exist.
From your follow on comment:
Populate your contentImageArray with instances of NSNull for each of the objects you have. When you iterate the objects, keep the index and then when the image is ready you can:
[contentImageArray replaceObjectAtIndex:i withObject:[UIImage imageWithData:data]];
and when you try to use the image, check if it's available yet:
id image = [contentImageArray objectAtIndex:indexPath];
cell.contentImage.image = ([image isKindOfClass:[UIImage class]] ? image : nil);

Replace NSTextattachement Image on NSMutableattributedString

I'm building an UITextView with text and images (Subclassing NSTextstorage for displaying my content)
I'm having textcontent with images URLs.
So my problem is that i need download all the images if they're not cached.
So i want to first insert a placeholder image, download the image and then replace the placeholder image by the downloaded one.
Here's how i do my stuff.
First, i'm formatting my text with images url by replacing all urls with this tag :
[IMG]url[/IMG]
Then i'm using a regex to get all these tags.
I'm testing if there's a cached image or not. If not, i extract all the urls, download them and cache them.
I've created an NSObject class ImageCachingManager and declared a delegate method called when an image has been downloaded :
#protocol ImageCachingManagerDelegate <NSObject>
- (void)managerDidCacheImage:(UIImage *)image forUrl:(NSString *)url;
#end
Like this, I tough that I could use the url of the image got by the delegate method to search the matching url in my NSTextstorage attributedString and replace the current NSTextattachement image by the downloaded one.
But I don't know how to do that...
Thanks for help !
I'm working on something very similar to this at the moment and think this might help. The code is very much alpha but hopefully it will get you to the next step - I'll step through:
Overall Cycle
1. Find you image tags in the full text piece using Reg Ex or XPath - personally i find Hppl to be more powerful but if your content is well structured and reliable, regex is probably fine.
https://github.com/topfunky/hpple
Reduce the space of this match to 1 character and store that range - A textAttachment occupies only 1 character of space within a textview so it's best to reduce this to 1 otherwise when you replace your first match of characters in a range with the first textattachment the next range marker becomes out of date which will lead to issues. Depending on how much processing you need to do this text input during init, this is an important step, i have to do a lot of processing on the text and the ranges change during this parsing so I created an array of special characters that I know is never going to be in the inputs and push these single characters into the reserved space, at the same time i store this special character and the src of the image in an array of a very simple NSObject subclass that stores the SpecialChar, ImgSrc plus has space for the NSRange but i basically find the special character later in the process again because it has been moved about since this point and then set the nsrange at the very end of processing - this may not be necessary in your case but the principle is the same; You need a custom object with NsRange (which will become a text attachment) and the imgSource.
Loop through this array to add placeholder imageAttachments to your attributed string. You can do this by adding a transparent image or a 'loading' image. You could also check your cache for existing images during this point and skipping the placeholder if it exists in cache.
Using your delegate, when the image is successfully downloaded, you need to replace the current attachment with your new one. By replacing the placeholder in the range you've already stored in your object. Create a placeholder attributedString with the NSTextAttachment and then replace that range as below.
Some sample code:
Steps 1 & 2:
specialCharsArray = [[NSArray alloc]initWithObjects:#"Û", #"±", #"¥", #"å", #"æ", #"Æ", #"Ç", #"Ø", #"õ", nil];
//using Hppl
NSString *allImagesXpathQueryString = #"//img/#src";
NSArray *imageArray = [bodyTextParser searchWithXPathQuery:allImagesXpathQueryString];
//
imageRanges = [[NSMutableArray alloc] init];
if([imageArray count]){
for (TFHppleElement *element in imageArray) {
int i = 0;
NSString *imgSource = [[[element children] objectAtIndex:0] content];
NSString *replacementString = [specialCharsArray objectAtIndex:i];
UIImage *srcUIImage = [UIImage imageNamed:imgSource];
[srcUIImage setAccessibilityIdentifier:imgSource]; //only needed if you need to reference the image filename later as it's lost in a UIImage if stored directly
//imagePlacement is NSObject subclass to store the range, replacement and image as above
imagePlacement *foundImage = [[imagePlacement alloc]init] ;
[foundImage initWithSrc:srcUIImage replacement:replacementString];
[imageRanges addObject:foundImage];
i++;
}
Step 3:
-(void)insertImages{
if ([imageRanges count]) {
[self setScrollEnabled:NO]; //seems buggy with scrolling on
int i = 0; //used to track the array placement for tag
for(imagePlacement *myImagePlacement in imageRanges){
// creates a text attachment with an image
NSMutableAttributedString *placeholderAttString = [[NSMutableAttributedString alloc]initWithAttributedString:self.attributedText];
NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
//scales image down to ration of width of view - you probably don't need this
CGSize scaleToView = imagePlacement.imgSrc.size;
scaleToView.width = self.frame.size.width;
scaleToView.height = (self.frame.size.width/imagePlacement.imgSrc.size.width)*imagePlacement.imgSrc.size.height;
attachment.image = [self imageWithColor:[UIColor clearColor] andSize:scaleToView];
NSMutableAttributedString *imageAttrString = [[NSAttributedString attributedStringWithAttachment:attachment] mutableCopy];
[self setAttributedText:placeholderAttString];
i++;
}
}
[self setScrollEnabled:YES];
}
- (UIImage *)imageWithColor:(UIColor *)color andSize:(CGSize) size {
CGRect rect = CGRectMake(0.0f, 0.0f, size.width, size.height);
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [color CGColor]);
CGContextFillRect(context, rect);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}

Loading retina images from a subdirectory

I'm using NSDrawNinePartImage() to draw a stretchable control. Since this of course requires nine separate images to draw the parts (plus some extras that are drawn over that), I have a directory full of files like top-left.png and top-left#2x.png. I include this directory in my app bundle as a folder reference.
Unfortunately, the usual image-loading APIs like -[NSImage imageNamed:] and -[NSBundle imageForResource:] don't seem to support subdirectories, even if you put a slash in the name. Instead, I'm loading the images with this method:
- (NSImage*)popoverImage:(NSString*)name {
NSURL * url = [[NSBundle bundleForClass:self.class] URLForResource:name withExtension:#"png" subdirectory:#"popover"];
return [[NSImage alloc] initWithContentsOfURL:url];
}
This works fine for normal displays, but it ignores the 2x images for retina displays. How can I get it to load the retina images as well? Is there a better way than loading the two reps separately and combining them by hand? I'd rather not use TIFFs as my source files for these resources, because I use Acorn as my image editor and last time I checked, it doesn't really understand compound image formats like that very well.
The simple answer is to use TIFF's, your concern is misplaced. In Xcode set the project preference "Combine High Resolution Artwork" and continue to produce your two PNG's with Acorn as you do now. During build Xcode will combine those two PNG's into a single TIFF and store that in your bundle.
I ended up biting the bullet and combining the images manually at runtime. I did so by adding this method in a category on NSImage, and then using it in place of -initWithContentsOfURL::
- (id)initRetinaImageWithContentsOfURL:(NSURL *)url {
if((self = [self initWithContentsOfURL:url])) {
NSURL * baseURL = url.URLByDeletingLastPathComponent;
NSString * baseName = url.lastPathComponent.stringByDeletingPathExtension;
NSString * extension = url.lastPathComponent.pathExtension;
NSString * retinaBaseName = [NSString stringWithFormat:#"%##2x", baseName];
NSURL * retinaURL = [baseURL URLByAppendingPathComponent:[retinaBaseName stringByAppendingPathExtension:extension]];
if([retinaURL checkResourceIsReachableAndReturnError:NULL]) {
NSData * data = [[NSData alloc] initWithContentsOfURL:retinaURL];
NSBitmapImageRep * rep = [[NSBitmapImageRep alloc] initWithData:data];
rep.size = self.size;
[self addRepresentation:rep];
}
}
return self;
}
This is what I do:
NSArray *paths = [NSBundle.mainBundle pathsForResourcesOfType: #"icns" inDirectory: #"Collections/1"];
for (NSString *path in paths) {
NSString *fileName = [[path lastPathComponent] stringByDeletingPathExtension];
NSImage *image = [[NSImage alloc] initWithContentsOfFile: path];
image.name = fileName;
[iconCollectionController addObject: #{#"icon": image}];
}
but as CRD already pointed out you need to combine your artwork (either as tiff or icns files). But it frees you from manually selecting an image resolution (which also requires you to listen to backing store changes).

NSImage + NSBitmapImageRep = Converting RAW image file from one format to another

I am trying to write a prototype to prove that RAW conversion from one format to another is possible. I have to convert a Nikon's raw file which is of .NEF format to Canon's .CR2 format. With help of various posts I create the original image TIFF representation's BitmapImageRep and use this to write the output file which has a .CR2 extension.
It does work but only problem for me is, the input file is of 21.5 MB but the output am getting is of 144.4 MB. While using NSTIFFCompressionPackBits gives me 142.1 MB.
I want to understand what is happening, I have tried various compression enums available but with no success.
Please help me understanding it. This is the source code:
#interface NSImage(RawConversion)
- (void) saveAsCR2WithName:(NSString*) fileName;
#end
#implementation NSImage(RawConversion)
- (void) saveAsCR2WithName:(NSString*) fileName
{
// Cache the reduced image
NSData *imageData = [self TIFFRepresentation];
NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:imageData];
// http://www.cocoabuilder.com/archive/cocoa/151789-nsbitmapimagerep-compressed-tiff-large-files.html
NSDictionary *imageProps = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithInt:NSTIFFCompressionJPEG],NSImageCompressionMethod,
[NSNumber numberWithFloat: 1.0], NSImageCompressionFactor,
nil];
imageData = [imageRep representationUsingType:NSTIFFFileType properties:imageProps];
[imageData writeToFile:fileName atomically:NO];
}
#end
How could I get the output file which is in CR2 format but almost around the size of the input file with little variation as required for a CR2 file?
Edit 1:
Done changes based on Peter's suggestion of using CGImageDestinationAddImageFromSource method, but still I am getting the same result. The input source NEF file size 21.5 MB but the destination file size after conversion 144.4 MB.
Please review the code:
-(void)saveAsCR2WithCGImageMethodUsingName:(NSString*)inDestinationfileName withSourceFile:(NSString*)inSourceFileName
{
CGImageSourceRef sourceFile = MyCreateCGImageSourceRefFromFile(inSourceFileName);
CGImageDestinationRef destinationFile = createCGImageDestinationRefFromFile(inDestinationfileName);
CGImageDestinationAddImageFromSource(destinationFile, sourceFile, 0, NULL);
//https://developer.apple.com/library/mac/#documentation/graphicsimaging/Conceptual/ImageIOGuide/ikpg_dest/ikpg_dest.html
CGImageDestinationFinalize(destinationFile);
}
CGImageSourceRef MyCreateCGImageSourceRefFromFile (NSString* path)
{
// Get the URL for the pathname passed to the function.
NSURL *url = [NSURL fileURLWithPath:path];
CGImageSourceRef myImageSource;
CFDictionaryRef myOptions = NULL;
CFStringRef myKeys[2];
CFTypeRef myValues[2];
// Set up options if you want them. The options here are for
// caching the image in a decoded form and for using floating-point
// values if the image format supports them.
myKeys[0] = kCGImageSourceShouldCache;
myValues[0] = (CFTypeRef)kCFBooleanTrue;
myKeys[1] = kCGImageSourceShouldAllowFloat;
myValues[1] = (CFTypeRef)kCFBooleanTrue;
// Create the dictionary
myOptions = CFDictionaryCreate(NULL, (const void **) myKeys,
(const void **) myValues, 2,
&kCFTypeDictionaryKeyCallBacks,
& kCFTypeDictionaryValueCallBacks);
// Create an image source from the URL.
myImageSource = CGImageSourceCreateWithURL((CFURLRef)url, myOptions);
CFRelease(myOptions);
// Make sure the image source exists before continuing
if (myImageSource == NULL){
fprintf(stderr, "Image source is NULL.");
return NULL;
}
return myImageSource;
}
CGImageDestinationRef createCGImageDestinationRefFromFile (NSString *path)
{
NSURL *url = [NSURL fileURLWithPath:path];
CGImageDestinationRef myImageDestination;
//https://developer.apple.com/library/mac/#documentation/graphicsimaging/Conceptual/ImageIOGuide/ikpg_dest/ikpg_dest.html
float compression = 1.0; // Lossless compression if available.
int orientation = 4; // Origin is at bottom, left.
CFStringRef myKeys[3];
CFTypeRef myValues[3];
CFDictionaryRef myOptions = NULL;
myKeys[0] = kCGImagePropertyOrientation;
myValues[0] = CFNumberCreate(NULL, kCFNumberIntType, &orientation);
myKeys[1] = kCGImagePropertyHasAlpha;
myValues[1] = kCFBooleanTrue;
myKeys[2] = kCGImageDestinationLossyCompressionQuality;
myValues[2] = CFNumberCreate(NULL, kCFNumberFloatType, &compression);
myOptions = CFDictionaryCreate( NULL, (const void **)myKeys, (const void **)myValues, 3,
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
//https://developer.apple.com/library/mac/#documentation/graphicsimaging/Conceptual/ImageIOGuide/imageio_basics/ikpg_basics.html#//apple_ref/doc/uid/TP40005462-CH216-SW3
CFStringRef destFileType = CFSTR("public.tiff");
// CFStringRef destFileType = kUTTypeJPEG;
CFArrayRef types = CGImageDestinationCopyTypeIdentifiers(); CFShow(types);
myImageDestination = CGImageDestinationCreateWithURL((CFURLRef)url, destFileType, 1, myOptions);
return myImageDestination;
}
Edit 2: Used the second approach told by #Peter. This gives interesting result. It's effect is the same as renaming the file in finder something like "example_image.NEF" to "example_image.CR2". Surprisingly what happens when converting both programmatically and in finder is, the source file which is 21.5 MB will turn out to be 59 KB. This is without any compression set in the code. Please see the code and suggest:
-(void)convertNEFWithTiffIntermediate:(NSString*)inNEFFile toCR2:(NSString*)inCR2File
{
NSData *fileData = [[NSData alloc] initWithContentsOfFile:inNEFFile];
if (fileData)
{
NSBitmapImageRep *imageRep = [NSBitmapImageRep imageRepWithData:fileData];
// [imageRep setCompression:NSTIFFCompressionNone
// factor:1.0];
NSDictionary *imageProps = nil;
NSData *destinationImageData = [imageRep representationUsingType:NSTIFFFileType properties:imageProps];
[destinationImageData writeToFile:inCR2File atomically:NO];
}
}
The first thing I would try doesn't involve NSImage or NSBitmapImageRep at all. Instead, I would create a CGImageSource for the source file and a CGImageDestination for the destination file, and use CGImageDestinationAddImageFromSource to transfer all of the images from A to B.
You're converting to TIFF twice in this code:
You create an NSImage, I assume from the source file.
You ask the NSImage for its TIFFRepresentation (TIFF conversion #1).
You create an NSBitmapImageRep from the first TIFF data.
You ask the NSBitmapImageRep to generate a second TIFF representation (TIFF conversion #2).
Consider creating an NSBitmapImageRep directly from the source data, and not using NSImage at all. You would then skip directly to step 4 to generate the output data.
(But I still would try CGImageDestinationAddImageFromSource first.)
Raw image files have their own (proprietary) representation.
For example, they may use 14-bit per component, and mosaic patterns, which are not supported by your code.
I think you should use a lower-level API and really reverse engineer the RAW format you are trying to save to.
I would start with DNG, which is relatively easy, as Adobe provides an SDK to write it.

Getting filename and path from nsfilewrapper / nstextattachment in NSAttributedString

I have a basic NSTextView with rich text and graphics enabled (in IB). What I'd like to get is the path and filename of any images dragged in so I can pass those to another class.
I'm new to NSAttributedString but I've got a loop using enumerateAttributesInRange:options:usingBlock: looking for NSAttachmentAttributeName and that's all working fine. But going deeper, I get to the fileWrapper class and it's apparent inability to give me the path of the item.
How would I go about getting the name and path of the NSTextAttachment?
Related: Is there an easier way to get them all then stepping through the attributes?
Thanks much!
While I personally hold the design of NSFileWrapper in contempt, if you just need the data of each attachment you can access it as an NSData instance via NSFileWrapper's regularFileContents method. However, I needed a valid and explicit pathname to the attachment for my application. To get it is much more work than it should be:
You can subclass your NSTextView and override the NSDraggingDestination Protocol method draggingEntered: and you can traverse the NSPasteboardItem objects passed to your application during the dragging operation. I chose to keep the pathname and its inode number in an NSMutableDictionary, as NSFileWrapper can provide you with the inode of the referenced file. Later, when I access the NSTextView contents via an NSAttributedString, I can fetch the pathname of an attachment using the inode as an index.
- (NSDragOperation)draggingEntered:(id < NSDraggingInfo >)sender {
// get pasteboard from dragging operation
NSPasteboard *pasteboard = [sender draggingPasteboard];
NSArray *pasteboardItems = [pasteboard pasteboardItems];
for ( NSPasteboardItem *pasteboardItem in pasteboardItems ) {
// look for a file url type from the pasteboard item
NSString *draggedURLString = [pasteboardItem stringForType:#"public.file-url"];
if (draggedURLString != nil) {
NSURL *draggedURL = [NSURL URLWithString:draggedURLString];
NSString *draggedPath = [draggedURL path];
NSLog(#"pathname: %#", draggedPath);
// do something with the path
// get file attributes
NSDictionary *draggedAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:draggedPath error:nil];
if ( draggedAttributes == nil)
continue;
// the NSFileWrapper allows access to the absolute file via NSFileSystemFileNumber
// put the path and the inode (returned as an NSNumber) into a NSMutableDictionary
NSNumber *draggedInode = [draggedAttributes objectForKey:NSFileSystemFileNumber];
[draggedFiles setObject:draggedPath forKey:draggedInode];
}
}
return [super draggingEntered:sender];
}
One issue with my solution, that doesn't effect my application, is that multiple files dragged into the view (either singly or together) which are hard links to the same file, will only be indexed as the last pathname added to the dictionary which shares the inode. Depending on how the pathnames are utilized by your application this could be an issue.

Resources