image_optim 0.28.0 → 0.31.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check.yml +89 -0
  3. data/.pre-commit-hooks.yaml +9 -0
  4. data/.rubocop.yml +6 -3
  5. data/CHANGELOG.markdown +20 -0
  6. data/CONTRIBUTING.markdown +1 -1
  7. data/Gemfile +1 -7
  8. data/LICENSE.txt +1 -1
  9. data/README.markdown +18 -9
  10. data/Vagrantfile +1 -1
  11. data/image_optim.gemspec +6 -3
  12. data/lib/image_optim/bin_resolver/bin.rb +9 -9
  13. data/lib/image_optim/cache.rb +6 -0
  14. data/lib/image_optim/cmd.rb +45 -6
  15. data/lib/image_optim/config.rb +11 -5
  16. data/lib/image_optim/elapsed_time.rb +26 -0
  17. data/lib/image_optim/errors.rb +9 -0
  18. data/lib/image_optim/path.rb +1 -1
  19. data/lib/image_optim/runner/option_parser.rb +21 -17
  20. data/lib/image_optim/runner.rb +1 -1
  21. data/lib/image_optim/timer.rb +25 -0
  22. data/lib/image_optim/worker/advpng.rb +7 -7
  23. data/lib/image_optim/worker/gifsicle.rb +11 -11
  24. data/lib/image_optim/worker/jhead.rb +2 -2
  25. data/lib/image_optim/worker/jpegoptim.rb +11 -11
  26. data/lib/image_optim/worker/jpegrecompress.rb +6 -6
  27. data/lib/image_optim/worker/jpegtran.rb +4 -4
  28. data/lib/image_optim/worker/optipng.rb +7 -7
  29. data/lib/image_optim/worker/oxipng.rb +53 -0
  30. data/lib/image_optim/worker/pngcrush.rb +6 -6
  31. data/lib/image_optim/worker/pngout.rb +7 -7
  32. data/lib/image_optim/worker/pngquant.rb +9 -9
  33. data/lib/image_optim/worker/svgo.rb +2 -2
  34. data/lib/image_optim/worker.rb +32 -29
  35. data/lib/image_optim.rb +16 -10
  36. data/script/update_worker_options_in_readme +1 -1
  37. data/script/worker_analysis +16 -18
  38. data/spec/image_optim/bin_resolver_spec.rb +5 -5
  39. data/spec/image_optim/cache_path_spec.rb +7 -10
  40. data/spec/image_optim/cache_spec.rb +7 -7
  41. data/spec/image_optim/cmd_spec.rb +64 -6
  42. data/spec/image_optim/config_spec.rb +36 -20
  43. data/spec/image_optim/elapsed_time_spec.rb +14 -0
  44. data/spec/image_optim/handler_spec.rb +1 -1
  45. data/spec/image_optim/hash_helpers_spec.rb +18 -18
  46. data/spec/image_optim/option_definition_spec.rb +6 -6
  47. data/spec/image_optim/path_spec.rb +8 -11
  48. data/spec/image_optim/runner/option_parser_spec.rb +4 -4
  49. data/spec/image_optim/timer_spec.rb +32 -0
  50. data/spec/image_optim/worker/jpegrecompress_spec.rb +2 -2
  51. data/spec/image_optim/worker/optipng_spec.rb +11 -11
  52. data/spec/image_optim/worker/oxipng_spec.rb +89 -0
  53. data/spec/image_optim/worker/pngquant_spec.rb +5 -5
  54. data/spec/image_optim/worker_spec.rb +17 -17
  55. data/spec/image_optim_spec.rb +47 -10
  56. data/spec/images/invisiblepixels/generate +1 -1
  57. data/spec/spec_helper.rb +24 -21
  58. metadata +35 -11
  59. data/.appveyor.yml +0 -53
  60. data/.travis.yml +0 -48
@@ -38,7 +38,7 @@ class ImageOptim
38
38
  if size_a == size_b
39
39
  "------ #{Space::EMPTY_SPACE}"
40
40
  else
41
- percent = 100 - 100.0 * size_b / size_a
41
+ percent = 100 - (100.0 * size_b / size_a)
42
42
  space = Space.space(size_a - size_b)
43
43
  format('%5.2f%% %s', percent, space)
