Skip to content

Instantly share code, notes, and snippets.

@dieter-medium
Last active May 5, 2025 06:59
Show Gist options
  • Save dieter-medium/01a5bbe9e8e24dee6627820d8002b734 to your computer and use it in GitHub Desktop.
Save dieter-medium/01a5bbe9e8e24dee6627820d8002b734 to your computer and use it in GitHub Desktop.
This proof of concept demonstrates how to generate a PDF from a local HTML file using plain Ruby and WebDriver BiDi. The script starts Chromedriver in headless mode, creates a new browsing context via BiDi, navigates to the specified local file (using a placeholder for the file path), and prints the page as a PDF over a WebSocket connection. No …
#!/usr/bin/env ruby
# see:
# https://w3c.github.io/webdriver-bidi/#command-browsingContext-print
# https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-printToPDF
# https://github.com/GoogleChromeLabs/chromium-bidi/blob/main/examples/print_example.py
# https://github.com/GoogleChromeLabs/chromium-bidi/blob/main/examples/_helpers.py
require 'net/http'
require 'uri'
require 'json'
require 'websocket-client-simple'
require 'base64'
require 'webdrivers/chromedriver'
Webdrivers.logger.level = :DEBUG
Webdrivers::Chromedriver.update
chromedriver_bin = Webdrivers::Chromedriver.driver_path
# Start Chromedriver on port 9515 in headless mode
chromedriver_cmd = "#{chromedriver_bin} --headless --port=9515"
chromedriver_pid = Process.spawn(chromedriver_cmd)
puts "Started Chromedriver with PID #{chromedriver_pid}"
# Ensure Chromedriver is terminated when the program exits
at_exit do
Process.kill("TERM", chromedriver_pid) rescue nil
end
# Wait a short moment to ensure Chromedriver is ready
sleep 2
# Create a new WebDriver session
uri = URI("http://localhost:9515/session")
session_request = {
"capabilities" => {
"alwaysMatch" => {
"browserName" => "chrome",
"goog:chromeOptions" => {
"args" => ["--headless", "--disable-gpu"]
},
"acceptInsecureCerts" => true,
"webSocketUrl" => true
}
}
}
response = Net::HTTP.post(uri, session_request.to_json, "Content-Type" => "application/json")
session_data = JSON.parse(response.body)
session_id = session_data["value"]["sessionId"]
websocket_url = session_data["value"]["capabilities"]["webSocketUrl"]
puts "Session data: #{session_data}"
puts "Created session with ID: #{session_id}"
puts "WebSocket URL: #{websocket_url}"
# Define the path to your local HTML file.
# Replace "YOUR_LOCAL_HTML_FILE.html" with your actual file path.
local_html_path = File.expand_path("YOUR_LOCAL_HTML_FILE.html")
local_file_url = "file://#{local_html_path}"
# Connect to the WebDriver BiDi endpoint via WebSocket
ws = WebSocket::Client::Simple.connect(websocket_url)
browsing_context_id = nil
ws.on :open do
puts "Connected to WebDriver BiDi WebSocket."
# Create a new browsing context (i.e. a new tab)
create_context_msg = {
id: 1,
method: "browsingContext.create",
params: { type: "tab" }
}
ws.send(create_context_msg.to_json)
end
ws.on :message do |msg|
data = JSON.parse(msg.data)
puts data
if data["id"] == 1 && data["result"]
browsing_context_id = data["result"]["context"]
puts "Created browsing context: #{browsing_context_id}"
puts "Navigating to #{local_file_url}"
# Navigate to the local HTML file and wait for the page to load completely
navigate_msg = {
id: 2,
method: "browsingContext.navigate",
params: {
url: local_file_url,
context: browsing_context_id,
wait: "complete"
}
}
ws.send(navigate_msg.to_json)
elsif data["id"] == 2 && data["result"]
puts "Navigation complete in browsing context: #{browsing_context_id}"
# Print the page as a PDF using the WebDriver BiDi print command
print_pdf_msg = {
id: 3,
method: "browsingContext.print",
params: {
context: browsing_context_id
}
}
ws.send(print_pdf_msg.to_json)
elsif data["id"] == 3 && data["result"]
pdf_base64 = data["result"]["data"]
File.open("output.pdf", "wb") do |file|
file.write(Base64.decode64(pdf_base64))
end
puts "PDF saved as 'output.pdf'."
# Close the browsing context
close_context_msg = {
id: 4,
method: "browsingContext.close",
params: { context: browsing_context_id }
}
ws.send(close_context_msg.to_json)
ws.close
end
end
ws.on :error do |e|
puts "Error: #{e.message}"
end
ws.on :close do |_e|
puts "WebSocket connection closed."
exit 0
end
# Keep the program running until the WebSocket connection is closed
loop do
sleep 1
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment