iconoodle - Convenient ICNS to PNG image conversion

Soon after this article about ICNS to PNG conversion was published by Rob Griffiths on Macworld, a good friend of mine called me to ask if I could write an AppleScript wrapper for the mentioned «sips» command, as he needed some advanced features to simplify his daily work life as a stressed Mac admin.

Yes, he knew that their already existed scripts and apps for this purpose…
Yes, he knew that I was busy with other projects…
Yes, he knew that I simply cannot deny a request from a good friend :wink:

And so here it is, the result of one week of coding in the evenings after work:

iconoodle - Convenient ICNS to PNG conversion (v0.7b, ca. 68 KB)

So now you might say: “Hey Martin, what exactly are those advanced features of your ‘yet another ICNS to PNG conversion script’?”

OK, here you are:

Cool and convenient features of iconoodle
¢ You can not only drag & drop ICNS files, but also applications onto iconoodle’s icon and it will convert all ICNS files found in the application package folder (cautious clapping)
¢ You can set a default archive folder for the created PNG files (growing enthusiasm)
¢ You can execute post action scripts to further process the created PNG files (giggle of the geeks), sample post actions are included with the download
¢ Preferences panel to adjust the settings (the mob goes wild!)

Requirements
¢ Mac OS X 10.4.10 (might also run on older systems)

Usage
After you installed iconoodle on your Mac - just drop it anywhere you like, dock, desktop, folder bar, you name it - you can use it in exactly two ways:

  1. Drag and drop ICNS files and/or applications bundles onto iconoodle’s icon to start the ICNS to PNG conversion process. Or drag a single folder onto iconoodle’s icon to set this folder as the default archive folder for created PNG files.

  2. Start iconoodle by double-clicking its icon to display the preferences panel. Here you can set/reset the default archive folder and add/remove post action scripts.

Post actions
To further enhance the iconoodle experience, you can also execute various (self-written) post action scripts to process the created PNG files. Two sample post action scripts are included with the download.

The first script, named «openpngs.scpt», will open all created PNG files in the default viewer application. The second script, named «mailpngs.scpt», will create a new eMail message in Apple Mail with the created PNG files attached.

Please have a look at the source code of those two AppleScripts to see how easy it is to write your own post action scripts, if you are familiar with AppleScript.

To add or remove post actions, just open the preferences panel and select the corresponding menu items.

FAQ
Q: Who came up with the lame name of the script?
A: I own this book and am a follower. Moreover I am absolutely blown away by this costume.


-- created: October 2007
-- modified:
-- version: 0.7b
-- history:
-- ¢ v0.7b:
--   + first public beta release

-- =====================
-- == CORE PROPERTIES ==
-- =====================

-- what a silly name for a script :)
-- DON'T INSULT OUR GOD, THE FLYING SPAGHETTI MONSTER!!!
property mytitle : "iconoodle"
property domain : ("com.jos." & mytitle)

-- =========================
-- == START MODE HANDLERS ==
-- ========================= 

on open dropitems
	my main("on open", dropitems)
end open

on run
	my main("on run", missing value)
end run

-- ==================
-- == MAIN ROUTINE ==
-- ==================