44
44
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'image_optim/elapsed_time'
4
+
5
+ class ImageOptim
6
+ # Hold start time and timeout
7
+ class Timer
8
+ include ElapsedTime
9
+
10
+ def initialize(seconds)
11
+ @start = now
12
+ @seconds = seconds
13
+ end
14
+
15
+ def elapsed
16
+ now - @start
17
+ end
18
+
19
+ def left
20
+ @seconds - elapsed
21
+ end
22
+
23
+ alias_method :to_f, :left
24
+ end
25
+ end
@@ -9,11 +9,11 @@ class ImageOptim
9
9
  class Advpng < Worker
10
10
  LEVEL_OPTION =
11
11
  option(:level, 4, 'Compression level: '\
12
- '`0` - don\'t compress, '\
13
- '`1` - fast, '\
14
- '`2` - normal, '\
15
- '`3` - extra, '\
16
- '`4` - extreme') do |v|
12
+ '`0` - don\'t compress, '\
13
+ '`1` - fast, '\
14
+ '`2` - normal, '\
15
+ '`3` - extra, '\
16
+ '`4` - extreme') do |v|
17
17
  OptionHelpers.limit_with_range(v.to_i, 0..4)
18
18
  end
19
19
 
@@ -21,7 +21,7 @@ class ImageOptim
21
21
  4
22
22
  end
23
23
 
24
- def optimize(src, dst)
24
+ def optimize(src, dst, options = {})
25
25
  src.copy(dst)
26
26
  args = %W[
27
27
  --recompress
@@ -30,7 +30,7 @@ class ImageOptim
30
30
  --
31
31
  #{dst}
32
32
  ]
33
- execute(:advpng, *args) && optimized?(src, dst)
33
+ execute(:advpng, args, options) && optimized?(src, dst)
34
34
  end
35
35
  end
36
36
  end
@@ -12,32 +12,32 @@ class ImageOptim
12
12
  return super if options.key?(:interlace)
13
13
 
14
14
  [false, true].map do |interlace|
15
- new(image_optim, options.merge(:interlace => interlace))
15
+ new(image_optim, options.merge(interlace: interlace))
16
16
  end
17
17
  end
18
18
 
19
19
  INTERLACE_OPTION =
20
20
  option(:interlace, false, TrueFalseNil, 'Interlace: '\
21
- '`true` - interlace on, '\
22
- '`false` - interlace off, '\
23
- '`nil` - as is in original image '\
24
- '(defaults to running two instances, one with interlace off and '\
25
- 'one with on)') do |v|
21
+ '`true` - interlace on, '\
22
+ '`false` - interlace off, '\
23
+ '`nil` - as is in original image '\
24
+ '(defaults to running two instances, one with interlace off and '\
25
+ 'one with on)') do |v|
26
26
  TrueFalseNil.convert(v)
27
27
  end
28
28
 
29
29
  LEVEL_OPTION =
30
30
  option(:level, 3, 'Compression level: '\
31
- '`1` - light and fast, '\
32
- '`2` - normal, '\
33
- '`3` - heavy (slower)') do |v|
31
+ '`1` - light and fast, '\
32
+ '`2` - normal, '\
33
+ '`3` - heavy (slower)') do |v|
34
34
  OptionHelpers.limit_with_range(v.to_i, 1..3)
35
35
  end
36
36
 
37
37
  CAREFUL_OPTION =
38
38
  option(:careful, false, 'Avoid bugs with some software'){ |v| !!v }
39
39
 
40
- def optimize(src, dst)
40
+ def optimize(src, dst, options = {})
41
41
  args = %W[
42
42
  --output=#{dst}
43
43
  --no-comments
@@ -58,7 +58,7 @@ class ImageOptim
58
58
  end
59
59
  args.unshift '--careful' if careful
60
60
  args.unshift "--optimize=#{level}" if level
61
- execute(:gifsicle, *args) && optimized?(src, dst)
61
+ execute(:gifsicle, args, options) && optimized?(src, dst)
62
62
  end
63
63
  end
64
64
  end
@@ -25,7 +25,7 @@ class ImageOptim
25
25
  [:jhead, :jpegtran]
26
26
  end
27
27
 
28
- def optimize(src, dst)
28
+ def optimize(src, dst, options = {})
29
29
  return false unless oriented?(src)
30
30
 
31
31
  src.copy(dst)
@@ -34,7 +34,7 @@ class ImageOptim
34
34
  #{dst}
35
35
  ]
