inkcite 1.13.0 → 1.14.0
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 +4 -4
- data/README.md +1 -1
- data/inkcite.gemspec +1 -0
- data/lib/inkcite.rb +1 -1
- data/lib/inkcite/cli/server.rb +31 -6
- data/lib/inkcite/facade.rb +6 -0
- data/lib/inkcite/{animation.rb → facade/animation.rb} +19 -56
- data/lib/inkcite/{renderer → facade}/element.rb +0 -0
- data/lib/inkcite/facade/keyframe.rb +83 -0
- data/lib/inkcite/{renderer → facade}/style.rb +4 -0
- data/lib/inkcite/minifier.rb +141 -57
- data/lib/inkcite/parser.rb +1 -1
- data/lib/inkcite/renderer.rb +2 -2
- data/lib/inkcite/renderer/background.rb +1 -1
- data/lib/inkcite/renderer/base.rb +1 -1
- data/lib/inkcite/renderer/container_base.rb +3 -4
- data/lib/inkcite/renderer/fireworks.rb +231 -0
- data/lib/inkcite/renderer/in_browser.rb +1 -2
- data/lib/inkcite/renderer/responsive.rb +29 -2
- data/lib/inkcite/renderer/snow.rb +1 -1
- data/lib/inkcite/renderer/special_effect.rb +66 -29
- data/lib/inkcite/renderer/table_base.rb +6 -0
- data/lib/inkcite/renderer/td.rb +7 -2
- data/lib/inkcite/renderer/video_preview.rb +7 -1
- data/lib/inkcite/util.rb +56 -0
- data/lib/inkcite/version.rb +1 -1
- data/lib/inkcite/view.rb +42 -1
- data/test/animation_spec.rb +38 -5
- data/test/renderer/div_spec.rb +30 -0
- data/test/renderer/td_spec.rb +25 -0
- data/test/renderer/video_preview_spec.rb +1 -1
- data/test/util_spec.rb +11 -0
- metadata +24 -5
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: a5968f3e718b373eb3740d89dd019165130776a2
         | 
| 4 | 
            +
              data.tar.gz: e961a846c6e2d0664a66a1ae5abc9ed791db34dd
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 068fe33972b2d0f91a9c8be212ac3781392860d518aaf5277104cb3af1e1491766e2f3a5da47bed778bea41273bbd1fba3335c8f0d026845e16d29da85e06311
         | 
| 7 | 
            +
              data.tar.gz: 4f39b31b971373bb1beb2ab1c6689a53bdd73a0e75d3def5bc2eeaae61e83449e7a43581bce9375d2434c2c7e6178a4b912b098459a304080acb11be46664595
         | 
    
        data/README.md
    CHANGED
    
    | @@ -112,7 +112,7 @@ developer questions in a timely manner. | |
| 112 112 |  | 
| 113 113 | 
             
            ## License
         | 
| 114 114 |  | 
| 115 | 
            -
            Copyright (c) 2014- | 
| 115 | 
            +
            Copyright (c) 2014-2017 Jeffrey D. Hoffman. MIT Licensed, see [LICENSE] for
         | 
| 116 116 | 
             
            details.
         | 
| 117 117 |  | 
| 118 118 | 
             
            [Middleman]: http://middlemanapp.com
         | 
    
        data/inkcite.gemspec
    CHANGED
    
    | @@ -33,6 +33,7 @@ Gem::Specification.new do |spec| | |
| 33 33 | 
             
              spec.add_dependency 'htmlbeautifier'
         | 
| 34 34 | 
             
              spec.add_dependency 'image_optim'
         | 
| 35 35 | 
             
              spec.add_dependency 'image_optim_pack'
         | 
| 36 | 
            +
              spec.add_dependency 'kraken-io'
         | 
| 36 37 | 
             
              spec.add_dependency 'listen'
         | 
| 37 38 | 
             
              spec.add_dependency 'litmus'
         | 
| 38 39 | 
             
              spec.add_dependency 'mail'
         | 
    
        data/lib/inkcite.rb
    CHANGED
    
    | @@ -23,13 +23,13 @@ require 'active_support/core_ext/string/inflections' | |
