adzap-wicked_pdf 2.0.0.beta1

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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.github/issue_template.md +15 -0
  3. data/.gitignore +21 -0
  4. data/.rubocop.yml +22 -0
  5. data/.rubocop_todo.yml +63 -0
  6. data/.travis.yml +30 -0
  7. data/CHANGELOG.md +135 -0
  8. data/Gemfile +3 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +446 -0
  11. data/Rakefile +31 -0
  12. data/gemfiles/4.2.gemfile +6 -0
  13. data/gemfiles/5.0.gemfile +6 -0
  14. data/gemfiles/5.1.gemfile +6 -0
  15. data/gemfiles/5.2.gemfile +9 -0
  16. data/gemfiles/rails_edge.gemfile +9 -0
  17. data/generators/wicked_pdf/templates/wicked_pdf.rb +21 -0
  18. data/generators/wicked_pdf/wicked_pdf_generator.rb +7 -0
  19. data/init.rb +2 -0
  20. data/lib/generators/wicked_pdf_generator.rb +6 -0
  21. data/lib/wicked_pdf.rb +29 -0
  22. data/lib/wicked_pdf/asset_helper.rb +141 -0
  23. data/lib/wicked_pdf/binary.rb +56 -0
  24. data/lib/wicked_pdf/command.rb +52 -0
  25. data/lib/wicked_pdf/document.rb +47 -0
  26. data/lib/wicked_pdf/middleware.rb +101 -0
  27. data/lib/wicked_pdf/option_parser.rb +220 -0
  28. data/lib/wicked_pdf/pdf_helper.rb +17 -0
  29. data/lib/wicked_pdf/progress.rb +33 -0
  30. data/lib/wicked_pdf/railtie.rb +19 -0
  31. data/lib/wicked_pdf/renderer.rb +121 -0
  32. data/lib/wicked_pdf/tempfile.rb +13 -0
  33. data/lib/wicked_pdf/version.rb +3 -0
  34. data/test/dummy/app/assets/javascripts/application.js +16 -0
  35. data/test/dummy/app/assets/javascripts/wicked.js +1 -0
  36. data/test/dummy/app/assets/stylesheets/application.css +15 -0
  37. data/test/dummy/app/assets/stylesheets/wicked.css +1 -0
  38. data/test/dummy/config/database.yml +3 -0
  39. data/test/dummy/config/routes.rb +5 -0
  40. data/test/dummy/log/.gitignore +1 -0
  41. data/test/dummy/public/favicon.ico +0 -0
  42. data/test/fixtures/database.yml +4 -0
  43. data/test/fixtures/document_with_long_line.html +16 -0
  44. data/test/fixtures/wicked.css +1 -0
  45. data/test/fixtures/wicked.js +1 -0
  46. data/test/functional/pdf_helper_test.rb +61 -0
  47. data/test/functional/wicked_pdf_asset_helper_test.rb +118 -0
  48. data/test/test_helper.rb +33 -0
  49. data/test/unit/wicked_pdf_binary_test.rb +52 -0
  50. data/test/unit/wicked_pdf_command_test.rb +4 -0
  51. data/test/unit/wicked_pdf_document_test.rb +60 -0
  52. data/test/unit/wicked_pdf_option_parser_test.rb +128 -0
  53. data/test/unit/wicked_pdf_renderer_test.rb +43 -0
  54. data/test/unit/wicked_pdf_test.rb +8 -0
  55. data/test/unit/wkhtmltopdf_location_test.rb +50 -0
  56. data/wicked_pdf.gemspec +38 -0
  57. metadata +249 -0
