Project

image_pack

0.0
The project is in a healthy, maintained state
Single API, vendored pure-C MozJPEG/libjpeg codec, in-memory compression and atomic file output, Ruby 2.7.1+ native JPEG execution; Ruby 3.4+ enables Fiber::Scheduler-aware offload. Ships vendored C sources — no system libjpeg, git, or CMake required.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 5.0
~> 13.0
 Project Readme

image_pack

Ruby-native JPEG compression and optimization backed by vendored pure-C MozJPEG/libjpeg.

No system libjpeg, mozjpeg, git, or CMake is required for gem users.

gem "image_pack"
require "image_pack"

Quick use

jpeg = File.binread("photo.jpg")

small = ImagePack.compress_bytes(jpeg, quality: 82)
File.binwrite("photo.small.jpg", small)

ImagePack.compress_file("photo.jpg", output: "photo.small.jpg")
ImagePack.optimize_file("photo.jpg", output: "photo.optimized.jpg")

Prefer explicit helpers:

ImagePack.compress_bytes(jpeg)
ImagePack.compress_file("photo.jpg", output: "out.jpg")
ImagePack.optimize_bytes(jpeg)
ImagePack.optimize_file("photo.jpg", output: "out.jpg")

Compression

ImagePack.compress_bytes(jpeg,
  algo: :size,
  quality: 82,
  strip_metadata: true
)

Algorithms:

  • :size / :mozjpeg — smaller files, default; uses optimized progressive MozJPEG output by default
  • :fast / :jpeg_turbo — faster baseline mode

Common options:

ImagePack.compress_bytes(jpeg, min_ssim: 0.985)
ImagePack.compress_bytes(jpeg, progressive: false) # force baseline output
ImagePack.compress_bytes(jpeg, strict: true)
ImagePack.compress_bytes(jpeg, report: true)
ImagePack.compress_bytes(jpeg, subsampling: 444)
ImagePack.compress_bytes(jpeg, scale: 0.5) # decode-time 1/2, 1/4, or 1/8
ImagePack.compress_bytes(jpeg, tune: :hvs, effort: :max)

algo: :size / :mozjpeg now defaults to optimized progressive scans plus scan-aware MozJPEG trellis tuning because that is the strongest built-in size profile used by the gem. Pass progressive: false when you explicitly need baseline JPEG output.

algo: :fast / :jpeg_turbo keeps baseline output by default and remains the throughput path.

min_ssim: searches for the lowest acceptable quality using a fast native luma SSIM guard. The metric is luma-only; with subsampling: :auto (the default) a failed guard retries 4:4:4 before raising QualityConstraintError. Pass subsampling: 420, 422, or 444 (or the symbols :auto, :"420", :"422", :"444") to force chroma sampling. For algo: :size / :mozjpeg, :auto uses 4:4:4 at quality >= 90 and 4:2:0 otherwise. algo: :fast / :jpeg_turbo keeps 4:2:0 unless you set subsampling: explicitly.

strip_metadata: true (the compress default) still keeps the ICC profile. Pass strip_icc: true to drop it. Existing JPEG→JPEG compress uses a planar YCbCr path when sampling can be preserved, so chroma is not upsampled and downsampled again.

tune: is one of :hvs (default), :ssim, :ms_ssim, :psnr — the cjpeg-style MozJPEG presets. effort: is :default, :fast (skips scan search unless mozjpeg_scan_opt: is passed), or :max (extra trellis loops + quant-table opt).

scale: is decode-time only (1, 0.5, 0.25, 0.125, a Rational, or [num, denom]).

strict: true raises ImagePack::InvalidImageError on damaged/truncated JPEG warnings.

report: true returns a Hash:

{
  output: "\xFF\xD8...",
  quality: 84,
  ssim: 0.9861,
  algo: :mozjpeg,
  bytesize: 50122,
  input_bytesize: 81344,
  warning_count: 0,
  warning: nil
}

With output: "file.jpg", output is true.

