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,3 @@
1
+ class ImageOptim
2
+ class ConfigurationError < StandardError; end
3
+ end
@@ -0,0 +1,57 @@
1
+ require 'image_optim/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 |_key, value_a, value_b|
20
+ if value_a.is_a?(Hash) && value_b.is_a?(Hash)
21
+ deep_merge(value_a, value_b)
22
+ else
23
+ value_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 |key, value|
35
+ new_hash[yield key] = if value.is_a?(Hash)
36
+ deep_transform_keys(value, &block)
37
+ else
38
+ value
39
+ end
40
+ end
41
+ new_hash
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,20 @@
1
+ require 'image_size'
2
+
3
+ class ImageOptim
4
+ # Getting format of image at path or as data
5
+ module ImageMeta
6
+ def self.format_for_path(path)
7
+ is = ImageSize.path(path)
8
+ is.format if is
9
+ rescue ImageSize::FormatError => e
10
+ warn "#{e} (detecting format of image at #{path})"
11
+ end
12
+
13
+ def self.format_for_data(data)
14
+ is = ImageSize.new(data)
15
+ is.format if is
16
+ rescue ImageSize::FormatError => e
17
+ warn "#{e} (detecting format of image data)"
18
+ end
19
+ end
20
+ 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,25 @@
1
+ require 'image_optim/path'
2
+
3
+ class ImageOptim
4
+ # Holds optimized image with reference to original and its size
5
+ class OptimizedPath < DelegateClass(Path)
6
+ def initialize(path, original_or_size = nil)
7
+ path = Path.convert(path)
8
+ __setobj__(path)
9
+ if original_or_size.is_a?(Integer)
10
+ @original = path
11
+ @original_size = original_or_size
12
+ elsif original_or_size
13
+ @original = Path.convert(original_or_size)
14
+ @original_size = @original.size
15
+ end
16
+ end
17
+
18
+ # Original path, use original_size to get its size as original can be
19
+ # overwritten
20
+ attr_reader :original
21
+
22
+ # Stored size of original
23
+ attr_reader :original_size
24
+ end
25
+ end
@@ -0,0 +1,38 @@
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
+
32
+ # Describe default value, returns string as is otherwise surrounds
33
+ # inspected value with backticks
34
+ def default_description
35
+ default.is_a?(String) ? default : "`#{default.inspect}`"
36
+ end
37
+ end
38
+ 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,70 @@
1
+ require 'fspath'
2
+ require 'image_optim/image_meta'
3
+
4
+ class ImageOptim
5
+ # FSPath with additional helpful methods
6
+ class Path < FSPath
7
+ NULL = if defined?(IO::NULL)
8
+ IO::NULL
9
+ else
10
+ %w[/dev/null NUL: NUL nul NIL: NL:].find{ |dev| File.exist?(dev) }
11
+ end
12
+
13
+ # Get temp path for this file with same extension
14
+ def temp_path(*args, &block)
15
+ ext = extname
16
+ self.class.temp_file_path([basename(ext).to_s, ext], *args, &block)
17
+ end
18
+
19
+ # Copy file to dst, optionally preserving attributes
20
+ #
21
+ # See FileUtils.copy_file
22
+ def copy(dst, preserve = false)
23
+ FileUtils.copy_file(self, dst, preserve)
24
+ end
25
+
26
+ # Move file to dst: rename on same device, copy and unlink original
27
+ # otherwise
28
+ #
29
+ # See FileUtils.mv
30
+ def move(dst)
31
+ FileUtils.move(self, dst)
32
+ end
33
+
34
+ # Copy metadata: uid, gid, mode, optionally atime and mtime
35
+ #
36
+ # Adapted from FileUtils::Entry_#copy_metadata by Minero Aoki
37
+ def copy_metadata(dst, time = false)
38
+ stat = lstat
39
+ dst.utime(stat.atime, stat.mtime) if time
40
+ begin
41
+ dst.chown(stat.uid, stat.gid)
42
+ rescue Errno::EPERM
43
+ dst.chmod(stat.mode & 0o1777)
44
+ else
45
+ dst.chmod(stat.mode)
46
+ end
47
+ end
48
+
49
+ # Atomic replace dst with self
50
+ def replace(dst)
51
+ dst = self.class.new(dst)
52
+ dst.temp_path(dst.dirname) do |temp|
53
+ move(temp)
54
+ dst.copy_metadata(temp)
55
+ temp.rename(dst.to_s)
56
+ end
57
+ end
58
+
59
+ # Get format using ImageSize
60
+ def image_format
61
+ ImageMeta.format_for_path(self)
62
+ end
63
+
64
+ # Returns path if it is already an instance of this class otherwise new
65
+ # instance
66
+ def self.convert(path)
67
+ path.is_a?(self) ? path : new(path)
68
+ end
69
+ end
70
+ 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
@@ -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