bidi2pdf 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/CHANGELOG.md +32 -4
  4. data/README.md +14 -0
  5. data/docker/Dockerfile +1 -1
  6. data/docker/Dockerfile.chromedriver +1 -1
  7. data/docker/Dockerfile.slim +2 -2
  8. data/lib/bidi2pdf/bidi/browser_console_logger.rb +92 -0
  9. data/lib/bidi2pdf/bidi/browser_tab.rb +391 -41
  10. data/lib/bidi2pdf/bidi/client.rb +85 -23
  11. data/lib/bidi2pdf/bidi/command_manager.rb +46 -48
  12. data/lib/bidi2pdf/bidi/commands/base.rb +39 -1
  13. data/lib/bidi2pdf/bidi/commands/browser_remove_user_context.rb +27 -0
  14. data/lib/bidi2pdf/bidi/commands/browsing_context_print.rb +4 -0
  15. data/lib/bidi2pdf/bidi/commands/print_parameters_validator.rb +5 -0
  16. data/lib/bidi2pdf/bidi/commands.rb +1 -0
  17. data/lib/bidi2pdf/bidi/event_manager.rb +1 -1
  18. data/lib/bidi2pdf/bidi/interceptor.rb +1 -1
  19. data/lib/bidi2pdf/bidi/js_logger_helper.rb +16 -0
  20. data/lib/bidi2pdf/bidi/logger_events.rb +25 -45
  21. data/lib/bidi2pdf/bidi/network_event.rb +15 -0
  22. data/lib/bidi2pdf/bidi/network_event_formatters/network_event_console_formatter.rb +4 -3
  23. data/lib/bidi2pdf/bidi/network_events.rb +27 -17
  24. data/lib/bidi2pdf/bidi/session.rb +119 -12
  25. data/lib/bidi2pdf/bidi/user_context.rb +62 -0
  26. data/lib/bidi2pdf/bidi/web_socket_dispatcher.rb +7 -7
  27. data/lib/bidi2pdf/chromedriver_manager.rb +48 -21
  28. data/lib/bidi2pdf/cli.rb +10 -2
  29. data/lib/bidi2pdf/dsl.rb +33 -0
  30. data/lib/bidi2pdf/launcher.rb +30 -0
  31. data/lib/bidi2pdf/notifications/event.rb +52 -0
  32. data/lib/bidi2pdf/notifications/instrumenter.rb +65 -0
  33. data/lib/bidi2pdf/notifications/logging_subscriber.rb +136 -0
  34. data/lib/bidi2pdf/notifications.rb +78 -0
  35. data/lib/bidi2pdf/session_runner.rb +35 -3
  36. data/lib/bidi2pdf/verbose_logger.rb +79 -0
  37. data/lib/bidi2pdf/version.rb +1 -1
  38. data/lib/bidi2pdf.rb +99 -7
  39. data/sig/bidi2pdf/bidi/client.rbs +1 -1
  40. metadata +39 -4
  41. data/lib/bidi2pdf/utils.rb +0 -15
@@ -5,6 +5,36 @@ require_relative "session_runner"
5
5
  require_relative "bidi/session"
6
6
 
7
7
  module Bidi2pdf
8
+ # Represents a launcher for managing browser sessions and executing tasks
9
+ # using the Bidi2pdf library. This class handles the setup and teardown
10
+ # of browser sessions, as well as the execution of tasks within those sessions.
11
+ #
12
+ # @example Launching a session
13
+ # launcher = Bidi2pdf::Launcher.new(
14
+ # url: "http://example.com",
15
+ # inputfile: "input.pdf",
16
+ # output: "output.pdf",
17
+ # cookies: [],
18
+ # headers: {},
19
+ # auth: nil,
20
+ # headless: true
21
+ # )
22
+ # launcher.launch
23
+ # launcher.stop
24
+ #
25
+ # @param [String] url The URL to navigate to in the browser session.
26
+ # @param [String] inputfile The path to the input file to be processed.
27
+ # @param [String] output The path to the output file to be generated.
28
+ # @param [Array<Hash>] cookies An array of cookies to set in the browser session.
29
+ # @param [Hash] headers A hash of HTTP headers to include in the browser session.
30
+ # @param [Hash, nil] auth Authentication credentials (e.g., username and password).
31
+ # @param [Boolean] headless Whether to run the browser in headless mode. Defaults to true.
32
+ # @param [Integer] port The port to use for the browser session. Defaults to 0.
33
+ # @param [Boolean] wait_window_loaded Whether to wait for the window to fully load. Defaults to false.
34
+ # @param [Boolean] wait_network_idle Whether to wait for the network to become idle. Defaults to false.
35
+ # @param [Hash] print_options Options for printing the page. Defaults to an empty hash.
36
+ # @param [String, nil] remote_browser_url The URL of a remote browser to connect to. Defaults to nil.
37
+ # @param [Symbol] network_log_format The format for network logs. Defaults to :console.
8
38
  class Launcher
