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 +1 -1
- data/README.markdown +43 -19
- data/TODO +6 -1
- data/bin/image_optim +17 -11
- data/image_optim.gemspec +1 -1
- data/lib/image_optim.rb +86 -26
- data/lib/image_optim/image_path.rb +2 -0
- data/lib/image_optim/option_helpers.rb +5 -1
- data/lib/image_optim/worker.rb +36 -49
- data/lib/image_optim/worker/advpng.rb +22 -0
- data/lib/image_optim/worker/gifsicle.rb +22 -0
- data/lib/image_optim/worker/jpegoptim.rb +41 -0
- data/lib/image_optim/worker/jpegtran.rb +27 -0
- data/lib/image_optim/worker/optipng.rb +27 -0
- data/lib/image_optim/worker/pngcrush.rb +39 -0
- data/lib/image_optim/worker/pngout.rb +30 -0
- data/spec/image_optim_spec.rb +213 -92
- metadata +11 -12
- data/lib/image_optim/util.rb +0 -27
- data/lib/image_optim/workers/advpng.rb +0 -19
- data/lib/image_optim/workers/gifsicle.rb +0 -20
- data/lib/image_optim/workers/jpegoptim.rb +0 -50
- data/lib/image_optim/workers/jpegtran.rb +0 -25
- data/lib/image_optim/workers/optipng.rb +0 -27
- data/lib/image_optim/workers/pngcrush.rb +0 -37
- data/lib/image_optim/workers/pngout.rb +0 -27
data/LICENSE.txt
CHANGED
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
|
3
|
+
Optimize (lossless compress) images (jpeg, png, gif) using external utilities:
|
4
4
|
|
5
|
-
|
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
|
-
|
13
|
+
Based on [ImageOptim.app](http://imageoptim.com/).
|
14
|
+
|
15
|
+
## Gem installation
|
8
16
|
|
9
17
|
gem install image_optim
|
10
18
|
|
11
|
-
## Binaries
|
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.
|
35
|
-
tar
|
36
|
-
cd pngcrush-1.7.
|
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
|
61
|
+
### OS X: Macports
|
40
62
|
|
41
63
|
sudo port install advancecomp gifsicle jpegoptim jpeg optipng pngcrush
|
42
64
|
|
43
|
-
### OS X
|
65
|
+
### OS X: Brew
|
44
66
|
|
45
67
|
brew install advancecomp gifsicle jpegoptim jpeg optipng pngcrush
|
46
68
|
|
47
|
-
|
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
|
-
|
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
|
-
|
87
|
+
image_optim = ImageOptim.new
|
64
88
|
|
65
|
-
|
89
|
+
image_optim = ImageOptim.new(:pngout => false)
|
66
90
|
|
67
|
-
|
91
|
+
image_optim = ImageOptim.new(:nice => 20)
|
68
92
|
|
69
93
|
Optimize image getting temp path:
|
70
94
|
|
71
|
-
|
95
|
+
image_optim.optimize_image('a.png')
|
72
96
|
|
73
97
|
Optimize image in place:
|
74
98
|
|
75
|
-
|
99
|
+
image_optim.optimize_image!('b.jpg')
|
76
100
|
|
77
101
|
Multiple images:
|
78
102
|
|
79
|
-
|
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
|
-
|
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
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}
|
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("--
|
31
|
-
options[bin] =
|
38
|
+
op.on("--no-#{bin}", "disable #{bin} worker") do |enable|
|
39
|
+
options[bin] = enable
|
32
40
|
end
|
33
41
|
end
|
34
42
|
|
35
|
-
op.
|
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
|
-
|
70
|
+
image_optim = begin
|
65
71
|
ImageOptim.new(options)
|
66
|
-
rescue ImageOptim::ConfigurationError
|
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
|
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) &&
|
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 =
|
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.
|
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
|
-
|
4
|
-
|
5
|
-
|
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
|
10
|
+
class BinNotFoundError < StandardError; end
|
11
11
|
|
12
12
|
include OptionHelpers
|
13
13
|
|
14
|
-
#
|
15
|
-
attr_reader :
|
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
|
-
#
|
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}
|
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(:
|
41
|
+
# ImageOptim.new(:nice => 20)
|
41
42
|
def initialize(options = {})
|
42
|
-
|
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
|
-
|
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(
|
78
|
-
|
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(&:
|
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 =~ /^
|
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/
|
244
|
+
require "image_optim/worker/#{worker}"
|
185
245
|
end
|
@@ -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
|
-
|
10
|
+
end
|
11
|
+
if block_given?
|
12
|
+
value = yield(value)
|
9
13
|
end
|
10
14
|
instance_variable_set("@#{name}", value)
|
11
15
|
end
|