image_optim 0.31.4 → 0.32.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28ea4eea777b5a6eac029c628c28509b70d65f5784ef16466c26b758d189d88b
4
- data.tar.gz: b4cdec32a6fde2d92b7c3382b14fb90c094e523e46c79fd8fc24d21737348167
3
+ metadata.gz: ef1f30c74ae2a4479ec6d68f006313ae236cd92e3f8338a15619a74d5fbb2dc7
4
+ data.tar.gz: 3db0340fc04da059048f162c4b4e35870213f3ce40a6f8af896059ffd2703cbe
5
5
  SHA512:
6
- metadata.gz: ad0b465ae7de080c2af44efaaeea7a7b17b8bc343faa0f8034105b027362abfc952bf72fead0d9c30f368a94cb74a1a7d2e19d49b07c626f49bfb59b99f4de69
7
- data.tar.gz: 8d7070adae2ff71fe71df2ffaf25addb42acdddca2a427bcfad3f91cd3138d0fccfc2b3284d2cf146dd8f513be9c88a4b9c2f378d6564bcfdf629f5f6ee492cf
6
+ metadata.gz: 2fdbf6c9cb715760ce38200157ddaa9142e5ec83611813572b475893c24c50f18983207bf268cd9f73d98689d7fb28e3703a8ab9633d5c28bad2c9e003a9f667
7
+ data.tar.gz: d9826620e1554b8cac570b79995636c8abd652f66318bf8f1bf498156756250919bd48374038c0db99554c9e2ce1ba1f2af194715c73c41ff9afea97e4cc33eb
@@ -3,4 +3,6 @@ updates:
3
3
  - package-ecosystem: github-actions
4
4
  directory: /
5
5
  schedule:
6
- interval: weekly
6
+ interval: daily
7
+ cooldown:
8
+ default-days: 7
@@ -0,0 +1,60 @@
1
+ name: build-test-containers
2
+ on:
3
+ workflow_dispatch:
4
+ schedule:
5
+ - cron: 11 2 13 * *
6
+ jobs:
7
+ base:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v6
11
+ - uses: docker/login-action@v4
12
+ with:
13
+ registry: ghcr.io
14
+ username: ${{ github.actor }}
15
+ password: ${{ secrets.GITHUB_TOKEN }}
16
+ - run:
17
+ docker build
18
+ -f Dockerfile.test
19
+ --target ruby-build
20
+ -t ghcr.io/toy/image_optim.test:ruby-build
21
+ --push
22
+ .
23
+
24
+ test-images:
25
+ needs: base
26
+ runs-on: ubuntu-latest
27
+ strategy:
28
+ matrix:
29
+ ruby:
30
+ - '1.9'
31
+ - '2.0'
32
+ - '2.1'
33
+ - '2.2'
34
+ - '2.3'
35
+ - '2.4'
36
+ - '2.5'
37
+ - '2.6'
38
+ - '2.7'
39
+ - '3.0'
40
+ - '3.1'
41
+ - '3.2'
42
+ - '3.3'
43
+ - '3.4'
44
+ - '4.0'
45
+ fail-fast: false
46
+ steps:
47
+ - uses: actions/checkout@v6
48
+ - uses: docker/login-action@v4
49
+ with:
50
+ registry: ghcr.io
51
+ username: ${{ github.actor }}
52
+ password: ${{ secrets.GITHUB_TOKEN }}
53
+ - run:
54
+ docker build
55
+ -f Dockerfile.test
56
+ --target test
57
+ --build-arg "RUBY_VERSION=${{ matrix.ruby }}"
58
+ -t "ghcr.io/toy/image_optim.test:${{ matrix.ruby }}"
59
+ --push
60
+ .
@@ -6,20 +6,22 @@ on:
6
6
  - cron: 45 4 * * 2
7
7
  jobs:
8
8
  check:
9
- runs-on: ubuntu-latest
9
+ runs-on: ubuntu-22.04
10
10
  strategy:
11
11
  matrix:
12
12
  ruby:
13
- - '1.9.3'
14
13
  - '2.7'
15
14
  - '3.0'
16
15
  - '3.1'
17
16
  - '3.2'
18
17
  - '3.3'
18
+ - '3.4'
19
+ - '4.0'
19
20
  - jruby-9.4
21
+ - jruby-10.1
20
22
  fail-fast: false
21
23
  steps:
22
- - uses: actions/checkout@v4
24
+ - uses: actions/checkout@v6
23
25
  - uses: ruby/setup-ruby@v1
24
26
  with:
25
27
  ruby-version: "${{ matrix.ruby }}"
@@ -33,13 +35,12 @@ jobs:
33
35
  strategy:
34
36
  matrix:
35
37
  container:
36
- - debian:buster
37
38
  - debian:bullseye
38
39
  - debian:bookworm
39
40
  # - alpine
40
41
  fail-fast: false
41
42
  steps:
42
- - uses: actions/checkout@v4
43
+ - uses: actions/checkout@v6
43
44
  - run: |
44
45
  if command -v apt-get &> /dev/null; then
45
46
  apt-get update
@@ -65,14 +66,16 @@ jobs:
65
66
  - '3.1'
66
67
  - '3.2'
67
68
  - '3.3'
69
+ - '3.4'
70
+ - '4.0'
68
71
  fail-fast: false
69
72
  steps:
70
- - uses: actions/checkout@v4
73
+ - uses: actions/checkout@v6
71
74
  - uses: ruby/setup-ruby@v1
72
75
  with:
73
76
  ruby-version: "${{ matrix.ruby }}"
74
77
  bundler-cache: true
75
- - uses: actions/cache@v4
78
+ - uses: actions/cache@v5
76
79
  with:
77
80
  path: "$HOME/bin"
78
81
  key: ${{ runner.os }}
@@ -90,23 +93,9 @@ jobs:
90
93
  update_worker_options_in_readme:
91
94
  runs-on: ubuntu-latest
92
95
  steps:
93
- - uses: actions/checkout@v4
96
+ - uses: actions/checkout@v6
94
97
  - uses: ruby/setup-ruby@v1
