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 install
require '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 poppler
require '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

ApproachBest forTradeoff
MiniMagickRails apps, already using ImageMagickNeeds ImageMagick + Ghostscript; policy.xml may block PDFs
pdftoppmFast batch multi-page conversionRequires poppler-utils system package
ChangeThisFile APIPaaS/Heroku, no binary installs, quick scriptsNetwork 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.