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.



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

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



123
124
125
126
127
128
# File 'lib/palapala/renderer.rb', line 123

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



130
131
132
# File 'lib/palapala/renderer.rb', line 130

def self.ping
  thread_local_instance.ping
end

.resetObject

Reset the thread-local instance of the renderer



40
41
42
43
# File 'lib/palapala/renderer.rb', line 40

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



35
36
37
# File 'lib/palapala/renderer.rb', line 35

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



140
141
142
143
144
145
146
147
148
149
150
# File 'lib/palapala/renderer.rb', line 140

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



134
135
136
137
# File 'lib/palapala/renderer.rb', line 134

def close
  @driver.close
  @client.close
end

#current_idObject

Get the current ID



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

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



103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/palapala/renderer.rb', line 103

def html_to_pdf(html, params: {})
  server = start_local_server(html)
  begin
    file = File.basename(server[:file].path)
    url = "http://localhost:#{server[:port]}/#{URI.encode_www_form_component(file)}"
    send_command_and_wait_for_event("Page.navigate", params: { url: url },
                                                         event_name: "Page.frameStoppedLoading")
    result = send_command_and_wait_for_result("Page.printToPDF", params:)
    Base64.decode64(result["data"])
  ensure
    server[:thread].kill # Stop the server after use
    server[:file].unlink # Delete the temporary file
  end
end

#next_idObject

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



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

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

#on_message(e) ⇒ Object

Callback to handle the incomming WebSocket messages



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

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



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

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



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

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



70
71
72
73
74
# File 'lib/palapala/renderer.rb', line 70

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



77
78
79
# File 'lib/palapala/renderer.rb', line 77

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



91
92
93
94
95
96
97
# File 'lib/palapala/renderer.rb', line 91

def send_command_and_wait_for_event(method, event_name:, params: {})
  send_command(method, params:) do
    # chrome refuses to load pages that are bigger than 2MB and returns a net::ERR_ABORTED error
    raise "Page cannot be loaded" if @response.dig("result", "errorText") == "net::ERR_ABORTED"
    @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



83
84
85
86
87
88
# File 'lib/palapala/renderer.rb', line 83

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



27
28
29
30
31
32
# File 'lib/palapala/renderer.rb', line 27

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