adzap-wicked_pdf 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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