image_optim 0.9.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- OGFiMDdmODY3N2ZkMTgwZWNmMTlhOTFkNmY1NjVjZGRjMmI3ZDQ0MA==
4
+ NzI5M2M3MWM1OWNiMDM5MGFiMmFkNDYwYzBiZjY5NWFiNDc0NTkyNQ==
5
5
  data.tar.gz: !binary |-
6
- MmNjYTZlNDFkMGVmM2U1NmUyNDQ3NDIxYWFmNjAzYTUzMGY0YWQ2Mg==
6
+ YWI3NDcwN2U3ZmIzZGI4NjYxZmJkOTVhYmQ5OGMzNTYyNGFlODI3Ng==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- ZjVkYWZkM2ExYWU2MDAxODU1MGNmNDI2OWNhNGZiNTI2YzU1NWYzN2QyYzRh
10
- M2E3Njg0ZjA1MmVlOTc3ZGNkYjdlZTY1NDk1MDJhYzg3NjhkM2I2ZTYxNmNi
11
- MmQwMjc4OWJlYTRlMDNiZmE2YzkxMTQ4YWIwMTlmZTM0Zjk4NjM=
9
+ YjVkYjUwYTEzMzQ2ZWZjNjg3ZWU1ZDJlMGRmY2FjOTc3NzU4YThlYTVmMmE2
10
+ YWEwODAzMGVkN2QyZDc4MjUyZGQ4ZDAyMTgzMDA3ZjVjNDQ3ODc5NTBlMzI1
11
+ MDU4ZDRlZTAzNjc2M2IwNWMxZGNjZjY0OWQ0MDQ2NGNhMThlYTE=
12
12
  data.tar.gz: !binary |-
13
- NDAxMzU2ODRiYzNhZGMwZDM2M2MzYzk0M2VhYTQ2MjZlMGFlZGNiMTM3Yzk0
14
- YmRkZDk1MTQ2M2U4ZmJiOThjNzBjN2YxNzAzMDFhOGUxNmZlYmNkMGNkNjk0
15
- YjFlMmUzNDM0NTFiMGMyOTQ1MGE1YmRjM2YyYWI0NWM3MWE4Mzk=
13
+ YjlhYjk1NDIyM2FlMzE4NjZlM2QxOTJjYzk0ZjJlYTI1YTYwMGUxODA3Yzdj
14
+ NzdjODY2NDBjZDIwODA2YTIzNThjZWJjMTg2ZGRlYjllYjg3NGIxNzU0Y2Fk
15
+ NWYwNDYwODkwN2FjNmI2ODU5NWNlNjdmMjY5NjdkZjk3ZTY3NDM=
data/.travis.yml CHANGED
@@ -7,8 +7,6 @@ rvm:
7
7
  - 2.0.0
8
8
  - jruby-18mode
9
9
  - jruby-19mode
10
- - rbx-18mode
11
- - rbx-19mode
12
10
  - ree
13
11
  script: bundle exec rspec
14
12
  before_install:
data/README.markdown CHANGED
@@ -13,7 +13,10 @@ Optimize (lossless compress) images (jpeg, png, gif) using external utilities:
13
13
 
