π Bidi2pdfRails
Bidi2pdfRails is the official Rails integration for Bidi2pdf β a
modern, headless-browser-based PDF rendering engine.
Generate high-fidelity PDFs directly from your Rails views or external URLs with minimal setup.
β¨ Features
- π Accurate PDF rendering using a real browser engine
- πΎ Supports both HTML string rendering and remote URL conversion
- π Built-in support for authentication (Basic Auth, cookies, headers)
- π§° Full test suite with examples for Rails controller integration
- π§ Sensible defaults, yet fully configurable
π§ Installation
Add to your Gemfile:
gem "bidi2pdf-rails"
# for development only
# gem "bidi2pdf-rails", github: "dieter-medium/bidi2pdf-rails", branch: "main"
# Optional for performance:
# gem "websocket-native"
Install it:
bundle install
Generate the config initializer:
bin/rails generate bidi2pdf_rails:initializer
π Architecture
%%{ init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#E0E7FF",
"secondaryColor":"#FEF9C3",
"tertiaryColor": "#DCFCE7",
"edgeLabelBackground":"#FFFFFF",
"fontSize":"14px",
"nodeBorderRadius":"6"
}
}
}%%
flowchart LR
%% βββββββββββββββββββββββββββββββββββ
%% Rails world
subgraph R["fa:fa-rails Rails World"]
direction TB
R1["fa:fa-gem Your Rails App"]
R2["fa:fa-plug bidi2pdf-rails Engine"]
R3["fa:fa-cog ActionController::Renderers<br/><code>render pdf:</code>"]
R4["fa:fa-file-code Rails View (ERB/Haml)"]
end
%% bidi2pdf core
subgraph B["fa:fa-gem bidi2pdf Core"]
direction TB
B1["fa:fa-gem bidi2pdf"]
end
%% Chrome env
subgraph C["fa:fa-chrome Chrome Environment"]
direction LR
C1["fa:fa-chrome Local Chrome<br/>(sub-process)"]
C2["fa:fa-docker Docker Chrome<br/>(remote)"]
end
%% Artifact
P[[PDF File]]
%% βββ Flows βββββββββββββββββββββββββ
R1 -- " Controller invokes<br/><code>render pdf:</code> " --> R3
R3 -- " HTML + Assets<br/>(via <code>render_to_string</code>) " --> R2
R2 -- " HTML / URL + CSS/JS " --> B1
B1 -- " WebDriver BiDi " --> C1
B1 -- " WebDriver BiDi " --> C2
C1 -- " PDF bytes " --> B1
C2 -- " PDF bytes " --> B1
B1 -- " PDF stream " --> R2
R2 -- " send_data " --> R1
R1 -- " Download/inline " --> P
%% βββ Styling classes βββββββββββββββ
classDef rails fill: #E0E7FF, stroke: #6366F1, color: #1E1B4B
classDef engine fill: #c7d2fe, stroke: #4338CA, color: #1E1B4B
classDef bidi fill: #E0E7FF, stroke: #4f46e5, color: #1E1B4B
classDef chrome fill: #FEF9C3, stroke: #F59E0B, color: #78350F
classDef artifact fill: #DCFCE7, stroke: #16A34A, color: #065F46
class R1 rails
class R2 engine
class R3,R4 rails
class B1 bidi
class C1,C2 chrome
class P artifact
π¦ Usage Examples
π Rendering a Rails View as PDF
# app/controllers/invoices_controller.rb
def show
render pdf: "invoice",
template: "invoices/show",
layout: "pdf",
locals: { invoice: @invoice },
print_options: { landscape: true },
wait_for_network_idle: true
end
π Rendering a Remote URL to PDF
def convert
render pdf: "external-report",
url: "https://example.com/dashboard",
wait_for_page_loaded: false,
print_options: { page: { format: :A4 } }
end
π‘οΈ Authentication Support
Need to convert pages that require authentication? No problem. Use:
auth: { username:, password: }
cookies: { session_key: value }
headers: { "Authorization" => "Bearer ..." }
Example:
render pdf: "secure",
url: secure_report_url,
auth: { username: "admin", password: "secret" }
Or use global config in bidi2pdf_rails.rb
initializer:
config.render_remote_settings.basic_auth_user = ->(_) { "admin" }
config.render_remote_settings.basic_auth_pass = ->(_) { Rails.application.credentials.dig(:pdf, :auth_pass) }
π Asset Access via CORS
When rendering HTML with render_to_string
, Chromium needs access to your assets (CSS, images, fonts).
Enable CORS for /assets
using rack-cors
:
# Gemfile
gem 'rack-cors'
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '/assets/*', headers: :any, methods: [:get, :options]
end
end
π Development Mode Considerations
Deadlock Warning
In Rails development mode, loopback asset or page requests (e.g., when ChromeDriver or Grover fetches your own appβs URL) can deadlock under Railsβ autoload interlock. See Pumaβs docs: https://puma.io/puma/file.rails_dev_mode.html
Workarounds:
-
Precompile & serve assets statically (in
config/environments/development.rb
):config.public_file_server.enabled = true
-
Run Puma with single-threaded workers:
workers ENV.fetch("WEB_CONCURRENCY") { 2 } threads 1, 1
Implementing these steps helps avoid interlock-induced deadlocks when generating PDFs in development.
π§ͺ Acceptance Examples
This repo includes real integration tests that serve as usage documentation:
- Download PDF with
.pdf
format - Render protected remote URLs using Basic Auth, cookies, and headers
- Inject custom CSS into a Webpage before printing
- Inject custom JS into a Webpage before printing
- Using callbacks to modify the PDF before sending
- Using a remote chromedriver
- Using ActiveStorage and ActiveJob to generate PDFs in the background
π§ Configuration
Bidi2pdfRails is highly configurable.
See full config options in:
bin/rails generate bidi2pdf_rails:initializer
Or explore Bidi2pdfRails::Config::CONFIG_OPTIONS in the source.
π§ͺ Test Helpers
On top of Bidi2pdf test helpers, Bidi2pdfRails provides a suite of RSpec helpers (activated with pdf: true
) to
simplify PDF-related testing:
EnvironmentHelper
β inside_container?
β true if running in Docker
β environment_type
β one of :ci
, :codespaces
, :container
, :local
β environment_β¦?
predicates for each type
SettingsHelper
β with_render_setting(key, value)
β with_pdf_settings(key, value)
β with_lifecycle_settings(key, value)
β with_chromedriver_settings(key, value)
β with_proxy_settings(key, value)
β¦plus automatic reset after each pdf: true
example
ServerHelper
β server_running?
, server_port
, server_host
, server_url
β boots a Puma test server before suite type: :request, pdf: true
specs
β shuts it down afterward
RequestHelper
β get_pdf_response(path)
β fetches raw HTTP response
β follow_redirects(response, max_redirects = 10)
Usage
Tag your examples or example groups:
RSpec.describe "Invoice PDF", type: :request, pdf: true do
it "renders a complete PDF" do
response = get_pdf_response "/invoices/123.pdf"
expect(response['Content-Type']).to eq("application/pdf")
end
end
π Contributing
Pull requests, issues, and ideas are all welcome π
Want to contribute? Just fork, branch, and PR like a boss.
Contribution guide coming soon!
π License
This gem is released under the MIT License.
Use freely β and responsibly.