36
36
  resolve_bin!(:jpegtran)
37
- execute(:jhead, *args) && dst.size?
37
+ execute(:jhead, args, options) && dst.size?
38
38
  end
39
39
 
40
40
  private
@@ -12,13 +12,13 @@ class ImageOptim
12
12
 
13
13
  STRIP_OPTION =
14
14
  option(:strip, :all, Array, 'List of markers to strip: '\
15
- '`:com`, '\
16
- '`:exif`, '\
17
- '`:iptc`, '\
18
- '`:icc`, '\
19
- '`:xmp`, '\
20
- '`:none` or '\
21
- '`:all`') do |v|
15
+ '`:com`, '\
16
+ '`:exif`, '\
17
+ '`:iptc`, '\
18
+ '`:icc`, '\
19
+ '`:xmp`, '\
20
+ '`:none` or '\
21
+ '`:all`') do |v|
22
22
  values = Array(v).map(&:to_s)
23
23
  known_values = %w[com exif iptc icc xmp none all]
24
24
  unknown_values = values - known_values
@@ -30,13 +30,13 @@ class ImageOptim
30
30
 
31
31
  MAX_QUALITY_OPTION =
32
32
  option(:max_quality, 100, 'Maximum image quality factor '\
33
- '`0`..`100`, ignored in default/lossless mode') do |v, opt_def|
33
+ '`0`..`100`, ignored in default/lossless mode') do |v, opt_def|
34
34
  if allow_lossy
35
35
  OptionHelpers.limit_with_range(v.to_i, 0..100)
36
36
  else
37
37
  if v != opt_def.default
38
38
  warn "#{self.class.bin_sym} #{opt_def.name} #{v} ignored " \
39
- 'in lossless mode'
39
+ 'in lossless mode'
40
40
  end
41
41
  opt_def.default
42
42
  end
@@ -47,7 +47,7 @@ class ImageOptim
47
47
  max_quality < 100 ? -1 : 0
48
48
  end
49
49
 
50
- def optimize(src, dst)
50
+ def optimize(src, dst, options = {})
51
51
  src.copy(dst)
52
52
  args = %W[
53
53
  --quiet
@@ -58,7 +58,7 @@ class ImageOptim
58
58
  args.unshift "--strip-#{strip_marker}"
59
59
  end
60
60
  args.unshift "--max=#{max_quality}" if max_quality < 100
61
- execute(:jpegoptim, *args) && optimized?(src, dst)
61
+ execute(:jpegoptim, args, options) && optimized?(src, dst)
62
62
  end
63
63
  end
64
64
  end
@@ -28,10 +28,10 @@ class ImageOptim
28
28
 
29
29
  METHOD_OPTION =
30
30
  option(:method, 'ssim', 'Comparison Metric: '\
31
- '`mpe` - Mean pixel error, '\
32
- '`ssim` - Structural similarity, '\
33
- '`ms-ssim` - Multi-scale structural similarity (slow!), '\
34
- '`smallfry` - Linear-weighted BBCQ-like (may be patented)') do |v, opt_def|
31
+ '`mpe` - Mean pixel error, '\
32
+ '`ssim` - Structural similarity, '\
33
+ '`ms-ssim` - Multi-scale structural similarity (slow!), '\
34
+ '`smallfry` - Linear-weighted BBCQ-like (may be patented)') do |v, opt_def|
35
35
  if %w[mpe ssim ms-ssim smallfry].include? v
36
36
  v
37
37
  else
@@ -49,7 +49,7 @@ class ImageOptim
49
49
  -5
50
50
  end
51
51
 
52
- def optimize(src, dst)
52
+ def optimize(src, dst, options = {})
53
53
  args = %W[
54
54
  --quality #{QUALITY_NAMES[quality]}
55
55
  --method #{method}
@@ -57,7 +57,7 @@ class ImageOptim
57
57
  #{src}
58
58
  #{dst}
59
59
  ]
60
- execute(:'jpeg-recompress', *args) && optimized?(src, dst)
60
+ execute(:'jpeg-recompress', args, options) && optimized?(src, dst)
61
61
  end
62
62
  end
63
63
  end
@@ -17,13 +17,13 @@ class ImageOptim
17
17
 
18
18
  JPEGRESCAN_OPTION =
19
19
  option(:jpegrescan, true, 'Use jpegtran through jpegrescan, '\
20
- 'ignore progressive option'){ |v| !!v }
20
+ 'ignore progressive option'){ |v| !!v }
21
21
 
