icns Maker

Someone wanted this over on Stack Overflow, and I thought you all might find it useful, too. Its a script to whip up icns files from any old image you have lying around.

set picFile to choose file with prompt "Choose an image to iconize." of type {"public.image"}
set workingFolder to POSIX path of (path to temporary items from user domain)
set outputFolder to POSIX path of (path to desktop from user domain)

set sizesList to {16, 32, 128, 256, 512}

tell application "System Events"
	set pictureFilePath to quoted form of (get POSIX path of picFile)
	set {pictureName, ext} to {name, name extension} of picFile
	if ext is not "" then
		set pictureName to text 1 through -((length of ext) + 2) of pictureName
	end if
	
	-- create iconset folder
	set iconsetFolder to make new folder at folder workingFolder with properties {name:pictureName & ".iconset"}
	
	-- cycle through sizes to create normal and hi-def sized icon images
	repeat with thisSize in sizesList
		set iconFilePath to POSIX path of iconsetFolder & "/" & my makeFileNameFromSize(thisSize, false)
		do shell script "sips -z " & thisSize & " " & thisSize & " " & "-s format png " & pictureFilePath & " --out " & iconFilePath
		set iconFilePath to POSIX path of iconsetFolder & "/" & my makeFileNameFromSize(thisSize, true)
		do shell script "sips -z " & thisSize * 2 & " " & thisSize * 2 & " " & "-s format png " & pictureFilePath & " --out " & iconFilePath
	end repeat
	
	-- create new icns file
	set iconsetPath to quoted form of (POSIX path of iconsetFolder as text)
	set outputPath to quoted form of (outputFolder & pictureName & ".icns")
	do shell script "iconutil -c icns -o " & outputPath & " " & iconsetPath
end tell

on makeFileNameFromSize(s, x2)
	set fileName to "icon_" & s & "x" & s
	if x2 then set fileName to fileName & "@2x"
	set fileName to fileName & ".png"
	return fileName
end makeFileNameFromSize

I haven’t added any error checking. I know that sips complains if there are certain special characters in the file name — e.g. parentheses — but I haven’t tried to figure out which. Also, it won’t preserve aspect ratio; it simply squishes the image as needed to make it square. It would be easy enough to tweak the sips command to crop or pad it to be square first, but…

That looks well thought out and seems to work nicely. :cool: But are those sizes correct? You end up with ten PNG files, six of which are identical pairs (ie. two 32s, two 256s, and two 512s), just with different names.

Ignoring that, here’s a version of your script with sip’s alter ego Image Events creating the PNGs. Image Events is a scriptable application which comes with the system and acts as a front end to sips. It lives in the CoreServices folder. It’s slightly faster here than doing ten 'do shell script’s, but it doesn’t explicitly resample, so the aspect ratio has to be maintained. :wink: Each image is scaled so that its larger dimension matches the required size and the smaller dimension’s edges are then padded out to match.

set picFile to (choose file with prompt "Choose an image to iconize." of type {"public.image"})
set workingFolder to POSIX path of (path to temporary items from user domain)
set outputFolder to POSIX path of (path to desktop from user domain)

set sizesList to {16, 32, 128, 256, 512}

tell application "System Events"
	set {pictureName, ext} to {name, name extension} of picFile
	if ext is not "" then
		set pictureName to text 1 through -((length of ext) + 2) of pictureName
	end if
	
	-- create iconset folder
	set iconsetPath to (make new folder at folder workingFolder with properties {name:pictureName & ".iconset"})'s POSIX path & "/"
end tell

tell application "Image Events"
	launch
	
	-- cycle through sizes to create normal and hi-def sized icon images
	repeat with thisSize in sizesList
		set thisImage to (open picFile)
		scale thisImage to size thisSize
		pad thisImage to dimensions {thisSize, thisSize}
		save thisImage in (iconsetPath & my makeFileNameFromSize(thisSize, false)) as PNG
		set thisImage to (open picFile)
		scale thisImage to size thisSize * 2
		pad thisImage to dimensions {thisSize * 2, thisSize * 2}
		save thisImage in (iconsetPath & my makeFileNameFromSize(thisSize, true)) as PNG
	end repeat
	quit
end tell

-- create new icns file
set iconsetPath to quoted form of iconsetPath
set outputPath to quoted form of (outputFolder & pictureName & ".icns")
do shell script "iconutil -c icns -o " & outputPath & " " & iconsetPath


