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.
@@ -0,0 +1,107 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'image_optim'
4
+ require 'image_optim/hash_helpers'
5
+ require 'image_optim/true_false_nil'
6
+ require 'progress'
7
+ require 'optparse'
8
+ require 'find'
9
+ require 'yaml'
10
+
11
+ class ImageOptim
12
+ class Runner
13
+ module Space
14
+ SIZE_SYMBOLS = %w[B K M G T P E].freeze
15
+ PRECISION = 1
16
+ LENGTH = 4 + PRECISION + 1
17
+
18
+ EMPTY_SPACE = ' ' * LENGTH
19
+
20
+ class << self
21
+ attr_writer :base10
22
+ def denominator
23
+ @denominator ||= @base10 ? 1000.0 : 1024.0
24
+ end
25
+
26
+ def space(size)
27
+ case size
28
+ when 0, nil
29
+ EMPTY_SPACE
30
+ else
31
+ log_denominator = Math.log(size) / Math.log(denominator)
32
+ degree = [log_denominator.floor, SIZE_SYMBOLS.length - 1].min
33
+ number = size / (denominator ** degree)
34
+ "#{degree == 0 ? number.to_i : "%.#{PRECISION}f" % number}#{SIZE_SYMBOLS[degree]}".rjust(LENGTH)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def initialize(args, options)
41
+ raise 'specify paths to optimize' if args.empty?
42
+ options = HashHelpers.deep_symbolise_keys(options)
43
+ @recursive = options.delete(:recursive)
44
+ @image_optim = ImageOptim.new(options)
45
+ @files = find_files(args)
46
+ end
47
+
48
+ def run!
49
+ unless @files.empty?
50
+ lines, original_sizes, optimized_sizes =
51
+ @image_optim.optimize_images!(@files.with_progress('optimizing')) do |original, optimized|
52
+ original_size = optimized ? optimized.original_size : original.size
53
+ optimized_size = optimized ? optimized.size : original.size
54
+ ["#{size_percent(original_size, optimized_size)} #{original}", original_size, optimized_size]
55
+ end.transpose
56
+
57
+ puts lines, "Total: #{size_percent(original_sizes.inject(:+), optimized_sizes.inject(:+))}"
58
+ end
59
+
60
+ !warnings?
61
+ end
62
+
63
+ def warnings?
64
+ !!@warnings
65
+ end
66
+
67
+ def self.run!(args, options)
68
+ new(args, options).run!
69
+ end
70
+
71
+ private
72
+
73
+ def find_files(args)
74
+ files = []
75
+ args.each do |arg|
76
+ if File.file?(arg)
77
+ if @image_optim.optimizable?(arg)
78
+ files << arg
79
+ else
80
+ warning "#{arg} is not an image or there is no optimizer for it"
81
+ end
82
+ elsif @recursive && File.directory?(arg)
83
+ Find.find(arg) do |path|
84
+ files << path if File.file?(path) && @image_optim.optimizable?(path)
85
+ end
86
+ else
87
+ warning "#{arg} does not exist"
88
+ end
89
+ end
90
+ files
91
+ end
92
+
93
+ def warning(message)
94
+ @warnings = true
95
+ warn message
96
+ end
97
+
98
+ def size_percent(size_a, size_b)
99
+ if size_a == size_b
100
+ "------ #{Space::EMPTY_SPACE}"
101
+ else
102
+ '%5.2f%% %s' % [100 - 100.0 * size_b / size_a, Space.space(size_a - size_b)]
103
+ end
104
+ end
105
+
106
+ end
107
+ end
@@ -0,0 +1,3 @@
1
+ class ImageOptim
2
+ class TrueFalseNil; end
3
+ end
@@ -1,4 +1,5 @@
1
1
  require 'image_optim/worker'
2
+ require 'image_optim/option_helpers'
2
3
 
3
4
  class ImageOptim
4
5
  class Worker
@@ -1,4 +1,5 @@
1
1
  require 'image_optim/worker'
2
+ require 'image_optim/option_helpers'
2
3
 
3
4
  class ImageOptim
4
5
  class Worker
@@ -1,4 +1,6 @@
1
1
  require 'image_optim/worker'
2
+ require 'image_optim/option_helpers'
3
+ require 'image_optim/true_false_nil'
2
4
 
3
5
  class ImageOptim
4
6
  class Worker
@@ -1,4 +1,5 @@
1
1
  require 'image_optim/worker'
2
+ require 'image_optim/option_helpers'
2
3
 
3
4
  class ImageOptim
4
5
  class Worker
@@ -1,9 +1,9 @@
1
1
  # encoding: UTF-8
2
2
 
3
+ require 'image_optim/option_definition'
4
+ require 'image_optim/option_helpers'
3
5
  require 'shellwords'
4
6
 
5
- require 'image_optim'
6
-
7
7
  class ImageOptim
8
8
  class Worker
9
9
  class << self
@@ -32,15 +32,22 @@ class ImageOptim
32
32
  end
33
33
  end
34
34
 
35
- include OptionHelpers
36
-
37
35
  # Configure (raises on extra options)
38
36
  def initialize(image_optim, options = {})
39
37
  @image_optim = image_optim
40
38
  self.class.option_definitions.each do |option_definition|
41
- get_option!(options, option_definition.name, option_definition.default, &option_definition.proc)
39
+ value = if options.has_key?(option_definition.name)
40
+ options[option_definition.name]
41
+ else
42
+ option_definition.default
43
+ end
44
+ if option_definition.proc
45
+ value = option_definition.proc[value]
46
+ end
47
+ instance_variable_set("@#{option_definition.name}", value)
42
48
  end
43
- assert_options_empty!(options)
49
+
50
+ assert_no_unknown_options!(options)
44
51
  end
45
52
 
46
53
  # List of formats which worker can optimize
@@ -55,6 +62,10 @@ class ImageOptim
55
62
  0
56
63
  end
57
64
 
65
+ def <=>(other)
66
+ run_order <=> other.run_order
67
+ end
68
+
58
69
  # Check if operation resulted in optimized file
59
70
  def optimized?(src, dst)
60
71
  dst.size? && dst.size < src.size
@@ -62,6 +73,14 @@ class ImageOptim
62
73
 
63
74
  private
64
75
 
76
+ def assert_no_unknown_options!(options)
77
+ known_keys = self.class.option_definitions.map(&:name)
78
+ unknown_options = options.reject{ |key, value| known_keys.include?(key) }
79
+ unless unknown_options.empty?
80
+ raise ConfigurationError, "unknown options #{unknown_options.inspect} for #{self}"
81
+ end
82
+ end
83
+
65
84
  # Forward bin resolving to image_optim
66
85
  def resolve_bin!(bin)
67
86
  @image_optim.resolve_bin!(bin)
data/lib/image_optim.rb CHANGED
@@ -1,19 +1,12 @@
1
- require 'in_threads'
2
- require 'shellwords'
3
-
1
+ require 'image_optim/bin_resolver'
2
+ require 'image_optim/config'
3
+ require 'image_optim/handler'
4
4
  require 'image_optim/image_path'
5
- require 'image_optim/option_helpers'
6
- require 'image_optim/option_definition'
7
5
  require 'image_optim/worker'
6
+ require 'in_threads'
7
+ require 'shellwords'
8
8
 
9
9
  class ImageOptim
10
- class ConfigurationError < StandardError; end
11
- class BinNotFoundError < StandardError; end
12
-
13
- class TrueFalseNil; end
14
-
15
- include OptionHelpers
16
-
17
10
  # Nice level
18
11
  attr_reader :nice
19
12
 
@@ -43,88 +36,71 @@ class ImageOptim
43
36
  #
44
37
  # ImageOptim.new(:nice => 20)
45
38
  def initialize(options = {})
46
- @resolved_bins = {}
47
- @resolver_lock = Mutex.new
48
-
49
- nice = options.delete(:nice)
50
- @nice = case nice
51
- when true, nil
52
- 10
53
- when false
54
- 0
55
- else
56
- nice.to_i
57
- end
39
+ @bin_resolver = BinResolver.new
58
40
 
59
- threads = options.delete(:threads)
60
- threads = case threads
61
- when true, nil
62
- processor_count
63
- when false
64
- 1
65
- else
66
- threads.to_i
67
- end
68
- @threads = OptionHelpers.limit_with_range(threads, 1..16)
69
-
70
- @verbose = !!options.delete(:verbose)
41
+ config = Config.new(options)
42
+ @nice = config.nice
43
+ @threads = config.threads
44
+ @verbose = config.verbose
71
45
 
72
46
  @workers_by_format = {}
73
47
  Worker.klasses.each do |klass|
74
- case worker_options = options.delete(klass.bin_sym)
75
- when Hash
76
- when true, nil
77
- worker_options = {}
78
- when false
79
- next
80
- else
81
- raise ConfigurationError, "Got #{worker_options.inspect} for #{klass.name} options"
82
- end
83
- worker = klass.new(self, worker_options)
84
- worker.image_formats.each do |format|
85
- @workers_by_format[format] ||= []
86
- @workers_by_format[format] << worker
48
+ if worker_options = config.for_worker(klass)
49
+ worker = klass.new(self, worker_options)
50
+ worker.image_formats.each do |format|
51
+ @workers_by_format[format] ||= []
52
+ @workers_by_format[format] << worker
53
+ end
87
54
  end
88
55
  end
89
- @workers_by_format.each do |format, workers|
90
- workers.replace workers.sort_by(&:run_order) # There is no sort_by! in ruby 1.8
91
- end
56
+ @workers_by_format.values.each(&:sort!)
92
57
 
93
- assert_options_empty!(options)
58
+ config.assert_no_unused_options!
59
+
60
+ puts config if verbose?
94
61
  end
95
62
 
96
63
  # Get workers for image
97
64
  def workers_for_image(path)
98
- @workers_by_format[ImagePath.new(path).format]
65
+ @workers_by_format[ImagePath.convert(path).format]
99
66
  end
100
67
 
101
- # Optimize one file, return new path or nil if optimization failed
68
+ # Optimize one file, return new path as OptimizedImagePath or nil if optimization failed
102
69
  def optimize_image(original)
103
- original = ImagePath.new(original)
70
+ original = ImagePath.convert(original)
104
71
  if workers = workers_for_image(original)
105
- result = nil
106
- ts = [original, original.temp_path]
72
+ handler = Handler.new(original)
107
73
  workers.each do |worker|
108
- if result && ts.length < 3
109
- ts << original.temp_path
110
- end
111
- if worker.optimize(*ts.last(2))
112
- result = ts.last
113
- if ts.length == 3
114
- ts[-2, 2] = ts[-1], ts[-2]
115
- end
74
+ handler.process do |src, dst|
75
+ worker.optimize(src, dst)
116
76
  end
117
77
  end
118
- result
78
+ if handler.result
79
+ ImagePath::Optimized.new(handler.result, original)
80
+ end
119
81
  end
120
82
  end
121
83
 
122
- # Optimize one file in place, return optimization status
84
+ # Optimize one file in place, return original as OptimizedImagePath or nil if optimization failed
123
85
  def optimize_image!(original)
124
- original = ImagePath.new(original)
86
+ original = ImagePath.convert(original)
125
87
  if result = optimize_image(original)
126
88
  result.replace(original)
127
- true
89
+ ImagePath::Optimized.new(original, result.original_size)
90
+ end
91
+ end
92
+
93
+ # Optimize image data, return new data or nil if optimization failed
94
+ def optimize_image_data(original_data)
95
+ format = ImageSize.new(original_data).format
96
+ ImagePath.temp_file %W[image_optim .#{format}] do |temp|
97
+ temp.binmode
98
+ temp.write(original_data)
99
+ temp.close
100
+
101
+ if result = optimize_image(temp.path)
102
+ result.read
103
+ end
128
104
  end
129
105
  end
130
106
 
@@ -132,19 +108,26 @@ class ImageOptim
132
108
  # if block given yields path and result for each image and returns array of yield results
133
109
  # else return array of results
134
110
  def optimize_images(paths, &block)
135
- run_method_for(paths, :optimize_image, &block)
111
+ run_method_for(paths.map{ |path| ImagePath.convert(path) }, :optimize_image, &block)
136
112
  end
137
113
 
138
114
  # Optimize multiple images in place
139
115
  # if block given yields path and result for each image and returns array of yield results
140
116
  # else return array of results
141
117
  def optimize_images!(paths, &block)
142
- run_method_for(paths, :optimize_image!, &block)
118
+ run_method_for(paths.map{ |path| ImagePath.convert(path) }, :optimize_image!, &block)
119
+ end
120
+
121
+ # Optimize multiple image datas
122
+ # if block given yields original and result for each image data and returns array of yield results
123
+ # else return array of results
124
+ def optimize_images_data(datas, &block)
125
+ run_method_for(datas, :optimize_image_data, &block)
143
126
  end
144
127
 
145
128
  # Optimization methods with default options
146
129
  def self.method_missing(method, *args, &block)
147
- if method.to_s =~ /^optimize_images?\!?$/
130
+ if method_defined?(method) && method.to_s =~ /^optimize_image/
148
131
  new.send(method, *args, &block)
149
132
  else
150
133
  super
@@ -161,37 +144,14 @@ class ImageOptim
161
144
  !!workers_for_image(path)
162
145
  end
163
146
 
164
- # Temp directory for symlinks to bins with path coming from ENV
165
- attr_reader :resolve_dir
166
-
167
147
  # Check existance of binary, create symlink if ENV contains path for key XXX_BIN where XXX is upper case bin name
168
148
  def resolve_bin!(bin)
169
- bin = bin.to_sym
170
- @resolved_bins.include?(bin) || @resolver_lock.synchronize do
171
- @resolved_bins.include?(bin) || begin
172
- if path = ENV["#{bin}_bin".upcase]
173
- unless @resolve_dir
174
- @resolve_dir = FSPath.temp_dir
175
- at_exit{ FileUtils.remove_entry_secure @resolve_dir }
176
- end
177
- symlink = @resolve_dir / bin
178
- symlink.make_symlink(File.expand_path(path))
179
- at_exit{ symlink.unlink }
180
-
181
- @resolved_bins[bin] = bin_accessible?(symlink)
182
- else
183
- @resolved_bins[bin] = bin_accessible?(bin)
184
- end
185
- end
186
- end
187
- @resolved_bins[bin] or raise BinNotFoundError, "`#{bin}` not found"
149
+ @bin_resolver.resolve!(bin)
188
150
  end
189
151
 
190
- VENDOR_PATH = File.expand_path('../../vendor', __FILE__)
191
-
192
152
  # Join resolve_dir, default path and vendor path for PATH environment variable
193
153
  def env_path
194
- "#{resolve_dir}:#{ENV['PATH']}:#{VENDOR_PATH}"
154
+ @bin_resolver.env_path
195
155
  end
196
156
 
197
157
  private
@@ -199,7 +159,6 @@ private
199
159
  # Run method for each path and yield each path and result if block given
200
160
  def run_method_for(paths, method_name, &block)
201
161
  apply_threading(paths).map do |path|
202
- path = ImagePath.new(path)
203
162
  result = send(method_name, path)
204
163
  if block
205
164
  block.call(path, result)
@@ -217,33 +176,6 @@ private
217
176
  enum
218
177
  end
219
178
  end
220
-
221
- # Check if bin can be accessed
222
- def bin_accessible?(bin)
223
- `env PATH=#{env_path.shellescape} which #{bin.to_s.shellescape}` != ''
224
- end
225
-
226
- # http://stackoverflow.com/questions/891537/ruby-detect-number-of-cpus-installed
227
- def processor_count
228
- @processor_count ||= case host_os = RbConfig::CONFIG['host_os']
229
- when /darwin9/
230
- `hwprefs cpu_count`
231
- when /darwin/
232
- (`which hwprefs` != '') ? `hwprefs thread_count` : `sysctl -n hw.ncpu`
233
- when /linux/
234
- `grep -c processor /proc/cpuinfo`
235
- when /freebsd/
236
- `sysctl -n hw.ncpu`
237
- when /mswin|mingw/
238
- require 'win32ole'
239
- wmi = WIN32OLE.connect('winmgmts://')
240
- cpu = wmi.ExecQuery('select NumberOfLogicalProcessors from Win32_Processor')
241
- cpu.to_enum.first.NumberOfLogicalProcessors
242
- else
243
- warn "Unknown architecture (#{host_os}) assuming one processor."
244
- 1
245
- end.to_i
246
- end
247
179
  end
248
180
 
249
181
  %w[
@@ -253,3 +185,5 @@ end
253
185
  ].each do |worker|
254
186
  require "image_optim/worker/#{worker}"
255
187
  end
188
+
189
+ require 'image_optim/railtie' if defined?(Rails)
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ $:.unshift File.expand_path('../../lib', __FILE__)
5
+
6
+ require 'image_optim'
7
+
8
+ README_FILE = File.expand_path('../../README.markdown', __FILE__)
9
+ BEGIN_MARKER = '<!---<worker-options>-->'
10
+ END_MARKER = '<!---</worker-options>-->'
11
+
12
+ def worker_options
13
+ io = StringIO.new
14
+
15
+ ImageOptim::Worker.klasses.each_with_index do |klass, i|
16
+ unless klass.option_definitions.empty?
17
+ io.puts "### #{klass.bin_sym}"
18
+ klass.option_definitions.each do |option_definition|
19
+ io.puts "* `:#{option_definition.name}` — #{option_definition.description} *(defaults to #{option_definition.default})*"
20
+ end
21
+ io.puts
22
+ end
23
+ end
24
+
25
+ io.string
26
+ end
27
+
28
+ readme = File.read(README_FILE)
29
+
30
+ if readme.sub!(/#{Regexp.escape(BEGIN_MARKER)}.*#{Regexp.escape(END_MARKER)}/m, "#{BEGIN_MARKER}\n\n#{worker_options.strip}\n\n#{END_MARKER}")
31
+ File.open(README_FILE, 'w') do |f|
32
+ f.write readme
33
+ end
34
+ else
35
+ abort "Did not update worker options"
36
+ end
@@ -0,0 +1,92 @@
1
+ $:.unshift File.expand_path('../../../lib', __FILE__)
2
+ require 'rspec'
3
+ require 'image_optim/bin_resolver'
4
+
5
+ def with_env(key, value)
6
+ saved, ENV[key] = ENV[key], value
7
+ yield
8
+ ensure
9
+ ENV[key] = saved
10
+ end
11
+
12
+ describe ImageOptim::BinResolver do
13
+ it "should resolve bin in path" do
14
+ with_env 'LS_BIN', nil do
15
+ resolver = ImageOptim::BinResolver.new
16
+ resolver.should_receive(:accessible?).with(:ls).once.and_return(true)
17
+ FSPath.should_not_receive(:temp_dir)
18
+
19
+ 5.times do
20
+ resolver.resolve!(:ls).should be_true
21
+ end
22
+ end
23
+ end
24
+
25
+ it "should resolve bin specified in ENV" do
26
+ path = (FSPath(__FILE__).dirname / '../bin/image_optim').relative_path_from(Dir.pwd).to_s
27
+ with_env 'IMAGE_OPTIM_BIN', path do
28
+ tmpdir = double(:tmpdir)
29
+ symlink = double(:symlink)
30
+
31
+ resolver = ImageOptim::BinResolver.new
32
+ resolver.should_receive(:accessible?).with(symlink).once.and_return(true)
33
+ FSPath.should_receive(:temp_dir).once.and_return(tmpdir)
34
+ tmpdir.should_receive(:/).with(:image_optim).once.and_return(symlink)
35
+ symlink.should_receive(:make_symlink).with(File.expand_path(path)).once
36
+
37
+ at_exit_blocks = []
38
+ resolver.should_receive(:at_exit).once do |&block|
39
+ at_exit_blocks.unshift(block)
40
+ end
41
+
42
+ 5.times do
43
+ resolver.resolve!(:image_optim).should be_true
44
+ end
45
+
46
+ FileUtils.should_receive(:remove_entry_secure).with(tmpdir)
47
+ at_exit_blocks.each(&:call)
48
+ end
49
+ end
50
+
51
+ it "should raise on failure to resolve bin" do
52
+ with_env 'SHOULD_NOT_EXIST_BIN', nil do
53
+ resolver = ImageOptim::BinResolver.new
54
+ resolver.should_receive(:accessible?).with(:should_not_exist).once.and_return(false)
55
+ FSPath.should_not_receive(:temp_dir)
56
+
57
+ 5.times do
58
+ expect do
59
+ resolver.resolve!(:should_not_exist)
60
+ end.to raise_error ImageOptim::BinNotFoundError
61
+ end
62
+ end
63
+ end
64
+
65
+ it "should raise on failure to resolve bin specified in ENV" do
66
+ path = (FSPath(__FILE__).dirname / '../bin/should_not_exist_bin').relative_path_from(Dir.pwd).to_s
67
+ with_env 'SHOULD_NOT_EXIST_BIN', path do
68
+ tmpdir = double(:tmpdir)
69
+ symlink = double(:symlink)
70
+
71
+ resolver = ImageOptim::BinResolver.new
72
+ resolver.should_receive(:accessible?).with(symlink).once.and_return(false)
73
+ FSPath.should_receive(:temp_dir).once.and_return(tmpdir)
74
+ tmpdir.should_receive(:/).with(:should_not_exist).once.and_return(symlink)
75
+ symlink.should_receive(:make_symlink).with(File.expand_path(path)).once
76
+
77
+ at_exit_blocks = []
78
+ resolver.should_receive(:at_exit).once do |&block|
79
+ at_exit_blocks.unshift(block)
80
+ end
81
+
82
+ 5.times do
83
+ expect do
84
+ resolver.resolve!(:should_not_exist)
85
+ end.to raise_error ImageOptim::BinNotFoundError
86
+ end
87
+
88
+ FileUtils.should_receive(:remove_entry_secure).with(tmpdir)
89
+ at_exit_blocks.each(&:call)
90
+ end
91
+ end
92
+ end