22
22
  def used_bins
23
23
  jpegrescan ? [:jpegtran, :jpegrescan] : [:jpegtran]
24
24
  end
25
25
 
26
- def optimize(src, dst)
26
+ def optimize(src, dst, options = {})
27
27
  if jpegrescan
28
28
  args = %W[
29
29
  #{src}
@@ -31,7 +31,7 @@ class ImageOptim
31
31
  ]
32
32
  args.unshift '-s' unless copy_chunks
33
33
  resolve_bin!(:jpegtran)
34
- execute(:jpegrescan, *args) && optimized?(src, dst)
34
+ execute(:jpegrescan, args, options) && optimized?(src, dst)
35
35
  else
36
36
  args = %W[
37
37
  -optimize
@@ -40,7 +40,7 @@ class ImageOptim
40
40
  ]
41
41
  args.unshift '-copy', (copy_chunks ? 'all' : 'none')
42
42
  args.unshift '-progressive' if progressive
43
- execute(:jpegtran, *args) && optimized?(src, dst)
43
+ execute(:jpegtran, args, options) && optimized?(src, dst)
44
44
  end
45
45
  end
46
46
  end
@@ -10,16 +10,16 @@ class ImageOptim
10
10
  class Optipng < Worker
11
11
  LEVEL_OPTION =
12
12
  option(:level, 6, 'Optimization level preset: '\
13
- '`0` is least, '\
14
- '`7` is best') do |v|
13
+ '`0` is least, '\
14
+ '`7` is best') do |v|
15
15
  OptionHelpers.limit_with_range(v.to_i, 0..7)
16
16
  end
17
17
 
18
18
  INTERLACE_OPTION =
19
19
  option(:interlace, false, TrueFalseNil, 'Interlace: '\
20
- '`true` - interlace on, '\
21
- '`false` - interlace off, '\
22
- '`nil` - as is in original image') do |v|
20
+ '`true` - interlace on, '\
21
+ '`false` - interlace off, '\
22
+ '`nil` - as is in original image') do |v|
23
23
  TrueFalseNil.convert(v)
24
24
  end
25
25
 
@@ -30,7 +30,7 @@ class ImageOptim
30
30
  -4
31
31
  end
32
32
 
33
- def optimize(src, dst)
33
+ def optimize(src, dst, options = {})
34
34
  src.copy(dst)
35
35
  args = %W[
36
36
  -o #{level}
@@ -42,7 +42,7 @@ class ImageOptim
42
42
  if strip && resolve_bin!(:optipng).version >= '0.7'
43
43
  args.unshift '-strip', 'all'
44
44
  end
45
- execute(:optipng, *args) && optimized?(src, dst)
45
+ execute(:optipng, args, options) && optimized?(src, dst)
46
46
  end
47
47
 
48
48
  def optimized?(src, dst)
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'image_optim/worker'
4
+ require 'image_optim/option_helpers'
5
+ require 'image_optim/true_false_nil'
6
+
7
+ class ImageOptim
8
+ class Worker
9
+ # https://github.com/shssoichiro/oxipng
10
+ class Oxipng < Worker
11
+ LEVEL_OPTION =
12
+ option(:level, 3, 'Optimization level preset: '\
13
+ '`0` is least, '\
14
+ '`6` is best') do |v|
15
+ OptionHelpers.limit_with_range(v.to_i, 0..6)
16
+ end
17
+
18
+ INTERLACE_OPTION =
19
+ option(:interlace, false, TrueFalseNil, 'Interlace: '\
20
+ '`true` - interlace on, '\
21
+ '`false` - interlace off, '\
22
+ '`nil` - as is in original image') do |v|
23
+ TrueFalseNil.convert(v)
24
+ end
25
+
26
+ STRIP_OPTION =
27
+ option(:strip, true, 'Remove all auxiliary chunks'){ |v| !!v }
28
+
29
+ def run_order
30
+ -4
31
+ end
32
+
33
+ def optimize(src, dst, options = {})
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 strip
43
+ args.unshift '--strip', 'all'
44
+ end
45
+ execute(:oxipng, args, options) && optimized?(src, dst)
46
+ end
47
+
48
+ def optimized?(src, dst)
49
+ interlace ? dst.size? : super
50
+ end
51
+ end
52
+ end
53
+ end
@@ -8,18 +8,18 @@ class ImageOptim
8
8
  class Pngcrush < Worker
9
9
  CHUNKS_OPTION =
10
10
  option(:chunks, :alla, Array, 'List of chunks to remove or '\
11
- '`:alla` - all except tRNS/transparency or '\
12
- '`:allb` - all except tRNS and gAMA/gamma') do |v|
11
+ '`:alla` - all except tRNS/transparency or '\
12
+ '`:allb` - all except tRNS and gAMA/gamma') do |v|
13
13
  Array(v).map(&:to_s)
