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