image_optim 0.7.3 → 0.8.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.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MTcxMThlMmE2OGE1YWYxZjFkODZjY2EyZjYxYWM0MDk0YTZmNGJhMg==
5
+ data.tar.gz: !binary |-
6
+ ZDAwNmMyYjc3M2MzN2UzNjZmYmUzMWQwYzg1NTA2NDhmZjU2YTVmNQ==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ YmYwYmQ1YWRhYjljNDAwYmQ3OTk3MzFhYTg5YzM2MmMyMDAxOWY1OGNlMmNl
10
+ NTg2OWI5YWNlNmIyMDQ2NmZmOGZmMzFjYjJmNTJmYjI1M2QxNzk5NjhhMTg5
11
+ Zjc1MzFhY2FjZjg5YTlhOGY0YWFmOTUxNjY0YzNkODM4OTU1YTQ=
12
+ data.tar.gz: !binary |-
13
+ YjJmMWJiZjEzOThiOGQwMzkyNTQxNzRiOGUyZDNkMGY5ZGU5MWJhYzViN2Iy
14
+ Yjc5N2U1ZWUyNDNlNTgzZmQ0MTU0YjEyYmQ4MDNlMDU0OTJlZjY1NDkyMDg5
15
+ ZTFjYjZlYjlmZTdiMjkxMmRiMGYwMGMwYjM4MDc0MWIwNGRiODk=
@@ -82,7 +82,7 @@ _Note: pngout is free to use even in commercial soft, but you can not redistribu
82
82
 
83
83
  ### From ruby
84
84
 
85
- Initilize optimizer (options are described in comments for ImageOptim, Worker and all workers):
85
+ Initilize optimizer:
86
86
 
87
87
  image_optim = ImageOptim.new
88
88
 
@@ -108,6 +108,38 @@ Multiple images:
108
108
 
109
109
  image_optim.optimize_images!(Dir['*.*'])
110
110
 
111
+ ## Options
112
+
113
+ Worker can be disabled by passing false instead of options hash.
114
+
115
+ ### pngcrush
116
+ * `:chunks` — List of chunks to remove or 'alla' - all except tRNS/transparency or 'allb' - all except tRNS and gAMA/gamma *(defaults to alla)*
117
+ * `:fix` — Fix otherwise fatal conditions such as bad CRCs *(defaults to false)*
118
+ * `:brute` — Brute force try all methods, very time-consuming and generally not worthwhile *(defaults to false)*
119
+
120
+ ### pngout
121
+ * `:copy_chunks` — Copy optional chunks *(defaults to false)*
122
+ * `:strategy` — Strategy: 0 - xtreme, 1 - intense, 2 - longest Match, 3 - huffman Only, 4 - uncompressed *(defaults to 0)*
123
+
124
+ ### optipng
125
+ * `:level` — Optimization level preset 0 is least, 7 is best *(defaults to 6)*
126
+ * `:interlace` — Interlace, true - interlace on, false - interlace off, nil - as is in original image *(defaults to false)*
127
+
128
+ ### advpng
129
+ * `:level` — Compression level: 0 - don't compress, 1 - fast, 2 - normal, 3 - extra, 4 - extreme *(defaults to 4)*
130
+
131
+ ### jpegoptim
132
+ * `:strip` — List of extra markers to strip: comments, exif, iptc, icc or all *(defaults to all)*
133
+ * `:max_quality` — Maximum image quality factor 0..100 *(defaults to 100)*
134
+
135
+ ### jpegtran
136
+ * `:copy_chunks` — Copy all chunks *(defaults to false)*
137
+ * `:progressive` — Create progressive JPEG file *(defaults to true)*
138
+ * `:jpegrescan` — Use jpegtran through jpegrescan, ignore progressive option *(defaults to true)*
139
+
140
+ ### gifsicle
141
+ * `:interlace` — Turn interlacing on *(defaults to false)*
142
+
111
143
  ## Copyright
112
144
 
113
145
  Copyright (c) 2012-2013 Ivan Kuchin. See LICENSE.txt for details.
@@ -8,6 +8,8 @@ require 'find'
8
8
 
9
9
  options = {}
10
10
 
11
+ OptionParser.accept(ImageOptim::TrueFalseNil, OptionParser.top.atype[TrueClass][0].merge('nil' => nil)){ |arg, val| val }
12
+
11
13
  option_parser = OptionParser.new do |op|
12
14
  op.banner = <<-TEXT
13
15
  #{op.program_name} v#{ImageOptim.version}
@@ -23,26 +25,61 @@ Usege:
23
25
 
24
26
  op.separator nil
25
27
 
26
- op.on('--[no-]threads NUMBER', Integer, 'Number of threads or disable (defaults to number of processors)') do |threads|
28
+ op.on('--[no-]threads N', Integer, 'Number of threads or disable (defaults to number of processors)') do |threads|
27
29
  options[:threads] = threads
