discourse_image_optim 0.24.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +7 -0
  2. data/.appveyor.yml +46 -0
  3. data/.gitignore +18 -0
  4. data/.rubocop.yml +110 -0
  5. data/.travis.yml +42 -0
  6. data/CHANGELOG.markdown +316 -0
  7. data/CONTRIBUTING.markdown +11 -0
  8. data/Gemfile +16 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.markdown +358 -0
  11. data/Vagrantfile +38 -0
  12. data/bin/image_optim +28 -0
  13. data/image_optim.gemspec +34 -0
  14. data/lib/image_optim.rb +267 -0
  15. data/lib/image_optim/bin_resolver.rb +142 -0
  16. data/lib/image_optim/bin_resolver/bin.rb +115 -0
  17. data/lib/image_optim/bin_resolver/comparable_condition.rb +60 -0
  18. data/lib/image_optim/bin_resolver/error.rb +6 -0
  19. data/lib/image_optim/bin_resolver/simple_version.rb +31 -0
  20. data/lib/image_optim/cache.rb +72 -0
  21. data/lib/image_optim/cache_path.rb +16 -0
  22. data/lib/image_optim/cmd.rb +122 -0
  23. data/lib/image_optim/config.rb +219 -0
  24. data/lib/image_optim/configuration_error.rb +3 -0
  25. data/lib/image_optim/handler.rb +57 -0
  26. data/lib/image_optim/hash_helpers.rb +45 -0
  27. data/lib/image_optim/image_meta.rb +20 -0
  28. data/lib/image_optim/non_negative_integer_range.rb +11 -0
  29. data/lib/image_optim/optimized_path.rb +25 -0
  30. data/lib/image_optim/option_definition.rb +38 -0
  31. data/lib/image_optim/option_helpers.rb +17 -0
  32. data/lib/image_optim/path.rb +70 -0
  33. data/lib/image_optim/runner.rb +139 -0
  34. data/lib/image_optim/runner/glob_helpers.rb +45 -0
  35. data/lib/image_optim/runner/option_parser.rb +246 -0
  36. data/lib/image_optim/space.rb +29 -0
  37. data/lib/image_optim/true_false_nil.rb +16 -0
  38. data/lib/image_optim/worker.rb +170 -0
  39. data/lib/image_optim/worker/advpng.rb +37 -0
  40. data/lib/image_optim/worker/class_methods.rb +107 -0
  41. data/lib/image_optim/worker/gifsicle.rb +65 -0
  42. data/lib/image_optim/worker/jhead.rb +47 -0
  43. data/lib/image_optim/worker/jpegoptim.rb +63 -0
  44. data/lib/image_optim/worker/jpegrecompress.rb +49 -0
  45. data/lib/image_optim/worker/jpegtran.rb +48 -0
  46. data/lib/image_optim/worker/optipng.rb +53 -0
  47. data/lib/image_optim/worker/pngcrush.rb +56 -0
  48. data/lib/image_optim/worker/pngout.rb +40 -0
  49. data/lib/image_optim/worker/pngquant.rb +61 -0
  50. data/lib/image_optim/worker/svgo.rb +34 -0
  51. data/script/template/jquery-2.1.3.min.js +4 -0
  52. data/script/template/sortable-0.6.0.min.js +2 -0
  53. data/script/template/worker_analysis.erb +254 -0
  54. data/script/update_worker_options_in_readme +59 -0
  55. data/script/worker_analysis +589 -0
  56. data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +37 -0
  57. data/spec/image_optim/bin_resolver/simple_version_spec.rb +65 -0
  58. data/spec/image_optim/bin_resolver_spec.rb +290 -0
  59. data/spec/image_optim/cache_path_spec.rb +57 -0
  60. data/spec/image_optim/cache_spec.rb +162 -0
  61. data/spec/image_optim/cmd_spec.rb +93 -0
  62. data/spec/image_optim/config_spec.rb +254 -0
  63. data/spec/image_optim/handler_spec.rb +90 -0
  64. data/spec/image_optim/hash_helpers_spec.rb +74 -0
  65. data/spec/image_optim/image_meta_spec.rb +61 -0
  66. data/spec/image_optim/optimized_path_spec.rb +58 -0
  67. data/spec/image_optim/option_definition_spec.rb +138 -0
  68. data/spec/image_optim/option_helpers_spec.rb +25 -0
  69. data/spec/image_optim/path_spec.rb +103 -0
  70. data/spec/image_optim/runner/glob_helpers_spec.rb +21 -0
  71. data/spec/image_optim/runner/option_parser_spec.rb +105 -0
  72. data/spec/image_optim/space_spec.rb +23 -0
  73. data/spec/image_optim/worker/optipng_spec.rb +102 -0
  74. data/spec/image_optim/worker/pngquant_spec.rb +67 -0
  75. data/spec/image_optim/worker_spec.rb +303 -0
  76. data/spec/image_optim_spec.rb +259 -0
  77. data/spec/images/broken_jpeg +1 -0
  78. data/spec/images/comparison.png +0 -0
  79. data/spec/images/decompressed.jpeg +0 -0
  80. data/spec/images/icecream.gif +0 -0
  81. data/spec/images/image.jpg +0 -0
  82. data/spec/images/invisiblepixels/generate +24 -0
  83. data/spec/images/invisiblepixels/image.png +0 -0
  84. data/spec/images/lena.jpg +0 -0
  85. data/spec/images/orient/0.jpg +0 -0
  86. data/spec/images/orient/1.jpg +0 -0
  87. data/spec/images/orient/2.jpg +0 -0
  88. data/spec/images/orient/3.jpg +0 -0
  89. data/spec/images/orient/4.jpg +0 -0
  90. data/spec/images/orient/5.jpg +0 -0
  91. data/spec/images/orient/6.jpg +0 -0
  92. data/spec/images/orient/7.jpg +0 -0
  93. data/spec/images/orient/8.jpg +0 -0
  94. data/spec/images/orient/generate +23 -0
  95. data/spec/images/orient/original.jpg +0 -0
  96. data/spec/images/quant/64.png +0 -0
  97. data/spec/images/quant/generate +25 -0
  98. data/spec/images/rails.png +0 -0
  99. data/spec/images/test.svg +3 -0
  100. data/spec/images/transparency1.png +0 -0
  101. data/spec/images/transparency2.png +0 -0
  102. data/spec/images/vergroessert.jpg +0 -0
  103. data/spec/spec_helper.rb +93 -0
  104. metadata +281 -0
@@ -0,0 +1,60 @@
1
+ class ImageOptim
2
+ class BinResolver
3
+ # Allows to externalize conditions for an instance of Comparable to use in
4
+ # case statemens
5
+ #
6
+ # is = ComparableCondition.is
7
+ # case rand(100)
8
+ # when is < 10 then # ...
9
+ # when is.between?(13, 23) then # ...
10
+ # when is >= 90 then # ...
11
+ # end
12
+ class ComparableCondition
13
+ # Helper class for creating conditions using ComparableCondition.is
14
+ class Builder
15
+ Comparable.instance_methods.each do |method|
16
+ define_method method do |*args|
17
+ ComparableCondition.new(method, *args)
18
+ end
19
+ end
20
+ end
21
+
22
+ def self.is
23
+ Builder.new
24
+ end
25
+
26
+ attr_reader :method, :args
27
+ def initialize(method, *args)
28
+ @method, @args = method.to_sym, args
29
+
30
+ case @method
31
+ when :between?
32
+ @args.length == 2 || argument_error!("`between?' expects 2 arguments")
33
+ when :<, :<=, :==, :>, :>=
34
+ @args.length == 1 || argument_error!("`#{method}' expects 1 argument")
35
+ else
36
+ argument_error! "Unknown method `#{method}'"
37
+ end
38
+ end
39
+
40
+ def ===(other)
41
+ other.send(@method, *@args)
42
+ end
43
+ alias_method :match, :===
44
+
45
+ def to_s
46
+ if @method == :between?
47
+ @args.join('..')
48
+ else
49
+ "#{@method} #{@args.first}"
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def argument_error!(message)
56
+ fail ArgumentError, message
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,6 @@
1
+ class ImageOptim
2
+ class BinResolver
3
+ # Base error during bin resolving
4
+ class Error < StandardError; end
5
+ end
6
+ end
@@ -0,0 +1,31 @@
1
+ class ImageOptim
2
+ class BinResolver
3
+ # Allows comparision of simple versions, only numbers separated by dots are
4
+ # taken into account
5
+ class SimpleVersion
6
+ include Comparable
7
+
8
+ # Numbers extracted from version string
9
+ attr_reader :parts
10
+
11
+ # Initialize with a string or an object convertible to string
12
+ #
13
+ # SimpleVersion.new('2.0.1') <=> SimpleVersion.new(2)
14
+ def initialize(str)
15
+ @str = String(str)
16
+ @parts = @str.split('.').map(&:to_i).reverse.drop_while(&:zero?).reverse
17
+ end
18
+
19
+ # Returns original version string
20
+ def to_s
21
+ @str
22
+ end
23
+
24
+ # Compare version parts of self with other
25
+ def <=>(other)
26
+ other = self.class.new(other) unless other.is_a?(self.class)
27
+ parts <=> other.parts
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,72 @@
1
+ require 'digest/sha1'
2
+ require 'fspath'
3
+ require 'image_optim/cache_path'
4
+
5
+ class ImageOptim
6
+ # Handles image cache
7
+ class Cache
8
+ def initialize(image_optim, workers_by_format)
9
+ return unless image_optim.cache_dir
10
+ @cache_dir = FSPath.new(image_optim.cache_dir)
11
+ @cache_worker_digests = image_optim.cache_worker_digests
12
+ @options_by_format = Hash[workers_by_format.map do |format, workers|
13
+ [format, workers.map(&:inspect).sort.join(', ')]
14
+ end]
15
+ @bins_by_format = Hash[workers_by_format.map do |format, workers|
16
+ [format, workers.map(&:used_bins).flatten!.map! do |sym|
17
+ bin = image_optim.resolve_bin!(sym)
18
+ "#{bin.name}[#{bin.digest}]"
19
+ end.sort!.uniq.join(', ')]
20
+ end]
21
+ end
22
+
23
+ def fetch(original)
24
+ return yield unless @cache_dir
25
+
26
+ digest = digest(original, original.image_format)
27
+ cached = @cache_dir / digest
28
+ return cached.size? && CachePath.convert(cached) if cached.file?
29
+
30
+ optimized = yield
31
+
32
+ cached.dirname.mkpath
33
+
34
+ if optimized
35
+ tmp = FSPath.temp_file_path(digest, @cache_dir)
36
+ FileUtils.mv(optimized, tmp)
37
+ tmp.chmod(0o666 & ~File.umask)
38
+ tmp.rename(cached)
39
+ cached_path = CachePath.convert(cached)
40
+
41
+ # mark cached image as already optimized
42
+ cached = @cache_dir / digest(cached, original.image_format)
43
+ cached.dirname.mkpath
44
+ FileUtils.touch(cached)
45
+
46
+ cached_path
47
+ else
48
+ # mark image as already optimized
49
+ FileUtils.touch(cached)
50
+ nil
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def options_by_format(format)
57
+ @options_by_format[format]
58
+ end
59
+
60
+ def bins_by_format(format)
61
+ @bins_by_format[format]
62
+ end
63
+
64
+ def digest(path, format)
65
+ digest = Digest::SHA1.file(path)
66
+ digest.update options_by_format(format)
67
+ digest.update bins_by_format(format) if @cache_worker_digests
68
+ s = digest.hexdigest
69
+ "#{s[0..1]}/#{s[2..-1]}"
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,16 @@
1
+ require 'image_optim/path'
2
+
3
+ class ImageOptim
4
+ # ImageOptiom::Path with a non self destructing #replace method
5
+ class CachePath < Path
6
+ # Atomic replace dst with self
7
+ def replace(dst)
8
+ dst = self.class.new(dst)
9
+ dst.temp_path(dst.dirname) do |temp|
10
+ copy(temp)
11
+ dst.copy_metadata(temp)
12
+ temp.rename(dst.to_s)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,122 @@
1
+ require 'English'
2
+ require 'open3'
3
+
4
+ class ImageOptim
5
+ # Helper for running commands
6
+ module Cmd
7
+ class TimeoutExceeded < StandardError; end
8
+
9
+ class << self
10
+ # Run using `system`
11
+ # Return success status
12
+ # Will raise SignalException if process was interrupted
13
+ def run(*args)
14
+ success = system(*args)
15
+
16
+ check_status!
17
+
18
+ success
19
+ end
20
+
21
+ def support_timeout?
22
+ if defined?(JRUBY_VERSION)
23
+ JRUBY_VERSION >= '9.0.0.0'
24
+ else
25
+ RUBY_VERSION >= '1.9'
26
+ end
27
+ end
28
+
29
+ # Run the specified command, and kill it off if it runs longer
30
+ # than `timeout` seconds.
31
+ #
32
+ # Return success status
33
+ # Will raise an error when command timeouts
34
+ def run_with_timeout(timeout, *args)
35
+ return run(*args) unless timeout > 0 && support_timeout?
36
+
37
+ success = false
38
+ init_options!(args)
39
+
40
+ begin
41
+ stdin, stdout, thread = Open3.popen2(*args)
42
+ stdin.close
43
+ stdout.close
44
+
45
+ pid = thread[:pid]
46
+
47
+ if thread.join(timeout).nil?
48
+ cleanup_process(pid)
49
+ thread.kill
50
+ fail TimeoutExceeded
51
+ else
52
+ success = thread.value.exitstatus.zero?
53
+ end
54
+ end
55
+
56
+ success
57
+ end
58
+
59
+ # Run using backtick
60
+ # Return captured output
61
+ # Will raise SignalException if process was interrupted
62
+ def capture(cmd)
63
+ output = `#{cmd}`
64
+
65
+ check_status!
66
+
67
+ output
68
+ end
69
+
70
+ private
71
+
72
+ def check_status!
73
+ status = $CHILD_STATUS
74
+
75
+ return unless status.signaled?
76
+
77
+ # jruby incorrectly returns true for `signaled?` if process exits with
78
+ # non zero status. For following code
79
+ #
80
+ # `sh -c 'exit 66'`
81
+ # p [$?.signaled?, $?.exitstatus, $?.termsig]
82
+ #
83
+ # jruby outputs `[true, 66, 66]` instead of expected `[false, 66, nil]`
84
+ return if defined?(JRUBY_VERSION) && status.exitstatus == status.termsig
85
+
86
+ fail SignalException, status.termsig
87
+ end
88
+
89
+ def init_options!(args)
90
+ pgroup_opt = Gem.win_platform? ? :new_pgroup : :pgroup
91
+
92
+ if args.last.is_a?(Hash)
93
+ args.last[pgroup_opt] = true
94
+ else
95
+ args.push(pgroup_opt => true)
96
+ end
97
+ end
98
+
99
+ def cleanup_process(pid)
100
+ Thread.new do
101
+ Process.kill('-TERM', pid)
102
+ Process.detach(pid)
103
+ now = Time.now
104
+
105
+ while Time.now - now < 10
106
+ begin
107
+ Process.kill(0, pid)
108
+ sleep 0.001
109
+ next
110
+ rescue Errno::ESRCH
111
+ break
112
+ end
113
+ end
114
+
115
+ if Process.getpgid(pid)
116
+ Process.kill('-KILL', pid)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,219 @@
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 'image_optim/cmd'
6
+ require 'set'
7
+ require 'yaml'
8
+
9
+ class ImageOptim
10
+ # Read, merge and parse configuration
11
+ class Config
12
+ include OptionHelpers
13
+
14
+ # Global config path at `$XDG_CONFIG_HOME/image_optim.yml` (by default
15
+ # `~/.config/image_optim.yml`)
16
+ GLOBAL_PATH = begin
17
+ File.join(ENV['XDG_CONFIG_HOME'] || '~/.config', 'image_optim.yml')
18
+ end
19
+
20
+ # Local config path at `./.image_optim.yml`
21
+ LOCAL_PATH = './.image_optim.yml'.freeze
22
+
23
+ class << self
24
+ # Read options at path: expand path (warn on failure), return {} if file
25
+ # does not exist or is empty, read yaml, check if it is a Hash, deep
26
+ # symbolise keys
27
+ def read_options(path)
28
+ begin
29
+ full_path = File.expand_path(path)
30
+ rescue ArgumentError => e
31
+ warn "Can't expand path #{path}: #{e}"
32
+ return {}
33
+ end
34
+ return {} unless File.size?(full_path)
35
+ config = YAML.load_file(full_path)
36
+ unless config.is_a?(Hash)
37
+ fail "expected hash, got #{config.inspect}"
38
+ end
39
+ HashHelpers.deep_symbolise_keys(config)
40
+ rescue => e
41
+ warn "exception when reading #{full_path}: #{e}"
42
+ {}
43
+ end
44
+ end
45
+
46
+ # Merge config from files with passed options
47
+ # Config files are checked at `GLOBAL_PATH` and `LOCAL_PATH` unless
48
+ # overriden using `:config_paths`
49
+ def initialize(options)
50
+ config_paths = options.delete(:config_paths) || [GLOBAL_PATH, LOCAL_PATH]
51
+ config_paths = Array(config_paths)
52
+
53
+ to_merge = config_paths.map{ |path| self.class.read_options(path) }
54
+ to_merge << HashHelpers.deep_symbolise_keys(options)
55
+
56
+ @options = to_merge.reduce do |memo, hash|
57
+ HashHelpers.deep_merge(memo, hash)
58
+ end
59
+ @used = Set.new
60
+ end
61
+
62
+ # Gets value for key converted to symbol and mark option as used
63
+ def get!(key)
64
+ key = key.to_sym
65
+ @used << key
66
+ @options[key]
67
+ end
68
+
69
+ # Check if key is present
70
+ def key?(key)
71
+ key = key.to_sym
72
+ @options.key?(key)
73
+ end
74
+
75
+ # Fail unless all options were marked as used (directly or indirectly
76
+ # accessed using `get!`)
77
+ def assert_no_unused_options!
78
+ unknown_options = @options.reject{ |key, _value| @used.include?(key) }
79
+ return if unknown_options.empty?
80
+ fail ConfigurationError, "unknown options #{unknown_options.inspect}"
81
+ end
82
+
83
+ # Nice level:
84
+ # * `10` by default and for `nil` or `true`
85
+ # * `0` for `false`
86
+ # * otherwise convert to integer
87
+ def nice
88
+ nice = get!(:nice)
89
+
90
+ case nice
91
+ when true, nil
92
+ 10
93
+ when false
94
+ 0
95
+ else
96
+ nice.to_i
97
+ end
98
+ end
99
+
100
+ # Number of parallel threads:
101
+ # * `processor_count` by default and for `nil` or `true`
102
+ # * `1` for `false`
103
+ # * otherwise convert to integer
104
+ def threads
105
+ threads = get!(:threads)
106
+
107
+ case threads
108
+ when true, nil
109
+ processor_count
110
+ when false
111
+ 1
112
+ else
113
+ threads.to_i
114
+ end
115
+ end
116
+
117
+ # Verbose mode, converted to boolean
118
+ def verbose
119
+ !!get!(:verbose)
120
+ end
121
+
122
+ # Using image_optim_pack:
123
+ # * `false` to disable
124
+ # * `nil` to use if available
125
+ # * everything else to require
126
+ def pack
127
+ pack = get!(:pack)
128
+ return false if pack == false
129
+
130
+ require 'image_optim/pack'
131
+ true
132
+ rescue LoadError => e
133
+ raise "Cannot load image_optim_pack: #{e}" if pack
134
+ false
135
+ end
136
+
137
+ # Skip missing workers, converted to boolean
138
+ def skip_missing_workers
139
+ if key?(:skip_missing_workers)
140
+ !!get!(:skip_missing_workers)
141
+ else
142
+ pack
143
+ end
144
+ end
145
+
146
+ # Allow lossy workers and optimizations, converted to boolean
147
+ def allow_lossy
148
+ !!get!(:allow_lossy)
149
+ end
150
+
151
+ def cache_dir
152
+ dir = get!(:cache_dir)
153
+ dir unless dir.nil? || dir.empty?
154
+ end
155
+
156
+ def cache_worker_digests
157
+ !!get!(:cache_worker_digests)
158
+ end
159
+
160
+ def timeout
161
+ timeout = get!(:timeout)
162
+ timeout ? timeout.to_i : 0
163
+ end
164
+
165
+ # Options for worker class by its `bin_sym`:
166
+ # * `Hash` passed as is
167
+ # * `{}` for `true` or `nil`
168
+ # * `false` for `false`
169
+ # * otherwise fail with `ConfigurationError`
170
+ def for_worker(klass)
171
+ worker_options = get!(klass.bin_sym)
172
+
173
+ case worker_options
174
+ when Hash
175
+ worker_options
176
+ when true, nil
177
+ {}
178
+ when false
179
+ {:disable => true}
180
+ else
181
+ fail ConfigurationError, "Got #{worker_options.inspect} for "\
182
+ "#{klass.name} options"
183
+ end
184
+ end
185
+
186
+ # yaml dump without document beginning prefix `---`
187
+ def to_s
188
+ YAML.dump(HashHelpers.deep_stringify_keys(@options)).sub(/\A---\n/, '')
189
+ end
190
+
191
+ private
192
+
193
+ # http://stackoverflow.com/a/6420817
194
+ def processor_count
195
+ @processor_count ||= case host_os = RbConfig::CONFIG['host_os']
196
+ when /darwin9/
197
+ Cmd.capture 'hwprefs cpu_count'
198
+ when /darwin/
199
+ if (Cmd.capture 'which hwprefs') != ''
200
+ Cmd.capture 'hwprefs thread_count'
201
+ else
202
+ Cmd.capture 'sysctl -n hw.ncpu'
203
+ end
204
+ when /linux/
205
+ Cmd.capture 'grep -c processor /proc/cpuinfo'
206
+ when /freebsd/
207
+ Cmd.capture 'sysctl -n hw.ncpu'
208
+ when /mswin|mingw/
209
+ require 'win32ole'
210
+ query = 'select NumberOfLogicalProcessors from Win32_Processor'
211
+ result = WIN32OLE.connect('winmgmts://').ExecQuery(query)
212
+ result.to_enum.first.NumberOfLogicalProcessors
213
+ else
214
+ warn "Unknown architecture (#{host_os}) assuming one processor."
215
+ 1
216
+ end.to_i
217
+ end
218
+ end
219
+ end