image_optim 0.9.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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