28
30
  end
29
31
 
30
- op.on('--[no-]nice NUMBER', Integer, 'Nice level (defaults to 10)') do |nice|
32
+ op.on('--[no-]nice N', Integer, 'Nice level (defaults to 10)') do |nice|
31
33
  options[:nice] = nice
32
34
  end
33
35
 
34
36
  op.separator nil
37
+ op.separator ' Disabling workers:'
35
38
 
36
39
  ImageOptim::Worker.klasses.each do |klass|
37
- bin = klass.underscored_name.to_sym
40
+ bin = klass.bin_sym
38
41
  op.on("--no-#{bin}", "disable #{bin} worker") do |enable|
39
42
  options[bin] = enable
40
43
  end
41
44
  end
42
45
 
43
46
  op.separator nil
47
+ op.separator ' Worker options:'
48
+
49
+ ImageOptim::Worker.klasses.each_with_index do |klass, i|
50
+ op.separator nil unless i.zero?
51
+
52
+ bin = klass.bin_sym
53
+ klass.option_definitions.each do |option_definition|
54
+ name = option_definition.name.to_s.gsub('_', '-')
55
+ default = option_definition.default
56
+ type = option_definition.type
57
+
58
+ type, marking = case
59
+ when [TrueClass, FalseClass, ImageOptim::TrueFalseNil].include?(type)
60
+ [type, 'B']
61
+ when Integer >= type
62
+ [Integer, 'N']
63
+ when Array >= type
64
+ [Array, 'a,b,c']
65
+ else
66
+ raise "Unknown type #{type}"
67
+ end
44
68
 
45
- op.on('-v', '--verbose', 'Verbose info') do |verbose|
69
+ description = "#{option_definition.description.gsub(' - ', ' - ')} (defaults to #{default})"
70
+ description = description.scan(/(.*?.{1,60})(?:\s|\z)/).flatten.join("\n ").split("\n")
71
+
72
+ op.on("--#{bin}-#{name} #{marking}", type, *description) do |value|
73
+ options[bin] = {} unless options[bin].is_a?(Hash)
74
+ options[bin][option_definition.name.to_sym] = value
75
+ end
76
+ end
77
+ end
78
+
79
+ op.separator nil
80
+ op.separator ' Common options:'
81
+
82
+ op.on('-v', '--verbose', 'Verbose output') do |verbose|
46
83
  options[:verbose] = verbose
47
84
  end
48
85
 
@@ -63,6 +100,23 @@ rescue OptionParser::ParseError => e
63
100
  abort "#{e.to_s}\n\n#{option_parser.help}"
64
101
  end
65
102
 
103
+ if options[:verbose]
104
+ def print_options(options, level = 1)
105
+ prefix = ' ' * level
106
+ options.each do |key, value|
107
+ if value.is_a?(Hash)
108
+ puts "#{prefix}#{key}:"
109
+ print_options(value, level + 1)
110
+ else
111
+ puts "#{prefix}#{key}: #{value.inspect}"
112
+ end
113
+ end
114
+ end
115
+
116
+ puts 'Options:'
117
+ print_options(options)
118
+ end
119
+
66
120
  if ARGV.empty?
67
121
  abort "specify image paths to optimize\n\n#{option_parser.help}"
68
122
  else
@@ -70,7 +124,7 @@ else
70
124
  image_optim = begin
71
125
  ImageOptim.new(options)
72
126
  rescue ImageOptim::ConfigurationError => e
73
- abort e
127
+ abort e.to_s
74
128
  end
75
129
 
76
130
  paths = []
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'image_optim'
5
- s.version = '0.7.3'
5
+ s.version = '0.8.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']
@@ -3,12 +3,15 @@ require 'shellwords'
3
3
 
4
4
  require 'image_optim/image_path'
5
5
  require 'image_optim/option_helpers'
6
+ require 'image_optim/option_definition'
6
7
  require 'image_optim/worker'
7
8
 
8
9
  class ImageOptim
9
10
  class ConfigurationError < StandardError; end
10
11
  class BinNotFoundError < StandardError; end
11
12
 
13
+ class TrueFalseNil; end
14
+
12
15
  include OptionHelpers
13
16
 
14
17
  # Nice level
@@ -62,13 +65,13 @@ class ImageOptim
62
65
  else
63
66
  threads.to_i
64
67
  end
65
- @threads = limit_with_range(threads, 1..16)
68
+ @threads = OptionHelpers.limit_with_range(threads, 1..16)
66
69
 
67
70
  @verbose = !!options.delete(:verbose)
68
71
 
69
72
  @workers_by_format = {}
70
73
  Worker.klasses.each do |klass|
