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.
- 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
|
+
}
|