image_optim 0.9.1 → 0.10.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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- OGFiMDdmODY3N2ZkMTgwZWNmMTlhOTFkNmY1NjVjZGRjMmI3ZDQ0MA==
4
+ NzI5M2M3MWM1OWNiMDM5MGFiMmFkNDYwYzBiZjY5NWFiNDc0NTkyNQ==
5
5
  data.tar.gz: !binary |-
6
- MmNjYTZlNDFkMGVmM2U1NmUyNDQ3NDIxYWFmNjAzYTUzMGY0YWQ2Mg==
6
+ YWI3NDcwN2U3ZmIzZGI4NjYxZmJkOTVhYmQ5OGMzNTYyNGFlODI3Ng==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- ZjVkYWZkM2ExYWU2MDAxODU1MGNmNDI2OWNhNGZiNTI2YzU1NWYzN2QyYzRh
10
- M2E3Njg0ZjA1MmVlOTc3ZGNkYjdlZTY1NDk1MDJhYzg3NjhkM2I2ZTYxNmNi
11
- MmQwMjc4OWJlYTRlMDNiZmE2YzkxMTQ4YWIwMTlmZTM0Zjk4NjM=
9
+ YjVkYjUwYTEzMzQ2ZWZjNjg3ZWU1ZDJlMGRmY2FjOTc3NzU4YThlYTVmMmE2
10
+ YWEwODAzMGVkN2QyZDc4MjUyZGQ4ZDAyMTgzMDA3ZjVjNDQ3ODc5NTBlMzI1
11
+ MDU4ZDRlZTAzNjc2M2IwNWMxZGNjZjY0OWQ0MDQ2NGNhMThlYTE=
12
12
  data.tar.gz: !binary |-
13
- NDAxMzU2ODRiYzNhZGMwZDM2M2MzYzk0M2VhYTQ2MjZlMGFlZGNiMTM3Yzk0
14
- YmRkZDk1MTQ2M2U4ZmJiOThjNzBjN2YxNzAzMDFhOGUxNmZlYmNkMGNkNjk0
15
- YjFlMmUzNDM0NTFiMGMyOTQ1MGE1YmRjM2YyYWI0NWM3MWE4Mzk=
13
+ YjlhYjk1NDIyM2FlMzE4NjZlM2QxOTJjYzk0ZjJlYTI1YTYwMGUxODA3Yzdj
14
+ NzdjODY2NDBjZDIwODA2YTIzNThjZWJjMTg2ZGRlYjllYjg3NGIxNzU0Y2Fk
15
+ NWYwNDYwODkwN2FjNmI2ODU5NWNlNjdmMjY5NjdkZjk3ZTY3NDM=
data/.travis.yml CHANGED
@@ -7,8 +7,6 @@ rvm:
7
7
  - 2.0.0
8
8
  - jruby-18mode
9
9
  - jruby-19mode
10
- - rbx-18mode
11
- - rbx-19mode
12
10
  - ree
13
11
  script: bundle exec rspec
14
12
  before_install:
data/README.markdown CHANGED
@@ -13,7 +13,10 @@ Optimize (lossless compress) images (jpeg, png, gif) using external utilities:
13
13
 