on makeFileNameFromSize(s, x2)
	set fileName to "icon_" & s & "x" & s
	if x2 then set fileName to fileName & "@2x"
	set fileName to fileName & ".png"
	return fileName
end makeFileNameFromSize

Edit: Ambiguous statement in first paragraph clarified. The image is now opened from the file each time instead of being ‘copied’. (There’s no ‘copy’ command in Image Events’s dictionary and its ‘duplicate’ command doesn’t work. AppleScript’s ‘copy’ was probably only copying the reference to the image already loaded, causing the same image to be constantly scaled and rescaled.)

That’s correct. In a world where time and artistic talent are in abundance, the idea is that each image can be individually optimized for its particular size and resolution.

I was just going off of this Apple link — https://developer.apple.com/library/archive/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Optimizing/Optimizing.html — specifically where it says:

I think the duplications are intentional so that the system can query for the same base name and get different results on hi-def displays (rather than looking for the next size up on hi-def displays), but you know… I’m not in Apple’s head.

Always wondered why Image Events doesn’t allow aspect ratio changes. I guess it makes a kind of sense, but it’s an odd limitation. I am a little surprised image events is faster, and I wonder how much of that is due to the linear reduction rather than the full resample…

I did realize that we can speed up performance in either script just by copying the the current file to the lower resolution 2x file; that means we only have to do 6 image processing events rather than 10. Looks like this on my version:

	repeat with thisSize in sizesList
		set iconSizeFileName to my makeFileNameFromSize(thisSize, false)
		set iconFilePath to quoted form of (POSIX path of iconsetFolder & "/" & iconSizeFileName)
		do shell script "sips -z " & thisSize & " " & thisSize & " " & "-s format png " & pictureFilePath & " --out " & iconFilePath
		if thisSize > 16 then
			set lastSizeFileName to my makeFileNameFromSize(lastSize, true)
			set lastIconFilePath to quoted form of (POSIX path of iconsetFolder & "/" & lastSizeFileName)
			do shell script "cp " & iconFilePath & " " & lastIconFilePath
		end if
		if thisSize = 512 then
			set iconFilePath to POSIX path of iconsetFolder & "/" & my makeFileNameFromSize(thisSize, true)
			do shell script "sips -z " & thisSize * 2 & " " & thisSize * 2 & " " & "-s format png " & pictureFilePath & " --out " & iconFilePath
		end if
		set lastSize to contents of thisSize
	end repeat

Do you think there would be much improvement in speed if switched over to AppleScriptObjC and used NSImage? I expect that’s what Image Events is doing in any case, so it may not be any improvement at all.

It shouldn’t make any difference: the process is the same. I suspect sips might be slower because of the overhead of launching a process 10 times, plus the extra do shell script adds. But if you’re comparing times in a script editor, the differences might simply be artefacts.

It should be a tad quicker because there’d be no need for repeated opening/copying of the original. But under the hood they’re all calling similar code, and that underlying code is probably what’s taking the bulk of the time.

I did some tests, and it knocks about 25% off the time taken. (This is without the copying optimization.)

use AppleScript version "2.5" -- macOS 10.11 or later
use framework "Foundation"
use framework "AppKit"
use scripting additions

set imagePath to POSIX path of (choose file with prompt "Choose an image to iconize." of type {"public.image"})
set workingFolder to (path to temporary items from user domain)
set outputFolder to POSIX path of (path to desktop from user domain)

