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 πŸ™‚

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

πŸ‘‹ Kindness is contagious

If this post resonated with you, feel free to hit ❀️ or leave a quick comment to share your thoughts!

Okay