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 🙂

Runner H image

🦺 CrisisCopilot – Your AI Agent for Personal Emergency Management

Check out this winning submission to the Runner H "AI Agent Prompting" Challenge. 👀

Read more →

Top comments (0)

Feature flag article image

Create a feature flag in your IDE in 5 minutes with LaunchDarkly’s MCP server 🏁

How to create, evaluate, and modify flags from within your IDE or AI client using natural language with LaunchDarkly's new MCP server. Follow along with this tutorial for step by step instructions.

Read full post

👋 Kindness is contagious

Explore a fresh perspective in this standout article, celebrated across the DEV Community. Developers of every specialty are welcome to add their voice and boost our collective expertise.

Even a quick “thank you” can brighten someone’s day—share yours in the comments!

On DEV, collaboration drives growth and fosters genuine connections. Found value here? Let the author know with a simple thanks.

Join DEV