PNG-to-PDF in Ruby is about wrapping an image in a printable document. Prawn is the most-used PDF generation library in the Ruby ecosystem — it's used in many Rails apps for invoice generation and handles PNG images cleanly. HexaPDF is newer, faster, and stricter about PDF compliance. Both work for the common case of embedding one or more PNGs in a PDF document.

Method 1: Prawn (Ruby PDF generation standard)

Prawn is the most widely used Ruby PDF library. It handles PNG images, custom page sizes, and multi-page documents.

gem install prawn prawn-table
# Or Gemfile:
# gem 'prawn'
bundle install
require 'prawn'

# Single PNG to PDF, page sized to image
def png_to_pdf(png_path, out_path)
  # Get image dimensions
  png_info = IO.popen(['identify', '-format', '%wx%h', png_path], &:read)
  img_w, img_h = png_info.split('x').map(&:to_f)

  # Convert pixels to points (72 pts per inch, 96px per inch is typical screen)
  pts_w = img_w * 72.0 / 96.0
  pts_h = img_h * 72.0 / 96.0

  Prawn::Document.generate(out_path,
    page_size: [pts_w, pts_h],
    margin: 0
  ) do |pdf|
    pdf.image png_path, at: [0, pts_h], width: pts_w, height: pts_h
  end
end

# Multiple PNGs to multi-page PDF (A4)
def pngs_to_pdf(png_paths, out_path)
  Prawn::Document.generate(out_path, page_size: 'A4', margin: [30, 30, 30, 30]) do |pdf|
    png_paths.each_with_index do |png, idx|
      pdf.start_new_page if idx > 0

      # Scale image to fit within page margins
      avail_w = pdf.bounds.width
      avail_h = pdf.bounds.height
      pdf.image png, fit: [avail_w, avail_h], position: :center, vposition: :center
    end
  end
end

png_to_pdf('screenshot.png', 'output.pdf')
pngs_to_pdf(['scan1.png', 'scan2.png', 'scan3.png'], 'scans.pdf')
puts 'Done'

Prawn's image() method with fit: [w, h] scales the image to fit within the given bounds while preserving aspect ratio. The position: :center option centers it on the page both horizontally and vertically.

Note: For the size calculation without ImageMagick's identify, use the pure-Ruby fastimage gem: require 'fastimage'; w, h = FastImage.size(png_path).

Method 2: HexaPDF (faster, strict PDF compliance)

HexaPDF is a newer Ruby PDF library — faster than Prawn and stricter about PDF spec compliance. Good for high-volume or archival use cases.

gem install hexapdf
# Or Gemfile: gem 'hexapdf'
require 'hexapdf'

def png_to_pdf_hexapdf(png_path, out_path)
  doc = HexaPDF::Document.new
  page = doc.pages.add

  # Load the image
  img = doc.images.add(png_path)
  img_w = img.width
  img_h = img.height

  # Set page size to image size (in points at 72 DPI)
  # If PNG is at 96 DPI screen res, convert: pts = px * 72 / 96
  pts_w = img_w * 72.0 / 96.0
  pts_h = img_h * 72.0 / 96.0

  page.box(:media, width: pts_w, height: pts_h)

  # Draw image filling the page
  canvas = page.canvas
  canvas.xobject(img, at: [0, 0], width: pts_w, height: pts_h)

  doc.write(out_path)
end

png_to_pdf_hexapdf('image.png', 'output.pdf')
puts 'Done'

HexaPDF is significantly faster than Prawn for large images and produces smaller PDF files. The API is lower-level than Prawn — you work directly with the PDF canvas rather than a higher-level document model.

Method 3: ChangeThisFile API (Net::HTTP, no gems)

The API converts PNG to PDF server-side. Source auto-detected from filename — pass target=pdf. Free tier: 1,000 conversions/month, no card needed.

require 'net/http'
require 'uri'
require 'securerandom'

API_KEY = 'ctf_sk_your_key_here'

def png_to_pdf_api(png_path, out_path)
  uri = URI('https://changethisfile.com/v1/convert')
  boundary = "CTF#{SecureRandom.hex(8)}"

  file_data = File.binread(png_path)
  body = [
    "--#{boundary}\r\n",
    'Content-Disposition: form-data; name="file"; filename="' + File.basename(png_path) + "\"\r\n",
    "Content-Type: image/png\r\n\r\n",
    file_data, "\r\n",
    "--#{boundary}\r\n",
    "Content-Disposition: form-data; name=\"target\"\r\n\r\n",
    "pdf\r\n",
    "--#{boundary}--\r\n"
  ].join

  req = Net::HTTP::Post.new(uri)
  req['Authorization'] = "Bearer #{API_KEY}"
  req['Content-Type'] = "multipart/form-data; boundary=#{boundary}"
  req.body = body

  resp = Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: 60) { |h| h.request(req) }
  raise "API error: #{resp.code}" unless resp.code == '200'

  File.binwrite(out_path, resp.body)
end

png_to_pdf_api('screenshot.png', 'output.pdf')
puts 'Done'

When to use each

ApproachBest forTradeoff
PrawnRails apps, invoice generation, multi-page documentsSlower and larger output than HexaPDF; mature ecosystem
HexaPDFHigh volume, archival PDFs, strict complianceLower-level API than Prawn; newer, smaller community
ChangeThisFile APIZero gems, quick scripts, shared hostingNetwork call; free tier 25MB limit

Production tips

  • Use fit: [w, h] not width: w in Prawn for images. width: alone stretches the image if the aspect ratio doesn't match the specified size. fit: [w, h] preserves aspect ratio.
  • Get PNG dimensions without ImageMagick. Use the fastimage gem (gem install fastimage): FastImage.size(path) returns [width, height] by reading just the first bytes of the PNG header.
  • Prawn image at: [0, pdf.cursor] anchors to current position. In Prawn's coordinate system, y increases upward. Use pdf.cursor for the current vertical position, not a hardcoded y value.
  • HexaPDF is better for large batches. For converting 100+ PNGs to PDFs, HexaPDF's lower memory footprint and faster rendering make a significant difference in throughput.

Prawn is the right default for Rails apps and invoice generation. HexaPDF is worth considering for high-volume batch jobs. For scripts where you'd rather not add a gem, the API handles it with just Net::HTTP. Free tier: 1,000 conversions/month.