openstreetmap-image_optim 0.21.0.1

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 (94) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rubocop.yml +65 -0
  4. data/.travis.yml +42 -0
  5. data/CHANGELOG.markdown +272 -0
  6. data/CONTRIBUTING.markdown +10 -0
  7. data/Gemfile +9 -0
  8. data/LICENSE.txt +20 -0
  9. data/README.markdown +344 -0
  10. data/Vagrantfile +33 -0
  11. data/bin/image_optim +28 -0
  12. data/image_optim.gemspec +29 -0
  13. data/lib/image_optim.rb +228 -0
  14. data/lib/image_optim/bin_resolver.rb +144 -0
  15. data/lib/image_optim/bin_resolver/bin.rb +105 -0
  16. data/lib/image_optim/bin_resolver/comparable_condition.rb +60 -0
  17. data/lib/image_optim/bin_resolver/error.rb +6 -0
  18. data/lib/image_optim/bin_resolver/simple_version.rb +31 -0
  19. data/lib/image_optim/cmd.rb +49 -0
  20. data/lib/image_optim/config.rb +205 -0
  21. data/lib/image_optim/configuration_error.rb +3 -0
  22. data/lib/image_optim/handler.rb +57 -0
  23. data/lib/image_optim/hash_helpers.rb +45 -0
  24. data/lib/image_optim/image_meta.rb +25 -0
  25. data/lib/image_optim/image_path.rb +68 -0
  26. data/lib/image_optim/non_negative_integer_range.rb +11 -0
  27. data/lib/image_optim/option_definition.rb +32 -0
  28. data/lib/image_optim/option_helpers.rb +17 -0
  29. data/lib/image_optim/railtie.rb +38 -0
  30. data/lib/image_optim/runner.rb +139 -0
  31. data/lib/image_optim/runner/glob_helpers.rb +45 -0
  32. data/lib/image_optim/runner/option_parser.rb +227 -0
  33. data/lib/image_optim/space.rb +29 -0
  34. data/lib/image_optim/true_false_nil.rb +16 -0
  35. data/lib/image_optim/worker.rb +159 -0
  36. data/lib/image_optim/worker/advpng.rb +35 -0
  37. data/lib/image_optim/worker/class_methods.rb +91 -0
  38. data/lib/image_optim/worker/gifsicle.rb +63 -0
  39. data/lib/image_optim/worker/jhead.rb +43 -0
  40. data/lib/image_optim/worker/jpegoptim.rb +58 -0
  41. data/lib/image_optim/worker/jpegrecompress.rb +44 -0
  42. data/lib/image_optim/worker/jpegtran.rb +46 -0
  43. data/lib/image_optim/worker/optipng.rb +45 -0
  44. data/lib/image_optim/worker/pngcrush.rb +54 -0
  45. data/lib/image_optim/worker/pngout.rb +38 -0
  46. data/lib/image_optim/worker/pngquant.rb +51 -0
  47. data/lib/image_optim/worker/svgo.rb +32 -0
  48. data/script/template/jquery-2.1.3.min.js +4 -0
  49. data/script/template/sortable-0.6.0.min.js +2 -0
  50. data/script/template/worker_analysis.erb +254 -0
  51. data/script/update_worker_options_in_readme +60 -0
  52. data/script/worker_analysis +599 -0
  53. data/spec/image_optim/bin_resolver/comparable_condition_spec.rb +37 -0
  54. data/spec/image_optim/bin_resolver/simple_version_spec.rb +57 -0
  55. data/spec/image_optim/bin_resolver_spec.rb +272 -0
  56. data/spec/image_optim/cmd_spec.rb +66 -0
  57. data/spec/image_optim/config_spec.rb +217 -0
  58. data/spec/image_optim/handler_spec.rb +95 -0
  59. data/spec/image_optim/hash_helpers_spec.rb +76 -0
  60. data/spec/image_optim/image_path_spec.rb +54 -0
  61. data/spec/image_optim/railtie_spec.rb +121 -0
  62. data/spec/image_optim/runner/glob_helpers_spec.rb +25 -0
  63. data/spec/image_optim/runner/option_parser_spec.rb +99 -0
  64. data/spec/image_optim/space_spec.rb +25 -0
  65. data/spec/image_optim/worker_spec.rb +192 -0
  66. data/spec/image_optim_spec.rb +242 -0
  67. data/spec/images/comparison.png +0 -0
  68. data/spec/images/decompressed.jpeg +0 -0
  69. data/spec/images/icecream.gif +0 -0
  70. data/spec/images/image.jpg +0 -0
  71. data/spec/images/invisiblepixels/generate +24 -0
  72. data/spec/images/invisiblepixels/image.png +0 -0
  73. data/spec/images/lena.jpg +0 -0
  74. data/spec/images/orient/0.jpg +0 -0
  75. data/spec/images/orient/1.jpg +0 -0
  76. data/spec/images/orient/2.jpg +0 -0
  77. data/spec/images/orient/3.jpg +0 -0
  78. data/spec/images/orient/4.jpg +0 -0
  79. data/spec/images/orient/5.jpg +0 -0
  80. data/spec/images/orient/6.jpg +0 -0
  81. data/spec/images/orient/7.jpg +0 -0
  82. data/spec/images/orient/8.jpg +0 -0
  83. data/spec/images/orient/generate +23 -0
  84. data/spec/images/orient/original.jpg +0 -0
  85. data/spec/images/quant/64.png +0 -0
  86. data/spec/images/quant/generate +25 -0
  87. data/spec/images/rails.png +0 -0
  88. data/spec/images/test.svg +3 -0
  89. data/spec/images/transparency1.png +0 -0
  90. data/spec/images/transparency2.png +0 -0
  91. data/spec/images/vergroessert.jpg +0 -0
  92. data/spec/spec_helper.rb +64 -0
  93. data/vendor/jpegrescan +143 -0
  94. metadata +308 -0
