selectpdf 1.3.0 → 1.6.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/CHANGELOG.md +16 -1
- data/README.md +191 -4
- data/lib/selectpdf.rb +2088 -120
- data/samples/accessible_pdf.rb +69 -0
- data/samples/async_conversion.rb +48 -0
- data/samples/electronic_invoice.rb +106 -0
- data/samples/html_to_pdf_headers_and_footers.rb +5 -5
- data/samples/html_to_pdf_main.rb +40 -16
- data/samples/pdf_merge.rb +40 -0
- data/samples/pdf_to_text.rb +45 -0
- data/samples/search_pdf.rb +38 -0
- data/samples/simple_html_string_to_pdf.rb +2 -2
- data/samples/simple_url_to_pdf.rb +2 -2
- data/samples/web_elements.rb +42 -0
- metadata +16 -8
data/lib/selectpdf.rb
CHANGED
|
@@ -5,16 +5,175 @@ require 'json'
|
|
|
5
5
|
require 'fileutils'
|
|
6
6
|
|
|
7
7
|
#
|
|
8
|
-
# SelectPdf Online REST API Ruby client library. Contains
|
|
8
|
+
# SelectPdf Online REST API Ruby client library. Contains HTML to PDF converter, PDF merge, PDF to text extractor, search PDF.
|
|
9
9
|
#
|
|
10
10
|
#
|
|
11
11
|
# Convert HTML to PDF
|
|
12
12
|
#
|
|
13
|
-
#
|
|
13
|
+
# require 'selectpdf'
|
|
14
|
+
# print "This is SelectPdf-#{SelectPdf::CLIENT_VERSION}\n"
|
|
15
|
+
#
|
|
16
|
+
# url = 'https://selectpdf.com'
|
|
17
|
+
# local_file = 'Test.pdf'
|
|
18
|
+
# api_key = 'Your API key here'
|
|
19
|
+
#
|
|
20
|
+
# begin
|
|
21
|
+
# api = SelectPdf::HtmlToPdfClient.new(api_key)
|
|
22
|
+
#
|
|
23
|
+
# api.page_size = SelectPdf::PageSize::A4
|
|
24
|
+
# api.margins = 0
|
|
25
|
+
# api.page_numbers = false
|
|
26
|
+
# api.page_breaks_enhanced_algorithm = true
|
|
27
|
+
#
|
|
28
|
+
# api.convert_url_to_file(url, local_file)
|
|
29
|
+
# rescue SelectPdf::ApiException => e
|
|
30
|
+
# print("An error occurred: #{e}")
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# Merge PDFs from local disk or public url and save result into a file on disk.
|
|
34
|
+
#
|
|
35
|
+
# require 'selectpdf'
|
|
36
|
+
#
|
|
37
|
+
# $stdout.sync = true
|
|
38
|
+
#
|
|
39
|
+
# print "This is SelectPdf-#{SelectPdf::CLIENT_VERSION}\n"
|
|
40
|
+
#
|
|
41
|
+
# test_url = 'https://selectpdf.com/demo/files/selectpdf.pdf'
|
|
42
|
+
# test_pdf = 'Input.pdf'
|
|
43
|
+
# local_file = 'Result.pdf'
|
|
44
|
+
# api_key = 'Your API key here'
|
|
45
|
+
#
|
|
46
|
+
# begin
|
|
47
|
+
# client = SelectPdf::PdfMergeClient.new(api_key)
|
|
48
|
+
#
|
|
49
|
+
# # set parameters - see full list at https://selectpdf.com/pdf-merge-api/
|
|
50
|
+
#
|
|
51
|
+
# # specify the pdf files that will be merged (order will be preserved in the final pdf)
|
|
52
|
+
# client.add_file(test_pdf) # add PDF from local file
|
|
53
|
+
# client.add_url_file(test_url) # add PDF from public url
|
|
54
|
+
# # client.add_file(test_pdf, 'pdf_password') # add PDF (that requires a password) from local file
|
|
55
|
+
# # client.add_url_file(test_url, 'pdf_password') # add PDF (that requires a password) from public url
|
|
56
|
+
#
|
|
57
|
+
# print "Starting pdf merge ...\n"
|
|
58
|
+
#
|
|
59
|
+
# # merge pdfs to local file
|
|
60
|
+
# client.save_to_file(local_file)
|
|
61
|
+
#
|
|
62
|
+
# # merge pdfs to memory
|
|
63
|
+
# # pdf = client.save
|
|
64
|
+
#
|
|
65
|
+
# print "Finished! Number of pages: #{client.number_of_pages}.\n"
|
|
66
|
+
#
|
|
67
|
+
# # get API usage
|
|
68
|
+
# usage_client = SelectPdf::UsageClient.new(api_key)
|
|
69
|
+
# usage = usage_client.get_usage(false)
|
|
70
|
+
# print("Usage: #{usage}\n")
|
|
71
|
+
# print('Conversions remained this month: ', usage['available'], "\n")
|
|
72
|
+
# rescue SelectPdf::ApiException => e
|
|
73
|
+
# print("An error occurred: #{e}")
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
76
|
+
# Extract text from PDF
|
|
77
|
+
#
|
|
78
|
+
# require 'selectpdf'
|
|
79
|
+
#
|
|
80
|
+
# $stdout.sync = true
|
|
81
|
+
#
|
|
82
|
+
# print "This is SelectPdf-#{SelectPdf::CLIENT_VERSION}\n"
|
|
83
|
+
#
|
|
84
|
+
# test_url = 'https://selectpdf.com/demo/files/selectpdf.pdf'
|
|
85
|
+
# test_pdf = 'Input.pdf'
|
|
86
|
+
# local_file = 'Result.txt'
|
|
87
|
+
# api_key = 'Your API key here'
|
|
88
|
+
#
|
|
89
|
+
# begin
|
|
90
|
+
# client = SelectPdf::PdfToTextClient.new(api_key)
|
|
91
|
+
#
|
|
92
|
+
# # set parameters - see full list at https://selectpdf.com/pdf-to-text-api/
|
|
93
|
+
# client.start_page = 1 # start page (processing starts from here)
|
|
94
|
+
# client.end_page = 0 # end page (set 0 to process file til the end)
|
|
95
|
+
# client.output_format = SelectPdf::OutputFormat::TEXT # set output format (Text or HTML)
|
|
96
|
+
#
|
|
97
|
+
# print "Starting pdf to text ...\n"
|
|
98
|
+
#
|
|
99
|
+
# # convert local pdf to local text file
|
|
100
|
+
# client.text_from_file_to_file(test_pdf, local_file)
|
|
101
|
+
#
|
|
102
|
+
# # extract text from local pdf to memory
|
|
103
|
+
# # text = client.text_from_file(test_pdf)
|
|
104
|
+
# # print text
|
|
105
|
+
#
|
|
106
|
+
# # convert pdf from public url to local text file
|
|
107
|
+
# # client.text_from_url_to_file(test_url, local_file)
|
|
108
|
+
#
|
|
109
|
+
# # extract text from pdf from public url to memory
|
|
110
|
+
# # text = client.text_from_url(test_url)
|
|
111
|
+
# # print text
|
|
112
|
+
#
|
|
113
|
+
# print "Finished! Number of pages processed: #{client.number_of_pages}.\n"
|
|
114
|
+
#
|
|
115
|
+
# # get API usage
|
|
116
|
+
# usage_client = SelectPdf::UsageClient.new(api_key)
|
|
117
|
+
# usage = usage_client.get_usage(false)
|
|
118
|
+
# print("Usage: #{usage}\n")
|
|
119
|
+
# print('Conversions remained this month: ', usage['available'], "\n")
|
|
120
|
+
# rescue SelectPdf::ApiException => e
|
|
121
|
+
# print("An error occurred: #{e}")
|
|
122
|
+
# end
|
|
123
|
+
#
|
|
124
|
+
# Search Pdf
|
|
125
|
+
#
|
|
126
|
+
# require 'selectpdf'
|
|
127
|
+
#
|
|
128
|
+
# $stdout.sync = true
|
|
129
|
+
#
|
|
130
|
+
# print "This is SelectPdf-#{SelectPdf::CLIENT_VERSION}\n"
|
|
131
|
+
#
|
|
132
|
+
# test_url = 'https://selectpdf.com/demo/files/selectpdf.pdf'
|
|
133
|
+
# test_pdf = 'Input.pdf'
|
|
134
|
+
# api_key = 'Your API key here'
|
|
135
|
+
#
|
|
136
|
+
# begin
|
|
137
|
+
# client = SelectPdf::PdfToTextClient.new(api_key)
|
|
138
|
+
#
|
|
139
|
+
# # set parameters - see full list at https://selectpdf.com/pdf-to-text-api/
|
|
140
|
+
# client.start_page = 1 # start page (processing starts from here)
|
|
141
|
+
# client.end_page = 0 # end page (set 0 to process file til the end)
|
|
142
|
+
# client.output_format = SelectPdf::OutputFormat::TEXT # set output format (Text or HTML)
|
|
143
|
+
#
|
|
144
|
+
# print "Starting search pdf ...\n"
|
|
145
|
+
#
|
|
146
|
+
# # search local pdf
|
|
147
|
+
# results = client.search_file(test_pdf, 'pdf')
|
|
148
|
+
#
|
|
149
|
+
# # search pdf from public url
|
|
150
|
+
# # results = client.search_url(test_url, 'pdf')
|
|
151
|
+
#
|
|
152
|
+
# print "Search results: #{results}.\nSearch results count: #{results.length}\n"
|
|
153
|
+
#
|
|
154
|
+
# print "Finished! Number of pages processed: #{client.number_of_pages}.\n"
|
|
155
|
+
#
|
|
156
|
+
# # get API usage
|
|
157
|
+
# usage_client = SelectPdf::UsageClient.new(api_key)
|
|
158
|
+
# usage = usage_client.get_usage(false)
|
|
159
|
+
# print("Usage: #{usage}\n")
|
|
160
|
+
# print('Conversions remained this month: ', usage['available'], "\n")
|
|
161
|
+
# rescue SelectPdf::ApiException => e
|
|
162
|
+
# print("An error occurred: #{e}")
|
|
163
|
+
# end
|
|
164
|
+
#
|
|
14
165
|
module SelectPdf
|
|
166
|
+
# Multipart/form-data boundary
|
|
15
167
|
MULTIPART_FORM_DATA_BOUNDARY = '------------SelectPdf_Api_Boundry_$'
|
|
168
|
+
|
|
169
|
+
# New line
|
|
16
170
|
NEW_LINE = "\r\n"
|
|
17
|
-
|
|
171
|
+
|
|
172
|
+
# Library version
|
|
173
|
+
CLIENT_VERSION = '1.6.0'
|
|
174
|
+
|
|
175
|
+
# Default upgrade URL displayed in demo-mode error messages.
|
|
176
|
+
DEMO_UPGRADE_URL = 'https://selectpdf.com/pricing/'
|
|
18
177
|
|
|
19
178
|
attr_reader :code, :message
|
|
20
179
|
#
|
|
@@ -34,6 +193,115 @@ module SelectPdf
|
|
|
34
193
|
end
|
|
35
194
|
end
|
|
36
195
|
|
|
196
|
+
# Exception raised when the keyless demo endpoint refuses a request because of a rate limit (HTTP 429 or 503).
|
|
197
|
+
# Inspect reason to distinguish:
|
|
198
|
+
# * per_ip - the source IP exceeded its hourly conversion budget
|
|
199
|
+
# * concurrency - too many demo conversions are running right now
|
|
200
|
+
# * daily_cap - the global demo budget for today has been reached
|
|
201
|
+
class DemoRateLimitException < ApiException
|
|
202
|
+
# HTTP status code returned by the server (429 or 503).
|
|
203
|
+
attr_reader :status_code
|
|
204
|
+
|
|
205
|
+
# Machine-readable rate-limit reason: per_ip, concurrency or daily_cap.
|
|
206
|
+
attr_reader :reason
|
|
207
|
+
|
|
208
|
+
# Seconds the client should wait before retrying (parsed from the Retry-After header). Zero if absent.
|
|
209
|
+
attr_reader :retry_after
|
|
210
|
+
|
|
211
|
+
# URL the user can visit to upgrade out of demo mode.
|
|
212
|
+
attr_reader :upgrade_url
|
|
213
|
+
|
|
214
|
+
# The raw JSON body the server returned, for diagnostics / logging.
|
|
215
|
+
attr_reader :response_body
|
|
216
|
+
|
|
217
|
+
# Constructor invoked by the client when the demo endpoint returns a rate-limit error.
|
|
218
|
+
#
|
|
219
|
+
# @param status_code HTTP status code.
|
|
220
|
+
# @param reason Rate-limit reason.
|
|
221
|
+
# @param retry_after Seconds to wait before retrying.
|
|
222
|
+
# @param upgrade_url Upgrade URL.
|
|
223
|
+
# @param response_body Raw JSON body.
|
|
224
|
+
def initialize(status_code, reason, retry_after, upgrade_url, response_body)
|
|
225
|
+
upgrade = upgrade_url.nil? || upgrade_url.empty? ? DEMO_UPGRADE_URL : upgrade_url
|
|
226
|
+
retry_after = retry_after.to_i
|
|
227
|
+
retry_text = retry_after > 0 ? " Retry after #{retry_after}s." : ''
|
|
228
|
+
super("(#{status_code}) Demo rate limit reached (reason=#{reason || '?'}).#{retry_text} Upgrade at #{upgrade}.")
|
|
229
|
+
|
|
230
|
+
@status_code = status_code
|
|
231
|
+
@reason = reason || ''
|
|
232
|
+
@retry_after = retry_after
|
|
233
|
+
@upgrade_url = upgrade
|
|
234
|
+
@response_body = response_body
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Exception raised when the demo safety guard rejects a request because a URL field references a non-public host (HTTP 400).
|
|
239
|
+
# Inspect field for which parameter was rejected and reason for why.
|
|
240
|
+
class DemoSafetyException < ApiException
|
|
241
|
+
# HTTP status code returned by the server (400).
|
|
242
|
+
attr_reader :status_code
|
|
243
|
+
|
|
244
|
+
# Which input field was rejected: url, html, base_url, header_url, footer_url.
|
|
245
|
+
attr_reader :field
|
|
246
|
+
|
|
247
|
+
# Why the field was rejected: blocked_host, private_ip, loopback, link_local, cgnat, metadata, multicast, bad_scheme, bad_url, inline_internal_ref:<sub-reason>.
|
|
248
|
+
attr_reader :reason
|
|
249
|
+
|
|
250
|
+
# The raw JSON body the server returned, for diagnostics / logging.
|
|
251
|
+
attr_reader :response_body
|
|
252
|
+
|
|
253
|
+
# Constructor invoked by the client when the demo safety guard rejects a URL field.
|
|
254
|
+
#
|
|
255
|
+
# @param status_code HTTP status code.
|
|
256
|
+
# @param field Rejected field.
|
|
257
|
+
# @param reason Rejection reason.
|
|
258
|
+
# @param response_body Raw JSON body.
|
|
259
|
+
def initialize(status_code, field, reason, response_body)
|
|
260
|
+
super("(#{status_code}) Demo safety guard rejected #{field || '?'} (reason=#{reason || '?'}). Demo conversions cannot fetch internal/private hosts.")
|
|
261
|
+
|
|
262
|
+
@status_code = status_code
|
|
263
|
+
@field = field || ''
|
|
264
|
+
@reason = reason || ''
|
|
265
|
+
@response_body = response_body
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Exception raised when the caller tried to use a feature that demo mode does not support - most commonly PDF passwords.
|
|
270
|
+
# For paid keys, this exception is never raised.
|
|
271
|
+
class DemoUnsupportedException < ApiException
|
|
272
|
+
# HTTP status code returned by the server (400). Zero if the exception was raised by the local client guard before the request was sent.
|
|
273
|
+
attr_reader :status_code
|
|
274
|
+
|
|
275
|
+
# Which feature is unsupported: user_password, owner_password, async.
|
|
276
|
+
attr_reader :field
|
|
277
|
+
|
|
278
|
+
# URL the user can visit to upgrade out of demo mode.
|
|
279
|
+
attr_reader :upgrade_url
|
|
280
|
+
|
|
281
|
+
# The raw JSON body the server returned, for diagnostics / logging. Nil when raised by the local client guard.
|
|
282
|
+
attr_reader :response_body
|
|
283
|
+
|
|
284
|
+
# Constructor. Pass status_code 0 (and no body) for the local client-side guard that runs before the request is sent.
|
|
285
|
+
#
|
|
286
|
+
# @param status_code HTTP status code (0 for the local client guard).
|
|
287
|
+
# @param field Unsupported feature.
|
|
288
|
+
# @param upgrade_url Upgrade URL.
|
|
289
|
+
# @param response_body Raw JSON body.
|
|
290
|
+
def initialize(status_code, field, upgrade_url = nil, response_body = nil)
|
|
291
|
+
upgrade = upgrade_url.nil? || upgrade_url.empty? ? DEMO_UPGRADE_URL : upgrade_url
|
|
292
|
+
if status_code.to_i == 0
|
|
293
|
+
super("Feature '#{field || '?'}' is not available in demo mode. Construct HtmlToPdfClient with a paid API key, or upgrade at #{DEMO_UPGRADE_URL}.")
|
|
294
|
+
else
|
|
295
|
+
super("(#{status_code}) Feature '#{field || '?'}' is not available in demo mode. Upgrade at #{upgrade}.")
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
@status_code = status_code.to_i
|
|
299
|
+
@field = field || ''
|
|
300
|
+
@upgrade_url = upgrade
|
|
301
|
+
@response_body = response_body
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
37
305
|
# Base class for API clients. Do not use this directly.
|
|
38
306
|
class ApiClient
|
|
39
307
|
# API endpoint
|
|
@@ -69,6 +337,22 @@ module SelectPdf
|
|
|
69
337
|
# Number of pages of the pdf document resulted from the conversion.
|
|
70
338
|
attr_reader :number_of_pages
|
|
71
339
|
|
|
340
|
+
# Subscription monthly conversion limit reported by the server (X-SelectPdf-Credits-Total response header).
|
|
341
|
+
# -1 = unlimited (Dedicated tier). Nil = the most recent response did not include credit info (e.g. demo endpoint, error response).
|
|
342
|
+
attr_reader :credits_total
|
|
343
|
+
|
|
344
|
+
# Conversions remaining in the current month reported by the server (X-SelectPdf-Credits-Remaining response header).
|
|
345
|
+
# -1 = unlimited (Dedicated tier). Nil = the most recent response did not include credit info.
|
|
346
|
+
attr_reader :credits_remaining
|
|
347
|
+
|
|
348
|
+
# Endpoint mode of the most recent response (X-SelectPdf-Mode response header): "production" or "demo".
|
|
349
|
+
# Empty when the response did not include the header (older server, or non-conversion endpoint).
|
|
350
|
+
attr_reader :mode
|
|
351
|
+
|
|
352
|
+
# Server-side execution path of the most recent response (X-SelectPdf-Execution response header): "in-process" or "worker".
|
|
353
|
+
# Empty for endpoints that do not perform a conversion (e.g. usage, web elements).
|
|
354
|
+
attr_reader :execution_mode
|
|
355
|
+
|
|
72
356
|
# Class constructor
|
|
73
357
|
def initialize
|
|
74
358
|
# API endpoint
|
|
@@ -98,6 +382,15 @@ module SelectPdf
|
|
|
98
382
|
# Job ID for asynchronous calls or for calls that require a second request.
|
|
99
383
|
@job_id = ''
|
|
100
384
|
|
|
385
|
+
# Last HTTP status code.
|
|
386
|
+
@last_http_code = 0
|
|
387
|
+
|
|
388
|
+
# Response telemetry.
|
|
389
|
+
@credits_total = nil
|
|
390
|
+
@credits_remaining = nil
|
|
391
|
+
@mode = ''
|
|
392
|
+
@execution_mode = ''
|
|
393
|
+
|
|
101
394
|
# Ping interval in seconds for asynchronous calls. Default value is 3 seconds.
|
|
102
395
|
@async_calls_ping_interval = 3
|
|
103
396
|
|
|
@@ -110,32 +403,62 @@ module SelectPdf
|
|
|
110
403
|
# @param out_stream Output response to this stream, if specified.
|
|
111
404
|
# @return If output stream is not specified, return response.
|
|
112
405
|
def perform_post(out_stream = nil)
|
|
406
|
+
uri = URI(@api_endpoint)
|
|
407
|
+
request = Net::HTTP::Post.new uri.request_uri
|
|
408
|
+
request.set_form_data(@parameters)
|
|
409
|
+
request['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
410
|
+
|
|
411
|
+
execute_request(uri, request, out_stream)
|
|
412
|
+
end
|
|
413
|
+
protected :perform_post
|
|
414
|
+
|
|
415
|
+
# Create a multipart/form-data POST request (that can handle file uploads).
|
|
416
|
+
#
|
|
417
|
+
# @param out_stream Output response to this stream, if specified.
|
|
418
|
+
# @return If output stream is not specified, return response.
|
|
419
|
+
def perform_post_as_multipart_formdata(out_stream = nil)
|
|
420
|
+
uri = URI(@api_endpoint)
|
|
421
|
+
request = Net::HTTP::Post.new uri.request_uri
|
|
422
|
+
request.body = encode_multipart_form_data
|
|
423
|
+
request['Content-Type'] = "multipart/form-data; boundary=#{MULTIPART_FORM_DATA_BOUNDARY}"
|
|
424
|
+
request['Content-Length'] = request.body.bytesize.to_s
|
|
425
|
+
|
|
426
|
+
execute_request(uri, request, out_stream)
|
|
427
|
+
end
|
|
428
|
+
protected :perform_post_as_multipart_formdata
|
|
429
|
+
|
|
430
|
+
# Send a prepared request and process the response.
|
|
431
|
+
#
|
|
432
|
+
# @param uri Endpoint URI.
|
|
433
|
+
# @param request Prepared POST request.
|
|
434
|
+
# @param out_stream Output response to this stream, if specified.
|
|
435
|
+
# @return If output stream is not specified, return response.
|
|
436
|
+
def execute_request(uri, request, out_stream)
|
|
113
437
|
# reset results
|
|
114
|
-
|
|
115
|
-
|
|
438
|
+
reset_results
|
|
439
|
+
|
|
440
|
+
# add headers
|
|
441
|
+
request['selectpdf-api-client'] = "ruby-#{RUBY_VERSION}-#{CLIENT_VERSION}"
|
|
442
|
+
@headers.each do |key, value|
|
|
443
|
+
request[key] = value
|
|
444
|
+
end
|
|
116
445
|
|
|
117
|
-
uri = URI(api_endpoint)
|
|
118
446
|
http = Net::HTTP.new(uri.host, uri.port)
|
|
119
447
|
http.use_ssl = @api_endpoint.downcase.start_with?('https')
|
|
120
448
|
http.read_timeout = 600 # timeout in seconds 600s=10minutes
|
|
121
449
|
|
|
122
450
|
http.start do |connection|
|
|
123
|
-
request = Net::HTTP::Post.new uri.request_uri
|
|
124
|
-
request.set_form_data(@parameters)
|
|
125
|
-
|
|
126
|
-
# add headers
|
|
127
|
-
request['selectpdf-api-client'] = "ruby-#{RUBY_VERSION}-#{CLIENT_VERSION}"
|
|
128
|
-
request['Content-Type'] = 'application/x-www-form-urlencoded'
|
|
129
|
-
@headers.each do |key, value|
|
|
130
|
-
request[key] = value
|
|
131
|
-
end
|
|
132
|
-
|
|
133
451
|
connection.request request do |response|
|
|
134
|
-
|
|
135
|
-
|
|
452
|
+
@last_http_code = response.code.to_i
|
|
453
|
+
|
|
454
|
+
if response.is_a?(Net::HTTPAccepted)
|
|
455
|
+
# request accepted (for asynchronous jobs)
|
|
456
|
+
read_response_headers(response)
|
|
457
|
+
|
|
458
|
+
return nil
|
|
459
|
+
elsif response.is_a?(Net::HTTPSuccess)
|
|
136
460
|
# all ok
|
|
137
|
-
|
|
138
|
-
@job_id = response['selectpdf-api-jobid']
|
|
461
|
+
read_response_headers(response)
|
|
139
462
|
|
|
140
463
|
return response.body unless out_stream # return response if out_stream is not provided
|
|
141
464
|
|
|
@@ -143,14 +466,20 @@ module SelectPdf
|
|
|
143
466
|
response.read_body do |chunk|
|
|
144
467
|
out_stream.write chunk
|
|
145
468
|
end
|
|
146
|
-
when Net::HTTPAccepted
|
|
147
|
-
# request accepted (for asynchronous jobs)
|
|
148
|
-
@job_id = response['selectpdf-api-jobid']
|
|
149
469
|
|
|
150
470
|
return nil
|
|
151
471
|
else
|
|
152
472
|
# error - get error message
|
|
153
|
-
|
|
473
|
+
body = response.body.to_s.dup.force_encoding('UTF-8')
|
|
474
|
+
|
|
475
|
+
# The demo endpoint returns JSON error bodies for 400 / 413 / 429 / 503.
|
|
476
|
+
# Parse those into typed exceptions so callers can react programmatically.
|
|
477
|
+
if response['Content-Type'].to_s.downcase.include?('application/json')
|
|
478
|
+
demo_exception = build_demo_exception(response.code.to_i, body, response['Retry-After'])
|
|
479
|
+
raise demo_exception unless demo_exception.nil?
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
raise ApiException.new(body, response.code), body
|
|
154
483
|
end
|
|
155
484
|
end
|
|
156
485
|
end
|
|
@@ -159,112 +488,167 @@ module SelectPdf
|
|
|
159
488
|
rescue SocketError => e
|
|
160
489
|
raise ApiException.new("Socket Error: #{e}"), "Socket Error: #{e}"
|
|
161
490
|
rescue Timeout::Error
|
|
162
|
-
raise ApiException.new(
|
|
491
|
+
raise ApiException.new('Connection Timeout: 600s exceeded'), 'Connection Timeout: 600s exceeded'
|
|
163
492
|
rescue OpenSSL::SSL::SSLError => e
|
|
164
493
|
raise ApiException.new("SSL Error: #{e}"), "SSL Error: #{e}"
|
|
165
494
|
rescue StandardError => e
|
|
166
495
|
raise ApiException.new("Connection refused: #{e}"), "Connection refused: #{e}"
|
|
167
496
|
end
|
|
168
|
-
|
|
497
|
+
private :execute_request
|
|
169
498
|
|
|
170
|
-
#
|
|
171
|
-
|
|
172
|
-
# @param out_stream Output response to this stream, if specified.
|
|
173
|
-
# @return If output stream is not specified, return response.
|
|
174
|
-
def perform_post_as_multipart_formdata(out_stream = nil)
|
|
175
|
-
# reset results
|
|
499
|
+
# Reset the results of the previous call.
|
|
500
|
+
def reset_results
|
|
176
501
|
@number_of_pages = 0
|
|
177
502
|
@job_id = ''
|
|
503
|
+
@last_http_code = 0
|
|
504
|
+
@credits_total = nil
|
|
505
|
+
@credits_remaining = nil
|
|
506
|
+
@mode = ''
|
|
507
|
+
@execution_mode = ''
|
|
508
|
+
end
|
|
509
|
+
private :reset_results
|
|
178
510
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
511
|
+
# Read the standard X-SelectPdf-* response headers.
|
|
512
|
+
#
|
|
513
|
+
# @param response HTTP response.
|
|
514
|
+
def read_response_headers(response)
|
|
515
|
+
pages = response['X-SelectPdf-Pages']
|
|
516
|
+
@number_of_pages = pages.to_i unless pages.nil? || pages.empty?
|
|
183
517
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
request.body = encode_multipart_form_data
|
|
518
|
+
job_id = response['X-SelectPdf-Job-Id']
|
|
519
|
+
@job_id = job_id unless job_id.nil? || job_id.empty?
|
|
187
520
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
request['Content-Type'] = "multipart/form-data; boundary=#{MULTIPART_FORM_DATA_BOUNDARY}"
|
|
191
|
-
request['Content-Length'] = request.body.length
|
|
521
|
+
@credits_total = parse_int_header(response['X-SelectPdf-Credits-Total'])
|
|
522
|
+
@credits_remaining = parse_int_header(response['X-SelectPdf-Credits-Remaining'])
|
|
192
523
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
end
|
|
524
|
+
mode = response['X-SelectPdf-Mode']
|
|
525
|
+
@mode = mode unless mode.nil? || mode.empty?
|
|
196
526
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
when Net::HTTPSuccess
|
|
200
|
-
# all ok
|
|
201
|
-
@number_of_pages = (response['selectpdf-api-pages'] || 0).to_i
|
|
202
|
-
@job_id = response['selectpdf-api-jobid']
|
|
527
|
+
execution_mode = response['X-SelectPdf-Execution']
|
|
528
|
+
@execution_mode = execution_mode unless execution_mode.nil? || execution_mode.empty?
|
|
203
529
|
|
|
204
|
-
|
|
530
|
+
begin
|
|
531
|
+
on_response_headers_received(response)
|
|
532
|
+
rescue StandardError
|
|
533
|
+
# ignore errors in subclass header processing
|
|
534
|
+
nil
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
private :read_response_headers
|
|
205
538
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
@job_id = response['selectpdf-api-jobid']
|
|
539
|
+
# Hook called after a successful response, with the raw response.
|
|
540
|
+
# Subclasses can override it to capture endpoint-specific headers. Default implementation does nothing.
|
|
541
|
+
#
|
|
542
|
+
# @param response HTTP response.
|
|
543
|
+
def on_response_headers_received(response); end
|
|
544
|
+
protected :on_response_headers_received
|
|
213
545
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
546
|
+
# Parse an integer header value.
|
|
547
|
+
#
|
|
548
|
+
# @param value Header value.
|
|
549
|
+
# @return Integer value or nil if the value is missing or not an integer.
|
|
550
|
+
def parse_int_header(value)
|
|
551
|
+
return nil if value.nil?
|
|
552
|
+
|
|
553
|
+
value = value.strip
|
|
554
|
+
return nil unless value =~ /\A[-+]?\d+\z/
|
|
555
|
+
|
|
556
|
+
value.to_i
|
|
557
|
+
end
|
|
558
|
+
private :parse_int_header
|
|
559
|
+
|
|
560
|
+
# Build a typed demo exception from a JSON error body returned by the demo endpoint.
|
|
561
|
+
#
|
|
562
|
+
# The demo endpoint returns structured JSON for 400 / 413 / 429 / 503 errors:
|
|
563
|
+
# {"error":"rate_limited","reason":"per_ip","upgrade":"..."}
|
|
564
|
+
# {"error":"unsafe_url","field":"url","reason":"private_ip"}
|
|
565
|
+
# {"error":"unsupported_in_demo","field":"user_password","upgrade":"..."}
|
|
566
|
+
# {"error":"body_too_large","max_bytes":1048576,"upgrade":"..."}
|
|
567
|
+
#
|
|
568
|
+
# @param status_code HTTP status code.
|
|
569
|
+
# @param body Response body.
|
|
570
|
+
# @param retry_after_header Value of the Retry-After header.
|
|
571
|
+
# @return The exception to raise, or nil if the body is not a demo error.
|
|
572
|
+
def build_demo_exception(status_code, body, retry_after_header)
|
|
573
|
+
return nil if body.nil? || body.empty?
|
|
574
|
+
|
|
575
|
+
begin
|
|
576
|
+
json = JSON.parse(body)
|
|
577
|
+
rescue JSON::ParserError
|
|
578
|
+
return nil
|
|
579
|
+
end
|
|
580
|
+
return nil unless json.is_a?(Hash)
|
|
581
|
+
|
|
582
|
+
error = json_string(json, 'error')
|
|
583
|
+
return nil if error.nil? || error.empty?
|
|
584
|
+
|
|
585
|
+
reason = json_string(json, 'reason')
|
|
586
|
+
field = json_string(json, 'field')
|
|
587
|
+
upgrade = json_string(json, 'upgrade')
|
|
588
|
+
|
|
589
|
+
retry_after = 0
|
|
590
|
+
retry_after = retry_after_header.strip.to_i if retry_after_header.to_s.strip =~ /\A\d+\z/
|
|
591
|
+
|
|
592
|
+
case error
|
|
593
|
+
when 'rate_limited'
|
|
594
|
+
DemoRateLimitException.new(status_code, reason, retry_after, upgrade, body)
|
|
595
|
+
when 'unsafe_url'
|
|
596
|
+
DemoSafetyException.new(status_code, field, reason, body)
|
|
597
|
+
when 'unsupported_in_demo'
|
|
598
|
+
DemoUnsupportedException.new(status_code, field, upgrade, body)
|
|
599
|
+
when 'body_too_large'
|
|
600
|
+
message = "(#{status_code}) Demo request body exceeds the demo cap. Upgrade at #{upgrade || DEMO_UPGRADE_URL}."
|
|
601
|
+
ApiException.new(message)
|
|
220
602
|
end
|
|
221
|
-
rescue ApiException
|
|
222
|
-
raise
|
|
223
|
-
rescue SocketError => e
|
|
224
|
-
raise ApiException.new("Socket Error: #{e}"), "Socket Error: #{e}"
|
|
225
|
-
rescue Timeout::Error
|
|
226
|
-
raise ApiException.new("Connection Timeout: #{http.read_timeout}s exceeded"), "Connection Timeout: #{http.read_timeout}s exceeded"
|
|
227
|
-
rescue OpenSSL::SSL::SSLError => e
|
|
228
|
-
raise ApiException.new("SSL Error: #{e}"), "SSL Error: #{e}"
|
|
229
|
-
rescue StandardError => e
|
|
230
|
-
raise ApiException.new("Connection refused: #{e}"), "Connection refused: #{e}"
|
|
231
603
|
end
|
|
232
|
-
|
|
604
|
+
private :build_demo_exception
|
|
605
|
+
|
|
606
|
+
# Get a top-level JSON field as string.
|
|
607
|
+
def json_string(json, field)
|
|
608
|
+
value = json[field]
|
|
609
|
+
value.nil? ? nil : value.to_s
|
|
610
|
+
end
|
|
611
|
+
private :json_string
|
|
233
612
|
|
|
234
613
|
# Encode data for multipart POST
|
|
235
614
|
def encode_multipart_form_data
|
|
236
|
-
data =
|
|
615
|
+
data = String.new # binary (ASCII-8BIT) buffer
|
|
237
616
|
|
|
238
617
|
# encode regular parameters
|
|
239
618
|
@parameters.each do |key, value|
|
|
240
|
-
|
|
619
|
+
next if value.nil?
|
|
620
|
+
|
|
621
|
+
data << "--#{MULTIPART_FORM_DATA_BOUNDARY}#{NEW_LINE}".b
|
|
622
|
+
data << "Content-Disposition: form-data; name=\"#{key}\"#{NEW_LINE}".b
|
|
623
|
+
data << NEW_LINE.b
|
|
624
|
+
data << value.to_s.b
|
|
625
|
+
data << NEW_LINE.b
|
|
241
626
|
end
|
|
242
627
|
|
|
243
628
|
# encode files
|
|
244
629
|
@files.each do |key, value|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
end
|
|
630
|
+
data << "--#{MULTIPART_FORM_DATA_BOUNDARY}#{NEW_LINE}".b
|
|
631
|
+
data << "Content-Disposition: form-data; name=\"#{key}\"; filename=\"#{value}\"#{NEW_LINE}".b
|
|
632
|
+
data << "Content-Type: application/octet-stream#{NEW_LINE}".b
|
|
633
|
+
data << NEW_LINE.b
|
|
634
|
+
data << File.binread(value)
|
|
635
|
+
data << NEW_LINE.b
|
|
252
636
|
end
|
|
253
637
|
|
|
254
638
|
# encode additional binary data
|
|
255
639
|
@binary_data.each do |key, value|
|
|
256
|
-
data <<
|
|
257
|
-
data <<
|
|
258
|
-
data <<
|
|
259
|
-
data <<
|
|
260
|
-
data << value.
|
|
640
|
+
data << "--#{MULTIPART_FORM_DATA_BOUNDARY}#{NEW_LINE}".b
|
|
641
|
+
data << "Content-Disposition: form-data; name=\"#{key}\"; filename=\"#{key}\"#{NEW_LINE}".b
|
|
642
|
+
data << "Content-Type: application/octet-stream#{NEW_LINE}".b
|
|
643
|
+
data << NEW_LINE.b
|
|
644
|
+
data << value.to_s.b
|
|
645
|
+
data << NEW_LINE.b
|
|
261
646
|
end
|
|
262
647
|
|
|
263
648
|
# final boundary
|
|
264
|
-
data <<
|
|
265
|
-
data << ''
|
|
649
|
+
data << "--#{MULTIPART_FORM_DATA_BOUNDARY}--#{NEW_LINE}".b
|
|
266
650
|
|
|
267
|
-
data
|
|
651
|
+
data
|
|
268
652
|
end
|
|
269
653
|
private :encode_multipart_form_data
|
|
270
654
|
|
|
@@ -287,6 +671,18 @@ module SelectPdf
|
|
|
287
671
|
@job_id
|
|
288
672
|
end
|
|
289
673
|
protected :start_async_job_multipart_form_data
|
|
674
|
+
|
|
675
|
+
# Copy the results of a finished asynchronous job (number of pages and response telemetry) into this client.
|
|
676
|
+
#
|
|
677
|
+
# @param async_job_client The async job client that retrieved the result.
|
|
678
|
+
def copy_async_job_results(async_job_client)
|
|
679
|
+
@number_of_pages = async_job_client.number_of_pages
|
|
680
|
+
@credits_total = async_job_client.credits_total unless async_job_client.credits_total.nil?
|
|
681
|
+
@credits_remaining = async_job_client.credits_remaining unless async_job_client.credits_remaining.nil?
|
|
682
|
+
@mode = async_job_client.mode unless async_job_client.mode.empty?
|
|
683
|
+
@execution_mode = async_job_client.execution_mode unless async_job_client.execution_mode.empty?
|
|
684
|
+
end
|
|
685
|
+
protected :copy_async_job_results
|
|
290
686
|
end
|
|
291
687
|
|
|
292
688
|
# Get usage details for SelectPdf Online API.
|
|
@@ -377,6 +773,9 @@ module SelectPdf
|
|
|
377
773
|
|
|
378
774
|
# Blink rendering engine.
|
|
379
775
|
BLINK = 'Blink'
|
|
776
|
+
|
|
777
|
+
# Chromium rendering engine.
|
|
778
|
+
CHROMIUM = 'Chromium'
|
|
380
779
|
end
|
|
381
780
|
|
|
382
781
|
# Protocol used for secure (HTTPS) connections.
|
|
@@ -466,20 +865,241 @@ module SelectPdf
|
|
|
466
865
|
HTML = 1
|
|
467
866
|
end
|
|
468
867
|
|
|
868
|
+
# PDF conformance target for the generated document.
|
|
869
|
+
# Tagged standards (PdfA3A) require the Blink or Chromium rendering engine.
|
|
870
|
+
# When no engine is specified the API promotes the request to Chromium and reports the engine used in the X-SelectPdf-Engine response header.
|
|
871
|
+
class PdfStandard
|
|
872
|
+
# The complete PDF feature set. Default.
|
|
873
|
+
FULL = 'Full'
|
|
874
|
+
|
|
875
|
+
# PDF/A - long term archiving.
|
|
876
|
+
PDF_A = 'PdfA'
|
|
877
|
+
|
|
878
|
+
# PDF/A-2B - long term archiving, transparencies allowed.
|
|
879
|
+
PDF_A2B = 'PdfA2B'
|
|
880
|
+
|
|
881
|
+
# PDF/A-3A - the accessible level of PDF/A-3. Implies a tagged document and can carry a ZUGFeRD / Factur-X electronic invoice.
|
|
882
|
+
PDF_A3A = 'PdfA3A'
|
|
883
|
+
|
|
884
|
+
# PDF/A-3B - long term archiving with arbitrary embedded files. Can carry a ZUGFeRD / Factur-X electronic invoice.
|
|
885
|
+
PDF_A3B = 'PdfA3B'
|
|
886
|
+
|
|
887
|
+
# PDF/A-3U - PDF/A-3B with Unicode mapping for all text. Can carry a ZUGFeRD / Factur-X electronic invoice.
|
|
888
|
+
PDF_A3U = 'PdfA3U'
|
|
889
|
+
|
|
890
|
+
# PDF/X - graphics exchange.
|
|
891
|
+
PDF_X = 'PdfX'
|
|
892
|
+
|
|
893
|
+
# PDF/SiqQ Level A - suitable for digital signatures, external links disabled.
|
|
894
|
+
PDF_SIQQ_A = 'PdfSiqQ_A'
|
|
895
|
+
|
|
896
|
+
# PDF/SiqQ Level B - suitable for digital signatures.
|
|
897
|
+
PDF_SIQQ_B = 'PdfSiqQ_B'
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
# The data profile of a ZUGFeRD / Factur-X hybrid electronic invoice.
|
|
901
|
+
# The profile determines how much of the EN 16931 semantic model the embedded XML carries.
|
|
902
|
+
class ZugferdProfile
|
|
903
|
+
# MINIMUM - accounting information only. Not a complete invoice.
|
|
904
|
+
MINIMUM = 'Minimum'
|
|
905
|
+
|
|
906
|
+
# BASIC WL - header and footer data without invoice lines. Not a complete invoice.
|
|
907
|
+
BASIC_WL = 'Basic_WL'
|
|
908
|
+
|
|
909
|
+
# BASIC - a subset of EN 16931 covering simple invoices, with lines.
|
|
910
|
+
BASIC = 'Basic'
|
|
911
|
+
|
|
912
|
+
# EN 16931 (formerly COMFORT) - the full European semantic standard.
|
|
913
|
+
EN16931 = 'En16931'
|
|
914
|
+
|
|
915
|
+
# EXTENDED - EN 16931 plus additional business terms.
|
|
916
|
+
EXTENDED = 'Extended'
|
|
917
|
+
|
|
918
|
+
# XRECHNUNG - the German public-sector reference profile. The embedded file is named xrechnung.xml instead of factur-x.xml.
|
|
919
|
+
XRECHNUNG = 'XRechnung'
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
# How the embedded invoice XML relates to the visible invoice page.
|
|
923
|
+
# When not set, the API derives this from the profile: Alternative for Minimum and Basic_WL, Data for the rest.
|
|
924
|
+
# Minimum and Basic_WL combined with Data are rejected, because those profiles do not carry a complete invoice.
|
|
925
|
+
class ZugferdRelationship
|
|
926
|
+
# The XML and the visible page carry exactly the same invoice content.
|
|
927
|
+
# Mandatory in Germany for the Basic, En16931, Extended and XRechnung profiles.
|
|
928
|
+
DATA = 'Data'
|
|
929
|
+
|
|
930
|
+
# The visible page carries more than the XML does - always the case for the Minimum and Basic_WL profiles - or the page was generated from the XML.
|
|
931
|
+
ALTERNATIVE = 'Alternative'
|
|
932
|
+
|
|
933
|
+
# The XML is the source the visible page was produced from.
|
|
934
|
+
SOURCE = 'Source'
|
|
935
|
+
|
|
936
|
+
# The XML supplements the visible page.
|
|
937
|
+
SUPPLEMENT = 'Supplement'
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
# The metadata schema used to identify a hybrid invoice inside the PDF.
|
|
941
|
+
class ZugferdSchema
|
|
942
|
+
# Factur-X 1.0 / ZUGFeRD 2.x - the current schema. Default.
|
|
943
|
+
FACTUR_X_10 = 'FacturX10'
|
|
944
|
+
|
|
945
|
+
# ZUGFeRD 2.0 - the legacy schema, deprecated but still accepted. Use only for recipients that explicitly require it.
|
|
946
|
+
ZUGFERD_20 = 'Zugferd20'
|
|
947
|
+
end
|
|
948
|
+
|
|
469
949
|
# Html To Pdf Conversion with SelectPdf Online API.
|
|
470
950
|
#
|
|
471
951
|
# Code sample:
|
|
472
952
|
#
|
|
473
|
-
#
|
|
953
|
+
# require 'selectpdf'
|
|
954
|
+
#
|
|
955
|
+
# $stdout.sync = true
|
|
956
|
+
#
|
|
957
|
+
# print "This is SelectPdf-#{SelectPdf::CLIENT_VERSION}\n"
|
|
958
|
+
#
|
|
959
|
+
# url = 'https://selectpdf.com'
|
|
960
|
+
# local_file = 'Test.pdf'
|
|
961
|
+
# api_key = 'Your API key here'
|
|
962
|
+
#
|
|
963
|
+
# begin
|
|
964
|
+
# client = SelectPdf::HtmlToPdfClient.new(api_key)
|
|
965
|
+
#
|
|
966
|
+
# # set parameters - see full list at https://selectpdf.com/html-to-pdf-api/
|
|
967
|
+
#
|
|
968
|
+
# client.page_size = SelectPdf::PageSize::A4 # PDF page size
|
|
969
|
+
# client.page_orientation = SelectPdf::PageOrientation::PORTRAIT # PDF page orientation
|
|
970
|
+
# client.margins = 0 # PDF page margins
|
|
971
|
+
# client.rendering_engine = SelectPdf::RenderingEngine::WEBKIT # rendering engine
|
|
972
|
+
# client.conversion_delay = 1 # conversion delay
|
|
973
|
+
# client.navigation_timeout = 30 # navigation timeout
|
|
974
|
+
# client.page_numbers = false # page numbers
|
|
975
|
+
# client.page_breaks_enhanced_algorithm = true # enhanced page break algorithm
|
|
976
|
+
#
|
|
977
|
+
# # additional properties
|
|
978
|
+
#
|
|
979
|
+
# # client.use_css_print = true # enable CSS media print
|
|
980
|
+
# # client.disable_javascript = true # disable javascript
|
|
981
|
+
# # client.disable_internal_links = true # disable internal links
|
|
982
|
+
# # client.disable_external_links = true # disable external links
|
|
983
|
+
# # client.keep_images_together = true # keep images together
|
|
984
|
+
# # client.scale_images = true # scale images to create smaller pdfs
|
|
985
|
+
# # client.single_page_pdf = true # generate a single page PDF
|
|
986
|
+
# # client.user_password = 'password' # secure the PDF with a password
|
|
987
|
+
#
|
|
988
|
+
# # generate automatic bookmarks
|
|
989
|
+
#
|
|
990
|
+
# # client.pdf_bookmarks_selectors = 'H1, H2' # create outlines (bookmarks) for the specified elements
|
|
991
|
+
# # client.viewer_page_mode = SelectPdf::PageMode::USE_OUTLINES # display outlines (bookmarks) in viewer
|
|
992
|
+
#
|
|
993
|
+
# print "Starting conversion ...\n"
|
|
994
|
+
#
|
|
995
|
+
# # convert url to file
|
|
996
|
+
# client.convert_url_to_file(url, local_file)
|
|
997
|
+
#
|
|
998
|
+
# # convert url to memory
|
|
999
|
+
# # pdf = client.convert_url(url)
|
|
1000
|
+
#
|
|
1001
|
+
# # convert html string to file
|
|
1002
|
+
# # client.convert_html_string_to_file('This is some <b>html</b>.', local_file)
|
|
1003
|
+
#
|
|
1004
|
+
# # convert html string to memory
|
|
1005
|
+
# # pdf = client.convert_html_string('This is some <b>html</b>.')
|
|
1006
|
+
#
|
|
1007
|
+
# print "Finished! Number of pages: #{client.number_of_pages}.\n"
|
|
1008
|
+
#
|
|
1009
|
+
# # get API usage
|
|
1010
|
+
# usage_client = SelectPdf::UsageClient.new(api_key)
|
|
1011
|
+
# usage = usage_client.get_usage(false)
|
|
1012
|
+
# print("Usage: #{usage}\n")
|
|
1013
|
+
# print('Conversions remained this month: ', usage['available'], "\n")
|
|
1014
|
+
# rescue SelectPdf::ApiException => e
|
|
1015
|
+
# print("An error occurred: #{e}")
|
|
1016
|
+
# end
|
|
474
1017
|
class HtmlToPdfClient < ApiClient
|
|
1018
|
+
# Names of the parameters the demo endpoint clamped on the most recent conversion (e.g. ["max_load_time", "engine"]),
|
|
1019
|
+
# parsed from the X-SelectPdf-Demo-Clamped response header.
|
|
1020
|
+
# Empty array if nothing was clamped or for non-demo responses. "Clamped" means the value was modified (capped, force-set), not discarded.
|
|
1021
|
+
attr_reader :clamped_fields
|
|
1022
|
+
|
|
1023
|
+
# Names of the parameters the demo endpoint silently dropped on the most recent conversion (e.g. ["auth_username", "cookies"]),
|
|
1024
|
+
# parsed from the X-SelectPdf-Demo-Dropped response header.
|
|
1025
|
+
# The demo endpoint refuses to honor a small set of fields for safety reasons - auth credentials, cookies, raw_parameters, pdf_name, async.
|
|
1026
|
+
# Empty array if nothing was dropped or for non-demo responses. Clamped = value modified, dropped = value thrown away.
|
|
1027
|
+
attr_reader :dropped_fields
|
|
1028
|
+
|
|
475
1029
|
# Construct the Html To Pdf Client.
|
|
476
1030
|
#
|
|
477
|
-
#
|
|
478
|
-
|
|
1031
|
+
# Pass a paid API key for production use. Pass nil, empty string, or "demo" (case-insensitive) to use the keyless demo endpoint -
|
|
1032
|
+
# output is watermarked and capped at 5 pages, but no signup is required.
|
|
1033
|
+
#
|
|
1034
|
+
# @param api_key API Key. Defaults to nil, which selects demo mode. Pass a real key for full unwatermarked output.
|
|
1035
|
+
def initialize(api_key = nil)
|
|
479
1036
|
super()
|
|
480
|
-
|
|
481
|
-
@
|
|
1037
|
+
|
|
1038
|
+
@clamped_fields = []
|
|
1039
|
+
@dropped_fields = []
|
|
1040
|
+
|
|
1041
|
+
@demo_mode = api_key.nil? || api_key.empty? || api_key.strip.casecmp('demo').zero?
|
|
1042
|
+
|
|
1043
|
+
if @demo_mode
|
|
1044
|
+
# Demo is keyless - the key parameter is not sent.
|
|
1045
|
+
@api_endpoint = 'https://selectpdf.com/api2/convert/demo/'
|
|
1046
|
+
else
|
|
1047
|
+
@api_endpoint = 'https://selectpdf.com/api2/convert/'
|
|
1048
|
+
@parameters['key'] = api_key
|
|
1049
|
+
end
|
|
1050
|
+
end
|
|
1051
|
+
|
|
1052
|
+
# True if the client was constructed for the keyless demo endpoint (api_key was nil, empty, or "demo").
|
|
1053
|
+
# Set at construction time and stable for the lifetime of the client. Changing api_endpoint does NOT change this flag.
|
|
1054
|
+
#
|
|
1055
|
+
# @return True if the client is in demo mode.
|
|
1056
|
+
def demo_mode?
|
|
1057
|
+
@demo_mode
|
|
1058
|
+
end
|
|
1059
|
+
|
|
1060
|
+
# True if the most recent response was tagged X-SelectPdf-Mode: demo (i.e. the request actually landed on a demo endpoint).
|
|
1061
|
+
#
|
|
1062
|
+
# @return True if the last response came from the demo endpoint.
|
|
1063
|
+
def demo_response?
|
|
1064
|
+
!@mode.nil? && @mode.casecmp('demo').zero?
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
# True if the most recent response had any clamped fields.
|
|
1068
|
+
#
|
|
1069
|
+
# @return True if any field was clamped.
|
|
1070
|
+
def was_clamped?
|
|
1071
|
+
!@clamped_fields.nil? && !@clamped_fields.empty?
|
|
1072
|
+
end
|
|
1073
|
+
|
|
1074
|
+
# True if the most recent response reported any dropped fields.
|
|
1075
|
+
#
|
|
1076
|
+
# @return True if any field was dropped.
|
|
1077
|
+
def was_any_field_dropped?
|
|
1078
|
+
!@dropped_fields.nil? && !@dropped_fields.empty?
|
|
1079
|
+
end
|
|
1080
|
+
|
|
1081
|
+
# Capture demo-specific response headers (the demo-clamped fields list and the demo-dropped fields list).
|
|
1082
|
+
#
|
|
1083
|
+
# @param response HTTP response.
|
|
1084
|
+
def on_response_headers_received(response)
|
|
1085
|
+
@clamped_fields = split_fields(response['X-SelectPdf-Demo-Clamped'])
|
|
1086
|
+
@dropped_fields = split_fields(response['X-SelectPdf-Demo-Dropped'])
|
|
482
1087
|
end
|
|
1088
|
+
protected :on_response_headers_received
|
|
1089
|
+
|
|
1090
|
+
# Split a comma separated header value.
|
|
1091
|
+
def split_fields(raw)
|
|
1092
|
+
return [] if raw.nil? || raw.empty?
|
|
1093
|
+
|
|
1094
|
+
raw.split(',', -1).map(&:strip)
|
|
1095
|
+
end
|
|
1096
|
+
private :split_fields
|
|
1097
|
+
|
|
1098
|
+
# Raise DemoUnsupportedException for asynchronous calls in demo mode (the demo endpoint is synchronous only).
|
|
1099
|
+
def ensure_async_supported
|
|
1100
|
+
raise DemoUnsupportedException.new(0, 'async') if @demo_mode
|
|
1101
|
+
end
|
|
1102
|
+
private :ensure_async_supported
|
|
483
1103
|
|
|
484
1104
|
# Convert the specified url to PDF.
|
|
485
1105
|
# SelectPdf online API can convert http:// and https:// publicly available urls.
|
|
@@ -559,7 +1179,10 @@ module SelectPdf
|
|
|
559
1179
|
#
|
|
560
1180
|
# @param url Address of the web page being converted.
|
|
561
1181
|
# @return The resulted pdf.
|
|
1182
|
+
# Raises DemoUnsupportedException in demo mode (the demo endpoint does not run asynchronous jobs).
|
|
562
1183
|
def convert_url_async(url)
|
|
1184
|
+
ensure_async_supported
|
|
1185
|
+
|
|
563
1186
|
if !url.downcase.start_with?('http://') && !url.downcase.start_with?('https://')
|
|
564
1187
|
raise ApiException.new('The supported protocols for the converted webpage are http:// and https://.'), 'The supported protocols for the converted webpage are http:// and https://.'
|
|
565
1188
|
end
|
|
@@ -593,7 +1216,7 @@ module SelectPdf
|
|
|
593
1216
|
|
|
594
1217
|
next if result.nil?
|
|
595
1218
|
|
|
596
|
-
|
|
1219
|
+
copy_async_job_results(async_job_client)
|
|
597
1220
|
return result
|
|
598
1221
|
end
|
|
599
1222
|
|
|
@@ -677,7 +1300,10 @@ module SelectPdf
|
|
|
677
1300
|
#
|
|
678
1301
|
# @param html_string HTML string with the content being converted.
|
|
679
1302
|
# @param base_url Base url used to resolve relative paths to resources (css, images, javascript, etc). Must be a http:// or https:// publicly available url.
|
|
1303
|
+
# Raises DemoUnsupportedException in demo mode (the demo endpoint does not run asynchronous jobs).
|
|
680
1304
|
def convert_html_string_with_base_url_async(html_string, base_url)
|
|
1305
|
+
ensure_async_supported
|
|
1306
|
+
|
|
681
1307
|
@parameters.delete('url')
|
|
682
1308
|
@parameters['html'] = html_string
|
|
683
1309
|
@parameters['base_url'] = base_url unless base_url.nil? || base_url.empty?
|
|
@@ -703,7 +1329,7 @@ module SelectPdf
|
|
|
703
1329
|
|
|
704
1330
|
next if result.nil?
|
|
705
1331
|
|
|
706
|
-
|
|
1332
|
+
copy_async_job_results(async_job_client)
|
|
707
1333
|
return result
|
|
708
1334
|
end
|
|
709
1335
|
|
|
@@ -867,26 +1493,68 @@ module SelectPdf
|
|
|
867
1493
|
|
|
868
1494
|
# Set the rendering engine used for the HTML to PDF conversion. Default value is WebKit.
|
|
869
1495
|
#
|
|
870
|
-
# @param rendering_engine HTML rendering engine. Use constants from SelectPdf::RenderingEngine class.
|
|
1496
|
+
# @param rendering_engine HTML rendering engine. Possible values: WebKit, Restricted, Blink, Chromium. Use constants from SelectPdf::RenderingEngine class.
|
|
871
1497
|
def rendering_engine=(rendering_engine)
|
|
872
|
-
unless /(?i)^(WebKit|Restricted|Blink)$/.match(rendering_engine)
|
|
873
|
-
raise ApiException.new('Allowed values for Rendering Engine: WebKit, Restricted, Blink.'), 'Allowed values for Rendering Engine: WebKit, Restricted, Blink.'
|
|
1498
|
+
unless /(?i)^(WebKit|Restricted|Blink|Chromium)$/.match(rendering_engine)
|
|
1499
|
+
raise ApiException.new('Allowed values for Rendering Engine: WebKit, Restricted, Blink, Chromium.'), 'Allowed values for Rendering Engine: WebKit, Restricted, Blink, Chromium.'
|
|
874
1500
|
end
|
|
875
1501
|
|
|
876
1502
|
@parameters['engine'] = rendering_engine
|
|
877
1503
|
end
|
|
878
1504
|
|
|
1505
|
+
# Produce a tagged, accessible PDF: a logical structure tree covering headings, paragraphs, lists, tables,
|
|
1506
|
+
# figures with alternate text, links and reading order. The default value is False.
|
|
1507
|
+
#
|
|
1508
|
+
# Requires the Blink or Chromium rendering engine - the WebKit engines cannot produce a structure tree.
|
|
1509
|
+
# If no engine is set, the API promotes the request to Chromium and reports it in the X-SelectPdf-Engine response header.
|
|
1510
|
+
# Setting an explicit WebKit engine together with tagged output is rejected by the API.
|
|
1511
|
+
# A tagged document also needs a title, so set doc_title - the converter falls back to the HTML document title when it is not set.
|
|
1512
|
+
#
|
|
1513
|
+
# @param tagged Produce a tagged, accessible PDF or not.
|
|
1514
|
+
def tagged=(tagged)
|
|
1515
|
+
@parameters['tagged'] = tagged
|
|
1516
|
+
end
|
|
1517
|
+
|
|
1518
|
+
# Set the PDF conformance target - PDF/A for archiving, PDF/X for graphics exchange, PDF/SiqQ for digital signatures. The default value is Full.
|
|
1519
|
+
#
|
|
1520
|
+
# PdfA3A is the accessible level of PDF/A-3: it implies a tagged document, so it carries the same rendering-engine requirement as tagged.
|
|
1521
|
+
#
|
|
1522
|
+
# @param pdf_standard PDF conformance target. Possible values: Full, PdfA, PdfA2B, PdfA3A, PdfA3B, PdfA3U, PdfX, PdfSiqQ_A, PdfSiqQ_B.
|
|
1523
|
+
# Use constants from SelectPdf::PdfStandard class.
|
|
1524
|
+
def pdf_standard=(pdf_standard)
|
|
1525
|
+
unless /(?i)^(Full|PdfA|PdfA2B|PdfA3A|PdfA3B|PdfA3U|PdfX|PdfSiqQ_A|PdfSiqQ_B)$/.match(pdf_standard)
|
|
1526
|
+
raise ApiException.new('Allowed values for Pdf Standard: Full, PdfA, PdfA2B, PdfA3A, PdfA3B, PdfA3U, PdfX, PdfSiqQ_A, PdfSiqQ_B.'),
|
|
1527
|
+
'Allowed values for Pdf Standard: Full, PdfA, PdfA2B, PdfA3A, PdfA3B, PdfA3U, PdfX, PdfSiqQ_A, PdfSiqQ_B.'
|
|
1528
|
+
end
|
|
1529
|
+
|
|
1530
|
+
@parameters['pdf_standard'] = pdf_standard
|
|
1531
|
+
end
|
|
1532
|
+
|
|
1533
|
+
# Set the natural language of the document, for example "en-US" or "de-DE".
|
|
1534
|
+
# Written as the PDF /Lang entry and onto tagged structure elements. The default value is "en-US".
|
|
1535
|
+
#
|
|
1536
|
+
# @param doc_language Language tag, for example "en-US".
|
|
1537
|
+
def doc_language=(doc_language)
|
|
1538
|
+
@parameters['doc_language'] = doc_language
|
|
1539
|
+
end
|
|
1540
|
+
|
|
879
1541
|
# Set PDF user password.
|
|
1542
|
+
# Not available in demo mode: raises DemoUnsupportedException if the client was constructed without an API key.
|
|
880
1543
|
#
|
|
881
1544
|
# @param user_password PDF user password.
|
|
882
1545
|
def user_password=(user_password)
|
|
1546
|
+
raise DemoUnsupportedException.new(0, 'user_password') if @demo_mode && !(user_password.nil? || user_password.empty?)
|
|
1547
|
+
|
|
883
1548
|
@parameters['user_password'] = user_password
|
|
884
1549
|
end
|
|
885
1550
|
|
|
886
1551
|
# Set PDF owner password.
|
|
1552
|
+
# Not available in demo mode: raises DemoUnsupportedException if the client was constructed without an API key.
|
|
887
1553
|
#
|
|
888
1554
|
# @param owner_password PDF owner password.
|
|
889
1555
|
def owner_password=(owner_password)
|
|
1556
|
+
raise DemoUnsupportedException.new(0, 'owner_password') if @demo_mode && !(owner_password.nil? || owner_password.empty?)
|
|
1557
|
+
|
|
890
1558
|
@parameters['owner_password'] = owner_password
|
|
891
1559
|
end
|
|
892
1560
|
|
|
@@ -905,6 +1573,17 @@ module SelectPdf
|
|
|
905
1573
|
@parameters['web_page_height'] = web_page_height
|
|
906
1574
|
end
|
|
907
1575
|
|
|
1576
|
+
# Leave out the content below the web page height (set with web_page_height) instead of letting the page flow onto further pages.
|
|
1577
|
+
#
|
|
1578
|
+
# When not set, each rendering engine keeps its own behavior: WebKit and WebKit Restricted leave the content out whenever a web page height is set,
|
|
1579
|
+
# Blink and Chromium convert the whole page. Set it to True or False to choose explicitly. It needs a non-zero web page height; with 0 there is no
|
|
1580
|
+
# height to fix the page at and the setting is ignored. With WebKit, a fixed size also cuts off content wider than the web page width.
|
|
1581
|
+
#
|
|
1582
|
+
# @param web_page_fixed_size True to cut the page at the web page height, False to convert the whole page.
|
|
1583
|
+
def web_page_fixed_size=(web_page_fixed_size)
|
|
1584
|
+
@parameters['web_page_fixed_size'] = web_page_fixed_size
|
|
1585
|
+
end
|
|
1586
|
+
|
|
908
1587
|
# Introduce a delay (in seconds) before the actual conversion to allow the web page to fully load. This property is an alias for conversion_delay.
|
|
909
1588
|
# The default value is 1 second. Use a larger value if the web page has content that takes time to render when it is displayed in the browser.
|
|
910
1589
|
#
|
|
@@ -1448,10 +2127,39 @@ module SelectPdf
|
|
|
1448
2127
|
end
|
|
1449
2128
|
|
|
1450
2129
|
# Set HTTP cookies for the web page being converted.
|
|
2130
|
+
# The demo endpoint does not send cookies; it reports the value in dropped_fields.
|
|
1451
2131
|
#
|
|
1452
|
-
# @param cookies HTTP cookies that will be sent to the page being converted.
|
|
2132
|
+
# @param cookies HTTP cookies that will be sent to the page being converted (a Hash of cookie names and values).
|
|
1453
2133
|
def cookies=(cookies)
|
|
1454
|
-
|
|
2134
|
+
# Serialized as name=value&name=value&..., each part percent-encoded (spaces as %20, not +), the same way the .NET client sends it.
|
|
2135
|
+
serialized = ''.dup
|
|
2136
|
+
cookies.each do |name, value|
|
|
2137
|
+
serialized << "#{encode_cookie_part(name)}=#{encode_cookie_part(value)}&"
|
|
2138
|
+
end
|
|
2139
|
+
|
|
2140
|
+
@parameters['cookies_string'] = serialized
|
|
2141
|
+
end
|
|
2142
|
+
|
|
2143
|
+
# Percent-encode a cookie name or value.
|
|
2144
|
+
def encode_cookie_part(part)
|
|
2145
|
+
URI.encode_www_form_component(part.to_s).gsub('+', '%20')
|
|
2146
|
+
end
|
|
2147
|
+
private :encode_cookie_part
|
|
2148
|
+
|
|
2149
|
+
# Set the user name for HTTP Basic authentication on the web page being converted.
|
|
2150
|
+
# Use it together with auth_password. The demo endpoint does not send credentials; it reports the value in dropped_fields.
|
|
2151
|
+
#
|
|
2152
|
+
# @param auth_username User name for HTTP Basic authentication.
|
|
2153
|
+
def auth_username=(auth_username)
|
|
2154
|
+
@parameters['auth_username'] = auth_username
|
|
2155
|
+
end
|
|
2156
|
+
|
|
2157
|
+
# Set the password for HTTP Basic authentication on the web page being converted.
|
|
2158
|
+
# Use it together with auth_username. The demo endpoint does not send credentials; it reports the value in dropped_fields.
|
|
2159
|
+
#
|
|
2160
|
+
# @param auth_password Password for HTTP Basic authentication.
|
|
2161
|
+
def auth_password=(auth_password)
|
|
2162
|
+
@parameters['auth_password'] = auth_password
|
|
1455
2163
|
end
|
|
1456
2164
|
|
|
1457
2165
|
# Set a custom parameter. Do not use this method unless advised by SelectPdf.
|
|
@@ -1473,21 +2181,395 @@ module SelectPdf
|
|
|
1473
2181
|
end
|
|
1474
2182
|
end
|
|
1475
2183
|
|
|
1476
|
-
#
|
|
1477
|
-
#
|
|
1478
|
-
|
|
1479
|
-
|
|
2184
|
+
# Create ZUGFeRD / Factur-X hybrid electronic invoices with SelectPdf Online API.
|
|
2185
|
+
#
|
|
2186
|
+
# A hybrid electronic invoice is one PDF/A-3 file carrying both halves of the invoice: the page a human reads,
|
|
2187
|
+
# and the XML a recipient's accounting system reads. This client converts a URL or an HTML string into the visible invoice
|
|
2188
|
+
# and embeds the XML into it as an associated file, with the metadata invoice software looks for.
|
|
2189
|
+
#
|
|
2190
|
+
# It derives from HtmlToPdfClient, so every conversion setting - page size, margins, headers, footers, rendering engine - applies here too.
|
|
2191
|
+
# Use the create_from_* methods rather than the inherited convert_* methods: the invoice endpoint takes a multipart request,
|
|
2192
|
+
# because the XML is uploaded as a file part.
|
|
2193
|
+
#
|
|
2194
|
+
# The carrier document must be PDF/A-3. The default is SelectPdf::PdfStandard::PDF_A3A, the accessible level, which the standards recommend
|
|
2195
|
+
# because it makes the visible invoice readable by assistive technology as well as archivable. Because PdfA3A is a tagged standard,
|
|
2196
|
+
# a request that does not set a rendering engine is promoted to Chromium by the API, which reports the engine used in the X-SelectPdf-Engine response header.
|
|
2197
|
+
#
|
|
2198
|
+
# There is no way to attach an invoice XML to an existing PDF you already have: the XML can only be embedded into a document created as PDF/A-3.
|
|
2199
|
+
#
|
|
2200
|
+
# Code sample:
|
|
2201
|
+
#
|
|
2202
|
+
# require 'selectpdf'
|
|
2203
|
+
#
|
|
2204
|
+
# api_key = 'Your API key here'
|
|
2205
|
+
# invoice_html = '<html><body><h1>Invoice INV-2026-001</h1></body></html>'
|
|
2206
|
+
# invoice_xml = 'factur-x.xml'
|
|
2207
|
+
# local_file = 'Invoice.pdf'
|
|
2208
|
+
#
|
|
2209
|
+
# begin
|
|
2210
|
+
# client = SelectPdf::InvoiceClient.new(api_key)
|
|
2211
|
+
#
|
|
2212
|
+
# client.invoice_xml_file = invoice_xml
|
|
2213
|
+
# client.zugferd_profile = SelectPdf::ZugferdProfile::EN16931
|
|
2214
|
+
# client.doc_title = 'Invoice INV-2026-001'
|
|
2215
|
+
#
|
|
2216
|
+
# client.create_from_html_string_to_file(invoice_html, local_file)
|
|
2217
|
+
#
|
|
2218
|
+
# print "Finished! Number of pages: #{client.number_of_pages}.\n"
|
|
2219
|
+
# rescue SelectPdf::ApiException => e
|
|
2220
|
+
# print("An error occurred: #{e}")
|
|
2221
|
+
# end
|
|
2222
|
+
class InvoiceClient < HtmlToPdfClient
|
|
2223
|
+
# The production endpoint for hybrid electronic invoices.
|
|
2224
|
+
INVOICE_ENDPOINT = 'https://selectpdf.com/api2/invoice/'
|
|
2225
|
+
|
|
2226
|
+
# Construct the Invoice Client.
|
|
2227
|
+
#
|
|
2228
|
+
# Unlike HtmlToPdfClient, this client has no demo mode - the keyless demo endpoint does not produce electronic invoices - so an API key is required.
|
|
2229
|
+
# Raises ApiException when no API key is supplied.
|
|
1480
2230
|
#
|
|
1481
2231
|
# @param api_key API Key.
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
@parameters['job_id'] = job_id
|
|
1488
|
-
end
|
|
2232
|
+
def initialize(api_key)
|
|
2233
|
+
if api_key.nil? || api_key.empty? || api_key.strip.downcase == 'demo'
|
|
2234
|
+
raise ApiException.new('An API key is required to create electronic invoices. The keyless demo endpoint does not support them.'),
|
|
2235
|
+
'An API key is required to create electronic invoices. The keyless demo endpoint does not support them.'
|
|
2236
|
+
end
|
|
1489
2237
|
|
|
1490
|
-
|
|
2238
|
+
super(api_key)
|
|
2239
|
+
|
|
2240
|
+
@api_endpoint = INVOICE_ENDPOINT
|
|
2241
|
+
|
|
2242
|
+
# The carrier has to be PDF/A-3; default to the accessible level, which the standards recommend. Overridable with pdf_standard.
|
|
2243
|
+
@parameters['pdf_standard'] = PdfStandard::PDF_A3A
|
|
2244
|
+
end
|
|
2245
|
+
|
|
2246
|
+
# Set the invoice XML from a local file.
|
|
2247
|
+
#
|
|
2248
|
+
# Only the content of the file is used - the name recorded inside the PDF is the one the standard prescribes
|
|
2249
|
+
# ("factur-x.xml", or "xrechnung.xml" for the XRECHNUNG profile), because recipients look it up by name.
|
|
2250
|
+
#
|
|
2251
|
+
# @param invoice_xml_file Path to the local invoice XML file.
|
|
2252
|
+
def invoice_xml_file=(invoice_xml_file)
|
|
2253
|
+
@binary_data.delete('zugferd_xml')
|
|
2254
|
+
@files['zugferd_xml'] = invoice_xml_file
|
|
2255
|
+
end
|
|
2256
|
+
|
|
2257
|
+
# Set the invoice XML from memory.
|
|
2258
|
+
#
|
|
2259
|
+
# @param invoice_xml The invoice XML, as a string or as raw bytes (a binary string). Text strings are sent UTF-8 encoded.
|
|
2260
|
+
def invoice_xml=(invoice_xml)
|
|
2261
|
+
data = invoice_xml.to_s
|
|
2262
|
+
data = data.encode('UTF-8') unless data.encoding == Encoding::BINARY || data.encoding == Encoding::UTF_8
|
|
2263
|
+
|
|
2264
|
+
@files.delete('zugferd_xml')
|
|
2265
|
+
@binary_data['zugferd_xml'] = data.b
|
|
2266
|
+
end
|
|
2267
|
+
|
|
2268
|
+
# Set the data profile of the invoice XML. Required.
|
|
2269
|
+
#
|
|
2270
|
+
# @param zugferd_profile The invoice data profile. Possible values: Minimum, Basic_WL, Basic, En16931, Extended, XRechnung.
|
|
2271
|
+
# Use constants from SelectPdf::ZugferdProfile class.
|
|
2272
|
+
def zugferd_profile=(zugferd_profile)
|
|
2273
|
+
unless /(?i)^(Minimum|Basic_WL|Basic|En16931|Extended|XRechnung)$/.match(zugferd_profile)
|
|
2274
|
+
raise ApiException.new('Allowed values for Zugferd Profile: Minimum, Basic_WL, Basic, En16931, Extended, XRechnung.'),
|
|
2275
|
+
'Allowed values for Zugferd Profile: Minimum, Basic_WL, Basic, En16931, Extended, XRechnung.'
|
|
2276
|
+
end
|
|
2277
|
+
|
|
2278
|
+
@parameters['zugferd_profile'] = zugferd_profile
|
|
2279
|
+
end
|
|
2280
|
+
|
|
2281
|
+
# Set how the embedded XML relates to the visible invoice page.
|
|
2282
|
+
#
|
|
2283
|
+
# Optional. When not set, the API derives it from the profile: Alternative for Minimum and Basic_WL, which do not carry a complete invoice,
|
|
2284
|
+
# and Data for the rest. Those two profiles combined with Data are rejected by the API.
|
|
2285
|
+
#
|
|
2286
|
+
# @param zugferd_relationship The relationship between XML and page. Possible values: Data, Alternative, Source, Supplement.
|
|
2287
|
+
# Use constants from SelectPdf::ZugferdRelationship class.
|
|
2288
|
+
def zugferd_relationship=(zugferd_relationship)
|
|
2289
|
+
unless /(?i)^(Data|Alternative|Source|Supplement)$/.match(zugferd_relationship)
|
|
2290
|
+
raise ApiException.new('Allowed values for Zugferd Relationship: Data, Alternative, Source, Supplement.'),
|
|
2291
|
+
'Allowed values for Zugferd Relationship: Data, Alternative, Source, Supplement.'
|
|
2292
|
+
end
|
|
2293
|
+
|
|
2294
|
+
@parameters['zugferd_relationship'] = zugferd_relationship
|
|
2295
|
+
end
|
|
2296
|
+
|
|
2297
|
+
# Set the metadata schema identifying the invoice. The default value is FacturX10.
|
|
2298
|
+
#
|
|
2299
|
+
# @param zugferd_schema The invoice metadata schema. Possible values: FacturX10, Zugferd20. Use constants from SelectPdf::ZugferdSchema class.
|
|
2300
|
+
def zugferd_schema=(zugferd_schema)
|
|
2301
|
+
unless /(?i)^(FacturX10|Zugferd20)$/.match(zugferd_schema)
|
|
2302
|
+
raise ApiException.new('Allowed values for Zugferd Schema: FacturX10, Zugferd20.'), 'Allowed values for Zugferd Schema: FacturX10, Zugferd20.'
|
|
2303
|
+
end
|
|
2304
|
+
|
|
2305
|
+
@parameters['zugferd_schema'] = zugferd_schema
|
|
2306
|
+
end
|
|
2307
|
+
|
|
2308
|
+
# Create a hybrid electronic invoice from the invoice page at the specified url.
|
|
2309
|
+
#
|
|
2310
|
+
# @param url Url of the invoice page.
|
|
2311
|
+
# @return The resulted hybrid invoice PDF.
|
|
2312
|
+
def create_from_url(url)
|
|
2313
|
+
prepare_invoice_url(url)
|
|
2314
|
+
@parameters['async'] = 'False'
|
|
2315
|
+
|
|
2316
|
+
perform_post_as_multipart_formdata
|
|
2317
|
+
end
|
|
2318
|
+
|
|
2319
|
+
# Create a hybrid electronic invoice from the invoice page at the specified url and write it to an output stream.
|
|
2320
|
+
#
|
|
2321
|
+
# @param url Url of the invoice page.
|
|
2322
|
+
# @param stream The output stream where the resulted PDF will be written.
|
|
2323
|
+
def create_from_url_to_stream(url, stream)
|
|
2324
|
+
prepare_invoice_url(url)
|
|
2325
|
+
@parameters['async'] = 'False'
|
|
2326
|
+
|
|
2327
|
+
perform_post_as_multipart_formdata(stream)
|
|
2328
|
+
end
|
|
2329
|
+
|
|
2330
|
+
# Create a hybrid electronic invoice from the invoice page at the specified url and write it to a local file.
|
|
2331
|
+
#
|
|
2332
|
+
# @param url Url of the invoice page.
|
|
2333
|
+
# @param file_path Local file including path if necessary.
|
|
2334
|
+
def create_from_url_to_file(url, file_path)
|
|
2335
|
+
write_invoice_to_file(file_path) do |file|
|
|
2336
|
+
create_from_url_to_stream(url, file)
|
|
2337
|
+
end
|
|
2338
|
+
end
|
|
2339
|
+
|
|
2340
|
+
# Create a hybrid electronic invoice from a raw HTML string.
|
|
2341
|
+
#
|
|
2342
|
+
# @param html_string The invoice HTML.
|
|
2343
|
+
# @return The resulted hybrid invoice PDF.
|
|
2344
|
+
def create_from_html_string(html_string)
|
|
2345
|
+
create_from_html_string_with_base_url(html_string, nil)
|
|
2346
|
+
end
|
|
2347
|
+
|
|
2348
|
+
# Create a hybrid electronic invoice from a raw HTML string. Use a base url to resolve relative paths to resources.
|
|
2349
|
+
#
|
|
2350
|
+
# @param html_string The invoice HTML.
|
|
2351
|
+
# @param base_url Base url used to resolve relative paths to resources (css, images, javascript, etc). Must be a http:// or https:// publicly available url.
|
|
2352
|
+
# @return The resulted hybrid invoice PDF.
|
|
2353
|
+
def create_from_html_string_with_base_url(html_string, base_url)
|
|
2354
|
+
prepare_invoice_html(html_string, base_url)
|
|
2355
|
+
@parameters['async'] = 'False'
|
|
2356
|
+
|
|
2357
|
+
perform_post_as_multipart_formdata
|
|
2358
|
+
end
|
|
2359
|
+
|
|
2360
|
+
# Create a hybrid electronic invoice from a raw HTML string and write it to an output stream.
|
|
2361
|
+
#
|
|
2362
|
+
# @param html_string The invoice HTML.
|
|
2363
|
+
# @param stream The output stream where the resulted PDF will be written.
|
|
2364
|
+
def create_from_html_string_to_stream(html_string, stream)
|
|
2365
|
+
create_from_html_string_to_stream_with_base_url(html_string, nil, stream)
|
|
2366
|
+
end
|
|
2367
|
+
|
|
2368
|
+
# Create a hybrid electronic invoice from a raw HTML string and write it to an output stream. Use a base url to resolve relative paths to resources.
|
|
2369
|
+
#
|
|
2370
|
+
# @param html_string The invoice HTML.
|
|
2371
|
+
# @param base_url Base url used to resolve relative paths to resources (css, images, javascript, etc). Must be a http:// or https:// publicly available url.
|
|
2372
|
+
# @param stream The output stream where the resulted PDF will be written.
|
|
2373
|
+
def create_from_html_string_to_stream_with_base_url(html_string, base_url, stream)
|
|
2374
|
+
prepare_invoice_html(html_string, base_url)
|
|
2375
|
+
@parameters['async'] = 'False'
|
|
2376
|
+
|
|
2377
|
+
perform_post_as_multipart_formdata(stream)
|
|
2378
|
+
end
|
|
2379
|
+
|
|
2380
|
+
# Create a hybrid electronic invoice from a raw HTML string and write it to a local file.
|
|
2381
|
+
#
|
|
2382
|
+
# @param html_string The invoice HTML.
|
|
2383
|
+
# @param file_path Local file including path if necessary.
|
|
2384
|
+
def create_from_html_string_to_file(html_string, file_path)
|
|
2385
|
+
create_from_html_string_with_base_url_to_file(html_string, nil, file_path)
|
|
2386
|
+
end
|
|
2387
|
+
|
|
2388
|
+
# Create a hybrid electronic invoice from a raw HTML string and write it to a local file. Use a base url to resolve relative paths to resources.
|
|
2389
|
+
#
|
|
2390
|
+
# @param html_string The invoice HTML.
|
|
2391
|
+
# @param base_url Base url used to resolve relative paths to resources (css, images, javascript, etc). Must be a http:// or https:// publicly available url.
|
|
2392
|
+
# @param file_path Local file including path if necessary.
|
|
2393
|
+
def create_from_html_string_with_base_url_to_file(html_string, base_url, file_path)
|
|
2394
|
+
write_invoice_to_file(file_path) do |file|
|
|
2395
|
+
create_from_html_string_to_stream_with_base_url(html_string, base_url, file)
|
|
2396
|
+
end
|
|
2397
|
+
end
|
|
2398
|
+
|
|
2399
|
+
# Create a hybrid electronic invoice from the invoice page at the specified url, using an asynchronous call.
|
|
2400
|
+
# Recommended for long invoice pages or callers that cannot hold an HTTP connection open for the whole conversion.
|
|
2401
|
+
#
|
|
2402
|
+
# @param url Url of the invoice page.
|
|
2403
|
+
# @return The resulted hybrid invoice PDF.
|
|
2404
|
+
def create_from_url_async(url)
|
|
2405
|
+
prepare_invoice_url(url)
|
|
2406
|
+
run_invoice_async_job
|
|
2407
|
+
end
|
|
2408
|
+
|
|
2409
|
+
# Create a hybrid electronic invoice from the invoice page at the specified url, using an asynchronous call, and write it to an output stream.
|
|
2410
|
+
#
|
|
2411
|
+
# @param url Url of the invoice page.
|
|
2412
|
+
# @param stream The output stream where the resulted PDF will be written.
|
|
2413
|
+
def create_from_url_to_stream_async(url, stream)
|
|
2414
|
+
result = create_from_url_async(url)
|
|
2415
|
+
stream.write(result)
|
|
2416
|
+
end
|
|
2417
|
+
|
|
2418
|
+
# Create a hybrid electronic invoice from the invoice page at the specified url, using an asynchronous call, and write it to a local file.
|
|
2419
|
+
#
|
|
2420
|
+
# @param url Url of the invoice page.
|
|
2421
|
+
# @param file_path Local file including path if necessary.
|
|
2422
|
+
def create_from_url_to_file_async(url, file_path)
|
|
2423
|
+
result = create_from_url_async(url)
|
|
2424
|
+
File.open(file_path, 'wb') do |file|
|
|
2425
|
+
file.write(result)
|
|
2426
|
+
end
|
|
2427
|
+
end
|
|
2428
|
+
|
|
2429
|
+
# Create a hybrid electronic invoice from a raw HTML string, using an asynchronous call.
|
|
2430
|
+
#
|
|
2431
|
+
# @param html_string The invoice HTML.
|
|
2432
|
+
# @return The resulted hybrid invoice PDF.
|
|
2433
|
+
def create_from_html_string_async(html_string)
|
|
2434
|
+
create_from_html_string_with_base_url_async(html_string, nil)
|
|
2435
|
+
end
|
|
2436
|
+
|
|
2437
|
+
# Create a hybrid electronic invoice from a raw HTML string, using an asynchronous call. Use a base url to resolve relative paths to resources.
|
|
2438
|
+
#
|
|
2439
|
+
# @param html_string The invoice HTML.
|
|
2440
|
+
# @param base_url Base url used to resolve relative paths to resources (css, images, javascript, etc). Must be a http:// or https:// publicly available url.
|
|
2441
|
+
# @return The resulted hybrid invoice PDF.
|
|
2442
|
+
def create_from_html_string_with_base_url_async(html_string, base_url)
|
|
2443
|
+
prepare_invoice_html(html_string, base_url)
|
|
2444
|
+
run_invoice_async_job
|
|
2445
|
+
end
|
|
2446
|
+
|
|
2447
|
+
# Create a hybrid electronic invoice from a raw HTML string, using an asynchronous call, and write it to an output stream.
|
|
2448
|
+
#
|
|
2449
|
+
# @param html_string The invoice HTML.
|
|
2450
|
+
# @param stream The output stream where the resulted PDF will be written.
|
|
2451
|
+
def create_from_html_string_to_stream_async(html_string, stream)
|
|
2452
|
+
result = create_from_html_string_async(html_string)
|
|
2453
|
+
stream.write(result)
|
|
2454
|
+
end
|
|
2455
|
+
|
|
2456
|
+
# Create a hybrid electronic invoice from a raw HTML string, using an asynchronous call, and write it to a local file.
|
|
2457
|
+
#
|
|
2458
|
+
# @param html_string The invoice HTML.
|
|
2459
|
+
# @param file_path Local file including path if necessary.
|
|
2460
|
+
def create_from_html_string_to_file_async(html_string, file_path)
|
|
2461
|
+
result = create_from_html_string_async(html_string)
|
|
2462
|
+
File.open(file_path, 'wb') do |file|
|
|
2463
|
+
file.write(result)
|
|
2464
|
+
end
|
|
2465
|
+
end
|
|
2466
|
+
|
|
2467
|
+
# Validate the url and set the url parameters.
|
|
2468
|
+
def prepare_invoice_url(url)
|
|
2469
|
+
if !url.downcase.start_with?('http://') && !url.downcase.start_with?('https://')
|
|
2470
|
+
raise ApiException.new('The supported protocols for the converted webpage are http:// and https://.'), 'The supported protocols for the converted webpage are http:// and https://.'
|
|
2471
|
+
end
|
|
2472
|
+
|
|
2473
|
+
if url.downcase.start_with?('http://localhost')
|
|
2474
|
+
raise ApiException.new('Cannot convert local urls. SelectPdf online API can only convert publicly available urls.'), 'Cannot convert local urls. SelectPdf online API can only convert publicly available urls.'
|
|
2475
|
+
end
|
|
2476
|
+
|
|
2477
|
+
require_invoice_xml
|
|
2478
|
+
|
|
2479
|
+
@parameters['url'] = url
|
|
2480
|
+
@parameters.delete('html')
|
|
2481
|
+
@parameters.delete('base_url')
|
|
2482
|
+
end
|
|
2483
|
+
private :prepare_invoice_url
|
|
2484
|
+
|
|
2485
|
+
# Set the html parameters.
|
|
2486
|
+
def prepare_invoice_html(html_string, base_url)
|
|
2487
|
+
require_invoice_xml
|
|
2488
|
+
|
|
2489
|
+
@parameters.delete('url')
|
|
2490
|
+
@parameters['html'] = html_string
|
|
2491
|
+
|
|
2492
|
+
if base_url.nil? || base_url.empty?
|
|
2493
|
+
@parameters.delete('base_url')
|
|
2494
|
+
else
|
|
2495
|
+
@parameters['base_url'] = base_url
|
|
2496
|
+
end
|
|
2497
|
+
end
|
|
2498
|
+
private :prepare_invoice_html
|
|
2499
|
+
|
|
2500
|
+
# Fail here rather than spending a round trip on a request the API will reject with the same message.
|
|
2501
|
+
def require_invoice_xml
|
|
2502
|
+
unless @files.key?('zugferd_xml') || @binary_data.key?('zugferd_xml')
|
|
2503
|
+
raise ApiException.new('The invoice XML was not specified. Set invoice_xml_file or invoice_xml before creating the invoice.'),
|
|
2504
|
+
'The invoice XML was not specified. Set invoice_xml_file or invoice_xml before creating the invoice.'
|
|
2505
|
+
end
|
|
2506
|
+
|
|
2507
|
+
profile = @parameters['zugferd_profile']
|
|
2508
|
+
if profile.nil? || profile.to_s.empty?
|
|
2509
|
+
raise ApiException.new('The invoice profile was not specified. Set zugferd_profile before creating the invoice.'),
|
|
2510
|
+
'The invoice profile was not specified. Set zugferd_profile before creating the invoice.'
|
|
2511
|
+
end
|
|
2512
|
+
end
|
|
2513
|
+
private :require_invoice_xml
|
|
2514
|
+
|
|
2515
|
+
# Run the invoice request as an asynchronous job and wait for its result.
|
|
2516
|
+
def run_invoice_async_job
|
|
2517
|
+
job_id = start_async_job_multipart_form_data
|
|
2518
|
+
|
|
2519
|
+
if job_id.nil? || job_id.empty?
|
|
2520
|
+
raise ApiException.new('An error occurred launching the asynchronous call.'), 'An error occurred launching the asynchronous call.'
|
|
2521
|
+
end
|
|
2522
|
+
|
|
2523
|
+
no_pings = 0
|
|
2524
|
+
|
|
2525
|
+
while no_pings < @async_calls_max_pings
|
|
2526
|
+
no_pings += 1
|
|
2527
|
+
|
|
2528
|
+
# sleep for a few seconds before next ping
|
|
2529
|
+
sleep(@async_calls_ping_interval)
|
|
2530
|
+
|
|
2531
|
+
async_job_client = AsyncJobClient.new(@parameters['key'], job_id)
|
|
2532
|
+
async_job_client.api_endpoint = @api_async_endpoint
|
|
2533
|
+
|
|
2534
|
+
result = async_job_client.result
|
|
2535
|
+
|
|
2536
|
+
next unless async_job_client.finished?
|
|
2537
|
+
|
|
2538
|
+
copy_async_job_results(async_job_client)
|
|
2539
|
+
return result
|
|
2540
|
+
end
|
|
2541
|
+
|
|
2542
|
+
raise ApiException.new('Asynchronous call did not finish in expected timeframe.'), 'Asynchronous call did not finish in expected timeframe.'
|
|
2543
|
+
end
|
|
2544
|
+
private :run_invoice_async_job
|
|
2545
|
+
|
|
2546
|
+
# Write the result of a conversion to a local file. The file is deleted if the conversion fails.
|
|
2547
|
+
def write_invoice_to_file(file_path)
|
|
2548
|
+
File.open(file_path, 'wb') do |file|
|
|
2549
|
+
yield file
|
|
2550
|
+
end
|
|
2551
|
+
rescue ApiException
|
|
2552
|
+
FileUtils.rm(file_path) if File.exist?(file_path)
|
|
2553
|
+
raise
|
|
2554
|
+
end
|
|
2555
|
+
private :write_invoice_to_file
|
|
2556
|
+
end
|
|
2557
|
+
|
|
2558
|
+
# Get the locations of certain web elements.
|
|
2559
|
+
# This is retrieved if pdf_web_elements_selectors parameter was set during the initial conversion call and elements were found to match the selectors.
|
|
2560
|
+
class WebElementsClient < ApiClient
|
|
2561
|
+
# Construct the web elements client.
|
|
2562
|
+
#
|
|
2563
|
+
# @param api_key API Key.
|
|
2564
|
+
# @param job_id Job ID.
|
|
2565
|
+
def initialize(api_key, job_id)
|
|
2566
|
+
super()
|
|
2567
|
+
@api_endpoint = 'https://selectpdf.com/api2/webelements/'
|
|
2568
|
+
@parameters['key'] = api_key
|
|
2569
|
+
@parameters['job_id'] = job_id
|
|
2570
|
+
end
|
|
2571
|
+
|
|
2572
|
+
# Get the locations of certain web elements.
|
|
1491
2573
|
# This is retrieved if pdf_web_elements_selectors parameter is set and elements were found to match the selectors.
|
|
1492
2574
|
#
|
|
1493
2575
|
# @return List of web elements locations.
|
|
@@ -1520,9 +2602,895 @@ module SelectPdf
|
|
|
1520
2602
|
def result
|
|
1521
2603
|
result = perform_post
|
|
1522
2604
|
|
|
1523
|
-
|
|
2605
|
+
# 202 Accepted - the job is still running
|
|
2606
|
+
return nil unless finished?
|
|
2607
|
+
|
|
2608
|
+
result
|
|
2609
|
+
end
|
|
2610
|
+
|
|
2611
|
+
# Check if the asynchronous job is finished.
|
|
2612
|
+
# 200 OK means the job finished successfully, 202 Accepted means the job is still running.
|
|
2613
|
+
# Any other status code raises an ApiException from result (the job finished with an error).
|
|
2614
|
+
#
|
|
2615
|
+
# @return True if the job finished.
|
|
2616
|
+
def finished?
|
|
2617
|
+
@last_http_code != 202
|
|
2618
|
+
end
|
|
2619
|
+
end
|
|
2620
|
+
|
|
2621
|
+
# Pdf Merge with SelectPdf Online API.
|
|
2622
|
+
#
|
|
2623
|
+
# Code sample:
|
|
2624
|
+
#
|
|
2625
|
+
# require 'selectpdf'
|
|
2626
|
+
#
|
|
2627
|
+
# $stdout.sync = true
|
|
2628
|
+
#
|
|
2629
|
+
# print "This is SelectPdf-#{SelectPdf::CLIENT_VERSION}\n"
|
|
2630
|
+
#
|
|
2631
|
+
# test_url = 'https://selectpdf.com/demo/files/selectpdf.pdf'
|
|
2632
|
+
# test_pdf = 'Input.pdf'
|
|
2633
|
+
# local_file = 'Result.pdf'
|
|
2634
|
+
# api_key = 'Your API key here'
|
|
2635
|
+
#
|
|
2636
|
+
# begin
|
|
2637
|
+
# client = SelectPdf::PdfMergeClient.new(api_key)
|
|
2638
|
+
#
|
|
2639
|
+
# # set parameters - see full list at https://selectpdf.com/pdf-merge-api/
|
|
2640
|
+
#
|
|
2641
|
+
# # specify the pdf files that will be merged (order will be preserved in the final pdf)
|
|
2642
|
+
# client.add_file(test_pdf) # add PDF from local file
|
|
2643
|
+
# client.add_url_file(test_url) # add PDF from public url
|
|
2644
|
+
# # client.add_file(test_pdf, 'pdf_password') # add PDF (that requires a password) from local file
|
|
2645
|
+
# # client.add_url_file(test_url, 'pdf_password') # add PDF (that requires a password) from public url
|
|
2646
|
+
#
|
|
2647
|
+
# print "Starting pdf merge ...\n"
|
|
2648
|
+
#
|
|
2649
|
+
# # merge pdfs to local file
|
|
2650
|
+
# client.save_to_file(local_file)
|
|
2651
|
+
#
|
|
2652
|
+
# # merge pdfs to memory
|
|
2653
|
+
# # pdf = client.save
|
|
2654
|
+
#
|
|
2655
|
+
# print "Finished! Number of pages: #{client.number_of_pages}.\n"
|
|
2656
|
+
#
|
|
2657
|
+
# # get API usage
|
|
2658
|
+
# usage_client = SelectPdf::UsageClient.new(api_key)
|
|
2659
|
+
# usage = usage_client.get_usage(false)
|
|
2660
|
+
# print("Usage: #{usage}\n")
|
|
2661
|
+
# print('Conversions remained this month: ', usage['available'], "\n")
|
|
2662
|
+
# rescue SelectPdf::ApiException => e
|
|
2663
|
+
# print("An error occurred: #{e}")
|
|
2664
|
+
# end
|
|
2665
|
+
class PdfMergeClient < ApiClient
|
|
2666
|
+
# Construct the Pdf Merge Client.
|
|
2667
|
+
#
|
|
2668
|
+
# @param api_key API Key.
|
|
2669
|
+
def initialize(api_key)
|
|
2670
|
+
super()
|
|
2671
|
+
@api_endpoint = 'https://selectpdf.com/api2/pdfmerge/'
|
|
2672
|
+
@parameters['key'] = api_key
|
|
2673
|
+
|
|
2674
|
+
@file_idx = 0
|
|
2675
|
+
end
|
|
2676
|
+
|
|
2677
|
+
# Add local PDF document to the list of input files.
|
|
2678
|
+
#
|
|
2679
|
+
# @param input_pdf Path to a local PDF file.
|
|
2680
|
+
# @param user_password User password for the PDF document (optional).
|
|
2681
|
+
def add_file(input_pdf, user_password = nil)
|
|
2682
|
+
@file_idx += 1
|
|
2683
|
+
|
|
2684
|
+
@files["file_#{@file_idx}"] = input_pdf
|
|
2685
|
+
@parameters.delete("url_#{@file_idx}")
|
|
2686
|
+
|
|
2687
|
+
if user_password.nil? || user_password.empty?
|
|
2688
|
+
@parameters.delete("password_#{@file_idx}")
|
|
2689
|
+
else
|
|
2690
|
+
@parameters["password_#{@file_idx}"] = user_password
|
|
2691
|
+
end
|
|
2692
|
+
end
|
|
2693
|
+
|
|
2694
|
+
# Add remote PDF document to the list of input files.
|
|
2695
|
+
#
|
|
2696
|
+
# @param input_url Url of a remote PDF file.
|
|
2697
|
+
# @param user_password User password for the PDF document (optional).
|
|
2698
|
+
def add_url_file(input_url, user_password = nil)
|
|
2699
|
+
@file_idx += 1
|
|
2700
|
+
|
|
2701
|
+
@parameters["url_#{@file_idx}"] = input_url
|
|
2702
|
+
@files.delete("file_#{@file_idx}")
|
|
2703
|
+
|
|
2704
|
+
if user_password.nil? || user_password.empty?
|
|
2705
|
+
@parameters.delete("password_#{@file_idx}")
|
|
2706
|
+
else
|
|
2707
|
+
@parameters["password_#{@file_idx}"] = user_password
|
|
2708
|
+
end
|
|
2709
|
+
end
|
|
2710
|
+
|
|
2711
|
+
# Merge all specified input pdfs and return the resulted PDF.
|
|
2712
|
+
#
|
|
2713
|
+
# @return Byte array containing the resulted PDF.
|
|
2714
|
+
def save
|
|
2715
|
+
@parameters['async'] = 'False'
|
|
2716
|
+
@parameters['files_no'] = @file_idx
|
|
2717
|
+
|
|
2718
|
+
result = perform_post_as_multipart_formdata
|
|
2719
|
+
|
|
2720
|
+
@file_idx = 0
|
|
2721
|
+
@files = {}
|
|
2722
|
+
|
|
2723
|
+
result
|
|
2724
|
+
end
|
|
2725
|
+
|
|
2726
|
+
# Merge all specified input pdfs and writes the resulted PDF to a specified stream.
|
|
2727
|
+
#
|
|
2728
|
+
# @param stream The output stream where the resulted PDF will be written.
|
|
2729
|
+
def save_to_stream(stream)
|
|
2730
|
+
result = save
|
|
2731
|
+
stream.write(result)
|
|
2732
|
+
end
|
|
2733
|
+
|
|
2734
|
+
# Merge all specified input pdfs and writes the resulted PDF to a local file.
|
|
2735
|
+
#
|
|
2736
|
+
# @param file_path Local file including path if necessary.
|
|
2737
|
+
def save_to_file(file_path)
|
|
2738
|
+
result = save
|
|
2739
|
+
File.open(file_path, 'wb') do |file|
|
|
2740
|
+
file.write(result)
|
|
2741
|
+
end
|
|
2742
|
+
rescue ApiException
|
|
2743
|
+
FileUtils.rm(file_path) if File.exist?(file_path)
|
|
2744
|
+
raise
|
|
2745
|
+
end
|
|
2746
|
+
|
|
2747
|
+
# Merge all specified input pdfs and return the resulted PDF. An asynchronous call is used.
|
|
2748
|
+
#
|
|
2749
|
+
# @return Byte array containing the resulted PDF.
|
|
2750
|
+
def save_async
|
|
2751
|
+
@parameters['files_no'] = @file_idx
|
|
2752
|
+
|
|
2753
|
+
job_id = start_async_job_multipart_form_data
|
|
2754
|
+
|
|
2755
|
+
if job_id.nil? || job_id.empty?
|
|
2756
|
+
raise ApiException.new('An error occurred launching the asynchronous call.'), 'An error occurred launching the asynchronous call.'
|
|
2757
|
+
end
|
|
2758
|
+
|
|
2759
|
+
no_pings = 0
|
|
2760
|
+
|
|
2761
|
+
while no_pings < @async_calls_max_pings
|
|
2762
|
+
no_pings += 1
|
|
2763
|
+
|
|
2764
|
+
# sleep for a few seconds before next ping
|
|
2765
|
+
sleep(@async_calls_ping_interval)
|
|
2766
|
+
|
|
2767
|
+
async_job_client = AsyncJobClient.new(@parameters['key'], @job_id)
|
|
2768
|
+
async_job_client.api_endpoint = @api_async_endpoint
|
|
2769
|
+
|
|
2770
|
+
result = async_job_client.result
|
|
1524
2771
|
|
|
1525
|
-
|
|
2772
|
+
next if result.nil?
|
|
2773
|
+
|
|
2774
|
+
copy_async_job_results(async_job_client)
|
|
2775
|
+
@file_idx = 0
|
|
2776
|
+
@files = {}
|
|
2777
|
+
|
|
2778
|
+
return result
|
|
2779
|
+
end
|
|
2780
|
+
|
|
2781
|
+
@file_idx = 0
|
|
2782
|
+
@files = {}
|
|
2783
|
+
|
|
2784
|
+
raise ApiException.new('Asynchronous call did not finish in expected timeframe.'), 'Asynchronous call did not finish in expected timeframe.'
|
|
2785
|
+
end
|
|
2786
|
+
|
|
2787
|
+
# Merge all specified input pdfs and writes the resulted PDF to a specified stream. An asynchronous call is used.
|
|
2788
|
+
#
|
|
2789
|
+
# @param stream The output stream where the resulted PDF will be written.
|
|
2790
|
+
def save_to_stream_async(stream)
|
|
2791
|
+
result = save_async
|
|
2792
|
+
stream.write(result)
|
|
2793
|
+
end
|
|
2794
|
+
|
|
2795
|
+
# Merge all specified input pdfs and writes the resulted PDF to a local file. An asynchronous call is used.
|
|
2796
|
+
#
|
|
2797
|
+
# @param file_path Local file including path if necessary.
|
|
2798
|
+
def save_to_file_async(file_path)
|
|
2799
|
+
result = save_async
|
|
2800
|
+
File.open(file_path, 'wb') do |file|
|
|
2801
|
+
file.write(result)
|
|
2802
|
+
end
|
|
2803
|
+
rescue ApiException
|
|
2804
|
+
FileUtils.rm(file_path) if File.exist?(file_path)
|
|
2805
|
+
raise
|
|
2806
|
+
end
|
|
2807
|
+
|
|
2808
|
+
# Set the PDF document title.
|
|
2809
|
+
#
|
|
2810
|
+
# @param doc_title Document title.
|
|
2811
|
+
def doc_title=(doc_title)
|
|
2812
|
+
@parameters['doc_title'] = doc_title
|
|
2813
|
+
end
|
|
2814
|
+
|
|
2815
|
+
# Set the subject of the PDF document.
|
|
2816
|
+
#
|
|
2817
|
+
# @param doc_subject Document subject.
|
|
2818
|
+
def doc_subject=(doc_subject)
|
|
2819
|
+
@parameters['doc_subject'] = doc_subject
|
|
2820
|
+
end
|
|
2821
|
+
|
|
2822
|
+
# Set the PDF document keywords.
|
|
2823
|
+
#
|
|
2824
|
+
# @param doc_keywords Document keywords.
|
|
2825
|
+
def doc_keywords=(doc_keywords)
|
|
2826
|
+
@parameters['doc_keywords'] = doc_keywords
|
|
2827
|
+
end
|
|
2828
|
+
|
|
2829
|
+
# Set the name of the PDF document author.
|
|
2830
|
+
#
|
|
2831
|
+
# @param doc_author Document author.
|
|
2832
|
+
def doc_author=(doc_author)
|
|
2833
|
+
@parameters['doc_author'] = doc_author
|
|
2834
|
+
end
|
|
2835
|
+
|
|
2836
|
+
# Add the date and time when the PDF document was created to the PDF document information. The default value is False.
|
|
2837
|
+
#
|
|
2838
|
+
# @param doc_add_creation_date Add creation date to the document metadata or not.
|
|
2839
|
+
def doc_add_creation_date=(doc_add_creation_date)
|
|
2840
|
+
@parameters['doc_add_creation_date'] = doc_add_creation_date
|
|
2841
|
+
end
|
|
2842
|
+
|
|
2843
|
+
# Set the page layout to be used when the document is opened in a PDF viewer. The default value is SelectPdf::PageLayout::ONE_COLUMN.
|
|
2844
|
+
#
|
|
2845
|
+
# @param viewer_page_layout Page layout. Possible values: 0 (Single Page), 1 (One Column), 2 (Two Column Left), 3 (Two Column Right).
|
|
2846
|
+
# Use constants from SelectPdf::PageLayout class.
|
|
2847
|
+
def viewer_page_layout=(viewer_page_layout)
|
|
2848
|
+
unless [0, 1, 2, 3].include?(viewer_page_layout)
|
|
2849
|
+
raise ApiException.new('Allowed values for Page Layout: 0 (Single Page), 1 (One Column), 2 (Two Column Left), 3 (Two Column Right).'), 'Allowed values for Page Layout: 0 (Single Page), 1 (One Column), 2 (Two Column Left), 3 (Two Column Right).'
|
|
2850
|
+
end
|
|
2851
|
+
|
|
2852
|
+
@parameters['viewer_page_layout'] = viewer_page_layout
|
|
2853
|
+
end
|
|
2854
|
+
|
|
2855
|
+
# Set the document page mode when the pdf document is opened in a PDF viewer. The default value is SelectPdf::PageMode::USE_NONE.
|
|
2856
|
+
#
|
|
2857
|
+
# @param viewer_page_mode Page mode. Possible values: 0 (Use None), 1 (Use Outlines), 2 (Use Thumbs), 3 (Full Screen), 4 (Use OC), 5 (Use Attachments).
|
|
2858
|
+
# Use constants from SelectPdf::PageMode class.
|
|
2859
|
+
def viewer_page_mode=(viewer_page_mode)
|
|
2860
|
+
unless [0, 1, 2, 3, 4, 5].include?(viewer_page_mode)
|
|
2861
|
+
raise ApiException.new('Allowed values for Page Mode: 0 (Use None), 1 (Use Outlines), 2 (Use Thumbs), 3 (Full Screen), 4 (Use OC), 5 (Use Attachments).'),
|
|
2862
|
+
'Allowed values for Page Mode: 0 (Use None), 1 (Use Outlines), 2 (Use Thumbs), 3 (Full Screen), 4 (Use OC), 5 (Use Attachments).'
|
|
2863
|
+
end
|
|
2864
|
+
|
|
2865
|
+
@parameters['viewer_page_mode'] = viewer_page_mode
|
|
2866
|
+
end
|
|
2867
|
+
|
|
2868
|
+
# Set a flag specifying whether to position the document's window in the center of the screen. The default value is False.
|
|
2869
|
+
#
|
|
2870
|
+
# @param viewer_center_window Center window or not.
|
|
2871
|
+
def viewer_center_window=(viewer_center_window)
|
|
2872
|
+
@parameters['viewer_center_window'] = viewer_center_window
|
|
2873
|
+
end
|
|
2874
|
+
|
|
2875
|
+
# Set a flag specifying whether the window's title bar should display the document title taken from document information. The default value is False.
|
|
2876
|
+
#
|
|
2877
|
+
# @param viewer_display_doc_title Display title or not.
|
|
2878
|
+
def viewer_display_doc_title=(viewer_display_doc_title)
|
|
2879
|
+
@parameters['viewer_display_doc_title'] = viewer_display_doc_title
|
|
2880
|
+
end
|
|
2881
|
+
|
|
2882
|
+
# Set a flag specifying whether to resize the document's window to fit the size of the first displayed page. The default value is False.
|
|
2883
|
+
#
|
|
2884
|
+
# @param viewer_fit_window Fit window or not.
|
|
2885
|
+
def viewer_fit_window=(viewer_fit_window)
|
|
2886
|
+
@parameters['viewer_fit_window'] = viewer_fit_window
|
|
2887
|
+
end
|
|
2888
|
+
|
|
2889
|
+
# Set a flag specifying whether to hide the pdf viewer application's menu bar when the document is active. The default value is False.
|
|
2890
|
+
#
|
|
2891
|
+
# @param viewer_hide_menu_bar Hide menu bar or not.
|
|
2892
|
+
def viewer_hide_menu_bar=(viewer_hide_menu_bar)
|
|
2893
|
+
@parameters['viewer_hide_menu_bar'] = viewer_hide_menu_bar
|
|
2894
|
+
end
|
|
2895
|
+
|
|
2896
|
+
# Set a flag specifying whether to hide the pdf viewer application's tool bars when the document is active. The default value is False.
|
|
2897
|
+
#
|
|
2898
|
+
# @param viewer_hide_toolbar Hide tool bars or not.
|
|
2899
|
+
def viewer_hide_toolbar=(viewer_hide_toolbar)
|
|
2900
|
+
@parameters['viewer_hide_toolbar'] = viewer_hide_toolbar
|
|
2901
|
+
end
|
|
2902
|
+
|
|
2903
|
+
# Set a flag specifying whether to hide user interface elements in the document's window (such as scroll bars and navigation controls), leaving only the document's contents displayed.
|
|
2904
|
+
#
|
|
2905
|
+
# @param viewer_hide_window_ui Hide window UI or not.
|
|
2906
|
+
def viewer_hide_window_ui=(viewer_hide_window_ui)
|
|
2907
|
+
@parameters['viewer_hide_window_ui'] = viewer_hide_window_ui
|
|
2908
|
+
end
|
|
2909
|
+
|
|
2910
|
+
# Set PDF user password.
|
|
2911
|
+
#
|
|
2912
|
+
# @param user_password PDF user password.
|
|
2913
|
+
def user_password=(user_password)
|
|
2914
|
+
@parameters['user_password'] = user_password
|
|
2915
|
+
end
|
|
2916
|
+
|
|
2917
|
+
# Set PDF owner password.
|
|
2918
|
+
#
|
|
2919
|
+
# @param owner_password PDF owner password.
|
|
2920
|
+
def owner_password=(owner_password)
|
|
2921
|
+
@parameters['owner_password'] = owner_password
|
|
2922
|
+
end
|
|
2923
|
+
|
|
2924
|
+
# Set the maximum amount of time (in seconds) for this job.
|
|
2925
|
+
# The default value is 30 seconds. Use a larger value (up to 120 seconds allowed) for large documents.
|
|
2926
|
+
#
|
|
2927
|
+
# @param timeout Timeout in seconds.
|
|
2928
|
+
def timeout=(timeout)
|
|
2929
|
+
@parameters['timeout'] = timeout
|
|
2930
|
+
end
|
|
2931
|
+
|
|
2932
|
+
# Set a custom parameter. Do not use this method unless advised by SelectPdf.
|
|
2933
|
+
#
|
|
2934
|
+
# @param parameter_name Parameter name.
|
|
2935
|
+
# @param parameter_value Parameter value.
|
|
2936
|
+
def set_custom_parameter(parameter_name, parameter_value)
|
|
2937
|
+
@parameters[parameter_name] = parameter_value
|
|
2938
|
+
end
|
|
2939
|
+
end
|
|
2940
|
+
|
|
2941
|
+
# Pdf To Text Conversion with SelectPdf Online API.
|
|
2942
|
+
#
|
|
2943
|
+
# Code Sample for PDF To Text
|
|
2944
|
+
#
|
|
2945
|
+
# require 'selectpdf'
|
|
2946
|
+
#
|
|
2947
|
+
# $stdout.sync = true
|
|
2948
|
+
#
|
|
2949
|
+
# print "This is SelectPdf-#{SelectPdf::CLIENT_VERSION}\n"
|
|
2950
|
+
#
|
|
2951
|
+
# test_url = 'https://selectpdf.com/demo/files/selectpdf.pdf'
|
|
2952
|
+
# test_pdf = 'Input.pdf'
|
|
2953
|
+
# local_file = 'Result.txt'
|
|
2954
|
+
# api_key = 'Your API key here'
|
|
2955
|
+
#
|
|
2956
|
+
# begin
|
|
2957
|
+
# client = SelectPdf::PdfToTextClient.new(api_key)
|
|
2958
|
+
#
|
|
2959
|
+
# # set parameters - see full list at https://selectpdf.com/pdf-to-text-api/
|
|
2960
|
+
# client.start_page = 1 # start page (processing starts from here)
|
|
2961
|
+
# client.end_page = 0 # end page (set 0 to process file til the end)
|
|
2962
|
+
# client.output_format = SelectPdf::OutputFormat::TEXT # set output format (Text or HTML)
|
|
2963
|
+
#
|
|
2964
|
+
# print "Starting pdf to text ...\n"
|
|
2965
|
+
#
|
|
2966
|
+
# # convert local pdf to local text file
|
|
2967
|
+
# client.text_from_file_to_file(test_pdf, local_file)
|
|
2968
|
+
#
|
|
2969
|
+
# # extract text from local pdf to memory
|
|
2970
|
+
# # text = client.text_from_file(test_pdf)
|
|
2971
|
+
# # print text
|
|
2972
|
+
#
|
|
2973
|
+
# # convert pdf from public url to local text file
|
|
2974
|
+
# # client.text_from_url_to_file(test_url, local_file)
|
|
2975
|
+
#
|
|
2976
|
+
# # extract text from pdf from public url to memory
|
|
2977
|
+
# # text = client.text_from_url(test_url)
|
|
2978
|
+
# # print text
|
|
2979
|
+
#
|
|
2980
|
+
# print "Finished! Number of pages processed: #{client.number_of_pages}.\n"
|
|
2981
|
+
#
|
|
2982
|
+
# # get API usage
|
|
2983
|
+
# usage_client = SelectPdf::UsageClient.new(api_key)
|
|
2984
|
+
# usage = usage_client.get_usage(false)
|
|
2985
|
+
# print("Usage: #{usage}\n")
|
|
2986
|
+
# print('Conversions remained this month: ', usage['available'], "\n")
|
|
2987
|
+
# rescue SelectPdf::ApiException => e
|
|
2988
|
+
# print("An error occurred: #{e}")
|
|
2989
|
+
# end
|
|
2990
|
+
#
|
|
2991
|
+
# Code Sample for Search Pdf
|
|
2992
|
+
#
|
|
2993
|
+
# require 'selectpdf'
|
|
2994
|
+
#
|
|
2995
|
+
# $stdout.sync = true
|
|
2996
|
+
#
|
|
2997
|
+
# print "This is SelectPdf-#{SelectPdf::CLIENT_VERSION}\n"
|
|
2998
|
+
#
|
|
2999
|
+
# test_url = 'https://selectpdf.com/demo/files/selectpdf.pdf'
|
|
3000
|
+
# test_pdf = 'Input.pdf'
|
|
3001
|
+
# api_key = 'Your API key here'
|
|
3002
|
+
#
|
|
3003
|
+
# begin
|
|
3004
|
+
# client = SelectPdf::PdfToTextClient.new(api_key)
|
|
3005
|
+
#
|
|
3006
|
+
# # set parameters - see full list at https://selectpdf.com/pdf-to-text-api/
|
|
3007
|
+
# client.start_page = 1 # start page (processing starts from here)
|
|
3008
|
+
# client.end_page = 0 # end page (set 0 to process file til the end)
|
|
3009
|
+
# client.output_format = SelectPdf::OutputFormat::TEXT # set output format (Text or HTML)
|
|
3010
|
+
#
|
|
3011
|
+
# print "Starting search pdf ...\n"
|
|
3012
|
+
#
|
|
3013
|
+
# # search local pdf
|
|
3014
|
+
# results = client.search_file(test_pdf, 'pdf')
|
|
3015
|
+
#
|
|
3016
|
+
# # search pdf from public url
|
|
3017
|
+
# # results = client.search_url(test_url, 'pdf')
|
|
3018
|
+
#
|
|
3019
|
+
# print "Search results: #{results}.\nSearch results count: #{results.length}\n"
|
|
3020
|
+
#
|
|
3021
|
+
# print "Finished! Number of pages processed: #{client.number_of_pages}.\n"
|
|
3022
|
+
#
|
|
3023
|
+
# # get API usage
|
|
3024
|
+
# usage_client = SelectPdf::UsageClient.new(api_key)
|
|
3025
|
+
# usage = usage_client.get_usage(false)
|
|
3026
|
+
# print("Usage: #{usage}\n")
|
|
3027
|
+
# print('Conversions remained this month: ', usage['available'], "\n")
|
|
3028
|
+
# rescue SelectPdf::ApiException => e
|
|
3029
|
+
# print("An error occurred: #{e}")
|
|
3030
|
+
# end
|
|
3031
|
+
#
|
|
3032
|
+
class PdfToTextClient < ApiClient
|
|
3033
|
+
# Construct the Pdf To Text Client.
|
|
3034
|
+
#
|
|
3035
|
+
# @param api_key API Key.
|
|
3036
|
+
def initialize(api_key)
|
|
3037
|
+
super()
|
|
3038
|
+
@api_endpoint = 'https://selectpdf.com/api2/pdftotext/'
|
|
3039
|
+
@parameters['key'] = api_key
|
|
3040
|
+
|
|
3041
|
+
@file_idx = 0
|
|
3042
|
+
end
|
|
3043
|
+
|
|
3044
|
+
# Get the text from the specified pdf.
|
|
3045
|
+
#
|
|
3046
|
+
# @param input_pdf Path to a local PDF file.
|
|
3047
|
+
# @return Extracted text.
|
|
3048
|
+
def text_from_file(input_pdf)
|
|
3049
|
+
@parameters['async'] = 'False'
|
|
3050
|
+
@parameters['action'] = 'Convert'
|
|
3051
|
+
@parameters.delete('url')
|
|
3052
|
+
|
|
3053
|
+
@files = {}
|
|
3054
|
+
@files['inputPdf'] = input_pdf
|
|
3055
|
+
|
|
3056
|
+
perform_post_as_multipart_formdata
|
|
3057
|
+
end
|
|
3058
|
+
|
|
3059
|
+
# Get the text from the specified pdf and write it to the specified text file.
|
|
3060
|
+
#
|
|
3061
|
+
# @param input_pdf Path to a local PDF file.
|
|
3062
|
+
# @param output_file_path The output file where the resulted text will be written.
|
|
3063
|
+
def text_from_file_to_file(input_pdf, output_file_path)
|
|
3064
|
+
result = text_from_file(input_pdf)
|
|
3065
|
+
File.open(output_file_path, 'wb') do |file|
|
|
3066
|
+
file.write(result)
|
|
3067
|
+
end
|
|
3068
|
+
rescue ApiException
|
|
3069
|
+
FileUtils.rm(output_file_path) if File.exist?(output_file_path)
|
|
3070
|
+
raise
|
|
3071
|
+
end
|
|
3072
|
+
|
|
3073
|
+
# Get the text from the specified pdf and write it to the specified stream.
|
|
3074
|
+
#
|
|
3075
|
+
# @param input_pdf Path to a local PDF file.
|
|
3076
|
+
# @param stream The output stream where the resulted PDF will be written.
|
|
3077
|
+
def text_from_file_to_stream(input_pdf, stream)
|
|
3078
|
+
result = text_from_file(input_pdf)
|
|
3079
|
+
stream.write(result)
|
|
3080
|
+
end
|
|
3081
|
+
|
|
3082
|
+
# Get the text from the specified pdf with an asynchronous call.
|
|
3083
|
+
#
|
|
3084
|
+
# @param input_pdf Path to a local PDF file.
|
|
3085
|
+
# @return Extracted text.
|
|
3086
|
+
def text_from_file_async(input_pdf)
|
|
3087
|
+
@parameters['action'] = 'Convert'
|
|
3088
|
+
@parameters.delete('url')
|
|
3089
|
+
|
|
3090
|
+
@files = {}
|
|
3091
|
+
@files['inputPdf'] = input_pdf
|
|
3092
|
+
|
|
3093
|
+
job_id = start_async_job_multipart_form_data
|
|
3094
|
+
|
|
3095
|
+
if job_id.nil? || job_id.empty?
|
|
3096
|
+
raise ApiException.new('An error occurred launching the asynchronous call.'), 'An error occurred launching the asynchronous call.'
|
|
3097
|
+
end
|
|
3098
|
+
|
|
3099
|
+
no_pings = 0
|
|
3100
|
+
|
|
3101
|
+
while no_pings < @async_calls_max_pings
|
|
3102
|
+
no_pings += 1
|
|
3103
|
+
|
|
3104
|
+
# sleep for a few seconds before next ping
|
|
3105
|
+
sleep(@async_calls_ping_interval)
|
|
3106
|
+
|
|
3107
|
+
async_job_client = AsyncJobClient.new(@parameters['key'], @job_id)
|
|
3108
|
+
async_job_client.api_endpoint = @api_async_endpoint
|
|
3109
|
+
|
|
3110
|
+
result = async_job_client.result
|
|
3111
|
+
|
|
3112
|
+
next if result.nil?
|
|
3113
|
+
|
|
3114
|
+
copy_async_job_results(async_job_client)
|
|
3115
|
+
|
|
3116
|
+
return result
|
|
3117
|
+
end
|
|
3118
|
+
|
|
3119
|
+
raise ApiException.new('Asynchronous call did not finish in expected timeframe.'), 'Asynchronous call did not finish in expected timeframe.'
|
|
3120
|
+
end
|
|
3121
|
+
|
|
3122
|
+
# Get the text from the specified pdf with an asynchronous call and write it to the specified text file.
|
|
3123
|
+
#
|
|
3124
|
+
# @param input_pdf Path to a local PDF file.
|
|
3125
|
+
# @param output_file_path The output file where the resulted text will be written.
|
|
3126
|
+
def text_from_file_to_file_async(input_pdf, output_file_path)
|
|
3127
|
+
result = text_from_file_async(input_pdf)
|
|
3128
|
+
File.open(output_file_path, 'wb') do |file|
|
|
3129
|
+
file.write(result)
|
|
3130
|
+
end
|
|
3131
|
+
rescue ApiException
|
|
3132
|
+
FileUtils.rm(output_file_path) if File.exist?(output_file_path)
|
|
3133
|
+
raise
|
|
3134
|
+
end
|
|
3135
|
+
|
|
3136
|
+
# Get the text from the specified pdf with an asynchronous call and write it to the specified stream.
|
|
3137
|
+
#
|
|
3138
|
+
# @param input_pdf Path to a local PDF file.
|
|
3139
|
+
# @param stream The output stream where the resulted PDF will be written.
|
|
3140
|
+
def text_from_file_to_stream_async(input_pdf, stream)
|
|
3141
|
+
result = text_from_file_async(input_pdf)
|
|
3142
|
+
stream.write(result)
|
|
3143
|
+
end
|
|
3144
|
+
|
|
3145
|
+
# Get the text from the specified pdf.
|
|
3146
|
+
#
|
|
3147
|
+
# @param url Address of the PDF file.
|
|
3148
|
+
# @return Extracted text.
|
|
3149
|
+
def text_from_url(url)
|
|
3150
|
+
if !url.downcase.start_with?('http://') && !url.downcase.start_with?('https://')
|
|
3151
|
+
raise ApiException.new('The supported protocols for the PDFs available online are http:// and https://.'),
|
|
3152
|
+
'The supported protocols for the PDFs available online are http:// and https://.'
|
|
3153
|
+
end
|
|
3154
|
+
|
|
3155
|
+
if url.downcase.start_with?('http://localhost')
|
|
3156
|
+
raise ApiException.new('Cannot convert local urls via this method. Use getTextFromFile instead.'),
|
|
3157
|
+
'Cannot convert local urls via this method. Use text_from_file instead.'
|
|
3158
|
+
end
|
|
3159
|
+
|
|
3160
|
+
@parameters['async'] = 'False'
|
|
3161
|
+
@parameters['action'] = 'Convert'
|
|
3162
|
+
|
|
3163
|
+
@files = {}
|
|
3164
|
+
@parameters['url'] = url
|
|
3165
|
+
|
|
3166
|
+
perform_post_as_multipart_formdata
|
|
3167
|
+
end
|
|
3168
|
+
|
|
3169
|
+
# Get the text from the specified pdf and write it to the specified text file.
|
|
3170
|
+
#
|
|
3171
|
+
# @param url Address of the PDF file.
|
|
3172
|
+
# @param output_file_path The output file where the resulted text will be written.
|
|
3173
|
+
def text_from_url_to_file(url, output_file_path)
|
|
3174
|
+
result = text_from_url(url)
|
|
3175
|
+
File.open(output_file_path, 'wb') do |file|
|
|
3176
|
+
file.write(result)
|
|
3177
|
+
end
|
|
3178
|
+
rescue ApiException
|
|
3179
|
+
FileUtils.rm(output_file_path) if File.exist?(output_file_path)
|
|
3180
|
+
raise
|
|
3181
|
+
end
|
|
3182
|
+
|
|
3183
|
+
# Get the text from the specified pdf and write it to the specified stream.
|
|
3184
|
+
#
|
|
3185
|
+
# @param url Address of the PDF file.
|
|
3186
|
+
# @param stream The output stream where the resulted PDF will be written.
|
|
3187
|
+
def text_from_url_to_stream(url, stream)
|
|
3188
|
+
result = text_from_url(url)
|
|
3189
|
+
stream.write(result)
|
|
3190
|
+
end
|
|
3191
|
+
|
|
3192
|
+
# Get the text from the specified pdf with an asynchronous call.
|
|
3193
|
+
#
|
|
3194
|
+
# @param url Address of the PDF file.
|
|
3195
|
+
# @return Extracted text.
|
|
3196
|
+
def text_from_url_async(url)
|
|
3197
|
+
if !url.downcase.start_with?('http://') && !url.downcase.start_with?('https://')
|
|
3198
|
+
raise ApiException.new('The supported protocols for the PDFs available online are http:// and https://.'),
|
|
3199
|
+
'The supported protocols for the PDFs available online are http:// and https://.'
|
|
3200
|
+
end
|
|
3201
|
+
|
|
3202
|
+
if url.downcase.start_with?('http://localhost')
|
|
3203
|
+
raise ApiException.new('Cannot convert local urls via this method. Use getTextFromFile instead.'),
|
|
3204
|
+
'Cannot convert local urls via this method. Use text_from_file_async instead.'
|
|
3205
|
+
end
|
|
3206
|
+
|
|
3207
|
+
@parameters['action'] = 'Convert'
|
|
3208
|
+
|
|
3209
|
+
@files = {}
|
|
3210
|
+
@parameters['url'] = url
|
|
3211
|
+
|
|
3212
|
+
job_id = start_async_job_multipart_form_data
|
|
3213
|
+
|
|
3214
|
+
if job_id.nil? || job_id.empty?
|
|
3215
|
+
raise ApiException.new('An error occurred launching the asynchronous call.'), 'An error occurred launching the asynchronous call.'
|
|
3216
|
+
end
|
|
3217
|
+
|
|
3218
|
+
no_pings = 0
|
|
3219
|
+
|
|
3220
|
+
while no_pings < @async_calls_max_pings
|
|
3221
|
+
no_pings += 1
|
|
3222
|
+
|
|
3223
|
+
# sleep for a few seconds before next ping
|
|
3224
|
+
sleep(@async_calls_ping_interval)
|
|
3225
|
+
|
|
3226
|
+
async_job_client = AsyncJobClient.new(@parameters['key'], @job_id)
|
|
3227
|
+
async_job_client.api_endpoint = @api_async_endpoint
|
|
3228
|
+
|
|
3229
|
+
result = async_job_client.result
|
|
3230
|
+
|
|
3231
|
+
next if result.nil?
|
|
3232
|
+
|
|
3233
|
+
copy_async_job_results(async_job_client)
|
|
3234
|
+
|
|
3235
|
+
return result
|
|
3236
|
+
end
|
|
3237
|
+
|
|
3238
|
+
raise ApiException.new('Asynchronous call did not finish in expected timeframe.'),
|
|
3239
|
+
'Asynchronous call did not finish in expected timeframe.'
|
|
3240
|
+
end
|
|
3241
|
+
|
|
3242
|
+
# Get the text from the specified pdf with an asynchronous call and write it to the specified text file.
|
|
3243
|
+
#
|
|
3244
|
+
# @param url Address of the PDF file.
|
|
3245
|
+
# @param output_file_path The output file where the resulted text will be written.
|
|
3246
|
+
def text_from_url_to_file_async(url, output_file_path)
|
|
3247
|
+
result = text_from_url_async(url)
|
|
3248
|
+
File.open(output_file_path, 'wb') do |file|
|
|
3249
|
+
file.write(result)
|
|
3250
|
+
end
|
|
3251
|
+
rescue ApiException
|
|
3252
|
+
FileUtils.rm(output_file_path) if File.exist?(output_file_path)
|
|
3253
|
+
raise
|
|
3254
|
+
end
|
|
3255
|
+
|
|
3256
|
+
# Get the text from the specified pdf with an asynchronous call and write it to the specified stream.
|
|
3257
|
+
#
|
|
3258
|
+
# @param url Address of the PDF file.
|
|
3259
|
+
# @param stream The output stream where the resulted PDF will be written.
|
|
3260
|
+
def text_from_url_to_stream_async(url, stream)
|
|
3261
|
+
result = text_from_url_async(url)
|
|
3262
|
+
stream.write(result)
|
|
3263
|
+
end
|
|
3264
|
+
|
|
3265
|
+
# Search for a specific text in a PDF document.
|
|
3266
|
+
# Pages that participate to this operation are specified by start_page and end_page methods.
|
|
3267
|
+
#
|
|
3268
|
+
# @param input_pdf Path to a local PDF file.
|
|
3269
|
+
# @param text_to_search Text to search.
|
|
3270
|
+
# @param case_sensitive If the search is case sensitive or not.
|
|
3271
|
+
# @param whole_words_only If the search works on whole words or not.
|
|
3272
|
+
# @return List with text positions in the current PDF document.
|
|
3273
|
+
def search_file(input_pdf, text_to_search, case_sensitive = false, whole_words_only = false)
|
|
3274
|
+
if text_to_search.nil? || text_to_search.empty?
|
|
3275
|
+
raise ApiException.new('Search text cannot be empty.'), 'Search text cannot be empty.'
|
|
3276
|
+
end
|
|
3277
|
+
|
|
3278
|
+
@parameters['async'] = 'False'
|
|
3279
|
+
@parameters['action'] = 'Search'
|
|
3280
|
+
@parameters.delete('url')
|
|
3281
|
+
@parameters['search_text'] = text_to_search
|
|
3282
|
+
@parameters['case_sensitive'] = case_sensitive
|
|
3283
|
+
@parameters['whole_words_only'] = whole_words_only
|
|
3284
|
+
|
|
3285
|
+
@files = {}
|
|
3286
|
+
@files['inputPdf'] = input_pdf
|
|
3287
|
+
|
|
3288
|
+
@headers['Accept'] = 'text/json'
|
|
3289
|
+
|
|
3290
|
+
result = perform_post_as_multipart_formdata
|
|
3291
|
+
return [] if result.nil? || result.empty?
|
|
3292
|
+
|
|
3293
|
+
JSON.parse(result)
|
|
3294
|
+
end
|
|
3295
|
+
|
|
3296
|
+
# Search for a specific text in a PDF document with an asynchronous call.
|
|
3297
|
+
# Pages that participate to this operation are specified by start_page and end_page methods.
|
|
3298
|
+
#
|
|
3299
|
+
# @param input_pdf Path to a local PDF file.
|
|
3300
|
+
# @param text_to_search Text to search.
|
|
3301
|
+
# @param case_sensitive If the search is case sensitive or not.
|
|
3302
|
+
# @param whole_words_only If the search works on whole words or not.
|
|
3303
|
+
# @return List with text positions in the current PDF document.
|
|
3304
|
+
def search_file_async(input_pdf, text_to_search, case_sensitive = false, whole_words_only = false)
|
|
3305
|
+
if text_to_search.nil? || text_to_search.empty?
|
|
3306
|
+
raise ApiException.new('Search text cannot be empty.'), 'Search text cannot be empty.'
|
|
3307
|
+
end
|
|
3308
|
+
|
|
3309
|
+
@parameters['action'] = 'Search'
|
|
3310
|
+
@parameters.delete('url')
|
|
3311
|
+
@parameters['search_text'] = text_to_search
|
|
3312
|
+
@parameters['case_sensitive'] = case_sensitive
|
|
3313
|
+
@parameters['whole_words_only'] = whole_words_only
|
|
3314
|
+
|
|
3315
|
+
@files = {}
|
|
3316
|
+
@files['inputPdf'] = input_pdf
|
|
3317
|
+
|
|
3318
|
+
@headers['Accept'] = 'text/json'
|
|
3319
|
+
|
|
3320
|
+
job_id = start_async_job_multipart_form_data
|
|
3321
|
+
|
|
3322
|
+
if job_id.nil? || job_id.empty?
|
|
3323
|
+
raise ApiException.new('An error occurred launching the asynchronous call.'),
|
|
3324
|
+
'An error occurred launching the asynchronous call.'
|
|
3325
|
+
end
|
|
3326
|
+
|
|
3327
|
+
no_pings = 0
|
|
3328
|
+
|
|
3329
|
+
while no_pings < @async_calls_max_pings
|
|
3330
|
+
no_pings += 1
|
|
3331
|
+
|
|
3332
|
+
# sleep for a few seconds before next ping
|
|
3333
|
+
sleep(@async_calls_ping_interval)
|
|
3334
|
+
|
|
3335
|
+
async_job_client = AsyncJobClient.new(@parameters['key'], @job_id)
|
|
3336
|
+
async_job_client.api_endpoint = @api_async_endpoint
|
|
3337
|
+
|
|
3338
|
+
result = async_job_client.result
|
|
3339
|
+
|
|
3340
|
+
next if result.nil?
|
|
3341
|
+
|
|
3342
|
+
copy_async_job_results(async_job_client)
|
|
3343
|
+
return [] if result.empty?
|
|
3344
|
+
|
|
3345
|
+
return JSON.parse(result)
|
|
3346
|
+
end
|
|
3347
|
+
|
|
3348
|
+
raise ApiException.new('Asynchronous call did not finish in expected timeframe.'),
|
|
3349
|
+
'Asynchronous call did not finish in expected timeframe.'
|
|
3350
|
+
end
|
|
3351
|
+
|
|
3352
|
+
# Search for a specific text in a PDF document.
|
|
3353
|
+
# Pages that participate to this operation are specified by start_page and end_page methods.
|
|
3354
|
+
#
|
|
3355
|
+
# @param url Address of the PDF file.
|
|
3356
|
+
# @param text_to_search Text to search.
|
|
3357
|
+
# @param case_sensitive If the search is case sensitive or not.
|
|
3358
|
+
# @param whole_words_only If the search works on whole words or not.
|
|
3359
|
+
# @return List with text positions in the current PDF document.
|
|
3360
|
+
def search_url(url, text_to_search, case_sensitive = false, whole_words_only = false)
|
|
3361
|
+
if text_to_search.nil? || text_to_search.empty?
|
|
3362
|
+
raise ApiException.new('Search text cannot be empty.'), 'Search text cannot be empty.'
|
|
3363
|
+
end
|
|
3364
|
+
|
|
3365
|
+
@parameters['async'] = 'False'
|
|
3366
|
+
@parameters['action'] = 'Search'
|
|
3367
|
+
@parameters['search_text'] = text_to_search
|
|
3368
|
+
@parameters['case_sensitive'] = case_sensitive
|
|
3369
|
+
@parameters['whole_words_only'] = whole_words_only
|
|
3370
|
+
|
|
3371
|
+
@files = {}
|
|
3372
|
+
@parameters['url'] = url
|
|
3373
|
+
|
|
3374
|
+
@headers['Accept'] = 'text/json'
|
|
3375
|
+
|
|
3376
|
+
result = perform_post_as_multipart_formdata
|
|
3377
|
+
return [] if result.nil? || result.empty?
|
|
3378
|
+
|
|
3379
|
+
JSON.parse(result)
|
|
3380
|
+
end
|
|
3381
|
+
|
|
3382
|
+
# Search for a specific text in a PDF document with an asynchronous call.
|
|
3383
|
+
# Pages that participate to this operation are specified by start_page and end_page methods.
|
|
3384
|
+
#
|
|
3385
|
+
# @param url Address of the PDF file.
|
|
3386
|
+
# @param text_to_search Text to search.
|
|
3387
|
+
# @param case_sensitive If the search is case sensitive or not.
|
|
3388
|
+
# @param whole_words_only If the search works on whole words or not.
|
|
3389
|
+
# @return List with text positions in the current PDF document.
|
|
3390
|
+
def search_url_async(url, text_to_search, case_sensitive = false, whole_words_only = false)
|
|
3391
|
+
if text_to_search.nil? || text_to_search.empty?
|
|
3392
|
+
raise ApiException.new('Search text cannot be empty.'), 'Search text cannot be empty.'
|
|
3393
|
+
end
|
|
3394
|
+
|
|
3395
|
+
@parameters['action'] = 'Search'
|
|
3396
|
+
@parameters['search_text'] = text_to_search
|
|
3397
|
+
@parameters['case_sensitive'] = case_sensitive
|
|
3398
|
+
@parameters['whole_words_only'] = whole_words_only
|
|
3399
|
+
|
|
3400
|
+
@files = {}
|
|
3401
|
+
@parameters['url'] = url
|
|
3402
|
+
|
|
3403
|
+
@headers['Accept'] = 'text/json'
|
|
3404
|
+
|
|
3405
|
+
job_id = start_async_job_multipart_form_data
|
|
3406
|
+
|
|
3407
|
+
if job_id.nil? || job_id.empty?
|
|
3408
|
+
raise ApiException.new('An error occurred launching the asynchronous call.'),
|
|
3409
|
+
'An error occurred launching the asynchronous call.'
|
|
3410
|
+
end
|
|
3411
|
+
|
|
3412
|
+
no_pings = 0
|
|
3413
|
+
|
|
3414
|
+
while no_pings < @async_calls_max_pings
|
|
3415
|
+
no_pings += 1
|
|
3416
|
+
|
|
3417
|
+
# sleep for a few seconds before next ping
|
|
3418
|
+
sleep(@async_calls_ping_interval)
|
|
3419
|
+
|
|
3420
|
+
async_job_client = AsyncJobClient.new(@parameters['key'], @job_id)
|
|
3421
|
+
async_job_client.api_endpoint = @api_async_endpoint
|
|
3422
|
+
|
|
3423
|
+
result = async_job_client.result
|
|
3424
|
+
|
|
3425
|
+
next if result.nil?
|
|
3426
|
+
|
|
3427
|
+
copy_async_job_results(async_job_client)
|
|
3428
|
+
return [] if result.empty?
|
|
3429
|
+
|
|
3430
|
+
return JSON.parse(result)
|
|
3431
|
+
end
|
|
3432
|
+
|
|
3433
|
+
raise ApiException.new('Asynchronous call did not finish in expected timeframe.'),
|
|
3434
|
+
'Asynchronous call did not finish in expected timeframe.'
|
|
3435
|
+
end
|
|
3436
|
+
|
|
3437
|
+
# Set Start Page number. Default value is 1 (first page of the document).
|
|
3438
|
+
#
|
|
3439
|
+
# @param start_page Start page number (1-based).
|
|
3440
|
+
def start_page=(start_page)
|
|
3441
|
+
@parameters['start_page'] = start_page
|
|
3442
|
+
end
|
|
3443
|
+
|
|
3444
|
+
# Set End Page number. Default value is 0 (process till the last page of the document).
|
|
3445
|
+
#
|
|
3446
|
+
# @param end_page End page number (1-based).
|
|
3447
|
+
def end_page=(end_page)
|
|
3448
|
+
@parameters['end_page'] = end_page
|
|
3449
|
+
end
|
|
3450
|
+
|
|
3451
|
+
# Set PDF user password.
|
|
3452
|
+
#
|
|
3453
|
+
# @param user_password PDF user password.
|
|
3454
|
+
def user_password=(user_password)
|
|
3455
|
+
@parameters['user_password'] = user_password
|
|
3456
|
+
end
|
|
3457
|
+
|
|
3458
|
+
# Set the text layout. The default value is SelectPdf::TextLayout::ORIGINAL.
|
|
3459
|
+
#
|
|
3460
|
+
# @param text_layout The text layout. Possible values: Original, Reading. Use constants from SelectPdf::TextLayout class.
|
|
3461
|
+
def text_layout=(text_layout)
|
|
3462
|
+
unless [0, 1].include?(text_layout)
|
|
3463
|
+
raise ApiException.new('Allowed values for Text Layout: 0 (Original), 1 (Reading).'), 'Allowed values for Text Layout: 0 (Original), 1 (Reading).'
|
|
3464
|
+
end
|
|
3465
|
+
|
|
3466
|
+
@parameters['text_layout'] = text_layout
|
|
3467
|
+
end
|
|
3468
|
+
|
|
3469
|
+
# Set the output format. The default value is SelectPdf::OutputFormat::TEXT.
|
|
3470
|
+
#
|
|
3471
|
+
# @param output_format The output format. Possible values: Text, Html. Use constants from SelectPdf::OutputFormat class.
|
|
3472
|
+
def output_format=(output_format)
|
|
3473
|
+
unless [0, 1].include?(output_format)
|
|
3474
|
+
raise ApiException.new('Allowed values for Output Format: 0 (Text), 1 (Html).'), 'Allowed values for Output Format: 0 (Text), 1 (Html).'
|
|
3475
|
+
end
|
|
3476
|
+
|
|
3477
|
+
@parameters['output_format'] = output_format
|
|
3478
|
+
end
|
|
3479
|
+
|
|
3480
|
+
# Set the maximum amount of time (in seconds) for this job.
|
|
3481
|
+
# The default value is 30 seconds. Use a larger value (up to 120 seconds allowed) for large documents.
|
|
3482
|
+
#
|
|
3483
|
+
# @param timeout Timeout in seconds.
|
|
3484
|
+
def timeout=(timeout)
|
|
3485
|
+
@parameters['timeout'] = timeout
|
|
3486
|
+
end
|
|
3487
|
+
|
|
3488
|
+
# Set a custom parameter. Do not use this method unless advised by SelectPdf.
|
|
3489
|
+
#
|
|
3490
|
+
# @param parameter_name Parameter name.
|
|
3491
|
+
# @param parameter_value Parameter value.
|
|
3492
|
+
def set_custom_parameter(parameter_name, parameter_value)
|
|
3493
|
+
@parameters[parameter_name] = parameter_value
|
|
1526
3494
|
end
|
|
1527
3495
|
end
|
|
1528
3496
|
end
|