14
14
  end
15
15
 
16
16
  FIX_OPTION =
17
17
  option(:fix, false, 'Fix otherwise fatal conditions '\
18
- 'such as bad CRCs'){ |v| !!v }
18
+ 'such as bad CRCs'){ |v| !!v }
19
19
 
20
20
  BRUTE_OPTION =
21
21
  option(:brute, false, 'Brute force try all methods, '\
22
- 'very time-consuming and generally not worthwhile'){ |v| !!v }
22
+ 'very time-consuming and generally not worthwhile'){ |v| !!v }
23
23
 
24
24
  BLACKEN_OPTION =
25
25
  option(:blacken, true, 'Blacken fully transparent pixels'){ |v| !!v }
@@ -28,7 +28,7 @@ class ImageOptim
28
28
  -6
29
29
  end
30
30
 
31
- def optimize(src, dst)
31
+ def optimize(src, dst, options = {})
32
32
  flags = %w[
33
33
  -reduce
34
34
  -cc
@@ -49,7 +49,7 @@ class ImageOptim
49
49
  #{dst}
50
50
  ]
51
51
 
52
- execute(:pngcrush, *args) && optimized?(src, dst)
52
+ execute(:pngcrush, args, options) && optimized?(src, dst)
53
53
  end
54
54
  end
55
55
  end
@@ -12,11 +12,11 @@ class ImageOptim
12
12
 
13
13
  STRATEGY_OPTION =
14
14
  option(:strategy, 0, 'Strategy: '\
15
- '`0` - xtreme, '\
16
- '`1` - intense, '\
17
- '`2` - longest Match, '\
18
- '`3` - huffman Only, '\
19
- '`4` - uncompressed') do |v|
15
+ '`0` - xtreme, '\
16
+ '`1` - intense, '\
17
+ '`2` - longest Match, '\
18
+ '`3` - huffman Only, '\
19
+ '`4` - uncompressed') do |v|
20
20
  OptionHelpers.limit_with_range(v.to_i, 0..4)
21
21
  end
22
22
 
@@ -24,7 +24,7 @@ class ImageOptim
24
24
  2
25
25
  end
26
26
 
27
- def optimize(src, dst)
27
+ def optimize(src, dst, options = {})
28
28
  args = %W[
29
29
  -k#{copy_chunks ? 1 : 0}
30
30
  -s#{strategy}
@@ -33,7 +33,7 @@ class ImageOptim
33
33
  #{src}
34
34
  #{dst}
35
35
  ]
36
- execute(:pngout, *args) && optimized?(src, dst)
36
+ execute(:pngout, args, options) && optimized?(src, dst)
37
37
  rescue SignalException => e
38
38
  raise unless Signal.list.key(e.signo) == 'SEGV'
39
39
  raise unless resolve_bin!(:pngout).version <= '20150920'
@@ -19,9 +19,9 @@ class ImageOptim
19
19
  QUALITY_OPTION =
20
20
  option(:quality, '`100..100`, `0..100` in lossy mode',
21
21
  NonNegativeIntegerRange, 'min..max - don\'t '\
22
- 'save below min, use less colors below max (both in range `0..100`; '\
23
- 'in yaml - `!ruby/range 0..100`), ignored in default/lossless '\
24
- 'mode') do |v, opt_def|
22
+ 'save below min, use less colors below max (both in range `0..100`; '\
23
+ 'in yaml - `!ruby/range 0..100`), ignored in default/lossless '\
24
+ 'mode') do |v, opt_def|
25
25
  if allow_lossy
26
26
  if v == opt_def.default
27
27
  0..100
@@ -32,7 +32,7 @@ class ImageOptim
32
32
  else
33
33
  if v != opt_def.default
34
34
  warn "#{self.class.bin_sym} #{opt_def.name} #{v} ignored " \