9
39
  # rubocop:disable Metrics/ParameterLists
10
40
  def initialize(url:, inputfile:, output:, cookies:, headers:, auth:, headless: true, port: 0, wait_window_loaded: false,
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ # rubocop: disable Lint/RescueException
5
+ module Notifications
6
+ class Event
7
+ attr_reader :name, :transaction_id
8
+ attr_accessor :payload
9
+
10
+ def initialize(name, start, ending, transaction_id, payload)
11
+ @name = name
12
+ @payload = payload
13
+ @time = start ? start.to_f * 1_000.0 : start
14
+ @transaction_id = transaction_id
15
+ @end = ending ? ending.to_f * 1_000.0 : ending
16
+ end
17
+
18
+ def record # :nodoc:
19
+ start!
20
+ begin
21
+ yield payload if block_given?
22
+ rescue Exception => e
23
+ payload[:exception] = [e.class.name, e.message]
24
+ payload[:exception_object] = e
25
+ raise e
26
+ ensure
27
+ finish!
28
+ end
29
+ end
30
+
31
+ def start! = @time = now
32
+
33
+ def finish! = @end = now
34
+
35
+ def duration = @end - @time
36
+
37
+ def time
38
+ @time / 1000.0 if @time
39
+ end
40
+
41
+ def end
42
+ @end / 1000.0 if @end
43
+ end
44
+
45
+ private
46
+
47
+ def now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
48
+ end
49
+ end
50
+ end
51
+
52
+ # rubocop: enable Lint/RescueException
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Bidi2pdf
6
+ # This module provides a way to instrument events in the Bidi2pdf library.
7
+ # It it's heavyly inspired by ActiveSupport::Notifications.
8
+ # and thought to be used in a similar way.
9
+ # In Rails environment, ActiveSupport::Notifications should be use instead.
10
+ # via configuration: Bidi2pdf.notification_service = ActiveSupport::Notifications
11
+
12
+ # rubocop: disable Lint/RescueException, Lint/SuppressedException
13
+ module Notifications
14
+ class Instrumenter
15
+ attr_reader :id
16
+
17
+ def initialize
18
+ @id = SecureRandom.uuid
19
+ end
20
+
21
+ def notify(name, payload, &)
22
+ event = create_event(name, payload)
23
+ result = nil
24
+ begin
25
+ result = event.record(&)
26
+ rescue Exception => e
27
+ end
28
+
29
+ subscriber_exceptions = notify_subscribers(name, event)
30
+
31
+ raise Bidi2pdf::NotificationsError.new(subscriber_exceptions), cause: subscriber_exceptions.first if subscriber_exceptions.any?
32
+ raise e if e
33
+
34
+ result
35
+ end
36
+
37
+ private
38
+
39
+ def create_event(name, payload)
40
+ Event.new(name, nil, nil, @id, payload)
41
+ end
42
+
43
+ # rubocop:disable Style/CaseEquality
44
+ def notify_subscribers(name, event)
45
+ exceptions = []
46
+
47
+ Notifications.subscribers.each do |pattern, blocks|
48
+ next unless pattern === name
49
+
50
+ blocks.each do |subscriber|
51
+ subscriber.call(event)
52
+ rescue Exception => e
53
+ exceptions << e
54
+ end
55
+ end
56
+
57
+ exceptions
58
+ end
59
+
60
+ # rubocop:enable Style/CaseEquality
61
+ end
62
+ end
63
+ end
64
+
65
+ # rubocop: enable Lint/RescueException, Lint/SuppressedException
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module Notifications
5
+ # for reuse within ActiveSupport::LogSubscriber
6
+ module LoggingSubscriberActions
7
+ def handle_response(event)
8
+ payload = event.payload
9
+
10
+ if payload[:error]
11
+ logger.error "Received error: #{payload[:error].inspect} for cmd: #{payload[:id] || "-"}"
12
+ elsif !payload[:handled]
13
+ Bidi2pdf.logger.warn "Unknown response: #{payload[:data].inspect}"
14
+ end
15
+ end
16
+
17
+ def send_cmd(event)
18
+ logger.debug "Sending command: #{event.payload[:cmd].method_name} id: ##{event.payload[:cmd_payload][:id]}"
19
+
20
+ logger.debug1 do
21
+ payload = redact_sensitive_fields(event.payload[:cmd_payload])
22
+ "Sending command: #{payload.inspect} (#{event.duration.round(1)}ms)"
23
+ end
24
+ end
25
+
26
+ def send_cmd_and_wait(event)
27
+ return unless event.payload[:exception]
28
+
29
+ payload = redact_sensitive_fields(event.payload[:cmd]&.params || {})
30
+ logger.error "Error sending command: #{payload} (#{event.duration.round(1)}ms) - #{event.payload[:exception].inspect}"
31
+ end
32
+
33
+ def session_close(event)
34
+ return unless event.payload[:error]
35
+
36
+ logger.error "Session close error: #{event.payload[:error].inspect}, attempt: #{event.payload[:attempt]}, retry: #{event.payload[:retry]}"
37
+ end
38
+
39
+ # rubocop: disable Metrics/AbcSize
40
+ def network_event_received(event)
41
+ return unless logger.debug2?
42
+
43
+ msg = case event.payload[:method]
44
+ when "network.beforeRequestSent"
45
+ "Request url '#{event.payload[:url]}' started"
46
+
47
+ when "network.responseStarted"
48
+ nil
49
+ when "network.responseCompleted"
50
+ "Request url '#{event.payload[:url]}' completed"
51
+ when "network.fetchError"
52
+ "Request url '#{event.payload[:url]}' error."
53
+ else
54
+ "Unknown network event: #{event.payload[:method]} for url '#{event.payload[:url]}'"
55
+ end
56
+
57
+ logger.debug2 msg if msg
58
+ end
59
+
60
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
61
+ def network_idle(event)
62
+ return unless logger.info?
63
+
64
+ requests = event.payload[:requests]
65
+ transfered = requests.map { |request| request.bytes_received || 0 }.sum
66
+ status_counts = requests
67
+ .group_by { |evt| evt.http_status_code || 0 }
68
+ .transform_keys { |code| code.zero? || code.nil? ? "pending" : code.to_s }
69
+ .transform_values(&:count)
70
+ .map { |code, count| "#{code}: #{count}" }
71
+ .join(", ")
72
+
73
+ logger.info "Network was idle after #{event.duration.round(1)}ms, #{requests.size} requests, " \
74
+ "transferred #{transfered} bytes (status codes: #{status_counts})"
75
+ end
76
+
77
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
78
+
79
+ def page_loaded(event)
80
+ logger.info "Page loaded: #{event.duration.round(1)}ms"
81
+ end
82
+
83
+ def print(event)
84
+ logger.info "Page printed: #{event.duration.round(1)}ms"
85
+ end
86
+
87
+ private
88
+
89
+ def redact_sensitive_fields(obj, sensitive_keys = %w[value token password authorization username])
90
+ case obj
91
+ when Hash
92
+ obj.transform_values.with_index do |v, idx|
93
+ k = obj.keys[idx]
94
+ sensitive_keys.include?(k.to_s.downcase) ? "[REDACTED]" : redact_sensitive_fields(v, sensitive_keys)
95
+ end
96
+ when Array
97
+ obj.map { |item| redact_sensitive_fields(item, sensitive_keys) }
98
+ else
99
+ obj
100
+ end
101
+ end
102
+ end
103
+
104
+ class LoggingSubscriber
105
+ include LoggingSubscriberActions
106
+
107
+ attr_accessor :logger
108
+
109
+ # rubocop: disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
110
+ def initialize(logger: Logger.new($stdout))
111
+ @logger = logger
112
+ Bidi2pdf.notification_service&.subscribe("handle_response.bidi2pdf", &method(:handle_response))
113
+ Bidi2pdf.notification_service&.subscribe("send_cmd.bidi2pdf", &method(:send_cmd))
114
+ Bidi2pdf.notification_service&.subscribe("send_cmd_and_wait.bidi2pdf", &method(:send_cmd_and_wait))
115
+ Bidi2pdf.notification_service&.subscribe("session_close.bidi2pdf", &method(:session_close))
116
+ Bidi2pdf.notification_service&.subscribe("network_idle.bidi2pdf", &method(:network_idle))
117
+ Bidi2pdf.notification_service&.subscribe("page_loaded.bidi2pdf", &method(:page_loaded))
118
+ Bidi2pdf.notification_service&.subscribe("network_event_received.bidi2pdf", &method(:network_event_received))
119
+ Bidi2pdf.notification_service&.subscribe("print.bidi2pdf", &method(:network_event_received))
120
+ end
121
+
122
+ def unsubscribe
123
+ Bidi2pdf.notification_service&.unsubscribe("handle_response.bidi2pdf", &method(:handle_response))
124
+ Bidi2pdf.notification_service&.unsubscribe("send_cmd.bidi2pdf", &method(:send_cmd))
125
+ Bidi2pdf.notification_service&.unsubscribe("send_cmd_and_wait.bidi2pdf", &method(:send_cmd_and_wait))
126
+ Bidi2pdf.notification_service&.unsubscribe("session_close.bidi2pdf", &method(:session_close))
127
+ Bidi2pdf.notification_service&.unsubscribe("network_idle.bidi2pdf", &method(:network_idle))
128
+ Bidi2pdf.notification_service&.unsubscribe("page_loaded.bidi2pdf", &method(:page_loaded))
129
+ Bidi2pdf.notification_service&.unsubscribe("network_event_received.bidi2pdf", &method(:network_event_received))
130
+ Bidi2pdf.notification_service&.unsubscribe("print.bidi2pdf", &method(:network_event_received))
131
+ end
132
+
133
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "chromedriver_manager"
4
+ require_relative "session_runner"
5
+ require_relative "bidi/session"
6
+ require_relative "notifications/event"
7
+ require_relative "notifications/instrumenter"
8
+
9
+ require "securerandom"
10
+
11
+ module Bidi2pdf
12
+ # This module provides a way to instrument events in the Bidi2pdf library.
13
+ # It it's heavyly inspired by ActiveSupport::Notifications.
14
+ # and thought to be used in a similar way.
15
+ # In Rails environment, ActiveSupport::Notifications should be used instead.
16
+ # via configuration: config.notification_service = ActiveSupport::Notifications
17
+
18
+ module Notifications
19
+ Thread.attr_accessor :bidi2pdf_notification_instrumenter
20
+
21
+ @subscribers = Hash.new { |h, k| h[k] = [] }
22
+
23
+ class << self
24
+ attr_reader :subscribers
25
+
26
+ def instrument(name, payload = {})
27
+ payload = payload.dup
28
+
29
+ if listening?(name)
30
+ notify(name, payload) { yield payload if block_given? }
31
+ elsif block_given?
32
+ yield payload
33
+ end
34
+ end
35
+
36
+ def subscribe(event_pattern, &block)
37
+ pattern = normalize_pattern(event_pattern)
38
+
39
+ @subscribers[pattern] << block
40
+
41
+ block
42
+ end
43
+
44
+ def unsubscribe(event_pattern, block = nil)
45
+ pattern = normalize_pattern(event_pattern)
46
+
47
+ if block
48
+ @subscribers[pattern].delete(block)
49
+ else
50
+ @subscribers[pattern].clear
51
+ end
52
+ end
53
+
54
+ # rubocop: disable Style/CaseEquality
55
+ def listening?(name)
56
+ @subscribers.any? do |pattern, blocks|
57
+ pattern === name && blocks.any?
58
+ end
59
+ end
60
+
61
+ # rubocop: enable Style/CaseEquality
62
+
63
+ private
64
+
65
+ def bidi2pdf_notification_instrumenter = Thread.current.bidi2pdf_notification_instrumenter ||= Instrumenter.new
66
+
67
+ def notify(name, payload, &) = bidi2pdf_notification_instrumenter.notify(name, payload, &)
68
+
69
+ def normalize_pattern(pat)
70
+ case pat
71
+ when String, Regexp then pat
72
+ else
73
+ raise ArgumentError, "Pattern must be String or Regexp"
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -1,6 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bidi2pdf
4
+ # Represents a runner for managing browser sessions and executing tasks
5
+ # using the Bidi2pdf library. This class handles the setup, configuration,
6
+ # and execution of browser-related workflows, including navigation, cookie
7
+ # management, and printing.
8
+ #
9
+ # @example Running a session
10
+ # session_runner = Bidi2pdf::SessionRunner.new(
11
+ # session: session,
12
+ # url: "http://example.com",
13
+ # inputfile: "input.html",
14
+ # output: "output.pdf",
15
+ # cookies: { "key" => "value" },
16
+ # headers: { "Authorization" => "Bearer token" },
17
+ # auth: { username: "user", password: "pass" },
18
+ # wait_window_loaded: true,
19
+ # wait_network_idle: true,
20
+ # print_options: { landscape: true },
21
+ # network_log_format: :json
22
+ # )
23
+ # session_runner.run
24
+ #
25
+ # @param [Object] session The browser session object to use.
26
+ # @param [String, nil] url The URL to navigate to in the browser session.
27
+ # @param [String, nil] inputfile The path to the input file to be processed if no URL is provided.
28
+ # @param [String, nil] output The path to the output file to be generated.
29
+ # @param [Hash] cookies A hash of cookies to set in the browser session. Defaults to an empty hash.
30
+ # @param [Hash] headers A hash of HTTP headers to include in the browser session. Defaults to an empty hash.
31
+ # @param [Hash, nil] auth Authentication credentials (e.g., username and password). Defaults to an empty hash.
32
+ # @param [Boolean] wait_window_loaded Whether to wait for the window to fully load. Defaults to false.
33
+ # @param [Boolean] wait_network_idle Whether to wait for the network to become idle. Defaults to false.
34
+ # @param [Hash] print_options Options for printing the page. Defaults to an empty hash.
35
+ # @param [Symbol] network_log_format The format for network logs. Defaults to :console.
4
36
  class SessionRunner
5
37
  # rubocop: disable Metrics/ParameterLists
6
38
  def initialize(session:, url:, inputfile:, output:, cookies: {}, headers: {}, auth: {}, wait_window_loaded: false,
@@ -39,6 +71,7 @@ module Bidi2pdf
39
71
 
40
72
  @window = window
41
73
  @tab = tab
74
+ @user_context = user_context
42
75
 
43
76
  add_cookies(tab)
44
77
 
@@ -99,15 +132,14 @@ module Bidi2pdf
99
132
 
100
133
  if @wait_window_loaded
101
134
  Bidi2pdf.logger.info "Waiting for window to be loaded"
102
- @tab.execute_script <<-EOF_SCRIPT
103
- new Promise(resolve => { const check = () => window.loaded ? resolve('done') : setTimeout(check, 100); check(); });
104
- EOF_SCRIPT
135
+ @tab.wait_until_page_loaded
105
136
  end
106
137
 
107
138
  @tab.print(@output, print_options: @print_options)
108
139
  ensure
109
140
  @tab.close
110
141
  @window.close
142
+ @user_context.close
111
143
  end
112
144
 
113
145
  # rubocop: enable Metrics/AbcSize
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ class VerboseLogger < SimpleDelegator
5
+ VERBOSITY_LEVELS = {
6
+ none: 0,
7
+ low: 1,
8
+ medium: 2,
9
+ high: 3
10
+ }.freeze
11
+
12
+ attr_reader :logger, :verbosity
13
+
14
+ def initialize(logger, verbosity = :low)
15
+ super(logger)
16
+ self.verbosity = verbosity
17
+ @logger = logger
18
+ end
19
+
20
+ def verbosity=(verbosity)
21
+ min_verbosity = VERBOSITY_LEVELS.values.min
22
+
23
+ @verbosity = if verbosity.is_a?(Numeric)
24
+ verbosity = verbosity.to_i
25
+ max_verbosity = VERBOSITY_LEVELS.values.max
26
+
27
+ verbosity.clamp(min_verbosity, max_verbosity)
28
+ else
29
+ VERBOSITY_LEVELS.fetch verbosity.to_sym, min_verbosity
30
+ end
31
+ end
32
+
33
+ def verbosity_sym
34
+ VERBOSITY_LEVELS.find { |_, v| v == verbosity }.first
35
+ end
36
+
37
+ def debug1(progname = nil, &)
38
+ return unless debug1?
39
+
40
+ logger.debug("[D1] #{progname}", &)
41
+ end
42
+
43
+ def debug1?
44
+ verbosity >= 1
45
+ end
46
+
47
+ def debug1!
48
+ @verbosity = VERBOSITY_LEVELS[:high]
49
+ end
50
+
51
+ def debug2(progname = nil, &)
52
+ return unless debug2?
53
+
54
+ logger.debug("[D2] #{progname}", &)
55
+ end
56
+
57
+ def debug2?
58
+ verbosity >= 2
59
+ end
60
+
61
+ def debug2!
62
+ @verbosity = VERBOSITY_LEVELS[:high]
63
+ end
64
+
65
+ def debug3(progname = nil, &)
66
+ return unless debug3?
67
+
68
+ logger.debug("[D3] #{progname}", &)
69
+ end
70
+
71
+ def debug3?
72
+ verbosity >= 3
73
+ end
74
+
75
+ def debug3!
76
+ @verbosity = VERBOSITY_LEVELS[:high]
77
+ end
78
+ end
79
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bidi2pdf
4
- VERSION = "0.1.6"
4
+ VERSION = "0.1.7"
5
5
  end
data/lib/bidi2pdf.rb CHANGED
@@ -1,14 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "bidi2pdf/utils"
4
3
  require_relative "bidi2pdf/process_tree"
5
4
  require_relative "bidi2pdf/launcher"
6
5
  require_relative "bidi2pdf/bidi/session"
7
6
  require_relative "bidi2pdf/dsl"
7
+ require_relative "bidi2pdf/notifications"
8
+ require_relative "bidi2pdf/notifications/logging_subscriber"
9
+ require_relative "bidi2pdf/verbose_logger"
8
10
 
9
11
  require "logger"
10
12
 
11
13
  module Bidi2pdf
14
+ PAPER_FORMATS_CM = {
15
+ letter: { width: 21.59, height: 27.94 },
16
+ legal: { width: 21.59, height: 35.56 },
17
+ tabloid: { width: 27.94, height: 43.18 },
18
+ ledger: { width: 43.18, height: 27.94 },
19
+ a0: { width: 84.1, height: 118.9 },
20
+ a1: { width: 59.4, height: 84.1 },
21
+ a2: { width: 42.0, height: 59.4 },
22
+ a3: { width: 29.7, height: 42.0 },
23
+ a4: { width: 21.0, height: 29.7 },
24
+ a5: { width: 14.8, height: 21.0 },
25
+ a6: { width: 10.5, height: 14.8 }
26
+ }.freeze
27
+
12
28
  class Error < StandardError; end
13
29
 
14
30
  class SessionNotStartedError < Error; end
@@ -25,19 +41,95 @@ module Bidi2pdf
25
41
 
26
42
  class PrintError < Error; end
27
43
 
28
- @logger = Logger.new($stdout)
29
- @network_events_logger = Logger.new($stdout)
30
- @logger.level = Logger::INFO
31
- @network_events_logger.level = Logger::FATAL
44
+ class ScriptInjectionError < Error; end
45
+
46
+ class StyleInjectionError < Error; end
47
+
48
+ class NotificationsError < Error
49
+ attr_reader :causes
32
50
 
33
- @default_timeout = 60
51
+ def initialize(causes)
52
+ @causes = causes
53
+ exception_class_names = causes.map { |e| e.class.name }
54
+ super("Notifications errors: #{exception_class_names.join(", ")}")
55
+ end
56
+ end
34
57
 
35
58
  class << self
36
- attr_accessor :logger, :default_timeout, :network_events_logger
59
+ attr_accessor :default_timeout, :enable_default_logging_subscriber
60
+ attr_reader :logging_subscriber, :logger, :network_events_logger, :browser_console_logger, :notification_service
37
61
 
38
62
  # Allow configuration through a block
39
63
  def configure
40
64
  yield self if block_given?
65
+
66
+ init
67
+ end
68
+
69
+ def init
70
+ self.logging_subscriber = (Notifications::LoggingSubscriber.new(logger: logger) if enable_default_logging_subscriber)
71
+ begin
72
+ require "websocket-native"
73
+
74
+ logger.debug "websocket-native available; use enhance performance."
75
+ rescue LoadError => e
76
+ raise unless e.message =~ /websocket-native/
77
+
78
+ logger.warn "websocket-native not available; installing it may enhance performance."
79
+ end
80
+ end
81
+
82
+ def translate_paper_format(format)
83
+ format = format.to_s.downcase.to_sym
84
+
85
+ dim = PAPER_FORMATS_CM[format]
86
+
87
+ raise ArgumentError, "Invalid paper format: #{format}" unless dim
88
+
89
+ width = dim[:width] || 0
90
+ height = dim[:height] || 0
91
+
92
+ { width: width, height: height }
93
+ end
94
+
95
+ def logger=(new_logger)
96
+ @logger = Bidi2pdf::VerboseLogger.new new_logger
97
+ end
98
+
99
+ def network_events_logger=(new_network_events_logger)
100
+ @network_events_logger = Bidi2pdf::VerboseLogger.new(new_network_events_logger)
101
+ end
102
+
103
+ def browser_console_logger=(new_browser_console_logger)
104
+ @browser_console_logger = Bidi2pdf::VerboseLogger.new(new_browser_console_logger)
41
105
  end
106
+
107
+ def logging_subscriber=(new_logging_subscriber)
108
+ @logging_subscriber&.unsubscribe
109
+ @logging_subscriber = new_logging_subscriber
110
+ end
111
+
112
+ def notification_service=(new_notification_service)
113
+ @logging_subscriber&.unsubscribe
114
+
115
+ @notification_service = new_notification_service
116
+ end
117
+ end
118
+
119
+ configure do |config|
120
+ config.logger = Logger.new($stdout)
121
+ config.logger.level = Logger::INFO
122
+
123
+ config.network_events_logger = Logger.new($stdout)
124
+ config.network_events_logger.level = Logger::FATAL
125
+
126
+ config.browser_console_logger = Logger.new($stdout)
127
+ config.browser_console_logger.level = Logger::WARN
128
+
129
+ config.enable_default_logging_subscriber = true
130
+
131
+ config.default_timeout = 60
132
+
133
+ config.notification_service = Notifications
42
134
  end
43
135
  end
@@ -34,7 +34,7 @@ module Bidi2pdf
34
34
 
35
35
  def wait_until_open: (?timeout: untyped) -> untyped
36
36
 
37
- def send_cmd: (untyped method, ?::Hash[untyped, untyped] params) -> untyped
37
+ def send_cmd: (Bidi2pdf::Bidi::Commands::Base cmd) -> untyped
38
38
 
39
39
  # rubocop:disable Metrics/AbcSize
40
40
  def send_cmd_and_wait: (untyped method, ?::Hash[untyped, untyped] params, ?timeout: untyped) ?{ (untyped) -> untyped } -> untyped