DEV Community

Jonathan Hooper
Jonathan Hooper

Posted on β€’ Edited on

Using Chromedriver to create PDFs with ruby

Generating PDFs from HTML is a common challenge in web development, especially when maintaining precise layouts and styling. Whether it's invoices, reports, or dynamically generated documents, developers need reliable tools to convert HTML to PDFs accurately. There are many tools out there to solve it. Recently, I worked on html2pdf_chrome, a Ruby gem that wraps ChromeDriver to generate PDFs, and I learned a lot in the process.

There are plenty of libraries for generating PDFs from HTML, including wkhtmltopdf and Puppeteer. However, Chrome’s built-in printing capabilities often produce more accurate results, especially for modern web layouts that rely on CSS Grid, flexbox, and web fonts. ChromeDriver provides programmatic access to this functionality, allowing us to script PDF generation.

Installing Chromedriver

Obviously using Chromedriver means installing Chromedriver. It can be installed with most package managers (e.g. Homebrew):

brew install chromedriver
Enter fullscreen mode Exit fullscreen mode

A simple approach

To manage the interface with Chromedriver I used Selenium. Here's a simple example of using Selenium to generate a PDF from HTML:

require 'base64'
require 'selenium-webdriver'

# Create an options object for starting Chromedriver
options = Selenium::WebDriver::Chrome::Options.new
options.add_argument('--headless=new')
options.add_argument('--disable-gpu')
options.add_argument('--no-sandbox')
options.add_argument('--disable-software-rasterizer')

# Create a Chromedriver instance
driver = Selenium::WebDriver.for(:chrome, options: options)

# Create a data URL for the html string
html_string = "<h1>This will be a PDF soon</h1>"
encoded_html = Base64.strict_encode64(html_string)
data_url = "data:text/html;base64,#{encoded_html}"

# Visit the data URL and get the rendered PDF
driver.navigate.to(data_url)
cdp_response = driver.execute_cdp('Page.printToPDF')
pdf_data = Base64.decode64(cdp_response['data'])

# Save the PDF!
File.write("output.pdf", pdf_data)
Enter fullscreen mode Exit fullscreen mode

Productionization

Creating the driver with Selenium starts a ChromeDriver process, which can be resource-intensive and takes time to initialize. This can be slow and expensive. It will be better for us to create a single driver instance once and then re-use it multiple times.

In a multi-threaded environment, multiple threads may attempt to generate PDFs simultaneously. We need to control access to the driver to prevent interference. I did that in the html2pdf_chrome gem. An abbreviated example of what that looks like is below:

# Create a function for creating a driver.
def initialize_driver
  options = Selenium::WebDriver::Chrome::Options.new
  options.add_argument('--headless=new')
  options.add_argument('--disable-gpu')
  options.add_argument('--no-sandbox')
  options.add_argument('--disable-software-rasterizer')

  Selenium::WebDriver.for(:chrome, options: options)
end

# Create a function for fetching a singleton driver with a Mutex
def fetch_driver
  @driver ||= initialize_driver
  @semaphore ||= Mutex.new
  @semaphore.synchronize do
    yield @driver
  end
  nil
end

# Encode the HTML like before
html_string = "<h1>This will be a PDF soon</h1>"
encoded_html = Base64.strict_encode64(html_string)
data_url = "data:text/html;base64,#{encoded_html}"

# Get the driver and use it to generate the PDF
pdf_data = nil
fetch_driver do |driver|
  driver.navigate.to(data_url)
  cdp_response = driver.execute_cdp('Page.printToPDF')
  pdf_data = Base64.decode64(cdp_response['data'])
end

# Save the PDF!
File.write("output.pdf", pdf_data)
Enter fullscreen mode Exit fullscreen mode

Conclusion

Using ChromeDriver with Selenium to generate PDFs from HTML works well and turns out to be surprisingly easy. The hardest part is installing Chromedriver. If you want to do this in production make sure to control access to the driver to enable concurrent PDF generation in multi-threaded environments. You could also just use the gem I wrote that does it all for you πŸ™‚

Neon image

Serverless Postgres in 300ms (❗️)

10 free databases with autoscaling, scale-to-zero, and read replicas. Start building without infrastructure headaches. No credit card needed.

Try for Free β†’

Top comments (0)

Jetbrains Survey

Calling all developers!

Participate in the Developer Ecosystem Survey 2025 and get the chance to win a MacBook Pro, an iPhone 16, or other exciting prizes. Contribute to our research on the development landscape.

Take the survey

πŸ‘‹ Kindness is contagious

Please leave a ❀️ or a friendly comment on this post if you found it helpful!

Okay