ios_icon_generator 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ }