gotenberg-rails 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6afda17f3ec1d2563dd371cfa5a36c9e368dc507062db2aa1e3dd8ae10b0b5f2
4
+ data.tar.gz: c988d6aa39af30f7f1e0c52b8348e483a33665aa6e55f5b00c094160de55e252
5
+ SHA512:
6
+ metadata.gz: 87351a0801cbf3e392f29e8824ce1dcae7b743efb1afd23ddeef0f67a4fc1f4420c44761eaa6f2aca319862b628c487fbbc7b5a7bb30f440da0f26ab6c1d3479
7
+ data.tar.gz: 290ef76a4572942e746029014b411241bf64eed842c309cdcb9b45a6a9ed43e3f697c014bcbfd98203824046ee060f2ed5f2ad641b61f450814c7f932a89b13c
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shinichi Maeshima
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # Gotenberg Rails
2
+
3
+ Render Rails HTML as PDFs with [Gotenberg](https://gotenberg.dev/).
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your Gemfile:
8
+
9
+ ```ruby
10
+ gem "gotenberg-rails"
11
+ ```
12
+
13
+ Run Gotenberg:
14
+
15
+ ```sh
16
+ docker run --rm -p 3000:3000 gotenberg/gotenberg:8
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ Render a PDF from a Rails controller:
22
+
23
+ ```ruby
24
+ def show
25
+ respond_to do |format|
26
+ format.html
27
+ format.pdf do
28
+ render gotenberg_pdf: {}, disposition: :inline, filename: "invoice.pdf"
29
+ end
30
+ end
31
+ end
32
+ ```
33
+
34
+ Customize the rendered template and Gotenberg options:
35
+
36
+ ```ruby
37
+ def show
38
+ render gotenberg_pdf: {
39
+ print_background: true,
40
+ paper_width: "8.27",
41
+ paper_height: "11.7",
42
+ margin_top: "0.4",
43
+ margin_bottom: "0.4",
44
+ margin_left: "0.4",
45
+ margin_right: "0.4"
46
+ },
47
+ layout: "pdf",
48
+ template: "invoices/show",
49
+ disposition: :inline,
50
+ filename: "invoice.pdf"
51
+ end
52
+ ```
53
+
54
+ You can also render directly:
55
+
56
+ ```ruby
57
+ Gotenberg::Rails.render_pdf(html: html)
58
+ Gotenberg::Rails.render_pdf(html: html, display_url: "https://example.com/invoice")
59
+ Gotenberg::Rails.render_pdf(url: "https://example.com/invoice")
60
+ ```
61
+
62
+ When rendering HTML, `display_url` is used to rewrite relative image, link, JavaScript, stylesheet, and CSS `url(...)` references to absolute URLs before sending the HTML to Gotenberg. Controller rendering uses `request.original_url` automatically.
63
+
64
+ Options are sent to Gotenberg as Chromium form fields. Ruby-style snake case keys are converted to Gotenberg camel case keys:
65
+
66
+ ```ruby
67
+ Gotenberg::Rails.render_pdf(
68
+ html: html,
69
+ pdf_options: {
70
+ print_background: true,
71
+ emulated_media_type: "screen",
72
+ wait_delay: "2s",
73
+ fail_on_http_status_codes: [499, 599],
74
+ metadata: { Title: "Invoice" }
75
+ }
76
+ )
77
+ ```
78
+
79
+ ## Configuration
80
+
81
+ ```ruby
82
+ Gotenberg::Rails.configure do |config|
83
+ config.endpoint = ENV.fetch("GOTENBERG_ENDPOINT", "http://gotenberg:3000")
84
+ config.open_timeout = 5
85
+ config.request_timeout = 30
86
+ config.headers = { "X-Request-Source" => "rails" }
87
+ config.pdf_options = {
88
+ print_background: true,
89
+ prefer_css_page_size: true
90
+ }
91
+ end
92
+ ```
93
+
94
+ Gotenberg receives rendered HTML as an `index.html` upload. Use absolute URLs for stylesheets, images, and fonts that Gotenberg must fetch from your Rails app.
95
+
96
+ ## License
97
+
98
+ The gem is available as open source under the terms of the MIT License.
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "stringio"
6
+ require "uri"
7
+
8
+ module Gotenberg
9
+ module Rails
10
+ class Client
11
+ HTML_PATH = "/forms/chromium/convert/html"
12
+ URL_PATH = "/forms/chromium/convert/url"
13
+
14
+ attr_reader :endpoint, :open_timeout, :request_timeout, :headers
15
+
16
+ def initialize(endpoint:, open_timeout:, request_timeout:, headers: {})
17
+ @endpoint = endpoint.to_s.delete_suffix("/")
18
+ @open_timeout = open_timeout
19
+ @request_timeout = request_timeout
20
+ @headers = headers
21
+ end
22
+
23
+ def render_pdf(html: nil, url: nil, pdf_options: {}, filename: nil, trace: nil)
24
+ if html && url
25
+ raise ArgumentError, "Provide either :html or :url, not both"
26
+ elsif html
27
+ post(HTML_PATH, html_form(html, pdf_options), filename:, trace:)
28
+ elsif url
29
+ post(URL_PATH, url_form(url, pdf_options), filename:, trace:)
30
+ else
31
+ raise ArgumentError, "Provide :html or :url"
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def html_form(html, pdf_options)
38
+ [
39
+ ["files", StringIO.new(html.to_s), { filename: "index.html", content_type: "text/html" }],
40
+ *option_fields(pdf_options)
41
+ ]
42
+ end
43
+
44
+ def url_form(url, pdf_options)
45
+ [
46
+ ["url", url.to_s],
47
+ *option_fields(pdf_options)
48
+ ]
49
+ end
50
+
51
+ def option_fields(options)
52
+ options.compact.map do |key, value|
53
+ [camelize_option(key), encode_option(value)]
54
+ end
55
+ end
56
+
57
+ def camelize_option(key)
58
+ key.to_s.gsub(/_([a-z])/) { Regexp.last_match(1).upcase }
59
+ end
60
+
61
+ def encode_option(value)
62
+ case value
63
+ when Array, Hash
64
+ JSON.generate(value)
65
+ else
66
+ value.to_s
67
+ end
68
+ end
69
+
70
+ def post(path, form, filename:, trace:)
71
+ uri = URI.join("#{endpoint}/", path.delete_prefix("/"))
72
+ request = Net::HTTP::Post.new(uri)
73
+ headers.each { |key, value| request[key] = value }
74
+ request["Gotenberg-Output-Filename"] = filename if filename
75
+ request["Gotenberg-Trace"] = trace if trace
76
+ request.set_form(form, "multipart/form-data")
77
+
78
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |connection|
79
+ connection.open_timeout = open_timeout
80
+ connection.read_timeout = request_timeout
81
+ connection.request(request)
82
+ end
83
+
84
+ return response.body if response.is_a?(Net::HTTPSuccess)
85
+
86
+ raise ConversionError, response
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gotenberg
4
+ module Rails
5
+ class Configuration
6
+ attr_accessor :endpoint, :request_timeout, :open_timeout, :pdf_options, :headers
7
+
8
+ def initialize
9
+ @endpoint = ENV.fetch("GOTENBERG_ENDPOINT", "http://localhost:3000")
10
+ @request_timeout = 60
11
+ @open_timeout = 10
12
+ @pdf_options = {}
13
+ @headers = {}
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gotenberg
4
+ module Rails
5
+ class Error < StandardError; end
6
+
7
+ class ConversionError < Error
8
+ attr_reader :response
9
+
10
+ def initialize(response)
11
+ @response = response
12
+ super("Gotenberg conversion failed with #{response.code}: #{response.body}")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "uri"
5
+
6
+ module Gotenberg
7
+ module Rails
8
+ class HtmlPreprocessor
9
+ URL_ATTRIBUTES = %w[action href poster src].freeze
10
+ DATA_URL_ATTRIBUTES = {
11
+ "object" => %w[data]
12
+ }.freeze
13
+ SRCSET_ATTRIBUTES = %w[srcset].freeze
14
+ SKIPPED_SCHEMES = %w[cid data javascript mailto tel].freeze
15
+ CSS_URL_PATTERN = /url\(\s*(['"]?)([^'")]+)\1\s*\)/i
16
+
17
+ attr_reader :html, :display_url
18
+
19
+ def initialize(html, display_url:)
20
+ @html = html.to_s
21
+ @display_url = display_url
22
+ end
23
+
24
+ def call
25
+ return html if display_url.nil? || display_url.to_s.empty?
26
+
27
+ document = Nokogiri::HTML5(html)
28
+ base_url = document.at_css("base[href]")&.[]("href") || display_url
29
+
30
+ preprocess_url_attributes(document, base_url)
31
+ preprocess_srcset_attributes(document, base_url)
32
+ preprocess_css_urls(document, base_url)
33
+
34
+ document.to_html
35
+ end
36
+
37
+ private
38
+
39
+ def preprocess_url_attributes(document, base_url)
40
+ URL_ATTRIBUTES.each do |attribute|
41
+ document.css("[#{attribute}]").each do |node|
42
+ node[attribute] = absolute_url(node[attribute], base_url)
43
+ end
44
+ end
45
+
46
+ DATA_URL_ATTRIBUTES.each do |selector, attributes|
47
+ attributes.each do |attribute|
48
+ document.css("#{selector}[#{attribute}]").each do |node|
49
+ node[attribute] = absolute_url(node[attribute], base_url)
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def preprocess_srcset_attributes(document, base_url)
56
+ SRCSET_ATTRIBUTES.each do |attribute|
57
+ document.css("[#{attribute}]").each do |node|
58
+ node[attribute] = absolutize_srcset(node[attribute], base_url)
59
+ end
60
+ end
61
+ end
62
+
63
+ def preprocess_css_urls(document, base_url)
64
+ document.css("[style]").each do |node|
65
+ node["style"] = absolutize_css_urls(node["style"], base_url)
66
+ end
67
+
68
+ document.css("style").each do |node|
69
+ node.content = absolutize_css_urls(node.content, base_url)
70
+ end
71
+ end
72
+
73
+ def absolutize_srcset(value, base_url)
74
+ value.to_s.split(",").map do |candidate|
75
+ url, descriptor = candidate.strip.split(/\s+/, 2)
76
+ [absolute_url(url, base_url), descriptor].compact.join(" ")
77
+ end.join(", ")
78
+ end
79
+
80
+ def absolutize_css_urls(value, base_url)
81
+ value.to_s.gsub(CSS_URL_PATTERN) do
82
+ quote = Regexp.last_match(1)
83
+ url = Regexp.last_match(2)
84
+ "url(#{quote}#{absolute_url(url, base_url)}#{quote})"
85
+ end
86
+ end
87
+
88
+ def absolute_url(value, base_url)
89
+ url = value.to_s.strip
90
+ return value if url.empty? || url.start_with?("#") || skipped_scheme?(url)
91
+
92
+ URI.join(base_url, url).to_s
93
+ rescue URI::InvalidURIError
94
+ value
95
+ end
96
+
97
+ def skipped_scheme?(url)
98
+ scheme = URI.parse(url).scheme
99
+ scheme && SKIPPED_SCHEMES.include?(scheme.downcase)
100
+ rescue URI::InvalidURIError
101
+ false
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_controller/railtie"
4
+
5
+ module Gotenberg
6
+ module Rails
7
+ class Railtie < ::Rails::Railtie
8
+ initializer "gotenberg_rails.renderer" do
9
+ ActiveSupport.on_load(:action_controller) do
10
+ Gotenberg::Rails::Renderer.register
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gotenberg
4
+ module Rails
5
+ module Renderer
6
+ def self.register
7
+ ActionController::Renderers.add :gotenberg_pdf do |options, render_options|
8
+ pdf_options = options || {}
9
+ filename = render_options[:filename] || pdf_options.delete(:filename) || "#{controller_name}.pdf"
10
+ disposition = render_options[:disposition] || pdf_options.delete(:disposition) || "attachment"
11
+ display_url = render_options[:display_url] || pdf_options.delete(:display_url) || request.original_url
12
+
13
+ html = render_to_string(render_options.except(:display_url, :filename, :disposition).merge(formats: [:html]))
14
+ pdf = Gotenberg::Rails.render_pdf(html:, display_url:, pdf_options:, filename:)
15
+
16
+ send_data pdf,
17
+ filename: filename,
18
+ type: "application/pdf",
19
+ disposition: disposition
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gotenberg
4
+ module Rails
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/except"
4
+ require_relative "rails/client"
5
+ require_relative "rails/configuration"
6
+ require_relative "rails/error"
7
+ require_relative "rails/html_preprocessor"
8
+ require_relative "rails/renderer"
9
+ require_relative "rails/version"
10
+
11
+ require_relative "rails/railtie" if defined?(::Rails::Railtie)
12
+
13
+ module Gotenberg
14
+ module Rails
15
+ class << self
16
+ attr_writer :configuration, :client
17
+
18
+ def configuration
19
+ @configuration ||= Configuration.new
20
+ end
21
+
22
+ def configure
23
+ yield configuration
24
+ end
25
+
26
+ def client
27
+ @client ||= Client.new(
28
+ endpoint: configuration.endpoint,
29
+ open_timeout: configuration.open_timeout,
30
+ request_timeout: configuration.request_timeout,
31
+ headers: configuration.headers
32
+ )
33
+ end
34
+
35
+ def render_pdf(html: nil, url: nil, display_url: nil, pdf_options: {}, **options)
36
+ merged_options = configuration.pdf_options.merge(pdf_options || {})
37
+ html = HtmlPreprocessor.new(html, display_url:).call if html
38
+
39
+ client.render_pdf(html:, url:, pdf_options: merged_options, **options)
40
+ end
41
+
42
+ def reset_configuration!
43
+ @configuration = Configuration.new
44
+ @client = nil
45
+ end
46
+ end
47
+ end
48
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gotenberg-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Shinichi Maeshima
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: actionpack
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.2'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activesupport
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.2'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.2'
40
+ - !ruby/object:Gem::Dependency
41
+ name: nokogiri
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 1.8.5
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 1.8.5
54
+ - !ruby/object:Gem::Dependency
55
+ name: minitest
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '5.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '5.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rake
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '13.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '13.0'
82
+ description: A small Rails renderer and Ruby API for converting HTML or URLs to PDF
83
+ using Gotenberg.
84
+ email:
85
+ - netwillnet@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - LICENSE
91
+ - README.md
92
+ - lib/gotenberg/rails.rb
93
+ - lib/gotenberg/rails/client.rb
94
+ - lib/gotenberg/rails/configuration.rb
95
+ - lib/gotenberg/rails/error.rb
96
+ - lib/gotenberg/rails/html_preprocessor.rb
97
+ - lib/gotenberg/rails/railtie.rb
98
+ - lib/gotenberg/rails/renderer.rb
99
+ - lib/gotenberg/rails/version.rb
100
+ homepage: https://github.com/willnet/gotenberg-rails
101
+ licenses:
102
+ - MIT
103
+ metadata:
104
+ homepage_uri: https://github.com/willnet/gotenberg-rails
105
+ source_code_uri: https://github.com/willnet/gotenberg-rails
106
+ changelog_uri: https://github.com/willnet/gotenberg-rails/releases
107
+ rubygems_mfa_required: 'true'
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '3.2'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubygems_version: 4.0.6
123
+ specification_version: 4
124
+ summary: Render Rails HTML as PDFs with Gotenberg.
125
+ test_files: []