35
- 'in lossless mode'
35
+ 'in lossless mode'
36
36
  end
37
37
  100..100
38
38
  end
@@ -40,9 +40,9 @@ class ImageOptim
40
40
 
41
41
  SPEED_OPTION =
42
42
  option(:speed, 3, 'speed/quality trade-off: '\
43
- '`1` - slow, '\
44
- '`3` - default, '\
45
- '`11` - fast & rough') do |v|
43
+ '`1` - slow, '\
44
+ '`3` - default, '\
45
+ '`11` - fast & rough') do |v|
46
46
  OptionHelpers.limit_with_range(v.to_i, 1..11)
47
47
  end
48
48
 
@@ -50,7 +50,7 @@ class ImageOptim
50
50
  -2
51
51
  end
52
52
 
53
- def optimize(src, dst)
53
+ def optimize(src, dst, options = {})
54
54
  args = %W[
55
55
  --quality=#{quality.begin}-#{quality.end}
56
56
  --speed=#{speed}
@@ -61,7 +61,7 @@ class ImageOptim
61
61
  --
62
62
  #{src}
63
63
  ]
64
- execute(:pngquant, *args) && optimized?(src, dst)
64
+ execute(:pngquant, args, options) && optimized?(src, dst)
65
65
  end
66
66
  end
67
67
  end
@@ -16,7 +16,7 @@ class ImageOptim
16
16
  Array(v).map(&:to_s)
17
17
  end
18
18
 
