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
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)
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)
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 π
Top comments (0)