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:fast/:jpeg_turbo— faster mode
Common options:
ImagePack.compress_bytes(jpeg, min_ssim: 0.985)
ImagePack.compress_bytes(jpeg, progressive: true)
ImagePack.compress_bytes(jpeg, strict: true)
ImagePack.compress_bytes(jpeg, report: true)
min_ssim: searches for the lowest acceptable quality using a fast native luma SSIM guard.
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.
Defaults: progressive: true, strip_metadata: false.
If strip_metadata: true would remove EXIF Orientation, optimize_jpeg raises UnsupportedError instead of silently changing visual orientation.
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
bundle exec rake vendor
bundle exec rake compile
bundle exec rake test
rake vendor pins MozJPEG v4.1.5.
Limits
- JPEG only.
- Ruby
>= 3.1;execution: :offloadrequires Ruby>= 3.4. - Pixel-level
compressrejects CMYK/YCCK JPEG input; useoptimize_jpegfor existing CMYK/YCCK JPEGs. - Arithmetic-coded JPEG support is disabled in
0.2.4. - 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/*_filehelpers.