PDF-to-JPG in Ruby comes down to two questions: do you have ImageMagick or Poppler installed, and how many pages do you need? MiniMagick (ImageMagick wrapper) is the most familiar approach for Rails developers. pdftoppm is faster for batch work. If you're on a PaaS where you can't install binaries, the API is the no-dependency path.
Method 1: MiniMagick (ImageMagick wrapper)
MiniMagick wraps the ImageMagick CLI. It's widely used in Rails apps for image processing and handles PDF pages via the [N] page syntax.
gem install mini_magick
# Or add to Gemfile: gem 'mini_magick'
bundle installrequire 'mini_magick'
require 'pathname'
def pdf_to_jpg(pdf_path, out_dir, dpi: 150, quality: 90)
Pathname(out_dir).mkpath
# Get page count without loading all pages
identify = MiniMagick::Tool::Identify.new
identify << "-format" << "%p "
identify << pdf_path
page_count = identify.call.split.size
page_count = [page_count, 1].max
paths = []
page_count.times do |i|
image = MiniMagick::Image.open("#{pdf_path}[#{i}]") do |b|
b.density dpi # must be set before loading
end
image.format 'jpg'
image.quality quality
image.colorspace 'sRGB' # prevent CMYK color shifts
out_path = File.join(out_dir, "page-#{i + 1}".rjust(7, 'page-00') + ".jpg")
out_path = File.join(out_dir, format('page-%03d.jpg', i + 1))
image.write out_path
paths << out_path
end
paths
end
pages = pdf_to_jpg('document.pdf', './pages', dpi: 150)
puts "Wrote #{pages.size} pages"
The density setting must be applied at load time — use the block form of MiniMagick::Image.open to pass density before the file is read. Setting it after loading has no effect.
Check Imagick policy.xml if you get 'not authorized' errors on PDFs — see production tips below.
Method 2: Poppler pdftoppm (faster for batch)
Poppler's pdftoppm renders PDF pages to images faster than ImageMagick for large batch jobs. It's a system binary — no gem needed.
# Ubuntu/Debian
apt install poppler-utils
# macOS
brew install popplerrequire 'open3'
require 'pathname'
def pdf_to_jpg_poppler(pdf_path, out_dir, dpi: 150, quality: 90)
Pathname(out_dir).mkpath
prefix = File.join(out_dir, 'page')
# pdftoppm renders all pages; -jpeg outputs JPEG, -r sets DPI
cmd = [
'pdftoppm',
'-jpeg',
'-r', dpi.to_s,
'-jpegopt', "quality=#{quality}",
pdf_path,
prefix
]
stdout, stderr, status = Open3.capture3(*cmd)
unless status.success?
raise "pdftoppm failed (exit #{status.exitstatus}): #{stderr.strip}"
end
# pdftoppm names files as prefix-1.jpg, prefix-2.jpg...
Dir.glob("#{out_dir}/page-*.jpg").sort
end
pages = pdf_to_jpg_poppler('document.pdf', './pages', dpi: 150)
puts "Wrote #{pages.size} pages"
pdftoppm is 2-4x faster than ImageMagick for multi-page PDFs and doesn't require Ghostscript. For single-page PDFs or thumbnail generation, the speed difference is negligible.
Method 3: ChangeThisFile API (Net::HTTP, no installs)
The ChangeThisFile API runs Poppler server-side. Source format is auto-detected from the filename — pass target=jpg only. Free tier: 1,000 conversions/month, no card needed.
require 'net/http'
require 'uri'
require 'tempfile'
require 'zip'
API_KEY = 'ctf_sk_your_key_here'
def pdf_to_jpg_api(pdf_path, out_dir)
Dir.mkdir(out_dir) unless Dir.exist?(out_dir)
uri = URI('https://changethisfile.com/v1/convert')
boundary = "CTF#{SecureRandom.hex(8)}"
file_data = File.binread(pdf_path)
body = build_multipart(boundary, File.basename(pdf_path), file_data, 'jpg')
req = Net::HTTP::Post.new(uri)
req['Authorization'] = "Bearer #{API_KEY}"
req['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
req.body = body
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 120) do |http|
http.request(req)
end
raise "API error: HTTP #{response.code}" unless response.code == '200'
content_type = response['content-type'] || ''
if content_type.include?('application/zip')
# Multi-page PDF — extract ZIP
Tempfile.create(['ctf_pages', '.zip'], binmode: true) do |f|
f.write(response.body)
f.flush
Zip::File.open(f.path) { |zip| zip.each { |e| e.extract(File.join(out_dir, e.name)) } }
end
Dir.glob("#{out_dir}/*.jpg").sort
else
out_path = File.join(out_dir, 'page-001.jpg')
File.binwrite(out_path, response.body)
[out_path]
end
end
def build_multipart(boundary, filename, data, target)
body = String.new
body << "--#{boundary}\r\n"
body << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n"
body << "Content-Type: application/pdf\r\n\r\n"
body << data
body << "\r\n--#{boundary}\r\n"
body << "Content-Disposition: form-data; name=\"target\"\r\n\r\n"
body << target
body << "\r\n--#{boundary}--\r\n"
body
end
pages = pdf_to_jpg_api('document.pdf', './pages')
puts "Got #{pages.size} pages"
Note: the rubyzip gem is needed for ZIP extraction when the input is a multi-page PDF. Add gem 'rubyzip' to your Gemfile, or handle the single-page case only if your PDFs are always one page.
When to use each
| Approach | Best for | Tradeoff |
|---|---|---|
| MiniMagick | Rails apps, already using ImageMagick | Needs ImageMagick + Ghostscript; policy.xml may block PDFs |
| pdftoppm | Fast batch multi-page conversion | Requires poppler-utils system package |
| ChangeThisFile API | PaaS/Heroku, no binary installs, quick scripts | Network call; 25MB file limit on free tier |
Production tips
- MiniMagick 'not authorized' for PDFs? ImageMagick policy.xml disables PDF by default on many distros. Edit /etc/ImageMagick-6/policy.xml — change the PDF pattern's rights from 'none' to 'read|write'.
- Set density in the open block, not after. MiniMagick::Image.open(path) { |b| b.density 150 } applies DPI before loading. Setting image.density 150 after open has no effect on already-rendered pages.
- Use Open3 instead of backticks for system commands. Open3.capture3 gives you stdout, stderr, and exit status separately. Backticks swallow stderr and hide errors.
- Process pages in parallel for large PDFs. Split pdftoppm's page range with -f and -l flags and run multiple processes simultaneously. ImageMagick is single-threaded per call.
- Heroku/Render: use Aptfile for poppler-utils. On Heroku with the apt buildpack, add poppler-utils to Aptfile. On Render, add it to the build command.
MiniMagick is the natural choice for Rails apps already using ImageMagick. pdftoppm wins on speed for batch jobs. For Heroku or other PaaS where binary installs are painful, the API skips all of it. Free tier: 1,000 conversions/month.