image_optim 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012 Ivan Kuchin
1
+ Copyright (c) 2012-2013 Ivan Kuchin
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.markdown CHANGED
@@ -1,14 +1,36 @@
1
1
  # image_optim
2
2
 
3
- Optimize (lossless compress) images (jpeg, png, gif) using external utilities (advpng, gifsicle, jpegoptim, jpegtran, optipng, pngcrush, pngout).
3
+ Optimize (lossless compress) images (jpeg, png, gif) using external utilities:
4
4
 
5
- Based on [ImageOptim.app](http://imageoptim.pornel.net/).
5
+ * [advpng](http://advancemame.sourceforge.net/doc-advpng.html) from [AdvanceCOMP](http://advancemame.sourceforge.net/comp-readme.html)
6
+ * [gifsicle](http://www.lcdf.org/gifsicle/)
7
+ * [jpegoptim](http://www.kokkonen.net/tjko/projects.html)
8
+ * jpegtran from [Independent JPEG Group's JPEG library](http://www.ijg.org/)
9
+ * [optipng](http://optipng.sourceforge.net/)
10
+ * [pngcrush](http://pmt.sourceforge.net/pngcrush/)
11
+ * [pngout](http://www.advsys.net/ken/util/pngout.htm)
6
12
 
7
- ## Gem Installation
13
+ Based on [ImageOptim.app](http://imageoptim.com/).
14
+
15
+ ## Gem installation
8
16
 
9
17
  gem install image_optim
10
18
 
11
- ## Binaries Installation
19
+ ## Binaries location
20
+
21
+ Simplest way for `image_optim` to locate binaries is to install them in common location present in `PATH` (see [Binaries installation](#binaries-installation)).
22
+
23
+ If you cannot install to common location, then install to custom one and add it to `PATH`.
24
+
25
+ Specify custom bin location using `XXX_BIN` environment variable (`JPEGOPTIM_BIN`, `OPTIPNG_BIN`, …).
26
+
27
+ Besides permanently setting environment variables in `~/.profile`, `~/.bash_profile`, `~/.bashrc`, `~/.zshrc`, … they can be set:
28
+
29
+ * before command: `PATH="/custom/location:$PATH" image_optim *.jpg`
30
+
31
+ * inside script: `ENV['PATH'] = "/custom/location:#{ENV['PATH']}"; ImageOptim.optimize_images([…])`
32
+
33
+ ## Binaries installation
12
34
 
13
35
  ### Linux - Debian/Ubuntu
14
36
 
@@ -31,20 +53,20 @@ You will also need to install `jpegoptim` and `pngcrush` from source:
31
53
  #### pngcrush
32
54
 
33
55
  cd /tmp
34
- curl -O http://iweb.dl.sourceforge.net/project/pmt/pngcrush/1.7.24/pngcrush-1.7.24.tar.bz2
35
- tar jxf pngcrush-1.7.24.tar.bz2
36
- cd pngcrush-1.7.24
56
+ curl -O http://iweb.dl.sourceforge.net/project/pmt/pngcrush/1.7.43/pngcrush-1.7.43.tar.gz
57
+ tar zxf pngcrush-1.7.43.tar.gz
58
+ cd pngcrush-1.7.43
37
59
  make && cp -f pngcrush /usr/local/bin
38
60
 
39
- ### OS X - Macports
61
+ ### OS X: Macports
40
62
 
41
63
  sudo port install advancecomp gifsicle jpegoptim jpeg optipng pngcrush
42
64
 
43
- ### OS X - Brew
65
+ ### OS X: Brew
44
66
 
45
67
  brew install advancecomp gifsicle jpegoptim jpeg optipng pngcrush
46
68
 
47
- ## pngout Installation (optional)
69
+ ### pngout installation (optional)
48
70
 
49
71
  You can install `pngout` by downloading and installing the [binary versions](http://www.jonof.id.au/kenutils).
50
72
 
@@ -52,38 +74,40 @@ _Note: pngout is free to use even in commercial soft, but you can not redistribu
52
74
 
53
75
  ## Usage
54
76
 
55
- In terminal:
77
+ ### From shell
56
78
 
57
79
  image_optim *.{jpg,png,gif}
58
80
 
59
81
  image_optim -h
60
82
 
83
+ ### From ruby
84
+
61
85
  Initilize optimizer (options are described in comments for ImageOptim, Worker and all workers):
62
86
 
63
- io = ImageOptim.new
87
+ image_optim = ImageOptim.new
64
88
 
65
- io = ImageOptim.new(:pngout => false)
89
+ image_optim = ImageOptim.new(:pngout => false)
66
90
 
67
- io = ImageOptim.new(:nice => 20)
91
+ image_optim = ImageOptim.new(:nice => 20)
68
92
 
69
93
  Optimize image getting temp path:
70
94
 
71
- io.optimize_image('a.png')
95
+ image_optim.optimize_image('a.png')
72
96
 
73
97
  Optimize image in place:
74
98
 
75
- io.optimize_image!('b.jpg')
99
+ image_optim.optimize_image!('b.jpg')
76
100
 
77
101
  Multiple images:
78
102
 
79
- io.optimize_images(Dir['*.png']) do |unoptimized, optimized|
103
+ image_optim.optimize_images(Dir['*.png']) do |unoptimized, optimized|
80
104
  if optimized
81
105
  puts "#{unoptimized} => #{optimized}"
82
106
  end
83
107
  end
84
108
 
85
- io.optimize_images!(Dir['*.*'])
109
+ image_optim.optimize_images!(Dir['*.*'])
86
110
 
87
111
  ## Copyright
88
112
 
89
- Copyright (c) 2012 Ivan Kuchin. See LICENSE.txt for details.
113
+ Copyright (c) 2012-2013 Ivan Kuchin. See LICENSE.txt for details.
data/TODO CHANGED
@@ -1,2 +1,7 @@
1
- autorotate jpeg based on exif
1
+ leave_color branch
2
+ autorotate jpeg based on exif (jhead -auotrot)
2
3
  timeout workers?
4
+ don't fail but warn on bins not present
5
+ better documentation
6
+ preserve time/attrs option?
7
+ jpegrescan
data/bin/image_optim CHANGED
@@ -10,13 +10,19 @@ options = {}
10
10
 
11
11
  option_parser = OptionParser.new do |op|
12
12
  op.banner = <<-TEXT
13
- #{op.program_name}, version #{ImageOptim.version}
13
+ #{op.program_name} v#{ImageOptim.version}
14
14
 
15
15
  Usege:
16
16
  #{op.program_name} [options] image_path …
17
17
 
18
18
  TEXT
19
19
 
20
+ op.on('-r', '-R', '--recursive', 'Recurively scan directories for images') do |recursive|
21
+ options[:recursive] = recursive
22
+ end
23
+
24
+ op.separator nil
25
+
20
26
  op.on('--[no-]threads NUMBER', Integer, 'Number of threads or disable (defaults to number of processors)') do |threads|
21
27
  options[:threads] = threads
22
28
  end
@@ -25,16 +31,16 @@ Usege:
25
31
  options[:nice] = nice
26
32
  end
27
33
 
34
+ op.separator nil
35
+
28
36
  ImageOptim::Worker.klasses.each do |klass|
29
37
  bin = klass.underscored_name.to_sym
30
- op.on("--[no-]#{bin} PATH", "#{bin} path or disable") do |path|
31
- options[bin] = path
38
+ op.on("--no-#{bin}", "disable #{bin} worker") do |enable|
39
+ options[bin] = enable
32
40
  end
33
41
  end
34
42
 
35
- op.on('-r', '-R', '--recursive', 'Scan directories') do |recursive|
36
- options[:recursive] = recursive
37
- end
43
+ op.separator nil
38
44
 
39
45
  op.on('-v', '--verbose', 'Verbose info') do |verbose|
40
46
  options[:verbose] = verbose
@@ -61,16 +67,16 @@ if ARGV.empty?
61
67
  abort "specify image paths to optimize\n\n#{option_parser.help}"
62
68
  else
63
69
  recursive = options.delete(:recursive)
64
- io = begin
70
+ image_optim = begin
65
71
  ImageOptim.new(options)
66
- rescue ImageOptim::ConfigurationError, ImageOptim::BinaryNotFoundError => e
72
+ rescue ImageOptim::ConfigurationError => e
67
73
  abort e
68
74
  end
69
75
 
70
76
  paths = []
71
77
  ARGV.each do |arg|
72
78
  if File.file?(arg)
73
- if io.optimizable?(arg)
79
+ if image_optim.optimizable?(arg)
74
80
  paths << arg
75
81
  else
76
82
  warn "#{arg} is not an image or there is no optimizer for it"
@@ -78,7 +84,7 @@ else
78
84
  else
79
85
  if recursive
80
86
  Find.find(arg) do |path|
81
- paths << path if File.file?(path) && io.optimizable?(path)
87
+ paths << path if File.file?(path) && image_optim.optimizable?(path)
82
88
  end
83
89
  else
84
90
  warn "#{arg} is not a file"
@@ -125,7 +131,7 @@ else
125
131
  '%5.2f%% %s' % [100 - 100.0 * dst_size / src_size, Space.space(src_size - dst_size)]
126
132
  end
127
133
 
128
- results = io.optimize_images(paths) do |src, dst|
134
+ results = image_optim.optimize_images(paths) do |src, dst|
129
135
  if dst
130
136
  src_size, dst_size = src.size, dst.size
131
137
  percent = size_percent(src_size, dst_size)
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.6.0'
5
+ s.version = '0.7.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']
data/lib/image_optim.rb CHANGED
@@ -1,35 +1,36 @@
1
1
  require 'in_threads'
2
+ require 'shellwords'
2
3
 
3
- class ImageOptim
4
- autoload :ImagePath, 'image_optim/image_path'
5
- autoload :OptionHelpers, 'image_optim/option_helpers'
6
- autoload :Util, 'image_optim/util'
7
- autoload :Worker, 'image_optim/worker'
4
+ require 'image_optim/image_path'
5
+ require 'image_optim/option_helpers'
6
+ require 'image_optim/worker'
8
7
 
8
+ class ImageOptim
9
9
  class ConfigurationError < StandardError; end
10
- class BinaryNotFoundError < StandardError; end
10
+ class BinNotFoundError < StandardError; end
11
11
 
12
12
  include OptionHelpers
13
13
 
14
- # Hash of initialized workers by format they apply to
15
- attr_reader :workers_by_format
14
+ # Nice level
15
+ attr_reader :nice
16
16
 
17
17
  # Number of threads to run with
18
18
  attr_reader :threads
19
19
 
20
+ # Verbose output?
21
+ def verbose?
22
+ @verbose
23
+ end
24
+
20
25
  # Initialize workers, specify options using worker underscored name:
21
26
  #
22
27
  # pass false to disable worker
23
28
  #
24
29
  # ImageOptim.new(:pngcrush => false)
25
30
  #
26
- # string to set binary
27
- #
28
- # ImageOptim.new(:pngout => '/special/path/bin/pngout123')
29
- #
30
- # or hash with options to worker and :bin specifying binary
31
+ # or hash with options to worker
31
32
  #
32
- # ImageOptim.new(:advpng => {:level => 3}, :optipng => {:level => 2}, :jpegoptim => {:bin => 'jpegoptim345'})
33
+ # ImageOptim.new(:advpng => {:level => 3}, :optipng => {:level => 2})
33
34
  #
34
35
  # use :threads to set number of parallel optimizers to run (passing true or nil determines number of processors, false disables parallel processing)
35
36
  #
@@ -37,9 +38,13 @@ class ImageOptim
37
38
  #
38
39
  # use :nice to specify optimizers nice level (true or nil makes it 10, false makes it 0)
39
40
  #
40
- # ImageOptim.new(:threads => 8)
41
+ # ImageOptim.new(:nice => 20)
41
42
  def initialize(options = {})
42
- nice = case nice = options.delete(:nice)
43
+ @resolved_bins = {}
44
+ @resolver_lock = Mutex.new
45
+
46
+ nice = options.delete(:nice)
47
+ @nice = case nice
43
48
  when true, nil
44
49
  10
45
50
  when false
@@ -51,7 +56,7 @@ class ImageOptim
51
56
  threads = options.delete(:threads)
52
57
  threads = case threads
53
58
  when true, nil
54
- Util.processor_count
59
+ processor_count
55
60
  when false
56
61
  1
57
62
  else
@@ -59,7 +64,7 @@ class ImageOptim
59
64
  end
60
65
  @threads = limit_with_range(threads, 1..16)
61
66
 
62
- verbose = options.delete(:verbose)
67
+ @verbose = !!options.delete(:verbose)
63
68
 
64
69
  @workers_by_format = {}
65
70
  Worker.klasses.each do |klass|
@@ -69,19 +74,17 @@ class ImageOptim
69
74
  worker_options = {}
70
75
  when false
71
76
  next
72
- when String
73
- worker_options = {:bin => worker_options}
74
77
  else
75
78
  raise ConfigurationError, "Got #{worker_options.inspect} for #{klass.name} options"
76
79
  end
77
- worker = klass.new({:nice => nice, :verbose => verbose}.merge(worker_options))
78
- klass.image_formats.each do |format|
80
+ worker = klass.new(self, worker_options)
81
+ worker.image_formats.each do |format|
79
82
  @workers_by_format[format] ||= []
80
83
  @workers_by_format[format] << worker
81
84
  end
82
85
  end
83
86
  @workers_by_format.each do |format, workers|
84
- workers.replace workers.sort_by(&:run_priority)
87
+ workers.replace workers.sort_by(&:run_order) # There is no sort_by! in ruby 1.8
85
88
  end
86
89
 
87
90
  assert_options_empty!(options)
@@ -138,23 +141,52 @@ class ImageOptim
138
141
 
139
142
  # Optimization methods with default options
140
143
  def self.method_missing(method, *args, &block)
141
- if method.to_s =~ /^optimize/
144
+ if method.to_s =~ /^optimize_images?\!?$/
142
145
  new.send(method, *args, &block)
143
146
  else
144
147
  super
145
148
  end
146
149
  end
147
150
 
151
+ # Version of image_optim gem spec loaded
148
152
  def self.version
149
153
  Gem.loaded_specs['image_optim'].version.to_s rescue nil
150
154
  end
151
155
 
156
+ # Are there workers for file at path?
152
157
  def optimizable?(path)
153
158
  !!workers_for_image(path)
154
159
  end
155
160
 
161
+ # Temp directory for symlinks to bins with path coming from ENV
162
+ attr_reader :resolve_dir
163
+
164
+ # Check existance of binary, create symlink if ENV contains path for key XXX_BIN where XXX is upper case bin name
165
+ def resolve_bin!(bin)
166
+ bin = bin.to_sym
167
+ @resolved_bins.include?(bin) || @resolver_lock.synchronize do
168
+ @resolved_bins.include?(bin) || begin
169
+ if path = ENV["#{bin}_bin".upcase]
170
+ unless @resolve_dir
171
+ @resolve_dir = FSPath.temp_dir
172
+ at_exit{ FileUtils.remove_entry_secure @resolve_dir }
173
+ end
174
+ symlink = @resolve_dir / bin
175
+ symlink.make_symlink(File.expand_path(path))
176
+ at_exit{ symlink.unlink }
177
+
178
+ @resolved_bins[bin] = bin_accessible?(symlink)
179
+ else
180
+ @resolved_bins[bin] = bin_accessible?(bin)
181
+ end
182
+ end
183
+ end
184
+ @resolved_bins[bin] or raise BinNotFoundError, "`#{bin}` not found"
185
+ end
186
+
156
187
  private
157
188
 
189
+ # Run method for each path and yield each path and result if block given
158
190
  def run_method_for(paths, method_name, &block)
159
191
  apply_threading(paths).map do |path|
160
192
  path = ImagePath.new(path)
@@ -167,13 +199,41 @@ private
167
199
  end
168
200
  end
169
201
 
202
+ # Apply threading if threading is allowed and array is longer than 1
170
203
  def apply_threading(array)
171
- if threads > 1
204
+ if threads > 1 && array.length > 1
172
205
  array.in_threads(threads)
173
206
  else
174
207
  array
175
208
  end
176
209
  end
210
+
211
+ # Check if bin can be accessed
212
+ def bin_accessible?(bin)
213
+ `which #{bin.to_s.shellescape}` != ''
214
+ end
215
+
216
+ # http://stackoverflow.com/questions/891537/ruby-detect-number-of-cpus-installed
217
+ def processor_count
218
+ @processor_count ||= case host_os = RbConfig::CONFIG['host_os']
219
+ when /darwin9/
220
+ `hwprefs cpu_count`
221
+ when /darwin/
222
+ (`which hwprefs` != '') ? `hwprefs thread_count` : `sysctl -n hw.ncpu`
223
+ when /linux/
224
+ `grep -c processor /proc/cpuinfo`
225
+ when /freebsd/
226
+ `sysctl -n hw.ncpu`
227
+ when /mswin|mingw/
228
+ require 'win32ole'
229
+ wmi = WIN32OLE.connect('winmgmts://')
230
+ cpu = wmi.ExecQuery('select NumberOfLogicalProcessors from Win32_Processor')
231
+ cpu.to_enum.first.NumberOfLogicalProcessors
232
+ else
233
+ warn "Unknown architecture (#{host_os}) assuming one processor."
234
+ 1
235
+ end.to_i
236
+ end
177
237
  end
178
238
 
179
239
  %w[
@@ -181,5 +241,5 @@ end
181
241
  jpegoptim jpegtran
182
242
  gifsicle
183
243
  ].each do |worker|
184
- require "image_optim/workers/#{worker}"
244
+ require "image_optim/worker/#{worker}"
185
245
  end
@@ -1,6 +1,8 @@
1
1
  require 'fspath'
2
2
  require 'image_size'
3
3
 
4
+ require 'image_optim'
5
+
4
6
  class ImageOptim
5
7
  class ImagePath < FSPath
6
8
  # Get temp path for this file with same extension
@@ -1,3 +1,5 @@
1
+ require 'image_optim'
2
+
1
3
  class ImageOptim
2
4
  module OptionHelpers
3
5
  # Remove option from hash and run through block or return default
@@ -5,7 +7,9 @@ class ImageOptim
5
7
  value = default
6
8
  if options.has_key?(name)
7
9
  value = options.delete(name)
8
- value = yield(value) if block_given?
10
+ end
11
+ if block_given?
12
+ value = yield(value)
9
13
  end
10
14
  instance_variable_set("@#{name}", value)
11
15
  end