html2img-client 1.0.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 +7 -0
- data/CHANGELOG.md +34 -0
- data/LICENSE +21 -0
- data/README.md +472 -0
- data/exe/html2img +6 -0
- data/lib/html2img/cli.rb +218 -0
- data/lib/html2img/client.rb +271 -0
- data/lib/html2img/configuration.rb +98 -0
- data/lib/html2img/errors.rb +92 -0
- data/lib/html2img/render_response.rb +115 -0
- data/lib/html2img/request.rb +141 -0
- data/lib/html2img/transport.rb +64 -0
- data/lib/html2img/version.rb +6 -0
- data/lib/html2img-client.rb +5 -0
- metadata +62 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Html2img
|
|
4
|
+
# The result of a successful render call.
|
|
5
|
+
#
|
|
6
|
+
# Covers both the synchronous envelope (`url` populated) and the asynchronous
|
|
7
|
+
# acceptance envelope returned when a `webhook_url` was supplied (`status` is
|
|
8
|
+
# "processing" and `url` is nil until the webhook fires).
|
|
9
|
+
class RenderResponse
|
|
10
|
+
# @return [Boolean]
|
|
11
|
+
attr_reader :success
|
|
12
|
+
|
|
13
|
+
# @return [String, nil] the render id
|
|
14
|
+
attr_reader :id
|
|
15
|
+
|
|
16
|
+
# @return [String, nil] the CDN URL of the render
|
|
17
|
+
attr_reader :url
|
|
18
|
+
|
|
19
|
+
# @return [String, nil] when a hosted render expires, as an ISO 8601 string.
|
|
20
|
+
# Nil on paid plans, where renders stay hosted permanently; set on
|
|
21
|
+
# free-tier renders, which are hosted for seven days.
|
|
22
|
+
attr_reader :expires_at
|
|
23
|
+
|
|
24
|
+
# @return [Integer, nil] credits left after this call
|
|
25
|
+
attr_reader :credits_remaining
|
|
26
|
+
|
|
27
|
+
# @return [String, nil] "processing" for async jobs
|
|
28
|
+
attr_reader :status
|
|
29
|
+
|
|
30
|
+
# @return [String, nil]
|
|
31
|
+
attr_reader :message
|
|
32
|
+
|
|
33
|
+
# @return [String, nil] the template slug, when applicable
|
|
34
|
+
attr_reader :template
|
|
35
|
+
|
|
36
|
+
# @return [Hash] the full decoded JSON payload
|
|
37
|
+
attr_reader :raw
|
|
38
|
+
|
|
39
|
+
# Build a response from a decoded JSON payload.
|
|
40
|
+
def self.from_hash(data)
|
|
41
|
+
data = {} unless data.is_a?(Hash)
|
|
42
|
+
|
|
43
|
+
new(
|
|
44
|
+
success: data["success"] ? true : false,
|
|
45
|
+
id: string_or_nil(data["id"]),
|
|
46
|
+
url: string_or_nil(data["url"]),
|
|
47
|
+
expires_at: string_or_nil(data["expires_at"]),
|
|
48
|
+
credits_remaining: integer_or_nil(data["credits_remaining"]),
|
|
49
|
+
status: string_or_nil(data["status"]),
|
|
50
|
+
message: string_or_nil(data["message"]),
|
|
51
|
+
template: string_or_nil(data["template"]),
|
|
52
|
+
raw: data
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.string_or_nil(value)
|
|
57
|
+
return nil if value.nil? || value.is_a?(Hash) || value.is_a?(Array)
|
|
58
|
+
|
|
59
|
+
value.to_s
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.integer_or_nil(value)
|
|
63
|
+
return value if value.is_a?(Integer)
|
|
64
|
+
return value.to_i if value.is_a?(Float)
|
|
65
|
+
return Integer(value, exception: false) if value.is_a?(String)
|
|
66
|
+
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private_class_method :string_or_nil, :integer_or_nil
|
|
71
|
+
|
|
72
|
+
def initialize(success: false, id: nil, url: nil, expires_at: nil, credits_remaining: nil,
|
|
73
|
+
status: nil, message: nil, template: nil, raw: {})
|
|
74
|
+
@success = success
|
|
75
|
+
@id = id
|
|
76
|
+
@url = url
|
|
77
|
+
@expires_at = expires_at
|
|
78
|
+
@credits_remaining = credits_remaining
|
|
79
|
+
@status = status
|
|
80
|
+
@message = message
|
|
81
|
+
@template = template
|
|
82
|
+
@raw = raw
|
|
83
|
+
freeze
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Whether this is an async job still being rendered.
|
|
87
|
+
#
|
|
88
|
+
# When true, the final URL is delivered to the request's `webhook_url`
|
|
89
|
+
# rather than being available on this response.
|
|
90
|
+
def processing?
|
|
91
|
+
status == "processing"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Whether the render came back as a PDF rather than an image.
|
|
95
|
+
def pdf?
|
|
96
|
+
return false if url.nil?
|
|
97
|
+
|
|
98
|
+
url.to_s.split("?").first.to_s.downcase.end_with?(".pdf")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def success?
|
|
102
|
+
success
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The URL, so a response drops straight into string interpolation.
|
|
106
|
+
def to_s
|
|
107
|
+
url.to_s
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def inspect
|
|
111
|
+
"#<Html2img::RenderResponse url=#{url.inspect} status=#{status.inspect} " \
|
|
112
|
+
"credits_remaining=#{credits_remaining.inspect}>"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Html2img
|
|
4
|
+
# Builds and validates the JSON bodies sent to the render endpoints.
|
|
5
|
+
#
|
|
6
|
+
# The range checks mirror the server-side validation rules, so an obvious
|
|
7
|
+
# mistake fails fast with a clear message before a request is sent, and
|
|
8
|
+
# before a credit is spent. Any option left nil is omitted from the body, so
|
|
9
|
+
# the server applies its own default.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module Request
|
|
13
|
+
MIN_DIMENSION = 1
|
|
14
|
+
MAX_DIMENSION = 5000
|
|
15
|
+
MIN_DPI = 1
|
|
16
|
+
MAX_DPI = 4
|
|
17
|
+
MIN_MS_DELAY = 1
|
|
18
|
+
MAX_MS_DELAY = 5000
|
|
19
|
+
|
|
20
|
+
FORMATS = %w[png pdf].freeze
|
|
21
|
+
|
|
22
|
+
# Options accepted by both render endpoints.
|
|
23
|
+
COMMON_OPTIONS = %i[
|
|
24
|
+
css width height fullpage dpi webhook_url ms_delay wait_for_selector format scale_to_fit
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
# `selector` crops a screenshot to one element. There is no equivalent for
|
|
28
|
+
# an HTML render, since you control the markup.
|
|
29
|
+
SCREENSHOT_OPTIONS = (COMMON_OPTIONS + %i[selector]).freeze
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# Build the body for POST /api/html.
|
|
34
|
+
#
|
|
35
|
+
# @param html [String] a complete HTML document
|
|
36
|
+
# @return [Hash]
|
|
37
|
+
def html_body(html, options)
|
|
38
|
+
validate_keys!(options, COMMON_OPTIONS)
|
|
39
|
+
|
|
40
|
+
body = { "html" => required_string!("html", html) }
|
|
41
|
+
|
|
42
|
+
body.merge(common(options)).compact
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Build the body for POST /api/screenshot.
|
|
46
|
+
#
|
|
47
|
+
# @param url [String] a publicly reachable URL
|
|
48
|
+
# @return [Hash]
|
|
49
|
+
def screenshot_body(url, options)
|
|
50
|
+
validate_keys!(options, SCREENSHOT_OPTIONS)
|
|
51
|
+
|
|
52
|
+
body = { "url" => required_string!("url", url) }
|
|
53
|
+
body["selector"] = optional_string!("selector", options[:selector])
|
|
54
|
+
|
|
55
|
+
body.merge(common(options)).compact
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The options shared by both endpoints, mapped onto their API field names.
|
|
59
|
+
def common(options)
|
|
60
|
+
{
|
|
61
|
+
"css" => optional_string!("css", options[:css]),
|
|
62
|
+
"width" => dimension!("width", options[:width]),
|
|
63
|
+
"height" => dimension!("height", options[:height]),
|
|
64
|
+
"fullpage" => boolean!("fullpage", options[:fullpage]),
|
|
65
|
+
"dpi" => dpi!(options[:dpi]),
|
|
66
|
+
"webhook_url" => optional_string!("webhook_url", options[:webhook_url]),
|
|
67
|
+
"ms_delay" => ms_delay!(options[:ms_delay]),
|
|
68
|
+
"wait_for_selector" => optional_string!("wait_for_selector", options[:wait_for_selector]),
|
|
69
|
+
"format" => format!(options[:format]),
|
|
70
|
+
"scale_to_fit" => boolean!("scale_to_fit", options[:scale_to_fit])
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def validate_keys!(options, allowed)
|
|
75
|
+
unknown = options.keys - allowed
|
|
76
|
+
|
|
77
|
+
return if unknown.empty?
|
|
78
|
+
|
|
79
|
+
raise ArgumentError,
|
|
80
|
+
"Unknown option(s): #{unknown.join(", ")}. " \
|
|
81
|
+
"Valid options are: #{allowed.sort.join(", ")}."
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def required_string!(name, value)
|
|
85
|
+
raise ArgumentError, "The #{name} must be a String, got #{value.class}." unless value.is_a?(String)
|
|
86
|
+
|
|
87
|
+
raise ArgumentError, "The #{name} must not be empty." if value.empty?
|
|
88
|
+
|
|
89
|
+
value
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def optional_string!(name, value)
|
|
93
|
+
return nil if value.nil?
|
|
94
|
+
|
|
95
|
+
raise ArgumentError, "The #{name} must be a String, got #{value.class}." unless value.is_a?(String)
|
|
96
|
+
|
|
97
|
+
value
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def boolean!(name, value)
|
|
101
|
+
return nil if value.nil?
|
|
102
|
+
return value if [true, false].include?(value)
|
|
103
|
+
|
|
104
|
+
raise ArgumentError, "The #{name} must be true or false, got #{value.inspect}."
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def dimension!(name, value)
|
|
108
|
+
integer_in_range!(name, value, MIN_DIMENSION, MAX_DIMENSION)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def dpi!(value)
|
|
112
|
+
integer_in_range!("dpi", value, MIN_DPI, MAX_DPI)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def ms_delay!(value)
|
|
116
|
+
integer_in_range!("ms_delay", value, MIN_MS_DELAY, MAX_MS_DELAY)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def integer_in_range!(name, value, low, high)
|
|
120
|
+
return nil if value.nil?
|
|
121
|
+
|
|
122
|
+
raise ArgumentError, "The #{name} must be an Integer, got #{value.class}." unless value.is_a?(Integer)
|
|
123
|
+
|
|
124
|
+
unless value.between?(low, high)
|
|
125
|
+
raise ArgumentError, "The #{name} must be between #{low} and #{high}, got #{value}."
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
value
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def format!(value)
|
|
132
|
+
return nil if value.nil?
|
|
133
|
+
|
|
134
|
+
normalised = value.to_s.downcase
|
|
135
|
+
|
|
136
|
+
return normalised if FORMATS.include?(normalised)
|
|
137
|
+
|
|
138
|
+
raise ArgumentError, "The format must be one of: #{FORMATS.join(", ")}. Got #{value.inspect}."
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Html2img
|
|
8
|
+
# HTTP transport for the client.
|
|
9
|
+
#
|
|
10
|
+
# The default is built on Net::HTTP from the standard library, so the gem has
|
|
11
|
+
# no runtime dependencies. Anything responding to #call with the same
|
|
12
|
+
# signature can be passed to {Client} as `transport:`, which is the seam for
|
|
13
|
+
# Faraday, HTTPX, a proxy, retry middleware, or a stub in your tests.
|
|
14
|
+
#
|
|
15
|
+
# A transport returns the raw [status, body] pair for *any* HTTP response,
|
|
16
|
+
# including 4xx and 5xx. Mapping a status onto a typed error is the client's
|
|
17
|
+
# job. A transport only raises when no response was received at all.
|
|
18
|
+
class Transport
|
|
19
|
+
# @param method [String] "POST" or "GET"
|
|
20
|
+
# @param url [String] the absolute URL
|
|
21
|
+
# @param headers [Hash{String => String}]
|
|
22
|
+
# @param body [String, nil] the request body
|
|
23
|
+
# @param timeout [Float] seconds
|
|
24
|
+
# @return [Array(Integer, String)] status and body
|
|
25
|
+
def call(method:, url:, headers:, body:, timeout:)
|
|
26
|
+
uri = URI.parse(url)
|
|
27
|
+
request = build_request(method, uri, headers, body)
|
|
28
|
+
|
|
29
|
+
response = http(uri, timeout).request(request)
|
|
30
|
+
|
|
31
|
+
[response.code.to_i, response.body.to_s]
|
|
32
|
+
# Net::OpenTimeout and Net::ReadTimeout both descend from Timeout::Error.
|
|
33
|
+
rescue Timeout::Error => e
|
|
34
|
+
raise TimeoutError, "The request to #{url} timed out after #{timeout} seconds: #{e.message}"
|
|
35
|
+
rescue SocketError, SystemCallError, OpenSSL::SSL::SSLError, IOError, Net::HTTPBadResponse => e
|
|
36
|
+
raise ConnectionError, "Could not reach the html2img API: #{e.message}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def http(uri, timeout)
|
|
42
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
43
|
+
http.use_ssl = uri.scheme == "https"
|
|
44
|
+
http.open_timeout = timeout
|
|
45
|
+
http.read_timeout = timeout
|
|
46
|
+
http.write_timeout = timeout if http.respond_to?(:write_timeout=)
|
|
47
|
+
http
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_request(method, uri, headers, body)
|
|
51
|
+
request =
|
|
52
|
+
case method.to_s.upcase
|
|
53
|
+
when "POST" then Net::HTTP::Post.new(uri)
|
|
54
|
+
when "GET" then Net::HTTP::Get.new(uri)
|
|
55
|
+
else raise ArgumentError, "Unsupported HTTP method: #{method}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
headers.each { |name, value| request[name] = value }
|
|
59
|
+
request.body = body if body
|
|
60
|
+
|
|
61
|
+
request
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: html2img-client
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- html2img
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |-
|
|
13
|
+
Render HTML and CSS to images, capture screenshots of live URLs, render named
|
|
14
|
+
templates and export A4 PDFs, all in real Chrome. Zero runtime dependencies.
|
|
15
|
+
email:
|
|
16
|
+
- info@html2img.com
|
|
17
|
+
executables:
|
|
18
|
+
- html2img
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- CHANGELOG.md
|
|
23
|
+
- LICENSE
|
|
24
|
+
- README.md
|
|
25
|
+
- exe/html2img
|
|
26
|
+
- lib/html2img-client.rb
|
|
27
|
+
- lib/html2img/cli.rb
|
|
28
|
+
- lib/html2img/client.rb
|
|
29
|
+
- lib/html2img/configuration.rb
|
|
30
|
+
- lib/html2img/errors.rb
|
|
31
|
+
- lib/html2img/render_response.rb
|
|
32
|
+
- lib/html2img/request.rb
|
|
33
|
+
- lib/html2img/transport.rb
|
|
34
|
+
- lib/html2img/version.rb
|
|
35
|
+
homepage: https://html2img.com
|
|
36
|
+
licenses:
|
|
37
|
+
- MIT
|
|
38
|
+
metadata:
|
|
39
|
+
homepage_uri: https://html2img.com
|
|
40
|
+
source_code_uri: https://github.com/html2img/html2img-ruby
|
|
41
|
+
changelog_uri: https://github.com/html2img/html2img-ruby/blob/main/CHANGELOG.md
|
|
42
|
+
bug_tracker_uri: https://github.com/html2img/html2img-ruby/issues
|
|
43
|
+
documentation_uri: https://html2img.com/docs/usage/ruby
|
|
44
|
+
rubygems_mfa_required: 'true'
|
|
45
|
+
rdoc_options: []
|
|
46
|
+
require_paths:
|
|
47
|
+
- lib
|
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: 3.1.0
|
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: '0'
|
|
58
|
+
requirements: []
|
|
59
|
+
rubygems_version: 3.6.9
|
|
60
|
+
specification_version: 4
|
|
61
|
+
summary: Official Ruby client for the html2img HTML to Image API.
|
|
62
|
+
test_files: []
|