19
- def optimize(src, dst)
19
+ def optimize(src, dst, options = {})
20
20
  args = %W[
21
21
  --input #{src}
22
22
  --output #{dst}
@@ -27,7 +27,7 @@ class ImageOptim
27
27
  enable_plugins.each do |plugin_name|
28
28
  args.unshift "--enable=#{plugin_name}"
29
29
  end
30
- execute(:svgo, *args) && optimized?(src, dst)
30
+ execute(:svgo, args, options) && optimized?(src, dst)
31
31
  end
32
32
  end
33
33
  end
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'image_optim/cmd'
5
5
  require 'image_optim/configuration_error'
6
+ require 'image_optim/elapsed_time'
6
7
  require 'image_optim/path'
7
8
  require 'image_optim/worker/class_methods'
8
9
  require 'shellwords'
@@ -41,7 +42,7 @@ class ImageOptim
41
42
 
42
43
  # Optimize image at src, output at dst, must be overriden in subclass
43
44
  # return true on success
44
- def optimize(_src, _dst)
45
+ def optimize(_src, _dst, options = {})
45
46
  fail NotImplementedError, "implement method optimize in #{self.class}"
46
47
  end
47
48
 
@@ -104,7 +105,7 @@ class ImageOptim
104
105
  return if unknown_options.empty?
105
106
 
106
107
  fail ConfigurationError, "unknown options #{unknown_options.inspect} "\
107
- "for #{self}"
108
+ "for #{self}"
108
109
  end
109
110
 
110
111
  # Forward bin resolving to image_optim
@@ -117,47 +118,49 @@ class ImageOptim
117
118
  def wrap_resolver_error_message(message)
118
119
  name = self.class.bin_sym
119
120
  "#{name} worker: #{message}; please provide proper binary or "\
120
- "disable this worker (--no-#{name} argument or "\
121
- "`:#{name} => false` through options)"
121
+ "disable this worker (--no-#{name} argument or "\
122
+ "`:#{name} => false` through options)"
122
123
  end
123
124
 
124
125
  # Run command setting priority and hiding output
125
- def execute(bin, *arguments)
126
+ def execute(bin, arguments, options)
126
127
  resolve_bin!(bin)
127
128
 
128
129
  cmd_args = [bin, *arguments].map(&:to_s)
129
130
 
130
- start = Time.now
131
-
132
- success = run_command(cmd_args)
133
-
134
131
  if @image_optim.verbose
135
- seconds = Time.now - start
136
- $stderr << "#{success ? '✓' : '✗'} #{seconds}s #{cmd_args.shelljoin}\n"
132
+ run_command_verbose(cmd_args, options)
133
+ else
134
+ run_command(cmd_args, options)
137
135
  end
138
-
139
- success
140
136
  end
141
137
 
142
138
  # Run command defining environment, setting nice level, removing output and
143
139
  # reraising signal exception
144
- def run_command(cmd_args)
145
- args = if RUBY_VERSION < '1.9' || defined?(JRUBY_VERSION)
146
- %W[
147
- env PATH=#{@image_optim.env_path.shellescape}
148
- nice -n #{@image_optim.nice}
149
- #{cmd_args.shelljoin}
150
- > #{Path::NULL} 2>&1
151
- ].join(' ')
152
- else
153
- [
154
- {'PATH' => @image_optim.env_path},
155
- %W[nice -n #{@image_optim.nice}],
156
- cmd_args,
157
- {:out => Path::NULL, :err => Path::NULL},
158
- ].flatten
159
- end
140
+ def run_command(cmd_args, options)
141
+ args = [
142
+ {'PATH' => @image_optim.env_path},
143
+ *%W[nice -n #{@image_optim.nice}],
144
+ *cmd_args,
145
+ options.merge(out: Path::NULL, err: Path::NULL),
146
+ ]
160
147
  Cmd.run(*args)
161
148
  end
149
+
150
+ # Wrap run_command and output status, elapsed time and command
151
+ def run_command_verbose(cmd_args, options)
152
+ start = ElapsedTime.now
153
+
154
+ begin
155
+ success = run_command(cmd_args, options)
156
+ status = success ? '✓' : '✗'
157
+ success
158
+ rescue Errors::TimeoutExceeded
159
+ status = 'timeout'
160
+ raise
161
+ ensure
162
+ $stderr << format("%s %.1fs %s\n", status, ElapsedTime.now - start, cmd_args.shelljoin)
163
+ end
164
+ end
162
165
  end
163
166
  end
data/lib/image_optim.rb CHANGED
@@ -3,16 +3,18 @@
3
3
  require 'image_optim/bin_resolver'
4
4
  require 'image_optim/cache'
5
5
  require 'image_optim/config'
6
+ require 'image_optim/errors'
6
7
  require 'image_optim/handler'
7
8
  require 'image_optim/image_meta'
8
9
  require 'image_optim/optimized_path'
9
10
  require 'image_optim/path'
11
+ require 'image_optim/timer'
10
12
  require 'image_optim/worker'
11
13
  require 'in_threads'
12
14
  require 'shellwords'
13
15
 
14
16
  %w[
15
- pngcrush pngout advpng optipng pngquant
17
+ pngcrush pngout advpng optipng pngquant oxipng
16
18
  jhead jpegoptim jpegrecompress jpegtran
17
19
  gifsicle
18
20
  svgo
@@ -46,6 +48,9 @@ class ImageOptim
46
48
  # Cache worker digests
47
49
  attr_reader :cache_worker_digests
48
50
 
51
+ # Timeout in seconds for each image
52
+ attr_reader :timeout
53
+
49
54
  # Initialize workers, specify options using worker underscored name:
50
55
  #
51
56
  # pass false to disable worker
@@ -78,6 +83,7 @@ class ImageOptim
78
83
  allow_lossy
79
84
  cache_dir
80
85
  cache_worker_digests
86
+ timeout
81
87
  ].each do |name|
82
88
  instance_variable_set(:"@#{name}", config.send(name))
83
89
  $stderr << "#{name}: #{send(name)}\n" if verbose
@@ -110,11 +116,17 @@ class ImageOptim
110
116
  return unless (workers = workers_for_image(original))
111
117
 
112
118
  optimized = @cache.fetch(original) do
119
+ timer = timeout && Timer.new(timeout)
120
+
113
121
  Handler.for(original) do |handler|
114
- workers.each do |worker|
115
- handler.process do |src, dst|
116
- worker.optimize(src, dst)
122
+ begin
123
+ workers.each do |worker|
124
+ handler.process do |src, dst|
125
+ worker.optimize(src, dst, timeout: timer)
126
+ end
117
127
  end
128
+ rescue Errors::TimeoutExceeded
129
+ handler.result
118
130
  end
119
131
  end
120
132
  end
@@ -188,12 +200,6 @@ class ImageOptim
188
200
  optimize_image_method?(method) || super
189
201
  end
190
202
 
191
- if RUBY_VERSION < '1.9'
192
- def respond_to?(method, include_private = false)
193
- optimize_image_method?(method) || super
194
- end
195
- end
196
-
197
203
  # Version of image_optim gem spec loaded
198
204
  def version
199
205
  Gem.loaded_specs['image_optim'].version.to_s