ios_icon_generator 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +53 -0
- data/README.md +144 -0
- data/bin/icongen +16 -0
- data/lib/ios_icon_generator.rb +40 -0
- data/lib/ios_icon_generator/cli/commands/generate.rb +61 -0
- data/lib/ios_icon_generator/cli/commands/mask.rb +91 -0
- data/lib/ios_icon_generator/cli/commands/stub.rb +94 -0
- data/lib/ios_icon_generator/cli/commands/version.rb +32 -0
- data/lib/ios_icon_generator/helpers/generate_icon.rb +138 -0
- data/lib/ios_icon_generator/helpers/images_sets_definition.rb +35 -0
- data/lib/ios_icon_generator/helpers/mask_icon.rb +101 -0
- data/lib/ios_icon_generator/helpers/which.rb +41 -0
- data/lib/ios_icon_generator/version.rb +19 -0
- data/vendor/Contents-carplay.json +18 -0
- data/vendor/Contents-imessage.json +78 -0
- data/vendor/Contents-ipad.json +58 -0
- data/vendor/Contents-iphone.json +53 -0
- data/vendor/Contents-mac.json +58 -0
- data/vendor/Contents-watch.json +81 -0
- metadata +246 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2019 Fueled Digital Media, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require 'hanami/cli'
|
18
|
+
|
19
|
+
module IOSIconGenerator
|
20
|
+
module CLI
|
21
|
+
module Commands
|
22
|
+
class Version < Hanami::CLI::Command
|
23
|
+
desc 'Print version'
|
24
|
+
def call(*)
|
25
|
+
puts IOSIconGenerator::VERSION
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
register 'version', Version, aliases: ['v', '-v', '--version']
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2019 Fueled Digital Media, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require 'json'
|
18
|
+
require 'fileutils'
|
19
|
+
require 'ios_icon_generator/helpers/images_sets_definition'
|
20
|
+
|
21
|
+
module IOSIconGenerator
|
22
|
+
module Helpers
|
23
|
+
def self.generate_icon(icon_path:, output_folder:, types:, parallel_processes: nil, generate_icon: nil, progress: nil)
|
24
|
+
if icon_path
|
25
|
+
matches = /(\d+)x(\d+)/.match(`magick identify "#{icon_path}"`)
|
26
|
+
raise 'There is no icon at the path specified.' unless File.exist?(icon_path)
|
27
|
+
|
28
|
+
raise 'The icon specified must be .pdf.' if File.extname(icon_path) != '.pdf'
|
29
|
+
|
30
|
+
raise 'Unable to verify icon. Please make sure it\'s a valid pdf file and try again.' if matches.nil?
|
31
|
+
|
32
|
+
width, height = matches.captures
|
33
|
+
raise 'Invalid pdf specified.' if width.nil? || height.nil?
|
34
|
+
|
35
|
+
raise "The icon must at least be 1024x1024, it currently is #{width}x#{height}." unless width.to_i >= 1024 && height.to_i >= 1024
|
36
|
+
end
|
37
|
+
appiconset_path = File.join(output_folder, "#{types.include?(:imessage) ? 'iMessage App Icon' : 'AppIcon'}.#{types.include?(:imessage) ? 'stickersiconset' : 'appiconset'}")
|
38
|
+
|
39
|
+
FileUtils.mkdir_p(appiconset_path)
|
40
|
+
|
41
|
+
get_icon_path = lambda { |width, height|
|
42
|
+
return File.join(appiconset_path, "Icon-#{width.to_i}x#{height.to_i}.png")
|
43
|
+
}
|
44
|
+
|
45
|
+
generate_icon ||= lambda { |base_path, target_path, width, height|
|
46
|
+
size = [width, height].max
|
47
|
+
system(
|
48
|
+
'magick',
|
49
|
+
'convert',
|
50
|
+
'-density',
|
51
|
+
'400',
|
52
|
+
base_path,
|
53
|
+
'-colorspace',
|
54
|
+
'sRGB',
|
55
|
+
'-type',
|
56
|
+
'truecolor',
|
57
|
+
'-resize', "#{size}x#{size}",
|
58
|
+
'-gravity',
|
59
|
+
'center',
|
60
|
+
'-crop',
|
61
|
+
"#{width}x#{height}+0+0",
|
62
|
+
'+repage',
|
63
|
+
target_path
|
64
|
+
)
|
65
|
+
}
|
66
|
+
|
67
|
+
types.each do |type1|
|
68
|
+
types.each do |type2|
|
69
|
+
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)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
images_sets = Helpers.images_sets(types)
|
74
|
+
|
75
|
+
smaller_sizes = []
|
76
|
+
images_sets.each do |image|
|
77
|
+
width, height = /(\d+(?:\.\d)?)x(\d+(?:\.\d)?)/.match(image['size'])&.captures
|
78
|
+
scale, = /(\d+(?:\.\d)?)x/.match(image['scale'])&.captures
|
79
|
+
raise "Invalid size parameter in Contents.json: #{image['size']}" if width.nil? || height.nil? || scale.nil?
|
80
|
+
|
81
|
+
scale = scale.to_f
|
82
|
+
width = width.to_f * scale
|
83
|
+
height = height.to_f * scale
|
84
|
+
|
85
|
+
target_path = get_icon_path.call(width, height)
|
86
|
+
image['filename'] = File.basename(target_path)
|
87
|
+
if width > 512 || height > 512
|
88
|
+
generate_icon.call(
|
89
|
+
icon_path,
|
90
|
+
target_path,
|
91
|
+
width,
|
92
|
+
height
|
93
|
+
)
|
94
|
+
else
|
95
|
+
smaller_sizes << [width, height]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
total = smaller_sizes.count + 2
|
100
|
+
progress&.call(nil, total)
|
101
|
+
|
102
|
+
max_size = smaller_sizes.flatten.max
|
103
|
+
temp_icon_path = File.join(output_folder, '.temp_icon.pdf')
|
104
|
+
begin
|
105
|
+
system('magick', 'convert', '-density', '400', icon_path, '-colorspace', 'sRGB', '-type', 'truecolor', '-scale', "#{max_size}x#{max_size}", temp_icon_path) if icon_path
|
106
|
+
progress&.call(1, total)
|
107
|
+
Parallel.each(
|
108
|
+
smaller_sizes,
|
109
|
+
in_processes: parallel_processes,
|
110
|
+
finish: lambda do |_item, i, _result|
|
111
|
+
progress&.call(i + 1, total)
|
112
|
+
end
|
113
|
+
) do |width, height|
|
114
|
+
generate_icon.call(
|
115
|
+
temp_icon_path,
|
116
|
+
get_icon_path.call(width, height),
|
117
|
+
width,
|
118
|
+
height
|
119
|
+
)
|
120
|
+
end
|
121
|
+
ensure
|
122
|
+
FileUtils.rm(temp_icon_path) if File.exist?(temp_icon_path)
|
123
|
+
end
|
124
|
+
|
125
|
+
contents_json = {
|
126
|
+
images: images_sets,
|
127
|
+
info: {
|
128
|
+
version: 1,
|
129
|
+
author: 'xcode',
|
130
|
+
},
|
131
|
+
}
|
132
|
+
|
133
|
+
File.write(File.join(appiconset_path, 'Contents.json'), JSON.generate(contents_json))
|
134
|
+
|
135
|
+
progress&.call(total - 1, total)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2019 Fueled Digital Media, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require 'json'
|
18
|
+
|
19
|
+
module IOSIconGenerator
|
20
|
+
module Helpers
|
21
|
+
def self.images_sets(types)
|
22
|
+
types.flat_map do |type|
|
23
|
+
contents_path = File.expand_path(File.join(File.dirname(__FILE__), "../../../vendor/Contents-#{type}.json"))
|
24
|
+
raise "Unknown type #{type}" unless File.exist?(contents_path)
|
25
|
+
|
26
|
+
contents_json = JSON.parse(File.read(contents_path))
|
27
|
+
contents_json['images']
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.type_incompatible?(lhs, rhs)
|
32
|
+
(lhs == :imessage && rhs != :imessage || lhs != :imessage && rhs == :imessage)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2019 Fueled Digital Media, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
require 'json'
|
18
|
+
require 'base64'
|
19
|
+
require 'fileutils'
|
20
|
+
|
21
|
+
module IOSIconGenerator
|
22
|
+
module Helpers
|
23
|
+
def self.mask_icon(
|
24
|
+
appiconset_path:,
|
25
|
+
output_folder:,
|
26
|
+
mask: {
|
27
|
+
background_color: '#FFFFFF',
|
28
|
+
stroke_color: '#000000',
|
29
|
+
stroke_width_offset: 0.1,
|
30
|
+
suffix: 'Beta',
|
31
|
+
symbol: 'b',
|
32
|
+
symbol_color: '#7F0000',
|
33
|
+
font: 'Helvetica',
|
34
|
+
x_size_ratio: 0.5478515625,
|
35
|
+
y_size_ratio: 0.5478515625,
|
36
|
+
size_offset: 0.0,
|
37
|
+
x_offset: 0.0,
|
38
|
+
y_offset: 0.0,
|
39
|
+
shape: 'triangle',
|
40
|
+
},
|
41
|
+
parallel_processes: nil,
|
42
|
+
progress: nil
|
43
|
+
)
|
44
|
+
extension = File.extname(appiconset_path)
|
45
|
+
output_folder = File.join(output_folder, "#{File.basename(appiconset_path, extension)}-#{mask[:suffix]}#{extension}")
|
46
|
+
|
47
|
+
FileUtils.mkdir_p(output_folder)
|
48
|
+
|
49
|
+
contents_path = File.join(appiconset_path, 'Contents.json')
|
50
|
+
raise "Contents.json file not found in #{appiconset_path}" unless File.exist?(contents_path)
|
51
|
+
|
52
|
+
json_content = JSON.parse(File.read(contents_path))
|
53
|
+
progress&.call(nil, json_content['images'].count)
|
54
|
+
Parallel.each(
|
55
|
+
json_content['images'],
|
56
|
+
in_processes: parallel_processes,
|
57
|
+
finish: lambda do |_item, i, _result|
|
58
|
+
progress&.call(i, json_content['images'].count)
|
59
|
+
end
|
60
|
+
) do |image|
|
61
|
+
width, height = /(\d+(?:\.\d)?)x(\d+(?:\.\d)?)/.match(image['size'])&.captures
|
62
|
+
scale, = /(\d+(?:\.\d)?)x/.match(image['scale'])&.captures
|
63
|
+
raise "Invalid size parameter in Contents.json: #{image['size']}" if width.nil? || height.nil? || scale.nil?
|
64
|
+
|
65
|
+
scale = scale.to_f
|
66
|
+
width = width.to_f * scale
|
67
|
+
height = height.to_f * scale
|
68
|
+
|
69
|
+
mask_size_width = width * mask[:x_size_ratio].to_f
|
70
|
+
mask_size_height = height * mask[:y_size_ratio].to_f
|
71
|
+
|
72
|
+
extension = File.extname(image['filename'])
|
73
|
+
icon_output = "#{File.basename(image['filename'], extension)}-#{mask[:suffix]}#{extension}"
|
74
|
+
icon_output_path = File.join(output_folder, icon_output)
|
75
|
+
|
76
|
+
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'}'"
|
77
|
+
draw_shape =
|
78
|
+
case mask[:shape]
|
79
|
+
when :triangle
|
80
|
+
"-draw \"polyline 0,#{mask_size_height} 0,#{height} #{width - mask_size_width},#{height}\""
|
81
|
+
when :square
|
82
|
+
"-draw \"rectangle -#{width},#{height * 2.0} #{mask_size_height},#{width - mask_size_width}\""
|
83
|
+
else
|
84
|
+
raise "Unknown mask shape: #{mask[:shape]}"
|
85
|
+
end
|
86
|
+
|
87
|
+
draw_symbol =
|
88
|
+
if mask[:file]
|
89
|
+
"\\( -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"
|
90
|
+
else
|
91
|
+
"-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]}'"
|
92
|
+
end
|
93
|
+
system("convert '#{File.join(appiconset_path, image['filename'])}' #{draw_shape_parameters} #{draw_shape} #{draw_symbol} '#{icon_output_path}'")
|
94
|
+
|
95
|
+
image['filename'] = icon_output
|
96
|
+
end
|
97
|
+
|
98
|
+
File.write(File.join(output_folder, 'Contents.json'), JSON.generate(json_content))
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2019 Fueled Digital Media, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module IOSIconGenerator
|
18
|
+
module Library
|
19
|
+
##
|
20
|
+
# Cross-platform way of finding an executable in the +$PATH+.
|
21
|
+
#
|
22
|
+
# From http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby
|
23
|
+
#
|
24
|
+
# == Parameters:
|
25
|
+
# +cmd+::
|
26
|
+
# The name of the command to search the path for.
|
27
|
+
#
|
28
|
+
# == Returns:
|
29
|
+
# The full path to the command if found, and +nil+ otherwise.
|
30
|
+
def self.which(cmd)
|
31
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
32
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
33
|
+
exts.each do |ext|
|
34
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
35
|
+
return exe if File.executable?(exe) && !File.directory?(exe)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2019 Fueled Digital Media, LLC
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module IOSIconGenerator
|
18
|
+
VERSION = '0.1'
|
19
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
{
|
2
|
+
"images" : [
|
3
|
+
{
|
4
|
+
"size" : "29x29",
|
5
|
+
"idiom" : "iphone",
|
6
|
+
"scale" : "2x"
|
7
|
+
},
|
8
|
+
{
|
9
|
+
"size" : "29x29",
|
10
|
+
"idiom" : "iphone",
|
11
|
+
"scale" : "3x"
|
12
|
+
},
|
13
|
+
{
|
14
|
+
"size" : "60x45",
|
15
|
+
"idiom" : "iphone",
|
16
|
+
"scale" : "2x"
|
17
|
+
},
|
18
|
+
{
|
19
|
+
"size" : "60x45",
|
20
|
+
"idiom" : "iphone",
|
21
|
+
"scale" : "3x"
|
22
|
+
},
|
23
|
+
{
|
24
|
+
"size" : "29x29",
|
25
|
+
"idiom" : "ipad",
|
26
|
+
"scale" : "2x"
|
27
|
+
},
|
28
|
+
{
|
29
|
+
"size" : "67x50",
|
30
|
+
"idiom" : "ipad",
|
31
|
+
"scale" : "2x"
|
32
|
+
},
|
33
|
+
{
|
34
|
+
"size" : "74x55",
|
35
|
+
"idiom" : "ipad",
|
36
|
+
"scale" : "2x"
|
37
|
+
},
|
38
|
+
{
|
39
|
+
"size" : "1024x1024",
|
40
|
+
"idiom" : "ios-marketing",
|
41
|
+
"scale" : "1x"
|
42
|
+
},
|
43
|
+
{
|
44
|
+
"size" : "27x20",
|
45
|
+
"idiom" : "universal",
|
46
|
+
"scale" : "2x",
|
47
|
+
"platform" : "ios"
|
48
|
+
},
|
49
|
+
{
|
50
|
+
"size" : "27x20",
|
51
|
+
"idiom" : "universal",
|
52
|
+
"scale" : "3x",
|
53
|
+
"platform" : "ios"
|
54
|
+
},
|
55
|
+
{
|
56
|
+
"size" : "32x24",
|
57
|
+
"idiom" : "universal",
|
58
|
+
"scale" : "2x",
|
59
|
+
"platform" : "ios"
|
60
|
+
},
|
61
|
+
{
|
62
|
+
"size" : "32x24",
|
63
|
+
"idiom" : "universal",
|
64
|
+
"scale" : "3x",
|
65
|
+
"platform" : "ios"
|
66
|
+
},
|
67
|
+
{
|
68
|
+
"size" : "1024x768",
|
69
|
+
"idiom" : "ios-marketing",
|
70
|
+
"scale" : "1x",
|
71
|
+
"platform" : "ios"
|
72
|
+
}
|
73
|
+
],
|
74
|
+
"info" : {
|
75
|
+
"version" : 1,
|
76
|
+
"author" : "xcode"
|
77
|
+
}
|
78
|
+
}
|