ios_icon_generator 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,18 @@
1
+ {
2
+ "images" : [
3
+ {
4
+ "size" : "60x60",
5
+ "idiom" : "car",
6
+ "scale" : "2x"
7
+ },
8
+ {
9
+ "size" : "60x60",
10
+ "idiom" : "car",
11
+ "scale" : "3x"
12
+ }
13
+ ],
14
+ "info" : {
15
+ "version" : 1,
16
+ "author" : "xcode"
17
+ }
18
+ }
@@ -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
+ }