Module: IOSIconGenerator::Helpers
- Defined in:
- lib/ios_icon_generator.rb,
lib/ios_icon_generator/helpers/which.rb,
lib/ios_icon_generator/helpers/mask_icon.rb,
lib/ios_icon_generator/helpers/generate_icon.rb,
lib/ios_icon_generator/helpers/image_sets_definition.rb
Overview
The helpers used by the commands of IOSIconGenerator.
Class Method Summary collapse
-
.generate_icon(icon_path:, output_folder:, types:, parallel_processes: nil, generate_icon: nil, progress: nil) ⇒ String
Generate an icon using the base icon provided.
-
.image_sets(types) ⇒ Array<Hash<String, String>>
Get the image sets for the given types.
-
.mask_icon(appiconset_path:, output_folder:, mask: { background_color: '#FFFFFF', stroke_color: '#000000', stroke_width_offset: 0.1, suffix: 'Beta', symbol: 'b', symbol_color: '#7F0000', font: 'Helvetica', x_size_ratio: 0.54, y_size_ratio: 0.54, size_offset: 0.0, x_offset: 0.0, y_offset: 0.0, shape: 'triangle', }, parallel_processes: nil, progress: nil) ⇒ String
Mask an icon using the parameters provided.
-
.type_incompatible?(lhs, rhs) ⇒ Boolean
Check if the given types are compatible (if they can be used in the same set).
-
.which(cmd) ⇒ String
Cross-platform way of finding an executable in the $PATH.
Class Method Details
.generate_icon(icon_path:, output_folder:, types:, parallel_processes: nil, generate_icon: nil, progress: nil) ⇒ String
Generate an icon using the base icon provided.
If icon_path
is set to nil
, the function expects
generate_icon
to be set or the function will raise.
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/ios_icon_generator/helpers/generate_icon.rb', line 49 def self.generate_icon(icon_path:, output_folder:, types:, parallel_processes: nil, generate_icon: nil, progress: nil) if icon_path raise 'There is no icon at the path specified.' unless File.exist?(icon_path) raise 'The icon specified must be .pdf.' if File.extname(icon_path) != '.pdf' matches = /(\d+)x(\d+)/.match(`magick identify "#{icon_path}"`) raise 'Unable to verify icon. Please make sure it\'s a valid pdf file and try again.' if matches.nil? width, height = matches.captures raise 'Invalid pdf specified.' if width.nil? || height.nil? raise "The icon must at least be 1024x1024, it currently is #{width}x#{height}." unless width.to_i >= 1024 && height.to_i >= 1024 elsif generate_icon.nil? raise 'icon_path has been set to nil, generate_icon must be specified' end appiconset_path = File.join(output_folder, "#{types.include?(:imessage) ? 'iMessage App Icon' : 'AppIcon'}.#{types.include?(:imessage) ? 'stickersiconset' : 'appiconset'}") FileUtils.mkdir_p(appiconset_path) get_icon_path = lambda { |width, height| return File.join(appiconset_path, "Icon-#{width.to_i}x#{height.to_i}.png") } generate_icon ||= lambda { |base_path, target_path, width, height| size = [width, height].max system( 'magick', 'convert', '-density', '400', base_path, '-colorspace', 'sRGB', '-type', 'truecolor', '-resize', "#{size}x#{size}", '-gravity', 'center', '-crop', "#{width}x#{height}+0+0", '+repage', target_path ) } types.each do |type1| types.each do |type2| raise "Incompatible types used together: #{type1} and #{type2}. These types cannot be added to the same sets; please call the command twice with each different type." if Helpers.type_incompatible?(type1, type2) end end images_sets = Helpers.image_sets(types) smaller_sizes = [] images_sets.each do |image| width, height = /(\d+(?:\.\d)?)x(\d+(?:\.\d)?)/.match(image['size'])&.captures scale, = /(\d+(?:\.\d)?)x/.match(image['scale'])&.captures raise "Invalid size parameter in Contents.json: #{image['size']}" if width.nil? || height.nil? || scale.nil? scale = scale.to_f width = width.to_f * scale height = height.to_f * scale target_path = get_icon_path.call(width, height) image['filename'] = File.basename(target_path) if width > 512 || height > 512 generate_icon.call( icon_path, target_path, width, height ) else smaller_sizes << [width, height] end end total = smaller_sizes.count + 2 progress&.call(nil, total) max_size = smaller_sizes.flatten.max temp_icon_path = File.join(output_folder, '.temp_icon.pdf') begin system('magick', 'convert', '-density', '400', icon_path, '-colorspace', 'sRGB', '-type', 'truecolor', '-scale', "#{max_size}x#{max_size}", temp_icon_path) if icon_path progress&.call(1, total) Parallel.each( smaller_sizes, in_processes: parallel_processes, finish: lambda do |_item, i, _result| progress&.call(i + 1, total) end ) do |width, height| generate_icon.call( temp_icon_path, get_icon_path.call(width, height), width, height ) end ensure FileUtils.rm(temp_icon_path) if File.exist?(temp_icon_path) end contents_json = { images: images_sets, info: { version: 1, author: 'xcode', }, } File.write(File.join(appiconset_path, 'Contents.json'), JSON.pretty_generate(contents_json)) progress&.call(total - 1, total) appiconset_path end |
.image_sets(types) ⇒ Array<Hash<String, String>>
Get the image sets for the given types.
29 30 31 32 33 34 35 36 37 |
# File 'lib/ios_icon_generator/helpers/image_sets_definition.rb', line 29 def self.image_sets(types) types.flat_map do |type| contents_path = File.(File.join(File.dirname(__FILE__), "../../../vendor/Contents-#{type}.json")) raise "Unknown type #{type}" unless File.exist?(contents_path) contents_json = JSON.parse(File.read(contents_path)) contents_json['images'] end end |
.mask_icon(appiconset_path:, output_folder:, mask: { background_color: '#FFFFFF', stroke_color: '#000000', stroke_width_offset: 0.1, suffix: 'Beta', symbol: 'b', symbol_color: '#7F0000', font: 'Helvetica', x_size_ratio: 0.54, y_size_ratio: 0.54, size_offset: 0.0, x_offset: 0.0, y_offset: 0.0, shape: 'triangle', }, parallel_processes: nil, progress: nil) ⇒ String
Mask an icon using the parameters provided.
The mask is for now always generated in the bottom left corner of the image.
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/ios_icon_generator/helpers/mask_icon.rb', line 56 def self.mask_icon( appiconset_path:, output_folder:, mask: { background_color: '#FFFFFF', stroke_color: '#000000', stroke_width_offset: 0.1, suffix: 'Beta', symbol: 'b', symbol_color: '#7F0000', font: 'Helvetica', x_size_ratio: 0.54, y_size_ratio: 0.54, size_offset: 0.0, x_offset: 0.0, y_offset: 0.0, shape: 'triangle', }, parallel_processes: nil, progress: nil ) extension = File.extname(appiconset_path) output_folder = File.join(output_folder, "#{File.basename(appiconset_path, extension)}-#{mask[:suffix]}#{extension}") FileUtils.mkdir_p(output_folder) contents_path = File.join(appiconset_path, 'Contents.json') raise "Contents.json file not found in #{appiconset_path}" unless File.exist?(contents_path) json_content = JSON.parse(File.read(contents_path)) progress&.call(nil, json_content['images'].count) Parallel.each( json_content['images'], in_processes: parallel_processes, finish: lambda do |_item, i, result| json_content['images'][i]['filename'] = result progress&.call(i, json_content['images'].count) end ) do |image| width, height = /(\d+(?:\.\d)?)x(\d+(?:\.\d)?)/.match(image['size'])&.captures scale, = /(\d+(?:\.\d)?)x/.match(image['scale'])&.captures raise "Invalid size parameter in Contents.json: #{image['size']}" if width.nil? || height.nil? || scale.nil? scale = scale.to_f width = width.to_f * scale height = height.to_f * scale mask_size_width = width * mask[:x_size_ratio].to_f mask_size_height = height * mask[:y_size_ratio].to_f extension = File.extname(image['filename']) icon_output = "#{File.basename(image['filename'], extension)}-#{mask[:suffix]}#{extension}" icon_output_path = File.join(output_folder, icon_output) draw_shape_parameters = "-strokewidth '#{(mask[:stroke_width_offset] || 0) * [width, height].min}' -stroke '#{mask[:stroke_width_offset].zero? ? 'none' : (mask[:stroke_color] || '#000000')}' -fill '#{mask[:background_color] || '#FFFFFF'}'" draw_shape = case mask[:shape] when :triangle "-draw \"polyline -#{width},#{height - mask_size_height} 0,#{height - mask_size_height} #{mask_size_width},#{height} #{mask_size_width},#{height * 2.0} -#{width},#{height * 2.0}\"" when :square "-draw \"rectangle -#{width},#{height * 2.0} #{mask_size_height},#{width - mask_size_width}\"" else raise "Unknown mask shape: #{mask[:shape]}" end draw_symbol = if mask[:file] "\\( -background none -density 1536 -resize #{width * mask[:size_offset]}x#{height} \"#{mask[:file]}\" -geometry +#{width * mask[:x_offset]}+#{height * mask[:y_offset]} \\) -gravity southwest -composite" else "-strokewidth 0 -stroke none -fill '#{mask[:symbol_color] || '#7F0000'}' -font '#{mask[:font]}' -pointsize #{height * mask[:size_offset] * 2.0} -annotate +#{width * mask[:x_offset]}+#{height - height * mask[:y_offset]} '#{mask[:symbol]}'" end system("convert '#{File.join(appiconset_path, image['filename'])}' #{draw_shape_parameters} #{draw_shape} #{draw_symbol} '#{icon_output_path}'") next icon_output end File.write(File.join(output_folder, 'Contents.json'), JSON.pretty_generate(json_content)) output_folder end |
.type_incompatible?(lhs, rhs) ⇒ Boolean
Check if the given types are compatible (if they can be used in the same set)
46 47 48 |
# File 'lib/ios_icon_generator/helpers/image_sets_definition.rb', line 46 def self.type_incompatible?(lhs, rhs) (lhs == :imessage && rhs != :imessage) || (lhs != :imessage && rhs == :imessage) end |
.which(cmd) ⇒ String
Cross-platform way of finding an executable in the $PATH.
From stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby
27 28 29 30 31 32 33 34 35 36 |
# File 'lib/ios_icon_generator/helpers/which.rb', line 27 def self.which(cmd) exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| exts.each do |ext| exe = File.join(path, "#{cmd}#{ext}") return exe if File.executable?(exe) && !File.directory?(exe) end end nil end |