openstreetmap-image_optim 0.21.0.1

Sign up to get free protection for your applications and to get access to all the features.
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,3 @@
1
+ class ImageOptim
2
+ class ConfigurationError < StandardError; end
3
+ end
@@ -0,0 +1,57 @@
1
+ require 'image_optim/image_path'
2
+
3
+ class ImageOptim
4
+ # Handles processing of original to result using upto two temp files
5
+ class Handler
6
+ # Holds latest successful result
7
+ attr_reader :result
8
+
9
+ # original must respond to temp_path
10
+ def initialize(original)
11
+ unless original.respond_to?(:temp_path)
12
+ fail ArgumentError, 'original should respond to temp_path'
13
+ end
14
+
15
+ @original = original
16
+ @result = nil
17
+ end
18
+
19
+ # with no associated block, works as new. Otherwise creates instance and
20
+ # passes it to block, runs cleanup and returns result of handler
21
+ def self.for(original)
22
+ handler = new(original)
23
+ if block_given?
24
+ begin
25
+ yield handler
26
+ handler.result
27
+ ensure
28
+ handler.cleanup
29
+ end
30
+ else
31
+ handler
32
+ end
33
+ end
34
+
35
+ # Yields two paths, one to latest successful result or original, second to
36
+ # temp path
37
+ def process
38
+ @src ||= @original
39
+ @dst ||= @original.temp_path
40
+
41
+ return unless yield @src, @dst
42
+ @result = @dst
43
+ if @src == @original
44
+ @src, @dst = @dst, nil
45
+ else
46
+ @src, @dst = @dst, @src
47
+ end
48
+ end
49
+
50
+ # Remove extra temp files
51
+ def cleanup
52
+ return unless @dst
53
+ @dst.unlink
54
+ @dst = nil
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,45 @@
1
+ class ImageOptim
2
+ # Helper methods to manipulate Hash, mainly used in config
3
+ module HashHelpers
4
+ class << self
5
+ # Returns a new hash with all keys of root and nested hashes converted to
6
+ # strings
7
+ def deep_stringify_keys(hash)
8
+ deep_transform_keys(hash, &:to_s)
9
+ end
10
+
11
+ # Returns a new hash with all keys of root and nested hashes converted to
12
+ # symbols
13
+ def deep_symbolise_keys(hash)
14
+ deep_transform_keys(hash, &:to_sym)
15
+ end
16
+
17
+ # Returns a new hash with recursive merge of all keys
18
+ def deep_merge(a, b)
19
+ a.merge(b) do |_k, v_a, v_b|
20
+ if v_a.is_a?(Hash) && v_b.is_a?(Hash)
21
+ deep_merge(v_a, v_b)
22
+ else
23
+ v_b
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # Returns a new hash with all keys of root and nested hashes converted by
31
+ # provided block
32
+ def deep_transform_keys(hash, &block)
33
+ new_hash = {}
34
+ hash.each do |k, v|
35
+ new_hash[block.call(k)] = if v.is_a?(Hash)
36
+ deep_transform_keys(v, &block)
37
+ else
38
+ v
39
+ end
40
+ end
41
+ new_hash
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,25 @@
1
+ require 'image_size'
2
+
3
+ class ImageOptim
4
+ # Getting format of image at path or as data
5
+ class ImageMeta
6
+ def self.for_path(path)
7
+ is = ImageSize.path(path)
8
+ new(is.format)
9
+ rescue ImageSize::FormatError => e
10
+ warn "#{e} (detecting format of image at #{path})"
11
+ end
12
+
13
+ def self.for_data(data)
14
+ is = ImageSize.new(data)
15
+ new(is.format)
16
+ rescue ImageSize::FormatError => e
17
+ warn "#{e} (detecting format of image data)"
18
+ end
19
+
20
+ attr_reader :format
21
+ def initialize(format)
22
+ @format = format
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,68 @@
1
+ require 'fspath'
2
+ require 'image_optim/image_meta'
3
+
4
+ class ImageOptim
5
+ # FSPath with additional helpful methods
6
+ class ImagePath < FSPath
7
+ # Holds optimized image with reference to original and its size
8
+ class Optimized < DelegateClass(self)
9
+ def initialize(path, original_or_size = nil)
10
+ path = ImagePath.convert(path)
11
+ __setobj__(path)
12
+ if original_or_size.is_a?(Integer)
13
+ @original = path
14
+ @original_size = original_or_size
15
+ elsif original_or_size
16
+ @original = ImagePath.convert(original_or_size)
17
+ @original_size = @original.size
18
+ end
19
+ end
20
+
21
+ # Original path, use original_size to get its size as original can be
22
+ # overwritten
23
+ attr_reader :original
24
+
25
+ # Stored size of original
26
+ attr_reader :original_size
27
+ end
28
+
29
+ # Get temp path for this file with same extension
30
+ def temp_path(*args, &block)
31
+ ext = extname
32
+ self.class.temp_file_path([basename(ext).to_s, ext], *args, &block)
33
+ end
34
+
35
+ # Copy file to dest preserving attributes
36
+ def copy(dst)
37
+ FileUtils.copy_file(self, dst, true)
38
+ end
39
+
40
+ # Atomic replace src with self
41
+ def replace(src)
42
+ src = self.class.new(src)
43
+ src.temp_path(src.dirname) do |temp|
44
+ src.copy(temp)
45
+ temp.write(read)
46
+ temp.rename(src.to_s)
47
+ unlink
48
+ end
49
+ end
50
+
51
+ # Get format using ImageSize
52
+ def format
53
+ image_meta = ImageMeta.for_path(self)
54
+ image_meta && image_meta.format
55
+ end
56
+
57
+ # Read binary data
58
+ def binread
59
+ open('rb', &:read)
60
+ end
61
+
62
+ # Returns path if it is already an instance of this class otherwise new
63
+ # instance
64
+ def self.convert(path)
65
+ path.is_a?(self) ? path : new(path)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,11 @@
1
+ class ImageOptim
2
+ # Denote range of non negative integers for worker option
3
+ class NonNegativeIntegerRange
4
+ # Add handling of range of non negative integers in OptionParser instance
5
+ def self.add_to_option_parser(option_parser)
6
+ option_parser.accept(self, /(\d+)(?:-|\.\.)(\d+)/) do |_, m, n|
7
+ m.to_i..n.to_i
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ class ImageOptim
2
+ # Hold information about an option
3
+ class OptionDefinition
4
+ attr_reader :name, :default, :type, :description, :proc
5
+
6
+ def initialize(name, default, type_or_description, description = nil, &proc)
7
+ if type_or_description.is_a?(Class)
8
+ type = type_or_description
9
+ else
10
+ type, description = default.class, type_or_description
11
+ end
12
+
13
+ @name = name.to_sym
14
+ @description = description.to_s
15
+ @default, @type, @proc = default, type, proc
16
+ end
17
+
18
+ # Get value for worker from options
19
+ def value(worker, options)
20
+ value = options.key?(name) ? options[name] : default
21
+ if proc
22
+ if proc.arity == 2
23
+ worker.instance_exec(value, self, &proc)
24
+ else
25
+ worker.instance_exec(value, &proc)
26
+ end
27
+ else
28
+ value
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ class ImageOptim
2
+ # Helper methods for options
3
+ module OptionHelpers
4
+ # Ensure number is in range
5
+ def self.limit_with_range(number, range)
6
+ if range.include?(number)
7
+ number
8
+ elsif number < range.first
9
+ range.first
10
+ elsif range.exclude_end?
11
+ range.last - 1
12
+ else
13
+ range.last
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ require 'image_optim'
2
+
3
+ class ImageOptim
4
+ # Adds image_optim as preprocessor for gif, jpeg, png and svg images
5
+ class Railtie < Rails::Railtie
6
+ initializer 'image_optim.initializer' do |app|
7
+ register_preprocessor(app) if register_preprocessor?(app)
8
+ end
9
+
10
+ def register_preprocessor?(app)
11
+ return if app.config.assets.compress == false
12
+ return if app.config.assets.image_optim == false
13
+
14
+ app.assets
15
+ end
16
+
17
+ def options(app)
18
+ if app.config.assets.image_optim == true
19
+ {}
20
+ else
21
+ app.config.assets.image_optim || {}
22
+ end
23
+ end
24
+
25
+ def register_preprocessor(app)
26
+ image_optim = ImageOptim.new(options(app))
27
+
28
+ processor = proc do |_context, data|
29
+ image_optim.optimize_image_data(data) || data
30
+ end
31
+
32
+ app.assets.register_preprocessor 'image/gif', :image_optim, &processor
33
+ app.assets.register_preprocessor 'image/jpeg', :image_optim, &processor
34
+ app.assets.register_preprocessor 'image/png', :image_optim, &processor
35
+ app.assets.register_preprocessor 'image/svg+xml', :image_optim, &processor
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,139 @@
1
+ require 'image_optim'
2
+ require 'image_optim/hash_helpers'
3
+ require 'image_optim/runner/glob_helpers'
4
+ require 'image_optim/space'
5
+ require 'progress'
6
+ require 'find'
7
+ require 'yaml'
8
+
9
+ class ImageOptim
10
+ # Handling optimization using image_optim binary
11
+ class Runner
12
+ # Collect and output results of optimization
13
+ class Results
14
+ def initialize
15
+ @lines = []
16
+ @original_size_sum = 0
17
+ @optimized_size_sum = 0
18
+ end
19
+
20
+ def add(original, optimized)
21
+ original_size = optimized ? optimized.original_size : original.size
22
+ optimized_size = optimized ? optimized.size : original.size
23
+ @lines << "#{size_percent(original_size, optimized_size)} #{original}"
24
+ @original_size_sum += original_size
25
+ @optimized_size_sum += optimized_size
26
+ end
27
+
28
+ def print
29
+ puts @lines
30
+ puts "Total: #{size_percent(@original_size_sum, @optimized_size_sum)}"
31
+ end
32
+
33
+ private
34
+
35
+ def size_percent(size_a, size_b)
36
+ if size_a == size_b
37
+ "------ #{Space::EMPTY_SPACE}"
38
+ else
39
+ percent = 100 - 100.0 * size_b / size_a
40
+ space = Space.space(size_a - size_b)
41
+ format('%5.2f%% %s', percent, space)
42
+ end
43
+ end
44
+ end
45
+
46
+ def initialize(options)
47
+ options = HashHelpers.deep_symbolise_keys(options)
48
+ @recursive = options.delete(:recursive)
49
+ @progress = options.delete(:show_progress) != false
50
+ @exclude_dir_globs, @exclude_file_globs = %w[dir file].map do |type|
51
+ glob = options.delete(:"exclude_#{type}_glob") || '.*'
52
+ GlobHelpers.expand_braces(glob)
53
+ end
54
+ @image_optim = ImageOptim.new(options)
55
+ end
56
+
57
+ def run!(args)
58
+ to_optimize = find_to_optimize(args)
59
+ unless to_optimize.empty?
60
+ results = Results.new
61
+
62
+ optimize_images!(to_optimize).each do |original, optimized|
63
+ results.add(original, optimized)
64
+ end
65
+
66
+ results.print
67
+ end
68
+
69
+ !@warnings
70
+ end
71
+
72
+ private
73
+
74
+ def optimize_images!(to_optimize, &block)
75
+ to_optimize = to_optimize.with_progress('optimizing') if @progress
76
+ @image_optim.optimize_images!(to_optimize, &block)
77
+ end
78
+
79
+ def find_to_optimize(paths)
80
+ to_optimize = []
81
+ paths.each do |path|
82
+ if File.file?(path)
83
+ if @image_optim.optimizable?(path)
84
+ to_optimize << path
85
+ else
86
+ warning "#{path} is not an image or there is no optimizer for it"
87
+ end
88
+ elsif File.directory?(path)
89
+ if @recursive
90
+ to_optimize += find_to_optimize_recursive(path)
91
+ else
92
+ warning "#{path} is a directory, use --recursive option"
93
+ end
94
+ else
95
+ warning "#{path} is not a file or a directory or does not exist"
96
+ end
97
+ end
98
+ to_optimize
99
+ end
100
+
101
+ def find_to_optimize_recursive(dir)
102
+ to_optimize = []
103
+ Find.find(dir) do |path|
104
+ if File.file?(path)
105
+ next if exclude_file?(dir, path)
106
+ next unless @image_optim.optimizable?(path)
107
+ to_optimize << path
108
+ elsif File.directory?(path)
109
+ Find.prune if dir != path && exclude_dir?(dir, path)
110
+ end
111
+ end
112
+ to_optimize
113
+ end
114
+
115
+ def exclude_dir?(dir, path)
116
+ exclude?(dir, path, @exclude_dir_globs)
117
+ end
118
+
119
+ def exclude_file?(dir, path)
120
+ exclude?(dir, path, @exclude_file_globs)
121
+ end
122
+
123
+ # Check if any of globs matches either part of path relative from dir or
124
+ # just basename
125
+ def exclude?(dir, path, globs)
126
+ relative_path = Pathname(path).relative_path_from(Pathname(dir)).to_s
127
+ basename = File.basename(path)
128
+ globs.any? do |glob|
129
+ File.fnmatch(glob, relative_path, File::FNM_PATHNAME) ||
130
+ File.fnmatch(glob, basename, File::FNM_PATHNAME)
131
+ end
132
+ end
133
+
134
+ def warning(message)
135
+ @warnings = true
136
+ warn message
137
+ end
138
+ end
139
+ end