| 23 23 | 
             
            require 'active_support/core_ext/string/starts_ends_with'
         | 
| 24 24 |  | 
| 25 25 | 
             
            require 'inkcite/version'
         | 
| 26 | 
            +
            require 'inkcite/facade'
         | 
| 26 27 | 
             
            require 'inkcite/email'
         | 
| 27 28 | 
             
            require 'inkcite/util'
         | 
| 28 29 | 
             
            require 'inkcite/view'
         | 
| 29 30 | 
             
            require 'inkcite/minifier'
         | 
| 30 31 | 
             
            require 'inkcite/parser'
         | 
| 31 32 | 
             
            require 'inkcite/renderer'
         | 
| 32 | 
            -
            require 'inkcite/animation'
         | 
| 33 33 |  | 
| 34 34 | 
             
            module Inkcite
         | 
| 35 35 |  | 
    
        data/lib/inkcite/cli/server.rb
    CHANGED
    
    | @@ -53,7 +53,8 @@ module Inkcite | |
| 53 53 | 
             
                    # InkciteApp to server the email as the root index page.
         | 
| 54 54 | 
             
                    app = Rack::Builder.new do
         | 
| 55 55 | 
             
                      use Rack::LiveReload
         | 
| 56 | 
            -
                      use Rack::Static, :urls => %w( /images | 
| 56 | 
            +
                      use Rack::Static, :urls => %w( /images/ ), :root => '.'
         | 
| 57 | 
            +
                      use OptimizedImage, :email => email, :urls => %w( /images-optim/ ), :root => '.'
         | 
| 57 58 | 
             
                      run InkciteApp.new(email, opts)
         | 
| 58 59 | 
             
                    end
         | 
| 59 60 |  | 
| @@ -88,6 +89,35 @@ module Inkcite | |
| 88 89 |  | 
| 89 90 | 
             
                  private
         | 
| 90 91 |  | 
| 92 | 
            +
                  # Extends Rack::Static to provide dynamic image
         | 
| 93 | 
            +
                  # minification on demand.  When an image is requested
         | 
| 94 | 
            +
                  # from the images-optim directory, compression is
         | 
| 95 | 
            +
                  # performed on the desired image if necessary and then
         | 
| 96 | 
            +
                  # the optimized image is returned.
         | 
| 97 | 
            +
                  class OptimizedImage < Rack::Static
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                    def initialize app, opts
         | 
| 100 | 
            +
                      @email = opts[:email]
         | 
| 101 | 
            +
                      super
         | 
| 102 | 
            +
                    end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                    def call env
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                      # e.g. images-optim/my-image.jpg
         | 
| 107 | 
            +
                      path = env['PATH_INFO']
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                      # Minify the image if the source version in images/ is newer
         | 
| 110 | 
            +
                      # or if the configuration file controlling optimization has
         | 
| 111 | 
            +
                      # been updated since the last time the image was requested.
         | 
| 112 | 
            +
                      Minifier.image(@email, File.basename(path), false) if can_serve(path)
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                      # Let the super method handle the actual serving of the image.
         | 
| 115 | 
            +
                      super
         | 
| 116 | 
            +
             | 
| 117 | 
            +
                    end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                  end
         | 
| 120 | 
            +
             | 
| 91 121 | 
             
                  class InkciteApp
         | 
| 92 122 |  | 
| 93 123 | 
             
                    def initialize email, opts
         | 
| @@ -123,11 +153,6 @@ module Inkcite | |
| 123 153 | 
             
                        puts ''
         | 
| 124 154 | 
             
                        puts "#{ts} Rendering your email [environment=#{environment}, format=#{format}, version=#{version || 'default'}]"
         | 
| 125 155 |  | 
| 126 | 
            -
                        # Before the rendering takes place, trigger image optimization of any
         | 
| 127 | 
            -
                        # new or updated images.  The {image} tag takes care of injecting the
         | 
| 128 | 
            -
                        # right path (optimized or not) depending on which version is needed.
         | 
| 129 | 
            -
                        @email.optimize_images
         | 
| 130 | 
            -
             | 
| 131 156 | 
             
                        view = @email.view(environment, format, version)
         | 
| 132 157 |  | 
| 133 158 | 
             
                        html = view.render!
         | 
| @@ -1,71 +1,27 @@ | |
| 1 1 | 
             
            module Inkcite
         | 
| 2 2 | 
             
              class Animation
         | 
| 3 3 |  | 
| 4 | 
            -
                 | 
| 4 | 
            +
                # A collection of animations assigned to a single element.
         | 
| 5 | 
            +
                class Composite
         | 
| 5 6 |  | 
| 6 | 
            -
                   | 
| 7 | 
            -
             | 
| 8 | 
            -
                  def initialize percent, ctx, styles={}
         | 
| 9 | 
            -
             | 
| 10 | 
            -
                    # Animation percents are always rounded to the nearest whole number.
         | 
| 11 | 
            -
                    @percent = percent.round(0)
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                    # Instantiate a new Style for this percentage.
         | 
| 14 | 
            -
                    @style = Inkcite::Renderer::Style.new("#{@percent}%", ctx, styles)
         | 
| 15 | 
            -
             | 
| 16 | 
            -
                  end
         | 
| 17 | 
            -
             | 
| 18 | 
            -
                  def [] key
         | 
| 19 | 
            -
                    @style[key]
         | 
| 20 | 
            -
                  end
         | 
| 21 | 
            -
             | 
| 22 | 
            -
                  def []= key, val
         | 
| 23 | 
            -
                    @style[key] = val
         | 
| 24 | 
            -
                  end
         | 
| 25 | 
            -
             | 
| 26 | 
            -
                  # For style chaining - e.g. keyframe.add(:key1, 'val').add(:key)
         | 
| 27 | 
            -
                  def add key, val
         | 
| 28 | 
            -
                    @style[key] = val
         | 
| 29 | 
            -
                    self
         | 
| 7 | 
            +
                  def initialize
         | 
| 8 | 
            +
                    @animations = []
         | 
| 30 9 | 
             
                  end
         | 
| 31 10 |  | 
| 32 | 
            -
                   | 
| 33 | 
            -
             | 
| 34 | 
            -
             | 
| 35 | 
            -
                    @style[key] ||= ''
         | 
| 36 | 
            -
                    @style[key] << ' ' unless @style[key].blank?
         | 
| 37 | 
            -
                    @style[key] << val
         | 
| 38 | 
            -
             | 
| 39 | 
            -
                  end
         | 
| 40 | 
            -
             | 
| 41 | 
            -
                  def add_with_prefixes key, val, ctx
         | 
| 42 | 
            -
             | 
| 43 | 
            -
                    ctx.prefixes.each do |prefix|
         | 
| 44 | 
            -
                      _key = "#{prefix}#{key}".to_sym
         | 
| 45 | 
            -
                      self[_key] = val
         | 
| 46 | 
            -
                    end
         | 
| 47 | 
            -
             | 
| 48 | 
            -
                    self
         | 
| 11 | 
            +
                  def << animation
         | 
| 12 | 
            +
                    @animations << animation
         | 
| 49 13 | 
             
                  end
         | 
| 50 14 |  | 
| 51 | 
            -
                  def  | 
| 52 | 
            -
                    @ | 
| 15 | 
            +
                  def to_keyframe_css
         | 
| 16 | 
            +
                    @animations.collect(&:to_keyframe_css).join("\n")
         | 
| 53 17 | 
             
                  end
         | 
| 54 18 |  | 
| 55 | 
            -
                   | 
| 56 | 
            -
             | 
| 57 | 
            -
                  # Creates a copy of the array of styles with the appropriate
         | 
| 58 | 
            -
                  # properties (e.g. transform) prefixed.
         | 
| 59 | 
            -
                  def get_prefixed_styles prefix
         | 
| 60 | 
            -
             | 
| 61 | 
            -
                    _styles = {}
         | 
| 19 | 
            +
                  def to_s
         | 
| 62 20 |  | 
| 63 | 
            -
                     | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 66 | 
            -
                    end
         | 
| 21 | 
            +
                    # Render each of the animations in the collection and join them
         | 
| 22 | 
            +
                    # in a single, comma-delimited string.
         | 
| 23 | 
            +
                    @animations.collect(&:to_s).join(', ')
         | 
| 67 24 |  | 
| 68 | 
            -
                    _styles
         | 
| 69 25 | 
             
                  end
         | 
| 70 26 |  | 
| 71 27 | 
             
                end
         | 
| @@ -76,7 +32,9 @@ module Inkcite | |
| 76 32 | 
             
                # Timing functions
         | 
| 77 33 | 
             
                LINEAR = 'linear'
         | 
| 78 34 | 
             
                EASE = 'ease'
         | 
| 35 | 
            +
                EASE_IN = 'ease-in'
         | 
| 79 36 | 
             
                EASE_IN_OUT = 'ease-in-out'
         | 
| 37 | 
            +
                EASE_OUT = 'ease-out'
         | 
| 80 38 |  | 
| 81 39 | 
             
                # Animation name, view context and array of keyframes
         | 
| 82 40 | 
             
                attr_reader :name, :ctx
         | 
| @@ -106,6 +64,11 @@ module Inkcite | |
| 106 64 | 
             
                  keyframe
         | 
| 107 65 | 
             
                end
         | 
| 108 66 |  | 
| 67 | 
            +
                # Returns true if this animation is blank - e.g. it has no keyframes.
         | 
| 68 | 
            +
                def blank?
         | 
| 69 | 
            +
                  @keyframes.blank?
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
             | 
| 109 72 | 
             
                def to_keyframe_css
         | 
| 110 73 |  | 
| 111 74 | 
             
                  css = ''
         | 
| 
            File without changes
         | 
| @@ -0,0 +1,83 @@ | |
| 1 | 
            +
            module Inkcite
         | 
| 2 | 
            +
              class Animation
         | 
| 3 | 
            +
                class Keyframe
         | 
| 4 | 
            +
             | 
| 5 | 
            +
                  attr_reader :percent, :style
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                  # Ending percentage the animation stays at this keyframe.  For
         | 
| 8 | 
            +
                  # example, a keyframe that starts at 20% and has a duration
         | 
| 9 | 
            +
                  # of 19.9% would render as 25%, 39.9% { ... }
         | 
| 10 | 
            +
                  attr_accessor :duration
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def initialize percent, ctx, styles={}
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    # Animation percents are always rounded to the nearest whole number.
         | 
| 15 | 
            +
                    @percent = percent.round(0)
         | 
| 16 | 
            +
                    @duration = 0
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    # Instantiate a new Style for this percentage.
         | 
| 19 | 
            +
                    @style = Inkcite::Renderer::Style.new(nil, ctx, styles)
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  def [] key
         | 
| 24 | 
            +
                    @style[key]
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def []= key, val
         | 
| 28 | 
            +
                    @style[key] = val
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  # For style chaining - e.g. keyframe.add(:key1, 'val').add(:key)
         | 
| 32 | 
            +
                  def add key, val
         | 
| 33 | 
            +
                    @style[key] = val
         | 
| 34 | 
            +
                    self
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  # Appends a value to an existing key
         | 
| 38 | 
            +
                  def append key, val
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    @style[key] ||= ''
         | 
| 41 | 
            +
                    @style[key] << ' ' unless @style[key].blank?
         | 
| 42 | 
            +
                    @style[key] << val
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  def add_with_prefixes key, val, ctx
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    ctx.prefixes.each do |prefix|
         | 
| 49 | 
            +
                      _key = "#{prefix}#{key}".to_sym
         | 
| 50 | 
            +
                      self[_key] = val
         | 
| 51 | 
            +
                    end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    self
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  def to_css prefix
         | 
| 57 | 
            +
                    css = "#{@percent}%"
         | 
| 58 | 
            +
                    css << ", #{@percent + @duration.to_f}%" if @duration > 0
         | 
| 59 | 
            +
                    css << ' { '
         | 
| 60 | 
            +
                    css << @style.to_inline_css(prefix)
         | 
| 61 | 
            +
                    css << ' }'
         | 
| 62 | 
            +
                    css
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  private
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  # Creates a copy of the array of styles with the appropriate
         | 
| 68 | 
            +
                  # properties (e.g. transform) prefixed.
         | 
| 69 | 
            +
                  def get_prefixed_styles prefix
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                    _styles = {}
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                    @styles.each_pair do |key, val|
         | 
| 74 | 
            +
                      key = "#{prefix}#{key}".to_sym if Inkcite::Renderer::Style.needs_prefixing?(key)
         | 
| 75 | 
            +
                      _styles[key] = val
         | 
| 76 | 
            +
                    end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                    _styles
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                end
         | 
| 82 | 
            +
              end
         | 
| 83 | 
            +
            end
         | 
    
        data/lib/inkcite/minifier.rb
    CHANGED
    
    | @@ -2,7 +2,7 @@ module Inkcite | |
| 2 2 | 
             
              class Minifier
         | 
| 3 3 |  | 
| 4 4 | 
             
                # Directory of optimized images
         | 
| 5 | 
            -
                IMAGE_CACHE =  | 
| 5 | 
            +
                IMAGE_CACHE = 'images-optim'
         | 
| 6 6 |  | 
| 7 7 | 
             
                # Maximum line length for CSS and HTML - lines exceeding this length cause
         | 
| 8 8 | 
             
                # problems in certain email clients.
         | 
| @@ -40,14 +40,31 @@ module Inkcite | |
| 40 40 | 
             
                    # a semicolon or close bracket.
         | 
| 41 41 | 
             
                    if ctx.email? && code.length > MAXIMUM_LINE_LENGTH
         | 
| 42 42 |  | 
| 43 | 
            -
                      #  | 
| 44 | 
            -
                       | 
| 43 | 
            +
                      # Last position at which a line break was be inserted at.
         | 
| 44 | 
            +
                      last_break_at = 0
         | 
| 45 45 |  | 
| 46 46 | 
             
                      # Work through the code injecting line breaks until either no further
         | 
| 47 47 | 
             
                      # breakable characters are found or we've reached the end of the code.
         | 
| 48 | 
            -
                      while  | 
| 49 | 
            -
                        break_at = code.rindex(/[ | 
| 50 | 
            -
             | 
| 48 | 
            +
                      while last_break_at < code.length
         | 
| 49 | 
            +
                        break_at = code.rindex(/[ ,;{}]/, last_break_at + MAXIMUM_LINE_LENGTH)
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                        # No further characters match (unlikely) or an unbroken string since
         | 
| 52 | 
            +
                        # the last time a break was injected.  Either way, let's get out.
         | 
| 53 | 
            +
                        break if break_at.nil? || break_at <= last_break_at
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                        # If we've found a space we can break at, do a direct replacement of the
         | 
| 56 | 
            +
                        # space with a new line.  Otherwise, inject a new line one spot after
         | 
| 57 | 
            +
                        # the matching character.
         | 
| 58 | 
            +
                        if code[break_at] == ' '
         | 
| 59 | 
            +
                          code[break_at] = NEW_LINE
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                        else
         | 
| 62 | 
            +
                          break_at += 1
         | 
| 63 | 
            +
                          code.insert(break_at, NEW_LINE)
         | 
| 64 | 
            +
                          break_at += 1
         | 
| 65 | 
            +
                        end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                        last_break_at = break_at
         | 
| 51 68 | 
             
                      end
         | 
| 52 69 |  | 
| 53 70 | 
             
                    end
         | 
| @@ -107,75 +124,79 @@ module Inkcite | |
| 107 124 |  | 
| 108 125 | 
             
                end
         | 
| 109 126 |  | 
| 110 | 
            -
                def self. | 
| 127 | 
            +
                def self.image email, img_name, force=false
         | 
| 111 128 |  | 
| 112 | 
            -
                   | 
| 113 | 
            -
                   | 
| 129 | 
            +
                  # Original, unoptimized source image
         | 
| 130 | 
            +
                  source_img = File.join(email.image_dir, img_name)
         | 
| 114 131 |  | 
| 115 | 
            -
                  #  | 
| 116 | 
            -
                   | 
| 117 | 
            -
                   | 
| 132 | 
            +
                  # Cached, optimized path for this image.
         | 
| 133 | 
            +
                  cache_path = email.project_file(IMAGE_CACHE)
         | 
| 134 | 
            +
                  cached_img = File.join(cache_path, File.basename(img_name))
         | 
| 118 135 |  | 
| 119 | 
            -
                  #  | 
| 120 | 
            -
                   | 
| 121 | 
            -
                  if File.exist?(cache_path)
         | 
| 136 | 
            +
                  # Full path to the local project's kraken config if it exists
         | 
| 137 | 
            +
                  kraken_config_path = email.project_file(KRAKEN_CONFIG_YML)
         | 
| 122 138 |  | 
| 123 | 
            -
             | 
| 124 | 
            -
             | 
| 125 | 
            -
             | 
| 126 | 
            -
             | 
| 139 | 
            +
                  # This is the array of config files that will be searched to
         | 
| 140 | 
            +
                  # determine which algorithm to use to compress the images.
         | 
| 141 | 
            +
                  config_paths = [
         | 
| 142 | 
            +
                      kraken_config_path,
         | 
| 143 | 
            +
                      email.project_file(IMAGE_OPTIM_CONFIG_YML),
         | 
| 144 | 
            +
                      File.join(Inkcite.asset_path, 'init', IMAGE_OPTIM_CONFIG_YML)
         | 
| 145 | 
            +
                  ]
         | 
| 127 146 |  | 
| 128 | 
            -
             | 
| 129 | 
            -
             | 
| 130 | 
            -
                      removed_images = removed_images.collect { |img| File.join(cache_path, img) }
         | 
| 131 | 
            -
                      FileUtils.rm(removed_images)
         | 
| 147 | 
            +
                  # Grab the first file that exists for this project.
         | 
| 148 | 
            +
                  config_path = config_paths.detect { |p| File.exist?(p) }
         | 
| 132 149 |  | 
| 133 | 
            -
             | 
| 150 | 
            +
                  unless force
         | 
| 134 151 |  | 
| 135 | 
            -
             | 
| 152 | 
            +
                    # Get the last-modified date of the image optimization config
         | 
| 153 | 
            +
                    # file - if that file is newer than the image, re-optimization
         | 
| 154 | 
            +
                    # is necessary because the settings have changed.
         | 
| 155 | 
            +
                    config_last_modified = Util.last_modified(config_path)
         | 
| 136 156 |  | 
| 137 | 
            -
             | 
| 138 | 
            -
             | 
| 139 | 
            -
             | 
| 140 | 
            -
                  updated_images = Dir.glob(File.join(images_path, '*.*')).select do |img|
         | 
| 141 | 
            -
                    cached_img = File.join(cache_path, File.basename(img))
         | 
| 157 | 
            +
                    # Get the last-modified date of the actual image.  If the source
         | 
| 158 | 
            +
                    # image is newer than the cached version, we'll need to run it
         | 
| 159 | 
            +
                    # through optimization again, too.
         | 
| 142 160 | 
             
                    cache_last_modified = Util.last_modified(cached_img)
         | 
| 143 | 
            -
                     | 
| 144 | 
            -
             | 
| 161 | 
            +
                    source_last_modified = Util.last_modified(source_img)
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                    # Nothing to do unless the image in the cache is older than the
         | 
| 164 | 
            +
                    # source or the config file.
         | 
| 165 | 
            +
                    return unless config_last_modified > cache_last_modified || source_last_modified > cache_last_modified
         | 
| 145 166 |  | 
| 146 | 
            -
                   | 
| 147 | 
            -
                  return if updated_images.blank?
         | 
| 167 | 
            +
                  end
         | 
| 148 168 |  | 
| 169 | 
            +
                  # Make sure the image cache directory exists
         | 
| 149 170 | 
             
                  FileUtils.mkpath(cache_path)
         | 
| 150 171 |  | 
| 151 | 
            -
                  #  | 
| 152 | 
            -
                   | 
| 153 | 
            -
             | 
| 154 | 
            -
             | 
| 155 | 
            -
             | 
| 156 | 
            -
             | 
| 172 | 
            +
                  # Read the image compression configuration settings
         | 
| 173 | 
            +
                  config = Util::read_yml(config_path, :fail_if_not_exists => false)
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                  if config_path == kraken_config_path
         | 
| 176 | 
            +
                    minify_with_kraken_io email, config, source_img, cached_img
         | 
| 177 | 
            +
             | 
| 157 178 | 
             
                  else
         | 
| 158 | 
            -
                    {
         | 
| 159 | 
            -
                        :allow_lossy => true,
         | 
| 160 | 
            -
                        :gifsicle => { :level => 3 },
         | 
| 161 | 
            -
                        :jpegoptim => { :max_quality => 50 },
         | 
| 162 | 
            -
                        :jpegrecompress => { :quality => 1 },
         | 
| 163 | 
            -
                        :pngout => false,
         | 
| 164 | 
            -
                        :svgo => false
         | 
| 165 | 
            -
                    }
         | 
| 166 | 
            -
                  end
         | 
| 167 179 |  | 
| 168 | 
            -
             | 
| 180 | 
            +
                    # Default image optimization uses built-in ImageOptim
         | 
| 181 | 
            +
                    minify_with_image_optim email, config, source_img, cached_img
         | 
| 169 182 |  | 
| 170 | 
            -
                  # Copy all of the images that need updating into the temporary directory.
         | 
| 171 | 
            -
                  # Specifically joining the images_path to the image to avoid Email's
         | 
| 172 | 
            -
                  # image_path which may change it's directory if optimization is enabled.
         | 
| 173 | 
            -
                  updated_images.each do |img|
         | 
| 174 | 
            -
                    cached_img = File.join(cache_path, File.basename(img))
         | 
| 175 | 
            -
                    FileUtils.cp(img, cached_img)
         | 
| 176 | 
            -
                    image_optim.optimize_image!(cached_img)
         | 
| 177 183 | 
             
                  end
         | 
| 178 184 |  | 
| 185 | 
            +
                  original_size = File.size(source_img)
         | 
| 186 | 
            +
                  compressed_size = File.size(cached_img)
         | 
| 187 | 
            +
                  percent_compressed = ((1.0 - (compressed_size / original_size.to_f)) * 100).round(1)
         | 
| 188 | 
            +
                  puts "Compressed #{img_name} #{percent_compressed}%"
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                end
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                def self.images email, force=false
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                  images_path = email.image_dir
         | 
| 195 | 
            +
             | 
| 196 | 
            +
                  # Iterate through all of the images in the project and optimize them
         | 
| 197 | 
            +
                  # if necessary.
         | 
| 198 | 
            +
                  Dir.glob(File.join(images_path, '*.*')).each { |img| self.image(email, File.basename(img), force) }
         | 
| 199 | 
            +
             | 
| 179 200 | 
             
                end
         | 
| 180 201 |  | 
| 181 202 | 
             
                def self.js code, ctx
         | 
| @@ -188,11 +209,74 @@ module Inkcite | |
| 188 209 |  | 
| 189 210 | 
             
                private
         | 
| 190 211 |  | 
| 212 | 
            +
                def self.minify_with_image_optim email, config, source_img, cached_img
         | 
| 213 | 
            +
             | 
| 214 | 
            +
                  # Copy the image into the destination directory and then use Image Optim
         | 
| 215 | 
            +
                  # to optimize it in place.
         | 
| 216 | 
            +
                  FileUtils.cp(source_img, cached_img)
         | 
| 217 | 
            +
                  ImageOptim.new(config).optimize_image!(cached_img)
         | 
| 218 | 
            +
             | 
| 219 | 
            +
                end
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                def self.minify_with_kraken_io email, config, source_img, cached_img
         | 
| 222 | 
            +
             | 
| 223 | 
            +
                  require 'kraken-io'
         | 
| 224 | 
            +
                  require 'open-uri'
         | 
| 225 | 
            +
             | 
| 226 | 
            +
                  # Initialize the Kraken API using the API key and secret defined in the
         | 
| 227 | 
            +
                  # config.yml file.
         | 
| 228 | 
            +
                  kraken = Kraken::API.new(
         | 
| 229 | 
            +
                      :api_key => config[:api_key],
         | 
| 230 | 
            +
                      :api_secret => config[:api_secret]
         | 
| 231 | 
            +
                  )
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                  # As you might expect, Outlook doesn't support webp so it needs to be
         | 
| 234 | 
            +
                  # disabled by default.  Otherwise, Kraken always compresses with webp.
         | 
| 235 | 
            +
                  kraken_opts = { :webp => false }
         | 
| 236 | 
            +
             | 
| 237 | 
            +
                  # Get the file format (e.g. gif) of the file being optimized.
         | 
| 238 | 
            +
                  source_fmt = File.extname(source_img).delete('.')
         | 
| 239 | 
            +
             | 
| 240 | 
            +
                  # True if the configuration file does not specifically exclude
         | 
| 241 | 
            +
                  # this format from being processed.
         | 
| 242 | 
            +
                  compress_this_fmt = config[source_fmt.to_sym] != false
         | 
| 243 | 
            +
             | 
| 244 | 
            +
                  # Typically, we're going to want lossy compression to minify the file
         | 
| 245 | 
            +
                  # but if the user has put lossy: false specifically in their config
         | 
| 246 | 
            +
                  # file, we'll disable that feature in Kraken too.  Defaults to true.
         | 
| 247 | 
            +
                  kraken_opts[:lossy] = compress_this_fmt
         | 
| 248 | 
            +
             | 
| 249 | 
            +
                  # Send the quality metric to Kraken only if specified.  Per their
         | 
| 250 | 
            +
                  # documentation, Kraken will attempt to guess the best quality to
         | 
| 251 | 
            +
                  # use but in my experience it errs on the side of higher quality
         | 
| 252 | 
            +
                  # whereas setting a quality factor around 50 produces a good
         | 
| 253 | 
            +
                  # balance of image detail and file size.
         | 
| 254 | 
            +
                  if compress_this_fmt
         | 
| 255 | 
            +
                    quality = config[:quality].to_i
         | 
| 256 | 
            +
                    kraken_opts[:quality] = quality if quality > 0 and quality <= 100
         | 
| 257 | 
            +
                  end
         | 
| 258 | 
            +
             | 
| 259 | 
            +
                  # Upload the image to Kraken which blocks by default until the image
         | 
| 260 | 
            +
                  # has been optimized.
         | 
| 261 | 
            +
                  data = kraken.upload(source_img, kraken_opts)
         | 
| 262 | 
            +
                  if data.success
         | 
| 263 | 
            +
                    File.write(cached_img, open(data.kraked_url).read, { :mode => 'wb' })
         | 
| 264 | 
            +
                  else
         | 
| 265 | 
            +
                    puts "Failed to optimize #{img_name}: #{data.message}"
         | 
| 266 | 
            +
                  end
         | 
| 267 | 
            +
             | 
| 268 | 
            +
                end
         | 
| 269 | 
            +
             | 
| 191 270 | 
             
                # Name of the Image Optim configuration yml file that can be
         | 
| 192 271 | 
             
                # put in the project directory to explicitly control the image
         | 
| 193 272 | 
             
                # optimization process.
         | 
| 194 273 | 
             
                IMAGE_OPTIM_CONFIG_YML = 'image_optim.yml'
         | 
| 195 274 |  | 
| 275 | 
            +
                # Name of the Kraken configuration yml that, when present in
         | 
| 276 | 
            +
                # the project directory and populated with an API key and secret
         | 
| 277 | 
            +
                # causes Kraken.io paid image optimization service to be used.
         | 
| 278 | 
            +
                KRAKEN_CONFIG_YML = 'kraken.yml'
         | 
| 279 | 
            +
             | 
| 196 280 | 
             
                NEW_LINE = "\n"
         | 
| 197 281 |  | 
| 198 282 | 
             
                # Used to match inline styles that will be compressed when minifying
         |