How to change a folder icon ?

Thank you, installed it and no error anymore

beautiful applescript; was looking for this functionality in applescript, started writing my own but didn’t get very far due the lack of sufficient knowlegde

but i applied it to my music folder, in which every folder contains a jpg-file, but not every folder get an icon

i understand that the script takes the first jpg which the folder contains, but apparently there must be a difference somewhere

Edit: I found the difference, the jpg files contain special symbols like é and ü etc…

thx very much

Hello, this is a very old topic but I have just tried this script and it gives me an error

error “missing value doesn’t understand the “save” message.” number -1708 from missing value

on line

save with icon – save icon_image_file with itself as icon

Hoping someone can help as I am very new to AppleScript

Thanks,
Rob

Hi @rob71. Welcome to MacScripter.

It seems that the Image Events application in macOS no longer understands AppleScript’s ‘alias’ specifier, which is what’s passed to the saveImageWithItselfAsIcon(F) handler in Yvan’s script. The simplest fix seems to be to change this line in the handler:

tell (open F)

to:

tell (open (F as «class furl»))

This coerces the alias to a different kind of file specifier that Image Events does understand and can use to open the image file.

The script in the post to which you’ve replied depends on a third-part utility called “osxutils” which has to be downloaded and installed before it can be used. The URL for it in the script’s comments now appears to be dead, which isn’t totally surprising after 17 years! Google returns several hits for the name, but I haven’t checked any of them out to see if they’re the same utility and work with current versions of macOS. The other scripts in this thread don’t need it.

Hi Nigel, and thanks for your help! The script I was trying to use was the one Yvan posted before the one requiring osxutils. I have amended the line that was causing the error, but it is still not producing the intended result (i.e pointing at a folder, if an image is present then use that image as the folder icon, any subfolders containing their own image use this image for the folder icon, any subfolders without an image file use the image in the parent folder as their icon). It doesn’t actually change the icon to the image expected, it just creates an icon of a generic ‘‘jpeg’’ icon; and any subfolders are ignored. Sometimes it will complete without an error, but if pointing the script to a folder containing several subfolders it will produce a time-out error.
As I say I am just at the start of my journey with AppleScript so it is all beyond me at present, but grateful for any advice you can give (as from the comments it seemed to work on older versions of MacOS).

Hi rob71.

I’ve run the script right through with a test folder this morning and it turns out that there’s been a change in the Finder’s behaviour too which essentially renders Yvan’s ingenious workaround useless. :confused: Also, the script was never intended to change the icon for folders not containing an image file. Its modus operandi is:

  • Search the current folder for any image files — specifically JPEG ones.
  • If there are any:
    • Use Image Events to open the first one found and to resave it immediately with the image it contains as its icon. This still works once the ‘alias’ business is sorted out.
    • Open a Finder information window for the image file and copy the icon from it to the clipboard. This still works, BUT the icon shown in the information window is now the generic one for an image file, not the actual icon assigned to the file. Or I should say: in many cases, the icon’s a generic one. On both my Ventura and Sequoia machines, my testing so far has shown that only image file icons are genericised (if that’s a word) and only some image files are so affected. I’ve not so far been able to determine what sets affected and unaffected image files apart. It doesn’t seem to be related to size, age, colourisation, or dimensions.
    • Open a Finder information window for the current folder and paste the copied icon image into it. This sets the icon for the folder. (An invisible file whose name is (“Icon” & return) is saved in the folder.)
  • Recursively repeat the above with any subfolders, subfolders of subfolders, etc.

So I don’t know the answer at the moment. I’ll look into it further, and hopefully others will be inspired to do so too, but for now I’m stumped. :frowning:

1 Like

Hi peavine.

Yes. It seems to. When I apply your script to my test hierarchy, which contains one or two subfolders that don’t contain JPEG files, it complains that the folder is empty and only offers the option to stop. None of the folders’ icons are changed. It does work though with a folder containing a JPEG and no subfolders. And using ASObjC to handle the icons is definitely the way to go!

1 Like

Thanks Nigel for your explanation. I misunderstood the OP’s requirements, confusing the folder selected by the user with the parent folder. This raises the question as to what should happen if a subfolder does not have a JPG and that subfolder’s parent folder does not have a JPG. Do you just keep going up the path until you find a JPG?

1 Like

Hi peavine.

As I understand it from rob72’s description above (I expect he’ll clarify it himself later), he wants every folder in the chosen hierarchy that contains a JPEG file to have an icon that’s the same picture as the one in that file. If a folder doesn’t contain a JPEG file, he wants that folder to have the same icon as the one given to the chosen root folder. Yvan’s scripts do something similar except that folders not containing JPEGS aren’t given new icons. It may be a good idea to wait for rob72 to confirm what he wants before getting too deeply into the code development! I won’t be able to do anything about it myself for the next day or so, so I’ll leave it to your expertise with NSIcon methods!

2 Likes

Hi Nigel, that is exactly what I’m hoping to achieve, thank you. In the case that a subfolder does not have an image file (it could be a jpg or a png) my goal is that the subfolder inherits the same icon image as its parent folder.

Thanks Nigel. I’ve included below a script that works in the above fashion. Please note that the user is notified of an error and the script stops if the chosen root folder does not contain a JPG file. I tested this script on my Sequoia computer without issue.

--revised 2025.01.22

use framework "AppKit"
use framework "Foundation"
use scripting additions

on main()
	set allowedSubfolders to 10 --set to desired value
	set theFolder to POSIX path of (choose folder)
	set theFolder to current application's |NSURL|'s fileURLWithPath:theFolder
	set folderImage to getImage(theFolder)
	if (folderImage's |count|()) is 0 then display dialog "This script cannot proceed because an image file was not found in the chosen root folder." buttons {"OK"} cancel button 1 default button 1
	set folderImage to folderImage's objectAtIndex:0
	set theSubfolders to getSubfolders(theFolder)
	set subfolderCount to theSubfolders's |count|()
	if subfolderCount is greater than allowedSubfolders then display dialog "This script cannot proceed because the chosen root folder contains " & subfolderCount & " subfolders, which exceeds the allowed number of subfolders." buttons {"OK"} cancel button 1 default button 1
	display dialog "The chosen root folder contains " & subfolderCount & " subfolders. Do you want to change the icon of the chosen root folder and all of its subfolders? An existing Finder window may have to be closed and reopened to see the changed icons." cancel button 1 default button 1
	set folderImage to (current application's NSImage's alloc()'s initWithContentsOfURL:folderImage)
	(current application's NSWorkspace's sharedWorkspace()'s setIcon:folderImage forFile:(theFolder's |path|()) options:2) --option 2 suppresses QuickDraw format icons
	repeat with aSubfolder in theSubfolders
		set subfolderImage to getImage(aSubfolder)
		if (subfolderImage's |count|()) is 0 then --image file not found in subfolder
			set subfolderImage to folderImage
		else
			set subfolderImage to (current application's NSImage's alloc()'s initWithContentsOfURL:(subfolderImage's objectAtIndex:0))
		end if
		(current application's NSWorkspace's sharedWorkspace()'s setIcon:subfolderImage forFile:(aSubfolder's |path|()) options:2)
	end repeat
end main

