bidi2pdf-rails 0.0.1.pre.alpha → 0.1.0
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.
- checksums.yaml +4 -4
- data/.idea/bidi2pdf-rails.iml +55 -9
- data/.rubocop.yml +14 -0
- data/CHANGELOG.md +33 -0
- data/README.md +117 -27
- data/Rakefile +2 -0
- data/cliff.toml +126 -0
- data/lib/bidi2pdf_rails/browser_console_log_subscriber.rb +24 -0
- data/lib/bidi2pdf_rails/chromedriver_manager_singleton.rb +11 -11
- data/lib/bidi2pdf_rails/config.rb +133 -0
- data/lib/bidi2pdf_rails/configurable.rb +106 -0
- data/lib/bidi2pdf_rails/main_log_subscriber.rb +33 -0
- data/lib/bidi2pdf_rails/network_log_subscriber.rb +20 -0
- data/lib/bidi2pdf_rails/railtie.rb +12 -45
- data/lib/bidi2pdf_rails/services/html_renderer.rb +33 -0
- data/lib/bidi2pdf_rails/services/html_to_pdf_converter.rb +28 -0
- data/lib/bidi2pdf_rails/services/pdf_browser_session.rb +39 -0
- data/lib/bidi2pdf_rails/services/pdf_renderer.rb +82 -0
- data/lib/bidi2pdf_rails/services/url_to_pdf_converter.rb +82 -0
- data/lib/bidi2pdf_rails/version.rb +1 -1
- data/lib/bidi2pdf_rails.rb +41 -58
- data/lib/generators/bidi2pdf_rails/USAGE +12 -4
- data/lib/generators/bidi2pdf_rails/initializer_generator.rb +136 -30
- data/lib/generators/bidi2pdf_rails/templates/bidi2pdf_rails.rb.tt +25 -79
- data/spec/acceptance/user_can_download_report_pdf_spec.rb +133 -0
- data/spec/acceptance/user_can_generate_pdf_from_protected_remote_url_spec.rb +173 -0
- data/spec/dummy/app/controllers/reports_controller.rb +37 -0
- data/spec/dummy/app/controllers/secure_controller.rb +52 -0
- data/spec/dummy/app/views/layouts/simple.html.erb +17 -0
- data/spec/dummy/app/views/secure/show.html.erb +10 -0
- data/spec/dummy/config/environments/production.rb +1 -1
- data/spec/dummy/config/initializers/bidi2pdf_rails.rb +68 -54
- data/spec/dummy/config/initializers/cors.rb +1 -1
- data/spec/dummy/config/routes.rb +10 -0
- data/spec/dummy/log/development.log +16567 -156
- data/spec/dummy/log/test.log +53046 -0
- data/spec/dummy/tmp/pids/server.pid +1 -1
- data/spec/integration/generators/bidi2pdf_rails/initializer_generator_spec.rb +64 -0
- data/spec/rails_helper.rb +8 -1
- data/spec/spec_helper.rb +47 -5
- data/spec/support/default_dirs_helper.rb +32 -0
- data/spec/support/pdf_helper.rb +12 -0
- data/spec/support/render_setting_helpers.rb +28 -0
- data/spec/support/request_server_bootstrap.rb +44 -0
- data/spec/{bidi2pdf_rails → unit/bidi2pdf_rails}/bidi2pdf_rails_spec.rb +1 -1
- data/spec/unit/bidi2pdf_rails/configurable/base_nested_config_spec.rb +133 -0
- data/tasks/changelog.rake +29 -0
- data/tasks/coverage.rake +23 -0
- metadata +95 -25
- data/lib/bidi2pdf_rails/log_subscriber.rb +0 -13
- data/spec/dummy/spec/helpers/reports_helper_spec.rb +0 -15
- data/spec/dummy/spec/requests/reports_spec.rb +0 -10
- data/spec/dummy/spec/views/reports/show.html.erb_spec.rb +0 -5
- data/spec/generator/bidie2pdf_rails_initializer_generator_spec.rb +0 -5
- data/spec/generator/initializer_generator_spec.rb +0 -5
- data/spec/requests/reports_spec.rb +0 -17
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "configurable"
|
4
|
+
|
5
|
+
module Bidi2pdfRails
|
6
|
+
class Config
|
7
|
+
include Bidi2pdfRails::Configurable
|
8
|
+
|
9
|
+
CONFIG_OPTIONS = {
|
10
|
+
general_options: {
|
11
|
+
name: "General Options",
|
12
|
+
ask: "Configure general options? (y/n)",
|
13
|
+
options: [
|
14
|
+
{ name: :logger, desc: "The logger", default: -> { Rails.logger || ActiveSupport::TaggedLogging.new(Logger.new(nil)) }, default_as_str: "Rails.logger", ask: false, color: :yellow },
|
15
|
+
{ name: :verbosity, desc: "How verbose to be", default: Bidi2pdf::VerboseLogger::VERBOSITY_LEVELS.keys.sort_by { |k| Bidi2pdf::VerboseLogger::VERBOSITY_LEVELS[k] }.first.to_s, limited_to: Bidi2pdf::VerboseLogger::VERBOSITY_LEVELS.keys.sort_by { |k| Bidi2pdf::VerboseLogger::VERBOSITY_LEVELS[k] }.map(&:to_s), ask: true, color: :yellow },
|
16
|
+
{ name: :headless, desc: "Run Chrome in headless mode", default: -> { !Rails.env.development? }, default_as_str: "!Rails.env.development?", ask: true, color: :green },
|
17
|
+
{ name: :wait_for_network_idle, desc: "Wait for network idle", default: true, ask: true, color: :green },
|
18
|
+
{ name: :wait_for_page_loaded, desc: "Wait for page loaded", default: false, ask: true, color: :green },
|
19
|
+
{ name: :wait_for_page_check_script, desc: "Wait for page check script", default: nil, ask: false },
|
20
|
+
{ name: :notification_service, desc: "Notification service", default: -> { ActiveSupport::Notifications }, default_as_str: "-> { ActiveSupport::Notifications }", ask: false },
|
21
|
+
{ name: :default_timeout, desc: "Default timeout for various Bidi commands", default: 10, ask: true, color: :yellow },
|
22
|
+
{ name: :chrome_session_args, desc: "Chrome session arguments", default: Bidi2pdf::Bidi::Session::DEFAULT_CHROME_ARGS, ask: false }
|
23
|
+
]
|
24
|
+
},
|
25
|
+
|
26
|
+
chromedriver_settings: {
|
27
|
+
name: "Chromedriver Settings (when chromedriver run within your app)",
|
28
|
+
ask: "Configure chromedriver settings? (y/n)",
|
29
|
+
options: [
|
30
|
+
{ name: :install_dir, desc: "Chromedriver install directory", default: nil, ask: false, color: :yellow },
|
31
|
+
{ name: :port, desc: "Chromedriver port", default: 0, ask: true, color: :yellow }
|
32
|
+
]
|
33
|
+
},
|
34
|
+
|
35
|
+
proxy_settings: {
|
36
|
+
name: "Proxy Settings",
|
37
|
+
ask: "Use a proxy server? (y/n)",
|
38
|
+
options: [
|
39
|
+
{ name: :addr, desc: "Proxy address (e.g., 127.0.0.1)", default: nil, ask: true, color: :yellow },
|
40
|
+
{ name: :port, desc: "Proxy port (e.g., 8080)", default: nil, ask: true, color: :yellow },
|
41
|
+
{ name: :user, desc: "Proxy user", default: nil, ask: true, color: :yellow },
|
42
|
+
{
|
43
|
+
name: :pass,
|
44
|
+
desc: "Proxy password",
|
45
|
+
default: -> { -> { Rails.application.credentials.dig("bidi2pdf_rails", "proxy_pass") } },
|
46
|
+
default_as_str: "-> { Rails.application.credentials.dig('bidi2pdf_rails', 'proxy_pass') }",
|
47
|
+
secret: true,
|
48
|
+
ask: true,
|
49
|
+
color: :yellow
|
50
|
+
}
|
51
|
+
]
|
52
|
+
},
|
53
|
+
|
54
|
+
pdf_settings: {
|
55
|
+
name: "PDF Settings",
|
56
|
+
ask: "Configure custom PDF settings? (y/n)",
|
57
|
+
options: [
|
58
|
+
{ name: :orientation, desc: "PDF orientation (portrait/landscape)", default: "portrait", limited_to: %w[portrait landscape], ask: true },
|
59
|
+
{ name: :margins, desc: "Configure PDF margins?", default: false, ask: true, color: :green },
|
60
|
+
{ name: :margin_top, desc: "PDF margin top (cm)", default: 2.5, ask: true, color: :yellow },
|
61
|
+
{ name: :margin_bottom, desc: "PDF margin bottom (cm)", default: 2, ask: true, color: :yellow },
|
62
|
+
{ name: :margin_left, desc: "PDF margin left (cm)", default: 2, ask: true, color: :yellow },
|
63
|
+
{ name: :margin_right, desc: "PDF margin right (cm)", default: 2, ask: true, color: :yellow },
|
64
|
+
{ name: :page_format, desc: "PDF page format (e.g., A4)", default: nil, limited_to: Bidi2pdf::PAPER_FORMATS_CM.keys.map(&:to_s), ask: true },
|
65
|
+
{ name: :page_width, desc: "PDF page width (cm, not needed when format is specified)", default: Bidi2pdf::PAPER_FORMATS_CM[:a4][:width], ask: true, color: :yellow },
|
66
|
+
{ name: :page_height, desc: "PDF page height (cm, not needed when format is specified)", default: Bidi2pdf::PAPER_FORMATS_CM[:a4][:height], ask: true, color: :yellow },
|
67
|
+
{ name: :print_background, desc: "Print background graphics?", default: true, ask: true, color: :green },
|
68
|
+
{ name: :scale, desc: "PDF scale (e.g., 1.0)", default: 1.0, ask: true, color: :yellow },
|
69
|
+
{ name: :shrink_to_fit, desc: "Shrink to fit?", default: false, ask: true, color: :green }
|
70
|
+
]
|
71
|
+
},
|
72
|
+
|
73
|
+
render_remote_settings: {
|
74
|
+
name: "Remote URL Settings",
|
75
|
+
ask: false,
|
76
|
+
options: [
|
77
|
+
{ name: :browser_url, desc: "Remote browser URL (e.g. http://localhost:3001/sesion)", default: nil, ask: true, color: :yellow },
|
78
|
+
{ name: :basic_auth_user, desc: "Basic auth user", default: nil },
|
79
|
+
{ name: :basic_auth_pass, desc: "Basic auth password", default: nil, default_as_str: "-> { Rails.application.credentials.dig('bidi2pdf_rails', 'basic_auth_pass') }", secret: true },
|
80
|
+
{ name: :headers, desc: "Headers to be send when allong an url", default: {}, default_as_str: '{"X-API-INFO" => "my info"}' },
|
81
|
+
{ name: :cookies, desc: "Cookies to be send when alling an url", default: {}, default_as_str: '{"session_id" => "my session"}' }
|
82
|
+
]
|
83
|
+
}
|
84
|
+
|
85
|
+
}.freeze
|
86
|
+
|
87
|
+
configure_with(CONFIG_OPTIONS)
|
88
|
+
|
89
|
+
def initialize
|
90
|
+
reset_to_defaults!
|
91
|
+
end
|
92
|
+
|
93
|
+
def pdf_settings_for_bidi_cmd(overrides = {})
|
94
|
+
overrides ||= {}
|
95
|
+
opts = {}
|
96
|
+
|
97
|
+
opts[:background] = pdf_settings.print_background_value
|
98
|
+
opts[:orientation] = pdf_settings.orientation_value
|
99
|
+
opts[:scale] = pdf_settings.scale_value
|
100
|
+
opts[:shrinkToFit] = pdf_settings.shrink_to_fit_value
|
101
|
+
|
102
|
+
# Margins
|
103
|
+
margins = {
|
104
|
+
top: pdf_settings.margin_top_value,
|
105
|
+
bottom: pdf_settings.margin_bottom_value,
|
106
|
+
left: pdf_settings.margin_left_value,
|
107
|
+
right: pdf_settings.margin_right_value
|
108
|
+
}.compact
|
109
|
+
|
110
|
+
opts[:margin] = margins unless margins.empty?
|
111
|
+
|
112
|
+
# Page size
|
113
|
+
page = {
|
114
|
+
width: pdf_settings.page_width_value,
|
115
|
+
height: pdf_settings.page_height_value,
|
116
|
+
format: pdf_settings.page_format_value
|
117
|
+
}.compact
|
118
|
+
|
119
|
+
page = overrides[:page].compact if overrides[:page]&.compact.present?
|
120
|
+
page = { format: page[:format] } if page[:format]
|
121
|
+
|
122
|
+
opts[:page] = page unless page.empty?
|
123
|
+
|
124
|
+
opts.deep_merge(overrides).compact
|
125
|
+
end
|
126
|
+
|
127
|
+
def validate_print_options!
|
128
|
+
validator = Bidi2pdf::Bidi::Commands::PrintParametersValidator.new(pdf_settings_for_bidi_cmd)
|
129
|
+
|
130
|
+
validator.validate!
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
module Bidi2pdfRails
|
6
|
+
module Configurable
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
included do
|
9
|
+
include Singleton
|
10
|
+
end
|
11
|
+
|
12
|
+
class BaseNestedConfig
|
13
|
+
def initialize(option_defs)
|
14
|
+
@__options = option_defs
|
15
|
+
define_accessors!
|
16
|
+
reset_to_defaults!
|
17
|
+
end
|
18
|
+
|
19
|
+
def define_accessors!
|
20
|
+
@__options.each do |opt|
|
21
|
+
name = opt[:name]
|
22
|
+
unless respond_to?(name)
|
23
|
+
define_singleton_method(name) { instance_variable_get("@#{name}") }
|
24
|
+
define_singleton_method("#{name}=") { |v| instance_variable_set("@#{name}", v) }
|
25
|
+
|
26
|
+
define_singleton_method("#{name}_value") do |*args|
|
27
|
+
value = send("#{name}")
|
28
|
+
value.respond_to?(:call) ? value.call(*args) : value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def reset_to_defaults!
|
35
|
+
@__options.each do |opt|
|
36
|
+
default = opt[:default]
|
37
|
+
value = default
|
38
|
+
send("#{opt[:name]}=", value)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def config_options
|
43
|
+
@__options
|
44
|
+
end
|
45
|
+
|
46
|
+
def configure
|
47
|
+
yield self if block_given?
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_h
|
51
|
+
@__options.to_h do |opt|
|
52
|
+
[opt[:name], send(opt[:name])]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def inspect
|
57
|
+
"#<#{self.class.name || "ConfigSection"} #{to_h.inspect}>"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def configure
|
62
|
+
yield self if block_given?
|
63
|
+
end
|
64
|
+
|
65
|
+
class_methods do
|
66
|
+
def configure_with(grouped_options)
|
67
|
+
@config_groups = grouped_options
|
68
|
+
|
69
|
+
grouped_options.each do |group_key, group_data|
|
70
|
+
group_class = Class.new(BaseNestedConfig)
|
71
|
+
|
72
|
+
define_method(group_key) do
|
73
|
+
ivar = "@#{group_key}"
|
74
|
+
instance_variable_get(ivar) || instance_variable_set(ivar, group_class.new(group_data[:options]))
|
75
|
+
end
|
76
|
+
|
77
|
+
define_method("#{group_key}=") do |val|
|
78
|
+
raise ArgumentError, "Use individual accessors, not direct assignment to config groups"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
define_method(:reset_to_defaults!) do
|
83
|
+
grouped_options.each_key do |group_key|
|
84
|
+
section = send(group_key)
|
85
|
+
section.reset_to_defaults!
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
define_method(:config_groups) do
|
90
|
+
grouped_options
|
91
|
+
end
|
92
|
+
|
93
|
+
define_method(:to_h) do
|
94
|
+
grouped_options.keys.to_h do |group_key|
|
95
|
+
section = send(group_key)
|
96
|
+
[group_key, section.to_h]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
define_method(:inspect) do
|
101
|
+
"#<#{self.class.name} #{to_h.inspect}>"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdfRails
|
4
|
+
class MainLogSubscriber < ActiveSupport::LogSubscriber
|
5
|
+
@silenced_events = []
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :silenced_events
|
9
|
+
|
10
|
+
def silence(event)
|
11
|
+
self.silenced_events << event
|
12
|
+
end
|
13
|
+
|
14
|
+
def silenced?(event)
|
15
|
+
self.silenced_events.any? { |silenced_event| silenced_event === event }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
include Bidi2pdf::Notifications::LoggingSubscriberActions
|
20
|
+
|
21
|
+
def handle_printing(event)
|
22
|
+
logger.info "Page rendered and printed: #{event.duration.round(1)}ms"
|
23
|
+
end
|
24
|
+
|
25
|
+
def logger
|
26
|
+
Bidi2pdf::VerboseLogger.new(super.tagged("bidi2pdf_rails"), Bidi2pdfRails.config.general_options.verbosity_value)
|
27
|
+
end
|
28
|
+
|
29
|
+
def silenced?(event)
|
30
|
+
MainLogSubscriber.silenced?(event) || super(event)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bidi2pdf/bidi/network_event_formatters/network_event_console_formatter"
|
4
|
+
|
5
|
+
module Bidi2pdfRails
|
6
|
+
class NetworkLogSubscriber < ActiveSupport::LogSubscriber
|
7
|
+
def network_event_received(event)
|
8
|
+
payload = event.payload
|
9
|
+
|
10
|
+
return if payload[:method] == "network.responseStarted" || payload[:method] == "network.beforeRequestSent"
|
11
|
+
|
12
|
+
logger.tagged("bidi2pdf_rails", "network") do |tagged_logger|
|
13
|
+
verbose_logger = Bidi2pdf::VerboseLogger.new(tagged_logger, Bidi2pdfRails.config.general_options.verbosity_value)
|
14
|
+
formatter = Bidi2pdf::Bidi::NetworkEventFormatters::NetworkEventConsoleFormatter.new(logger: verbose_logger)
|
15
|
+
|
16
|
+
formatter.log [payload[:event]]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,5 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "rails/railtie"
|
4
|
+
|
5
|
+
require_relative "services/html_renderer"
|
6
|
+
require_relative "services/pdf_renderer"
|
7
|
+
require_relative "services/pdf_browser_session"
|
8
|
+
require_relative "services/url_to_pdf_converter"
|
9
|
+
require_relative "services/html_to_pdf_converter"
|
10
|
+
|
3
11
|
module Bidi2pdfRails
|
4
12
|
class Railtie < ::Rails::Railtie
|
5
13
|
initializer "bidi2pdf_rails.add_mime_type" do
|
@@ -9,7 +17,8 @@ module Bidi2pdfRails
|
|
9
17
|
initializer "bidi2pdf_rails.initialize_chromedriver_manager", after: :load_config_initializers do
|
10
18
|
Bidi2pdfRails.apply_bidi2pdf_config
|
11
19
|
|
12
|
-
|
20
|
+
# defer the initialization of the ChromedriverManager to the ActionController::Base class load event
|
21
|
+
ActiveSupport.on_load(:action_controller_base) do
|
13
22
|
ChromedriverManagerSingleton.initialize_manager
|
14
23
|
end
|
15
24
|
end
|
@@ -23,56 +32,14 @@ module Bidi2pdfRails
|
|
23
32
|
|
24
33
|
initializer "bidi2pdf_rails.add_pdf_renderer" do
|
25
34
|
ActionController::Renderers.add :pdf do |filename, options|
|
26
|
-
pdf_data = ""
|
27
35
|
options = options.dup
|
28
36
|
|
29
|
-
|
30
|
-
|
31
|
-
if options[:asset_host]
|
32
|
-
ActionController::Base.asset_host = options[:asset_host]
|
33
|
-
elsif !Rails.application.config.action_controller.asset_host
|
34
|
-
# Use request base URL as fallback if no asset host configured
|
35
|
-
ActionController::Base.asset_host = request.base_url
|
36
|
-
end
|
37
|
-
|
38
|
-
html = render_to_string(options.merge(formats: [ :html ]))
|
39
|
-
|
40
|
-
Bidi2pdf.default_timeout = 30
|
41
|
-
|
42
|
-
# without a thread, the app will be blocked in dev mode
|
43
|
-
thread = Thread.new {
|
44
|
-
Rails.application.executor.wrap do
|
45
|
-
# subscriptions in other threads will not be inherited
|
46
|
-
Bidi2pdfRails::LogSubscriber.attach_to "bidi2pdf", inherit_all: true
|
47
|
-
|
48
|
-
browser = ChromedriverManagerSingleton.session.browser
|
49
|
-
context = browser.create_user_context
|
50
|
-
window = context.create_browser_window
|
51
|
-
tab = window.create_browser_tab
|
37
|
+
pdf_content = Services::PdfRenderer.new(filename, options, self).render_pdf
|
52
38
|
|
53
|
-
|
54
|
-
|
55
|
-
tab.wait_until_network_idle
|
56
|
-
|
57
|
-
tab.wait_until_page_loaded
|
58
|
-
|
59
|
-
pdf_data = tab.print
|
60
|
-
ensure
|
61
|
-
tab&.close
|
62
|
-
window&.close
|
63
|
-
context&.close
|
64
|
-
end
|
65
|
-
}
|
66
|
-
|
67
|
-
thread.join
|
68
|
-
|
69
|
-
send_data Base64.decode64(pdf_data),
|
39
|
+
send_data pdf_content,
|
70
40
|
type: Mime[:pdf],
|
71
41
|
filename: "#{filename}.pdf",
|
72
42
|
disposition: options.fetch(:disposition, "inline")
|
73
|
-
ensure
|
74
|
-
|
75
|
-
ActionController::Base.asset_host = original_asset_host
|
76
43
|
end
|
77
44
|
end
|
78
45
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdfRails
|
4
|
+
module Services
|
5
|
+
# app/services/html_renderer.rb
|
6
|
+
class HtmlRenderer
|
7
|
+
def initialize(controller, options)
|
8
|
+
@controller = controller
|
9
|
+
@options = options.dup
|
10
|
+
end
|
11
|
+
|
12
|
+
def render
|
13
|
+
with_overridden_asset_host { @controller.render_to_string(@options.merge(formats: [:html])) }
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def with_overridden_asset_host
|
19
|
+
original_asset_host = ActionController::Base.asset_host
|
20
|
+
|
21
|
+
if @options[:asset_host]
|
22
|
+
ActionController::Base.asset_host = @options[:asset_host]
|
23
|
+
elsif !Rails.application.config.action_controller.asset_host
|
24
|
+
ActionController::Base.asset_host = @controller.request.base_url
|
25
|
+
end
|
26
|
+
|
27
|
+
yield
|
28
|
+
ensure
|
29
|
+
ActionController::Base.asset_host = original_asset_host
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdfRails
|
4
|
+
module Services
|
5
|
+
class HtmlToPdfConverter
|
6
|
+
include PdfBrowserSession
|
7
|
+
|
8
|
+
def initialize(html, print_options: {}, wait_for_network_idle: true, wait_for_page_loaded: true, wait_for_page_check_script: nil)
|
9
|
+
@html = html
|
10
|
+
@print_options = print_options
|
11
|
+
@wait_for_network_idle = wait_for_network_idle
|
12
|
+
@wait_for_page_loaded = wait_for_page_loaded
|
13
|
+
@wait_for_page_check_script = wait_for_page_check_script
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate
|
17
|
+
run_browser_session
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def prepare_tab(tab)
|
23
|
+
tab.render_html_content(@html)
|
24
|
+
wait_for_tab(tab)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdfRails
|
4
|
+
module Services
|
5
|
+
module PdfBrowserSession
|
6
|
+
def run_browser_session
|
7
|
+
pdf_data = nil
|
8
|
+
|
9
|
+
thread = Thread.new do
|
10
|
+
Rails.application.executor.wrap do
|
11
|
+
browser = ChromedriverManagerSingleton.session.browser
|
12
|
+
context = browser.create_user_context
|
13
|
+
window = context.create_browser_window
|
14
|
+
tab = window.create_browser_tab
|
15
|
+
|
16
|
+
begin
|
17
|
+
prepare_tab(tab)
|
18
|
+
pdf_data = tab.print(print_options: @print_options)
|
19
|
+
ensure
|
20
|
+
tab&.close
|
21
|
+
window&.close
|
22
|
+
context&.close
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
thread.join
|
28
|
+
Base64.decode64(pdf_data)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def wait_for_tab(tab)
|
34
|
+
tab.wait_until_network_idle if @wait_for_network_idle
|
35
|
+
tab.wait_until_page_loaded(check_script: @wait_for_page_check_script) if @wait_for_page_loaded
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdfRails
|
4
|
+
module Services
|
5
|
+
# PdfRenderer is responsible for rendering a Rails view into a PDF using headless Chrome.
|
6
|
+
#
|
7
|
+
# It accepts a mix of HTML rendering options (passed to `render_to_string`) and PDF generation options.
|
8
|
+
#
|
9
|
+
# @example Basic usage
|
10
|
+
# renderer = Bidi2pdfRails::Services::PdfRenderer.new("invoice", { template: "invoices/show" }, controller)
|
11
|
+
# renderer.render_pdf
|
12
|
+
#
|
13
|
+
# @param filename [String] the base filename for the generated PDF (used for download naming)
|
14
|
+
# @param options [Hash] rendering options including both HTML and PDF-specific options
|
15
|
+
# @option options [String] :template The template to render (or other render_to_string keys like :partial, :layout, etc.)
|
16
|
+
# @option options [String] :asset_host Optional asset host override
|
17
|
+
# @option options [Hash] :print_options Options passed to the Chrome PDF print API
|
18
|
+
# @option options [Boolean] :wait_for_network_idle Wait for network to go idle before generating PDF (default: config default)
|
19
|
+
# @option options [Boolean] :wait_for_page_loaded Wait for page load event before generating PDF (default: config default)
|
20
|
+
# @option options [String] :wait_for_page_check_script JavaScript condition to wait for before generating PDF
|
21
|
+
#
|
22
|
+
class PdfRenderer
|
23
|
+
PDF_OPTIONS = %i[
|
24
|
+
print_options
|
25
|
+
asset_host
|
26
|
+
wait_for_network_idle
|
27
|
+
wait_for_page_loaded
|
28
|
+
wait_for_page_check_script
|
29
|
+
html
|
30
|
+
].freeze
|
31
|
+
|
32
|
+
PRINT_URL_OPTIONS = %i[
|
33
|
+
url
|
34
|
+
auth
|
35
|
+
headers
|
36
|
+
cookies
|
37
|
+
].freeze
|
38
|
+
|
39
|
+
def initialize(filename, options, controller)
|
40
|
+
@filename = filename
|
41
|
+
@pdf_options = options.slice(*PDF_OPTIONS)
|
42
|
+
@print_url_options = options.slice(*PRINT_URL_OPTIONS)
|
43
|
+
@html_options = options.except(*(PDF_OPTIONS + PRINT_URL_OPTIONS))
|
44
|
+
@controller = controller
|
45
|
+
end
|
46
|
+
|
47
|
+
def render_pdf
|
48
|
+
ActiveSupport::Notifications.instrument("handle_printing.bidi2pdf_rails") do
|
49
|
+
print_options = Bidi2pdfRails.config.pdf_settings_for_bidi_cmd(@pdf_options[:print_options] || {})
|
50
|
+
wait_for_network_idle = @pdf_options.fetch(:wait_for_network_idle, Bidi2pdfRails.config.general_options.wait_for_network_idle_value)
|
51
|
+
wait_for_page_loaded = @pdf_options.fetch(:wait_for_page_loaded, Bidi2pdfRails.config.general_options.wait_for_page_loaded_value)
|
52
|
+
wait_for_page_check_script = @pdf_options.fetch(:wait_for_page_check_script, Bidi2pdfRails.config.general_options.wait_for_page_check_script_value)
|
53
|
+
|
54
|
+
if @print_url_options[:url]
|
55
|
+
headers = @print_url_options[:headers] || Bidi2pdfRails.config.render_remote_settings.headers_value(@controller)
|
56
|
+
cookies = @print_url_options[:cookies] || Bidi2pdfRails.config.render_remote_settings.cookies_value(@controller)
|
57
|
+
auth = @print_url_options[:auth] || { username: Bidi2pdfRails.config.render_remote_settings.basic_auth_user_value(@controller), password: Bidi2pdfRails.config.render_remote_settings.basic_auth_pass_value(@controller) }
|
58
|
+
|
59
|
+
UrlToPdfConverter.new(@print_url_options[:url],
|
60
|
+
headers: headers,
|
61
|
+
cookies: cookies,
|
62
|
+
auth: auth,
|
63
|
+
print_options: print_options,
|
64
|
+
wait_for_network_idle: wait_for_network_idle,
|
65
|
+
wait_for_page_loaded: wait_for_page_loaded,
|
66
|
+
wait_for_page_check_script: wait_for_page_check_script
|
67
|
+
).generate
|
68
|
+
else
|
69
|
+
html = @html_options.fetch(:inline, HtmlRenderer.new(@controller, @html_options).render)
|
70
|
+
|
71
|
+
HtmlToPdfConverter.new(html,
|
72
|
+
print_options: print_options,
|
73
|
+
wait_for_network_idle: wait_for_network_idle,
|
74
|
+
wait_for_page_loaded: wait_for_page_loaded,
|
75
|
+
wait_for_page_check_script: wait_for_page_check_script
|
76
|
+
).generate
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdfRails
|
4
|
+
module Services
|
5
|
+
class UrlToPdfConverter
|
6
|
+
include PdfBrowserSession
|
7
|
+
|
8
|
+
def initialize(url, headers: {}, cookies: {}, auth: {}, print_options: {}, wait_for_network_idle: true, wait_for_page_loaded: true, wait_for_page_check_script: nil)
|
9
|
+
@url = url
|
10
|
+
@headers = headers || {}
|
11
|
+
@cookies = cookies || {}
|
12
|
+
@auth = auth || {}
|
13
|
+
@print_options = print_options
|
14
|
+
@wait_for_network_idle = wait_for_network_idle
|
15
|
+
@wait_for_page_loaded = wait_for_page_loaded
|
16
|
+
@wait_for_page_check_script = wait_for_page_check_script
|
17
|
+
end
|
18
|
+
|
19
|
+
def generate
|
20
|
+
run_browser_session
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def prepare_tab(tab)
|
26
|
+
add_headers(tab)
|
27
|
+
add_basic_auth(tab)
|
28
|
+
add_cookies(tab)
|
29
|
+
|
30
|
+
tab.navigate_to(@url)
|
31
|
+
wait_for_tab(tab)
|
32
|
+
end
|
33
|
+
|
34
|
+
def add_cookies(tab)
|
35
|
+
@cookies.each do |name, value|
|
36
|
+
tab.set_cookie(
|
37
|
+
name: name,
|
38
|
+
value: value,
|
39
|
+
domain: domain,
|
40
|
+
secure: uri.scheme == "https"
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_headers(tab)
|
46
|
+
@headers.each do |name, value|
|
47
|
+
tab.add_headers(
|
48
|
+
url_patterns: url_patterns,
|
49
|
+
headers: [{ name: name, value: value }]
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def add_basic_auth(tab)
|
55
|
+
return unless @auth[:username] && @auth[:password]
|
56
|
+
|
57
|
+
tab.basic_auth(
|
58
|
+
url_patterns: url_patterns,
|
59
|
+
username: @auth[:username],
|
60
|
+
password: @auth[:password]
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
64
|
+
def uri
|
65
|
+
@uri ||= URI(@url)
|
66
|
+
end
|
67
|
+
|
68
|
+
def domain
|
69
|
+
uri.host
|
70
|
+
end
|
71
|
+
|
72
|
+
def url_patterns
|
73
|
+
[{
|
74
|
+
type: "pattern",
|
75
|
+
protocol: uri.scheme,
|
76
|
+
hostname: uri.host,
|
77
|
+
port: uri.port.to_s
|
78
|
+
}]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|