@@ -0,0 +1,45 @@
1
+ class ImageOptim
2
+ class Runner
3
+ # Helper methods for glob
4
+ module GlobHelpers
5
+ class << self
6
+ # Match inner curly braces in glob
7
+ # Negative lookbehind is not used as is not supported by ruby before 1.9
8
+ BRACE_REGEXP = /
9
+ \A
10
+ (
11
+ (?:.*[^\\]|) # anything ending not with slash or nothing
12
+ (?:\\\\)* # any number of self escaped slashes
13
+ )
14
+ \{ # open brace
15
+ (
16
+ (?:|.*?[^\\]) # nothing or non greedy anything ending not with slash
17
+ (?:\\\\)* # any number of self escaped slashes
18
+ )
19
+ \} # close brace
20
+ (
21
+ .* # what is left
22
+ )
23
+ \z
24
+ /x
25
+
26
+ # Expand curly braces in glob as fnmatch in ruby before 2.0 doesn't
27
+ # support them
28
+ def expand_braces(original_glob)
29
+ expanded = []
30
+ unexpanded = [original_glob]
31
+ while (glob = unexpanded.shift)
32
+ if (m = BRACE_REGEXP.match(glob))
33
+ m[2].split(',', -1).each do |variant|
34
+ unexpanded << "#{m[1]}#{variant}#{m[3]}"
35
+ end
36
+ else
37
+ expanded << glob
38
+ end
39
+ end
40
+ expanded.uniq
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,227 @@
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 (defaults to 10)') do |nice|
137
+ options[:nice] = nice
138
+ end
139
+
140
+ op.on('--[no-]pack', 'Require image_optim_pack or disable it, '\
141
+ 'by default image_optim_pack will be used if available, '\
142
+ 'will turn on skip-missing-workers unless explicitly disabled') do |pack|
143
+ options[:pack] = pack
144
+ end
145
+
146
+ op.separator nil
147
+ op.separator ' Disabling workers:'
148
+
149
+ op.on('--[no-]skip-missing-workers', 'Skip workers with missing or '\
150
+ 'problematic binaries') do |skip|
151
+ options[:skip_missing_workers] = skip
152
+ end
153
+
154
+ ImageOptim::Worker.klasses.each do |klass|
155
+ bin = klass.bin_sym
156
+ op.on("--no-#{bin}", "disable #{bin} worker") do |enable|
157
+ options[bin] = enable
158
+ end
159
+ end
160
+
161
+ op.separator nil
162
+ op.separator ' Worker options:'
163
+
164
+ op.on('--allow-lossy', 'Allow lossy workers and '\
165
+ 'optimizations') do |allow_lossy|
166
+ options[:allow_lossy] = allow_lossy
167
+ end
168
+
169
+ op.separator nil
170
+
171
+ ImageOptim::Worker.klasses.each_with_index do |klass, i|
172
+ next if klass.option_definitions.empty?
173
+ op.separator nil unless i.zero?
174
+
175
+ bin = klass.bin_sym
176
+ klass.option_definitions.each do |option_definition|
177
+ name = option_definition.name.to_s.gsub('_', '-')
178
+ default = option_definition.default
179
+ type = option_definition.type
180
+
181
+ type, marking = case
182
+ when [TrueClass, FalseClass, ImageOptim::TrueFalseNil].include?(type)
183
+ [type, 'B']
184
+ when Integer >= type
185
+ [Integer, 'N']
186
+ when Array >= type
187
+ [Array, 'a,b,c']
188
+ when ImageOptim::NonNegativeIntegerRange == type
189
+ [type, 'M-N']
190
+ else
191
+ fail "Unknown type #{type}"
192
+ end
193
+
194
+ description = option_definition.description.gsub(' - ', ' - ')
195
+ unless description['(defaults']
196
+ description << " (defaults to #{default})"
197
+ end
198
+
199
+ op.on("--#{bin}-#{name} #{marking}", type, description) do |value|
200
+ options[bin] = {} unless options[bin].is_a?(Hash)
201
+ options[bin][option_definition.name.to_sym] = value
202
+ end
203
+ end
204
+ end
205
+
206
+ op.separator nil
207
+ op.separator ' Common options:'
208
+
209
+ op.on_tail('-v', '--verbose', 'Verbose output') do
210
+ options[:verbose] = true
211
+ end
212
+
213
+ op.on_tail('-h', '--help', 'Show help and exit') do
214
+ puts op.help
215
+ exit
216
+ end
217
+
218
+ op.on_tail('--version', 'Show version and exit') do
219
+ puts ImageOptim.version
220
+ exit
221
+ end
222
+
223
+ op.on_tail('--info', 'Show environment info and exit') do
224
+ options[:verbose] = true
225
+ options[:only_info] = true
226
+ end
227
+ 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 == 0
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,159 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'image_optim/cmd'
4
+ require 'image_optim/configuration_error'
5
+ require 'image_optim/worker/class_methods'
6
+ require 'shellwords'
7
+ require 'English'
8
+
9
+ class ImageOptim
10
+ # Base class for all workers
11
+ class Worker
12
+ extend ClassMethods
13
+
14
+ class << self
15
+ # Default init for worker is new
16
+ # Check example of override in gifsicle worker
17
+ alias_method :init, :new
18
+ end
19
+
20
+ # Allow lossy optimizations
21
+ attr_reader :allow_lossy
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
+ @allow_lossy = !!options.delete(:allow_lossy)
30
+ parse_options(options)
31
+ assert_no_unknown_options!(options)
32
+ end
33
+
34
+ # Return hash with worker options
35
+ def options
36
+ hash = {}
37
+ self.class.option_definitions.each do |option|
38
+ hash[option.name] = send(option.name)
39
+ end
40
+ hash
41
+ end
42
+
43
+ # Optimize image at src, output at dst, must be overriden in subclass
44
+ # return true on success
45
+ def optimize(_src, _dst)
46
+ fail NotImplementedError, "implement method optimize in #{self.class}"
47
+ end
48
+
49
+ # List of formats which worker can optimize
50
+ def image_formats
51
+ format_from_name = self.class.name.downcase[/gif|jpeg|png|svg/]
52
+ unless format_from_name
53
+ fail "#{self.class}: can't guess applicable format from worker name"
54
+ end
55
+ [format_from_name.to_sym]
56
+ end
57
+
58
+ # Ordering in list of workers, 0 by default
59
+ def run_order
60
+ 0
61
+ end
62
+
63
+ # List of bins used by worker
64
+ def used_bins
65
+ [self.class.bin_sym]
66
+ end
67
+
68
+ # Resolve used bins, raise exception concatenating all messages
69
+ def resolve_used_bins!
70
+ errors = BinResolver.collect_errors(used_bins) do |bin|
71
+ @image_optim.resolve_bin!(bin)
72
+ end
73
+ return if errors.empty?
74
+ fail BinResolver::Error, wrap_resolver_error_message(errors.join(', '))
75
+ end
76
+
77
+ # Check if operation resulted in optimized file
78
+ def optimized?(src, dst)
79
+ dst.size? && dst.size < src.size
80
+ end
81
+
82
+ # Short inspect
83
+ def inspect
84
+ options_string = options.map do |name, value|
85
+ " @#{name}=#{value.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} > /dev/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 => '/dev/null', :err => '/dev/null'},
154
+ ].flatten
155
+ end
156
+ Cmd.run(*args)
157
+ end
158
+ end
159
+ end