71
- case worker_options = options.delete(klass.underscored_name.to_sym)
74
+ case worker_options = options.delete(klass.bin_sym)
72
75
  when Hash
73
76
  when true, nil
74
77
  worker_options = {}
@@ -150,7 +153,7 @@ class ImageOptim
150
153
 
151
154
  # Version of image_optim gem spec loaded
152
155
  def self.version
153
- Gem.loaded_specs['image_optim'].version.to_s rescue nil
156
+ Gem.loaded_specs['image_optim'].version.to_s rescue 'DEV'
154
157
  end
155
158
 
156
159
  # Are there workers for file at path?
@@ -184,6 +187,13 @@ class ImageOptim
184
187
  @resolved_bins[bin] or raise BinNotFoundError, "`#{bin}` not found"
185
188
  end
186
189
 
190
+ VENDOR_PATH = File.expand_path('../../vendor', __FILE__)
191
+
192
+ # Join resolve_dir, default path and vendor path for PATH environment variable
193
+ def env_path
194
+ "#{resolve_dir}:#{ENV['PATH']}:#{VENDOR_PATH}"
195
+ end
196
+
187
197
  private
188
198
 
189
199
  # Run method for each path and yield each path and result if block given
@@ -210,7 +220,7 @@ private
210
220
 
211
221
  # Check if bin can be accessed
212
222
  def bin_accessible?(bin)
213
- `which #{bin.to_s.shellescape}` != ''
223
+ `env PATH=#{env_path.shellescape} which #{bin.to_s.shellescape}` != ''
214
224
  end
215
225
 
216
226
  # http://stackoverflow.com/questions/891537/ruby-detect-number-of-cpus-installed
@@ -0,0 +1,14 @@
1
+ class OptionDefinition
2
+
3
+ attr_reader :name, :default, :type, :description, :proc
4
+
5
+ def initialize(name, default, type, description, &proc)
6
+ if type.is_a?(String)
7
+ type, description = default.class, type
8
+ end
9
+
10
+ @name = name.to_sym
11
+ @description = description.to_s
12
+ @default, @type, @proc = default, type, proc
13
+ end
14
+ end
@@ -15,7 +15,7 @@ class ImageOptim
15
15
  end
16
16
 
17
17
  # Ensure number is in range
18
- def limit_with_range(number, range)
18
+ def self.limit_with_range(number, range)
19
19
  if range.include?(number)
20
20
  number
21
21
  elsif number < range.first
@@ -7,7 +7,7 @@ require 'image_optim'
7
7
  class ImageOptim
8
8
  class Worker
9
9
  class << self
10
- # List of avaliable workers
10
+ # List of available workers
11
11
  def klasses
12
12
  @klasses ||= []
13
13
  end
@@ -17,9 +17,18 @@ class ImageOptim
17
17
  klasses << base
18
18
  end
19
19
 
20
- # Undercored class name
21
- def underscored_name
22
- @underscored_name ||= name.split('::').last.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
20
+ # Underscored class name symbol
21
+ def bin_sym
22
+ @underscored_name ||= name.split('::').last.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
23
+ end
24
+
25
+ def option_definitions
26
+ @option_definitions ||= []
27
+ end
28
+
29
+ def option(name, default, type, description = nil, &proc)
30
+ attr_reader name
31
+ option_definitions << OptionDefinition.new(name, default, type, description, &proc)
23
32
  end
24
33
  end
25
34
 
@@ -28,7 +37,9 @@ class ImageOptim
28
37
  # Configure (raises on extra options)
29
38
  def initialize(image_optim, options = {})
30
39
  @image_optim = image_optim
31
- parse_options(options)
40
+ self.class.option_definitions.each do |option_definition|
41
+ get_option!(options, option_definition.name, option_definition.default, &option_definition.proc)
42
+ end
32
43
  assert_options_empty!(options)
33
44
  end
34
45
 
@@ -60,10 +71,9 @@ class ImageOptim
60
71
  resolve_bin!(bin)
61
72
 
62
73
  command = [bin, *arguments].map(&:to_s).shelljoin
63
- env_path = "#{@image_optim.resolve_dir}:#{ENV['PATH']}"
64
74
  start = Time.now
65
75
 
66
- system "env PATH=#{env_path.shellescape} nice -n #{@image_optim.nice} #{command} > /dev/null 2>&1"
76
+ system "env PATH=#{@image_optim.env_path.shellescape} nice -n #{@image_optim.nice} #{command} > /dev/null 2>&1"
67
77
 
68
78
  raise SignalException.new($?.termsig) if $?.signaled?
69
79
 
@@ -3,20 +3,15 @@ require 'image_optim/worker'
3
3
  class ImageOptim
4
4
  class Worker
5
5
  class Advpng < Worker