set imagePath to current application's NSString's stringWithString:imagePath
set pictureName to imagePath's lastPathComponent()'s stringByDeletingPathExtension()
set ext to imagePath's pathExtension()
set iconsetURL to current application's NSURL's fileURLWithPath:(pictureName's stringByAppendingPathExtension:"iconset") relativeToURL:workingFolder
current application's NSFileManager's defaultManager()'s createDirectoryAtURL:iconsetURL withIntermediateDirectories:true attributes:(missing value) |error|:(missing value)

set sizesList to {16, 32, 128, 256, 512}
set theImage to current application's NSImage's alloc()'s initWithContentsOfFile:imagePath -- load the file as an NSImage

repeat with thisSize in sizesList
	set theWidth to thisSize * 2
	-- create new bitmaps
	set newRepHiRes to (current application's NSBitmapImageRep's alloc()'s initWithBitmapDataPlanes:(missing value) pixelsWide:theWidth pixelsHigh:theWidth bitsPerSample:8 samplesPerPixel:4 hasAlpha:true isPlanar:false colorSpaceName:(current application's NSCalibratedRGBColorSpace) bytesPerRow:0 bitsPerPixel:0)
	set newRepLowRes to (current application's NSBitmapImageRep's alloc()'s initWithBitmapDataPlanes:(missing value) pixelsWide:thisSize pixelsHigh:thisSize bitsPerSample:8 samplesPerPixel:4 hasAlpha:true isPlanar:false colorSpaceName:(current application's NSCalibratedRGBColorSpace) bytesPerRow:0 bitsPerPixel:0)
	current application's NSGraphicsContext's saveGraphicsState() -- save state
	(current application's NSGraphicsContext's setCurrentContext:(current application's NSGraphicsContext's graphicsContextWithBitmapImageRep:newRepHiRes)) -- sets where drawing happens
	(theImage's drawInRect:{{0, 0}, {theWidth, theWidth}} fromRect:(current application's NSZeroRect) operation:(current application's NSCompositeSourceOver) fraction:1.0)
	(current application's NSGraphicsContext's setCurrentContext:(current application's NSGraphicsContext's graphicsContextWithBitmapImageRep:newRepLowRes)) -- sets where drawing happens
	(theImage's drawInRect:{{0, 0}, {thisSize, thisSize}} fromRect:(current application's NSZeroRect) operation:(current application's NSCompositeSourceOver) fraction:1.0)
	current application's NSGraphicsContext's restoreGraphicsState() -- restore
	-- get data and store
	set theData to (newRepHiRes's representationUsingType:(current application's NSPNGFileType) |properties|:{NSImageGamma:1.0})
	set outURL to (iconsetURL's URLByAppendingPathComponent:(my makeFileNameFromSize(thisSize, true)))
	(theData's writeToURL:outURL atomically:true) -- write it to disk	
	set theData to (newRepLowRes's representationUsingType:(current application's NSPNGFileType) |properties|:{NSImageGamma:1.0})
	set outURL to (iconsetURL's URLByAppendingPathComponent:(my makeFileNameFromSize(thisSize, false)))
	(theData's writeToURL:outURL atomically:true) -- write it to disk	
end repeat

set iconsetPath to quoted form of (iconsetURL's |path|() as text)
set outputPath to quoted form of (outputFolder & pictureName & ".icns")
do shell script "iconutil -c icns -o " & outputPath & " " & iconsetPath

on makeFileNameFromSize(s as integer, x2)
	set fileName to "icon_" & s & "x" & s
	if x2 then set fileName to fileName & "@2x"
	set fileName to fileName & ".png"
	return fileName
end makeFileNameFromSize

Edited: See note in message below.

Hi TedW and Shane.

Thanks for the confirmations that the duplication of PNGs is intentional.

I’ve edited my post above to clear up the ambiguity in the first paragraph and, in the script, to have Image Events open the image afresh each time instead of ‘copy’-ing it, which I now believe to be a late-night goof. :rolleyes: The script’s now not quite as fast as it was, but is still faster than using ‘do shell script’ ten times.

Shane’s ASObjC script is definitely the fastest of the three, although the end result only contains seven images when opened in Preview.

Seven, not six. They’re helpfully labelled 1, 2, 3, 4, 5, 6, and 7.

See Shane’s answer to that in post #2 and TedW’s in post #3.

That’s because it’s not producing the files it should :(. I’ve edited (actually, rewritten) the script above to do it properly. If anything, it should be a whisker quicker.

TedW: I notice your original script is calling do shell script within an application tell block. That results in an error that is silently fixed by redirecting the event. I don’t know if that’s adding much time, or what, but you may want to change it.

It should be 10.

No, the code you cut out was deliberate. It didn’t work as intended, but making two images for each size is required.

Would it be better to ‘my’ it or to pop it into a handler and call it that way? I don’t really know much about the internal dynamics here; it would not have occurred to me that this threw a silent error, or that throwing an error that way might produce significantly more overhead than the context shift of a normal handler call.

Works perfect. Many thanks for your script.
Now, I have one little question. Icons with 48x48 and 1024x1024 pixel size is absent in the sizes list. No need, or is wrong to specify that size too?

I’m not sure it’s that simple because of the way you’re getting the POSIX path of iconsetFolder – it looks to me like it might be relying of System Events.

I think the best general approach is to put inside app tell blocks only what needs to be inside app tell blocks. There’s no need to be anal about it, but it can avoid some pitfalls and it does avoid the standard additions issue.

I don’t know about significantly, but FWIW you can see it happening in the log.

Apple decides which sizes belong and don’t. The whole idea is to have optimized images at the resolutions the OS will look for when drawing icons.