image_optim 0.1.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/.gitignore +12 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +9 -0
- data/TODO +3 -0
- data/bin/image_optim +61 -0
- data/image_optim.gemspec +23 -0
- data/lib/image_optim.rb +141 -0
- data/lib/image_optim/image_path.rb +22 -0
- data/lib/image_optim/option_helpers.rb +33 -0
- data/lib/image_optim/util.rb +34 -0
- data/lib/image_optim/worker.rb +67 -0
- data/lib/image_optim/workers/advpng.rb +19 -0
- data/lib/image_optim/workers/gifsicle.rb +20 -0
- data/lib/image_optim/workers/jpegoptim.rb +50 -0
- data/lib/image_optim/workers/jpegtran.rb +25 -0
- data/lib/image_optim/workers/optipng.rb +27 -0
- data/lib/image_optim/workers/pngcrush.rb +37 -0
- data/lib/image_optim/workers/pngout.rb +27 -0
- data/spec/image_optim_spec.rb +74 -0
- data/spec/images/comparison.png +0 -0
- data/spec/images/decompressed.jpeg +0 -0
- data/spec/images/icecream.gif +0 -0
- data/spec/images/image.jpg +0 -0
- data/spec/images/lena.jpg +0 -0
- data/spec/images/transparency1.png +0 -0
- data/spec/images/transparency2.png +0 -0
- data/spec/images/vergroessert.jpg +0 -0
- metadata +177 -0
data/.gitignore
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Ivan Kuchin
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# image_optim
|
2
|
+
|
3
|
+
Optimize images (jpeg, png, gif) using external utilities (advpng, gifsicle, jpegoptim, jpegtran, optipng, pngcrush, pngout).
|
4
|
+
|
5
|
+
Based on [ImageOptim.app](http://imageoptim.pornel.net/).
|
6
|
+
|
7
|
+
## Copyright
|
8
|
+
|
9
|
+
Copyright (c) 2012 Ivan Kuchin. See LICENSE.txt for details.
|
data/TODO
ADDED
data/bin/image_optim
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# encoding: UTF-8
|
3
|
+
|
4
|
+
require 'image_optim'
|
5
|
+
require 'progress'
|
6
|
+
require 'optparse'
|
7
|
+
|
8
|
+
options = {}
|
9
|
+
|
10
|
+
option_parser = OptionParser.new do |op|
|
11
|
+
op.banner = <<-TEXT
|
12
|
+
#{op.program_name}, version #{ImageOptim.version}
|
13
|
+
|
14
|
+
Usege:
|
15
|
+
#{op.program_name} [options] image_path …
|
16
|
+
|
17
|
+
TEXT
|
18
|
+
|
19
|
+
op.on('--[no-]threads NUMBER', Integer, 'Number of threads or disable (defaults to number of processors)') do |threads|
|
20
|
+
options[:threads] = threads
|
21
|
+
end
|
22
|
+
|
23
|
+
ImageOptim::Worker.klasses.each do |klass|
|
24
|
+
bin = klass.underscored_name.to_sym
|
25
|
+
op.on("--[no-]#{bin} PATH", "#{bin} path or disable") do |path|
|
26
|
+
options[bin] = path
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
op.on_tail('-h', '--help', 'Show full help') do
|
31
|
+
puts option_parser.help
|
32
|
+
exit
|
33
|
+
end
|
34
|
+
|
35
|
+
op.on_tail('-v', '--version', 'Show version') do
|
36
|
+
puts ImageOptim.version
|
37
|
+
exit
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
begin
|
42
|
+
option_parser.parse!
|
43
|
+
rescue OptionParser::ParseError => e
|
44
|
+
abort "#{e.to_s}\n\n#{option_parser.help}"
|
45
|
+
end
|
46
|
+
|
47
|
+
if ARGV.empty?
|
48
|
+
abort "specify image paths to optimize\n\n#{option_parser.help}"
|
49
|
+
else
|
50
|
+
io = ImageOptim.new(options)
|
51
|
+
paths = ARGV
|
52
|
+
paths = paths.with_progress('optimizing') if paths.length > 1
|
53
|
+
|
54
|
+
lines = paths.map do |path|
|
55
|
+
before = File.size(path)
|
56
|
+
result = io.optimize_image!(path)
|
57
|
+
after = File.size(path)
|
58
|
+
"#{result ? '%5.2f%%' % (100.0 * after / before) : '--.--%'} #{path}"
|
59
|
+
end
|
60
|
+
puts lines
|
61
|
+
end
|
data/image_optim.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = 'image_optim'
|
5
|
+
s.version = '0.1.0'
|
6
|
+
s.summary = %q{Optimize images (jpeg, png, gif) using external utilities (advpng, gifsicle, jpegoptim, jpegtran, optipng, pngcrush, pngout)}
|
7
|
+
s.homepage = "http://github.com/toy/#{s.name}"
|
8
|
+
s.authors = ['Ivan Kuchin']
|
9
|
+
s.license = 'MIT'
|
10
|
+
|
11
|
+
s.rubyforge_project = s.name
|
12
|
+
|
13
|
+
s.files = `git ls-files`.split("\n")
|
14
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
15
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
16
|
+
s.require_paths = %w[lib]
|
17
|
+
|
18
|
+
s.add_dependency 'fspath', '~> 2.0.1'
|
19
|
+
s.add_dependency 'image_size', '~> 1.0.4'
|
20
|
+
s.add_dependency 'progress', '~> 2.4.0'
|
21
|
+
s.add_dependency 'in_threads', '~> 1.1.1'
|
22
|
+
s.add_development_dependency 'rspec'
|
23
|
+
end
|
data/lib/image_optim.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'in_threads'
|
2
|
+
|
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'
|
8
|
+
|
9
|
+
include OptionHelpers
|
10
|
+
|
11
|
+
# Hash of initialized workers by format they apply to
|
12
|
+
attr_reader :workers_by_format
|
13
|
+
|
14
|
+
# Number of threads to run with
|
15
|
+
attr_reader :threads
|
16
|
+
|
17
|
+
# Initialize workers, specify options using worker underscored name:
|
18
|
+
#
|
19
|
+
# pass false to disable worker
|
20
|
+
#
|
21
|
+
# ImageOptim.new(:pngcrush => false)
|
22
|
+
#
|
23
|
+
# string to set binary
|
24
|
+
#
|
25
|
+
# ImageOptim.new(:pngout => '/special/path/bin/pngout123')
|
26
|
+
#
|
27
|
+
# or hash with options to worker and :bin specifying binary
|
28
|
+
#
|
29
|
+
# ImageOptim.new(:advpng => {:level => 3}, :optipng => {:level => 2}, :jpegoptim => {:bin => 'jpegoptim345'})
|
30
|
+
def initialize(options = {})
|
31
|
+
@workers_by_format = {}
|
32
|
+
Worker.klasses.each do |klass|
|
33
|
+
case worker_options = options.delete(klass.underscored_name.to_sym)
|
34
|
+
when Hash
|
35
|
+
when true, nil
|
36
|
+
worker_options = {}
|
37
|
+
when false
|
38
|
+
next
|
39
|
+
when String
|
40
|
+
worker_options = {:bin => worker_options}
|
41
|
+
else
|
42
|
+
raise "Got #{worker_options.inspect} for #{klass.name} options"
|
43
|
+
end
|
44
|
+
worker = klass.new(worker_options)
|
45
|
+
klass.image_formats.each do |format|
|
46
|
+
workers_by_format[format] ||= []
|
47
|
+
workers_by_format[format] << worker
|
48
|
+
end
|
49
|
+
end
|
50
|
+
workers_by_format.each do |format, workers|
|
51
|
+
workers.replace workers.sort_by(&:run_priority)
|
52
|
+
end
|
53
|
+
|
54
|
+
threads = case options[:threads]
|
55
|
+
when true, nil
|
56
|
+
Util.processor_count
|
57
|
+
when false
|
58
|
+
1
|
59
|
+
else
|
60
|
+
options[:threads].to_i
|
61
|
+
end
|
62
|
+
@threads = limit_with_range(threads, 1..16)
|
63
|
+
|
64
|
+
assert_options_empty!(options)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Optimize one file, return new path or nil if optimization failed
|
68
|
+
def optimize_image(original)
|
69
|
+
original = ImagePath.new(original)
|
70
|
+
if workers = workers_by_format[original.format]
|
71
|
+
result = workers.inject(original) do |current, worker|
|
72
|
+
worker.optimize(current) || current
|
73
|
+
end
|
74
|
+
result == original ? nil : result
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Optimize one file in place, return optimization status
|
79
|
+
def optimize_image!(original)
|
80
|
+
original = ImagePath.new(original)
|
81
|
+
if result = optimize_image(original)
|
82
|
+
original.temp_path(original.dirname) do |temp|
|
83
|
+
original.copy(temp)
|
84
|
+
temp.write(result.read)
|
85
|
+
temp.rename(original)
|
86
|
+
end
|
87
|
+
true
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Optimize multiple images, returning list of results
|
92
|
+
# yields path and result if block given
|
93
|
+
def optimize_images(paths)
|
94
|
+
apply_threading(paths).map do |path|
|
95
|
+
result = optimize_image(path)
|
96
|
+
yield path, result if block_given?
|
97
|
+
result
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Optimize multiple images in place, returning list of results
|
102
|
+
# yields path and result if block given
|
103
|
+
def optimize_images!(paths)
|
104
|
+
apply_threading(paths).map do |path|
|
105
|
+
result = optimize_image!(path)
|
106
|
+
yield path, result if block_given?
|
107
|
+
result
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Optimization methods with default options
|
112
|
+
def self.method_missing(method, *args, &block)
|
113
|
+
if method.to_s =~ /^optimize/
|
114
|
+
new.send(method, *args, &block)
|
115
|
+
else
|
116
|
+
super
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.version
|
121
|
+
Gem.loaded_specs['image_optim'].version.to_s rescue nil
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
125
|
+
|
126
|
+
def apply_threading(array)
|
127
|
+
if threads > 1 && array.length > 1
|
128
|
+
array.in_threads(threads)
|
129
|
+
else
|
130
|
+
array
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
%w[
|
136
|
+
pngcrush pngout optipng advpng
|
137
|
+
jpegoptim jpegtran
|
138
|
+
gifsicle
|
139
|
+
].each do |worker|
|
140
|
+
require "image_optim/workers/#{worker}"
|
141
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'fspath'
|
2
|
+
require 'image_size'
|
3
|
+
|
4
|
+
class ImageOptim
|
5
|
+
class ImagePath < FSPath
|
6
|
+
# Get temp path for this file with same extension
|
7
|
+
def temp_path(*args, &block)
|
8
|
+
ext = extname
|
9
|
+
self.class.temp_file_path([basename(ext), ext], *args, &block)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Copy file to dest preserving attributes
|
13
|
+
def copy(dst)
|
14
|
+
FileUtils.copy_file(self, dst, true)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Get format using ImageSize
|
18
|
+
def format
|
19
|
+
open{ |f| ImageSize.new(f) }.format
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class ImageOptim
|
2
|
+
module OptionHelpers
|
3
|
+
# Remove option from hash and run through block or return default
|
4
|
+
def get_option!(options, name, default)
|
5
|
+
value = default
|
6
|
+
if options.has_key?(name)
|
7
|
+
value = options.delete(name)
|
8
|
+
value = yield(value) if block_given?
|
9
|
+
end
|
10
|
+
instance_variable_set("@#{name}", value)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Ensure number is in range
|
14
|
+
def limit_with_range(number, range)
|
15
|
+
if range.include?(number)
|
16
|
+
number
|
17
|
+
elsif number < range.first
|
18
|
+
range.first
|
19
|
+
elsif range.exclude_end?
|
20
|
+
range.last - 1
|
21
|
+
else
|
22
|
+
range.last
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Raise unless all options are deleted
|
27
|
+
def assert_options_empty!(options)
|
28
|
+
unless options.empty?
|
29
|
+
raise "unknown options #{options.inspect} for #{self}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class ImageOptim
|
2
|
+
module Util
|
3
|
+
# Run command redirecting both stdout and stderr to /dev/null, raising signal if command was signaled and return successfulness
|
4
|
+
def self.run(*args)
|
5
|
+
res = system "#{args.map(&:to_s).shelljoin} &> /dev/null"
|
6
|
+
if $?.signaled?
|
7
|
+
raise SignalException.new($?.termsig)
|
8
|
+
end
|
9
|
+
res
|
10
|
+
end
|
11
|
+
|
12
|
+
# http://stackoverflow.com/questions/891537/ruby-detect-number-of-cpus-installed
|
13
|
+
def self.processor_count
|
14
|
+
@processor_count ||= case host_os = RbConfig::CONFIG['host_os']
|
15
|
+
when /darwin9/
|
16
|
+
`hwprefs cpu_count`
|
17
|
+
when /darwin/
|
18
|
+
(`which hwprefs` != '') ? `hwprefs thread_count` : `sysctl -n hw.ncpu`
|
19
|
+
when /linux/
|
20
|
+
`grep -c processor /proc/cpuinfo`
|
21
|
+
when /freebsd/
|
22
|
+
`sysctl -n hw.ncpu`
|
23
|
+
when /mswin|mingw/
|
24
|
+
require 'win32ole'
|
25
|
+
wmi = WIN32OLE.connect('winmgmts://')
|
26
|
+
cpu = wmi.ExecQuery('select NumberOfLogicalProcessors from Win32_Processor')
|
27
|
+
cpu.to_enum.first.NumberOfLogicalProcessors
|
28
|
+
else
|
29
|
+
warn "Unknown architecture (#{host_os}) assuming one processor."
|
30
|
+
1
|
31
|
+
end.to_i
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'image_optim'
|
3
|
+
|
4
|
+
class ImageOptim
|
5
|
+
class Worker
|
6
|
+
class << self
|
7
|
+
# List of avaliable workers
|
8
|
+
def klasses
|
9
|
+
@klasses ||= []
|
10
|
+
end
|
11
|
+
|
12
|
+
# Remember all classes inheriting from this one
|
13
|
+
def inherited(base)
|
14
|
+
klasses << base
|
15
|
+
end
|
16
|
+
|
17
|
+
# List of formats which worker can optimize
|
18
|
+
def image_formats
|
19
|
+
format_from_name = name.downcase[/gif|jpeg|png/]
|
20
|
+
format_from_name ? [format_from_name.to_sym] : []
|
21
|
+
end
|
22
|
+
|
23
|
+
# Undercored class name
|
24
|
+
def underscored_name
|
25
|
+
@underscored_name ||= name.split('::').last.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
include OptionHelpers
|
30
|
+
|
31
|
+
# Binary name or path
|
32
|
+
attr_reader :bin
|
33
|
+
|
34
|
+
# Configure (raises on extra options), find binary (raises if not found)
|
35
|
+
def initialize(options = {})
|
36
|
+
get_option!(options, :bin, default_bin)
|
37
|
+
parse_options(options)
|
38
|
+
raise "`#{bin}` not found" unless Util.run('which', bin)
|
39
|
+
assert_options_empty!(options)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Put first in list of workers
|
43
|
+
def run_first?
|
44
|
+
end
|
45
|
+
|
46
|
+
# Optimize file, return new path or nil if optimization failed
|
47
|
+
def optimize(src)
|
48
|
+
dst = src.temp_path
|
49
|
+
if Util.run bin, *command_args(src, dst)
|
50
|
+
if dst.size? && dst.size < src.size
|
51
|
+
dst
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Name of binary determined from class name
|
57
|
+
def default_bin
|
58
|
+
self.class.underscored_name
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def run_priority
|
64
|
+
run_first? ? 0 : 1
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'image_optim'
|
2
|
+
|
3
|
+
class ImageOptim
|
4
|
+
class Advpng < Worker
|
5
|
+
# Compression level: 0 - don't compress, 1 - fast, 2 - normal, 3 - extra, 4 - extreme (defaults to 4)
|
6
|
+
attr_reader :level
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def parse_options(options)
|
11
|
+
get_option!(options, :level, 4){ |v| limit_with_range(v.to_i, 0..4) }
|
12
|
+
end
|
13
|
+
|
14
|
+
def command_args(src, dst)
|
15
|
+
src.copy(dst)
|
16
|
+
%W[-#{level} -z -q -- #{dst}]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'image_optim'
|
2
|
+
|
3
|
+
class ImageOptim
|
4
|
+
class Gifsicle < Worker
|
5
|
+
# Turn on interlacing (defaults to false)
|
6
|
+
attr_reader :interlace
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def parse_options(options)
|
11
|
+
get_option!(options, :interlace, false){ |v| !!v }
|
12
|
+
end
|
13
|
+
|
14
|
+
def command_args(src, dst)
|
15
|
+
args = %W[-o #{dst} -O3 --no-comments --no-names --same-delay --same-loopcount --no-warnings -- #{src}]
|
16
|
+
args.unshift('-i') if interlace
|
17
|
+
args
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'image_optim'
|
2
|
+
|
3
|
+
class ImageOptim
|
4
|
+
class Jpegoptim < Worker
|
5
|
+
# Strip Comment markers from output file (defaults to true)
|
6
|
+
attr_reader :strip_comments
|
7
|
+
|
8
|
+
# Strip Exif markers from output file (defaults to true)
|
9
|
+
attr_reader :strip_exif
|
10
|
+
|
11
|
+
# Strip IPTC markers from output file (defaults to true)
|
12
|
+
attr_reader :strip_iptc
|
13
|
+
|
14
|
+
# Strip ICC profile markers from output file (defaults to true)
|
15
|
+
attr_reader :strip_icc
|
16
|
+
|
17
|
+
# Maximum image quality factor (defaults to 100)
|
18
|
+
attr_reader :max_quality
|
19
|
+
|
20
|
+
# Run first if max_quality < 100
|
21
|
+
def run_first?
|
22
|
+
max_quality < 100
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def parse_options(options)
|
28
|
+
get_option!(options, :strip_comments, true){ |v| !!v }
|
29
|
+
get_option!(options, :strip_exif, true){ |v| !!v }
|
30
|
+
get_option!(options, :strip_iptc, true){ |v| !!v }
|
31
|
+
get_option!(options, :strip_icc, true){ |v| !!v }
|
32
|
+
get_option!(options, :max_quality, 100){ |v| v.to_i }
|
33
|
+
end
|
34
|
+
|
35
|
+
def command_args(src, dst)
|
36
|
+
src.copy(dst)
|
37
|
+
args = %W[-q -- #{dst}]
|
38
|
+
if strip_comments && strip_exif && strip_iptc && strip_icc
|
39
|
+
args.unshift '--strip-all'
|
40
|
+
else
|
41
|
+
args.unshift '--strip-com' if strip_comments
|
42
|
+
args.unshift '--strip-exif' if strip_exif
|
43
|
+
args.unshift '--strip-iptc' if strip_iptc
|
44
|
+
args.unshift '--strip-icc' if strip_icc
|
45
|
+
end
|
46
|
+
args.unshift "-m#{max_quality}" if max_quality < 100
|
47
|
+
args
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'image_optim'
|
2
|
+
|
3
|
+
class ImageOptim
|
4
|
+
class Jpegtran < Worker
|
5
|
+
# Copy all chunks or none (defaults to false)
|
6
|
+
attr_reader :copy
|
7
|
+
|
8
|
+
# Create progressive JPEG file (defaults to true)
|
9
|
+
attr_reader :progressive
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def parse_options(options)
|
14
|
+
get_option!(options, :copy, false){ |v| !!v }
|
15
|
+
get_option!(options, :progressive, true){ |v| !!v }
|
16
|
+
end
|
17
|
+
|
18
|
+
def command_args(src, dst)
|
19
|
+
args = %W[-optimize -outfile #{dst} #{src}]
|
20
|
+
args.unshift '-copy', copy ? 'all' : 'none'
|
21
|
+
args.unshift '-progressive' if progressive
|
22
|
+
args
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'image_optim'
|
2
|
+
|
3
|
+
class ImageOptim
|
4
|
+
class Optipng < Worker
|
5
|
+
# Optimization level preset 0..7 (0 is least, 7 is best, defaults to 6)
|
6
|
+
attr_reader :level
|
7
|
+
|
8
|
+
# Interlace, true - interlace on, false - interlace off, nil - as is in original image (defaults to false)
|
9
|
+
attr_reader :interlace
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def parse_options(options)
|
14
|
+
get_option!(options, :level, 6){ |v| limit_with_range(v.to_i, 0..7) }
|
15
|
+
get_option!(options, :interlace, false){ |v| v && true }
|
16
|
+
end
|
17
|
+
|
18
|
+
def command_args(src, dst)
|
19
|
+
src.copy(dst)
|
20
|
+
args = %W[-o#{level} -quiet -- #{dst}]
|
21
|
+
unless interlace.nil?
|
22
|
+
args.unshift "-i#{interlace ? 1 : 0}"
|
23
|
+
end
|
24
|
+
args
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'image_optim'
|
2
|
+
|
3
|
+
class ImageOptim
|
4
|
+
class Pngcrush < Worker
|
5
|
+
# List of chunks to remove or 'alla' or 'allb' (defaults to 'alla')
|
6
|
+
attr_reader :chunks
|
7
|
+
|
8
|
+
# Fix otherwise fatal conditions such as bad CRCs (defaults to false)
|
9
|
+
attr_reader :fix
|
10
|
+
|
11
|
+
# Brute force try all methods, very time-consuming and generally not worthwhile (defaults to false)
|
12
|
+
attr_reader :brute
|
13
|
+
|
14
|
+
# Always run first
|
15
|
+
def run_first?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def parse_options(options)
|
22
|
+
get_option!(options, :chunks, :alla){ |v| Array(v).map(&:to_s) }
|
23
|
+
get_option!(options, :fix, false){ |v| !!v }
|
24
|
+
get_option!(options, :brute, false){ |v| !!v }
|
25
|
+
end
|
26
|
+
|
27
|
+
def command_args(src, dst)
|
28
|
+
args = %W[-reduce -cc -q -- #{src} #{dst}]
|
29
|
+
Array(chunks).each do |chunk|
|
30
|
+
args.unshift '-rem', chunk
|
31
|
+
end
|
32
|
+
args.unshift '-fix' if fix
|
33
|
+
args.unshift '-brute' if brute
|
34
|
+
args
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'image_optim'
|
2
|
+
|
3
|
+
class ImageOptim
|
4
|
+
class Pngout < Worker
|
5
|
+
# Keep optional chunks (defaults to false)
|
6
|
+
attr_reader :keep_chunks
|
7
|
+
|
8
|
+
# Strategy: 0 - xtreme, 1 - intense, 2 - longest Match, 3 - huffman Only, 4 - uncompressed (defaults to 0)
|
9
|
+
attr_reader :strategy
|
10
|
+
|
11
|
+
# Always run first
|
12
|
+
def run_first?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def parse_options(options)
|
19
|
+
get_option!(options, :keep_chunks, false){ |v| !!v }
|
20
|
+
get_option!(options, :strategy, 0){ |v| limit_with_range(v.to_i, 0..4) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def command_args(src, dst)
|
24
|
+
%W[-k#{keep_chunks ? 1 : 0} -s#{strategy} -q -y #{src} #{dst}]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
2
|
+
require 'rspec'
|
3
|
+
require 'image_optim'
|
4
|
+
|
5
|
+
spec_dir = ImageOptim::ImagePath.new(__FILE__).dirname.relative_path_from(Dir.pwd)
|
6
|
+
image_dir = spec_dir / 'images'
|
7
|
+
|
8
|
+
def temp_copy_path(original)
|
9
|
+
original.class.temp_dir do |dir|
|
10
|
+
temp_path = dir / original.basename
|
11
|
+
begin
|
12
|
+
original.copy(temp_path)
|
13
|
+
yield temp_path
|
14
|
+
ensure
|
15
|
+
temp_path.unlink if temp_path.exist?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe ImageOptim do
|
21
|
+
image_dir.glob('*') do |original|
|
22
|
+
describe "optimizing #{original}" do
|
23
|
+
it "should optimize image" do
|
24
|
+
temp_copy_path(original) do |unoptimized|
|
25
|
+
optimized = ImageOptim.optimize_image(unoptimized)
|
26
|
+
optimized.should be_a(FSPath)
|
27
|
+
unoptimized.read.should == original.read
|
28
|
+
optimized.size.should > 0
|
29
|
+
optimized.size.should < unoptimized.size
|
30
|
+
optimized.read.should_not == unoptimized.read
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should optimize image in place" do
|
35
|
+
temp_copy_path(original) do |path|
|
36
|
+
ImageOptim.optimize_image!(path).should be_true
|
37
|
+
path.size.should > 0
|
38
|
+
path.size.should < original.size
|
39
|
+
path.read.should_not == original.read
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should stop optimizing" do
|
44
|
+
temp_copy_path(original) do |unoptimized|
|
45
|
+
count = (1..10).find do |i|
|
46
|
+
unoptimized = ImageOptim.optimize_image(unoptimized)
|
47
|
+
unoptimized.nil?
|
48
|
+
end
|
49
|
+
count.should >= 2
|
50
|
+
count.should < 10
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "unsupported file" do
|
57
|
+
let(:original){ ImageOptim::ImagePath.new(__FILE__) }
|
58
|
+
|
59
|
+
it "should ignore" do
|
60
|
+
temp_copy_path(original) do |unoptimized|
|
61
|
+
optimized = ImageOptim.optimize_image(unoptimized)
|
62
|
+
optimized.should be_nil
|
63
|
+
unoptimized.read.should == original.read
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should ignore in place" do
|
68
|
+
temp_copy_path(original) do |unoptimized|
|
69
|
+
ImageOptim.optimize_image!(unoptimized).should_not be_true
|
70
|
+
unoptimized.read.should == original.read
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
metadata
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: image_optim
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Ivan Kuchin
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-01-09 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: fspath
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 13
|
29
|
+
segments:
|
30
|
+
- 2
|
31
|
+
- 0
|
32
|
+
- 1
|
33
|
+
version: 2.0.1
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: image_size
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 31
|
45
|
+
segments:
|
46
|
+
- 1
|
47
|
+
- 0
|
48
|
+
- 4
|
49
|
+
version: 1.0.4
|
50
|
+
type: :runtime
|
51
|
+
version_requirements: *id002
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: progress
|
54
|
+
prerelease: false
|
55
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ~>
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 31
|
61
|
+
segments:
|
62
|
+
- 2
|
63
|
+
- 4
|
64
|
+
- 0
|
65
|
+
version: 2.4.0
|
66
|
+
type: :runtime
|
67
|
+
version_requirements: *id003
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: in_threads
|
70
|
+
prerelease: false
|
71
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ~>
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
hash: 17
|
77
|
+
segments:
|
78
|
+
- 1
|
79
|
+
- 1
|
80
|
+
- 1
|
81
|
+
version: 1.1.1
|
82
|
+
type: :runtime
|
83
|
+
version_requirements: *id004
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: rspec
|
86
|
+
prerelease: false
|
87
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
hash: 3
|
93
|
+
segments:
|
94
|
+
- 0
|
95
|
+
version: "0"
|
96
|
+
type: :development
|
97
|
+
version_requirements: *id005
|
98
|
+
description:
|
99
|
+
email:
|
100
|
+
executables:
|
101
|
+
- image_optim
|
102
|
+
extensions: []
|
103
|
+
|
104
|
+
extra_rdoc_files: []
|
105
|
+
|
106
|
+
files:
|
107
|
+
- .gitignore
|
108
|
+
- LICENSE.txt
|
109
|
+
- README.markdown
|
110
|
+
- TODO
|
111
|
+
- bin/image_optim
|
112
|
+
- image_optim.gemspec
|
113
|
+
- lib/image_optim.rb
|
114
|
+
- lib/image_optim/image_path.rb
|
115
|
+
- lib/image_optim/option_helpers.rb
|
116
|
+
- lib/image_optim/util.rb
|
117
|
+
- lib/image_optim/worker.rb
|
118
|
+
- lib/image_optim/workers/advpng.rb
|
119
|
+
- lib/image_optim/workers/gifsicle.rb
|
120
|
+
- lib/image_optim/workers/jpegoptim.rb
|
121
|
+
- lib/image_optim/workers/jpegtran.rb
|
122
|
+
- lib/image_optim/workers/optipng.rb
|
123
|
+
- lib/image_optim/workers/pngcrush.rb
|
124
|
+
- lib/image_optim/workers/pngout.rb
|
125
|
+
- spec/image_optim_spec.rb
|
126
|
+
- spec/images/comparison.png
|
127
|
+
- spec/images/decompressed.jpeg
|
128
|
+
- spec/images/icecream.gif
|
129
|
+
- spec/images/image.jpg
|
130
|
+
- spec/images/lena.jpg
|
131
|
+
- spec/images/transparency1.png
|
132
|
+
- spec/images/transparency2.png
|
133
|
+
- spec/images/vergroessert.jpg
|
134
|
+
homepage: http://github.com/toy/image_optim
|
135
|
+
licenses:
|
136
|
+
- MIT
|
137
|
+
post_install_message:
|
138
|
+
rdoc_options: []
|
139
|
+
|
140
|
+
require_paths:
|
141
|
+
- lib
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
143
|
+
none: false
|
144
|
+
requirements:
|
145
|
+
- - ">="
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
hash: 3
|
148
|
+
segments:
|
149
|
+
- 0
|
150
|
+
version: "0"
|
151
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
152
|
+
none: false
|
153
|
+
requirements:
|
154
|
+
- - ">="
|
155
|
+
- !ruby/object:Gem::Version
|
156
|
+
hash: 3
|
157
|
+
segments:
|
158
|
+
- 0
|
159
|
+
version: "0"
|
160
|
+
requirements: []
|
161
|
+
|
162
|
+
rubyforge_project: image_optim
|
163
|
+
rubygems_version: 1.8.13
|
164
|
+
signing_key:
|
165
|
+
specification_version: 3
|
166
|
+
summary: Optimize images (jpeg, png, gif) using external utilities (advpng, gifsicle, jpegoptim, jpegtran, optipng, pngcrush, pngout)
|
167
|
+
test_files:
|
168
|
+
- spec/image_optim_spec.rb
|
169
|
+
- spec/images/comparison.png
|
170
|
+
- spec/images/decompressed.jpeg
|
171
|
+
- spec/images/icecream.gif
|
172
|
+
- spec/images/image.jpg
|
173
|
+
- spec/images/lena.jpg
|
174
|
+
- spec/images/transparency1.png
|
175
|
+
- spec/images/transparency2.png
|
176
|
+
- spec/images/vergroessert.jpg
|
177
|
+
has_rdoc:
|