on main(startmode, args)
	try
		-- first of all: initializing the preferences system
		my userdefaults's setdomain(domain)
		
		-- user started the script by double-clicking its icon
		-- -> display preferences panel
		if startmode is "on run" then
			my guiloop()
			-- user started the script by dropping items onto its icon
			-- -> process dropped items
		else if startmode is "on open" then
			set dropitems to args
			set dropapps to {}
			set dropicons to {}
			set dropdirs to {}
			-- screening dropped items for relevant item kinds (apps, folders, icns files)
			repeat with dropitem in dropitems
				set dropitem to dropitem as alias
				set iteminfo to info for (dropitem as alias)
				-- searching for dropped applications
				if package folder of iteminfo is true and name extension of iteminfo is "app" then
					set dropapps to dropapps & dropitem
					-- searching for dropped icns files
				else if folder of iteminfo is false and name extension of iteminfo is "icns" then
					set dropicons to dropicons & dropitem
					-- searching for dropped folders
				else if folder of iteminfo is true and package folder of iteminfo is false then
					set dropdirs to dropdirs & dropitem
				end if
			end repeat
			
			-- surveying the booty...
			set countdropicons to length of dropicons
			set countdropapps to length of dropapps
			set countdropdirs to length of dropdirs
			
			-- no icns files, noapps, no folders...
			if countdropicons is 0 and countdropapps is 0 and countdropdirs is 0 then
				set infomsg to "Sorry, but we could not detect any processable items." & return & return & my getusageinfo()
				my dspinfomsg(infomsg)
				return
				-- no icns files, no apps, no scripts, but more than 1 folder...
			else if countdropicons is 0 and countdropapps is 0 and countdropdirs is greater than 1 then
				set infomsg to "Sorry, you dropped " & countdropdirs & " folders onto the script." & return & return & "If you want to set the default archive folder, please just drop a single folder onto " & mytitle & "'s icon."
				my dspinfomsg(infomsg)
				return
				-- no icns files, no apps, but 1 folder was dropped onto iconoodle -> archive folder
			else if countdropicons is 0 and countdropapps is 0 and countdropdirs is 1 then
				-- Mac path! ':'
				set archivefolder to (item 1 of dropdirs) as Unicode text
				my userdefaults's setpref("archivefolder", archivefolder)
				set infomsg to "Your default archive folder is now located at:" & return & return & "'" & archivefolder & "'"
				my dspinfomsg(infomsg)
				return
			end if
			
			-- getting the archive folder...
			-- archive folder is a Mac path! ':'
			if my userdefaults's prefsfileexists() is false then
				-- no preferences file yet...so we have to ask the user for the archive folder
				set archivefolder to (my askforarchivefolder() as Unicode text)
			else
				-- preferences file exists and default archive folder is set
				if my userdefaults's haspref("archivefolder") is true then
					set archivefolder to my userdefaults's getpref("archivefolder")
					-- preferences file exists but default archive folder is NOT set
				else
					set archivefolder to (my askforarchivefolder() as Unicode text)
				end if
			end if
			
			-- container for the gathereed icns files
			set icnsfiles to {}
			
			-- dropped icns files are added directly to the list of icns files
			if countdropicons is greater than 0 then
				set icnsfiles to icnsfiles & dropicons
			end if
			
			-- searching the application bundles for icns files
			-- adding found icns files to the list of icns files
			repeat with dropapp in dropapps
				set icnsfiles to icnsfiles & my searchicnsfiles(dropapp)
			end repeat
			
			-- getting filepaths for the new png files, then:
			-- icns > png conversion process
			set converrcounter to 0
			set converrmsgs to ""
			repeat with icnsfile in icnsfiles
				set fileinfo to info for icnsfile
				set filename to (characters 1 through -6 of (name of fileinfo)) as Unicode text
				-- Posix path! '/'
				set pngpath to POSIX path of (my getunusedfilepath(archivefolder, filename, "png"))
				try
					my icns2png(POSIX path of icnsfile, pngpath)
				on error errmsg number errnum
					set converrcounter to converrcounter + 1
					set converrmsgs to converrmsgs & errmsg & " (" & errnum & ")" & return
				end try
			end repeat
			
			set pacterrcounter to 0
			set pacterrmsgs to ""
			-- executing post actions if available
			-- WALLA WALLA! We should check existence of postactions!
			if my userdefaults's prefsfileexists() is true and my userdefaults's haspref("postactions") is true then
				-- Mac paths! ':'
				set postactions to my userdefaults's getpref("postactions")
				set icnspaths to ""
				repeat with icnsfile in icnsfiles
					set icnspaths to icnspaths & " " & quoted form of (POSIX path of (icnsfile as Unicode text))
				end repeat
				repeat with postaction in postactions
					try
						set command to ("osascript " & quoted form of (POSIX path of postaction) & icnspaths) as «class utf8»
						do shell script command
					on error errmsg number errnum
						set pacterrcounter to pacterrcounter + 1
						set pacterrmsgs to pacterrmsgs & errmsg & " (" & errnum & ")" & return
					end try
				end repeat
			end if
			
			-- of course: I don't hope for many errors, but in case they occur,
			-- we should inform the user accordingly...
			if converrcounter > 0 or pacterrcounter > 0 then
				set errmsg to "Sorry, some errors occured:" & return & return & "Conversion errors: " & converrcounter & return & "Post action errors: " & pacterrcounter
				set errlog to "Conversion errors:" & return & converrmsgs & return & "Post action errors:" & return & pacterrmsgs
				tell me
					activate
					display dialog errmsg buttons {"Show log", "OK"} default button 2 with icon caution with title mytitle
					set choice to result
					if button returned of choice is "Show log" then
						tell application "TextEdit"
							make new document with properties {text:errlog}
						end tell
					end if
				end tell
			end if
		end if
	on error errmsg number errnum
		-- ignoring 'User canceled'-error
		if errnum is not -128 then
			my dsperrmsg(errmsg, errnum)
		end if
	end try
end main

-- ================
-- == ICNS TOOLS ==
-- ================

