Class: Palapala::Renderer

Inherits:
Object
  • Object
show all
Defined in:
lib/palapala/renderer.rb

Overview

Render HTML content to PDF using Chrome in headless mode with minimal dependencies

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeRenderer

Returns a new instance of Renderer.



10
11
12
13
14
15
16
17
18
19
20
21
22
23
# File 'lib/palapala/renderer.rb', line 10

def initialize
  puts "Initializing a renderer" if Palapala.debug
  # Create an instance of WebSocketClient with the WebSocket URL
  @client = Palapala::WebSocketClient.new(websocket_url)
  # Create the WebSocket driver
  @driver = WebSocket::Driver.client(@client)
  # Register the on_message callback
  @driver.on(:message, &method(:on_message))
  @driver.on(:close) { Thread.current[:renderer] = nil } # Reset the renderer on close
  # Start the WebSocket handshake
  @driver.start
  # Initialize the protocol to get the page events
  send_command_and_wait_for_result("Page.enable")
end

Class Method Details

.html_to_pdf(html, params: {}) ⇒ Object



111
112
113
114
115
116
# File 'lib/palapala/renderer.rb', line 111

def self.html_to_pdf(html, params: {})
  thread_local_instance.html_to_pdf(html, params: params)
rescue StandardError
  reset # Reset the renderer on error, the websocket connection might be broken
  thread_local_instance.html_to_pdf(html, params: params) # Retry (once)
end

.pingObject



118
119
120
# File 'lib/palapala/renderer.rb', line 118

def self.ping
  thread_local_instance.ping
end

.resetObject

Reset the thread-local instance of the renderer



38
39
40
41
# File 'lib/palapala/renderer.rb', line 38

def self.reset
  puts "Clearing the thread local renderer" if Palapala.debug
  Thread.current[:renderer] = nil
end

.thread_local_instanceObject

Create a thread-local instance of the renderer



33
34
35
# File 'lib/palapala/renderer.rb', line 33

def self.thread_local_instance
  Thread.current[:renderer] ||= Renderer.new
end

.websocket_urlObject

Open a new tab in the remote chrome and return the WebSocket URL



128
129
130
131
132
133
134
135
136
137
138
# File 'lib/palapala/renderer.rb', line 128

def self.websocket_url
  uri = URI("#{Palapala.headless_chrome_url}/json/new")
  http = Net::HTTP.new(uri.host, uri.port)
  request = Net::HTTP::Put.new(uri)
  request["Content-Type"] = "application/json"
  response = http.request(request)
  tab_info = JSON.parse(response.body)
  websocket_url = tab_info["webSocketDebuggerUrl"]
  puts "WebSocket URL: #{websocket_url}" if Palapala.debug
  websocket_url
end

Instance Method Details

#closeObject



122
123
124
125
# File 'lib/palapala/renderer.rb', line 122

def close
  @driver.close
  @client.close
end

#current_idObject

Get the current ID



56
# File 'lib/palapala/renderer.rb', line 56

def current_id = @id

#html_to_pdf(html, params: {}) ⇒ Object

Parameters:

  • html (String)

    The HTML content to convert to PDF

  • params (Hash) (defaults to: {})

    Additional parameters to pass to the CDP command



99
100
101
102
103
104
# File 'lib/palapala/renderer.rb', line 99

def html_to_pdf(html, params: {})
  send_command_and_wait_for_event("Page.navigate", params: { url: data_url_for_html(html) },
                                                   event_name: "Page.frameStoppedLoading")
  result = send_command_and_wait_for_result("Page.printToPDF", params:)
  Base64.decode64(result["data"])
end

#next_idObject

Update the current ID to the next ID (increment by 1)



53
# File 'lib/palapala/renderer.rb', line 53

def next_id = @id = (@id || 0) + 1

#on_message(e) ⇒ Object

Callback to handle the incomming WebSocket messages



44
45
46
47
48
49
50
# File 'lib/palapala/renderer.rb', line 44

def on_message(e)
  puts "Received: #{e.data[0..64]}" if Palapala.debug
  @response = JSON.parse(e.data) # Parse the JSON response
  if @response["error"] # Raise an error if the response contains an error
    raise "#{@response["error"]["message"]}: #{@response["error"]["data"]} (#{@response["error"]["code"]})"
  end
end

#pingObject



106
107
108
109
# File 'lib/palapala/renderer.rb', line 106

def ping
  result = send_command_and_wait_for_result("Runtime.evaluate", params: { expression: "1 + 1" })
  raise "Ping failed" unless result["result"]["value"] == 2
end

#process_until(&block) ⇒ Object

Process the WebSocket messages until some state is true



59
60
61
62
63
64
65
# File 'lib/palapala/renderer.rb', line 59

def process_until(&block)
  loop do
    @driver.parse(@client.read)
    return if block.call
    return if @driver.state == :closed
  end
end

#send_and_wait(message) ⇒ Object

Method to send a message (text) and wait for a response



68
69
70
71
72
# File 'lib/palapala/renderer.rb', line 68

def send_and_wait(message, &)
  puts "\nSending: #{message}" if Palapala.debug
  @driver.text(message)
  process_until(&)
end

#send_command(method, params: {}, &block) ⇒ Object

Method to send a CDP command and wait for some state to be true



75
76
77
# File 'lib/palapala/renderer.rb', line 75

def send_command(method, params: {}, &block)
  send_and_wait(JSON.generate({ id: next_id, method:, params: }), &block)
end

#send_command_and_wait_for_event(method, event_name:, params: {}) ⇒ Object

Method to send a CDP command and wait for a specific method to be called



89
90
91
92
93
# File 'lib/palapala/renderer.rb', line 89

def send_command_and_wait_for_event(method, event_name:, params: {})
  send_command(method, params:) do
    @response && @response["method"] == event_name
  end
end

#send_command_and_wait_for_result(method, params: {}) ⇒ Hash

Method to send a CDP command and wait for the matching event to get the result

Returns:

  • (Hash)

    The result of the command



81
82
83
84
85
86
# File 'lib/palapala/renderer.rb', line 81

def send_command_and_wait_for_result(method, params: {})
  send_command(method, params:) do
    @response && @response["id"] == current_id
  end
  @response["result"]
end

#websocket_urlObject



25
26
27
28
29
30
# File 'lib/palapala/renderer.rb', line 25

def websocket_url
  self.class.websocket_url
rescue Errno::ECONNREFUSED
  ChromeProcess.spawn_chrome # Spawn a new Chrome process
  self.class.websocket_url # Retry (once)
end