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,37 @@
1
+ require 'image_optim/worker'
2
+ require 'image_optim/option_helpers'
3
+
4
+ class ImageOptim
5
+ class Worker
6
+ # http://advancemame.sourceforge.net/doc-advpng.html
7
+ class Advpng < Worker
8
+ LEVEL_OPTION =
9
+ option(:level, 4, 'Compression level: '\
10
+ '`0` - don\'t compress, '\
11
+ '`1` - fast, '\
12
+ '`2` - normal, '\
13
+ '`3` - extra, '\
14
+ '`4` - extreme') do |v|
15
+ OptionHelpers.limit_with_range(v.to_i, 0..4)
16
+ end
17
+
18
+ TIMEOUT_OPTION = timeout_option
19
+
20
+ def run_order
21
+ 4
22
+ end
23
+
24
+ def optimize(src, dst)
25
+ src.copy(dst)
26
+ args = %W[
27
+ --recompress
28
+ -#{level}
29
+ --quiet
30
+ --
31
+ #{dst}
32
+ ]
33
+ execute(:advpng, *args) && optimized?(src, dst)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,107 @@
1
+ require 'image_optim/bin_resolver'
2
+ require 'image_optim/option_definition'
3
+
4
+ class ImageOptim
5
+ class Worker
6
+ # Class methods of ImageOptim::Worker
7
+ module ClassMethods
8
+ def self.extended(klass)
9
+ klass.instance_variable_set(:@klasses, [])
10
+ end
11
+
12
+ # List of available workers
13
+ def klasses
14
+ @klasses.to_enum
15
+ end
16
+
17
+ # Remember all classes inheriting from this one
18
+ def inherited(base)
19
+ @klasses << base
20
+ end
21
+
22
+ # Underscored class name symbol
23
+ def bin_sym
24
+ @underscored_name ||=
25
+ name.
26
+ split('::').last. # get last part
27
+ gsub(/([a-z])([A-Z])/, '\1_\2').downcase. # convert AbcDef to abc_def
28
+ to_sym
29
+ end
30
+
31
+ def option_definitions
32
+ @option_definitions ||= []
33
+ end
34
+
35
+ def timeout_option
36
+ option(
37
+ :timeout,
38
+ 0,
39
+ 'Number of seconds before worker is timed out. Must be greater than' \
40
+ '0 to enable timeout.'
41
+ ){ |v| v }
42
+ end
43
+
44
+ def option(name, default, type, description = nil, &proc)
45
+ attr_reader name
46
+ OptionDefinition.new(name, default, type, description, &proc).
47
+ tap{ |option_definition| option_definitions << option_definition }
48
+ end
49
+
50
+ # Create hash with format mapped to list of workers sorted by run order
51
+ def create_all_by_format(image_optim, &options_proc)
52
+ by_format = {}
53
+ create_all(image_optim, &options_proc).each do |worker|
54
+ worker.image_formats.each do |format|
55
+ by_format[format] ||= []
56
+ by_format[format] << worker
57
+ end
58
+ end
59
+ by_format
60
+ end
61
+
62
+ # Create list of workers sorted by run order
63
+ # Workers are initialized with options provided through options_proc
64
+ # Resolve all bins of all workers, if there are errors and
65
+ # skip_missing_workers of image_optim is true - show warnings, otherwise
66
+ # fail with one joint exception
67
+ def create_all(image_optim, &options_proc)
68
+ workers = init_all(image_optim, &options_proc)
69
+
70
+ resolved = []
71
+ errors = BinResolver.collect_errors(workers) do |worker|
72
+ worker.resolve_used_bins!
73
+ resolved << worker
74
+ end
75
+
76
+ unless errors.empty?
77
+ messages = errors.map(&:to_s).uniq
78
+ if image_optim.skip_missing_workers
79
+ messages.each{ |message| warn message }
80
+ else
81
+ joint_message = ['Bin resolving errors:', *messages].join("\n")
82
+ fail BinResolver::Error, joint_message
83
+ end
84
+ end
85
+
86
+ resolved.sort_by.with_index{ |worker, i| [worker.run_order, i] }
87
+ end
88
+
89
+ private
90
+
91
+ def init_all(image_optim, &options_proc)
92
+ klasses.map do |klass|
93
+ options = options_proc[klass]
94
+ next if options[:disable]
95
+
96
+ [:allow_lossy, :timeout].each do |option_key|
97
+ if !options.key?(option_key) && klass.method_defined?(option_key)
98
+ options[option_key] = image_optim.send(option_key)
99
+ end
100
+ end
101
+
102
+ klass.init(image_optim, options)
103
+ end.compact.flatten
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,65 @@
1
+ require 'image_optim/worker'
2
+
3
+ class ImageOptim
4
+ class Worker
5
+ # http://www.lcdf.org/gifsicle/
6
+ class Gifsicle < Worker
7
+ # If interlace specified initialize one instance
8
+ # Otherwise initialize two, one with interlace off and one with on
9
+ def self.init(image_optim, options = {})
10
+ return super if options.key?(:interlace)
11
+
12
+ [false, true].map do |interlace|
13
+ new(image_optim, options.merge(:interlace => interlace))
14
+ end
15
+ end
16
+
17
+ INTERLACE_OPTION =
18
+ option(:interlace, false, TrueFalseNil, 'Interlace: '\
19
+ '`true` - interlace on, '\
20
+ '`false` - interlace off, '\
21
+ '`nil` - as is in original image '\
22
+ '(defaults to running two instances, one with interlace off and '\
23
+ 'one with on)') do |v|
24
+ TrueFalseNil.convert(v)
25
+ end
26
+
27
+ LEVEL_OPTION =
28
+ option(:level, 3, 'Compression level: '\
29
+ '`1` - light and fast, '\
30
+ '`2` - normal, '\
31
+ '`3` - heavy (slower)') do |v|
32
+ OptionHelpers.limit_with_range(v.to_i, 1..3)
33
+ end
34
+
35
+ CAREFUL_OPTION =
36
+ option(:careful, false, 'Avoid bugs with some software'){ |v| !!v }
37
+
38
+ TIMEOUT_OPTION = timeout_option
39
+
40
+ def optimize(src, dst)
41
+ args = %W[
42
+ --output=#{dst}
43
+ --no-comments
44
+ --no-names
45
+ --same-delay
46
+ --same-loopcount
47
+ --no-warnings
48
+ --
49
+ #{src}
50
+ ]
51
+
52
+ if resolve_bin!(:gifsicle).version >= '1.85'
53
+ args.unshift '--no-extensions', '--no-app-extensions'
54
+ end
55
+
56
+ unless interlace.nil?
57
+ args.unshift interlace ? '--interlace' : '--no-interlace'
58
+ end
59
+ args.unshift '--careful' if careful
60
+ args.unshift "--optimize=#{level}" if level
61
+ execute(:gifsicle, *args) && optimized?(src, dst)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,47 @@
1
+ require 'image_optim/worker'
2
+ require 'exifr'
3
+
4
+ class ImageOptim
5
+ class Worker
6
+ # http://www.sentex.net/~mwandel/jhead/
7
+ #
8
+ # Jhead internally uses jpegtran which should be on path
9
+ class Jhead < Worker
10
+ TIMEOUT_OPTION = timeout_option
11
+
12
+ ORIENTED = 2..8 # not top-left
13
+
14
+ # Works on jpegs
15
+ def image_formats
16
+ [:jpeg]
17
+ end
18
+
19
+ # Run first, while exif is still present
20
+ def run_order
21
+ -10
22
+ end
23
+
24
+ def used_bins
25
+ [:jhead, :jpegtran]
26
+ end
27
+
28
+ def optimize(src, dst)
29
+ return false unless oriented?(src)
30
+ src.copy(dst)
31
+ args = %W[
32
+ -autorot
33
+ #{dst}
34
+ ]
35
+ resolve_bin!(:jpegtran)
36
+ execute(:jhead, *args) && dst.size?
37
+ end
38
+
39
+ private
40
+
41
+ def oriented?(image)
42
+ exif = EXIFR::JPEG.new(image.to_s)
43
+ ORIENTED.include?(exif.orientation.to_i)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,63 @@
1
+ require 'image_optim/worker'
2
+ require 'image_optim/option_helpers'
3
+
4
+ class ImageOptim
5
+ class Worker
6
+ # http://www.kokkonen.net/tjko/projects.html
7
+ class Jpegoptim < Worker
8
+ ALLOW_LOSSY_OPTION =
9
+ option(:allow_lossy, false, 'Allow limiting maximum quality'){ |v| !!v }
10
+
11
+ STRIP_OPTION =
12
+ option(:strip, :all, Array, 'List of extra markers to strip: '\
13
+ '`:comments`, '\
14
+ '`:exif`, '\
15
+ '`:iptc`, '\
16
+ '`:icc` or '\
17
+ '`:all`') do |v|
18
+ values = Array(v).map(&:to_s)
19
+ known_values = %w[all comments exif iptc icc]
20
+ unknown_values = values - known_values
21
+ unless unknown_values.empty?
22
+ warn "Unknown markers for jpegoptim: #{unknown_values.join(', ')}"
23
+ end
24
+ values & known_values
25
+ end
26
+
27
+ MAX_QUALITY_OPTION =
28
+ option(:max_quality, 100, 'Maximum image quality factor '\
29
+ '`0`..`100`, ignored in default/lossless mode') do |v, opt_def|
30
+ if allow_lossy
31
+ OptionHelpers.limit_with_range(v.to_i, 0..100)
32
+ else
33
+ if v != opt_def.default
34
+ warn "#{self.class.bin_sym} #{opt_def.name} #{v} ignored " \
35
+ 'in lossless mode'
36
+ end
37
+ opt_def.default
38
+ end
39
+ end
40
+
41
+ TIMEOUT_OPTION = timeout_option
42
+
43
+ # Run earlier if max_quality is less than 100
44
+ def run_order
45
+ max_quality < 100 ? -1 : 0
46
+ end
47
+
48
+ def optimize(src, dst)
49
+ src.copy(dst)
50
+ args = %W[
51
+ --quiet
52
+ --
53
+ #{dst}
54
+ ]
55
+ strip.each do |strip_marker|
56
+ args.unshift "--strip-#{strip_marker}"
57
+ end
58
+ args.unshift "--max=#{max_quality}" if max_quality < 100
59
+ execute(:jpegoptim, *args) && optimized?(src, dst)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,49 @@
1
+ require 'image_optim/worker'
2
+ require 'image_optim/option_helpers'
3
+
4
+ class ImageOptim
5
+ class Worker
6
+ # https://github.com/danielgtaylor/jpeg-archive#jpeg-recompress
7
+ class Jpegrecompress < Worker
8
+ ALLOW_LOSSY_OPTION =
9
+ option(:allow_lossy, false, 'Allow worker, it is always lossy'){ |v| !!v }
10
+
11
+ # Initialize only if allow_lossy
12
+ def self.init(image_optim, options = {})
13
+ super if options[:allow_lossy]
14
+ end
15
+
16
+ QUALITY_NAMES = [:low, :medium, :high, :veryhigh].freeze
17
+
18
+ quality_names_desc = QUALITY_NAMES.each_with_index.map do |name, i|
19
+ "`#{i}` - #{name}"
20
+ end.join(', ')
21
+
22
+ QUALITY_OPTION =
23
+ option(:quality, 3, "JPEG quality preset: #{quality_names_desc}") do |v|
24
+ OptionHelpers.limit_with_range(v.to_i, 0...QUALITY_NAMES.length)
25
+ end
26
+
27
+ TIMEOUT_OPTION = timeout_option
28
+
29
+ def used_bins
30
+ [:'jpeg-recompress']
31
+ end
32
+
33
+ # Run early as lossy worker
34
+ def run_order
35
+ -5
36
+ end
37
+
38
+ def optimize(src, dst)
39
+ args = %W[
40
+ --quality #{QUALITY_NAMES[quality]}
41
+ --no-copy
42
+ #{src}
43
+ #{dst}
44
+ ]
45
+ execute(:'jpeg-recompress', *args) && optimized?(src, dst)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,48 @@
1
+ require 'image_optim/worker'
2
+
3
+ class ImageOptim
4
+ class Worker
5
+ # http://www.ijg.org/
6
+ #
7
+ # Uses jpegtran through jpegrescan if enabled, jpegrescan is vendored with
8
+ # this gem
9
+ class Jpegtran < Worker
10
+ COPY_CHUNKS_OPTION =
11
+ option(:copy_chunks, false, 'Copy all chunks'){ |v| !!v }
12
+
13
+ PROGRESSIVE_OPTION =
14
+ option(:progressive, true, 'Create progressive JPEG file'){ |v| !!v }
15
+
16
+ JPEGRESCAN_OPTION =
17
+ option(:jpegrescan, false, 'Use jpegtran through jpegrescan, '\
18
+ 'ignore progressive option'){ |v| !!v }
19
+
20
+ TIMEOUT_OPTION = timeout_option
21
+
22
+ def used_bins
23
+ jpegrescan ? [:jpegtran, :jpegrescan] : [:jpegtran]
24
+ end
25
+
26
+ def optimize(src, dst)
27
+ if jpegrescan
28
+ args = %W[
29
+ #{src}
30
+ #{dst}
31
+ ]
32
+ args.unshift '-s' unless copy_chunks
33
+ resolve_bin!(:jpegtran)
34
+ execute(:jpegrescan, *args) && optimized?(src, dst)
35
+ else
36
+ args = %W[
37
+ -optimize
38
+ -outfile #{dst}
39
+ #{src}
40
+ ]
41
+ args.unshift '-copy', (copy_chunks ? 'all' : 'none')
42
+ args.unshift '-progressive' if progressive
43
+ execute(:jpegtran, *args) && optimized?(src, dst)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,53 @@
1
+ require 'image_optim/worker'
2
+ require 'image_optim/option_helpers'
3
+ require 'image_optim/true_false_nil'
4
+
5
+ class ImageOptim
6
+ class Worker
7
+ # http://optipng.sourceforge.net/
8
+ class Optipng < Worker
9
+ LEVEL_OPTION =
10
+ option(:level, 6, 'Optimization level preset: '\
11
+ '`0` is least, '\
12
+ '`7` is best') do |v|
13
+ OptionHelpers.limit_with_range(v.to_i, 0..7)
14
+ end
15
+
16
+ INTERLACE_OPTION =
17
+ option(:interlace, false, TrueFalseNil, 'Interlace: '\
18
+ '`true` - interlace on, '\
19
+ '`false` - interlace off, '\
20
+ '`nil` - as is in original image') do |v|
21
+ TrueFalseNil.convert(v)
22
+ end
23
+
24
+ STRIP_OPTION =
25
+ option(:strip, true, 'Remove all auxiliary chunks'){ |v| !!v }
26
+
27
+ TIMEOUT_OPTION = timeout_option
28
+
29
+ def run_order
30
+ -4
31
+ end
32
+
33
+ def optimize(src, dst)
34
+ src.copy(dst)
35
+ args = %W[
36
+ -o #{level}
37
+ -quiet
38
+ --
39
+ #{dst}
40
+ ]
41
+ args.unshift "-i#{interlace ? 1 : 0}" unless interlace.nil?
42
+ if resolve_bin!(:optipng).version >= '0.7'
43
+ args.unshift '-strip', 'all' if strip
44
+ end
45
+ execute(:optipng, *args) && optimized?(src, dst)
46
+ end
47
+
48
+ def optimized?(src, dst)
49
+ interlace ? dst.size? : super
50
+ end
51
+ end
52
+ end
53
+ end