-- returns icns files found in the application package folder
-- application must be passed as an alias
-- returns the icns files as a list of aliases
on searchicnsfiles(appalias)
	set icnsfiles to {}
	set apppath to (POSIX path of appalias)
	set command to ("find " & quoted form of apppath & " -name '*.icns'") as «class utf8»
	set icnspaths to paragraphs of (do shell script command)
	repeat with icnspath in icnspaths
		set icnsfiles to icnsfiles & (POSIX file icnspath)
	end repeat
	return icnsfiles
end searchicnsfiles

-- converts an icns file to a png file
-- icns source and png destination must be passed as a Posix path
on icns2png(icnspath, pngpath)
	set command to ("sips -s format png " & quoted form of icnspath & "  --out " & quoted form of pngpath) as «class utf8»
	do shell script command
end icns2png

-- ========================
-- == GUI CONTROL CENTER ==
-- ========================

-- the main GUI loop displaying the preferences panel
-- and coordinating actions triggered by chosen menu items
on guiloop()
	try
		set menuitem to my dspprefsmenu()
		if menuitem is missing value then
			return
		else
			if menuitem is "Show archive folder" then
				my showarchivefolder()
			else if menuitem is "Set archive folder" then
				my setarchivefolder()
			else if menuitem is "Reset archive folder" then
				my resetarchivefolder()
			else if menuitem is "Add post action(s)" then
				my addpostactions()
			else if menuitem is "Show post action(s)" then
				my showpostactions()
			else if menuitem is "Remove post action(s)" then
				my removepostactions()
			else if menuitem is "eMail feedback" then
				my emailfeedback()
			else if menuitem is "Visit JoS" then
				my visitjoswebsite()
			end if
		end if
		my guiloop()
	on error errmsg number errnum
		if errnum is -128 then
			my guiloop()
		else
			my dsperrmsg(errmsg, errnum)
		end if
	end try
end guiloop

-- displays the preferences menu and returns the chosen menu item
on dspprefsmenu()
	set menuitems to {"== Misc ==", "eMail feedback", "Visit JoS"}
	
	if my userdefaults's prefsfileexists() is false then
		set menuitems to {"== Archive folder  ==", "Set archive folder", "", "== Post Actions ==", "Add post action(s)", ""} & menuitems
	else
		if my userdefaults's haspref("postactions") is false then
			set menuitems to {"== Post Actions ==", "Add post action(s)", ""} & menuitems
		else
			set menuitems to {"== Post Actions ==", "Show post action(s)", "Add post action(s)", "Remove post action(s)", ""} & menuitems
		end if
		if my userdefaults's haspref("archivefolder") is false then
			set menuitems to {"== Archive folder  ==", "Set archive folder", ""} & menuitems
		else
			set menuitems to {"== Archive folder  ==", "Show archive folder", "Set archive folder", "Reset archive folder", ""} & menuitems
		end if
	end if
	
	choose from list menuitems with title mytitle with prompt "Please choose an option:" cancel button name "Quit" OK button name "Select" without multiple selections allowed and empty selection allowed
	set choice to result
	if choice is not false then
		set menuitem to item 1 of choice
		if menuitem begins with "==" or menuitem is "" then
			my dspprefsmenu()
		else
			return menuitem
		end if
	else
		return missing value
	end if
end dspprefsmenu

-- ================================
-- == SUBROUTINES FOR MENU ITEMS ==
-- ================================