95
98
  with:
96
- ruby-version: '3'
99
+ ruby-version: '4'
97
100
  bundler-cache: true
98
101
  - run: script/update_worker_options_in_readme -n
99
- coverage:
100
- runs-on: ubuntu-latest
101
- env:
102
- CC_TEST_REPORTER_ID: b433c6540d220a2da0663670c9b260806bafdb3a43c6f22b2e81bfb1f87b12fe
103
- steps:
104
- - uses: actions/checkout@v4
105
- - uses: ruby/setup-ruby@v1
106
- with:
107
- ruby-version: '3'
108
- bundler-cache: true
109
- - run: npm install -g svgo
110
- - uses: paambaati/codeclimate-action@v9
111
- with:
112
- coverageCommand: bundle exec rspec
@@ -8,9 +8,9 @@ jobs:
8
8
  rubocop:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
- - uses: actions/checkout@v4
11
+ - uses: actions/checkout@v6
12
12
  - uses: ruby/setup-ruby@v1
13
13
  with:
14
- ruby-version: '3'
14
+ ruby-version: '4'
15
15
  bundler-cache: true
16
16
  - run: bundle exec rubocop
data/.rubocop.yml CHANGED
@@ -67,6 +67,8 @@ Metrics/BlockLength:
67
67
  - 'spec/**/*.rb'
68
68
 
69
69
  Metrics/ClassLength:
70
+ Exclude:
71
+ - 'lib/image_optim.rb'
70
72
  Max: 150
71
73
 
72
74
  Metrics/CyclomaticComplexity:
data/CHANGELOG.markdown CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## unreleased
4
4
 