14
14
  Based on [ImageOptim.app](http://imageoptim.com/).
15
15
 
16
+ [![Gem Version](https://badge.fury.io/rb/image_optim.png)](http://badge.fury.io/rb/image_optim)
16
17
  [![Build Status](https://travis-ci.org/toy/image_optim.png?branch=master)](https://travis-ci.org/toy/image_optim)
18
+ [![Code Climate](https://codeclimate.com/github/toy/image_optim.png)](https://codeclimate.com/github/toy/image_optim)
19
+ [![Dependency Status](https://gemnasium.com/toy/image_optim.png)](https://gemnasium.com/toy/image_optim)
17
20
 
18
21
  ## Gem installation
19
22
 
@@ -123,10 +126,38 @@ Multiple images:
123
126
 
124
127
  image_optim.optimize_images!(Dir['*.*'])
125
128
 
129
+ ### From rails
130
+
131
+ `ImageOptim::Railtie` will automatically initialize processing of assets if `config.assets.compress` is true.
132
+
133
+ As image optimization can be time consuming you may prefer to optimize original asset files.
134
+
135
+ Automatic assets processing can be turned off by setting `config.assets.image_optim = false`.
136
+
137
+ ## Configuration
138
+
139
+ Configuration in YAML format will be read and prepanded to options from two paths:
140
+
141
+ * `$XDG_CONFIG_HOME/image_optim.yml` (by default `~/.config/image_optim.yml`)
142
+ * `.image_optim.yml` in current working directory
143
+
144
+ Example configuration:
145
+
146
+ nice: 20
147
+ pngout: false # disable
148
+ optipng:
149
+ level: 5
150
+
126
151
  ## Options
127
152
 
153
+ * `:nice` — Nice level *(defaults to 10)*
154
+ * `:threads` — Number of threads or disable *(defaults to number of processors)*
155
+ * `:verbose` — Verbose output *(defaults to false)*
156
+
128
157
  Worker can be disabled by passing false instead of options hash.
129
158
 
159
+ <!---<worker-options>-->
160
+
130
161
  ### pngcrush
131
162
  * `:chunks` — List of chunks to remove or 'alla' - all except tRNS/transparency or 'allb' - all except tRNS and gAMA/gamma *(defaults to alla)*
132
163
  * `:fix` — Fix otherwise fatal conditions such as bad CRCs *(defaults to false)*
@@ -150,11 +181,13 @@ Worker can be disabled by passing false instead of options hash.
150
181
  ### jpegtran
151
182
  * `:copy_chunks` — Copy all chunks *(defaults to false)*
152
183
  * `:progressive` — Create progressive JPEG file *(defaults to true)*
153
- * `:jpegrescan` — Use jpegtran through jpegrescan, ignore progressive option *(defaults to true)*
184
+ * `:jpegrescan` — Use jpegtran through jpegrescan, ignore progressive option *(defaults to false)*
154
185
 
155
186
  ### gifsicle
156
187
  * `:interlace` — Turn interlacing on *(defaults to false)*
157
188
 
189
+ <!---</worker-options>-->
190
+
158
191
  ## Copyright
159
192
 
160
193
  Copyright (c) 2012-2013 Ivan Kuchin. See LICENSE.txt for details.
data/bin/image_optim CHANGED
@@ -1,22 +1,24 @@
1
1
  #!/usr/bin/env ruby
2
2
  # encoding: UTF-8
3
3
 
4
- require 'image_optim'
5
- require 'progress'
6
- require 'optparse'
7
- require 'find'
4
+ require 'image_optim/runner'
8
5
 
6
+ args = ARGV.dup
9
7
  options = {}
10
8
 
11
- OptionParser.accept(ImageOptim::TrueFalseNil, OptionParser.top.atype[TrueClass][0].merge('nil' => nil)){ |arg, val| val }
12
-
13
9
  option_parser = OptionParser.new do |op|
14
- op.banner = <<-TEXT
15
- #{op.program_name} v#{ImageOptim.version}
16
-
17
- Usege:
18
- #{op.program_name} [options] image_path …
19
-
10
+ op.accept(ImageOptim::TrueFalseNil, OptionParser.top.atype[TrueClass][0].merge('nil' => nil)){ |arg, val| val }
11
+
12
+ op.banner = <<-TEXT.gsub(/^\s*\|/, '')
13
+ |#{op.program_name} v#{ImageOptim.version}
14
+ |
15
+ |Usege:
16
+ | #{op.program_name} [options] image_path …
17
+ |
18
+ |Configuration will be read and prepanded to options from two paths:
19
+ | #{ImageOptim::Config::GLOBAL_CONFIG_PATH}
20
+ | #{ImageOptim::Config::LOCAL_CONFIG_PATH} (in current working directory)
21
+ |
20
22
  TEXT
21
23
 
22
24
  op.on('-r', '-R', '--recursive', 'Recurively scan directories for images') do |recursive|
@@ -84,7 +86,7 @@ Usege:
84
86
  end
85
87
 
86
88
  op.on_tail('-h', '--help', 'Show full help') do
87
- puts option_parser.help
89
+ puts op.help
88
90
  exit
89
91
  end
90
92
 
@@ -95,108 +97,14 @@ Usege:
95
97
  end
96
98
 
97
99
  begin
98
- option_parser.parse!
100
+ option_parser.parse!(args)
101
+ ImageOptim::Runner.run!(args, options) or exit 1
99
102
  rescue OptionParser::ParseError => e
100
103
  abort "#{e.to_s}\n\n#{option_parser.help}"
101
- end
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
-
120
- if ARGV.empty?
121
- abort "specify image paths to optimize\n\n#{option_parser.help}"
122
- else
123
- recursive = options.delete(:recursive)
124
- image_optim = begin
125
- ImageOptim.new(options)
126
- rescue ImageOptim::ConfigurationError => e
104
+ rescue => e
105
+ if options[:verbose]
106
+ abort "#{e.to_s}\n#{e.backtrace.join("\n")}"
107
+ else
127
108
  abort e.to_s
128
109
  end
129
-
130
- paths = []
131
- ARGV.each do |arg|
132
- if File.file?(arg)
133
- if image_optim.optimizable?(arg)
134
- paths << arg
135
- else
136
- warn "#{arg} is not an image or there is no optimizer for it"
137
- end
138
- else
139
- if recursive
140
- Find.find(arg) do |path|
141
- paths << path if File.file?(path) && image_optim.optimizable?(path)
142
- end
143
- else
144
- warn "#{arg} is not a file"
145
- end
146
- end
147
- end
148
-
149
- module Space
150
- SIZE_SYMBOLS = %w[B K M G T P E Z Y].freeze
151
- PRECISION = 1
152
- LENGTH = 4 + PRECISION + 1
153
- COEF = 1 / Math.log(10)
154
-
155
- EMPTY_SPACE = ' ' * LENGTH
156
- NOT_COUNTED_SPACE = '!' * LENGTH
157
-
158
- class << self
159
- attr_writer :base10
160
- def denominator
161
- @denominator ||= @base10 ? 1000.0 : 1024.0
162
- end
163
-
164
- def space(size, options = {})
165
- case size
166
- when false
167
- NOT_COUNTED_SPACE.bold.red
168
- when 0, nil
169
- EMPTY_SPACE
170
- else
171
- number, degree = size, 0
172
- while number.abs >= 1000 && degree < SIZE_SYMBOLS.length - 1
173
- number /= denominator
174
- degree += 1
175
- end
176
-
177
- "#{degree == 0 ? number.to_s : "%.#{PRECISION}f" % number}#{SIZE_SYMBOLS[degree]}".rjust(LENGTH)
178
- end
179
- end
180
- end
181
- end
182
-
183
- def size_percent(src_size, dst_size)
184
- '%5.2f%% %s' % [100 - 100.0 * dst_size / src_size, Space.space(src_size - dst_size)]
185
- end
186
-
187
- paths = paths.with_progress('optimizing')
188
- results = image_optim.optimize_images(paths) do |src, dst|
189
- if dst
190
- src_size, dst_size = src.size, dst.size
191
- percent = size_percent(src_size, dst_size)
192
- dst.replace(src)
193
- ["#{percent} #{src}", src_size, dst_size]
194
- else
195
- ["------ #{Space::EMPTY_SPACE} #{src}", src.size, src.size]
196
- end
197
- end
198
- lines, src_sizes, dst_sizes = results.transpose
199
- if lines
200
- $stdout.puts lines, "Total: #{size_percent(src_sizes.inject(:+), dst_sizes.inject(:+))}\n"
201
- end
202
110
  end
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.9.1'
5
+ s.version = '0.10.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']
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
15
15
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
16
16
  s.require_paths = %w[lib]
17
17
 
18
- s.add_dependency 'fspath', '~> 2.0.5'
18
+ s.add_dependency 'fspath', '~> 2.1.0'
19
19
  s.add_dependency 'image_size', '~> 1.1.2'
20
20
  s.add_dependency 'exifr', '~> 1.1.3'
21
21
  s.add_dependency 'progress', '~> 3.0.0'
@@ -0,0 +1,3 @@
1
+ class ImageOptim
2
+ class BinNotFoundError < StandardError; end
3
+ end
@@ -0,0 +1,50 @@
1
+ require 'image_optim/bin_not_found_error'
2
+ require 'thread'
3
+ require 'fspath'
4
+
5
+ class ImageOptim
6
+ class BinResolver
7
+ attr_reader :dir
8
+ def initialize
9
+ @bins = {}
10
+ @lock = Mutex.new
11
+ end
12
+
13
+ def resolve!(bin)
14
+ bin = bin.to_sym
15
+ unless @bins.include?(bin)
16
+ @lock.synchronize do
17
+ @bins[bin] = resolve?(bin) unless @bins.include?(bin)
18
+ end
19
+ end
20
+ @bins[bin] or raise BinNotFoundError, "`#{bin}` not found"
21
+ end
22
+
23
+ VENDOR_PATH = File.expand_path('../../../vendor', __FILE__)
24
+
25
+ def env_path
26
+ "#{dir}:#{ENV['PATH']}:#{VENDOR_PATH}"
27
+ end
28
+
29
+ private
30
+
31
+ def resolve?(bin)
32
+ if path = ENV["#{bin}_bin".upcase]
33
+ unless @dir
34
+ @dir = FSPath.temp_dir
35
+ at_exit{ FileUtils.remove_entry_secure @dir }
36
+ end
37
+ symlink = @dir / bin
38
+ symlink.make_symlink(File.expand_path(path))
39
+
40
+ accessible?(symlink)
41
+ else
42
+ accessible?(bin)
43
+ end
44
+ end
45
+
46
+ def accessible?(bin)
47
+ `env PATH=#{env_path.shellescape} which #{bin.to_s.shellescape}` != ''
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,137 @@
1
+ require 'image_optim/option_helpers'
2
+ require 'image_optim/configuration_error'
3
+ require 'image_optim/hash_helpers'
4
+ require 'image_optim/worker'
5
+ require 'set'
6
+ require 'yaml'
7
+
8
+ class ImageOptim
9
+ class Config
10
+ include OptionHelpers
11
+
12
+ GLOBAL_CONFIG_PATH = File.join(File.expand_path(ENV['XDG_CONFIG_HOME'] || '~/.config'), 'image_optim.yml')
13
+ LOCAL_CONFIG_PATH = '.image_optim.yml'
14
+
15
+ class << self
16
+ def global
17
+ File.file?(GLOBAL_CONFIG_PATH) ? read(GLOBAL_CONFIG_PATH) : {}
18
+ end
19
+
20
+ def local
21
+ File.file?(LOCAL_CONFIG_PATH) ? read(LOCAL_CONFIG_PATH) : {}
22
+ end
23
+
24
+ private
25
+
26
+ def read(path)
27
+ config = YAML.load_file(path)
28
+ unless config.is_a?(Hash)
29
+ raise "excpected hash, got #{config.inspect}"
30
+ end
31
+ HashHelpers.deep_symbolise_keys(config)
32
+ rescue => e
33
+ warn "exception when reading #{path}: #{e}"
34
+ {}
35
+ end
36
+ end
37
+
38
+ def initialize(options)
39
+ @options = [
40
+ Config.global,
41
+ Config.local,
42
+ HashHelpers.deep_symbolise_keys(options),
43
+ ].inject do |memo, hash|
44
+ HashHelpers.deep_merge(memo, hash)
45
+ end
46
+ @used = Set.new
47
+ end
48
+
49
+ def get!(key)
50
+ key = key.to_sym
51
+ @used << key
52
+ @options[key]
53
+ end
54
+
55
+ def assert_no_unused_options!
56
+ unknown_options = @options.reject{ |key, value| @used.include?(key) }
57
+ unless unknown_options.empty?
58
+ raise ConfigurationError, "unknown options #{unknown_options.inspect} for #{self}"
59
+ end
60
+ end
61
+
62
+ def nice
63
+ nice = get!(:nice)
64
+
65
+ case nice
66
+ when true, nil
67
+ 10
68
+ when false
69
+ 0
70
+ else
71
+ nice.to_i
72
+ end
73
+ end
74
+
75
+ def threads
76
+ threads = get!(:threads)
77
+
78
+ threads = case threads
79
+ when true, nil
80
+ processor_count
81
+ when false
82
+ 1
83
+ else
84
+ threads.to_i
85
+ end
86
+
87
+ OptionHelpers.limit_with_range(threads, 1..16)
88
+ end
89
+
90
+ def verbose
91
+ !!get!(:verbose)
92
+ end
93
+
94
+ def for_worker(klass)
95
+ worker_options = get!(klass.bin_sym)
96
+
97
+ case worker_options
98
+ when Hash
99
+ worker_options
100
+ when true, nil
101
+ {}
102
+ when false
103
+ false
104
+ else
105
+ raise ConfigurationError, "Got #{worker_options.inspect} for #{klass.name} options"
106
+ end
107
+ end
108
+
109
+ def to_s
110
+ YAML.dump(HashHelpers.deep_stringify_keys(@options)).sub(/\A---\n/, '')
111
+ end
112
+
113
+ private
114
+
115
+ # http://stackoverflow.com/questions/891537/ruby-detect-number-of-cpus-installed
116
+ def processor_count
117
+ @processor_count ||= case host_os = RbConfig::CONFIG['host_os']
118
+ when /darwin9/
119
+ `hwprefs cpu_count`
120
+ when /darwin/
121
+ (`which hwprefs` != '') ? `hwprefs thread_count` : `sysctl -n hw.ncpu`
122
+ when /linux/
123
+ `grep -c processor /proc/cpuinfo`
124
+ when /freebsd/
125
+ `sysctl -n hw.ncpu`
126
+ when /mswin|mingw/
127
+ require 'win32ole'
128
+ wmi = WIN32OLE.connect('winmgmts://')
129
+ cpu = wmi.ExecQuery('select NumberOfLogicalProcessors from Win32_Processor')
130
+ cpu.to_enum.first.NumberOfLogicalProcessors
131
+ else
132
+ warn "Unknown architecture (#{host_os}) assuming one processor."
133
+ 1
134
+ end.to_i
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,3 @@
1
+ class ImageOptim
2
+ class ConfigurationError < StandardError; end
3
+ end
@@ -0,0 +1,27 @@
1
+ require 'image_optim/image_path'
2
+
3
+ class ImageOptim
4
+ class Handler
5
+ attr_reader :result
6
+ def initialize(original)
7
+ raise ArgumentError, 'original should respond to temp_path' unless original.respond_to?(:temp_path)
8
+
9
+ @original = original
10
+ @result = nil
11
+ end
12
+
13
+ def process
14
+ @src ||= @original
15
+ @dst ||= @original.temp_path
16
+
17
+ if yield @src, @dst
18
+ @result = @dst
19
+ if @src == @original
20
+ @src, @dst = @dst, nil
21
+ else
22
+ @src, @dst = @dst, @src
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,35 @@
1
+ class ImageOptim
2
+ module HashHelpers
3
+ class << self
4
+ def deep_transform_keys(hash, &block)
5
+ new_hash = {}
6
+ hash.each do |k, v|
7
+ new_hash[block.call(k)] = if v.is_a?(Hash)
8
+ deep_transform_keys(v, &block)
9
+ else
10
+ v
11
+ end
12
+ end
13
+ new_hash
14
+ end
15
+
16
+ def deep_stringify_keys(hash)
17
+ deep_transform_keys(hash, &:to_s)
18
+ end
19
+
20
+ def deep_symbolise_keys(hash)
21
+ deep_transform_keys(hash, &:to_sym)
22
+ end
23
+
24
+ def deep_merge(a, b)
25
+ a.merge(b) do |k, v_a, v_b|
26
+ if v_a.is_a?(Hash) && v_b.is_a?(Hash)
27
+ deep_merge(v_a, v_b)
28
+ else
29
+ v_b
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,10 +1,30 @@
1
1
  require 'fspath'
2
2
  require 'image_size'
3
3
 
4
- require 'image_optim'
5
-
6
4
  class ImageOptim
7
5
  class ImagePath < FSPath
6
+ class Optimized < DelegateClass(self)
7
+ def initialize(path, original_or_size = nil)
8
+ path = ImagePath.convert(path)
9
+ __setobj__(path)
10
+ if original_or_size
11
+ if original_or_size.is_a?(Integer)
12
+ @original = path
13
+ @original_size = original_or_size
14
+ else
15
+ @original = ImagePath.convert(original_or_size)
16
+ @original_size = @original.size
17
+ end
18
+ end
19
+ end
20
+
21
+ # Original path, use original_size to get its size as original can be overwritten
22
+ attr_reader :original
23
+
24
+ # Stored size of original
25
+ attr_reader :original_size
26
+ end
27
+
8
28
  # Get temp path for this file with same extension
9
29
  def temp_path(*args, &block)
10
30
  ext = extname
@@ -30,5 +50,10 @@ class ImageOptim
30
50
  def format
31
51
  open{ |f| ImageSize.new(f) }.format
32
52
  end
53
+
54
+ # Returns path if it is already an instance of this class otherwise new instance
55
+ def self.convert(path)
56
+ path.is_a?(self) ? path : new(path)
57
+ end
33
58
  end
34
59
  end
@@ -1,19 +1,7 @@
1
- require 'image_optim'
1
+ require 'image_optim/configuration_error'
2
2
 
3
3
  class ImageOptim
4
4
  module OptionHelpers
5
- # Remove option from hash and run through block or return default
6
- def get_option!(options, name, default)
7
- value = default
8
- if options.has_key?(name)
9
- value = options.delete(name)
10
- end
11
- if block_given?
12
- value = yield(value)
13
- end
14
- instance_variable_set("@#{name}", value)
15
- end
16
-
17
5
  # Ensure number is in range
18
6
  def self.limit_with_range(number, range)
19
7
  if range.include?(number)
@@ -26,12 +14,5 @@ class ImageOptim
26
14
  range.last
27
15
  end
28
16
  end
29
-
30
- # Raise unless all options are deleted
31
- def assert_options_empty!(options)
32
- unless options.empty?
33
- raise ConfigurationError, "unknown options #{options.inspect} for #{self}"
34
- end
35
- end
36
17
  end
37
18
  end
@@ -0,0 +1,19 @@
1
+ require 'image_optim'
2
+
3
+ class ImageOptim
4
+ class Railtie < Rails::Railtie
5
+ initializer 'image_optim.initializer' do |app|
6
+ if app.config.assets.compress && app.config.assets.image_optim != false
7
+ image_optim = ImageOptim.new
8
+
9
+ processor = proc do |context, data|
10
+ image_optim.optimize_image_data(data) || data
11
+ end
12
+
13
+ app.assets.register_preprocessor 'image/gif', :image_optim, &processor
14
+ app.assets.register_preprocessor 'image/jpeg', :image_optim, &processor
15
+ app.assets.register_preprocessor 'image/png', :image_optim, &processor
16
+ end
17
+ end
18
+ end
19
+ end