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 +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
|