image_optim 0.6.0 → 0.7.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.
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