やりたいこと
きのこ.heic という 3.3 MB の画像ファイルがあります。
- この画像を 1.0 MB 以下の JPEG 形式に圧縮したい
- 画質を最大限保つため、ファイルサイズはなるべく大きくしたい
方法
MiniMagick という Gem を使って、画像を JEPG に変換し、なおかつ MiniMagick::Image#quality
で画質 (1〜100) を指定します。
画質を 100, 99, 98 と小さくしていき、ファイルサイズが 1.0 MB 以下になる最大の画質を探すのはどうでしょう?しかし、これでは目的の画質が小さい場合に処理の回数が増えてしまいます。
そこで、目的の画質を検索するために二分探索のメソッドである Array#bsearch あるいは Range#bsearch を使い、効率化を図ります。
$ ruby -v
ruby 3.4.3 (2025-04-14 revision d0b7e5b6a0) +PRISM [arm64-darwin24]
$ gem install mini_magick activesupport
require 'mini_magick'
# ファイルサイズの計算や表示に ActiveSupport コア拡張が便利なので使用する。
require 'active_support'
require 'active_support/core_ext'
include(ActiveSupport::NumberHelper)
def compress_until_less_than_or_equal_to(filepath:, limit:, debug: false)
compressed_image = nil
# bsearch では条件を満たす最小の値を見つけるので昇順に並べておく。
(1..100).bsearch do |n|
quality = 100 - n
tmp_image = MiniMagick::Image.open(filepath)
tmp_image.format('jpeg')
tmp_image.quality(quality)
puts("画質 #{quality}: #{number_to_human_size(tmp_image.size)}") if debug
less_than_or_equal_to = tmp_image.size <= limit
# limit 以下の画像の中で一番サイズが大きいものを保持しておく。
if less_than_or_equal_to
compressed_image ||= tmp_image
compressed_image = [tmp_image, compressed_image].max_by(&:size)
end
less_than_or_equal_to
end
compressed_image
end
# ~/Downloads/きのこ.heic を読み込む。
filepath = Pathname(Dir.home).join('Downloads/きのこ.heic')
# 目的の画質を検索する過程を出力しながら画像を圧縮する。
compressed_image =
compress_until_less_than_or_equal_to(
filepath: filepath,
limit: 1.megabyte,
debug: true
)
# 画質 50: 1.8 MB
# 画質 25: 1.19 MB
# 画質 12: 767 KB
# 画質 19: 1020 KB
# 画質 22: 1.09 MB
# 画質 21: 1.06 MB
# 画質 20: 1.03 MB
# 1.0 MB (1,024 KB) 以下である。
number_to_human_size(compressed_image.size)
#=> "1020 KB"
# ~/Downloads/きのこ.jpg に書き込む。
compressed_image.write(Pathname(Dir.home).join('Downloads/きのこ.jpg'))
compress_until_less_than_or_equal_to
メソッド実行時の標準出力を見ると、以下のように探索しているのがわかります。