14
14
  Based on [ImageOptim.app](http://imageoptim.com/).
15
15
 
16
+ [![Gem Version](https://badge.fury.io/rb/image_optim.png)](http://badge.fury.io/rb/image_optim)
16
17
  [![Build Status](https://travis-ci.org/toy/image_optim.png?branch=master)](https://travis-ci.org/toy/image_optim)
18
+ [![Code Climate](https://codeclimate.com/github/toy/image_optim.png)](https://codeclimate.com/github/toy/image_optim)
19
+ [![Dependency Status](https://gemnasium.com/toy/image_optim.png)](https://gemnasium.com/toy/image_optim)
17
20
 
18
21
  ## Gem installation
19
22
 
@@ -123,10 +126,38 @@ Multiple images:
123
126
 
124
127
  image_optim.optimize_images!(Dir['*.*'])
125
128
 
129
+ ### From rails
130
+
131
+ `ImageOptim::Railtie` will automatically initialize processing of assets if `config.assets.compress` is true.
132
+
133
+ As image optimization can be time consuming you may prefer to optimize original asset files.
134
+
135
+ Automatic assets processing can be turned off by setting `config.assets.image_optim = false`.
136
+
137
+ ## Configuration
138
+
139
+ Configuration in YAML format will be read and prepanded to options from two paths:
140
+
141
+ * `$XDG_CONFIG_HOME/image_optim.yml` (by default `~/.config/image_optim.yml`)
142
+ * `.image_optim.yml` in current working directory
143
+
144
+ Example configuration:
145
+
146
+ nice: 20
147
+ pngout: false # disable
148
+ optipng:
149
+ level: 5
150
+
126
151
  ## Options
127
152
 
153
+ * `:nice` — Nice level *(defaults to 10)*
154
+ * `:threads` — Number of threads or disable *(defaults to number of processors)*
155
+ * `:verbose` — Verbose output *(defaults to false)*
156
+
128
157
  Worker can be disabled by passing false instead of options hash.
129
158
 
159
+ <!---<worker-options>-->
160
+
130
161
  ### pngcrush
131
162
  * `:chunks` — List of chunks to remove or 'alla' - all except tRNS/transparency or 'allb' - all except tRNS and gAMA/gamma *(defaults to alla)*
132
163
  * `:fix` — Fix otherwise fatal conditions such as bad CRCs *(defaults to false)*
@@ -150,11 +181,13 @@ Worker can be disabled by passing false instead of options hash.
150
181
  ### jpegtran
151
182
  * `:copy_chunks` — Copy all chunks *(defaults to false)*
152
183
  * `:progressive` — Create progressive JPEG file *(defaults to true)*
153
- * `:jpegrescan` — Use jpegtran through jpegrescan, ignore progressive option *(defaults to true)*
184
+ * `:jpegrescan` — Use jpegtran through jpegrescan, ignore progressive option *(defaults to false)*
154
185
 
155
186
  ### gifsicle
156
187
  * `:interlace` — Turn interlacing on *(defaults to false)*
157
188
 
189
+ <!---</worker-options>-->
190
+
158
191
  ## Copyright
159
192
 
160
193
  Copyright (c) 2012-2013 Ivan Kuchin. See LICENSE.txt for details.
data/bin/image_optim CHANGED
@@ -1,22 +1,24 @@
1
1
  #!/usr/bin/env ruby
2
2
  # encoding: UTF-8
3
3
 
4
- require 'image_optim'
5
- require 'progress'
6
- require 'optparse'
7
- require 'find'
4
+ require 'image_optim/runner'
8
5
 
6
+ args = ARGV.dup
9
7
  options = {}
10
8
 
11
- OptionParser.accept(ImageOptim::TrueFalseNil, OptionParser.top.atype[TrueClass][0].merge('nil' => nil)){ |arg, val| val }
12
-
13
9
  option_parser = OptionParser.new do |op|
14
- op.banner = <<-TEXT
15
- #{op.program_name} v#{ImageOptim.version}
16
-
17
- Usege:
18
- #{op.program_name} [options] image_path …
19
-
10
+ op.accept(ImageOptim::TrueFalseNil, OptionParser.top.atype[TrueClass][0].merge('nil' => nil)){ |arg, val| val }
11
+
12
+ op.banner = <<-TEXT.gsub(/^\s*\|/, '')
13
+ |#{op.program_name} v#{ImageOptim.version}
14
+ |
15
+ |Usege:
16
+ | #{op.program_name} [options] image_path …
17
+ |
18
+ |Configuration will be read and prepanded to options from two paths:
19
+ | #{ImageOptim::Config::GLOBAL_CONFIG_PATH}
20
+ | #{ImageOptim::Config::LOCAL_CONFIG_PATH} (in current working directory)
21
+ |
20
22
  TEXT
21
23
 
22
24
  op.on('-r', '-R', '--recursive', 'Recurively scan directories for images') do |recursive|
@@ -84,7 +86,7 @@ Usege:
84
86
  end
85
87
 
86
88
  op.on_tail('-h', '--help', 'Show full help') do
87
- puts option_parser.help
89
+ puts op.help
88
90
  exit
89
91
  end
90
92
 
@@ -95,108 +97,14 @@ Usege:
95
97
  end
96
98
 
97
99
  begin
98
- option_parser.parse!
100
+ option_parser.parse!(args)
101
+ ImageOptim::Runner.run!(args, options) or exit 1
99
102
  rescue OptionParser::ParseError => e
100
103
  abort "#{e.to_s}\n\n#{option_parser.help}"
101
- end
102
-
103
- if options[:verbose]
104
- def print_options(options, level = 1)
105
- prefix = ' ' * level
106
- options.each do |key, value|
107
- if value.is_a?(Hash)
108
- puts "#{prefix}#{key}:"
109
- print_options(value, level + 1)
110
- else
111
- puts "#{prefix}#{key}: #{value.inspect}"
112
- end
113
- end
114
- end
115
-
116
- puts 'Options:'
117
- print_options(options)
118
- end
119
-
120
- if ARGV.empty?
121
- abort "specify image paths to optimize\n\n#{option_parser.help}"
122
- else
123
- recursive = options.delete(:recursive)
124
- image_optim = begin
125
- ImageOptim.new(options)
126
- rescue ImageOptim::ConfigurationError => e
104
+ rescue => e
105
+ if options[:verbose]
106
+ abort "#{e.to_s}\n#{e.backtrace.join("\n")}"
107
+ else
127
108
  abort e.to_s
128
109
  end
129
-
130
- paths = []
131
- ARGV.each do |arg|
132
- if File.file?(arg)
133
- if image_optim.optimizable?(arg)
134
- paths << arg
135
- else
136
- warn "#{arg} is not an image or there is no optimizer for it"
137
- end
138
- else
139
- if recursive
140
- Find.find(arg) do |path|
141
- paths << path if File.file?(path) && image_optim.optimizable?(path)
142
- end
143
- else
144
- warn "#{arg} is not a file"
145
- end
146
- end
147
- end
148
-
149
- module Space
150
- SIZE_SYMBOLS = %w[B K M G T P E Z Y].freeze
151
- PRECISION = 1
152
- LENGTH = 4 + PRECISION + 1
153
- COEF = 1 / Math.log(10)
154
-
155
- EMPTY_SPACE = ' ' * LENGTH
156
- NOT_COUNTED_SPACE = '!' * LENGTH
157
-
158
- class << self
159
- attr_writer :base10
160
- def denominator
161
- @denominator ||= @base10 ? 1000.0 : 1024.0
162
- end
163
-
164
- def space(size, options = {})
165
- case size
166
- when false
167
- NOT_COUNTED_SPACE.bold.red
168
- when 0, nil
169
- EMPTY_SPACE
170
- else
171
- number, degree = size, 0
172
- while number.abs >= 1000 && degree < SIZE_SYMBOLS.length - 1
173
- number /= denominator
174
- degree += 1
175
- end
176
-
177
- "#{degree == 0 ? number.to_s : "%.#{PRECISION}f" % number}#{SIZE_SYMBOLS[degree]}".rjust(LENGTH)
178
- end
179
- end
180
- end
181
- end
182
-
183
- def size_percent(src_size, dst_size)
184
- '%5.2f%% %s' % [100 - 100.0 * dst_size / src_size, Space.space(src_size - dst_size)]
185
- end
186
-
187
- paths = paths.with_progress('optimizing')
188
- results = image_optim.optimize_images(paths) do |src, dst|
189
- if dst
190
- src_size, dst_size = src.size, dst.size
191
- percent = size_percent(src_size, dst_size)
192
- dst.replace(src)
193
- ["#{percent} #{src}", src_size, dst_size]
194
- else
195
- ["------ #{Space::EMPTY_SPACE} #{src}", src.size, src.size]
196
- end
197
- end
198
- lines, src_sizes, dst_sizes = results.transpose
199
- if lines
200
- $stdout.puts lines, "Total: #{size_percent(src_sizes.inject(:+), dst_sizes.inject(:+))}\n"
201
- end
202
110
  end
data/image_optim.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'image_optim'
5
- s.version = '0.9.1'
5
+ s.version = '0.10.0'
6
6
  s.summary = %q{Optimize (lossless compress) images (jpeg, png, gif) using external utilities (advpng, gifsicle, jpegoptim, jpegtran, optipng, pngcrush, pngout)}
7
7
  s.homepage = "http://github.com/toy/#{s.name}"
8
8
  s.authors = ['Ivan Kuchin']
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
15
15
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
16
  s.require_paths = %w[lib]
17
17
 
18
- s.add_dependency 'fspath', '~> 2.0.5'
18
+ s.add_dependency 'fspath', '~> 2.1.0'
19
19
  s.add_dependency 'image_size', '~> 1.1.2'
20
20
  s.add_dependency 'exifr', '~> 1.1.3'
21
21
  s.add_dependency 'progress', '~> 3.0.0'
@@ -0,0 +1,3 @@
1
+ class ImageOptim
2
+ class BinNotFoundError < StandardError; end
3
+ end
@@ -0,0 +1,50 @@
1
+ require 'image_optim/bin_not_found_error'
2
+ require 'thread'
3
+ require 'fspath'
4
+
5
+ class ImageOptim
6
+ class BinResolver
7
+ attr_reader :dir
8
+ def initialize
9
+ @bins = {}
10
+ @lock = Mutex.new
11
+ end
12
+
13
+ def resolve!(bin)
14
+ bin = bin.to_sym
15
+ unless @bins.include?(bin)
16
+ @lock.synchronize do
17
+ @bins[bin] = resolve?(bin) unless @bins.include?(bin)
18
+ end
19
+ end
20
+ @bins[bin] or raise BinNotFoundError, "`#{bin}` not found"
21
+ end
22
+
23
+ VENDOR_PATH = File.expand_path('../../../vendor', __FILE__)
24
+
25
+ def env_path
26
+ "#{dir}:#{ENV['PATH']}:#{VENDOR_PATH}"
27
+ end
28
+
29
+ private
30
+
31
+ def resolve?(bin)
32
+ if path = ENV["#{bin}_bin".upcase]
33
+ unless @dir
34
+ @dir = FSPath.temp_dir
35
+ at_exit{ FileUtils.remove_entry_secure @dir }
36
+ end
37
+ symlink = @dir / bin
38
+ symlink.make_symlink(File.expand_path(path))
39
+
40
+ accessible?(symlink)
41
+ else
42
+ accessible?(bin)
43
+ end
44
+ end
45
+
46
+ def accessible?(bin)
47
+ `env PATH=#{env_path.shellescape} which #{bin.to_s.shellescape}` != ''
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,137 @@
1
+ require 'image_optim/option_helpers'
2
+ require 'image_optim/configuration_error'
3
+ require 'image_optim/hash_helpers'
4
+ require 'image_optim/worker'
5
+ require 'set'
6
+ require 'yaml'
7
+
8
+ class ImageOptim
9
+ class Config
10
+ include OptionHelpers
11
+
12
+ GLOBAL_CONFIG_PATH = File.join(File.expand_path(ENV['XDG_CONFIG_HOME'] || '~/.config'), 'image_optim.yml')
13
+ LOCAL_CONFIG_PATH = '.image_optim.yml'
14
+
15
+ class << self
16
+ def global
17
+ File.file?(GLOBAL_CONFIG_PATH) ? read(GLOBAL_CONFIG_PATH) : {}
18
+ end
19
+
20
+ def local
21
+ File.file?(LOCAL_CONFIG_PATH) ? read(LOCAL_CONFIG_PATH) : {}
22
+ end
23
+
24
+ private
25
+
26
+ def read(path)
27
+ config = YAML.load_file(path)
28
+ unless config.is_a?(Hash)
29
+ raise "excpected hash, got #{config.inspect}"
30
+ end
31
+ HashHelpers.deep_symbolise_keys(config)
32
+ rescue => e
33
+ warn "exception when reading #{path}: #{e}"
34
+ {}
35
+ end
36
+ end
37
+
38
+ def initialize(options)
39
+ @options = [
40
+ Config.global,
41
+ Config.local,
42
+ HashHelpers.deep_symbolise_keys(options),
43
+ ].inject do |memo, hash|
44
+ HashHelpers.deep_merge(memo, hash)
45
+ end
46
+ @used = Set.new
47
+ end
48
+
49
+ def get!(key)
50
+ key = key.to_sym
51
+ @used << key
52
+ @options[key]
53
+ end
54
+
55
+ def assert_no_unused_options!
56
+ unknown_options = @options.reject{ |key, value| @used.include?(key) }
57
+ unless unknown_options.empty?
58
+ raise ConfigurationError, "unknown options #{unknown_options.inspect} for #{self}"
59
+ end
60
+ end
61
+
62
+ def nice
63
+ nice = get!(:nice)
64
+
65
+ case nice
66
+ when true, nil
67
+ 10
68
+ when false
69
+ 0
70
+ else
71
+ nice.to_i
72
+ end
73
+ end
74
+
75
+ def threads
76
+ threads = get!(:threads)
77
+
78
+ threads = case threads
79
+ when true, nil
80
+ processor_count
81
+ when false
82
+ 1
83
+ else
84
+ threads.to_i
85
+ end
86
+
87
+ OptionHelpers.limit_with_range(threads, 1..16)
88
+ end
89
+
90
+ def verbose
91
+ !!get!(:verbose)
92
+ end
93
+
94
+ def for_worker(klass)
95
+ worker_options = get!(klass.bin_sym)
96
+
97
+ case worker_options
98
+ when Hash
99
+ worker_options
100
+ when true, nil
101
+ {}
102
+ when false
103
+ false
104
+ else
105
+ raise ConfigurationError, "Got #{worker_options.inspect} for #{klass.name} options"
106
+ end
107
+ end
108
+
109
+ def to_s
110
+ YAML.dump(HashHelpers.deep_stringify_keys(@options)).sub(/\A---\n/, '')
111
+ end
112
+
113
+ private
114
+
115
+ # http://stackoverflow.com/questions/891537/ruby-detect-number-of-cpus-installed
116
+ def processor_count
117
+ @processor_count ||= case host_os = RbConfig::CONFIG['host_os']
118
+ when /darwin9/
119
+ `hwprefs cpu_count`
120
+ when /darwin/
121
+ (`which hwprefs` != '') ? `hwprefs thread_count` : `sysctl -n hw.ncpu`
122
+ when /linux/
123
+ `grep -c processor /proc/cpuinfo`
124
+ when /freebsd/
125
+ `sysctl -n hw.ncpu`
126
+ when /mswin|mingw/
127
+ require 'win32ole'
128
+ wmi = WIN32OLE.connect('winmgmts://')
129
+ cpu = wmi.ExecQuery('select NumberOfLogicalProcessors from Win32_Processor')
130
+ cpu.to_enum.first.NumberOfLogicalProcessors
131
+ else
132
+ warn "Unknown architecture (#{host_os}) assuming one processor."
133
+ 1
134
+ end.to_i
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,3 @@
1
+ class ImageOptim
2
+ class ConfigurationError < StandardError; end
3
+ end
@@ -0,0 +1,27 @@
1
+ require 'image_optim/image_path'
2
+
3
+ class ImageOptim
4
+ class Handler
5
+ attr_reader :result
6
+ def initialize(original)
7
+ raise ArgumentError, 'original should respond to temp_path' unless original.respond_to?(:temp_path)
8
+
9
+ @original = original
10
+ @result = nil
11
+ end
12
+
13
+ def process
14
+ @src ||= @original
15
+ @dst ||= @original.temp_path
16
+
17
+ if yield @src, @dst
18
+ @result = @dst
19
+ if @src == @original
20
+ @src, @dst = @dst, nil
21
+ else
22
+ @src, @dst = @dst, @src
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,35 @@
1
+ class ImageOptim
2
+ module HashHelpers
3
+ class << self
4
+ def deep_transform_keys(hash, &block)
5
+ new_hash = {}
6
+ hash.each do |k, v|
7
+ new_hash[block.call(k)] = if v.is_a?(Hash)
8
+ deep_transform_keys(v, &block)
9
+ else
10
+ v
11
+ end
12
+ end
13
+ new_hash
14
+ end
15
+
16
+ def deep_stringify_keys(hash)
17
+ deep_transform_keys(hash, &:to_s)
18
+ end
19
+
20
+ def deep_symbolise_keys(hash)
21
+ deep_transform_keys(hash, &:to_sym)
22
+ end
23
+
24
+ def deep_merge(a, b)
25
+ a.merge(b) do |k, v_a, v_b|
26
+ if v_a.is_a?(Hash) && v_b.is_a?(Hash)
27
+ deep_merge(v_a, v_b)
28
+ else
29
+ v_b
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,10 +1,30 @@
1
1
  require 'fspath'
2
2
  require 'image_size'
3
3
 
4
- require 'image_optim'
5
-
6
4
  class ImageOptim
7
5
  class ImagePath < FSPath
6
+ class Optimized < DelegateClass(self)
7
+ def initialize(path, original_or_size = nil)
8
+ path = ImagePath.convert(path)
9
+ __setobj__(path)
10
+ if original_or_size
11
+ if original_or_size.is_a?(Integer)
12
+ @original = path
13
+ @original_size = original_or_size
14
+ else
15
+ @original = ImagePath.convert(original_or_size)
16
+ @original_size = @original.size
17
+ end
18
+ end
19
+ end
20
+
21
+ # Original path, use original_size to get its size as original can be overwritten
22
+ attr_reader :original
23
+
24
+ # Stored size of original
25
+ attr_reader :original_size
26
+ end
27
+
8
28
  # Get temp path for this file with same extension
9
29
  def temp_path(*args, &block)
10
30
  ext = extname
@@ -30,5 +50,10 @@ class ImageOptim
30
50
  def format
31
51
  open{ |f| ImageSize.new(f) }.format
32
52
  end
53
+
54
+ # Returns path if it is already an instance of this class otherwise new instance
55
+ def self.convert(path)
56
+ path.is_a?(self) ? path : new(path)
57
+ end
33
58
  end
34
59
  end
@@ -1,19 +1,7 @@
1
- require 'image_optim'
1
+ require 'image_optim/configuration_error'
2
2
 
3
3
  class ImageOptim
4
4
  module OptionHelpers
5
- # Remove option from hash and run through block or return default
6
- def get_option!(options, name, default)
7
- value = default
8
- if options.has_key?(name)
9
- value = options.delete(name)
10
- end
11
- if block_given?
12
- value = yield(value)
13
- end
14
- instance_variable_set("@#{name}", value)
15
- end
16
-
17
5
  # Ensure number is in range
18
6
  def self.limit_with_range(number, range)
19
7
  if range.include?(number)
@@ -26,12 +14,5 @@ class ImageOptim
26
14
  range.last
27
15
  end
28
16
  end
29
-
30
- # Raise unless all options are deleted
31
- def assert_options_empty!(options)
32
- unless options.empty?
33
- raise ConfigurationError, "unknown options #{options.inspect} for #{self}"
34
- end
35
- end
36
17
  end
37
18
  end
@@ -0,0 +1,19 @@
1
+ require 'image_optim'
2
+
3
+ class ImageOptim
4
+ class Railtie < Rails::Railtie
5
+ initializer 'image_optim.initializer' do |app|
6
+ if app.config.assets.compress && app.config.assets.image_optim != false
7
+ image_optim = ImageOptim.new
8
+
9
+ processor = proc do |context, data|
10
+ image_optim.optimize_image_data(data) || data
11
+ end
12
+
13
+ app.assets.register_preprocessor 'image/gif', :image_optim, &processor
14
+ app.assets.register_preprocessor 'image/jpeg', :image_optim, &processor
15
+ app.assets.register_preprocessor 'image/png', :image_optim, &processor
16
+ end
17
+ end
18
+ end
19
+ end