@@ -0,0 +1,101 @@
1
+ module WickedPdf
2
+ class Middleware
3
+ def initialize(app, options = {}, conditions = {})
4
+ @app = app
5
+ @options = (WickedPdf.config || {}).merge(options)
6
+ @conditions = conditions
7
+ @command = command(options[:wkhtmltopdf])
8
+ end
9
+
10
+ def call(env)
11
+ @request = Rack::Request.new(env)
12
+ @render_pdf = false
13
+
14
+ set_request_to_render_as_pdf(env) if render_as_pdf?
15
+ status, headers, response = @app.call(env)
16
+
17
+ if rendering_pdf? && headers['Content-Type'] =~ /text\/html|application\/xhtml\+xml/
18
+ body = response.respond_to?(:body) ? response.body : response.join
19
+ body = body.join if body.is_a?(Array)
20
+
21
+ body = WickedPdf::Document.new(@command).pdf_from_string(translate_paths(body, env), @options)
22
+
23
+ response = [body]
24
+
25
+ # Do not cache PDFs
26
+ headers.delete('ETag')
27
+ headers.delete('Cache-Control')
28
+
29
+ headers['Content-Length'] = (body.respond_to?(:bytesize) ? body.bytesize : body.size).to_s
30
+ headers['Content-Type'] = 'application/pdf'
31
+ if @options.fetch(:disposition, '') == 'attachment'
32
+ headers['Content-Disposition'] = 'attachment'
33
+ headers['Content-Transfer-Encoding'] = 'binary'
34
+ end
35
+ end
36
+
37
+ [status, headers, response]
38
+ end
39
+
40
+ private
41
+
42
+ # Change relative paths to absolute
43
+ def translate_paths(body, env)
44
+ # Host with protocol
45
+ root = WickedPdf.config[:root_url] || "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}/"
46
+
47
+ body.gsub(/(href|src)=(['"])\/([^\"']*|[^"']*)['"]/, '\1=\2' + root + '\3\2')
48
+ end
49
+
50
+ def rendering_pdf?
51
+ @render_pdf
52
+ end
53
+
54
+ def render_as_pdf?
55
+ request_path_is_pdf = @request.path.match(%r{\.pdf$})
56
+
57
+ if request_path_is_pdf && @conditions[:only]
58
+ rules = [@conditions[:only]].flatten
59
+ rules.any? do |pattern|
60
+ if pattern.is_a?(Regexp)
61
+ @request.fullpath =~ pattern
62
+ else
63
+ @request.path[0, pattern.length] == pattern
64
+ end
65
+ end
66
+ elsif request_path_is_pdf && @conditions[:except]
67
+ rules = [@conditions[:except]].flatten
68
+ rules.map do |pattern|
69
+ if pattern.is_a?(Regexp)
70
+ return false if @request.fullpath =~ pattern
71
+ elsif @request.path[0, pattern.length] == pattern
72
+ return false
73
+ end
74
+ end
75
+
76
+ return true
77
+ else
78
+ request_path_is_pdf
79
+ end
80
+ end
81
+
82
+ def set_request_to_render_as_pdf(env)
83
+ @render_pdf = true
84
+ %w[PATH_INFO REQUEST_URI].each { |e| env[e] = env[e].sub(%r{\.pdf\b}, '') }
85
+ env['HTTP_ACCEPT'] = concat(env['HTTP_ACCEPT'], Rack::Mime.mime_type('.html'))
86
+ env['Rack-Middleware-WickedPdf'] = 'true'
87
+ end
88
+
89
+ def concat(accepts, type)
90
+ (accepts || '').split(',').unshift(type).compact.join(',')
91
+ end
92
+
93
+ def command(binary_path)
94
+ if binary_path
95
+ WickedPdf::Command.new
96
+ else
97
+ WickedPdf::Command.new(binary: WickedPdf::Binary.new(binary_path))
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,220 @@
1
+ module WickedPdf
2
+ class OptionParser
3
+ BINARY_VERSION_WITHOUT_DASHES = Gem::Version.new('0.12.0')
4
+
5
+ attr_reader :binary_version
6
+
7
+ def initialize(binary_version = WickedPdf::Binary::DEFAULT_BINARY_VERSION)
8
+ @binary_version = binary_version
9
+ end
10
+
11
+ def parse(options)
12
+ [
13
+ parse_extra(options),
14
+ parse_others(options),
15
+ parse_global(options),
16
+ parse_outline(options.delete(:outline)),
17
+ parse_header_footer(:header => options.delete(:header),
18
+ :footer => options.delete(:footer),
19
+ :layout => options[:layout]),
20
+ parse_cover(options.delete(:cover)),
21
+ parse_toc(options.delete(:toc)),
22
+ parse_basic_auth(options)
23
+ ].flatten
24
+ end
25
+
26
+ def format_option(name)
27
+ if binary_version < BINARY_VERSION_WITHOUT_DASHES
28
+ "--#{name}"
29
+ else
30
+ name
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def parse_extra(options)
37
+ return [] if options[:extra].nil?
38
+ return options[:extra].split if options[:extra].respond_to?(:split)
39
+ options[:extra]
40
+ end
41
+
42
+ def parse_basic_auth(options)
43
+ if options[:basic_auth]
44
+ user, passwd = Base64.decode64(options[:basic_auth]).split(':')
45
+ ['--username', user, '--password', passwd]
46
+ else
47
+ []
48
+ end
49
+ end
50
+
51
+ def parse_header_footer(options)
52
+ r = []
53
+ unless options.blank?
54
+ [:header, :footer].collect do |hf|
55
+ next if options[hf].blank?
56
+ opt_hf = options[hf]
57
+ r += make_options(opt_hf, [:center, :font_name, :left, :right], hf.to_s)
58
+ r += make_options(opt_hf, [:font_size, :spacing], hf.to_s, :numeric)
59
+ r += make_options(opt_hf, [:line], hf.to_s, :boolean)
60
+ if options[hf] && options[hf][:content]
61
+ @hf_tempfiles = [] unless defined?(@hf_tempfiles)
62
+ @hf_tempfiles.push(tf = WickedPdf::Tempfile.new("wicked_#{hf}_pdf.html"))
63
+ tf.write options[hf][:content]
64
+ tf.flush
65
+ options[hf][:html] = {}
66
+ options[hf][:html][:url] = "file:///#{tf.path}"
67
+ end
68
+ unless opt_hf[:html].blank?
69
+ r += make_option("#{hf}-html", opt_hf[:html][:url]) unless opt_hf[:html][:url].blank?
70
+ end
71
+ end
72
+ end
73
+ r
74
+ end
75
+
76
+ def parse_cover(argument)
77
+ arg = argument.to_s
78
+ return [] if arg.blank?
79
+ # Filesystem path or URL - hand off to wkhtmltopdf
80
+ if argument.is_a?(Pathname) || (arg[0, 4] == 'http')
81
+ [format_option('cover'), arg]
82
+ else # HTML content
83
+ @hf_tempfiles ||= []
84
+ @hf_tempfiles << tf = WickedPdf::Tempfile.new('wicked_cover_pdf.html')
85
+ tf.write arg
86
+ tf.flush
87
+ [format_option('cover'), tf.path]
88
+ end
89
+ end
90
+
91
+ def parse_toc(options)
92
+ return [] if options.nil?
93
+ r = [format_option('toc')]
94
+ unless options.blank?
95
+ r += make_options(options, [:font_name, :header_text], 'toc')
96
+ r += make_options(options, [:xsl_style_sheet])
97
+ r += make_options(options, [:depth,
98
+ :header_fs,
99
+ :text_size_shrink,
100
+ :l1_font_size,
101
+ :l2_font_size,
102
+ :l3_font_size,
103
+ :l4_font_size,
104
+ :l5_font_size,
105
+ :l6_font_size,
106
+ :l7_font_size,
107
+ :level_indentation,
108
+ :l1_indentation,
109
+ :l2_indentation,
110
+ :l3_indentation,
111
+ :l4_indentation,
112
+ :l5_indentation,
113
+ :l6_indentation,
114
+ :l7_indentation], 'toc', :numeric)
115
+ r += make_options(options, [:no_dots,
116
+ :disable_links,
117
+ :disable_back_links], 'toc', :boolean)
118
+ r += make_options(options, [:disable_dotted_lines,
119
+ :disable_toc_links], nil, :boolean)
120
+ end
121
+ r
122
+ end
123
+
124
+ def parse_outline(options)
125
+ r = []
126
+ unless options.blank?
127
+ r = make_options(options, [:outline], '', :boolean)
128
+ r += make_options(options, [:outline_depth], '', :numeric)
129
+ end
130
+ r
131
+ end
132
+
133
+ def parse_margins(options)
134
+ make_options(options, [:top, :bottom, :left, :right], 'margin', :numeric)
135
+ end
136
+
137
+ def parse_global(options)
138
+ r = []
139
+ unless options.blank?
140
+ r += make_options(options, [:orientation,
141
+ :dpi,
142
+ :page_size,
143
+ :page_width,
144
+ :title])
145
+ r += make_options(options, [:lowquality,
146
+ :grayscale,
147
+ :no_pdf_compression], '', :boolean)
148
+ r += make_options(options, [:image_dpi,
149
+ :image_quality,
150
+ :page_height], '', :numeric)
151
+ r += parse_margins(options.delete(:margin))
152
+ end
153
+ r
154
+ end
155
+
156
+ def parse_others(options)
157
+ r = []
158
+ unless options.blank?
159
+ r += make_options(options, [:proxy,
160
+ :username,
161
+ :password,
162
+ :encoding,
163
+ :user_style_sheet,
164
+ :viewport_size,
165
+ :window_status])
166
+ r += make_options(options, [:cookie,
167
+ :post], '', :name_value)
168
+ r += make_options(options, [:redirect_delay,
169
+ :zoom,
170
+ :page_offset,
171
+ :javascript_delay], '', :numeric)
172
+ r += make_options(options, [:book,
173
+ :default_header,
174
+ :disable_javascript,
175
+ :enable_plugins,
176
+ :disable_internal_links,
177
+ :disable_external_links,
178
+ :print_media_type,
179
+ :disable_smart_shrinking,
180
+ :use_xserver,
181
+ :no_background,
182
+ :images,
183
+ :no_images,
184
+ :no_stop_slow_scripts], '', :boolean)
185
+ end
186
+ r
187
+ end
188
+
189
+ def make_options(options, names, prefix = '', type = :string)
190
+ return [] if options.nil?
191
+ names.collect do |o|
192
+ if options[o].blank?
193
+ []
194
+ else
195
+ make_option("#{prefix.blank? ? '' : prefix + '-'}#{o}",
196
+ options[o],
197
+ type)
198
+ end
199
+ end
200
+ end
201
+
202
+ def make_option(name, value, type = :string)
203
+ if value.is_a?(Array)
204
+ return value.collect { |v| make_option(name, v, type) }
205
+ end
206
+ if type == :name_value
207
+ parts = value.to_s.split(' ')
208
+ ["--#{name.tr('_', '-')}", *parts]
209
+ elsif type == :boolean
210
+ if value
211
+ ["--#{name.tr('_', '-')}"]
212
+ else
213
+ []
214
+ end
215
+ else
216
+ ["--#{name.tr('_', '-')}", value.to_s]
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,17 @@
1
+ module WickedPdf
2
+ module PdfHelper
3
+ def self.prepended(base)
4
+ # Protect from trying to augment modules that appear
5
+ # as the result of adding other gems.
6
+ return if base != ActionController::Base
7
+ end
8
+
9
+ def render_to_string(options = nil, *args, &block)
10
+ if options.is_a?(Hash) && options.key?(:pdf)
11
+ WickedPdf::Renderer.new(self).render(options)
12
+ else
13
+ super
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ module WickedPdf
2
+ class Progress
3
+ require 'pty' # no support for windows
4
+ require 'English'
5
+
6
+ def initialize(callback = nil)
7
+ @callback = callback
8
+ end
9
+
10
+ def execute(command)
11
+ output = []
12
+ begin
13
+ PTY.spawn(command.join(' ')) do |stdout, _stdin, pid|
14
+ begin
15
+ stdout.sync
16
+ stdout.each_line("\r") do |line|
17
+ output << line.chomp
18
+ @callback.call(line) if @callback
19
+ end
20
+ rescue Errno::EIO # rubocop:disable Lint/HandleExceptions
21
+ # child process is terminated, this is expected behaviour
22
+ ensure
23
+ ::Process.wait pid
24
+ end
25
+ end
26
+ rescue PTY::ChildExited
27
+ puts 'The child process exited!'
28
+ end
29
+ err = output.join('\n')
30
+ raise "#{command} failed (exitstatus 0). Output was: #{err}" unless $CHILD_STATUS && $CHILD_STATUS.exitstatus.zero?
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ require 'wicked_pdf/pdf_helper'
2
+ require 'wicked_pdf/renderer'
3
+ require 'wicked_pdf/asset_helper'
4
+
5
+ module WickedPdf
6
+ class Railtie < Rails::Railtie
7
+ initializer 'wicked_pdf.register' do |_app|
8
+ ActionController::Base.send :prepend, PdfHelper
9
+ ActionController::Renderers.add :pdf do |template, options|
10
+ WickedPdf::Renderer.new(self).render(options.merge(:pdf => template))
11
+ end
12
+ ActionView::Base.send :include, WickedPdf::AssetHelper
13
+ end
14
+ end
15
+ end
16
+
17
+ if Mime::Type.lookup_by_extension(:pdf).nil?
18
+ Mime::Type.register('application/pdf', :pdf)
19
+ end
@@ -0,0 +1,121 @@
1
+ module WickedPdf
2
+ class Renderer
3
+ attr_reader :controller
4
+
5
+ delegate :request, :send_data, :controller_path, :action_name, :to => :controller
6
+
7
+ def initialize(controller)
8
+ @controller = controller
9
+ @hf_tempfiles = []
10
+ end
11
+
12
+ def render(options)
13
+ options[:basic_auth] = set_basic_auth(options)
14
+ make_and_send_pdf(options.delete(:pdf), (WickedPdf.config || {}).merge(options))
15
+ end
16
+
17
+ def render_to_string(options)
18
+ options[:basic_auth] = set_basic_auth(options)
19
+ options.delete :pdf
20
+ make_pdf((WickedPdf.config || {}).merge(options))
21
+ end
22
+
23
+ private
24
+
25
+ def set_basic_auth(options = {})
26
+ options[:basic_auth] ||= WickedPdf.config.fetch(:basic_auth) { false }
27
+ return unless options[:basic_auth] && request.env['HTTP_AUTHORIZATION']
28
+ request.env['HTTP_AUTHORIZATION'].split(' ').last
29
+ end
30
+
31
+ def make_pdf(options = {})
32
+ render_opts = {
33
+ :template => options[:template],
34
+ :prefixes => options[:prefixes],
35
+ :layout => options[:layout],
36
+ :formats => options[:formats],
37
+ :handlers => options[:handlers],
38
+ :assigns => options[:assigns]
39
+ }
40
+ render_opts[:inline] = options[:inline] if options[:inline]
41
+ render_opts[:locals] = options[:locals] if options[:locals]
42
+ render_opts[:file] = options[:file] if options[:file]
43
+
44
+ html_string = controller.render_to_string(render_opts)
45
+ options = prerender_header_and_footer(options)
46
+
47
+ document = WickedPdf::Document.new(command(options[:wkhtmltopdf]))
48
+ document.pdf_from_string(html_string, options)
49
+ ensure
50
+ clean_temp_files
51
+ end
52
+
53
+ def make_and_send_pdf(pdf_name, options = {})
54
+ options[:layout] ||= false
55
+ options[:template] ||= File.join(controller_path, action_name)
56
+ options[:disposition] ||= 'inline'
57
+ if options[:show_as_html]
58
+ render_opts = {
59
+ :template => options[:template],
60
+ :prefixes => options[:prefixes],
61
+ :layout => options[:layout],
62
+ :formats => options[:formats],
63
+ :handlers => options[:handlers],
64
+ :assigns => options[:assigns],
65
+ :content_type => 'text/html'
66
+ }
67
+ render_opts[:inline] = options[:inline] if options[:inline]
68
+ render_opts[:locals] = options[:locals] if options[:locals]
69
+ render_opts[:file] = options[:file] if options[:file]
70
+ render(render_opts)
71
+ else
72
+ pdf_content = make_pdf(options)
73
+ File.open(options[:save_to_file], 'wb') { |file| file << pdf_content } if options[:save_to_file]
74
+ send_data(pdf_content, :filename => pdf_name + '.pdf', :type => 'application/pdf', :disposition => options[:disposition]) unless options[:save_only]
75
+ end
76
+ end
77
+
78
+ # Given an options hash, prerenders content for the header and footer sections
79
+ # to temp files and return a new options hash including the URLs to these files.
80
+ def prerender_header_and_footer(options)
81
+ [:header, :footer].each do |hf|
82
+ next unless options[hf] && options[hf][:html] && options[hf][:html][:template]
83
+
84
+ options[hf][:html][:layout] ||= options[:layout]
85
+ render_opts = {
86
+ :template => options[hf][:html][:template],
87
+ :layout => options[hf][:html][:layout],
88
+ :formats => options[hf][:html][:formats],
89
+ :handlers => options[hf][:html][:handlers],
90
+ :assigns => options[hf][:html][:assigns]
91
+ }
92
+ render_opts[:locals] = options[hf][:html][:locals] if options[hf][:html][:locals]
93
+ render_opts[:file] = options[hf][:html][:file] if options[:file]
94
+
95
+ path = render_to_tempfile("wicked_#{hf}_pdf.html", render_opts)
96
+ options[hf][:html][:url] = "file:///#{path}"
97
+ end
98
+ options
99
+ end
100
+
101
+ def render_to_tempfile(filename, options)
102
+ tf = WickedPdf::Tempfile.new(filename)
103
+ @hf_tempfiles.push(tf)
104
+ tf.write controller.render_to_string(options)
105
+ tf.flush
106
+ tf.path
107
+ end
108
+
109
+ def command(binary_path)
110
+ if binary_path
111
+ WickedPdf::Command.new binary: WickedPdf::Binary.new(binary_path)
112
+ else
113
+ WickedPdf::Command.new
114
+ end
115
+ end
116
+
117
+ def clean_temp_files
118
+ @hf_tempfiles.each(&:close!)
119
+ end
120
+ end
121
+ end