Lossless optimize

ImagePack.optimize_bytes(jpeg)
ImagePack.optimize_file("photo.jpg", output: "photo.optimized.jpg")

This rewrites JPEG coefficients without decoding and re-encoding pixels. It is the right path for existing JPEGs when you only want optimized Huffman tables and optional progressive scans. progressive: true (the default) runs MozJPEG scan optimization (the jpegrescan-style search). Pass progressive: false for a baseline Huffman rewrite; that flag is applied explicitly and does not inherit the max-compression scan search.

Defaults: progressive: true, strip_metadata: false.

If strip_metadata: true would remove EXIF Orientation, optimize_jpeg applies a lossless coefficient rotate/flip when the image is MCU-aligned. If a rotate/flip would crop partial MCU edges, it raises UnsupportedError instead of trimming. Pass trim: true to allow jpegtran-style cropping. ICC is kept unless strip_icc: true.

Raw pixels

ImagePack.compress_pixels(rgb,
  width: 1920,
  height: 1080,
  channels: 3,
  output: "frame.jpg"
)

channels must be 1, 3, or 4. JPEG cannot store alpha, so RGBA input needs explicit opt-in:

ImagePack.compress_pixels(rgba, width: 100, height: 100, channels: 4, drop_alpha: true)

Inspect

ImagePack.inspect_image(jpeg)
# => { format: :jpeg, width: 1920, height: 1080, channels: 3, bit_depth: 8, decoded_bytes: 6220800 }

Execution

Default mode is :auto.

ImagePack.compress_bytes(jpeg, execution: :auto)
ImagePack.compress_bytes(jpeg, execution: :direct)
ImagePack.compress_bytes(jpeg, execution: :nogvl)
ImagePack.compress_bytes(jpeg, execution: :offload) # Ruby >= 3.4 only

Use ImagePack.offload_safe? or ImagePack.build_info to inspect runtime support.

Set IMAGE_PACK_DISABLE_OFFLOAD=1 before loading the gem to disable offload.

Long no-GVL/offload calls can be interrupted by raising into the worker thread:

worker = Thread.new { ImagePack.compress_bytes(jpeg, execution: :nogvl, cancellable: true) }
worker.raise(ImagePack::CancelledError, "cancelled")
worker.join

Configuration

ImagePack.configure do |config|
  config.execution = :auto
  config.max_input_size = 256 * 1024 * 1024
  config.max_output_size = 256 * 1024 * 1024
  config.max_pixels = 100_000_000
end

Development

Core build and test dependencies work on Ruby 2.7.1+:

bundle install
bundle exec rake compile
bundle exec rake test

Optional comparison benchmarks and Async tooling are kept out of Ruby 2.7 dependency resolution. Enable them on modern Ruby when needed:

BUNDLE_WITH=modern_development bundle install

Vendoring and release checks:

bundle exec rake vendor
bundle exec rake release:check

rake vendor pins MozJPEG v4.1.5.

rake release:check compiles, verifies tests, and fails release builds when SIMD is unavailable. Set IMAGE_PACK_ALLOW_SCALAR=1 only when intentionally shipping a scalar build.

Limits

  • JPEG only.
  • Ruby >= 2.7.1; execution: :offload requires Ruby >= 3.4. On Ruby 2.7–3.3, :auto uses :direct or :nogvl; it never attempts scheduler offload.
  • Pixel-level compress rejects CMYK/YCCK JPEG input; use optimize_jpeg for existing CMYK/YCCK JPEGs.
  • min_ssim is luma-only. Color artifacts from 4:2:0 are not scored; use subsampling: or rely on the auto 4:4:4 retry (size profile).
  • Arithmetic-coded JPEG support is disabled in 0.2.5.
  • Streaming output is not supported; file output uses atomic write-through-temp-file and rename.
  • ImagePack.compress(input, ...) keeps a legacy path-vs-bytes heuristic; prefer explicit *_bytes / *_file helpers.