image_optim 0.28.0 → 0.31.1

Sign up to get free protection for your applications and to get access to all the features.
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