discourse_image_optim 0.24.4

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