Skip to content

Instantly share code, notes, and snippets.

@ttscoff
Last active October 22, 2023 18:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ttscoff/6b54f187724ceffab9316998362b056f to your computer and use it in GitHub Desktop.
Save ttscoff/6b54f187724ceffab9316998362b056f to your computer and use it in GitHub Desktop.
Retrieve and process an icon for a local or App Store app
#!/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 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
# Size of icon
DEFAULT_SIZE = 512
# Icon retrieval
class GrabIcon
attr_reader :icon
def initialize(terms)
@terms = terms
@icon = if terms =~ /@(ios|ipad|iphone)\b/i
find_itunes_icon
else
appdir = find_app_dir
if appdir
retrieve_local_icon(appdir)
else
find_itunes_icon
end
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.gsub(/ *(@(mac|i(os|phone|pad))|%(s(m(all)?)?|m(ed(ium)?)?|l(g|arge)?|\d+))/, '').gsub(/\s+/, ' ').strip
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 size
case @terms
when /%l/
512
when /%m/
256
when /%s/
128
when /%(\d+)/
Regexp.last_match(1).to_i
else
DEFAULT_SIZE
end
end
def retrieve_local_icon(appdir)
app = @terms.gsub(/ *(@(mac|i(os|phone|pad))|%(s(m(all)?)?|m(ed(ium)?)?|l(g|arge)?|\d+))/, '').gsub(/\s+/, ' ').strip
icon = `defaults read "#{appdir}#{app}.app/Contents/Info" CFBundleIconFile`.strip.sub(/\.icns$/, '')
outfile = File.join(File.expand_path('~/Desktop'), "#{app}_icon.png")
cmd = [
"/usr/bin/sips -s format png --resampleHeightWidthMax #{size}",
%("#{File.join(appdir, "#{app}.app", "Contents/Resources/#{icon}.icns")}"),
%(--out "#{outfile}")
]
`#{cmd.join(' ')}`
warn "Wrote PNG to #{outfile}."
outfile
end
def define_entity
case @terms
when /\biphone\b/i
'software'
when /\bmac\b/i
'macSoftware'
else
'iPadSoftware'
end
end
def find_itunes_icon
@entity = define_entity
search = @terms.gsub(/ *(@(mac|i(os|phone|pad))|%(s(m(all)?)?|m(ed(ium)?)?|l(g|arge)?|\d+))/, '').gsub(/\s+/, ' ').strip
url = "http://itunes.apple.com/search?term=#{CGI.escape(search)}&entity=#{@entity}"
pp url
res = Net::HTTP.get_response(URI.parse(url)).body
begin
json = JSON.parse(res)
pp json
icon_url = json['results'][0]['artworkUrl512']
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_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, DEFAULT_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 = 512)
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
optparse = OptionParser.new do |opts|
opts.banner = "Usage: #{File.basename(__FILE__)} 'APP NAME/SEARCH TERM' SLUG"
opts.on('-h', '--help', 'Display this screen') do
puts opts
exit
end
end
optparse.parse!
icon = GrabIcon.new(ARGV.join(' ').sub(/\.app$/, ''))
puts icon.icon
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment