diff --git a/.rubocop.yml b/.rubocop.yml index 4b77de2..ef9f391 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -102,4 +102,4 @@ Layout/EndAlignment: AutoCorrect: true Lint/BooleanSymbol: - Enabled: false \ No newline at end of file + Enabled: false diff --git a/.yardopts b/.yardopts index dfac2c2..94de164 100644 --- a/.yardopts +++ b/.yardopts @@ -1 +1,4 @@ --output-dir docs +--readme README.md +--no-private +lib/ios_icon_generator/version.rb lib/ios_icon_generator.rb lib/ios_icon_generator/helpers/**/*.rb \ No newline at end of file diff --git a/README.md b/README.md index 4d87197..974e4f1 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ For now, the CLI supports generating app icon sets for all supported platforms: - Mac - Carplay -> Please note that the Display P3 is not supported yet +> Please note that the Display P3 color space is not supported yet And supports generating sticker packs icons for iMessage apps. diff --git a/docs/IOSIconGenerator.html b/docs/IOSIconGenerator.html index 4d36a76..a08fc8b 100644 --- a/docs/IOSIconGenerator.html +++ b/docs/IOSIconGenerator.html @@ -78,8 +78,8 @@
Defined in:
-
lib/ios_icon_generator.rb,
- lib/ios_icon_generator/helpers/generate_icon.rb,
lib/ios_icon_generator/helpers/mask_icon.rb,
lib/ios_icon_generator/helpers/which.rb
+
lib/ios_icon_generator/version.rb,
+ 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
@@ -88,7 +88,20 @@

Overview

-

The IOSIconGenerator module

+

Copyright © 2019 Fueled Digital Media, LLC

+ +

Licensed under the Apache License, Version 2.0 (the “License”); you may not +use this file except in compliance with the License. You may obtain a copy +of the License at

+ +

www.apache.org/licenses/LICENSE-2.0

+ +

Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an “AS IS” BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License.

@@ -100,13 +113,40 @@

Overview

- Modules: CLI, Helpers, Library + Modules: Helpers

+ +

+ Constant Summary + collapse +

+ +
+ +
VERSION = +
+
+ +

The current version of the gem.

+ + +
+
+
+ + +
+
+
'0.1'
+ +
+ + @@ -118,7 +158,7 @@

Overview

diff --git a/docs/IOSIconGenerator/CLI.html b/docs/IOSIconGenerator/CLI.html index 379c984..f848607 100644 --- a/docs/IOSIconGenerator/CLI.html +++ b/docs/IOSIconGenerator/CLI.html @@ -116,7 +116,7 @@

Overview

diff --git a/docs/IOSIconGenerator/CLI/Commands.html b/docs/IOSIconGenerator/CLI/Commands.html index 0100e93..9b67d04 100644 --- a/docs/IOSIconGenerator/CLI/Commands.html +++ b/docs/IOSIconGenerator/CLI/Commands.html @@ -112,7 +112,7 @@

Overview

diff --git a/docs/IOSIconGenerator/Helpers.html b/docs/IOSIconGenerator/Helpers.html index b704167..7b3d953 100644 --- a/docs/IOSIconGenerator/Helpers.html +++ b/docs/IOSIconGenerator/Helpers.html @@ -79,7 +79,7 @@
Defined in:
lib/ios_icon_generator.rb,
- lib/ios_icon_generator/helpers/generate_icon.rb,
lib/ios_icon_generator/helpers/mask_icon.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
@@ -114,7 +114,7 @@

  • - .generate_icon(icon_path:, output_folder:, type:, parallel_processes: nil, progress: nil) ⇒ Object + .generate_icon(icon_path:, output_folder:, types:, parallel_processes: nil, generate_icon: nil, progress: nil) ⇒ String @@ -128,7 +128,9 @@

    -
    +
    +

    Generate an icon using the base icon provided.

    +

  • @@ -136,7 +138,7 @@

  • - .mask_icon(appiconset_path:, output_folder:, mask: { background_color: '#AD0000', suffix: 'Beta', symbol: 'b', font: 'Helvetica', x_size_ratio: 0.5478515625, y_size_ratio: 0.5478515625, size_offset: 0.0, x_offset: 0.0, y_offset: 0.0, }, parallel_processes: nil, progress: nil) ⇒ Object + .image_sets(types) ⇒ Array<Hash<String, String>> @@ -150,7 +152,82 @@

    -
    +
    +

    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.

    +
  • @@ -167,58 +244,177 @@

    Class Method Details

    - .generate_icon(icon_path:, output_folder:, type:, parallel_processes: nil, progress: nil) ⇒ Object + .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.

    + + +
    +
    +
    +

    Parameters:

    +
      + +
    • + + icon_path + + + (String, #read) + + + + — +
      +

      The path to the icon to use as the base icon. If specified, it must point +to a valid pdf file (with a .pdf extension), with a resolution over +1024x1024. If not specified, generate_icon must be specified.

      +
      + +
    • + +
    • + + output_folder + + + (String, #read) + + + + — +
      +

      The folder to create the app icon set in.

      +
      + +
    • + +
    • + + types + + + (Array<Symbol>, #read) + + + + — +
      +

      The types to generate the sets of images for. Each type must be one of +:iphone, :ipad, :watch, +mac or carplay, or it can be an array of just +:imessage.

      +
      + +
    • + +
    • + + parallel_processes + + + (Symbol, #read) + + + + — +
      +

      The number of processes to use when generating the icons. nil +means it'll use as many processes as they are cores on the machine. +0 will disables spawning any processes.

      +
      + +
    • + +
    • + + generate_icon + + + (Lambda(base_path [String], target_path [String], width [Float], height [Float]), #read) + + + + — +
      +

      The lambda that actually generates the icon. If none is specified, and +default one will be used. It should take four parameters:

      +
      • +

        base_path: The base path to the reference image to use to +generate the new icon. If icon_path is set to +nil, the base_path parameter will +nil as well.

        +
      • +

        target_path: The path to generate the icon at.

        +
      • +

        width: The width of the icon to generate.

        +
      • +

        height: The height of the icon to generate.

        +
      +
      + +
    • + +
    • + + progress + + + (Lambda(progress [Int], total [Int]), #read) + + + + — +
      +

      An optional progress block called when progress has been made generating +the icons. It should take two parameters:

      +
      • +

        progress: An integer indicating the current progress out of +total

        +
      • +

        total: An integer indicating the total progress

        +
      +
      + +
    • + +
    +

    Returns:

    +
      - +
    • + + + (String) + + + + — +
      +

      Return the path to the generated app icon set.

      +
      + +
    • + + + +
      @@ -361,50 +661,73 @@

      -

      +

      - .mask_icon(appiconset_path:, output_folder:, mask: { - background_color: '#AD0000', - suffix: 'Beta', - symbol: 'b', - font: 'Helvetica', - x_size_ratio: 0.5478515625, - y_size_ratio: 0.5478515625, - size_offset: 0.0, - x_offset: 0.0, - y_offset: 0.0, - }, parallel_processes: nil, progress: nil) ⇒ Object + .image_sets(types) ⇒ Array<Hash<String, String>> + + +

      +
      + +

      Get the image sets for the given types.

      + +
      +
      +
      +

      Parameters:

      +
        + +
      • + + types + + + (Symbol, #read) + + + + — +
        +

        The types to return the sets of image for. This method won't fail if +the types aren't compatible as defined by +type_incompatible?

        +
        + +
      • + +
      + +

      Returns:

      +
        + +
      • + + + (Array<Hash<String, String>>) + + + + — +
        +

        The sets of image for the given types. Each hash will at least contain a +size [String] key, that has the format +<width>x<height>

        +
        + +
      • -

       
       
      -9
      -10
      -11
      -12
      -13
      -14
      -15
      -16
      -17
      -18
      -19
      -20
      -21
      -22
      -23
      -24
      -25
      -26
      -27
      -28
      -29
      -30
      -31
      -32
      -33
      -34
      -35
      -36
      -37
      -38
      -39
      -40
      -41
      -42
      -43
      -44
      -45
      -46
      -47
      -48
       49
       50
       51
      @@ -264,25 +460,108 @@ 

      91 92 93 -94

      +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 9
      +      
      # 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)
       
      -def self.generate_icon(icon_path:, output_folder:, type:, parallel_processes: nil, progress: nil)
      -  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?
      +    raise 'The icon specified must be .pdf.' if File.extname(icon_path) != '.pdf'
       
      -  width, height = matches.captures
      -  raise 'Invalid pdf specified.' if width.nil? || height.nil?
      +    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?
       
      -  raise "The icon must at least be 1024x1024, it currently is #{width}x#{height}." unless width.to_i >= 1024 && height.to_i >= 1024
      +    width, height = matches.captures
      +    raise 'Invalid pdf specified.' if width.nil? || height.nil?
       
      -  appiconset_path = File.join(output_folder, "#{type == :imessage ? 'iMessage App Icon' : 'AppIcon'}.#{type == :imessage ? 'stickersiconset' : 'appiconset'}")
      +    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)
       
      -  generate_icon = lambda { |base_path, width, height|
      +  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',
      @@ -300,60 +579,81 @@ 

      '-crop', "#{width}x#{height}+0+0", '+repage', - File.join(appiconset_path, "App-Icon-#{width}x#{height}.png") + target_path ) } - case type - when :iphoneipad - smaller_sizes = [[20, 20], [29, 29], [40, 40], [58, 58], [60, 60], [76, 76], [80, 80], [87, 87], [120, 120], [152, 152], [167, 167], [180, 180]] - when :iphone - smaller_sizes = [[40, 40], [58, 58], [60, 60], [80, 80], [87, 87], [120, 120], [180, 180]] - when :ipad - smaller_sizes = [[20, 20], [29, 29], [40, 40], [58, 58], [76, 76], [80, 80], [152, 152], [167, 167]] - when :imessage - generate_icon.call(icon_path, 1024, 768) - smaller_sizes = [[58, 58], [87, 87], [120, 90], [180, 135], [134, 100], [148, 110], [54, 40], [81, 60], [64, 48], [96, 72]] + 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 - total = smaller_sizes.count + 3 - progress&.call(nil, total) + images_sets = Helpers.image_sets(types) - generate_icon.call(icon_path, 1024, 1024) - progress&.call(0, total) + 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) + 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 + 2, total) + progress&.call(i + 1, total) end ) do |width, height| - generate_icon.call(temp_icon_path, 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 - File.open("#{appiconset_path}/Contents.json", 'w') do |file| - case type - when :iphoneipad - file.write("{\n \"images\" : [\n {\n \"size\" : \"20x20\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-40x40.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"20x20\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-60x60.png\",\n \"scale\" : \"3x\"\n },\n {\n \"size\" : \"29x29\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-58x58.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"29x29\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-87x87.png\",\n \"scale\" : \"3x\"\n },\n {\n \"size\" : \"40x40\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-80x80.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"40x40\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-120x120.png\",\n \"scale\" : \"3x\"\n },\n {\n \"size\" : \"60x60\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-120x120.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"60x60\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-180x180.png\",\n \"scale\" : \"3x\"\n },\n {\n \"size\" : \"20x20\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-20x20.png\",\n \"scale\" : \"1x\"\n },\n {\n \"size\" : \"20x20\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-40x40.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"29x29\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-29x29.png\",\n \"scale\" : \"1x\"\n },\n {\n \"size\" : \"29x29\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-58x58.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"40x40\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-40x40.png\",\n \"scale\" : \"1x\"\n },\n {\n \"size\" : \"40x40\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-80x80.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"76x76\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-76x76.png\",\n \"scale\" : \"1x\"\n },\n {\n \"size\" : \"76x76\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-152x152.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"83.5x83.5\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-167x167.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"1024x1024\",\n \"idiom\" : \"ios-marketing\",\n \"filename\" : \"App-Icon-1024x1024.png\",\n \"scale\" : \"1x\"\n }\n ],\n \"info\" : {\n \"version\" : 1,\n \"author\" : \"xcode\"\n }\n}") # rubocop:disable Metrics/LineLength - when :iphone - file.write("{\n \"images\" : [\n {\n \"size\" : \"20x20\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-40x40.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"20x20\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-60x60.png\",\n \"scale\" : \"3x\"\n },\n {\n \"size\" : \"29x29\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-58x58.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"29x29\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-87x87.png\",\n \"scale\" : \"3x\"\n },\n {\n \"size\" : \"40x40\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-80x80.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"40x40\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-120x120.png\",\n \"scale\" : \"3x\"\n },\n {\n \"size\" : \"60x60\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-120x120.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"60x60\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-180x180.png\",\n \"scale\" : \"3x\"\n },\n {\n \"size\" : \"1024x1024\",\n \"idiom\" : \"ios-marketing\",\n \"filename\" : \"App-Icon-1024x1024.png\",\n \"scale\" : \"1x\"\n }\n ],\n \"info\" : {\n \"version\" : 1,\n \"author\" : \"xcode\"\n }\n}") # rubocop:disable Metrics/LineLength - when :ipad - file.write("{\n \"images\" : [\n {\n \"size\" : \"20x20\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-20x20.png\",\n \"scale\" : \"1x\"\n },\n {\n \"size\" : \"20x20\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-40x40.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"29x29\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-29x29.png\",\n \"scale\" : \"1x\"\n },\n {\n \"size\" : \"29x29\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-58x58.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"40x40\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-40x40.png\",\n \"scale\" : \"1x\"\n },\n {\n \"size\" : \"40x40\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-80x80.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"76x76\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-76x76.png\",\n \"scale\" : \"1x\"\n },\n {\n \"size\" : \"76x76\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-152x152.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"83.5x83.5\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-167x167.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"1024x1024\",\n \"idiom\" : \"ios-marketing\",\n \"filename\" : \"App-Icon-1024x1024.png\",\n \"scale\" : \"1x\"\n }\n ],\n \"info\" : {\n \"version\" : 1,\n \"author\" : \"xcode\"\n }\n}") # rubocop:disable Metrics/LineLength - when :imessage - file.write("{\n \"images\" : [\n {\n \"size\" : \"29x29\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-58x58.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"29x29\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-87x87.png\",\n \"scale\" : \"3x\"\n },\n {\n \"size\" : \"60x45\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-120x90.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"60x45\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"App-Icon-180x135.png\",\n \"scale\" : \"3x\"\n },\n {\n \"size\" : \"29x29\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-58x58.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"67x50\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-134x100.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"74x55\",\n \"idiom\" : \"ipad\",\n \"filename\" : \"App-Icon-148x110.png\",\n \"scale\" : \"2x\"\n },\n {\n \"size\" : \"1024x1024\",\n \"idiom\" : \"ios-marketing\",\n \"filename\" : \"App-Icon-1024x1024.png\",\n \"scale\" : \"1x\"\n },\n {\n \"size\" : \"27x20\",\n \"idiom\" : \"universal\",\n \"filename\" : \"App-Icon-54x40.png\",\n \"scale\" : \"2x\",\n \"platform\" : \"ios\"\n },\n {\n \"size\" : \"27x20\",\n \"idiom\" : \"universal\",\n \"filename\" : \"App-Icon-81x60.png\",\n \"scale\" : \"3x\",\n \"platform\" : \"ios\"\n },\n {\n \"size\" : \"32x24\",\n \"idiom\" : \"universal\",\n \"filename\" : \"App-Icon-64x48.png\",\n \"scale\" : \"2x\",\n \"platform\" : \"ios\"\n },\n {\n \"size\" : \"32x24\",\n \"idiom\" : \"universal\",\n \"filename\" : \"App-Icon-96x72.png\",\n \"scale\" : \"3x\",\n \"platform\" : \"ios\"\n },\n {\n \"size\" : \"1024x768\",\n \"idiom\" : \"ios-marketing\",\n \"filename\" : \"App-Icon-1024x768.png\",\n \"scale\" : \"1x\",\n \"platform\" : \"ios\"\n }\n ],\n \"info\" : {\n \"version\" : 1,\n \"author\" : \"xcode\"\n }\n}") # rubocop:disable Metrics/LineLength - end - 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

      + + +
      + + +
       
       
      -9
      -10
      -11
      -12
      -13
      -14
      -15
      -16
      -17
      -18
      -19
      -20
      -21
      -22
      -23
      -24
      -25
      -26
      -27
      -28
       29
       30
       31
      @@ -413,25 +736,225 @@ 

      34 35 36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 +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.expand_path(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.

    + + +
    +
    +
    +

    Parameters:

    +
      + +
    • + + appiconset_path + + + (String, #read) + + + + — +
      +

      The path of the original app icon set to use to generate the new one.

      +
      + +
    • + +
    • + + output_folder + + + (String, #read) + + + + — +
      +

      The folder to create the new app icon set in.

      +
      + +
    • + +
    • + + mask + + + (Hash<String, Object>, #read) + + + + — +
      +

      A hash representing parameters for creating the mask. The Hash may contain +the following values:

      +
      • +

        background_color: The background color to use when generating +the mask

        +
      • +

        stroke_color: The stroke color to use when generating the +mask. Used for the outline of the mask.

        +
      • +

        stroke_width_offset: The stroke width of the mask, offset to +the image's minimum dimension (width or height). 1.0 means the stroke +will have the full width/height of the image

        +
      • +

        suffix: The suffix to use when generating the new mask

        +
      • +

        file: The file to use when generating the new mask. This file +should be an image, and it will be overlayed over the background.

        +
      • +

        symbol: The symbol to use when generating the new mask

        +
      • +

        symbol_color: The color to use for the symbol

        +
      • +

        font: The font to use for the symbol

        +
      • +

        x_size_ratio: The size ratio (of the width of the image) to +use when generating the mask. 1.0 means the full width, 0.5 means +half-width.

        +
      • +

        y_size_ratio: The size ratio (of the height of the image) to +use when generating the mask. 1.0 means the full height, 0.5 means +half-height.

        +
      • +

        size_offset: The size ratio (of the width and height) to use +when generating the symbol or file. 1.0 means the full width and height, +0.5 means half-width and half-height.

        +
      • +

        x_offset: The X offset (of the width of the image) to use when +generating the symbol or file. 1.0 means the full width, 0.5 means +half-width.

        +
      • +

        y_offset: The Y offset (of the width of the image) to use when +generating the symbol or file. 1.0 means the full height, 0.5 means +half-height.

        +
      • +

        shape: The shape to use when generating the mask. Can be +either :triangle or :square.

        +
      +
      + +
    • + +
    • + + parallel_processes + + + (Symbol, #read) + + + + — +
      +

      The number of processes to use when generating the icons. nil +means it'll use as many processes as they are cores on the machine. +0 will disables spawning any processes.

      +
      + +
    • + +
    • + + progress + + + (Lambda(progress [Int], total [Int]), #read) + + + + — +
      +

      An optional progress block called when progress has been made generating +the icons. It should take two parameters:

      +
      • +

        progress: An integer indicating the current progress out of +total

        +
      • +

        total: An integer indicating the total progress

        +
      +
      + +
    • + +
    + +

    Returns:

    +
      + +
    • + + + (String) + + + + — +
      +

      Return the path to the generated app icon set.

      +
      + +
    • + +
    + +
    + + + +
    +
    +
    +
     56
     57
     58
    @@ -444,24 +967,95 @@ 

    65 66 67 -68

    +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 9
    +      
    # File 'lib/ios_icon_generator/helpers/mask_icon.rb', line 56
     
     def self.mask_icon(
       appiconset_path:,
       output_folder:,
       mask: {
    -    background_color: '#AD0000',
    +    background_color: '#FFFFFF',
    +    stroke_color: '#000000',
    +    stroke_width_offset: 0.1,
         suffix: 'Beta',
         symbol: 'b',
    +    symbol_color: '#7F0000',
         font: 'Helvetica',
    -    x_size_ratio: 0.5478515625,
    -    y_size_ratio: 0.5478515625,
    +    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
    @@ -474,13 +1068,14 @@ 

    contents_path = File.join(appiconset_path, 'Contents.json') raise "Contents.json file not found in #{appiconset_path}" unless File.exist?(contents_path) - images = JSON.parse(File.read(contents_path))['images'] - progress&.call(nil, images.count) + json_content = JSON.parse(File.read(contents_path)) + progress&.call(nil, json_content['images'].count) Parallel.each( - images, + json_content['images'], in_processes: parallel_processes, - finish: lambda do |_item, i, _result| - progress&.call(i, images.count) + 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 @@ -498,16 +1093,224 @@

    icon_output = "#{File.basename(image['filename'], extension)}-#{mask[:suffix]}#{extension}" icon_output_path = File.join(output_folder, icon_output) - command = + 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] - "convert '#{File.join(appiconset_path, image['filename'])}' -strokewidth 0 -fill '#{mask[:background_color]}' -draw \"polyline 0,#{height - mask_size_height} 0,#{height} #{mask_size_width},#{height}\" \\( -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 '#{icon_output_path}'" # rubocop:disable Metrics/LineLength - else - "convert '#{File.join(appiconset_path, image['filename'])}' -strokewidth 0 -fill '#{mask[:background_color]}' -draw \"polyline 0,#{height - mask_size_height} 0,#{height} #{mask_size_width},#{height}\" -fill '#FFFFFF' -font '#{mask[:font]}' -pointsize #{height * mask[:size_offset] * 2.0} -annotate +#{width * mask[:x_offset]}+#{height - height * mask[:y_offset]} '#{mask[:symbol]}' '#{icon_output_path}'" # rubocop:disable Metrics/LineLength - end - system(command) + "\\( -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)) - image['filename'] = icon_output + output_folder +end

    +
    +
    + +
    +

    + + .type_incompatible?(lhs, rhs) ⇒ Boolean + + + + + +

    +
    + +

    Check if the given types are compatible (if they can be used in the same +set)

    + + +
    +
    +
    +

    Parameters:

    +
      + +
    • + + lhs + + + (Symbol, #read) + + + + — +
      +

      The first type to check against the second type.

      +
      + +
    • + +
    • + + rhs + + + (Symbol, #read) + + + + — +
      +

      The second type to check against the first type.

      +
      + +
    • + +
    + +

    Returns:

    +
      + +
    • + + + (Boolean) + + + + — +
      +

      true if the given are compatible together, false +otherwise

      +
      + +
    • + +
    + +
    + + + + +
    +
    +
    +
    +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

    + + +
    +
    +
    +

    Parameters:

    +
      + +
    • + + cmd + + + (String) + + + + — +
      +

      The name of the command to search the path for.

      +
      + +
    • + +
    + +

    Returns:

    +
      + +
    • + + + (String) + + + + — +
      +

      The full path to the command if found, and nil otherwise.

      +
      + +
    • + +
    + +
    + + + @@ -519,7 +1322,7 @@

    diff --git a/docs/IOSIconGenerator/Library.html b/docs/IOSIconGenerator/Library.html index 1fc65a1..707f420 100644 --- a/docs/IOSIconGenerator/Library.html +++ b/docs/IOSIconGenerator/Library.html @@ -78,7 +78,7 @@
    Defined in:
    -
    lib/ios_icon_generator/helpers/which.rb
    +
    ios_icon_generator/helpers/which.rb
    @@ -147,22 +147,50 @@

    From stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby

    - -

    Parameters:

    -
    cmd -
    -

    The name of the command to search the path for.

    -
    - -

    Returns:

    - -

    The full path to the command if found, and nil otherwise.

    +

    Parameters:

    +
      + +
    • + + cmd + + + (String) + + + + — +
      +

      The name of the command to search the path for.

      +
      + +
    • + +
    + +

    Returns:

    +
      + +
    • + + + + + + + +
      +

      The full path to the command if found, and nil otherwise.

      +
      + +
    • +
    +
    +
    +
    +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
    @@ -170,19 +198,19 @@

    Returns:

     
     
    -16
    -17
    -18
    -19
    -20
    -21
    -22
    -23
    -24
    -25
    +27 +28 +29 +30 +31 +32 +33 +34 +35 +36
    -
    # File 'lib/ios_icon_generator/helpers/which.rb', line 16
    +      
    # File 'ios_icon_generator/helpers/which.rb', line 27
     
     def self.which(cmd)
       exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
    @@ -204,7 +232,7 @@ 

    Returns:

    diff --git a/docs/_index.html b/docs/_index.html index 8d61020..e4e1451 100644 --- a/docs/_index.html +++ b/docs/_index.html @@ -76,28 +76,6 @@

    Namespace Listing A-Z

    -
      -
    • C
    • -
        - -
      • - CLI - - (IOSIconGenerator) - -
      • - -
      • - Commands - - (IOSIconGenerator::CLI) - -
      • - -
      -
    - -
    • H
      • @@ -125,21 +103,6 @@

        Namespace Listing A-Z

    - -
      -
    • L
    • -
        - -
      • - Library - - (IOSIconGenerator) - -
      • - -
      -
    -
    @@ -149,7 +112,7 @@

    Namespace Listing A-Z

    diff --git a/docs/class_list.html b/docs/class_list.html index 784a927..e3c079e 100644 --- a/docs/class_list.html +++ b/docs/class_list.html @@ -43,7 +43,7 @@

    Class List

    diff --git a/docs/file.README.html b/docs/file.README.html index bc54c25..39f40fa 100644 --- a/docs/file.README.html +++ b/docs/file.README.html @@ -57,13 +57,156 @@
    -

    ios-icon-generator

    +

    iOS Icon Generator

    -

    A terminal tool that allows to generate an icon based from a 1024x1024 PDF file.

    +

    Build Status +Documentation +LICENSE

    + +

    Usage

    + +

    The CLI currently supports 2 commands:

    + +
      +
    • generate: generates an iOS App Icon Set from a base PDF
    • +
    • mask: generates a new iOS App Icon Sets from a base one, adding a mask at its bottom left which can be customized.
    • +
    • stub: generates a default iOS App Icon Sets, using a background color, a text and its color.
    • +
    + +

    Installation

    + +

    iOS Icon Generator is built using Ruby, and can be installed using its default local installation in your terminal via:

    + +
    sudo gem install ios_icon_generator
    +
    + +

    If you wish not to use sudo permissions to RubyGems, you can follow the instructions of the CocoaPods guys here, by replacing all cocoapods with ios_icon_generator in the shell commands.

    + +

    Generate App Icon Sets

    + +

    For now, the CLI supports generating app icon sets for all supported platforms:

    + +
      +
    • iPhone
    • +
    • iPad
    • +
    • Apple Watch
    • +
    • Mac
    • +
    • Carplay
    • +
    + +
    +

    Please note that the Display P3 color space is not supported yet

    +
    + +

    And supports generating sticker packs icons for iMessage apps.

    + +

    You can specify iPhone and iPad at the same time, however iMessage must be specified on its own.

    + +

    The base syntax of the command is:

    + +
    icongen generate <PDF Base Icon> [XCAsset Folder] --type=<type>
    +
    + +

    If the XCAsset Folder is not specified, then the current working is assumed to be the output folder. +The type, if not specified, defaults to iphone.

    + +

    For example, to generate icons for iPhones and iPads, and output the images set in Images.xcassets, run (assuming the path Icon PDF file is also in the current directory and is called Icon.pdf):

    + +
    icongen generate Icon.pdf Images.xcassets --type=iphone,ipad
    +
    + +

    There are more options to icongen generate which you can discover by running icongen generate --help.

    + +

    Masking Existing App Icon Sets

    + +

    The application also supports "masking" app icons, adding a small mask at the bottom-left corner of the icons, allowing for example to convert the following icons: +Before Example +Into this: +Before Example

    + +

    The command used in the example was (it assumes that AppIcon.appiconset is the original App Icon Set and is in the current working directory):

    + +
    icongen mask AppIcon.appiconset \
    +    --mask-shape=square \
    +    --x-size-ratio=0.30 \
    +    --y-size-ratio=0.30 \
    +    --size-offset=0.11 \
    +    --x-offset=0.1 \
    +    --y-offset=0.1 \
    +    --background-color='#A36AE9' \
    +    --stroke-width-offset=0 \
    +    --font=Symbol \
    +    --symbol-color='#FFFFFF'
    +
    + +

    For now, the command only supports adding a mask into the bottom-left corner, but allowing to add it anywhere is definitely a feature we want.

    + +

    The command supports a lot of customization through options, and you can find more information about each option by typing icongen mask --help

    + +

    Creating temporary icons

    + +

    The application also supports creating "temporary" icons, that you would use during development if you don't have an icon yet (for example at project creation). For example, here are some icons created using various commands:

    + +

    Icons Example

    + +

    Here's the command that were run to get each icons above:

    + +
    #1
    +icongen stub \
    +    WS \
    +    --background-color='#A36AE9' \
    +    --stroke-width-offset=0 \
    +    --y-offset=-0.04 \
    +    --size-offset=0.5 \
    +    --symbol-color='#FFFFFF'
    +
    + +
    #2
    +icongen stub \
    +    W \
    +    --background-color='#3A7D31' \
    +    --font=Symbol \
    +    --stroke-width-offset=0.01 \
    +    --y-offset=-0.07 \
    +    --size-offset=0.5 \
    +    --symbol-color='#C9C9C9'
    +
    + +
    #3
    +icongen stub \
    +    A \
    +    --background-color='#000000' \
    +    --stroke-width-offset=0.01 \
    +    --stroke-color='#FFFFFF' \
    +    --font='Arial' \
    +    --size-offset=0.5 \
    +    --symbol-color='#000000'
    +
    + +

    The command supports a lot of customization through options, and you can find more information about each option by typing icongen stub --help.

    + +

    Development

    + +

    Set Up

    + +

    To set you up for developing on driveshaft, first make sure the repository is cloned and you're in its root folder, and then run the following:

    + +
    echo "BUNDLE_GEMFILE=\"`pwd`/Gemfile\" bundler exec ruby \"`pwd`/bin/icongen\" \"\${@---help}\"" > /usr/local/bin/icongen-dev
    +chmod +x /usr/local/bin/icongen-dev
    +
    + +

    All you'll be all set! +To run the development version of icongen from anywhere, just run icongen-dev rather than icongen.
    +You can then proceed on developing locally on the ios-icon-generator repository, and create a PR whenever a new piece of code is ready to be reviewed and integrated!

    + +

    To run the test suite, you may run the following command from the root folder:

    + +
    bundler exec ruby bin/rspec -I .
    +
    diff --git a/docs/index.html b/docs/index.html index 72b8798..1513e8d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -57,13 +57,156 @@
    -

    ios-icon-generator

    +

    iOS Icon Generator

    -

    A terminal tool that allows to generate an icon based from a 1024x1024 PDF file.

    +

    Build Status +Documentation +LICENSE

    + +

    Usage

    + +

    The CLI currently supports 2 commands:

    + +
      +
    • generate: generates an iOS App Icon Set from a base PDF
    • +
    • mask: generates a new iOS App Icon Sets from a base one, adding a mask at its bottom left which can be customized.
    • +
    • stub: generates a default iOS App Icon Sets, using a background color, a text and its color.
    • +
    + +

    Installation

    + +

    iOS Icon Generator is built using Ruby, and can be installed using its default local installation in your terminal via:

    + +
    sudo gem install ios_icon_generator
    +
    + +

    If you wish not to use sudo permissions to RubyGems, you can follow the instructions of the CocoaPods guys here, by replacing all cocoapods with ios_icon_generator in the shell commands.

    + +

    Generate App Icon Sets

    + +

    For now, the CLI supports generating app icon sets for all supported platforms:

    + +
      +
    • iPhone
    • +
    • iPad
    • +
    • Apple Watch
    • +
    • Mac
    • +
    • Carplay
    • +
    + +
    +

    Please note that the Display P3 color space is not supported yet

    +
    + +

    And supports generating sticker packs icons for iMessage apps.

    + +

    You can specify iPhone and iPad at the same time, however iMessage must be specified on its own.

    + +

    The base syntax of the command is:

    + +
    icongen generate <PDF Base Icon> [XCAsset Folder] --type=<type>
    +
    + +

    If the XCAsset Folder is not specified, then the current working is assumed to be the output folder. +The type, if not specified, defaults to iphone.

    + +

    For example, to generate icons for iPhones and iPads, and output the images set in Images.xcassets, run (assuming the path Icon PDF file is also in the current directory and is called Icon.pdf):

    + +
    icongen generate Icon.pdf Images.xcassets --type=iphone,ipad
    +
    + +

    There are more options to icongen generate which you can discover by running icongen generate --help.

    + +

    Masking Existing App Icon Sets

    + +

    The application also supports "masking" app icons, adding a small mask at the bottom-left corner of the icons, allowing for example to convert the following icons: +Before Example +Into this: +Before Example

    + +

    The command used in the example was (it assumes that AppIcon.appiconset is the original App Icon Set and is in the current working directory):

    + +
    icongen mask AppIcon.appiconset \
    +    --mask-shape=square \
    +    --x-size-ratio=0.30 \
    +    --y-size-ratio=0.30 \
    +    --size-offset=0.11 \
    +    --x-offset=0.1 \
    +    --y-offset=0.1 \
    +    --background-color='#A36AE9' \
    +    --stroke-width-offset=0 \
    +    --font=Symbol \
    +    --symbol-color='#FFFFFF'
    +
    + +

    For now, the command only supports adding a mask into the bottom-left corner, but allowing to add it anywhere is definitely a feature we want.

    + +

    The command supports a lot of customization through options, and you can find more information about each option by typing icongen mask --help

    + +

    Creating temporary icons

    + +

    The application also supports creating "temporary" icons, that you would use during development if you don't have an icon yet (for example at project creation). For example, here are some icons created using various commands:

    + +

    Icons Example

    + +

    Here's the command that were run to get each icons above:

    + +
    #1
    +icongen stub \
    +    WS \
    +    --background-color='#A36AE9' \
    +    --stroke-width-offset=0 \
    +    --y-offset=-0.04 \
    +    --size-offset=0.5 \
    +    --symbol-color='#FFFFFF'
    +
    + +
    #2
    +icongen stub \
    +    W \
    +    --background-color='#3A7D31' \
    +    --font=Symbol \
    +    --stroke-width-offset=0.01 \
    +    --y-offset=-0.07 \
    +    --size-offset=0.5 \
    +    --symbol-color='#C9C9C9'
    +
    + +
    #3
    +icongen stub \
    +    A \
    +    --background-color='#000000' \
    +    --stroke-width-offset=0.01 \
    +    --stroke-color='#FFFFFF' \
    +    --font='Arial' \
    +    --size-offset=0.5 \
    +    --symbol-color='#000000'
    +
    + +

    The command supports a lot of customization through options, and you can find more information about each option by typing icongen stub --help.

    + +

    Development

    + +

    Set Up

    + +

    To set you up for developing on driveshaft, first make sure the repository is cloned and you're in its root folder, and then run the following:

    + +
    echo "BUNDLE_GEMFILE=\"`pwd`/Gemfile\" bundler exec ruby \"`pwd`/bin/icongen\" \"\${@---help}\"" > /usr/local/bin/icongen-dev
    +chmod +x /usr/local/bin/icongen-dev
    +
    + +

    All you'll be all set! +To run the development version of icongen from anywhere, just run icongen-dev rather than icongen.
    +You can then proceed on developing locally on the ios-icon-generator repository, and create a PR whenever a new piece of code is ready to be reviewed and integrated!

    + +

    To run the test suite, you may run the following command from the root folder:

    + +
    bundler exec ruby bin/rspec -I .
    +
    diff --git a/docs/method_list.html b/docs/method_list.html index 2a77b7c..68b1d1a 100644 --- a/docs/method_list.html +++ b/docs/method_list.html @@ -53,6 +53,14 @@

    Method List

  • +
    + image_sets + IOSIconGenerator::Helpers +
    +
  • + + +
  • mask_icon IOSIconGenerator::Helpers @@ -60,10 +68,18 @@

    Method List

  • +
  • +
    + type_incompatible? + IOSIconGenerator::Helpers +
    +
  • + +
  • - which - IOSIconGenerator::Library + which + IOSIconGenerator::Helpers
  • diff --git a/docs/top-level-namespace.html b/docs/top-level-namespace.html index a6a39e4..2d6d7db 100644 --- a/docs/top-level-namespace.html +++ b/docs/top-level-namespace.html @@ -100,7 +100,7 @@

    Defined Under Namespace

    diff --git a/ios_icon_generator.gemspec b/ios_icon_generator.gemspec index ffb1e9a..7207718 100644 --- a/ios_icon_generator.gemspec +++ b/ios_icon_generator.gemspec @@ -13,11 +13,13 @@ Gem::Specification.new do |spec| spec.summary = 'Generates icons based and apply masks to them easily.' spec.homepage = 'https://github.com/Fueled/ios-icon-generator' - spec.files = Dir['lib/**/*.rb'] + Dir['vendor/**/*.json'] + %w[bin/icongen LICENSE README.md] + spec.files = Dir['lib/**/*.rb'] + Dir['vendor/**/*.json'] + %w[bin/icongen LICENSE README.md .yardopts] spec.license = 'Apache-2.0' spec.executables = %w[icongen] spec.require_paths = ['lib'] + spec.metadata['yard.run'] = 'yri' + spec.add_runtime_dependency 'colored2' spec.add_runtime_dependency 'hanami-cli' spec.add_runtime_dependency 'parallel' diff --git a/lib/.yardoc/checksums b/lib/.yardoc/checksums new file mode 100644 index 0000000..8381030 --- /dev/null +++ b/lib/.yardoc/checksums @@ -0,0 +1,6 @@ +ios_icon_generator/version.rb ce7d82f0b1be30d9858def51a225d6dafb93d75e +ios_icon_generator.rb 45ea6b19c1aa3874baa79fa4082997d0b7426d5d +ios_icon_generator/helpers/generate_icon.rb a63501ce864209c2ca2b7c011f54995d613113eb +ios_icon_generator/helpers/image_sets_definition.rb e723c7bfc63acf388e22c6356bee903c4ec76fe6 +ios_icon_generator/helpers/mask_icon.rb 439c9ec9ce9b5d41fce8fa067b2e2b36670ad284 +ios_icon_generator/helpers/which.rb 8770c89d2fe92c51b54f3d3499cff7f93b114381 diff --git a/lib/.yardoc/complete b/lib/.yardoc/complete new file mode 100644 index 0000000..e69de29 diff --git a/lib/.yardoc/object_types b/lib/.yardoc/object_types new file mode 100644 index 0000000..826dcee Binary files /dev/null and b/lib/.yardoc/object_types differ diff --git a/lib/.yardoc/objects/root.dat b/lib/.yardoc/objects/root.dat new file mode 100644 index 0000000..ae1c868 Binary files /dev/null and b/lib/.yardoc/objects/root.dat differ diff --git a/lib/.yardoc/proxy_types b/lib/.yardoc/proxy_types new file mode 100644 index 0000000..beefda1 Binary files /dev/null and b/lib/.yardoc/proxy_types differ diff --git a/lib/ios_icon_generator.rb b/lib/ios_icon_generator.rb index aecc069..c05729e 100644 --- a/lib/ios_icon_generator.rb +++ b/lib/ios_icon_generator.rb @@ -21,8 +21,10 @@ # The IOSIconGenerator module module IOSIconGenerator + # @private # :nodoc: module CLI + # @private # :nodoc: module Commands extend Hanami::CLI::Registry @@ -38,3 +40,4 @@ module Helpers require 'ios_icon_generator/cli/commands/generate' require 'ios_icon_generator/cli/commands/mask' require 'ios_icon_generator/cli/commands/stub' +require 'ios_icon_generator/cli/commands/version' diff --git a/lib/ios_icon_generator/cli/commands/generate.rb b/lib/ios_icon_generator/cli/commands/generate.rb index 3aaac76..d312a1d 100644 --- a/lib/ios_icon_generator/cli/commands/generate.rb +++ b/lib/ios_icon_generator/cli/commands/generate.rb @@ -33,7 +33,7 @@ class Generate < Hanami::CLI::Command option :parallel_processes, type: :integer, default: -1, desc: 'Number of processes to use to process the files. Defaults to -1, meaning the number of cores the machine. \ Set to 0 to disable parallel processing.' def call(icon_path:, xcasset_folder:, type:, **options) - raise "#{'ImageMagick'.blue.bold} is required. It can be installed via #{'homebrew'.bold.underlined} using #{'brew install imagemagick'.blue.bold.underlined}" unless Library.which('magick') + raise "#{'ImageMagick'.blue.bold} is required. It can be installed via #{'homebrew'.bold.underlined} using #{'brew install imagemagick'.blue.bold.underlined}" unless Helpers.which('magick') types = type.map(&:to_sym) diff --git a/lib/ios_icon_generator/cli/commands/mask.rb b/lib/ios_icon_generator/cli/commands/mask.rb index 3db1c3f..f2bc963 100644 --- a/lib/ios_icon_generator/cli/commands/mask.rb +++ b/lib/ios_icon_generator/cli/commands/mask.rb @@ -48,7 +48,7 @@ class Icon < Hanami::CLI::Command option :parallel_processes, type: :integer, default: -1, desc: 'Number of processes to use to process the files. Defaults to -1, meaning the number of cores the machine. \ Set to 0 to disable parallel processing.' def call(appiconset_path:, output_path:, **options) - raise "#{'ImageMagick'.blue.bold} is required. It can be installed via #{'homebrew'.bold.underlined} using #{'brew install imagemagick'.blue.bold.underlined}" unless Library.which('magick') + raise "#{'ImageMagick'.blue.bold} is required. It can be installed via #{'homebrew'.bold.underlined} using #{'brew install imagemagick'.blue.bold.underlined}" unless Helpers.which('magick') raise 'There is no App icon set at the path specified.' unless Dir.exist?(appiconset_path) diff --git a/lib/ios_icon_generator/cli/commands/stub.rb b/lib/ios_icon_generator/cli/commands/stub.rb index ecd6185..fe2ca44 100644 --- a/lib/ios_icon_generator/cli/commands/stub.rb +++ b/lib/ios_icon_generator/cli/commands/stub.rb @@ -42,7 +42,7 @@ class Stub < Hanami::CLI::Command option :parallel_processes, type: :integer, default: -1, desc: 'Number of processes to use to process the files. Defaults to -1, meaning the number of cores the machine. \ Set to 0 to disable parallel processing.' def call(text:, xcasset_folder:, type:, **options) - raise "#{'ImageMagick'.blue.bold} is required. It can be installed via #{'homebrew'.bold.underlined} using #{'brew install imagemagick'.blue.bold.underlined}" unless Library.which('magick') + raise "#{'ImageMagick'.blue.bold} is required. It can be installed via #{'homebrew'.bold.underlined} using #{'brew install imagemagick'.blue.bold.underlined}" unless Helpers.which('magick') types = type.map(&:to_sym) diff --git a/lib/ios_icon_generator/helpers/generate_icon.rb b/lib/ios_icon_generator/helpers/generate_icon.rb index 20c7060..618c711 100644 --- a/lib/ios_icon_generator/helpers/generate_icon.rb +++ b/lib/ios_icon_generator/helpers/generate_icon.rb @@ -16,23 +16,51 @@ require 'json' require 'fileutils' -require 'ios_icon_generator/helpers/images_sets_definition' +require 'ios_icon_generator/helpers/image_sets_definition' module IOSIconGenerator module Helpers + ## + # 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. + # + # @param [String, #read] icon_path The path to the icon to use as the base icon. + # If specified, it must point to a valid pdf file (with a .pdf extension), with a resolution over 1024x1024. + # If not specified, +generate_icon+ must be specified. + # @param [String, #read] output_folder The folder to create the app icon set in. + # @param [Array, #read] types The types to generate the sets of images for. Each type must be one of +:iphone+, +:ipad+, +:watch+, +mac+ or +carplay+, or it can be an array of just +:imessage+. + # @param [Symbol, #read] parallel_processes The number of processes to use when generating the icons. + # +nil+ means it'll use as many processes as they are cores on the machine. + # +0+ will disables spawning any processes. + # @param [Lambda(base_path [String], target_path [String], width [Float], height [Float]), #read] generate_icon The lambda that actually generates the icon. + # If none is specified, and default one will be used. + # It should take four parameters: + # - +base_path+: The base path to the reference image to use to generate the new icon. If +icon_path+ is set to +nil+, the +base_path+ parameter will +nil+ as well. + # - +target_path+: The path to generate the icon at. + # - +width+: The width of the icon to generate. + # - +height+: The height of the icon to generate. + # @param [Lambda(progress [Int], total [Int]), #read] progress An optional progress block called when progress has been made generating the icons. + # It should take two parameters: + # - +progress+: An integer indicating the current progress out of +total+ + # - +total+: An integer indicating the total progress + # + # @return [String] Return the path to the generated app icon set. def self.generate_icon(icon_path:, output_folder:, types:, parallel_processes: nil, generate_icon: nil, progress: nil) if icon_path - matches = /(\d+)x(\d+)/.match(`magick identify "#{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'}") @@ -70,7 +98,7 @@ def self.generate_icon(icon_path:, output_folder:, types:, parallel_processes: n end end - images_sets = Helpers.images_sets(types) + images_sets = Helpers.image_sets(types) smaller_sizes = [] images_sets.each do |image| @@ -133,6 +161,8 @@ def self.generate_icon(icon_path:, output_folder:, types:, parallel_processes: n File.write(File.join(appiconset_path, 'Contents.json'), JSON.pretty_generate(contents_json)) progress&.call(total - 1, total) + + appiconset_path end end end diff --git a/lib/ios_icon_generator/helpers/images_sets_definition.rb b/lib/ios_icon_generator/helpers/image_sets_definition.rb similarity index 54% rename from lib/ios_icon_generator/helpers/images_sets_definition.rb rename to lib/ios_icon_generator/helpers/image_sets_definition.rb index 3770bc1..31c486b 100644 --- a/lib/ios_icon_generator/helpers/images_sets_definition.rb +++ b/lib/ios_icon_generator/helpers/image_sets_definition.rb @@ -18,7 +18,15 @@ module IOSIconGenerator module Helpers - def self.images_sets(types) + ## + # Get the image sets for the given types. + # + # @param [Symbol, #read] types The types to return the sets of image for. + # This method won't fail if the types aren't compatible as defined by +type_incompatible?+ + # + # @return [Array>] The sets of image for the given types. + # Each hash will at least contain a +size+ [String] key, that has the format +x+ + def self.image_sets(types) types.flat_map do |type| contents_path = File.expand_path(File.join(File.dirname(__FILE__), "../../../vendor/Contents-#{type}.json")) raise "Unknown type #{type}" unless File.exist?(contents_path) @@ -28,8 +36,15 @@ def self.images_sets(types) end end + ## + # Check if the given types are compatible (if they can be used in the same set) + # + # @param [Symbol, #read] lhs The first type to check against the second type. + # @param [Symbol, #read] rhs The second type to check against the first type. + # + # @return [Boolean] +true+ if the given are compatible together, +false+ otherwise def self.type_incompatible?(lhs, rhs) - (lhs == :imessage && rhs != :imessage || lhs != :imessage && rhs == :imessage) + (lhs == :imessage && rhs != :imessage) || (lhs != :imessage && rhs == :imessage) end end end diff --git a/lib/ios_icon_generator/helpers/mask_icon.rb b/lib/ios_icon_generator/helpers/mask_icon.rb index c172305..cdd6129 100644 --- a/lib/ios_icon_generator/helpers/mask_icon.rb +++ b/lib/ios_icon_generator/helpers/mask_icon.rb @@ -20,6 +20,39 @@ module IOSIconGenerator module Helpers + ## + # Mask an icon using the parameters provided. + # + # The mask is for now always generated in the bottom left corner of the image. + # + # @param [String, #read] appiconset_path The path of the original app icon set to use to generate the new one. + # @param [String, #read] output_folder The folder to create the new app icon set in. + # @param [Hash, #read] mask A hash representing parameters for creating the mask. + # The Hash may contain the following values: + # - +background_color+: The background color to use when generating the mask + # - +stroke_color+: The stroke color to use when generating the mask. Used for the outline of the mask. + # - +stroke_width_offset+: The stroke width of the mask, offset to the image's minimum dimension (width or height). + # 1.0 means the stroke will have the full width/height of the image + # - +suffix+: The suffix to use when generating the new mask + # - +file+: The file to use when generating the new mask. This file should be an image, and it will be overlayed over the background. + # - +symbol+: The symbol to use when generating the new mask + # - +symbol_color+: The color to use for the symbol + # - +font+: The font to use for the symbol + # - +x_size_ratio+: The size ratio (of the width of the image) to use when generating the mask. 1.0 means the full width, 0.5 means half-width. + # - +y_size_ratio+: The size ratio (of the height of the image) to use when generating the mask. 1.0 means the full height, 0.5 means half-height. + # - +size_offset+: The size ratio (of the width and height) to use when generating the symbol or file. 1.0 means the full width and height, 0.5 means half-width and half-height. + # - +x_offset+: The X offset (of the width of the image) to use when generating the symbol or file. 1.0 means the full width, 0.5 means half-width. + # - +y_offset+: The Y offset (of the width of the image) to use when generating the symbol or file. 1.0 means the full height, 0.5 means half-height. + # - +shape+: The shape to use when generating the mask. Can be either +:triangle+ or +:square+. + # @param [Symbol, #read] parallel_processes The number of processes to use when generating the icons. + # +nil+ means it'll use as many processes as they are cores on the machine. + # +0+ will disables spawning any processes. + # @param [Lambda(progress [Int], total [Int]), #read] progress An optional progress block called when progress has been made generating the icons. + # It should take two parameters: + # - +progress+: An integer indicating the current progress out of +total+ + # - +total+: An integer indicating the total progress + # + # @return [String] Return the path to the generated app icon set. def self.mask_icon( appiconset_path:, output_folder:, @@ -54,7 +87,8 @@ def self.mask_icon( Parallel.each( json_content['images'], in_processes: parallel_processes, - finish: lambda do |_item, i, _result| + finish: lambda do |_item, i, result| + json_content['images'][i]['filename'] = result progress&.call(i, json_content['images'].count) end ) do |image| @@ -73,7 +107,9 @@ def self.mask_icon( 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_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 @@ -86,16 +122,29 @@ def self.mask_icon( 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" + "\\( -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]}'" + "-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}'") - image['filename'] = icon_output + next icon_output end File.write(File.join(output_folder, 'Contents.json'), JSON.pretty_generate(json_content)) + + output_folder end end end diff --git a/lib/ios_icon_generator/helpers/which.rb b/lib/ios_icon_generator/helpers/which.rb index 5caeb88..4710b10 100644 --- a/lib/ios_icon_generator/helpers/which.rb +++ b/lib/ios_icon_generator/helpers/which.rb @@ -15,18 +15,15 @@ # limitations under the License. module IOSIconGenerator - module Library + module Helpers ## # Cross-platform way of finding an executable in the +$PATH+. # # From http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby # - # == Parameters: - # +cmd+:: - # The name of the command to search the path for. + # @param cmd [String] The name of the command to search the path for. # - # == Returns: - # The full path to the command if found, and +nil+ otherwise. + # @return [String] The full path to the command if found, and +nil+ otherwise. def self.which(cmd) exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| diff --git a/lib/ios_icon_generator/version.rb b/lib/ios_icon_generator/version.rb index d287e0c..4fe4cf7 100644 --- a/lib/ios_icon_generator/version.rb +++ b/lib/ios_icon_generator/version.rb @@ -15,5 +15,8 @@ # limitations under the License. module IOSIconGenerator + ## + # The current version of the gem. + # VERSION = '0.1' end diff --git a/vendor/README.md b/vendor/README.md new file mode 100644 index 0000000..69ea670 --- /dev/null +++ b/vendor/README.md @@ -0,0 +1,4 @@ +Vendor +============ + +This folder contains the generated `Contents.json` files for the given platforms, with the `filename` stripped out. This is how the type are known to be valid or not.