-- shows the set archive folder in the Finder
on showarchivefolder()
	set archivefolder to POSIX path of (my userdefaults's getpref("archivefolder"))
	set command to ("open " & quoted form of archivefolder) as «class utf8»
	do shell script command
end showarchivefolder

-- asks for and sets the archive folder
on setarchivefolder()
	set archivefolder to (my askforarchivefolder()) as Unicode text
	my userdefaults's setpref("archivefolder", archivefolder)
end setarchivefolder

-- resets the current archive folder
on resetarchivefolder()
	my userdefaults's rempref("archivefolder")
end resetarchivefolder

-- shows the parent folder of selected post actions in the Finder
on showpostactions()
	set postactions to my dsppostactions()
	if postactions is missing value then
		return
	else
		repeat with postaction in postactions
			set pardirpath to my getpardirpath((postaction as Unicode text))
			set pardirpath to POSIX path of pardirpath
			set command to ("open " & quoted form of pardirpath) as «class utf8»
			do shell script command
		end repeat
	end if
end showpostactions

-- adds new post actions
on addpostactions()
	set scriptfiles to choose file with prompt "Please choose scripts for post actions:" of type {"osas"} with multiple selections allowed without showing package contents and invisibles
	set newpostactions to {}
	repeat with scriptfile in scriptfiles
		set newpostactions to newpostactions & (scriptfile as Unicode text)
	end repeat
	if my userdefaults's haspref("postactions") then
		set existpostactions to my userdefaults's getpref("postactions")
		repeat with newpostaction in newpostactions
			if newpostaction is not in existpostactions then
				set existpostactions to existpostactions & newpostaction
			end if
		end repeat
		my userdefaults's setpref("postactions", existpostactions)
	else
		my userdefaults's setpref("postactions", newpostactions)
	end if
end addpostactions

-- removes post actions
on removepostactions()
	set chosenpostactions to my dsppostactions()
	if chosenpostactions is missing value then
		return
	else
		set existpostactions to my userdefaults's getpref("postactions")
		set remainpostactions to {}
		repeat with existpostaction in existpostactions
			if existpostaction is not in chosenpostactions then
				set remainpostactions to remainpostactions & existpostaction
			end if
		end repeat
		if remainpostactions is {} then
			my userdefaults's rempref("postactions")
		else
			my userdefaults's setpref("postactions", remainpostactions)
		end if
	end if
end removepostactions

-- creates a new email message with my address in the default mail app
on emailfeedback()
	set command to "open 'mailto:martin@joyofscripting.com'" as «class utf8»
	do shell script command
end emailfeedback

-- opens the Joy of Scripting website in the default browser
on visitjoswebsite()
	set command to "open 'http://www.joyofscripting.com'" as «class utf8»
	do shell script command
end visitjoswebsite

-- =============================
-- == MISCELLANEOUS UTILITIES ==
-- =============================

-- displays a list of available post actions
-- returns the file path of the chosen post action
-- returns a Mac path! ':'
on dsppostactions()
	set postactionids to {}
	set postactionnames to {}
	set postactionpaths to {}
	-- FUTURE: the «postactions» should be sorted alphabetically (python script cmdline?)
	set postactions to my userdefaults's getpref("postactions")
	set idcounter to 0
	repeat with postaction in postactions
		set idcounter to idcounter + 1
		set olddelims to AppleScript's text item delimiters
		set AppleScript's text item delimiters to {":"}
		set txtitems to text items of postaction
		set AppleScript's text item delimiters to olddelims
		set postactionname to "[" & idcounter & "] " & last item of txtitems
		set postactionids to postactionids & idcounter
		set postactionnames to postactionnames & postactionname
		set postactionpaths to postactionpaths & postaction
	end repeat
	choose from list postactionnames with title mytitle with prompt "Please choose post actions to process:" OK button name "Select" cancel button name "Cancel" with multiple selections allowed without empty selection allowed
	set choice to result
	if choice is not false then
		set chosenpostactions to {}
		set menuitems to choice
		repeat with menuitem in menuitems
			set rightbracketoffset to offset of "]" in menuitem
			if rightbracketoffset is equal to 3 then
				set menuitemid to (character 2 of menuitem) as integer
			else
				set menuitemid to ((characters 2 through (rightbracketoffset - 1) of menuitem) as Unicode text) as integer
			end if
			repeat with i from 1 to (length of postactionids)
				set postactionid to item i of postactionids
				if menuitemid is equal to postactionid then
					set postactionpath to item i of postactionpaths
					set chosenpostactions to chosenpostactions & postactionpath
				end if
			end repeat
		end repeat
		log chosenpostactions
		return chosenpostactions
	else
		return missing value
	end if
end dsppostactions

-- returns the parent folder of an item as a Mac path! ':'
-- expects «itempath» to be a Mac path! ':'
-- origin: http://www.fischer-bayern.de/applescript/html/parent_f.html
on getpardirpath(itempath)
	set olddelims to AppleScript's text item delimiters
	set AppleScript's text item delimiters to {":"}
	set counttxtitems to (count text items of itempath)
	set lasttxtitem to the last text item of itempath
	if lasttxtitem = "" then
		set counttxtitems to counttxtitems - 2 -- bei Pfad zu einem Ordner 
	else
		set counttxtitems to counttxtitems - 1 -- bei Pfad zu einer Datei
	end if
	set pardirpath to text 1 thru text item counttxtitems of itempath & ":"
	set AppleScript's text item delimiters to olddelims
	return pardirpath
end getpardirpath

-- returns an unused file path (Mac path!)
-- expects «folderpath» to be a Mac path! ':'
-- example:
-- folderpath: 'DandyDisk:Users:bender:Desktop:icnsfiles:'
-- filename: 'example'
-- suffix: 'png'
-- -> filepath: 'DandyDisk:Users:bender:Desktop:icnsfiles:example.png'
-- if this filepath already exists:
-- -> filepath: 'DandyDisk:Users:bender:Desktop:icnsfiles:example 1.png'
-- and so on...
on getunusedfilepath(folderpath, filename, suffix)
	set counter to 0
	repeat
		if counter is equal to 0 then
			set filepath to folderpath & filename & "." & suffix
		else
			set filepath to folderpath & filename & " " & counter & "." & suffix
		end if
		try
			set filealias to filepath as alias
		on error
			exit repeat
		end try
		set counter to counter + 1
	end repeat
	return filepath
end getunusedfilepath

-- returns the usage info for iconoodle
on getusageinfo()
	set usageinfo to "To use " & mytitle & ", you can drop ICNS files & application bundles on its icon." & return & return & "If you want to quickly set your default archive folder, you can also drop the folder of your choice onto " & mytitle & "." & return & return & "Start " & mytitle & " by double-clicking its icon to access the preferences panel."
	return usageinfo
end getusageinfo

-- asks the user to provide an archive folder
on askforarchivefolder()
	tell me
		activate
		set archivefolder to choose folder with prompt "Please choose an archive folder for generated PNG files:" without multiple selections allowed, invisibles and showing package contents
	end tell
	return archivefolder
end askforarchivefolder

-- displays a simple info message
on dspinfomsg(infomsg)
	tell me
		activate
		display dialog infomsg buttons {"OK"} default button 1 with title mytitle
	end tell
end dspinfomsg

-- displays an error message
on dsperrmsg(errmsg, errnum)
	tell me
		activate
		display dialog "Sorry, an error occured:" & return & return & errmsg & " (" & errnum & ")" buttons {"OK"} default button 1 with title mytitle with icon stop
	end tell
end dsperrmsg

-- =================================
-- == USER DEFAULTS SCRIPT OBJECT ==
-- =================================

-- WOHOOOO!
script userdefaults
	property domain : missing value
	
	-- well...sets the domain
	on setdomain(thedomain)
		set domain to thedomain
	end setdomain
	
	-- indicates whether the preferences file already exists or not
	on prefsfileexists()
		set prefsfilepath to ((path to preferences folder from user domain) as Unicode text) & domain & ".plist"
		try
			set prefsfilealias to prefsfilepath as alias
			return true
		on error
			return false
		end try
	end prefsfileexists
	
	-- creates a preferences file
	on createprefsfile()
		-- :) yeah, lame hack, very quick and very dirty *g* 
		set command to ("defaults write " & domain & " foo foo") as «class utf8»
		do shell script command
	end createprefsfile
	
	-- returns the file path of the preferences file
	-- returns a Mac path! ':'
	on getprefsfilepath()
		set prefsfilepath to (((path to preferences folder from user domain) as Unicode text) & domain & ".plist")
		return prefsfilepath
	end getprefsfilepath
	
	-- indicates whether the given preferences key exists or not
	on haspref(prefkey)
		try
			tell application "System Events"
				set prefvalue to value of property list item prefkey of property list file (my getprefsfilepath())
			end tell
			return true
		on error
			return false
		end try
	end haspref
	
	-- returns the value for the given preferences key
	on getpref(prefkey)
		tell application "System Events"
			set prefvalue to value of property list item prefkey of property list file (my getprefsfilepath())
		end tell
		return prefvalue
	end getpref
	
	-- creates a key/value-pair in the preferences file
	-- creates the preferences file if it does not already exist
	on setpref(prefkey, prefvalue)
		-- if the preferences file does not already exist, we  create it
		if not prefsfileexists() then
			my createprefsfile()
		end if
		tell application "System Events"
			try
				-- maybe we are lucky and the property list item already exists
				set value of property list item prefkey of property list file (my getprefsfilepath()) to prefvalue
			on error
				-- no, we are unlucky and now have to create the property list item before manipulating its value
				my createprefkey(prefkey)
				set value of property list item prefkey of property list file (my getprefsfilepath()) to prefvalue
			end try
		end tell
	end setpref
	
	-- removes the given preferences key from the preferences file
	-- yes, also necessary sometimes...
	on rempref(prefkey)
		set command to ("defaults delete " & domain & " " & prefkey) as «class utf8»
		do shell script command
	end rempref
	
	-- creates a preferences key in a preferences file
	-- lame hack, once again, but using "System Events" sometimes just sucks ;)
	on createprefkey(prefkey)
		set command to ("defaults write " & domain & " " & prefkey & " dummyvalue") as «class utf8»
		do shell script command
	end createprefkey
end script