adzap-wicked_pdf 2.0.0.beta5 → 2.0.1
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/.rubocop.yml +7 -2
- data/.rubocop_todo.yml +2 -2
- data/lib/generators/wicked_pdf_generator.rb +1 -1
- data/lib/wicked_pdf/asset_cache.rb +33 -0
- data/lib/wicked_pdf/asset_helper.rb +35 -23
- data/lib/wicked_pdf/binary.rb +19 -3
- data/lib/wicked_pdf/command.rb +150 -10
- data/lib/wicked_pdf/configuration.rb +62 -0
- data/lib/wicked_pdf/document.rb +37 -6
- data/lib/wicked_pdf/middleware.rb +4 -4
- data/lib/wicked_pdf/option_parser.rb +9 -7
- data/lib/wicked_pdf/pdf_helper.rb +1 -1
- data/lib/wicked_pdf/progress.rb +8 -10
- data/lib/wicked_pdf/railtie.rb +4 -6
- data/lib/wicked_pdf/renderer.rb +30 -4
- data/lib/wicked_pdf/version.rb +1 -1
- data/lib/wicked_pdf.rb +27 -1
- data/test/dummy/app/assets/config/manifest.js +3 -0
- data/test/functional/wicked_pdf_asset_helper_test.rb +67 -3
- data/test/test_helper.rb +3 -4
- data/test/unit/wicked_pdf_asset_cache_test.rb +51 -0
- data/test/unit/wicked_pdf_binary_test.rb +58 -10
- data/test/unit/wicked_pdf_command_test.rb +236 -0
- data/test/unit/wicked_pdf_configuration_test.rb +81 -0
- data/test/unit/wicked_pdf_document_test.rb +105 -12
- data/test/unit/wicked_pdf_renderer_test.rb +62 -0
- data/wicked_pdf.gemspec +7 -8
- metadata +41 -56
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee59b976a1f8a763eb2a754baf8727f970fe5f06c88194b42735d5c37e3ac6b7
|
|
4
|
+
data.tar.gz: 543153109daf65bcbe75959aecf5416689fd54481dd31b08bf068322de805551
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc98c1fbb159ec1c228f320d8d79d805d8c9e85927fd3f6b4f96b7d0927c05da270a55d3e87f348a4c7602f350ce855761c3a65f671d133b7bab6e7712cf4296
|
|
7
|
+
data.tar.gz: 237bef4134c63adb17a206b197888e7a6f8845a7fa4f637454420ab91590c40706766a3d14f1d50f60b3c4df168158751716258b8865ae80c639c4afb75e128c
|
data/.rubocop.yml
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
inherit_from: .rubocop_todo.yml
|
|
2
2
|
|
|
3
3
|
AllCops:
|
|
4
|
-
TargetRubyVersion:
|
|
4
|
+
TargetRubyVersion: 3.4
|
|
5
|
+
NewCops: disable
|
|
6
|
+
SuggestExtensions: false
|
|
5
7
|
Exclude:
|
|
6
8
|
- 'test/dummy/**/*'
|
|
9
|
+
- 'spike/**/*'
|
|
10
|
+
- 'gemfiles/**/*'
|
|
11
|
+
- 'vendor/**/*'
|
|
7
12
|
|
|
8
13
|
Metrics/BlockLength:
|
|
9
14
|
Exclude:
|
|
@@ -19,4 +24,4 @@ Style/SymbolArray:
|
|
|
19
24
|
EnforcedStyle: brackets
|
|
20
25
|
|
|
21
26
|
Style/SafeNavigation:
|
|
22
|
-
Enabled: false
|
|
27
|
+
Enabled: false
|
data/.rubocop_todo.yml
CHANGED
|
@@ -28,7 +28,7 @@ Metrics/CyclomaticComplexity:
|
|
|
28
28
|
|
|
29
29
|
# Offense count: 92
|
|
30
30
|
# Configuration parameters: AllowURI, URISchemes.
|
|
31
|
-
|
|
31
|
+
Layout/LineLength:
|
|
32
32
|
Max: 563
|
|
33
33
|
|
|
34
34
|
# Offense count: 14
|
|
@@ -38,7 +38,7 @@ Metrics/MethodLength:
|
|
|
38
38
|
|
|
39
39
|
# Offense count: 4
|
|
40
40
|
Metrics/PerceivedComplexity:
|
|
41
|
-
Max:
|
|
41
|
+
Max: 14
|
|
42
42
|
|
|
43
43
|
# Offense count: 2
|
|
44
44
|
Naming/AccessorMethodName:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
class WickedPdfGenerator < Rails::Generators::Base
|
|
2
|
-
source_root(File.expand_path(File.dirname(__FILE__)
|
|
2
|
+
source_root(File.expand_path("#{File.dirname(__FILE__)}/../../generators/wicked_pdf/templates"))
|
|
3
3
|
def copy_initializer
|
|
4
4
|
copy_file 'wicked_pdf.rb', 'config/initializers/wicked_pdf.rb'
|
|
5
5
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module WickedPdf
|
|
2
|
+
# Process-wide memo for the output of the inlining asset helpers
|
|
3
|
+
# (wicked_pdf_asset_base64, wicked_pdf_stylesheet_link_tag,
|
|
4
|
+
# wicked_pdf_javascript_include_tag).
|
|
5
|
+
#
|
|
6
|
+
# Reading, concatenating and base64-encoding assets is deterministic for a
|
|
7
|
+
# given deployment, but the helpers are invoked once per rendered document —
|
|
8
|
+
# expensive when many documents are rendered in one request (e.g. printing a
|
|
9
|
+
# batch). Enable with `config.cache_assets = true`; disabled by default so
|
|
10
|
+
# environments that recompile assets on change (development) stay live.
|
|
11
|
+
#
|
|
12
|
+
# Cached values are frozen so a stale reference can't be mutated in place.
|
|
13
|
+
class AssetCache
|
|
14
|
+
def initialize
|
|
15
|
+
@store = {}
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def fetch(key)
|
|
20
|
+
@mutex.synchronize do
|
|
21
|
+
@store.fetch(key) { @store[key] = yield.freeze }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def clear
|
|
26
|
+
@mutex.synchronize { @store.clear }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def size
|
|
30
|
+
@mutex.synchronize { @store.size }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -13,25 +13,30 @@ module WickedPdf
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def wicked_pdf_asset_base64(path)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
wicked_pdf_asset_cache(:base64, path) do
|
|
17
|
+
asset = find_asset(path)
|
|
18
|
+
raise "Could not find asset '#{path}'" if asset.nil?
|
|
19
|
+
|
|
20
|
+
base64 = Base64.encode64(asset.to_s).gsub(/\s+/, '')
|
|
21
|
+
"data:#{asset.content_type};base64,#{Rack::Utils.escape(base64)}"
|
|
22
|
+
end
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
def wicked_pdf_stylesheet_link_tag(*sources)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
wicked_pdf_asset_cache(:stylesheet, *sources) do
|
|
27
|
+
stylesheet_contents = sources.collect do |source|
|
|
28
|
+
source = WickedPdf::AssetHelper.add_extension(source, 'css')
|
|
29
|
+
"<style type='text/css'>#{read_asset(source)}</style>"
|
|
30
|
+
end.join("\n")
|
|
31
|
+
|
|
32
|
+
stylesheet_contents.gsub(ASSET_URL_REGEX) do
|
|
33
|
+
if Regexp.last_match[1].starts_with?('data:')
|
|
34
|
+
"url(#{Regexp.last_match[1]})"
|
|
35
|
+
else
|
|
36
|
+
"url(#{wicked_pdf_asset_path(Regexp.last_match[1])})"
|
|
37
|
+
end
|
|
38
|
+
end.html_safe
|
|
39
|
+
end
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
def wicked_pdf_image_tag(img, options = {})
|
|
@@ -44,10 +49,12 @@ module WickedPdf
|
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
def wicked_pdf_javascript_include_tag(*sources)
|
|
47
|
-
sources
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
wicked_pdf_asset_cache(:javascript, *sources) do
|
|
53
|
+
sources.collect do |source|
|
|
54
|
+
source = WickedPdf::AssetHelper.add_extension(source, 'js')
|
|
55
|
+
"<script type='text/javascript'>#{read_asset(source)}</script>"
|
|
56
|
+
end.join("\n").html_safe
|
|
57
|
+
end
|
|
51
58
|
end
|
|
52
59
|
|
|
53
60
|
def wicked_pdf_asset_path(asset)
|
|
@@ -63,6 +70,11 @@ module WickedPdf
|
|
|
63
70
|
# borrowed from actionpack/lib/action_view/helpers/asset_url_helper.rb
|
|
64
71
|
URI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//}
|
|
65
72
|
|
|
73
|
+
# Inlined asset output is deterministic per deployment; memoized process-wide when config.cache_assets is on.
|
|
74
|
+
def wicked_pdf_asset_cache(*key, &block)
|
|
75
|
+
WickedPdf.config.cache_assets ? WickedPdf.asset_cache.fetch(key, &block) : yield
|
|
76
|
+
end
|
|
77
|
+
|
|
66
78
|
def asset_pathname(source)
|
|
67
79
|
if precompiled_or_absolute_asset?(source)
|
|
68
80
|
asset = asset_path(source)
|
|
@@ -95,7 +107,7 @@ module WickedPdf
|
|
|
95
107
|
# will prepend a http or default_protocol to a protocol relative URL
|
|
96
108
|
# or when no protcol is set.
|
|
97
109
|
def prepend_protocol(source)
|
|
98
|
-
protocol = WickedPdf.config
|
|
110
|
+
protocol = WickedPdf.config.default_protocol || 'http'
|
|
99
111
|
if source[0, 2] == '//'
|
|
100
112
|
source = [protocol, ':', source].join
|
|
101
113
|
elsif source[0] != '/' && !source[0, 8].include?('://')
|
|
@@ -107,7 +119,7 @@ module WickedPdf
|
|
|
107
119
|
def precompiled_or_absolute_asset?(source)
|
|
108
120
|
Rails.configuration.assets.compile == false ||
|
|
109
121
|
source.to_s[0] == '/' ||
|
|
110
|
-
source.to_s.match(/\Ahttps
|
|
122
|
+
source.to_s.match(/\Ahttps?:\/\//)
|
|
111
123
|
end
|
|
112
124
|
|
|
113
125
|
def read_asset(source)
|
|
@@ -125,7 +137,7 @@ module WickedPdf
|
|
|
125
137
|
|
|
126
138
|
def read_from_uri(uri)
|
|
127
139
|
asset = Net::HTTP.get(URI(uri))
|
|
128
|
-
asset = gzip(asset) if WickedPdf.config
|
|
140
|
+
asset = gzip(asset) if WickedPdf.config.expect_gzipped_remote_assets
|
|
129
141
|
asset
|
|
130
142
|
end
|
|
131
143
|
|
data/lib/wicked_pdf/binary.rb
CHANGED
|
@@ -5,6 +5,21 @@ module WickedPdf
|
|
|
5
5
|
|
|
6
6
|
attr_reader :path, :version
|
|
7
7
|
|
|
8
|
+
# Shared instance, so the version check (`wkhtmltopdf -V` subprocess) runs
|
|
9
|
+
# once instead of once per render. Re-resolved whenever config.exe_path
|
|
10
|
+
# changes — including back to nil (automatic path detection).
|
|
11
|
+
def self.default
|
|
12
|
+
configured_path = WickedPdf.config.exe_path
|
|
13
|
+
@default = nil if @default && @default_configured_path != configured_path
|
|
14
|
+
@default_configured_path = configured_path
|
|
15
|
+
@default ||= new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.reset_default!
|
|
19
|
+
@default = nil
|
|
20
|
+
@default_configured_path = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
8
23
|
def initialize(binary_path = nil)
|
|
9
24
|
@path = binary_path || find_binary_path
|
|
10
25
|
@version = retrieve_binary_version
|
|
@@ -27,14 +42,15 @@ module WickedPdf
|
|
|
27
42
|
private
|
|
28
43
|
|
|
29
44
|
def retrieve_binary_version
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
# argv form: no shell parsing, so paths with spaces work.
|
|
46
|
+
stdout, = Open3.capture3(@path, '-V')
|
|
47
|
+
parse_version_string(stdout)
|
|
32
48
|
rescue StandardError
|
|
33
49
|
MINIMUM_BINARY_VERSION
|
|
34
50
|
end
|
|
35
51
|
|
|
36
52
|
def find_binary_path
|
|
37
|
-
return WickedPdf.config
|
|
53
|
+
return WickedPdf.config.exe_path if WickedPdf.config.exe_path
|
|
38
54
|
|
|
39
55
|
begin
|
|
40
56
|
detected_path = (defined?(Bundler) ? Bundler.which('wkhtmltopdf') : `which wkhtmltopdf`).chomp
|
data/lib/wicked_pdf/command.rb
CHANGED
|
@@ -2,14 +2,14 @@ module WickedPdf
|
|
|
2
2
|
class Command
|
|
3
3
|
attr_reader :binary, :option_parser
|
|
4
4
|
|
|
5
|
-
def initialize(binary: Binary.
|
|
5
|
+
def initialize(binary: Binary.default, option_parser: OptionParser.new)
|
|
6
6
|
@binary = binary
|
|
7
7
|
@option_parser = option_parser
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
def execute(options, *args)
|
|
11
11
|
command = [binary.path]
|
|
12
|
-
command
|
|
12
|
+
command += global_argv
|
|
13
13
|
command += option_parser.parse(options)
|
|
14
14
|
command += args
|
|
15
15
|
|
|
@@ -18,28 +18,168 @@ module WickedPdf
|
|
|
18
18
|
if track_progress?(options)
|
|
19
19
|
Progress.new(options[:progress]).execute(command)
|
|
20
20
|
else
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
err = run_process(command)
|
|
22
|
+
|
|
23
|
+
raise GenerationError, "Error generating PDF\n Command Error: #{err}" if options[:raise_on_all_errors] && !err.empty?
|
|
24
|
+
|
|
25
|
+
err
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Runs several conversions through shared wkhtmltopdf processes using
|
|
30
|
+
# --read-args-from-stdin, avoiding one process boot per document.
|
|
31
|
+
#
|
|
32
|
+
# Each job is a Hash with:
|
|
33
|
+
# options: wkhtmltopdf options for this conversion only
|
|
34
|
+
# url: input URL (file:///... for local files)
|
|
35
|
+
# output_path: file the PDF is written to
|
|
36
|
+
#
|
|
37
|
+
# Jobs are split into `concurrency` contiguous slices, each processed by
|
|
38
|
+
# its own wkhtmltopdf process. wkhtmltopdf aborts a batch at the first
|
|
39
|
+
# failing conversion, so the failed job is retried as a normal single
|
|
40
|
+
# invocation (for a proper error) and the batch resumed with the rest.
|
|
41
|
+
# Raises WickedPdf::GenerationError if a job cannot be rendered.
|
|
42
|
+
#
|
|
43
|
+
# All argv compilation happens here on the calling thread: OptionParser
|
|
44
|
+
# is not thread-safe (it accumulates tempfile state for inline
|
|
45
|
+
# header/footer content), so worker threads only ever see frozen strings.
|
|
46
|
+
def execute_batch(jobs, concurrency: 1)
|
|
47
|
+
return if jobs.empty?
|
|
48
|
+
|
|
49
|
+
compiled = jobs.map { |job| BatchJob.new(job, batch_argv(job)) }
|
|
50
|
+
|
|
51
|
+
slices = compiled.each_slice(batch_slice_size(jobs.size, concurrency)).to_a
|
|
52
|
+
errors = Array.new(slices.size)
|
|
53
|
+
|
|
54
|
+
slices.each_with_index.map do |slice, index|
|
|
55
|
+
Thread.new do
|
|
56
|
+
process_batch_slice(slice)
|
|
25
57
|
rescue StandardError => e
|
|
26
|
-
|
|
58
|
+
errors[index] = e
|
|
27
59
|
end
|
|
60
|
+
end.each(&:join)
|
|
28
61
|
|
|
29
|
-
|
|
30
|
-
|
|
62
|
+
error = errors.compact.first
|
|
63
|
+
raise error if error
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A batch job with its argv compiled up front (thread-safe to run).
|
|
67
|
+
BatchJob = Struct.new(:job, :argv) do
|
|
68
|
+
def output_path
|
|
69
|
+
job[:output_path]
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def line
|
|
73
|
+
@line ||= argv.join(' ')
|
|
31
74
|
end
|
|
32
75
|
end
|
|
33
76
|
|
|
34
77
|
private
|
|
35
78
|
|
|
79
|
+
def batch_slice_size(job_count, concurrency)
|
|
80
|
+
concurrency = concurrency.clamp(1, job_count)
|
|
81
|
+
(job_count.to_f / concurrency).ceil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def process_batch_slice(batch_jobs)
|
|
85
|
+
pending = batch_jobs
|
|
86
|
+
until pending.empty?
|
|
87
|
+
run_batch_process(pending.map(&:line))
|
|
88
|
+
pending = pending.drop_while { |batch_job| batch_output_complete?(batch_job) }
|
|
89
|
+
break if pending.empty?
|
|
90
|
+
|
|
91
|
+
retry_batch_job(pending.shift)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# A failed conversion aborts its batch process, leaving this job and all
|
|
96
|
+
# jobs after it unrendered. Retrying it individually either renders it
|
|
97
|
+
# (transient failure) or produces a proper single-invocation error.
|
|
98
|
+
def retry_batch_job(batch_job)
|
|
99
|
+
command = [binary.path] + batch_job.argv.map { |token| unquote_batch_token(token) }
|
|
100
|
+
print_command(command.inspect) if in_development_mode?
|
|
101
|
+
|
|
102
|
+
err = run_process(command)
|
|
103
|
+
return if batch_output_complete?(batch_job)
|
|
104
|
+
|
|
105
|
+
raise GenerationError, "Error generating PDF for #{batch_job.output_path}\n Command Error: #{err}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# wkhtmltopdf aborts a failing batch conversion partway, which can leave
|
|
109
|
+
# a non-empty but truncated file behind. Only a well-formed PDF (header
|
|
110
|
+
# and end-of-file marker) counts as done; anything else is retried.
|
|
111
|
+
def batch_output_complete?(batch_job)
|
|
112
|
+
size = File.size?(batch_job.output_path)
|
|
113
|
+
return false unless size
|
|
114
|
+
|
|
115
|
+
File.open(batch_job.output_path, 'rb') do |file|
|
|
116
|
+
return false unless file.read(5) == '%PDF-'
|
|
117
|
+
|
|
118
|
+
file.seek([size - 64, 0].max)
|
|
119
|
+
file.read.include?('%%EOF')
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def batch_argv(job)
|
|
124
|
+
argv = global_argv
|
|
125
|
+
argv += option_parser.parse(job[:options].dup)
|
|
126
|
+
argv += [job[:url], job[:output_path]]
|
|
127
|
+
argv.map { |arg| batch_token(arg.to_s) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Flags controlled by gem configuration rather than per-render options.
|
|
131
|
+
def global_argv
|
|
132
|
+
WickedPdf.config.enable_local_file_access ? ['--enable-local-file-access'] : []
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# wkhtmltopdf tokenises each stdin line itself: spaces separate arguments
|
|
136
|
+
# unless quoted. Only quoted-space handling is verified, so reject
|
|
137
|
+
# characters with unknown escaping semantics rather than corrupt a line.
|
|
138
|
+
def batch_token(arg)
|
|
139
|
+
raise ArgumentError, "Unsupported character in wkhtmltopdf batch argument: #{arg.inspect}" if arg.match?(/["\n\r]/)
|
|
140
|
+
|
|
141
|
+
arg.include?(' ') ? %("#{arg}") : arg
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Reverses batch_token quoting when a compiled token is passed directly
|
|
145
|
+
# as a process argument instead of through the stdin line protocol.
|
|
146
|
+
def unquote_batch_token(token)
|
|
147
|
+
token.start_with?('"') && token.end_with?('"') ? token[1..-2] : token
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def run_process(command)
|
|
151
|
+
Open3.popen3(*command) do |_stdin, _stdout, stderr|
|
|
152
|
+
stderr.read
|
|
153
|
+
end
|
|
154
|
+
rescue StandardError => e
|
|
155
|
+
raise Error, "Failed to execute:\n#{command}\nError: #{e}"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def run_batch_process(lines)
|
|
159
|
+
print_command(lines.inspect) if in_development_mode?
|
|
160
|
+
|
|
161
|
+
err = nil
|
|
162
|
+
Open3.popen3(binary.path, '--read-args-from-stdin') do |stdin, stdout, stderr, wait_thr|
|
|
163
|
+
stdout_reader = Thread.new { stdout.read }
|
|
164
|
+
stderr_reader = Thread.new { stderr.read }
|
|
165
|
+
lines.each { |line| stdin.puts(line) }
|
|
166
|
+
stdin.close
|
|
167
|
+
stdout_reader.value
|
|
168
|
+
err = stderr_reader.value
|
|
169
|
+
wait_thr.value
|
|
170
|
+
end
|
|
171
|
+
err
|
|
172
|
+
rescue StandardError => e
|
|
173
|
+
raise Error, "Failed to execute wkhtmltopdf batch:\n#{lines.inspect}\nError: #{e}"
|
|
174
|
+
end
|
|
175
|
+
|
|
36
176
|
def in_development_mode?
|
|
37
177
|
defined?(Rails.env) && Rails.env.development?
|
|
38
178
|
end
|
|
39
179
|
|
|
40
180
|
def print_command(cmd)
|
|
41
181
|
# TODO: if no Rails what then?
|
|
42
|
-
Rails.logger.debug
|
|
182
|
+
Rails.logger.debug "[wicked_pdf]: #{cmd}"
|
|
43
183
|
end
|
|
44
184
|
|
|
45
185
|
def track_progress?(options)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module WickedPdf
|
|
2
|
+
# Global gem configuration, set via WickedPdf.configure or by assigning a
|
|
3
|
+
# Hash to WickedPdf.config (kept for backward compatibility).
|
|
4
|
+
#
|
|
5
|
+
# Two kinds of values live here:
|
|
6
|
+
#
|
|
7
|
+
# * Typed settings, declared as accessors below, which control the gem
|
|
8
|
+
# itself (binary location, middleware URLs, asset handling).
|
|
9
|
+
# * default_options, a Hash of wkhtmltopdf render options (:footer,
|
|
10
|
+
# :orientation, :dpi, ...) merged under every render's own options.
|
|
11
|
+
# Unknown keys assigned through a Hash or #[]= land here.
|
|
12
|
+
class Configuration
|
|
13
|
+
SETTINGS = [
|
|
14
|
+
:exe_path,
|
|
15
|
+
:basic_auth,
|
|
16
|
+
:root_url,
|
|
17
|
+
:default_protocol,
|
|
18
|
+
:expect_gzipped_remote_assets,
|
|
19
|
+
:enable_local_file_access,
|
|
20
|
+
:cache_assets
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
attr_accessor(*SETTINGS)
|
|
24
|
+
attr_reader :default_options
|
|
25
|
+
|
|
26
|
+
def self.from_hash(hash)
|
|
27
|
+
new.tap do |config|
|
|
28
|
+
(hash || {}).each { |key, value| config[key] = value }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize
|
|
33
|
+
@basic_auth = false
|
|
34
|
+
@enable_local_file_access = true
|
|
35
|
+
@cache_assets = false
|
|
36
|
+
@default_options = {}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def []=(key, value)
|
|
40
|
+
if setting?(key)
|
|
41
|
+
public_send("#{key}=", value)
|
|
42
|
+
else
|
|
43
|
+
default_options[key.to_sym] = value
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def [](key)
|
|
48
|
+
setting?(key) ? public_send(key) : default_options[key.to_sym]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_h
|
|
52
|
+
settings = SETTINGS.to_h { |key| [key, public_send(key)] }
|
|
53
|
+
settings.compact.merge(default_options)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def setting?(key)
|
|
59
|
+
SETTINGS.include?(key.to_sym)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/wicked_pdf/document.rb
CHANGED
|
@@ -10,7 +10,7 @@ module WickedPdf
|
|
|
10
10
|
|
|
11
11
|
def pdf_from_string(string, options = {})
|
|
12
12
|
options = options.dup
|
|
13
|
-
options.merge!(WickedPdf.config) { |_key, option, _config| option }
|
|
13
|
+
options.merge!(WickedPdf.config.default_options) { |_key, option, _config| option }
|
|
14
14
|
string_file = WickedPdf::Tempfile.new('wicked_pdf.html', options[:temp_path])
|
|
15
15
|
string_file.write_in_chunks(string)
|
|
16
16
|
|
|
@@ -22,21 +22,52 @@ module WickedPdf
|
|
|
22
22
|
string_file.close! if string_file
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# Renders many HTML strings via Command#execute_batch (shared wkhtmltopdf
|
|
26
|
+
# processes instead of one boot per document). Items are Hashes of
|
|
27
|
+
# { html: String, options: Hash }; each item's options apply to that
|
|
28
|
+
# conversion only. Returns the PDF strings in item order.
|
|
29
|
+
def pdfs_from_strings(items, concurrency: 1)
|
|
30
|
+
tempfiles = []
|
|
31
|
+
|
|
32
|
+
jobs = items.map do |item|
|
|
33
|
+
options = (item[:options] || {}).dup
|
|
34
|
+
options.merge!(WickedPdf.config.default_options) { |_key, option, _config| option }
|
|
35
|
+
|
|
36
|
+
input = WickedPdf::Tempfile.new('wicked_pdf_batch.html', options[:temp_path])
|
|
37
|
+
tempfiles << input
|
|
38
|
+
input.write_in_chunks(item.fetch(:html))
|
|
39
|
+
|
|
40
|
+
output = WickedPdf::Tempfile.new('wicked_pdf_batch.pdf', options[:temp_path])
|
|
41
|
+
tempfiles << output
|
|
42
|
+
|
|
43
|
+
{ options: options, url: "file:///#{input.path}", output_path: output.path, output_file: output }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
@command.execute_batch(jobs, concurrency: concurrency)
|
|
47
|
+
|
|
48
|
+
jobs.map do |job|
|
|
49
|
+
pdf = job[:output_file].read_in_chunks
|
|
50
|
+
raise GenerationError, "PDF could not be generated!\n Output: #{job[:output_path]}" if pdf.rstrip.empty?
|
|
51
|
+
|
|
52
|
+
pdf
|
|
53
|
+
end
|
|
54
|
+
ensure
|
|
55
|
+
tempfiles.each(&:close!)
|
|
56
|
+
end
|
|
57
|
+
|
|
25
58
|
def pdf_from_url(url, options = {})
|
|
26
59
|
# merge in global config options
|
|
27
|
-
options.merge!(WickedPdf.config) { |_key, option, _config| option }
|
|
60
|
+
options.merge!(WickedPdf.config.default_options) { |_key, option, _config| option }
|
|
28
61
|
generated_pdf_file = WickedPdf::Tempfile.new('wicked_pdf_generated_file.pdf', options[:temp_path])
|
|
29
62
|
return_file = options.delete(:return_file)
|
|
30
63
|
|
|
31
64
|
err = @command.execute(options, url, generated_pdf_file.path.to_s)
|
|
32
65
|
|
|
33
|
-
if return_file
|
|
34
|
-
return generated_pdf_file
|
|
35
|
-
end
|
|
66
|
+
return generated_pdf_file if return_file
|
|
36
67
|
|
|
37
68
|
pdf = generated_pdf_file.read_in_chunks
|
|
38
69
|
|
|
39
|
-
raise "PDF could not be generated!\n Command Error: #{err}" if pdf && pdf.rstrip.empty?
|
|
70
|
+
raise GenerationError, "PDF could not be generated!\n Command Error: #{err}" if pdf && pdf.rstrip.empty?
|
|
40
71
|
|
|
41
72
|
pdf
|
|
42
73
|
rescue Errno::EINVAL => e
|
|
@@ -2,7 +2,7 @@ module WickedPdf
|
|
|
2
2
|
class Middleware
|
|
3
3
|
def initialize(app, options = {}, conditions = {})
|
|
4
4
|
@app = app
|
|
5
|
-
@options =
|
|
5
|
+
@options = WickedPdf.config.default_options.merge(options)
|
|
6
6
|
@conditions = conditions
|
|
7
7
|
@command = command(options[:wkhtmltopdf])
|
|
8
8
|
end
|
|
@@ -42,9 +42,9 @@ module WickedPdf
|
|
|
42
42
|
# Change relative paths to absolute
|
|
43
43
|
def translate_paths(body, env)
|
|
44
44
|
# Host with protocol
|
|
45
|
-
root = WickedPdf.config
|
|
45
|
+
root = WickedPdf.config.root_url || "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}/"
|
|
46
46
|
|
|
47
|
-
body.gsub(/(href|src)=(['"])\/([
|
|
47
|
+
body.gsub(/(href|src)=(['"])\/([^"']*|[^"']*)['"]/, "\\1=\\2#{root}\\3\\2")
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def rendering_pdf?
|
|
@@ -73,7 +73,7 @@ module WickedPdf
|
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
true
|
|
77
77
|
else
|
|
78
78
|
request_path_is_pdf
|
|
79
79
|
end
|
|
@@ -20,6 +20,7 @@ module WickedPdf
|
|
|
20
20
|
def parse_extra(options)
|
|
21
21
|
return [] if options[:extra].nil?
|
|
22
22
|
return options[:extra].split if options[:extra].respond_to?(:split)
|
|
23
|
+
|
|
23
24
|
options[:extra]
|
|
24
25
|
end
|
|
25
26
|
|
|
@@ -37,6 +38,7 @@ module WickedPdf
|
|
|
37
38
|
unless options.blank?
|
|
38
39
|
[:header, :footer].collect do |hf|
|
|
39
40
|
next if options[hf].blank?
|
|
41
|
+
|
|
40
42
|
opt_hf = options[hf]
|
|
41
43
|
r += make_options(opt_hf, [:center, :font_name, :left, :right], hf.to_s)
|
|
42
44
|
r += make_options(opt_hf, [:font_size, :spacing], hf.to_s, :numeric)
|
|
@@ -49,9 +51,7 @@ module WickedPdf
|
|
|
49
51
|
options[hf][:html] = {}
|
|
50
52
|
options[hf][:html][:url] = "file:///#{tf.path}"
|
|
51
53
|
end
|
|
52
|
-
|
|
53
|
-
r += make_option("#{hf}-html", opt_hf[:html][:url]) unless opt_hf[:html][:url].blank?
|
|
54
|
-
end
|
|
54
|
+
r += make_option("#{hf}-html", opt_hf[:html][:url]) if !opt_hf[:html].blank? && !opt_hf[:html][:url].blank?
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
r
|
|
@@ -60,6 +60,7 @@ module WickedPdf
|
|
|
60
60
|
def parse_cover(argument)
|
|
61
61
|
arg = argument.to_s
|
|
62
62
|
return [] if arg.blank?
|
|
63
|
+
|
|
63
64
|
# Filesystem path or URL - hand off to wkhtmltopdf
|
|
64
65
|
if argument.is_a?(Pathname) || (arg[0, 4] == 'http')
|
|
65
66
|
['cover', arg]
|
|
@@ -74,6 +75,7 @@ module WickedPdf
|
|
|
74
75
|
|
|
75
76
|
def parse_toc(options)
|
|
76
77
|
return [] if options.nil?
|
|
78
|
+
|
|
77
79
|
r = ['toc']
|
|
78
80
|
unless options.blank?
|
|
79
81
|
r += make_options(options, [:font_name, :header_text], 'toc')
|
|
@@ -172,11 +174,12 @@ module WickedPdf
|
|
|
172
174
|
|
|
173
175
|
def make_options(options, names, prefix = '', type = :string)
|
|
174
176
|
return [] if options.nil?
|
|
177
|
+
|
|
175
178
|
names.collect do |o|
|
|
176
179
|
if options[o].blank?
|
|
177
180
|
[]
|
|
178
181
|
else
|
|
179
|
-
make_option("#{prefix.blank? ? '' : prefix
|
|
182
|
+
make_option("#{prefix.blank? ? '' : "#{prefix}-"}#{o}",
|
|
180
183
|
options[o],
|
|
181
184
|
type)
|
|
182
185
|
end
|
|
@@ -184,9 +187,8 @@ module WickedPdf
|
|
|
184
187
|
end
|
|
185
188
|
|
|
186
189
|
def make_option(name, value, type = :string)
|
|
187
|
-
if value.is_a?(Array)
|
|
188
|
-
|
|
189
|
-
end
|
|
190
|
+
return value.collect { |v| make_option(name, v, type) } if value.is_a?(Array)
|
|
191
|
+
|
|
190
192
|
if type == :name_value
|
|
191
193
|
parts = value.to_s.split(' ')
|
|
192
194
|
["--#{name.tr('_', '-')}", *parts]
|
|
@@ -3,7 +3,7 @@ module WickedPdf
|
|
|
3
3
|
def self.prepended(base)
|
|
4
4
|
# Protect from trying to augment modules that appear
|
|
5
5
|
# as the result of adding other gems.
|
|
6
|
-
|
|
6
|
+
nil if base != ActionController::Base
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def render_to_string(options = nil, *args, &block)
|