5
+ ## v0.32.0 (2026-05-28)
6
+
7
+ * Use `Etc.nprocessors` for default number of threads with fallback to manual way [@toy](https://github.com/toy)
8
+ * Correct environment variable to specify `jpeg-recompress` location [@toy](https://github.com/toy)
9
+ * Added --benchmark, to compare performance of each tool [#217](https://github.com/toy/image_optim/issues/217) [#218](https://github.com/toy/image_optim/pull/218) [@gurgeous](https://github.com/gurgeous)
10
+ * Don't require presence of `git` in gemspec [@toy](https://github.com/toy)
11
+ * Add a basic check for names of enabled and disabled svgo plugins [@toy](https://github.com/toy)
12
+ * Add support for enabling/disabling plugins in svgo 2.x, 3.x, 4.x [#191](https://github.com/toy/image_optim/issues/191) [#224](https://github.com/toy/image_optim/pull/224) [@tomhughes](https://github.com/tomhughes)
13
+
5
14
  ## v0.31.4 (2024-11-19)
6
15
 
7
16
  * Added `--svgo-allow-lossy` and `--svgo-precision` options to use svgo in lossy mode. This sets svgo `--precision`, which can result in substantially smaller svgs (see [#211](https://github.com/toy/image_optim/issues/211) for some experiments). Lower values are more lossy. 3 is the default, but many SVGs will work well even with 0 or 1. Like all worker specific lossy flags, this is also enabled with `--allow-lossy`. [#210](https://github.com/toy/image_optim/issues/210) [#211](https://github.com/toy/image_optim/issues/211) [@gurgeous](https://github.com/gurgeous)
data/Dockerfile.test ADDED
@@ -0,0 +1,46 @@
1
+ FROM node:slim AS ruby-build
2
+
3
+ RUN npm install -g svgo
4
+
5
+ RUN apt-get update \
6
+ && apt-get install -y --no-install-recommends \
7
+ build-essential \
8
+ ca-certificates \
9
+ curl \
10
+ git \
11
+ imagemagick \
12
+ libffi-dev \
13
+ libgmp-dev \
14
+ librsvg2-bin \
15
+ libssl-dev \
16
+ libyaml-dev \
17
+ rustc \
18
+ tini \
19
+ zlib1g-dev \
20
+ && rm -rf /var/lib/apt/lists/*
21
+
22
+ RUN git clone https://github.com/rbenv/ruby-build.git /ruby-build \
23
+ && cd /ruby-build \
24
+ && git checkout $(git describe --tags "$(git rev-list --tags --max-count=1)") \
25
+ && /ruby-build/install.sh \
26
+ && rm -r /ruby-build
27
+
28
+ FROM ghcr.io/toy/image_optim.test:ruby-build AS test
29
+
30
+ ARG RUBY_VERSION
31
+ RUN FULL_VERSION=$(ruby-build --definitions | perl -e '$p=shift; while(<>){$l=$_ if index($_,$p)==0} print $l' "${RUBY_VERSION}.") \
32
+ && RUBY_CONFIGURE_OPTS=--disable-install-doc ruby-build --verbose "$FULL_VERSION" /usr/local
33
+
34
+ ENV BUNDLE_SILENCE_ROOT_WARNING=1
35
+ COPY script/update-rubygems-n-bundler ./script/
36
+ RUN script/update-rubygems-n-bundler
37
+
38
+ WORKDIR /gem
39
+ # silence warnings about not being in git directory
40
+ RUN git init
41
+
42
+ COPY Gemfile *.gemspec ./
43
+ RUN bundle install \
44
+ && rm Gemfile Gemfile.lock *.gemspec
45
+
46
+ ENTRYPOINT ["/usr/bin/tini", "--"]
data/Gemfile CHANGED
@@ -4,6 +4,10 @@ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
 
7
+ if RUBY_VERSION >= '4'
8
+ gem 'logger'
9
+ end
10
+
7
11
  if ENV['CC_TEST_REPORTER_ID']
8
12
  group :test do
9
13
  gem 'simplecov'
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012-2024 Ivan Kuchin
1
+ Copyright (c) 2012-2026 Ivan Kuchin
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.markdown CHANGED
@@ -1,9 +1,7 @@
1
1
  [![Gem Version](https://img.shields.io/gem/v/image_optim?logo=rubygems)](https://rubygems.org/gems/image_optim)
2
- [![Build Status](https://img.shields.io/github/actions/workflow/status/toy/image_optim/check.yml?logo=github)](https://github.com/toy/image_optim/actions/workflows/check.yml)
2
+ [![Check](https://img.shields.io/github/actions/workflow/status/toy/image_optim/check.yml?label=check&logo=github)](https://github.com/toy/image_optim/actions/workflows/check.yml)
3
3
  [![Rubocop](https://img.shields.io/github/actions/workflow/status/toy/image_optim/rubocop.yml?label=rubocop&logo=rubocop)](https://github.com/toy/image_optim/actions/workflows/rubocop.yml)
4
4
  [![CodeQL](https://img.shields.io/github/actions/workflow/status/toy/image_optim/codeql.yml?label=codeql&logo=github)](https://github.com/toy/image_optim/actions/workflows/codeql.yml)
5
- [![Code Climate](https://img.shields.io/codeclimate/maintainability/toy/image_optim?logo=codeclimate)](https://codeclimate.com/github/toy/image_optim)
6
- [![Code Climate Coverage](https://img.shields.io/codeclimate/coverage/toy/image_optim?logo=codeclimate)](https://codeclimate.com/github/toy/image_optim)
7
5
  [![Depfu](https://img.shields.io/depfu/toy/image_optim)](https://depfu.com/github/toy/image_optim)
8
6
  [![Inch CI](https://inch-ci.org/github/toy/image_optim.svg?branch=master)](https://inch-ci.org/github/toy/image_optim)
9
7
 
@@ -62,7 +60,7 @@ With version:
62
60
 
63
61
  <!---<update-version>-->
64
62
  ```ruby
65
- gem 'image_optim', '~> 0.31'
63
+ gem 'image_optim', '~> 0.32'
66
64
  ```
67
65
  <!---</update-version>-->
68
66
 
@@ -90,7 +88,7 @@ Simplest way for `image_optim` to locate binaries is to install them in common l
90
88
 
91
89
  If you cannot install to common location, then install to custom one and add it to `PATH`.
92
90
 
93
- Specify custom bin location using `XXX_BIN` environment variable (`JPEGOPTIM_BIN`, `OPTIPNG_BIN`, …).
91
+ Specify custom bin location using `XXX_BIN` environment variable (`JPEGOPTIM_BIN`, `OPTIPNG_BIN`, `JPEG_RECOMPRESS_BIN`, …).
94
92
 
95
93
  Besides permanently setting environment variables in `~/.profile`, `~/.bash_profile`, `~/.bashrc`, `~/.zshrc`, … they can be set:
96
94
 
@@ -303,6 +301,29 @@ optipng:
303
301
 
304
302
  `image_optim` uses standard ruby library for creating temporary files. Temporary directory can be changed using one of `TMPDIR`, `TMP` or `TEMP` environment variables.
305
303
 
304
+ ### Benchmark
305
+
306
+ Run with `--benchmark` to compare the performance of each individual tool on your images:
307
+
308
+ ```sh
309
+ image_optim --benchmark=isolated -r /tmp/corpus/
310
+ ```
311
+
312
+ ```
313
+ benchmarking: 100.0% (elapsed: 3.9m)
314
+
315
+ BENCHMARK RESULTS
316
+
317
+ name files elapsed kb saved kb/s
318
+ -------- ----- ------- -------- -------
319
+ oxipng 50 8.906 1867.253 209.664
320
+ pngquant 50 1.980 214.597 108.386
321
+ pngcrush 50 22.529 1753.704 77.841
322
+ optipng 50 142.940 1641.101 11.481
323
+ advpng 50 137.753 962.549 6.987
324
+ pngout 50 426.706 444.679 1.042
325
+ ```
326
+
306
327
  ## Options
307
328
 
308
329
  * `:nice` — Nice level, priority of all used tools with higher value meaning lower priority, in range `-20..19`, negative values can be set only if run by root user *(defaults to `10`)*
@@ -392,4 +413,4 @@ In separate file [CHANGELOG.markdown](CHANGELOG.markdown).
392
413
 
393
414
  ## Copyright
394
415
 
395
- Copyright (c) 2012-2024 Ivan Kuchin. See [LICENSE.txt](LICENSE.txt) for details.
416
+ Copyright (c) 2012-2026 Ivan Kuchin. See [LICENSE.txt](LICENSE.txt) for details.
data/image_optim.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'image_optim'
5
- s.version = '0.31.4'
5
+ s.version = '0.32.0'
6
6
  s.summary = %q{Command line tool and ruby interface to optimize (lossless compress, optionally lossy) jpeg, png, gif and svg images using external utilities (advpng, gifsicle, jhead, jpeg-recompress, jpegoptim, jpegrescan, jpegtran, optipng, oxipng, pngcrush, pngout, pngquant, svgo)}
7
7
  s.homepage = "https://github.com/toy/#{s.name}"
8
8
  s.authors = ['Ivan Kuchin']
@@ -17,9 +17,19 @@ Gem::Specification.new do |s|
17
17
  'source_code_uri' => "https://github.com/toy/#{s.name}",
18
18
  } if s.respond_to?(:metadata=)
19
19
 
20
- s.files = `git ls-files`.split("\n")
21
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.files = Dir[*%w[
21
+ .gitignore
22
+ .pre-commit-hooks.yaml
23
+ .rubocop.yml
24
+ Dockerfile.test
25
+ Gemfile
26
+ LICENSE.txt
27
+ *.markdown
28
+ *.gemspec
29
+ {.github,bin,lib,script,spec,vendor}/**/*
30
+ ]].reject(&File.method(:directory?))
31
+ s.test_files = Dir['spec/**/*'].reject(&File.method(:directory?))
32
+ s.executables = Dir['bin/*'].map(&File.method(:basename))
23
33
  s.require_paths = %w[lib]
24
34
 
25
35
  s.post_install_message = <<-EOF
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ImageOptim
4
+ # Benchmark result for one worker+src
5
+ class BenchmarkResult
6
+ attr_reader :bytes, :elapsed, :worker
7
+
8
+ def initialize(src, dst, elapsed, worker)
9
+ @bytes = bytes_saved(src, dst)
10
+ @elapsed = elapsed
11
+ @worker = worker.class.bin_sym.to_s
12
+ end
13
+
14
+ private
15
+
16
+ def bytes_saved(src, dst)
17
+ src, dst = src.size, dst.size
18
+ return 0 if dst == 0 # failure
19
+ return 0 if dst > src # the file got bigger
20
+
21
+ src - dst
22
+ end
23
+ end
24
+ end
@@ -108,7 +108,7 @@ class ImageOptim
108
108
  # Check path in XXX_BIN to exist, be a file and be executable and symlink to
109
109
  # dir as name
110
110
  def symlink_custom_bin!(name)
111
- env_name = "#{name}_bin".upcase
111
+ env_name = "#{name.to_s.tr('-', '_').upcase}_BIN"
112
112
  path = ENV.fetch(env_name, nil)
113
113
  return unless path
114
114
 
@@ -5,12 +5,13 @@ require 'image_optim/configuration_error'
5
5
  require 'image_optim/hash_helpers'
6
6
  require 'image_optim/worker'
7
7
  require 'image_optim/cmd'
8
+ require 'etc'
8
9
  require 'set'
9
10
  require 'yaml'
10
11
 
11
12
  class ImageOptim
12
13
  # Read, merge and parse configuration
13
- class Config
14
+ class Config # rubocop:disable Metrics/ClassLength
14
15
  include OptionHelpers
15
16
 
16
17
  # Global config path at `$XDG_CONFIG_HOME/image_optim.yml` (by default
@@ -134,7 +135,7 @@ class ImageOptim
134
135
  end
135
136
 
136
137
  # Verbose mode, converted to boolean
137
- def verbose
138
+ def verbose # rubocop:disable Naming/PredicateMethod
138
139
  !!get!(:verbose)
139
140
  end
140
141
 
@@ -164,7 +165,7 @@ class ImageOptim
164
165
  end
165
166
 
166
167
  # Allow lossy workers and optimizations, converted to boolean
167
- def allow_lossy
168
+ def allow_lossy # rubocop:disable Naming/PredicateMethod
168
169
  !!get!(:allow_lossy)
169
170
  end
170
171
 
@@ -173,7 +174,7 @@ class ImageOptim
173
174
  dir unless dir.nil? || dir.empty?
174
175
  end
175
176
 
176
- def cache_worker_digests
177
+ def cache_worker_digests # rubocop:disable Naming/PredicateMethod
177
178
  !!get!(:cache_worker_digests)
178
179
  end
179
180
 
@@ -205,9 +206,17 @@ class ImageOptim
205
206
 
206
207
  private
207
208
 
208
- # http://stackoverflow.com/a/6420817
209
209
  def processor_count
210
- @processor_count ||= case host_os = RbConfig::CONFIG['host_os']
210
+ @processor_count ||= if Etc.respond_to?(:nprocessors)
211
+ Etc.nprocessors
212
+ else
213
+ processor_count_manual
214
+ end
215
+ end
216
+
217
+ # http://stackoverflow.com/a/6420817
218
+ def processor_count_manual
219
+ case host_os = RbConfig::CONFIG['host_os']
211
220
  when /darwin9/
212
221
  Cmd.capture 'hwprefs cpu_count'
213
222
  when /darwin/
@@ -153,6 +153,15 @@ ImageOptim::Runner::OptionParser::DEFINE = proc do |op, options|
153
153
  options[:pack] = pack
154
154
  end
155
155
 
156
+ op.separator nil
157
+ op.on(
158
+ '--benchmark TYPE',
159
+ [:isolated],
160
+ 'Run benchmarks, to compare tools without modifying images. `isolated` is the only supported type so far.'
161
+ ) do |benchmark|
162
+ options[:benchmark] = benchmark
163
+ end
164
+
156
165
  op.separator nil
157
166
  op.separator ' Caching:'
158
167
 
@@ -45,6 +45,43 @@ class ImageOptim
45
45
  end
46
46
  end
47
47
 
48
+ # files, elapsed, kb saved, kb/s
49
+ class BenchmarkResults
50
+ def initialize
51
+ @all = []
52
+ end
53
+
54
+ def add(rows)
55
+ @all.concat(rows)
56
+ end
57
+
58
+ def print
59
+ if @all.empty?
60
+ puts 'nothing to report'
61
+ return
62
+ end
63
+
64
+ report = @all.group_by(&:worker).map do |name, results|
65
+ kb = (results.sum(&:bytes) / 1024.0)
66
+ elapsed = results.sum(&:elapsed)
67
+ {
68
+ 'name' => name,
69
+ 'files' => results.length,
70
+ 'elapsed' => elapsed,
71
+ 'kb saved' => kb,
72
+ 'kb/s' => (kb / elapsed),
73
+ }
74
+ end
75
+
76
+ report = report.sort_by do |row|
77
+ [-row['kb/s'], row['name']]
78
+ end
79
+
80
+ puts "\nBENCHMARK RESULTS\n\n"
81
+ Table.new(report).write($stdout)
82
+ end
83
+ end
84
+
48
85
  def initialize(options)
49
86
  options = HashHelpers.deep_symbolise_keys(options)
50
87
  @recursive = options.delete(:recursive)
@@ -53,19 +90,40 @@ class ImageOptim
53
90
  glob = options.delete(:"exclude_#{type}_glob") || '.*'
54
91
  GlobHelpers.expand_braces(glob)
55
92
  end
93
+
94
+ # --benchmark
95
+ @benchmark = options.delete(:benchmark)
96
+ if @benchmark
97
+ unless options[:threads].nil?
98
+ warning '--benchmark ignores --threads'
99
+ options[:threads] = 1 # for consistency
100
+ end
101
+ if options[:timeout]
102
+ warning '--benchmark ignores --timeout'
103
+ end
104
+ end
105
+
56
106
  @image_optim = ImageOptim.new(options)
57
107
  end
58
108
 
59
- def run!(args)
109
+ def run!(args) # rubocop:disable Naming/PredicateMethod
60
110
  to_optimize = find_to_optimize(args)
61
111
  unless to_optimize.empty?
62
- results = Results.new
112
+ if @benchmark
113
+ benchmark_results = BenchmarkResults.new
114
+ benchmark_images(to_optimize).each do |_original, rows| # rubocop:disable Style/HashEachMethods
115
+ benchmark_results.add(rows)
116
+ end
117
+ benchmark_results.print
118
+ else
119
+ results = Results.new
63
120
 
64
- optimize_images!(to_optimize).each do |original, optimized|
65
- results.add(original, optimized)
66
- end
121
+ optimize_images!(to_optimize).each do |original, optimized|
122
+ results.add(original, optimized)
123
+ end
67
124
 
68
- results.print
125
+ results.print
126
+ end
69
127
  end
70
128
 
71
129
  !@warnings
@@ -73,6 +131,11 @@ class ImageOptim
73
131
 
74
132
  private
75
133
 
134
+ def benchmark_images(to_optimize, &block)
135
+ to_optimize = to_optimize.with_progress('benchmarking') if @progress
136
+ @image_optim.benchmark_images(to_optimize, &block)
137
+ end
138
+
76
139
  def optimize_images!(to_optimize, &block)
77
140
  to_optimize = to_optimize.with_progress('optimizing') if @progress
78
141
  @image_optim.optimize_images!(to_optimize, &block)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ImageOptim
4
+ # Handy class for pretty printing a table in the terminal. This is very simple, switch to Terminal
5
+ # Table, Table Tennis or similar if we need more.
6
+ class Table
7
+ attr_reader :rows
8
+
9
+ def initialize(rows)
10
+ @rows = rows
11
+ end
12
+
13
+ def write(io)
14
+ io.puts render_row(columns)
15
+ io.puts render_sep
16
+ rows.each do |row|
17
+ io.puts render_row(row.values)
18
+ end
19
+ end
20
+
21
+ protected
22
+
23
+ # array of column names
24
+ def columns
25
+ @columns ||= rows.first.keys
26
+ end
27
+
28
+ # should columns be justified left or right?
29
+ def justs
30
+ @justs ||= columns.map do |col|
31
+ rows.first[col].is_a?(Numeric) ? :rjust : :ljust
32
+ end
33
+ end
34
+
35
+ # max width of each column
36
+ def widths
37
+ @widths ||= columns.map do |col|
38
+ values = rows.map{ |row| fmt(row[col]) }
39
+ ([col] + values).map(&:length).max
40
+ end
41
+ end
42
+
43
+ # render an array of row values
44
+ def render_row(values)
45
+ values.zip(justs, widths).map do |value, just, width|
46
+ fmt(value).send(just, width)
47
+ end.join(' ')
48
+ end
49
+
50
+ # render a separator line
51
+ def render_sep
52
+ render_row(widths.map{ |width| '-' * width })
53
+ end
54
+
55
+ # format one cell value
56
+ def fmt(value)
57
+ if value.is_a?(Float)
58
+ format('%0.3f', value)
59
+ else
60
+ value.to_s
61
+ end
62
+ end
63
+ end
64
+ end
@@ -2,19 +2,22 @@
2
2
 
3
3
  require 'image_optim/option_helpers'
4
4
  require 'image_optim/worker'
5
+ require 'fspath'
5
6
 
6
7
  class ImageOptim
7
8
  class Worker
8
9
  # https://github.com/svg/svgo
9
10
  class Svgo < Worker
11
+ PLUGIN_NAME_R = /\A[a-zA-Z]+\z/.freeze
12
+
10
13
  DISABLE_PLUGINS_OPTION =
11
14
  option(:disable_plugins, [], 'List of plugins to disable') do |v|
12
- Array(v).map(&:to_s)
15
+ parse_plugin_names(v)
13
16
  end
14
17
 
15
18
  ENABLE_PLUGINS_OPTION =
16
19
  option(:enable_plugins, [], 'List of plugins to enable') do |v|
17
- Array(v).map(&:to_s)
20
+ parse_plugin_names(v)
18
21
  end
19
22
 
20
23
  ALLOW_LOSSY_OPTION =
@@ -40,15 +43,57 @@ class ImageOptim
40
43
  --input #{src}
41
44
  --output #{dst}
42
45
  ]
43
- disable_plugins.each do |plugin_name|
44
- args.unshift "--disable=#{plugin_name}"
45
- end
46
- enable_plugins.each do |plugin_name|
47
- args.unshift "--enable=#{plugin_name}"
46
+ if resolve_bin!(:svgo).version >= '2.0.0'
47
+ unless disable_plugins.empty? && enable_plugins.empty?
48
+ config_file = plugins_config_file
49
+ args.unshift "--config=#{config_file.path}"
50
+ end
51
+ else
52
+ disable_plugins.each do |plugin_name|
53
+ args.unshift "--disable=#{plugin_name}"
54
+ end
55
+ enable_plugins.each do |plugin_name|
56
+ args.unshift "--enable=#{plugin_name}"
57
+ end
48
58
  end
49
59
  args.unshift "--precision=#{precision}" if allow_lossy
50
60
  execute(:svgo, args, options) && optimized?(src, dst)
51
61
  end
62
+
63
+ private
64
+
65
+ def parse_plugin_names(value)
66
+ Array(value).map(&:to_s).select do |name|
67
+ if name =~ PLUGIN_NAME_R
68
+ true
69
+ else
70
+ warn "Doesn't look like svgo plugin name: #{name}"
71
+ end
72
+ end
73
+ end
74
+
75
+ def plugins_config_file
76
+ @plugins_config_file ||= FSPath.temp_file(%w[image_optim .js]).tap do |config_file|
77
+ config_file.puts 'export default {'
78
+ config_file.puts ' plugins: ['
79
+ config_file.puts ' {'
80
+ config_file.puts ' name: \'preset-default\','
81
+ config_file.puts ' params: {'
82
+ config_file.puts ' overrides: {'
83
+ disable_plugins.each do |plugin_name|
84
+ config_file.puts " #{plugin_name}: false,"
85
+ end
86
+ config_file.puts ' }'
87
+ config_file.puts ' }'
88
+ config_file.puts ' },'
89
+ enable_plugins.each do |plugin_name|
90
+ config_file.puts " '#{plugin_name}',"
91
+ end
92
+ config_file.puts ' ]'
93
+ config_file.puts '};'
94
+ config_file.close
95
+ end
96
+ end
52
97
  end
53
98
  end
54
99
  end
data/lib/image_optim.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'image_optim/benchmark_result'
3
4
  require 'image_optim/bin_resolver'
4
5
  require 'image_optim/cache'
5
6
  require 'image_optim/config'
@@ -8,6 +9,7 @@ require 'image_optim/handler'
8
9
  require 'image_optim/image_meta'
9
10
  require 'image_optim/optimized_path'
10
11
  require 'image_optim/path'
12
+ require 'image_optim/table'
11
13
  require 'image_optim/timer'
12
14
  require 'image_optim/worker'
13
15
  require 'in_threads'
@@ -162,6 +164,22 @@ class ImageOptim
162
164
  end
163
165
  end
164
166
 
167
+ def benchmark_image(original)
168
+ src = Path.convert(original)
169
+ return unless (workers = workers_for_image(src))
170
+
171
+ dst = src.temp_path
172
+ begin
173
+ workers.map do |worker|
174
+ start = ElapsedTime.now
175
+ worker.optimize(src, dst)
176
+ BenchmarkResult.new(src, dst, ElapsedTime.now - start, worker)
177
+ end
178
+ ensure
179
+ dst.unlink
180
+ end
181
+ end
182
+
165
183
  # Optimize multiple images
166
184
  # if block given yields path and result for each image and returns array of
167
185
  # yield results
@@ -186,6 +204,10 @@ class ImageOptim
186
204
  run_method_for(datas, :optimize_image_data, &block)
187
205
  end
188
206
 
207
+ def benchmark_images(paths, &block)
208
+ run_method_for(paths, :benchmark_image, &block)
209
+ end
210
+
189
211
  class << self
190
212
  # Optimization methods with default options
191
213
  def method_missing(method, *args, &block)
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euxo pipefail
4
+
5
+ ruby <<'RUBY'
6
+ short_version = RUBY_VERSION.to_f
7
+
8
+ gemrc_path = File.expand_path('~/.gemrc')
9
+ unless File.exist?(gemrc_path)
10
+ File.open(gemrc_path, 'w') do |f|
11
+ if short_version < 2.0
12
+ f.puts 'gem: --no-ri --no-rdoc'
13
+ else
14
+ f.puts 'gem: --no-document'
15
+ end
16
+ end
17
+ end
18
+
19
+ def sh(*args)
20
+ abort unless system(*args)
21
+ end
22
+
23
+ case
24
+ when short_version < 2.3
25
+ sh 'curl --output rubygems-update.gem https://rubygems.org/downloads/rubygems-update-2.7.11.gem'
26
+ sh 'gem install --local rubygems-update.gem'
27
+ sh 'update_rubygems'
28
+ File.unlink(`which bundle`.strip)
29
+ sh 'curl --output bundler.gem https://rubygems.org/downloads/bundler-1.17.3.gem'
30
+ sh 'gem install --local bundler.gem'
31
+ when short_version < 2.6
32
+ sh 'gem update --system 3.3.27'
33
+ sh 'gem install bundler --version 2.3.27'
34
+ when short_version < 3.0
35
+ sh 'gem update --system 3.4.22'
36
+ sh 'gem install bundler --version 2.4.22'
37
+ when short_version < 3.1
38
+ sh 'gem update --system 3.5.23'
39
+ sh 'gem install bundler --version 2.5.23'
40
+ when short_version < 3.2
41
+ sh 'gem update --system 3.6.9'
42
+ sh 'gem install bundler --version 2.6.9'
43
+ else
44
+ sh 'gem update --system'
45
+ sh 'gem install bundler'
46
+ end
47
+ RUBY
48
+
49
+ gem -v
50
+ bundle -v
@@ -143,14 +143,14 @@ describe ImageOptim::BinResolver do
143
143
  expect(FSPath).to receive(:temp_dir).
144
144
  once.and_return(tmpdir)
145
145
  expect(tmpdir).to receive(:/).
146
- with(:the_optimizer).once.and_return(symlink)
146
+ with(:'the-optimizer').once.and_return(symlink)
147
147
  expect(symlink).to receive(:make_symlink).
148
148
  with(File.expand_path(path)).once
149
149
 
150
150
  expect(resolver).not_to receive(:full_path)
151
151
  bin = double
152
152
  expect(Bin).to receive(:new).
153
- with(:the_optimizer, File.expand_path(path)).and_return(bin)
153
+ with(:'the-optimizer', File.expand_path(path)).and_return(bin)
154
154
  expect(bin).to receive(:check!).once
155
155
  expect(bin).to receive(:check_fail!).exactly(5).times
156
156
 
@@ -160,7 +160,7 @@ describe ImageOptim::BinResolver do
160
160
  end
161
161
 
162
162
  5.times do
163
- resolver.resolve!(:the_optimizer)
163
+ resolver.resolve!(:'the-optimizer')
164
164
  end
165
165
  expect(resolver.env_path).to eq([
166
166
  tmpdir,
@@ -15,7 +15,9 @@ describe ImageOptim::Cache do
15
15
 
16
16
  let(:cache_dir) do
17
17
  dir = '/somewhere/cache'
18
- allow(FileUtils).to receive(:mkpath).with(Regexp.new(Regexp.escape(dir)), any_args)
18
+ allow(Dir).to receive(:mkdir).with(File.dirname(dir))
19
+ allow(Dir).to receive(:mkdir).with(dir)
20
+ allow(Dir).to receive(:mkdir).with(%r{\A#{Regexp.escape(dir)}/[^/]+\z})
19
21
  allow(FileUtils).to receive(:touch)
20
22
  allow(FSPath).to receive(:temp_file_path) do
21
23
  tmp_file
@@ -71,9 +71,7 @@ describe ImageOptim::OptionDefinition do
71
71
 
72
72
  context 'when proc given' do
73
73
  subject do
74
- # not using &:inspect due to ruby Bug #13087
75
- # to_s is just to calm rubocop
76
- described_class.new('abc', :def, 'desc'){ |o| o.inspect.to_s }
74
+ described_class.new('abc', :def, 'desc', &:inspect)
77
75
  end
78
76
 
79
77
  context 'when option not provided' do
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'image_optim/true_false_nil'
5
+
6
+ describe ImageOptim::TrueFalseNil do
7
+ describe '.convert' do
8
+ it 'keeps true' do
9
+ expect(described_class.convert(true)).to eq(true)
10
+ end
11
+
12
+ it 'keeps false' do
13
+ expect(described_class.convert(false)).to eq(false)
14
+ end
15
+
16
+ it 'keeps nil' do
17
+ expect(described_class.convert(nil)).to eq(nil)
18
+ end
19
+
20
+ it 'converts truthy to true' do
21
+ expect(described_class.convert(1)).to eq(true)
22
+ end
23
+ end
24
+ end
@@ -4,6 +4,49 @@ require 'spec_helper'
4
4
  require 'image_optim/worker/svgo'
5
5
 
6
6
  describe ImageOptim::Worker::Svgo do
7
+ %i[
8
+ disable_plugins
9
+ enable_plugins
10
+ ].each do |option|
11
+ describe "#{option} option" do
12
+ let(:subject){ described_class.new(ImageOptim.new, value).send(option) }
13
+
14
+ context 'default' do
15
+ let(:value){ {} }
16
+
17
+ it{ is_expected.to eq([]) }
18
+ end
19
+
20
+ context 'when passed single valid value' do
21
+ let(:value){ {option => :pluginName} }
22
+
23
+ it 'converts it to a string array' do
24
+ is_expected.to eq(%w[pluginName])
25
+ end
26
+ end
27
+
28
+ context 'when passed multiple valid values' do
29
+ let(:value){ {option => %i[pluginName anotherName]} }
30
+
31
+ it 'converts them to a string array' do
32
+ is_expected.to eq(%w[pluginName anotherName])
33
+ end
34
+ end
35
+
36
+ context 'when given invalid values' do
37
+ let(:value){ {option => %w[1abc pluginName alert() anotherName]} }
38
+
39
+ it 'warns and skips them' do
40
+ expect_any_instance_of(described_class).
41
+ to receive(:warn).with('Doesn\'t look like svgo plugin name: 1abc')
42
+ expect_any_instance_of(described_class).
43
+ to receive(:warn).with('Doesn\'t look like svgo plugin name: alert()')
44
+ is_expected.to eq(%w[pluginName anotherName])
45
+ end
46
+ end
47
+ end
48
+ end
49
+
7
50
  describe 'precision option' do
8
51
  describe 'default' do
9
52
  subject{ described_class::PRECISION_OPTION.default }
@@ -61,9 +61,10 @@ describe ImageOptim do
61
61
 
62
62
  base_options = {skip_missing_workers: false}
63
63
  [
64
- ['lossless', base_options, 0],
65
- ['lossy', base_options.merge(allow_lossy: true), 0.001],
66
- ].each do |type, options, max_difference|
64
+ # 120 comes from https://github.com/ImageMagick/ImageMagick/commit/8a7495a6d9
65
+ ['lossless', base_options, 120],
66
+ ['lossy', base_options.merge(allow_lossy: true), 30],
67
+ ].each do |type, options, psnr_min|
67
68
  it "does it #{type}" do
68
69
  image_optim = ImageOptim.new(options)
69
70
  copies = test_images.map{ |image| temp_copy(image) }
@@ -78,7 +79,7 @@ describe ImageOptim do
78
79
  expect(optimized).not_to have_same_data_as(original)
79
80
 
80
81
  compare_to = rotate_images.include?(original) ? rotated : original
81
- expect(optimized).to be_similar_to(compare_to, max_difference)
82
+ expect(optimized).to be_similar_to(compare_to, psnr_min)
82
83
  end
83
84
  end
84
85
  end
@@ -268,6 +269,20 @@ describe ImageOptim do
268
269
  end
269
270
  end
270
271
 
272
+ describe 'benchmark_images' do
273
+ it 'does it' do
274
+ image_optim = ImageOptim.new
275
+ pairs = image_optim.benchmark_images(test_images)
276
+ test_images.zip(pairs).each do |original, (src, bm)|
277
+ expect(original).to equal(src)
278
+ expect(bm[0]).to be_a(ImageOptim::BenchmarkResult)
279
+ expect(bm[0].bytes).to be_a(Numeric)
280
+ expect(bm[0].elapsed).to be_a(Numeric)
281
+ expect(bm[0].worker).to be_a(String)
282
+ end
283
+ end
284
+ end
285
+
271
286
  %w[
272
287
  optimize_image
273
288
  optimize_image!
@@ -7,6 +7,8 @@ require 'shellwords'
7
7
 
8
8
  side = 64
9
9
 
10
+ colors = Array.new(256){ [rand(256), rand(256), rand(256)] }
11
+
10
12
  IO.popen(%W[
11
13
  convert
12
14
  -depth 8
@@ -18,7 +20,7 @@ IO.popen(%W[
18
20
  side.times do |a|
19
21
  side.times do |b|
20
22
  alpha = [0, 1, 0x7f, 0xff][((a / 8) + (b / 8)) % 4]
21
- f << [rand(256), rand(256), rand(256), alpha].pack('C*')
23
+ f << [*colors.sample, alpha].pack('C*')
22
24
  end
23
25
  end
24
26
  end
Binary file
data/spec/spec_helper.rb CHANGED
@@ -42,12 +42,12 @@ def flatten_animation(image)
42
42
  end
43
43
  end
44
44
 
45
- def mepp(image_a, image_b)
45
+ def psnr(image_a, image_b)
46
46
  coalesce_a = flatten_animation(image_a)
47
47
  coalesce_b = flatten_animation(image_b)
48
48
  output = ImageOptim::Cmd.capture((IMAGEMAGICK_PREFIX + %W[
49
49
  compare
50
- -metric MEPP
50
+ -metric PSNR
51
51
  -alpha Background
52
52
  #{coalesce_a.to_s.shellescape}
53
53
  #{coalesce_b.to_s.shellescape}
@@ -59,21 +59,21 @@ def mepp(image_a, image_b)
59
59
  end
60
60
 
61
61
  num_r = '\d+(?:\.\d+(?:[eE][-+]?\d+)?)?'
62
- output[/\((#{num_r}), #{num_r}\)/, 1].to_f
62
+ num = output[/\A(#{num_r})/, 1].to_f
63
+ num == 0 ? Float::INFINITY : num
63
64
  end
64
65
 
65
66
  RSpec::Matchers.define :be_smaller_than do |expected|
66
67
  match{ |actual| actual.size < expected.size }
67
68
  end
68
69
 
69
- RSpec::Matchers.define :be_similar_to do |expected, max_difference|
70
+ RSpec::Matchers.define :be_similar_to do |expected, psnr_min|
70
71
  match do |actual|
71
- @diff = mepp(actual, expected)
72
- @diff <= max_difference
72
+ @diff = psnr(actual, expected)
73
+ @diff >= psnr_min
73
74
  end
74
75
  failure_message do |actual|
75
- "expected #{actual} to have at most #{max_difference} difference from " \
76
- "#{expected}, got mean error per pixel of #{@diff}"
76
+ "expected peaks signal to noise ratio between #{actual} and #{expected} to be #{psnr_min}, got #{@diff}"
77
77
  end
78
78
  end
79
79
 
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: image_optim
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.31.4
4
+ version: 0.32.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Kuchin
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-11-19 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: fspath
@@ -166,14 +165,13 @@ dependencies:
166
165
  - - "~>"
167
166
  - !ruby/object:Gem::Version
168
167
  version: '2.0'
169
- description:
170
- email:
171
168
  executables:
172
169
  - image_optim
173
170
  extensions: []
174
171
  extra_rdoc_files: []
175
172
  files:
176
173
  - ".github/dependabot.yml"
174
+ - ".github/workflows/build-test-containers.yml"
177
175
  - ".github/workflows/check.yml"
178
176
  - ".github/workflows/rubocop.yml"
179
177
  - ".gitignore"
@@ -181,12 +179,14 @@ files:
181
179
  - ".rubocop.yml"
182
180
  - CHANGELOG.markdown
183
181
  - CONTRIBUTING.markdown
182
+ - Dockerfile.test
184
183
  - Gemfile
185
184
  - LICENSE.txt
186
185
  - README.markdown
187
186
  - bin/image_optim
188
187
  - image_optim.gemspec
189
188
  - lib/image_optim.rb
189
+ - lib/image_optim/benchmark_result.rb
190
190
  - lib/image_optim/bin_resolver.rb
191
191
  - lib/image_optim/bin_resolver/bin.rb
192
192
  - lib/image_optim/bin_resolver/comparable_condition.rb
@@ -211,6 +211,7 @@ files:
211
211
  - lib/image_optim/runner/glob_helpers.rb
212
212
  - lib/image_optim/runner/option_parser.rb
213
213
  - lib/image_optim/space.rb
214
+ - lib/image_optim/table.rb
214
215
  - lib/image_optim/timer.rb
215
216
  - lib/image_optim/true_false_nil.rb
216
217
  - lib/image_optim/worker.rb
@@ -230,6 +231,7 @@ files:
230
231
  - script/template/jquery-2.1.3.min.js
231
232
  - script/template/sortable-0.6.0.min.js
232
233
  - script/template/worker_analysis.erb
234
+ - script/update-rubygems-n-bundler
233
235
  - script/update_worker_options_in_readme
234
236
  - script/worker_analysis
235
237
  - spec/files/config_with_range.yaml
@@ -252,6 +254,7 @@ files:
252
254
  - spec/image_optim/runner/option_parser_spec.rb
253
255
  - spec/image_optim/space_spec.rb
254
256
  - spec/image_optim/timer_spec.rb
257
+ - spec/image_optim/true_false_nil_spec.rb
255
258
  - spec/image_optim/worker/jpegrecompress_spec.rb
256
259
  - spec/image_optim/worker/optipng_spec.rb
257
260
  - spec/image_optim/worker/oxipng_spec.rb
@@ -294,7 +297,7 @@ licenses:
294
297
  metadata:
295
298
  bug_tracker_uri: https://github.com/toy/image_optim/issues
296
299
  changelog_uri: https://github.com/toy/image_optim/blob/master/CHANGELOG.markdown
297
- documentation_uri: https://www.rubydoc.info/gems/image_optim/0.31.4
300
+ documentation_uri: https://www.rubydoc.info/gems/image_optim/0.32.0
298
301
  source_code_uri: https://github.com/toy/image_optim
299
302
  post_install_message: |
300
303
  Rails image assets optimization is extracted into image_optim_rails gem
@@ -313,8 +316,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
313
316
  - !ruby/object:Gem::Version
314
317
  version: '0'
315
318
  requirements: []
316
- rubygems_version: 3.5.23
317
- signing_key:
319
+ rubygems_version: 4.0.3
318
320
  specification_version: 4
319
321
  summary: Command line tool and ruby interface to optimize (lossless compress, optionally
320
322
  lossy) jpeg, png, gif and svg images using external utilities (advpng, gifsicle,
@@ -341,6 +343,7 @@ test_files:
341
343
  - spec/image_optim/runner/option_parser_spec.rb
342
344
  - spec/image_optim/space_spec.rb
343
345
  - spec/image_optim/timer_spec.rb
346
+ - spec/image_optim/true_false_nil_spec.rb
344
347
  - spec/image_optim/worker/jpegrecompress_spec.rb
345
348
  - spec/image_optim/worker/optipng_spec.rb
346
349
  - spec/image_optim/worker/oxipng_spec.rb