Skip to content

Instantly share code, notes, and snippets.

@ttscoff
Last active May 2, 2024 07:53
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ttscoff/1f30e9042a4cfbdc68bc3fb3fec64046 to your computer and use it in GitHub Desktop.
Save ttscoff/1f30e9042a4cfbdc68bc3fb3fec64046 to your computer and use it in GitHub Desktop.
Grab an iOS or Mac app icon from local version or iTunes search
#!/usr/bin/env ruby -W1
# frozen_string_literal: true
# Mac only
#
# Usage: grabicon.rb SEARCH TERMS [%[small|medium|large] [@[mac|ios|iphone|ipad]]
# If the search terms match a local app, that app's icon will be extracted and converted to PNG
# If the search terms don't match a local app, iTunes will be searched
# If the search terms end with "@mac", "@iphone", "@ipad", or "@ios", iTunes will be searched for a match
# If the search terms end with "%large", "%medium", "%small", or "%XXX" icon will be scaled to size
# @ and % values can be specified by -p PLATFORM and -s SIZE
# -o PATH will output icon to specified path
#
# If an iOS app is retrieved from iTunes, its corners will be automatically rounded and padding will be
# added to the resulting image.
#
# Requires that ImageMagick be installed (brew install imagemagick)
%w[net/http cgi json fileutils optparse].each do |filename|
require filename
end
# Default size of icon
DEFAULT_SIZE = 512
class ::String
def remove_parameters
gsub(/ *(@(mac|i(os|phone|pad))|%(s(m(all)?)?|m(ed(ium)?)?|l(g|arge)?|\d+))/, '').gsub(/\s+/, ' ').strip
end
end
# Icon retrieval
class GrabIcon
attr_reader :icon
def initialize(terms, options = {})
@terms = terms
@options = {
outfile: nil,
platform: 'mac',
preview: false,
size: DEFAULT_SIZE
}.merge(options)
@icon = if terms =~ /@(ios|ipad|iphone)\b/i || @options[:platform] !~ /mac/i
find_itunes_icon
else
appdir = find_app_dir
if appdir
retrieve_local_icon(appdir)
else
find_itunes_icon
end
end
if @options[:outfile]
FileUtils.mv(@icon, @options[:outfile], force: true)
@icon = @options[:outfile]
end
end
def appdirs
[
'/System/Applications/',
'/Developer/Applications/',
'/Developer/Applications/Utilities/',
'/Applications/Utilities/',
'/Applications/Setapp/',
'/Applications/'
]
end
def find_app_dir
appdir = nil
app = @terms.remove_parameters
appdirs.filter do |dir|
if File.exist?(File.join(dir, "#{app}.app"))
appdir = dir
elsif File.exist?(File.join(dir, "#{app}.localized/#{app}.app"))
appdir = File.join(dir, "#{app}.localized/")
end
end
appdir
end
def icon_size
case @terms
when /%l/
512
when /%m/
100
when /%s/
60
when /%(\d+)/
Regexp.last_match(1).to_i
else
case @options[:size]
when /^l/
512
when /^m/
100
when /^s/
60
else
@options[:size]
end
end
end
def retrieve_local_icon(appdir)
app = @terms.remove_parameters
tempfile = false
icon = `defaults read "#{appdir}#{app}.app/Contents/Info" CFBundleIconName 2> /dev/null`.strip
if icon == ''
icon = `defaults read "#{appdir}#{app}.app/Contents/Info" CFBundleIconFile 2> /dev/null`.strip.sub(/\.icns$/, '')
icon = File.join(appdir, "#{app}.app", "Contents/Resources/#{icon}.icns")
else
tempfile = true
temp_icon = File.join(File.expand_path('~/Desktop'), "#{app}.icns")
`iconutil -c icns #{appdir}#{app}.app/Contents/Resources/Assets.car #{icon} -o #{temp_icon}`
icon = temp_icon
end
outfile = File.join(File.expand_path('~/Desktop'), "#{app}_icon.png")
cmd = [
"/usr/bin/sips -s format png --resampleHeightWidthMax #{icon_size}",
%("#{icon}"),
%(--out "#{outfile}")
]
`#{cmd.join(' ')}`
warn "Wrote PNG to #{outfile}."
FileUtils.rm(icon) if tempfile
outfile
end
def define_entity
case @terms
when /@iphone/i
'software'
when /@mac/i
'macSoftware'
else
case @options[:platform]
when /iphone/i
'software'
when /mac/i
'macSoftware'
else
'iPadSoftware'
end
end
end
def find_itunes_icon
@entity = define_entity
search = @terms.remove_parameters
search = search.gsub(/\s+/, ' ').strip
url = "http://itunes.apple.com/search?term=#{CGI.escape(search)}&entity=#{@entity}"
res = Net::HTTP.get_response(URI.parse(url)).body
begin
json = JSON.parse(res)
selected = json['results'][0]
description = selected['description'].split(/\n/)[0]
title = selected['trackName']
dev = selected['sellerName']
warn "Found #{title} from #{dev}\n\n#{description}..."
icon_url = selected['artworkUrl512'].sub(/512x512/, "#{icon_size}x#{icon_size}")
retrieve_itunes_icon(icon_url, search)
rescue StandardError
raise 'Invalid JSON returned'
end
end
def retrieve_itunes_icon(icon_url, app)
url = URI.parse(icon_url)
filename = "#{app.gsub(/[^a-z0-9]+/i, '-')}_icon.#{icon_url.match(/\.(jpg|png)$/)[1]}"
outfile = File.join(File.expand_path('~/Desktop'), filename)
res = Net::HTTP.get_response(url)
File.open(outfile, 'w+') { |f| f.puts res.body }
warn "Wrote iTunes search result to #{outfile}."
process_ios_image(outfile, icon_size) unless @entity == 'macSoftware'
outfile
rescue StandardError
raise 'Failed to write icon from iTunes search.'
end
def round_corners(filename, size, round = 0.2)
round = (size * round).round.to_s
target = filename.sub(/\.\w+$/, '_round.png')
cmd = [
"convert -size #{size}x#{size} xc:none -fill white -draw 'roundRectangle 0,0 #{size},#{size} #{round},#{round}'",
filename,
'-compose SrcIn -composite',
target
]
`#{cmd.join(' ')} &> /dev/null`
target
end
def process_ios_image(filename, size = nil)
size ||= icon_size
target = round_corners(filename, size, 0.205)
reduced = size - (size * 0.10)
reduced_rect = "#{reduced}x#{reduced}"
`convert -bordercolor 'rgba(0,0,0,0)' -border 50x50 -resize #{reduced_rect} #{target} #{target} &> /dev/null`
FileUtils.rm(filename)
filename.sub!(/\.\w+$/, '.png')
FileUtils.mv(target, filename)
filename
rescue StandardError
raise 'Error rounding corners'
end
end
options = {
preview: false,
size: DEFAULT_SIZE,
platform: 'mac',
outfile: nil
}
optparse = OptionParser.new do |opts|
opts.banner = "Usage: #{File.basename(__FILE__)} 'APP NAME/SEARCH TERM' [@PLATFORM] [%SIZE]"
opts.banner += "\n @ and % modifiers override options\n\nOPTIONS:"
opts.on('-p', '--preview', 'Show a Quick Look preview after saving') do
options[:preview] = true
end
opts.on('-s', '--size SIZE', 'Size to save (small, medium, large, or pixels)') do |size|
options[:size] = size
end
opts.on('-d', '--device PLATFORM', 'Platform to search (mac, ios, iphone, ipad)') do |platform|
options[:platform] = platform if platform =~ /(mac|i(os|phone|pad))/
end
opts.on('-o', '--output PATH', 'Path to save icon to (default ~/Desktop/ICON.png)') do |path|
path = File.expand_path(path)
raise "Directory #{File.dirname(path)} does not exist" unless File.directory?(File.dirname(path))
options[:outfile] = path.sub(/\.(\w+)$/, '.png')
end
opts.on('-h', '--help', 'Display this screen') do
puts opts
exit
end
end
optparse.parse!
icon = GrabIcon.new(ARGV.join(' ').sub(/\.app$/, ''), options)
`qlmanage >/dev/null 2> /dev/null -p #{icon.icon} &` if options[:preview]
puts icon.icon
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment