Project

bidi2pdf

0.01
A long-lived project that still receives updates
Bidi2pdf is a powerful PDF generation tool that uses Chrome's BiDirectional Protocol to render web pages as high-quality PDF documents. It offers: * Command-line interface for easy PDF generation * Support for cookies, headers, and basic authentication * Waiting conditions (window loaded, network idle) * Headless Chrome operation for server environments * Docker compatibility * Customizable PDF output options Bidi2pdf uses ChromeDriver to control Chrome through its BiDi protocol, providing precise rendering for reports, invoices, documentation, and other PDF documents from web-based content. It automatically manages the ChromeDriver binary and browser sessions for a seamless experience.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Development

Runtime

>= 0.2, < 0.4
~> 2.10
~> 2.4
~> 1.3
~> 1.0, >= 1.3.1
 Project Readme

Build Status Maintainability Gem Version Test Coverage Open Source Helpers


πŸ“„ Bidi2pdf – Bulletproof PDF generation via Chrome's BiDi Protocol

Bidi2pdf is a powerful Ruby gem that transforms modern web pages into high-fidelity PDFs using Chrome’s BiDirectional (BiDi) protocol. Whether you're automating reports, archiving websites, or shipping documentation, Bidi2pdf gives you precision, flexibility, and full control.


πŸ“š Table of Contents

  1. Key Features
  2. Quick Start
  3. Why BiDi?
  4. Installation
  5. CLI Usage
  6. Library API
  7. Architecture
  8. Docker
  9. Configuration Options
  10. Rails Integration
  11. Test Helpers
  12. Development
  13. Contributing
  14. License

✨ Key Features

βœ… One-liner CLI – From URL to PDF in a single command
βœ… Full customization – Inject cookies, headers, auth credentials
βœ… Smart waiting – Wait for complete page load or network idle
βœ… Headless support – Run quietly in the background
βœ… Docker-ready – Plug and play with containers
βœ… Modern architecture – Built on Chrome's next-gen BiDi protocol
βœ… Network logging – Know which requests fail during rendering
βœ… Console log capture – See what goes wrong inside the browser


⚑ Quick Start

Get up and running in three easy steps:

# 1. Install the gem (system-wide)
gem install bidi2pdf

# 2. Render any page to PDF
bidi2pdf render --url https://example.com --output example.pdf

# 3. Open the PDF (macOS shown; use xdg-open on Linux)
open example.pdf

Bundler users – Add it to your project with bundle add bidi2pdf.


πŸš€ Installation

Bundler

gem 'bidi2pdf'

Standalone

gem install bidi2pdf

Requirements


βš™οΈ Basic Usage

Command-line

bidi2pdf render --url https://example.com/invoice/14432423 --output example.pdf

Advanced CLI Options

bidi2pdf render \
  --url https://example.com/invoice/14432423 \
  --output example.pdf \
  --cookie session=abc123 \
  --header X-API-KEY=token \
  --auth admin:password \
  --wait_network_idle \
  --wait_window_loaded \
  --log-level debug

🧠 Programmatic API

Classic Approach

require 'bidi2pdf'

launcher = Bidi2pdf::Launcher.new(
  url: 'https://example.com/invoice/14432423',
  output: 'example.pdf',
  cookies: { 'session' => 'abc123' },
  headers: { 'X-API-KEY' => 'token' },
  auth: { username: 'admin', password: 'password' },
  wait_window_loaded: true,
  wait_network_idle: true
)

launcher.launch

DSL – Quick & Clean

require "bidi2pdf"

Bidi2pdf::DSL.with_tab(headless: true) do |tab|
  tab.navigate_to("https://example.com/invoice/14432423")
  tab.wait_until_network_idle
  tab.print("example.pdf")
end

🧬 Deep Integration Example

Get fine-grained control using Chrome sessions, tabs, and BiDi commands:

πŸ” Show full example
require "bidi2pdf"

# 1. Remote or local session?
session = Bidi2pdf::Bidi::Session.new(
  session_url: "http://localhost:9092/session",
  headless: true,
)

# Alternative: local session via ChromeDriver
# manager = Bidi2pdf::ChromedriverManager.new(headless: false)
# manager.start
# session = manager.session

session.start
session.client.on_close { puts "WebSocket session closed" }

# 2. Create browser/tab
browser = session.browser
context = browser.create_user_context
window = context.create_browser_window
tab = window.create_browser_tab

# 3. Inject configuration
tab.set_cookie(name: "auth", value: "secret", domain: "example.com", secure: true)
tab.add_headers(url_patterns: [{ type: "pattern", protocol: "https", hostname: "example.com", port: "443" }],
                headers: [{ name: "X-API-KEY", value: "12345678" }])
tab.basic_auth(url_patterns: [{ type: "pattern", protocol: "https", hostname: "example.com", port: "443" }],
               username: "username", password: "secret")

# 4. Render PDF
tab.navigate_to "https://example.com/invoice/14432423"

# Alternative: send html code to the browser
# tab.render_html_content("<html>...</html>")

# Inject JavaScript if, needed
# as an url
# tab.inject_script "https://example.com/script.js" 
# or inline
# tab.inject_script "console.log('Hello from injected script!')"

# Inject CSS if needed
# as an url
# tab.inject_style url: "https://example.com/simple.css"
# or inline
# tab.inject_style content: "body { background-color: red; }"

tab.wait_until_network_idle
tab.print("my.pdf")

# 5. Cleanup
tab.close
window.close
context.close
session.close

🌐 Architecture

%%{  init: {
      "theme": "base",
      "themeVariables": {
        "primaryColor":  "#E0E7FF",
        "secondaryColor":"#FEF9C3",
        "edgeLabelBackground":"#FFFFFF",
        "fontSize":"14px",
        "nodeBorderRadius":"6"
      }
    }
}%%
flowchart LR
%% ----- Ruby side ---------
    A["fa:fa-gem Ruby Application"]
    B["fa:fa-gem bidi2pdf<br/>Library"]
%% ----Chrome environment -----------
    subgraph C["fa:fa-chrome Chrome Environment"]
        direction TB
        C1["fa:fa-chrome Local Chrome<br/>(sub-process)"]
        C2["fa:fa-docker Docker Chrome<br/>(remote)"]
    end

    D[[PDF File]]
%% ---- Data / control flows ------
    A -- " HTML / URL + JS / CSS " --> B
    B -- " WebDriver BiDi " --> C1
    B -- " WebDriver BiDi " --> C2
    C1 -- " PDF bytes " --> B
    C2 -- " PDF bytes " --> B
    B -- " PDF " --> D
%% --- Optional extra styling classes (for future tweaks) ---
    classDef ruby fill:#E0E7FF,stroke:#6366F1,color:#1E1B4B;
    classDef chrome fill:#FEF9C3,stroke:#F59E0B,color:#78350F;
    class A,B ruby;
    class C1,C2 chrome;
Loading

🐳 Docker Support

πŸ› οΈ Build & Run Locally

# Prepare the environment
rake build

# Build the Docker image
docker build -t bidi2pdf -f docker/Dockerfile .

# Run the container and generate a PDF
docker run -it --rm \
  -v ./output:/reports \
  bidi2pdf \
  bidi2pdf render --url=https://example.com/invoice/14432423 --output /reports/example.pdf

⚑ Use the Prebuilt Image (Recommended for Fast Start)

Grab it directly from Docker Hub

docker run -it --rm \
  -v ./output:/reports \
  dieters877565/bidi2pdf:main-slim \
  bidi2pdf render --url=https://example.com/invoice/14432423 --output /reports/example.pdf

βœ… Tip: Mount your local directory (e.g. ./output) to /reports in the container to easily access the generated PDFs.

Docker Compose

rake build
docker compose -f docker/docker-compose.yml up -d

# simple example
docker compose -f docker/docker-compose.yml exec app bidi2pdf render --url=http://nginx/sample.html --wait_window_loaded --wait_network_idle --output /reports/simple.pdf

# with a local file
docker compose -f docker/docker-compose.yml exec app bidi2pdf render --url=file:///reports/sample.html--wait_network_idle --output /reports/simple.pdf


# basic auth example
docker compose -f docker/docker-compose.yml exec app bidi2pdf render --url=http://nginx/basic/sample.html --auth admin:secret --wait_window_loaded --wait_network_idle --output /reports/basic.pdf

# header example
docker compose -f docker/docker-compose.yml exec app bidi2pdf render --url=http://nginx/header/sample.html --header "X-API-KEY=secret" --wait_window_loaded --wait_network_idle --output /reports/header.pdf

# cookie example
docker compose -f docker/docker-compose.yml exec app bidi2pdf render --url=http://nginx/cookie/sample.html --cookie "auth=secret" --wait_window_loaded --wait_network_idle --output /reports/cookie.pdf

# remote chrome example
docker compose -f docker/docker-compose.yml exec app bidi2pdf render --url=http://nginx/cookie/sample.html --remote_browser_url http://remote-chrome:3000/session --cookie "auth=secret" --wait_window_loaded --wait_network_idle --output /reports/remote.pdf

docker compose -f docker/docker-compose.yml down

🧩 Configuration Options

Flag Description
--url Target URL (required)
--output Output PDF file (default: output.pdf)
--cookie Set cookie in name=value format
--header Inject custom header name=value
--auth Basic auth as user:pass
--headless Run Chrome headless (default: true)
--port ChromeDriver port (0 = auto)
--wait_window_loaded Wait until window.loaded is set to true
--wait_network_idle Wait until network is idle
--log_level Log level: debug, info, warn, error, fatal
--remote_browser_url Connect to remote Chrome session
--default_timeout Operation timeout (default: 60s)

πŸš‚ Rails Integration

Rails integration is available as an additional gem:

# In your Gemfile
gem 'bidi2pdf-rails'

For full documentation and usage examples, visit: https://github.com/dieter-medium/bidi2pdf-rails


πŸ§ͺ Test Helpers

Bidi2pdf provides a suite of RSpec helpers (activated with pdf: true) to simplify PDF-related testing:

SpecPathsHelper

– spec_dir β†’ returns your spec directory
– tmp_dir β†’ returns your tmp directory
– tmp_file(*parts) β†’ builds a tmp file path
– random_tmp_dir(*dirs, prefix:) β†’ builds a random tmp directory

  • fixture_file(*parts) β†’ returns the path to a fixture file

PdfFileHelper

– with_pdf_debug(pdf_data) { |data| … } β†’ on failure, writes PDF to disk
– store_pdf_file(pdf_data, filename_prefix = "test") β†’ saves PDF and returns path

Rspec Matchers

  • have_pdf_page_count β†’ checks if the PDF has a specific number of pages
  • match_pdf_text β†’ checks if the PDF equals a specific text, after stripping whitespace and normalizing characters
  • contains_pdf_text β†’ checks if the PDF contains a specific text, after stripping whitespace and normalizing characters, supporting regex
  • contains_pdf_image β†’ checks if the PDF contains a specific image

ChromedriverContainer

require "bidi2pdf/test_helpers/testcontainers" you can use the chromedriver_container helper to start a ChromeDriver container for your tests. This is useful if you don't want to run ChromeDriver locally or if you want to ensure a clean environment for your tests.

This also provides the helper methods:

  • session_url β†’ returns the session URL for the ChromeDriver container
  • chromedriver_container β†’ returns the Testcontainers container object
  • create_session -> creates a Bidi2pdf::Bidi::Session object for the ChromeDriver container

With the environment variable DISABLE_CHROME_SANDBOX set to true, the container will run Chrome without the sandbox. This is useful for CI environments where the sandbox may cause issues.

Example

require "bidi2pdf/test_helpers"
require "bidi2pdf/test_helpers/images" # <= for image matching, requires lib-vips
require "bidi2pdf/test_helpers/testcontainers" # <= requires testcontainers gem

RSpec.describe "PDF generation", :pdf, :chromedriver do
  it "generates a PDF with the correct content" do
    pdf_data = generate_pdf("https://example.com/invoice/14432423")
    expect(pdf_data).to have_pdf_page_count(1)
    expect(pdf_data).to match_pdf_text("Hello, world!")
    expect(pdf_data).to contain_pdf_image(fixture_file("logo.png"))
  end
end

πŸ›  Development

# Setup
bin/setup

# Run tests
rake spec

# Open interactive console
bin/console

πŸ“œ License

This project is licensed under the MIT License.