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,246 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'image_optim'
4
+ require 'image_optim/true_false_nil'
5
+ require 'image_optim/non_negative_integer_range'
6
+ require 'optparse'
7
+
8
+ class ImageOptim
9
+ class Runner
10
+ # Parse options from arguments to image_optim binary
11
+ class OptionParser < ::OptionParser
12
+ # Parse and remove options from args, return options Hash
13
+ # Calls abort in case of parse error
14
+ def self.parse!(args)
15
+ # assume -v to be a request to print version if it is the only argument
16
+ args = %w[--version] if args == %w[-v]
17
+
18
+ options = {}
19
+ parser = new(options)
20
+ parser.parse!(args)
21
+ options
22
+ rescue OptionParser::ParseError => e
23
+ abort "#{e}\n\n#{parser.help}"
24
+ end
25
+
26
+ # After initialization passes self and options to DEFINE
27
+ def initialize(options)
28
+ super
29
+ DEFINE.call(self, options)
30
+ end
31
+
32
+ # Wraps and indents lines of overriden method
33
+ def help
34
+ text = super
35
+
36
+ # reserve one column
37
+ columns = terminal_columns - 1
38
+ # 1 for distance between summary and description
39
+ # 2 for additional indent
40
+ wrapped_indent = summary_indent + ' ' * (summary_width + 1 + 2)
41
+ wrapped_width = columns - wrapped_indent.length
42
+ # don't try to wrap if there is too little space for description
43
+ return text if wrapped_width < 20
44
+
45
+ wrapped = ''
46
+ text.split("\n").each do |line|
47
+ if line.length <= columns
48
+ wrapped << line << "\n"
49
+ else
50
+ indented = line =~ /^\s/
51
+ wrapped << line.slice!(wrap_regex(columns)) << "\n"
52
+ line.scan(wrap_regex(wrapped_width)) do |part|
53
+ wrapped << wrapped_indent if indented
54
+ wrapped << part << "\n"
55
+ end
56
+ end
57
+ end
58
+ wrapped
59
+ end
60
+
61
+ private
62
+
63
+ def terminal_columns
64
+ stty_columns = `stty size 2> /dev/null`[/^\d+ (\d+)$/, 1]
65
+ stty_columns ? stty_columns.to_i : `tput cols`.to_i
66
+ end
67
+
68
+ def wrap_regex(width)
69
+ /.*?.{1,#{width}}(?:\s|\z)/
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ ImageOptim::Runner::OptionParser::DEFINE = proc do |op, options|
76
+ unless op.is_a?(OptionParser)
77
+ fail ArgumentError, "expected instance of OptionParser, got #{op.inspect}"
78
+ end
79
+ unless options.is_a?(Hash)
80
+ fail ArgumentError, "expected instance of Hash, got #{options.inspect}"
81
+ end
82
+
83
+ ImageOptim::TrueFalseNil.add_to_option_parser(op)
84
+ ImageOptim::NonNegativeIntegerRange.add_to_option_parser(op)
85
+
86
+ op.banner = <<-TEXT.gsub(/^\s*\|/, '')
87
+ |#{ImageOptim.full_version}
88
+ |
89
+ |Usage:
90
+ | #{op.program_name} [options] image_path …
91
+ |
92
+ |Configuration will be read and prepended to options from two paths:
93
+ | #{ImageOptim::Config::GLOBAL_PATH}
94
+ | #{ImageOptim::Config::LOCAL_PATH}
95
+ |
96
+ TEXT
97
+
98
+ op.on('--config-paths PATH1,PATH2', Array, 'Config paths to use instead of '\
99
+ 'default ones') do |paths|
100
+ options[:config_paths] = paths
101
+ end
102
+
103
+ op.separator nil
104
+
105
+ op.on('-r', '-R', '--recursive', 'Recursively scan directories '\
106
+ 'for images') do |recursive|
107
+ options[:recursive] = recursive
108
+ end
109
+
110
+ op.on("--exclude-dir 'GLOB'", 'Glob for excluding directories '\
111
+ '(defaults to .*)') do |glob|
112
+ options[:exclude_dir_glob] = glob
113
+ end
114
+
115
+ op.on("--exclude-file 'GLOB'", 'Glob for excluding files '\
116
+ '(defaults to .*)') do |glob|
117
+ options[:exclude_file_glob] = glob
118
+ end
119
+
120
+ op.on("--exclude 'GLOB'", 'Set glob for excluding both directories and '\
121
+ 'files') do |glob|
122
+ options[:exclude_file_glob] = options[:exclude_dir_glob] = glob
123
+ end
124
+
125
+ op.separator nil
126
+
127
+ op.on('--no-progress', 'Disable showing progress') do |show_progress|
128
+ options[:show_progress] = show_progress
129
+ end
130
+
131
+ op.on('--[no-]threads N', Integer, 'Number of threads or disable '\
132
+ '(defaults to number of processors)') do |threads|
133
+ options[:threads] = threads
134
+ end
135
+
136
+ op.on('--[no-]nice N', Integer, 'Nice level, priority of all used tools '\
137
+ 'with higher value meaning lower priority, in range -20..19, negative '\
138
+ 'values can be set only if run by root user (defaults to 10)') do |nice|
139
+ options[:nice] = nice
140
+ end
141
+
142
+ op.on('--[no-]pack', 'Require image_optim_pack or disable it, '\
143
+ 'by default image_optim_pack will be used if available, '\
144
+ 'will turn on skip-missing-workers unless explicitly disabled') do |pack|
145
+ options[:pack] = pack
146
+ end
147
+
148
+ op.separator nil
149
+ op.separator ' Caching:'
150
+
151
+ op.on('--cache-dir DIR', 'Cache optimized images '\
152
+ 'into the specified directory') do |cache_dir|
153
+ options[:cache_dir] = cache_dir
154
+ end
155
+
156
+ op.on('--cache-worker-digests', 'Cache worker digests '\
157
+ '(updating workers invalidates cache)') do |cache_worker_digests|
158
+ options[:cache_worker_digests] = cache_worker_digests
159
+ end
160
+
161
+ op.separator nil
162
+ op.separator ' Disabling workers:'
163
+
164
+ op.on('--[no-]skip-missing-workers', 'Skip workers with missing or '\
165
+ 'problematic binaries') do |skip|
166
+ options[:skip_missing_workers] = skip
167
+ end
168
+
169
+ ImageOptim::Worker.klasses.each do |klass|
170
+ bin = klass.bin_sym
171
+ op.on("--no-#{bin}", "disable #{bin} worker") do |enable|
172
+ options[bin] = enable
173
+ end
174
+ end
175
+
176
+ op.separator nil
177
+ op.separator ' Worker options:'
178
+
179
+ op.on('--allow-lossy', 'Allow lossy workers and '\
180
+ 'optimizations') do |allow_lossy|
181
+ options[:allow_lossy] = allow_lossy
182
+ end
183
+
184
+ op.on('--timeout N', 'Sets a timeout for workers') do |timeout|
185
+ options[:timeout] = timeout.to_i
186
+ end
187
+
188
+ op.separator nil
189
+
190
+ ImageOptim::Worker.klasses.each_with_index do |klass, i|
191
+ next if klass.option_definitions.empty?
192
+ op.separator nil unless i.zero?
193
+
194
+ bin = klass.bin_sym
195
+ klass.option_definitions.each do |option_definition|
196
+ name = option_definition.name.to_s.tr('_', '-')
197
+ default = option_definition.default_description
198
+ type = option_definition.type
199
+
200
+ type, marking = case
201
+ when [TrueClass, FalseClass, ImageOptim::TrueFalseNil].include?(type)
202
+ [type, 'B']
203
+ when Integer >= type
204
+ [Integer, 'N']
205
+ when Array >= type
206
+ [Array, 'a,b,c']
207
+ when ImageOptim::NonNegativeIntegerRange == type
208
+ [type, 'M-N']
209
+ else
210
+ fail "Unknown type #{type}"
211
+ end
212
+
213
+ description = option_definition.description.gsub(' - ', ' - ')
214
+ unless description['(defaults']
215
+ description << " (defaults to #{default})"
216
+ end
217
+
218
+ op.on("--#{bin}-#{name} #{marking}", type, description) do |value|
219
+ options[bin] = {} unless options[bin].is_a?(Hash)
220
+ options[bin][option_definition.name.to_sym] = value
221
+ end
222
+ end
223
+ end
224
+
225
+ op.separator nil
226
+ op.separator ' Common options:'
227
+
228
+ op.on_tail('-v', '--verbose', 'Verbose output') do
229
+ options[:verbose] = true
230
+ end
231
+
232
+ op.on_tail('-h', '--help', 'Show help and exit') do
233
+ puts op.help
234
+ exit
235
+ end
236
+
237
+ op.on_tail('--version', 'Show version and exit') do
238
+ puts ImageOptim.version
239
+ exit
240
+ end
241
+
242
+ op.on_tail('--info', 'Show environment info and exit') do
243
+ options[:verbose] = true
244
+ options[:only_info] = true
245
+ end
246
+ end
@@ -0,0 +1,29 @@
1
+ class ImageOptim
2
+ # Present size in readable form as fixed length string
3
+ module Space
4
+ SIZE_SYMBOLS = %w[B K M G T P E].freeze
5
+ BASE = 1024.0
6
+ PRECISION = 1
7
+ LENGTH = 4 + PRECISION + 1
8
+
9
+ EMPTY_SPACE = ' ' * LENGTH
10
+
11
+ def self.space(size)
12
+ case size
13
+ when 0, nil
14
+ EMPTY_SPACE
15
+ else
16
+ log_denominator = Math.log(size.abs) / Math.log(BASE)
17
+ degree = [log_denominator.floor, SIZE_SYMBOLS.length - 1].min
18
+ number_string = if degree.zero?
19
+ size.to_s
20
+ else
21
+ denominator = BASE**degree
22
+ number = size / denominator
23
+ format("%.#{PRECISION}f", number)
24
+ end
25
+ "#{number_string}#{SIZE_SYMBOLS[degree]}".rjust(LENGTH)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ class ImageOptim
2
+ # Denote ternary value (`true`/`false`/`nil`) for worker option
3
+ class TrueFalseNil
4
+ # Add handling of ternary value in OptionParser instance, maps `nil` and
5
+ # `'nil'` to `nil`
6
+ def self.add_to_option_parser(option_parser)
7
+ completing = OptionParser.top.atype[TrueClass][0].merge('nil' => nil)
8
+ option_parser.accept(self, completing){ |_arg, val| val }
9
+ end
10
+
11
+ # Convert everything truthy to `true`, leave `false` and `nil` as is
12
+ def self.convert(v)
13
+ v && true
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,170 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'image_optim/cmd'
4
+ require 'image_optim/configuration_error'
5
+ require 'image_optim/path'
6
+ require 'image_optim/worker/class_methods'
7
+ require 'shellwords'
8
+ require 'English'
9
+
10
+ class ImageOptim
11
+ # Base class for all workers
12
+ class Worker
13
+ extend ClassMethods
14
+
15
+ class TimeoutExceeded < StandardError; end
16
+
17
+ class << self
18
+ # Default init for worker is new
19
+ # Check example of override in gifsicle worker
20
+ alias_method :init, :new
21
+ end
22
+
23
+ # Configure (raises on extra options)
24
+ def initialize(image_optim, options = {})
25
+ unless image_optim.is_a?(ImageOptim)
26
+ fail ArgumentError, 'first parameter should be an ImageOptim instance'
27
+ end
28
+ @image_optim = image_optim
29
+ parse_options(options)
30
+ assert_no_unknown_options!(options)
31
+ end
32
+
33
+ # Return hash with worker options
34
+ def options
35
+ hash = {}
36
+ self.class.option_definitions.each do |option|
37
+ hash[option.name] = send(option.name)
38
+ end
39
+ hash
40
+ end
41
+
42
+ # Optimize image at src, output at dst, must be overriden in subclass
43
+ # return true on success
44
+ def optimize(_src, _dst)
45
+ fail NotImplementedError, "implement method optimize in #{self.class}"
46
+ end
47
+
48
+ # List of formats which worker can optimize
49
+ def image_formats
50
+ format_from_name = self.class.name.downcase[/gif|jpeg|png|svg/]
51
+ unless format_from_name
52
+ fail "#{self.class}: can't guess applicable format from worker name"
53
+ end
54
+ [format_from_name.to_sym]
55
+ end
56
+
57
+ # Ordering in list of workers, 0 by default
58
+ def run_order
59
+ 0
60
+ end
61
+
62
+ # List of bins used by worker
63
+ def used_bins
64
+ [self.class.bin_sym]
65
+ end
66
+
67
+ # Resolve used bins, raise exception concatenating all messages
68
+ def resolve_used_bins!
69
+ errors = BinResolver.collect_errors(used_bins) do |bin|
70
+ @image_optim.resolve_bin!(bin)
71
+ end
72
+ return if errors.empty?
73
+ fail BinResolver::Error, wrap_resolver_error_message(errors.join(', '))
74
+ end
75
+
76
+ # Check if operation resulted in optimized file
77
+ def optimized?(src, dst)
78
+ dst_size = dst.size?
79
+ dst_size && dst_size < src.size
80
+ end
81
+
82
+ # Short inspect
83
+ def inspect
84
+ options_string = self.class.option_definitions.map do |option|
85
+ " @#{option.name}=#{send(option.name).inspect}"
86
+ end.join(',')
87
+ "#<#{self.class}#{options_string}>"
88
+ end
89
+
90
+ private
91
+
92
+ def parse_options(options)
93
+ self.class.option_definitions.each do |option_definition|
94
+ value = option_definition.value(self, options)
95
+ instance_variable_set("@#{option_definition.name}", value)
96
+ end
97
+ end
98
+
99
+ def assert_no_unknown_options!(options)
100
+ known_keys = self.class.option_definitions.map(&:name)
101
+ unknown_options = options.reject{ |key, _value| known_keys.include?(key) }
102
+ return if unknown_options.empty?
103
+ fail ConfigurationError, "unknown options #{unknown_options.inspect} "\
104
+ "for #{self}"
105
+ end
106
+
107
+ # Forward bin resolving to image_optim
108
+ def resolve_bin!(bin)
109
+ @image_optim.resolve_bin!(bin)
110
+ rescue BinResolver::Error => e
111
+ raise e, wrap_resolver_error_message(e.message), e.backtrace
112
+ end
113
+
114
+ def wrap_resolver_error_message(message)
115
+ name = self.class.bin_sym
116
+ "#{name} worker: #{message}; please provide proper binary or "\
117
+ "disable this worker (--no-#{name} argument or "\
118
+ "`:#{name} => false` through options)"
119
+ end
120
+
121
+ # Run command setting priority and hiding output
122
+ def execute(bin, *arguments)
123
+ resolve_bin!(bin)
124
+
125
+ cmd_args = [bin, *arguments].map(&:to_s)
126
+
127
+ start = Time.now
128
+
129
+ success = run_command(cmd_args)
130
+
131
+ if @image_optim.verbose
132
+ seconds = Time.now - start
133
+ $stderr << "#{success ? '✓' : '✗'} #{seconds}s #{cmd_args.shelljoin}\n"
134
+ end
135
+
136
+ success
137
+ end
138
+
139
+ # Run command defining environment, setting nice level, removing output and
140
+ # reraising signal exception
141
+ def run_command(cmd_args)
142
+ args = if RUBY_VERSION < '1.9' || defined?(JRUBY_VERSION)
143
+ %W[
144
+ env PATH=#{@image_optim.env_path.shellescape}
145
+ nice -n #{@image_optim.nice}
146
+ #{cmd_args.shelljoin} > #{Path::NULL} 2>&1
147
+ ].join(' ')
148
+ else
149
+ [
150
+ {'PATH' => @image_optim.env_path},
151
+ %W[nice -n #{@image_optim.nice}],
152
+ cmd_args,
153
+ {:out => Path::NULL, :err => Path::NULL},
154
+ ].flatten
155
+ end
156
+
157
+ seconds_to_timeout = timeout || @image_optim.timeout
158
+
159
+ if seconds_to_timeout > 0
160
+ begin
161
+ Cmd.run_with_timeout(seconds_to_timeout, *args)
162
+ rescue Cmd::TimeoutExceeded
163
+ raise ImageOptim::Worker::TimeoutExceeded
164
+ end
165
+ else
166
+ Cmd.run(*args)
167
+ end
168
+ end
169
+ end
170
+ end