Once again a conversion problem between a path returned by a NSFileManager call and the Image Events file path:
set theFile to NSString's |string|()
set theList to manager's enumeratorAtPath_(POSIX path of myFolder)'s allObjects()
tell application "Image Events"
launch
repeat with theFile in theList
set fileDic to manager's attributesOfItemAtPath_error_(theFile,missing value)
if ((fileDic's fileType() as string) is "NSFileTypeRegular") and ((theFile's pathExtension() as string) is "jpg") then
if (fileDic's fileSize() as integer > 200000) then
try
set this_image to open (theFile as posix file) as alias -- does not work.
I think I’ve tried every possible combination – excepted the correct one. Which is?
I think there are several problems here. First, not a problem, but unnecessary, is your first line “set theFile to NSString’s |string|()”. In a repeat loop the variable that you create will have the class of an item of your array, you don’t need to set it. Second, theList will only contain the names of the files in your folder not their full paths, so the statement “set fileDic to manager’s attributesOfItemAtPath_error_(theFile,missing value)” won’t work. Also there is no need to use imageEvents, just use NSImage’s method, initWithContentsOfFile: to create an image.
In the following sample code, I change the working directory to myFolder before going into the repeat loop so just the file names will be sufficient to get the file. This works where the files are in the top layer of myFolder, I don’t know if this would work if the files are buried deeper in the folder.
script PhotosAppDelegate
property parent : class "NSObject"
property myFolder : POSIX path of ((path to desktop as string) & "pics:")
on applicationWillFinishLaunching_(aNotification)
set manager to current application's NSFileManager's defaultManager()
set theList to manager's contentsOfDirectoryAtPath_error_(myFolder, missing value)
manager's changeCurrentDirectoryPath_(myFolder)
repeat with theFile in theList
set fileDic to manager's attributesOfItemAtPath_error_(theFile, missing value)
if theFile's pathExtension() as string is "jpg" then
if (fileDic's fileSize() as integer > 20000) then
set this_image to current application's NSImage's alloc()'s initWithContentsOfFile_(theFile)
log this_image
--do something with this _image
end if
end if
end repeat
end applicationWillFinishLaunching_
end script
Hi Ric (once again thank for the map concept - I just can’t stop creating maps now :P)
I didn’t know what this cocoa’s enumerator would think of my loop variable, so I initialized it (maybe a “variable x is not defined” phobia.)
I wrote the same line:
set this_image to current application's NSImage's alloc()'s initWithContentsOfFile_(theFile)
before I realized that scaling a NSImage was not as simple as using this “Image Event” ghost application. As I understand it, you must do some sordid calculations on the height and width of the picture’s bounds rect. and then there is the compression problem – the image treatment is:
scale this_image by factor 0.50
save this_image with compression level medium
OK, that’s a simple way to do it – but the price is to launch an external process and convert these *** paths from posix to HFS.
I was curious about these vImage methods: are this library reachable from ASOC?
I don’t know if this is true. What is it that you want to do with these images? Where in your project are these images going?
I don’t know anything about vImage, and from what I see in the docs, I don’t think you need to use it for this project unless you’re doing a lot of image processing.
This is a disk-only application, for image serial treatment. The NSImage will never be displayed, only resized and compressed on disk, by different factors based on the original’s size.
Speed is not essential, but there may be hundreds or thousands of images, so. To say, only the change from «Tell application “Finder”» to NSFileManager calls divides the time by four! Making a Finder’s list is very time-consuming (the spinning rainbow cursor plays with my nerves: has the app crashed or not?)
It is intentional to use enumeratorAtPath_ because, of course, files may be nested into directories. But I cannot notice a delay between the call and the beginning of the treatment – it’s really a light’s speed method!
I went back to iterating through the folder using enumeratorAtPath – I see that it gives paths relative to the folder that you pass it, so you still need to prefix the file names with the folder’s directory path so you get the absolute file path. I also changed the if statements a little to avoid creating the dictionary unless the file is a jpg – I also don’t think you need to check if the file is a regular file since it should be if it’s a jpg. After getting the path, I call resizeImage which does the resizing and also creates a new file path that is the same as the old one except that it adds “New_” to the beginning of the file name. This could be changed of course, if you want to place all the new smaller images in a different location. So an example of code to do this is as follows:
script PhotosAppDelegate
property parent : class "NSObject"
property myFolder : POSIX path of ((path to desktop as string) & "pics:")
property prefix : missing value
on applicationWillFinishLaunching_(aNotification)
set prefix to current application's NSString's stringWithString_("New_")
set manager to current application's NSFileManager's defaultManager()
set theList to manager's enumeratorAtPath_(myFolder)'s allObjects()
manager's changeCurrentDirectoryPath_(myFolder)
set curDir to manager's currentDirectoryPath()
repeat with theFile in theList
if theFile's pathExtension() as string is "jpg" then
set absoluteFilePath to curDir's stringByAppendingPathComponent_(theFile)
set fileDic to manager's attributesOfItemAtPath_error_(absoluteFilePath, missing value)
if (fileDic's fileSize() as integer > 200000) then
resizeImage_(absoluteFilePath)
end if
end if
end repeat
end applicationWillFinishLaunching_
on resizeImage_(absoluteFilePath)
set newLastPathComponent to prefix's stringByAppendingString_(absoluteFilePath's lastPathComponent())
set newFile to absoluteFilePath's stringByDeletingLastPathComponent()'s stringByAppendingPathComponent_(newLastPathComponent)
set theImage to current application's NSImage's alloc()'s initWithContentsOfFile_(absoluteFilePath)
set theData to theImage's jpegDataWithWidth_height_compression_(200, 150, 0.5)
theData's writeToFile_atomically_(newFile, 1)
end resizeImage_
end script
After trying what I thought would be the way to change the image size, setImageSize, I found out that resizing the image isn’t as easy as it seems. Using setImageSize changes what is logged as the image size but not the number of pixels or the file size. After a google search, I found some code ((by Jeff Disher at http://www.cocoadev.com/index.pl?ThumbnailImages) that I modified. I created a category on NSImage that adds the method, jpegDataWithWidth:height:compression: , which resizes the image by creating a new smaller empty image, and then drawing the larger image into it using drawInRect:fromRect:operation:fraction: . I then get the imageRep of that image and use representationUsingType:properties: to format the image data to the jpeg storage type, compress the data and return an NSData object suitable for saving to a file. I don’t know if there is an easier way to do this – I’d be interested in seeing one if anyone knows how. The code to do this in objective C is as follows:
@implementation NSImage (ImageResize)
- (NSData *)jpegDataWithWidth:(double) w height:(double) h compression:(NSNumber *) c //c=1 for no compression, 0 for max
{
NSSize newSize = NSMakeSize(w, h);
NSImage *newImage = [[NSImage alloc] initWithSize:newSize];
NSRect newRect = NSMakeRect(0.0, 0.0, newSize.width, newSize.height);
[newImage lockFocus];
[self drawInRect:newRect fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0];
NSBitmapImageRep *bm = [[NSBitmapImageRep alloc]initWithFocusedViewRect:newRect];
NSData *theData = [bm representationUsingType:NSJPEGFileType properties:[NSDictionary dictionaryWithObject:c forKey:NSImageCompressionFactor]];
[newImage unlockFocus];
return theData;
}
@end
this is indeed the conventional method to resize NSImage,
my preferred way is CoreGraphics using CGImageRef (as described on the same cocoadev site).
Note: Although AppleScriptObjC works by default in Garbage Collection mode,
it’s recommended for categories to (auto)release allocated objects (in this case bm and newImage).
Other people in Memory Management environment could use these categories too,
and they will run into memory leaks
I have to admit that I’m edified by the level of knowledge you have to deploy to make Cocoa reproduce these two lines of Image Events:
scale this_image by factor 0.50
save this_image with compression level medium
So, I have the choice:
keep the Image Events methods
make an Objective-C implementation of a new category on NSImage;
add a specialized framework (CoreGraphics) and get along with the disk access
I regret there is no myNSImage’s scaleImage_ or myNSImage’s compressionFactor_, but of course in a regular application it would be unnecessary.
I think I’ll try Ric’s method first (no need to make a .h file in ASOC, correct?) to see if speed increases. Stefan, I’ll try CoreGraphics only if NSImage method is not faster. I’m not sure I can do it from ASOC – and I’ll have to scavenge the objects myself.
Thanks for the reminder – I should get in the habit of doing the memory management since I’m working through the Stanford university online course on iPhone app development.
It’s my understanding that I can just add the lines [newImage release]; and [bm release]; just before the return statement to release the 2 objects that I created using an alloc. I think also, that the dictionary that gets created is created as an autorelease object so I don’t need to do anything about that, correct? The one thing I’m a little fuzzy about is whether I need to have a dealloc method in a category. My understanding is that you usually override dealloc in subclasses so that you can release your instance variables, but since you don’t have any in a category, do I need one?
repeat with theFile in theList
if theFile's pathExtension() as string is "jpg" then
set absoluteFilePath to curDir's stringByAppendingPathComponent_(theFile)
set fileDic to manager's attributesOfItemAtPath_error_(absoluteFilePath, missing value)
if (fileDic's fileSize() as integer > 200000) then
set theImage to current application's NSImage's alloc()'s initWithContentsOfFile_(absoluteFilePath)
set newSize to current application's NSMakeSize(w, h) ------<should be a scale factor>
set newImage to current application's NSImage's alloc's initWithSize_(newSize)
set newRect to current application's NSMakeRect({0.0, 0.0}, {newSize's width, newSize's height})
newImage's lockFocus()
newImage's drawInRect_newRect ------<here I'm lost>
set bm to current application's NSBitmapImageRep's alloc's initWithFocusedViewRect_(newRect)
set theData to current application's NSData's ------<here I'm lost>
newImage's unlockFocus()
theData's writeToFile_atomically_(absoluteFilePath, 1)
end if
end repeat
I’m lost in some critical details of the ObjC code… You see the two lines above ?
on jpgDataWithWidth_height_compression_(w, h, c)
set newImage to current application's NSImage's alloc()'s initWithSize_({w, h})
set dict to current application's NSDictionary's dictionaryWithObject_forKey_(c, current application's NSImageCompressionFactor)
set newRect to {{0, 0}, {w, h}}
set oldRect to {{0, 0}, {0, 0}}
newImage's lockFocus()
theImage's drawInRect_fromRect_operation_fraction_(newRect, oldRect, current application's NSCompositeCopy, 1)
set bm to current application's NSBitmapImageRep's alloc()'s initWithFocusedViewRect_(newRect)
set theData to bm's representationUsingType_properties_(current application's NSJPEGFileType, dict)
newImage's unlockFocus()
return theData
end jpgDataWithWidth_height_compression_
I tested this version and the objective C version with a folder containing 314 images, and they both took 35 seconds. The bottleneck seems to be in the disk access (I’m hearing a lot of disc sounds). This method is just added to the code I posted above, and the call to the objC method commented out, and replaced with a call to this one (notice the name starts jpg instead of jpeg so it would call the right one). I also added a property theImage.
Seems not much faster than Image Events. What config do you have? Mine is 2 x 3.2 GHz Quad-Core.
Question : is it possible to set w and h to be proportional to the original height and width (passing two args in range [0…1], 0 for min percentage/compression and 1 for max? I suppose you can get the picture’s original bounds.
About your discussion: I have a property gMyDic which points to
→ a NSMutableDictionary when the user creates it and uses it;
→ missing value if the user gets rid of it.
When the user wants to delete the NSMutableDictionary, I set the property gMyDic to missing value (so I can check if there is no defined dic). But this is only a fake destruction: the object continues to send notifications, and of course I end up with run-time error like “Can’t get of missing value”.
This is bad programming and I dislike it. Is there a way to effectively dispose of the NSMutableDictionary, before setting gMyDic to missing value? Some of
What notifications? As far as I know, dictionaries don’t send notifications. I set up an observer to log all notifications from a dictionary, and I never got any.
When do you get this run time error? In response to what? If I set the dictionary’s outlet to missing value, I just get null if I try to log the dictionary, and I get no errors at all if I try to add or delete something from it.
Even in Objective C, if an object gets deallocated, that doesn’t stop you from trying to access it and getting an error. It’s up to you, the programmer, to make sure no messages are sent to an object that no longer exits.
It’s OK for me, I spend years to avoid referencing dangling pointers in Pascal – not always successfully.
The problem comes from tableviews. They simply adore sending notifications, and parts of my dictionary are bound to these tables, so (knowing that NIL bindings are not a problem) I should do (pseudocode) :
on deleteMyDic_(sender)
set gMyDic's tableOne to missing value // (or myDic's setValue_forKey_(missing value, "tableOne")
... // for every key bound in IB
set gMyDic to missing value
end
So the dictionary is properly released.
By code, there is another way to avoid errors : to check into the notification if the gMyDic is not NIL before sending a message to (or referring to) one of its keys. But with the binding mechanism, it’s less obvious.
After Edit : Good gracious – the art of programming :rolleyes: the problem was not on release, but on init!
I confess my errors on this site because (who knows?) someone may fall into the same trap:
Always be aware of the order you initialize your variables. In my case, I had two table views. I filled the first one, then the second one, in the same order they were in my dictionary. Not a problem in most cases, except that a table view that you fill sends a notification at each added value. In my case, the tableViewSelectionDidChange_ method referred to variables in the second table, which was not yet initialized.
From pointers that go bump in the night, O Lord, do protect us.