on getSubfolders(theFolder)
	set fileManager to current application's NSFileManager's defaultManager()
	set folderKey to current application's NSURLIsDirectoryKey
	set packageKey to current application's NSURLIsPackageKey
	set booleanTrue to current application's NSNumber's numberWithBool:true
	set folderContents to (fileManager's enumeratorAtURL:theFolder includingPropertiesForKeys:{} options:6 errorHandler:(missing value))'s allObjects() --option 6 skips hidden files and package descendants
	set theSubfolders to current application's NSMutableArray's new()
	repeat with anItem in folderContents
		set {theResult, aFolder} to (anItem's getResourceValue:(reference) forKey:folderKey |error|:(missing value))
		if aFolder is booleanTrue then
			set {theResult, aPackage} to (anItem's getResourceValue:(reference) forKey:packageKey |error|:(missing value))
			if aPackage is not booleanTrue then (theSubfolders's addObject:anItem)
		end if
	end repeat
	return theSubfolders
end getSubfolders

on getImage(theFolder)
	set fileManager to current application's NSFileManager's defaultManager()
	set folderContents to fileManager's contentsOfDirectoryAtURL:(theFolder) includingPropertiesForKeys:{} options:4 |error|:(missing value) --option 4 skips hidden files
	set thePredicate to current application's NSPredicate's predicateWithFormat_("pathExtension.lowercaseString IN %@", {"jpg", "jpeg", "png"}) --set desired image formats which are case-insensitive
	return (folderContents's filteredArrayUsingPredicate:thePredicate)
end getImage

main()
2 Likes

It works well on both my Sequoia and Ventura machines. :sunglasses:

1 Like

Thank you that is great! I’ve tried this on my Sequoia Mac and it works perfectly.

Out of interest, what would be the best way to test whether the image file is a ‘jpg’ or a ‘png’? I’ve tried putting an OR and a >= into the ''path extension ==[c]" line but I get an error each time.

As rob71 stated that he’s just at the start of his AppleScript journey, it occurred to me that he might be a bit perplexed by peavine’s script, which is written entirely in densely packed AppleScript-ObjC, has no comments, and has a couple of misleading labels. So I’ve presumed to do some cosmetic work on it so that people (including myself!) can at least understand what’s going on, even though the Objective-C niceties may currently be beyond them. I hope I haven’t overdone it:

use framework "AppKit"
use framework "Foundation"
use scripting additions

on main()
	set maximumFolderCount to 10 --set to desired value
	-- Choose a folder.
	set topFolder to POSIX path of (choose folder)
	-- Get the Objective-C NSURL specifier for it. All file and folder specifiers henceforth are in NSURL format.
	set topFolder to current application's |NSURL|'s fileURLWithPath:topFolder
	-- Get all the JPEG files in the top folder.
	set JPEGsInTopFolder to getJPEGs(topFolder)
	-- If none, stop here.
	if (JPEGsInTopFolder's |count|()) is 0 then display dialog "This script cannot proceed because a JPG file was not found in the chosen root folder." buttons {"OK"} cancel button 1 default button 1
	-- Otherwise, use the first JPEG in the list of those found.
	set topFolderJPEG to JPEGsInTopFolder's firstObject()
	-- Get all the subfolders in the top folder's hierarchy.
	set subFolders to getSubfolders(topFolder)
	-- Stop if too many.
	set subfoldersCount to subFolders's |count|()
	if subfoldersCount is greater than maximumFolderCount then display dialog "This script cannot proceed because the chosen root folder contains " & subfoldersCount & " subfolders, which exceeds the allowed number of subfolders." buttons {"OK"} cancel button 1 default button 1
	-- Otherwise check that the user still wants to continue and stop if they click "Cancel".
	display dialog "The chosen root folder contains " & subfoldersCount & " subfolders. Do you want to change the icon of the chosen root folder and all of its subfolders?" cancel button 1 default button 1
	-- Get the image from the top folder's JPEG and set the top folder's icon to it.
	set topFolderImage to (current application's NSImage's alloc()'s initWithContentsOfURL:topFolderJPEG)
	set noQuickDraw to current application's NSExcludeQuickDrawElementsIconCreationOption
	(current application's NSWorkspace's sharedWorkspace()'s setIcon:topFolderImage forFile:(topFolder's |path|()) options:noQuickDraw)
	-- Work through the list of subfolders, setting their icons either to the top folder's image or, if they contains JPEG(s), to their first JPEG's image.
	repeat with aSubfolder in subFolders
		set JPEGsInSubfolder to getJPEGs(aSubfolder)
		if (JPEGsInSubfolder's |count|()) is 0 then
			set subfolderImage to topFolderImage
		else
			set subfolderImage to (current application's NSImage's alloc()'s initWithContentsOfURL:(JPEGsInSubfolder's firstObject()))
		end if
		(current application's NSWorkspace's sharedWorkspace()'s setIcon:subfolderImage forFile:(aSubfolder's |path|()) options:noQuickDraw)
	end repeat
	-- Inform the user that the task's complete.
	beep
	display dialog "Done!" buttons {"OK"} default button 1 giving up after 5
end main

-- Get all the subfolders in the hierarchy of a given folder.
on getSubfolders(theFolder)
	-- Get everything in the hierarchy that isn't in a package or hidden.
	set fileManager to current application's NSFileManager's defaultManager()
	set folderAndPackageKeys to {current application's NSURLIsDirectoryKey, current application's NSURLIsPackageKey}
	set searchOptions to (current application's NSDirectoryEnumerationSkipsPackageDescendants) + (get current application's NSDirectoryEnumerationSkipsHiddenFiles)
	set entireContents to (fileManager's enumeratorAtURL:(theFolder) includingPropertiesForKeys:(folderAndPackageKeys) options:(searchOptions) errorHandler:(missing value))'s allObjects()
	-- Go through the results, building an array of items which are folders but not packages.
	set theSubfolders to current application's NSMutableArray's new()
	set folderResult to current application's NSDictionary's dictionaryWithObjects:({true, false}) forKeys:(folderAndPackageKeys)
	repeat with anItem in entireContents
		set theResult to (anItem's resourceValuesForKeys:(folderAndPackageKeys) |error|:(missing value))
		if (theResult's isEqualToDictionary:(folderResult)) then (theSubfolders's addObject:anItem)
	end repeat
	return theSubfolders
end getSubfolders

-- Get all the JPEGs at the root level of a given folder.
on getJPEGs(theFolder)
	-- Get all the visible items in the folder.
	set fileManager to current application's NSFileManager's defaultManager()
	set folderContents to fileManager's contentsOfDirectoryAtURL:(theFolder) includingPropertiesForKeys:{} options:(current application's NSDirectoryEnumerationSkipsHiddenFiles) |error|:(missing value)
	-- Filter for those whose name extensions are "jpg" or "jpeg" or the upper-case equivalents.
	set thePredicate to current application's NSPredicate's predicateWithFormat:"pathExtension MATCHES[c] 'jpe?g'"
	return (folderContents's filteredArrayUsingPredicate:thePredicate)
end getJPEGs

main()

While working on this, it occurred to me that the folder’s entire visible contents are effectively fetched from the disk twice: once to be tested for being folders and again to be tested for being JPEGs. Also, I couldn’t see the point of limiting the number of subfolders handled. The script below fetches everything just once and works from the resulting array and others derived from it. The logic is that the paths to folders containing JPEGs can be derived from the paths to the JPEGs themselves, so the items represented by those paths don’t need to be tested for folderhood or for the presence of JPEGs. They can simply have their icons set to the images from the JPEGs which generated the first instances of their paths. Once these and the JPEGs are handled and removed from the entire contents array, there’s less to test and any folder URLs that remain must be for JPEG-less subfolders. This seems to be a slightly faster approach, but, with my test folder, any advantage is largely swamped out by the time it takes to set the icons. :slightly_smiling_face:

use framework "AppKit"
use framework "Foundation"
use scripting additions

on main()
	-- Choose a folder.
	set topFolder to (choose folder)
	-- Get an Objective-C NSURL specifier for it.
	set |⌘| to current application
	set topFolder to |⌘|'s NSURL's fileURLWithPath:(topFolder's POSIX path)
	
	-- Get NSURLs for all the items in the folder's hierarchy that aren't in packages or hidden.
	set fileManager to |⌘|'s NSFileManager's defaultManager()
	set folderAndPackageKeys to {|⌘|'s NSURLIsDirectoryKey, |⌘|'s NSURLIsPackageKey}
	set searchOptions to (|⌘|'s NSDirectoryEnumerationSkipsPackageDescendants) + (get |⌘|'s NSDirectoryEnumerationSkipsHiddenFiles)
	set entireContents to (fileManager's enumeratorAtURL:(topFolder) includingPropertiesForKeys:(folderAndPackageKeys) options:(searchOptions) errorHandler:(missing value))'s allObjects()
	-- Also a separate array of NSURLs for just the JPEGs.
	set JPEGsOnly to |⌘|'s NSPredicate's predicateWithFormat:("pathExtension MATCHES[c] 'jpe?g'")
	set theJPEGs to entireContents's filteredArrayUsingPredicate:(JPEGsOnly)
	-- And a matching array of the paths to the folders containing the JPEGs.
	set JPEGContainerPaths to theJPEGs's valueForKeyPath:("path.stringByDeletingLastPathComponent")
	-- If the path to the top folder isn't one of these, end here.
	set topFolderPath to topFolder's |path|()
	if not (JPEGContainerPaths's containsObject:(topFolderPath)) then ¬
		display dialog "This script cannot proceed because a JPG file was not found in the chosen root folder." buttons {"OK"} cancel button 1 default button 1
	
	-- Use the index of the first instance of the top folder path in the container paths array to get the URL for the top folder's first JPEG.
	set topFolderJPEG to theJPEGs's objectAtIndex:(JPEGContainerPaths's indexOfObject:(topFolderPath))
	-- Set the top folder's icon to the image from the JPEG.
	set topFolderImage to (|⌘|'s NSImage's alloc()'s initWithContentsOfURL:(topFolderJPEG))
	set sharedWorkspace to |⌘|'s NSWorkspace's sharedWorkspace()
	set noQuickDraw to |⌘|'s NSExcludeQuickDrawElementsIconCreationOption
	(sharedWorkspace's setIcon:(topFolderImage) forFile:(topFolderPath) options:(noQuickDraw))
	
	-- Get single instances of the paths to the subfolders containing JPEGs.
	set containerPathSet to |⌘|'s NSMutableOrderedSet's orderedSetWithArray:(JPEGContainerPaths)
	containerPathSet's removeObject:(topFolderPath)
	-- Work through the paths, setting the folders' icons to the images from the corresponding first JPEGs.
	set JPEGCount to theJPEGs's |count|()
	set idx to -1
	repeat with subfolderPath in containerPathSet
		tell (idx + 1) to set searchRange to {it, JPEGCount - it}
		set idx to (JPEGContainerPaths's indexOfObject:(subfolderPath) inRange:searchRange)
		set subfolderJPEG to (theJPEGs's objectAtIndex:(idx))
		set subfolderImage to (|⌘|'s NSImage's alloc()'s initWithContentsOfURL:(subfolderJPEG))
		(sharedWorkspace's setIcon:(subfolderImage) forFile:(subfolderPath) options:(noQuickDraw))
	end repeat
	
	-- Get a copy of the entire contents array without the JPEGs and folders already handled.
	set everythingElse to |⌘|'s NSPredicate's predicateWithFormat_("NOT ((self IN %@) OR (path IN %@))", theJPEGs, containerPathSet)
	set entireContents to entireContents's filteredArrayUsingPredicate:(everythingElse)
	-- Any remaining folder URLs are for JPEG-less subfolders. They get the same icon as the top folder.
	set folderResult to |⌘|'s NSDictionary's dictionaryWithObjects:({true, false}) forKeys:(folderAndPackageKeys) -- {Is folder, isn't package}.
	repeat with thisURL in entireContents
		if ((thisURL's resourceValuesForKeys:(folderAndPackageKeys) |error|:(missing value))'s isEqualToDictionary:(folderResult)) then ¬
			(sharedWorkspace's setIcon:(topFolderImage) forFile:(thisURL's |path|()) options:(noQuickDraw))
	end repeat
	
	-- Inform the user that the task's complete.
	beep 2
	display dialog "Done!" buttons {"OK"} default button 1 giving up after 5
end main

main()
1 Like

Hi rob71.

In peavine’s script, you can use either:

-- The [c] means case-insensitive comparisons.
set thePredicate to current application's NSPredicate's predicateWithFormat:"(pathExtension ==[c] 'jpg') OR (pathExtension ==[c] 'png')"

… or:

set thePredicate to current application's NSPredicate's predicateWithFormat:"pathExtension IN {'jpg','JPG','png','PNG'}"

In the scripts I’ve just posted, the predicate string would be:

"pathExtension MATCHES[c] 'jpe?g|png'"
1 Like

rob71. I’ve edited my script to allow you to specify one or multiple file extensions in a list. The specified file extensions should be lowercase, and uppercase file extensions will automatically be matched. The changed line in the script is:

set thePredicate to current application's NSPredicate's predicateWithFormat_("pathExtension.lowercaseString IN %@", {"jpg", "jpeg", "png"})

I’m sure Nigel’s suggestions also work great. I should have included this approach in my script originally and wanted to make this change.

2 Likes

Nigel, thanks for looking at my script and for the suggestions.

I disagree with your above comment. If the user inadvertently selects the wrong folder, and if that folder contains an image file, the script will quickly change the icons of hundreds of subfolders without the user’s knowledge. The allowedSubfolders setting only consumes a few lines of code and provides some protection against a large number of unwanted icon changes. If that’s not a concern, the user can set the allowedSubfolders variable to some extremely large number.

I tested your optimized script. With a folder containing 145 subfolders, both of our scripts took about 545 milliseconds to run. I always enjoy learning new ways of doing things and will review your script.

2 Likes

Here’s a little something to correct that misfortune should it occur. It assumes that none of the subfolders had icons originally, but offers the choice of keeping the top folder’s or not. It can easily be adapted to ask in every individual case if required.

use AppleScript version "2.4" -- OS X 10.10 (Yosemite) or later
use framework "Foundation"
use scripting additions

on main()
	-- Choose a folder and get user confirmation.
	set topPOSIX to (choose folder)'s POSIX path
	set modeButtons to {"Yes, including top folder", "Subfolders only"}
	set mode to displayMessage("This script will remove the icon from every folder in the hierarchy \"" & topPOSIX & "\" except for packages and their contents. Do you want to continue?", {"Cancel"} & modeButtons, 0)
	
	-- Get the paths to all the items in the folder's hierarchy, including hidden files, that aren't in packages.
	set |⌘| to current application
	set topFolder to |⌘|'s NSURL's fileURLWithPath:(topPOSIX)
	set fileManager to |⌘|'s NSFileManager's defaultManager()
	set entireContents to (fileManager's enumeratorAtURL:(topFolder) includingPropertiesForKeys:({}) options:(|⌘|'s NSDirectoryEnumerationSkipsPackageDescendants) errorHandler:(missing value))'s allObjects()'s valueForKey:("path")
	-- Filter for folder icon paths. They all end with ("/Icon" & return).
	set iconsOnly to |⌘|'s NSPredicate's predicateWithFormat_("self ENDSWITH %@", "/Icon" & return)
	set iconPaths to entireContents's filteredArrayUsingPredicate:(iconsOnly)
	-- Sort by path length, the logic being that the shortest path must be for the top folder's icon.
	set byLength to |⌘|'s NSSortDescriptor's sortDescriptorWithKey:("length") ascending:(true)
	set iconPaths to iconPaths's sortedArrayUsingDescriptors:({byLength})
	-- Remove the icon files, including the top folder's or not as appropriate.
	repeat with i from (((mode = modeButtons's end) as integer) + 1) to (count iconPaths)
		(fileManager's removeItemAtPath:(iconPaths's item i) |error|:(missing value))
	end repeat
	
	-- Inform the user that the task's complete.
	beep 2
	displayMessage("Done!", {"OK"}, 5)
end main

on displayMessage(msg, buttonList, wait)
	tell application (path to frontmost application as text) to ¬
		return (display dialog msg buttons buttonList cancel button 1 default button 1 with title "Remove all folder icons in hierarchy" giving up after wait)'s button returned
end displayMessage

main()

Edits: Changed to handling the items as paths rather than as NSURLs, since the filtering and sorting involves the paths anyway. (It probably doesn’t make much difference. :smile:) Also handler for the dialogs.

Nigel. I tested this script on my Sequoia computer, and it changed the icon of the chosen root folder but not its subfolders. I tried this on my boot drive and an external SSD with the same result. I restarted the Finder window to make sure any changes were visible. The following is a screenshot after running your script and restarting the Finder window.

Hi peavine.

That’s interesting! I tested both scripts on my Sequoia and Ventura machines and they worked perfectly, the icons changing or reverting as required. But I was using my existing test hierarchy, where the icons had originally been set with the setIcon:forFile:options: method. I’ve now added a couple of new empty folders to the hierarchy on the Sequoia machine and their icons are not visibly changed by moving the icon file into them, although all of the older JPEG-less ones’ icons still are. The official method must be doing something else besides just creating an icon file. I’ve deleted my post containing the offending script.

Hi @Nigel_Garvey and @peavine

Thank you both for your help with this, I’m very grateful. The added comments are very useful and will help in my study.