Last active
October 22, 2023 18:10
-
-
Save ttscoff/6b54f187724ceffab9316998362b056f to your computer and use it in GitHub Desktop.
Retrieve and process an icon for a local or App Store app
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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