6
- # Compression level: 0 - don't compress, 1 - fast, 2 - normal, 3 - extra, 4 - extreme (defaults to 4)
7
- attr_reader :level
6
+ option(:level, 4, 'Compression level: 0 - don\'t compress, 1 - fast, 2 - normal, 3 - extra, 4 - extreme') do |v|
7
+ OptionHelpers.limit_with_range(v.to_i, 0..4)
8
+ end
8
9
 
9
10
  def optimize(src, dst)
10
11
  src.copy(dst)
11
12
  args = %W[-#{level} -z -q -- #{dst}]
12
13
  execute(:advpng, *args) && optimized?(src, dst)
13
14
  end
14
-
15
- private
16
-
17
- def parse_options(options)
18
- get_option!(options, :level, 4){ |v| limit_with_range(v.to_i, 0..4) }
19
- end
20
15
  end
21
16
  end
22
17
  end
@@ -3,20 +3,13 @@ require 'image_optim/worker'
3
3
  class ImageOptim
4
4
  class Worker
5
5
  class Gifsicle < Worker
6
- # Turn on interlacing (defaults to false)
7
- attr_reader :interlace
6
+ option(:interlace, false, 'Turn interlacing on'){ |v| !!v }
8
7
 
9
8
  def optimize(src, dst)
10
9
  args = %W[-o #{dst} -O3 --no-comments --no-names --same-delay --same-loopcount --no-warnings -- #{src}]
11
10
  args.unshift('-i') if interlace
12
11
  execute(:gifsicle, *args) && optimized?(src, dst)
13
12
  end
14
-
15
- private
16
-
17
- def parse_options(options)
18
- get_option!(options, :interlace, false){ |v| !!v }
19
- end
20
13
  end
21
14
  end
22
15
  end
@@ -3,11 +3,15 @@ require 'image_optim/worker'
3
3
  class ImageOptim
4
4
  class Worker
5
5
  class Jpegoptim < Worker
6
- # List of extra markers to strip: comments, exif, iptc, icc (defaults to 'all')
7
- attr_reader :strip
6
+ option(:strip, :all, Array, 'List of extra markers to strip: comments, exif, iptc, icc or all') do |v|
7
+ values = Array(v).map(&:to_s)
8
+ known_values = %w[all comments exif iptc icc]
9
+ unknown_values = values - known_values
10
+ warn "Unknown markers for jpegoptim: #{unknown_values.join(', ')}" unless unknown_values.empty?
11
+ values & unknown_values
12
+ end
8
13
 
9
- # Maximum image quality factor (defaults to 100)
10
- attr_reader :max_quality
14
+ option(:max_quality, 100, 'Maximum image quality factor 0..100'){ |v| OptionHelpers.limit_with_range(v.to_i, 0..100) }
11
15
 
12
16
  # Run first if max_quality < 100
13
17
  def run_order
@@ -23,19 +27,6 @@ class ImageOptim
23
27
  args.unshift "-m#{max_quality}" if max_quality < 100
24
28
  execute(:jpegoptim, *args) && optimized?(src, dst)
25
29
  end
26
-
27
- private
28
-
29
- def parse_options(options)
30
- get_option!(options, :strip, :all) do |v|
31
- markers = Array(v).map(&:to_s)
32
- possible_markers = %w[all comments exif iptc icc]
33
- unknown_markers = markers - possible_markers
34
- warn "Unknown markers for jpegoptim: #{unknown_markers.join(', ')}" unless unknown_markers.empty?
35
- markers & possible_markers
36
- end
37
- get_option!(options, :max_quality, 100){ |v| v.to_i }
38
- end
39
30
  end
40
31
  end
41
32
  end
@@ -3,24 +3,24 @@ require 'image_optim/worker'
3
3
  class ImageOptim
4
4
  class Worker
5
5
  class Jpegtran < Worker
6
- # Copy all chunks or none (defaults to false)
7
- attr_reader :copy_chunks
6
+ option(:copy_chunks, false, 'Copy all chunks'){ |v| !!v }
8
7
 
9
- # Create progressive JPEG file (defaults to true)
10
- attr_reader :progressive
8
+ option(:progressive, true, 'Create progressive JPEG file'){ |v| !!v }
11
9
 
12
- def optimize(src, dst)
13
- args = %W[-optimize -outfile #{dst} #{src}]
14
- args.unshift '-copy', copy_chunks ? 'all' : 'none'
15
- args.unshift '-progressive' if progressive
16
- execute(:jpegtran, *args) && optimized?(src, dst)
17
- end
10
+ option(:jpegrescan, false, 'Use jpegtran through jpegrescan, ignore progressive option'){ |v| !!v }
18
11
 
19
- private
20
-
21
- def parse_options(options)
22
- get_option!(options, :copy_chunks, false){ |v| !!v }
23
- get_option!(options, :progressive, true){ |v| !!v }
12
+ def optimize(src, dst)
13
+ if jpegrescan
14
+ args = %W[#{src} #{dst}]
15
+ args.unshift '-s' unless copy_chunks
16
+ resolve_bin!(:jpegtran)
17
+ execute(:jpegrescan, *args) && optimized?(src, dst)
18
+ else
19
+ args = %W[-optimize -outfile #{dst} #{src}]
20
+ args.unshift '-copy', copy_chunks ? 'all' : 'none'
21
+ args.unshift '-progressive' if progressive
22
+ execute(:jpegtran, *args) && optimized?(src, dst)
23
+ end
24
24
  end
25
25
  end
26
26
  end
@@ -3,11 +3,11 @@ require 'image_optim/worker'
3
3
  class ImageOptim
4
4
  class Worker
5
5
  class Optipng < Worker
6
- # Optimization level preset 0..7 (0 is least, 7 is best, defaults to 6)
7
- attr_reader :level
6
+ option(:level, 6, 'Optimization level preset 0 is least, 7 is best'){ |v| OptionHelpers.limit_with_range(v.to_i, 0..7) }
8
7
 
9
- # Interlace, true - interlace on, false - interlace off, nil - as is in original image (defaults to false)
10
- attr_reader :interlace
8
+ option(:interlace, false, TrueFalseNil, 'Interlace, true - interlace on, false - interlace off, nil - as is in original image') do |v|
9
+ v && true
10
+ end
11
11
 
12
12
  def optimize(src, dst)
13
13
  src.copy(dst)
@@ -15,13 +15,6 @@ class ImageOptim
15
15
  args.unshift "-i#{interlace ? 1 : 0}" unless interlace.nil?
16
16
  execute(:optipng, *args) && optimized?(src, dst)
17
17
  end
18
-
19
- private
20
-
21
- def parse_options(options)
22
- get_option!(options, :level, 6){ |v| limit_with_range(v.to_i, 0..7) }
23
- get_option!(options, :interlace, false){ |v| v && true }
24
- end
25
18
  end
26
19
  end
27
20
  end
@@ -3,14 +3,12 @@ require 'image_optim/worker'
3
3
  class ImageOptim
4
4
  class Worker
5
5
  class Pngcrush < Worker
6
- # List of chunks to remove or 'alla' - all except tRNS/transparency or 'allb' - all except tRNS and gAMA/gamma (defaults to 'alla')
7
- attr_reader :chunks
6
+ option(:chunks, :alla, Array, 'List of chunks to remove or \'alla\' - all except tRNS/transparency or '\
7
+ '\'allb\' - all except tRNS and gAMA/gamma'){ |v| Array(v).map(&:to_s) }
8
8
 
9
- # Fix otherwise fatal conditions such as bad CRCs (defaults to false)
10
- attr_reader :fix
9
+ option(:fix, false, 'Fix otherwise fatal conditions such as bad CRCs'){ |v| !!v }
11
10
 
12
- # Brute force try all methods, very time-consuming and generally not worthwhile (defaults to false)
13
- attr_reader :brute
11
+ option(:brute, false, 'Brute force try all methods, very time-consuming and generally not worthwhile'){ |v| !!v }
14
12
 
15
13
  # Always run first
16
14
  def run_order
@@ -26,14 +24,6 @@ class ImageOptim
26
24
  args.unshift '-brute' if brute
27
25
  execute(:pngcrush, *args) && optimized?(src, dst)
28
26
  end
29
-
30
- private
31
-
32
- def parse_options(options)
33
- get_option!(options, :chunks, :alla){ |v| Array(v).map(&:to_s) }
34
- get_option!(options, :fix, false){ |v| !!v }
35
- get_option!(options, :brute, false){ |v| !!v }
36
- end
37
27
  end
38
28
  end
39
29
  end
@@ -3,11 +3,11 @@ require 'image_optim/worker'
3
3
  class ImageOptim
4
4
  class Worker
5
5
  class Pngout < Worker
6
- # Copy optional chunks (defaults to false)
7
- attr_reader :copy_chunks
6
+ option(:copy_chunks, false, 'Copy optional chunks'){ |v| !!v }
8
7
 
9
- # Strategy: 0 - xtreme, 1 - intense, 2 - longest Match, 3 - huffman Only, 4 - uncompressed (defaults to 0)
10
- attr_reader :strategy
8
+ option(:strategy, 0, 'Strategy: 0 - xtreme, 1 - intense, 2 - longest Match, 3 - huffman Only, 4 - uncompressed') do |v|
9
+ OptionHelpers.limit_with_range(v.to_i, 0..4)
10
+ end
11
11
 
12
12
  # Always run first
13
13
  def run_order
@@ -18,13 +18,6 @@ class ImageOptim
18
18
  args = %W[-k#{copy_chunks ? 1 : 0} -s#{strategy} -q -y #{src} #{dst}]
19
19
  execute(:pngout, *args) && optimized?(src, dst)
20
20
  end
21
-
22
- private
23
-
24
- def parse_options(options)
25
- get_option!(options, :copy_chunks, false){ |v| !!v }
26
- get_option!(options, :strategy, 0){ |v| limit_with_range(v.to_i, 0..4) }
27
- end
28
21
  end
29
22
  end
30
23
  end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ $: << File.expand_path('../../lib', __FILE__)
5
+
6
+ require 'image_optim'
7
+
8
+ ImageOptim::Worker.klasses.each_with_index do |klass, i|
9
+ puts "### #{klass.bin_sym}"
10
+ klass.option_definitions.each do |option_definition|
11
+ puts "* `:#{option_definition.name}` — #{option_definition.description} *(defaults to #{option_definition.default})*"
12
+ end
13
+ puts
14
+ end
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/perl -ws
2
+ # jpegrescan by Loren Merritt
3
+ # Last updated: 2008-11-29 / 2013-03-19
4
+ # This code is public domain.
5
+
6
+ use File::Slurp;
7
+ use File::Temp qw/ tempfile /;
8
+
9
+ @ARGV==2 or die "usage: jpegrescan in.jpg out.jpg
10
+ tries various progressive scan orders
11
+ switches:
12
+ -s strip from all extra markers (`jpegtran -copy none` otherwise `jpegtran -copy all`)
13
+ -v verbose output
14
+ -q supress all output
15
+ ";
16
+ $fin = $ARGV[0];
17
+ $fout = $ARGV[1];
18
+ (undef, $ftmp) = tempfile(SUFFIX => ".scan");
19
+ $jtmp = $fout;
20
+ $verbose = $v;
21
+ $quiet = $q;
22
+ @strip = $s ? ("-copy","none") : ("-copy","all");
23
+ undef $_ for $v,$q,$s;
24
+ undef $/;
25
+ $|=1;
26
+
27
+ # convert the input to baseline, just to make all the other conversions faster
28
+ # FIXME there's still a bunch of redundant computation in separate calls to jpegtran
29
+ open $OLDERR, ">&", STDERR;
30
+ open STDERR, ">", $ftmp;
31
+ open TRAN, "-|", "jpegtran", "-v", @strip, "-optimize", $fin or die;
32
+ write_file($jtmp, <TRAN>);
33
+ close TRAN;
34
+ open STDERR, ">&", $OLDERR;
35
+
36
+ $type = read_file($ftmp);
37
+ $type =~ /components=(\d+)/ or die;
38
+ $rgb = $1==3 ? 1 : $1==1 ? 0 : die "not RGB nor gray\n";
39
+
40
+ # FIXME optimize order for either progressive transfer or decoding speed
41
+ sub canonize {
42
+ my $txt = $prefix.$suffix.shift;
43
+ $txt =~ s/\s*;\s*/;\n/g;
44
+ $txt =~ s/^\s*//;
45
+ $txt =~ s/ +/ /g;
46
+ $txt =~ s/: (\d+) (\d+)/sprintf ": %2d %2d", $1, $2/ge;
47
+ # treat u and v identically. I shouldn't need to do this, but with jpegtran overhead it saves 9% speed. cost: .008% bitrate.
48
+ $txt =~ s/^2:.*\n//gm;
49
+ $txt =~ s/^1:(.+)\n/1:$1\n2:$1\n/gm;
50
+ # dc before ac, coarse before fine
51
+ my @txt = sort {"$a\n$b" =~ /: *(\d+) .* (\d);\n.*: *(\d+) .* (\d);/ or die; !$3 <=> !$1 or $4 <=> $2 or $a cmp $b;} split /\n/, $txt;
52
+ return join "\n", @txt;
53
+ }
54
+
55
+ sub try {
56
+ my $txt = canonize(shift);
57
+ return $memo{$txt} if $memo{$txt};
58
+ write_file($ftmp, $txt);
59
+ open TRAN, "-|", "jpegtran", @strip, "-scans", $ftmp, $jtmp or die;
60
+ $data = <TRAN>;
61
+ close TRAN;
62
+ my $s = length $data;
63
+ $s or die;
64
+ $memo{$txt} = $s;
65
+ !$quiet && print $verbose ? "$txt\n$s\n\n" : ".";
66
+ return $s;
67
+ }
68
+
69
+ sub triesn {
70
+ my($bmode, $bsize);
71
+ my ($limit, @modes) = @_;
72
+ my $overshoot = 0;
73
+ for(@modes) {
74
+ my $s = try($_);
75
+ if(!$bsize || $s < $bsize) {
76
+ $bsize = $s;
77
+ $bmode = $_;
78
+ $overshoot = 0;
79
+ } elsif(++$overshoot >= $limit) {
80
+ last;
81
+ }
82
+ }
83
+ return $bmode;
84
+ }
85
+
86
+ sub tries { triesn(99, @_); }
87
+
88
+ $prefix = "";
89
+ $suffix = "";
90
+
91
+ if($rgb) {
92
+ # 012 helps very little
93
+ # 0/12 and 0/1/2 are pretty evenly matched in frequency, but 0/12 wins in total size if every image had to use the same mode
94
+ # dc refinement passes never help
95
+ $dc = tries(
96
+ # "0: 0 0 0 0; 1 2: 0 0 0 0;", # two scans expose a bug in Opera <= 11.61
97
+ "0: 0 0 0 0; 1: 0 0 0 0; 2: 0 0 0 0;");
98
+ # jpegtran won't let me omit dc entirely, but I can at least quantize it away to make the rest of the tests faster.
99
+ $prefix = "0 1 2: 0 0 0 9;";
100
+ } else {
101
+ $dc = "0: 0 0 0 0;";
102
+ $prefix = "0: 0 0 0 9;";
103
+ }
104
+
105
+ # luma can make use of up to 3 refinement passes.
106
+ # chroma can make use of up to 2 refinement passes.
107
+ # refinement passes have some chance of being split (luma: 4%,4%,4%. chroma: 20%,8%) but the total bit gain is negligible.
108
+ # msb pass should almost always be split (luma: 87%, chroma: 81%).
109
+ # I have no theoretical reason for this list of split positions, they're just the most common in practice.
110
+ # splitting into 3 ections is often slightly better, but the total number of bits saved is negligible.
111
+ # FIXME: penalize lots of refinement passes because it's slower to decode. if so, then also force overwrite if bigger than the input.
112
+ sub try_splits {
113
+ my $str = shift;
114
+ my %n = map {$_ => sprintf "$c: 1 %d $str; $c: %d 63 $str;", $_, $_+1} 2,5,8,12,18;
115
+ my $mode = triesn(2, "$c: 1 63 $str;", @n{2,8,5});
116
+ return $mode if $mode ne $n{8};
117
+ return triesn(1, $mode, @n{12,18});
118
+ }
119
+
120
+ foreach $c (0..$rgb) {
121
+ my @modes;
122
+ my $ml = "";
123
+ for(0..($c?2:3)) {
124
+ push @modes, "$c: 1 8 0 $_; $c: 9 63 0 $_;".$ml;
125
+ $ml .= sprintf("$c: 1 63 %d %d;", $_+1, $_);
126
+ }
127
+ my $refine = triesn(1, @modes);
128
+ $refine =~ s/.* (0 \d);//;
129
+ $ac .= $refine . try_splits($1);
130
+ }
131
+
132
+ $prefix = "";
133
+ undef %memo;
134
+ $mode = canonize($dc.$ac);
135
+ try($mode);
136
+ $size = $memo{$mode};
137
+ !$quiet && print "\n$mode\n$size\n";
138
+ $old_size = -s $fin;
139
+ !$quiet && printf "%+.2f%%\n", ($size/$old_size-1)*100;
140
+ if($size < $old_size) {
141
+ write_file($fout, $data);
142
+ }
143
+ unlink $ftmp;
metadata CHANGED
@@ -1,109 +1,92 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: image_optim
3
- version: !ruby/object:Gem::Version
4
- hash: 5
5
- prerelease:
6
- segments:
7
- - 0
8
- - 7
9
- - 3
10
- version: 0.7.3
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
11
5
  platform: ruby
12
- authors:
6
+ authors:
13
7
  - Ivan Kuchin
14
8
  autorequire:
15
9
  bindir: bin
16
10
  cert_chain: []
17
-
18
- date: 2013-02-24 00:00:00 Z
19
- dependencies:
20
- - !ruby/object:Gem::Dependency
11
+ date: 2013-03-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
21
14
  name: fspath
22
- prerelease: false
23
- requirement: &id001 !ruby/object:Gem::Requirement
24
- none: false
25
- requirements:
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
26
17
  - - ~>
27
- - !ruby/object:Gem::Version
28
- hash: 9
29
- segments:
30
- - 2
31
- - 0
32
- - 3
18
+ - !ruby/object:Gem::Version
33
19
  version: 2.0.3
34
20
  type: :runtime
35
- version_requirements: *id001
36
- - !ruby/object:Gem::Dependency
37
- name: image_size
38
21
  prerelease: false
39
- requirement: &id002 !ruby/object:Gem::Requirement
40
- none: false
41
- requirements:
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: 2.0.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: image_size
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
42
31
  - - ~>
43
- - !ruby/object:Gem::Version
44
- hash: 23
45
- segments:
46
- - 1
47
- - 1
48
- - 2
32
+ - !ruby/object:Gem::Version
49
33
  version: 1.1.2
50
34
  type: :runtime
51
- version_requirements: *id002
52
- - !ruby/object:Gem::Dependency
53
- name: progress
54
35
  prerelease: false
55
- requirement: &id003 !ruby/object:Gem::Requirement
56
- none: false
57
- requirements:
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: 1.1.2
41
+ - !ruby/object:Gem::Dependency
42
+ name: progress
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
58
45
  - - ~>
59
- - !ruby/object:Gem::Version
60
- hash: 31
61
- segments:
62
- - 2
63
- - 4
64
- - 0
46
+ - !ruby/object:Gem::Version
65
47
  version: 2.4.0
66
48
  type: :runtime
67
- version_requirements: *id003
68
- - !ruby/object:Gem::Dependency
69
- name: in_threads
70
49
  prerelease: false
71
- requirement: &id004 !ruby/object:Gem::Requirement
72
- none: false
73
- requirements:
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
74
52
  - - ~>
75
- - !ruby/object:Gem::Version
76
- hash: 17
77
- segments:
78
- - 1
79
- - 1
80
- - 1
53
+ - !ruby/object:Gem::Version
54
+ version: 2.4.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: in_threads
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
81
61
  version: 1.1.1
82
62
  type: :runtime
83
- version_requirements: *id004
84
- - !ruby/object:Gem::Dependency
85
- name: rspec
86
63
  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"
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: 1.1.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ! '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
96
76
  type: :development
97
- version_requirements: *id005
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
98
83
  description:
99
84
  email:
100
- executables:
85
+ executables:
101
86
  - image_optim
102
87
  extensions: []
103
-
104
88
  extra_rdoc_files: []
105
-
106
- files:
89
+ files:
107
90
  - .gitignore
108
91
  - LICENSE.txt
109
92
  - README.markdown
@@ -112,6 +95,7 @@ files:
112
95
  - image_optim.gemspec
113
96
  - lib/image_optim.rb
114
97
  - lib/image_optim/image_path.rb
98
+ - lib/image_optim/option_definition.rb
115
99
  - lib/image_optim/option_helpers.rb
116
100
  - lib/image_optim/worker.rb
117
101
  - lib/image_optim/worker/advpng.rb
@@ -121,6 +105,7 @@ files:
121
105
  - lib/image_optim/worker/optipng.rb
122
106
  - lib/image_optim/worker/pngcrush.rb
123
107
  - lib/image_optim/worker/pngout.rb
108
+ - script/options_for_readme
124
109
  - spec/image_optim_spec.rb
125
110
  - spec/images/comparison.png
126
111
  - spec/images/decompressed.jpeg
@@ -130,40 +115,33 @@ files:
130
115
  - spec/images/transparency1.png
131
116
  - spec/images/transparency2.png
132
117
  - spec/images/vergroessert.jpg
118
+ - vendor/jpegrescan
133
119
  homepage: http://github.com/toy/image_optim
134
- licenses:
120
+ licenses:
135
121
  - MIT
122
+ metadata: {}
136
123
  post_install_message:
137
124
  rdoc_options: []
138
-
139
- require_paths:
125
+ require_paths:
140
126
  - lib
141
- required_ruby_version: !ruby/object:Gem::Requirement
142
- none: false
143
- requirements:
144
- - - ">="
145
- - !ruby/object:Gem::Version
146
- hash: 3
147
- segments:
148
- - 0
149
- version: "0"
150
- required_rubygems_version: !ruby/object:Gem::Requirement
151
- none: false
152
- requirements:
153
- - - ">="
154
- - !ruby/object:Gem::Version
155
- hash: 3
156
- segments:
157
- - 0
158
- version: "0"
127
+ required_ruby_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ! '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - ! '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
159
137
  requirements: []
160
-
161
138
  rubyforge_project: image_optim
162
- rubygems_version: 1.8.24
139
+ rubygems_version: 2.0.3
163
140
  signing_key:
164
- specification_version: 3
165
- summary: Optimize (lossless compress) images (jpeg, png, gif) using external utilities (advpng, gifsicle, jpegoptim, jpegtran, optipng, pngcrush, pngout)
166
- test_files:
141
+ specification_version: 4
142
+ summary: Optimize (lossless compress) images (jpeg, png, gif) using external utilities
143
+ (advpng, gifsicle, jpegoptim, jpegtran, optipng, pngcrush, pngout)
144
+ test_files:
167
145
  - spec/image_optim_spec.rb
168
146
  - spec/images/comparison.png
169
147
  - spec/images/decompressed.jpeg
@@ -173,4 +151,3 @@ test_files:
173
151
  - spec/images/transparency1.png
174
152
  - spec/images/transparency2.png
175
153
  - spec/images/vergroessert.jpg
176
- has_rdoc: