wicked_pdf 1.1.0 → 2.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +5 -5
  2. data/.github/issue_template.md +15 -0
  3. data/.github/workflows/ci.yml +56 -0
  4. data/.rubocop.yml +62 -0
  5. data/.rubocop_todo.yml +81 -39
  6. data/CHANGELOG.md +194 -45
  7. data/README.md +136 -28
  8. data/Rakefile +12 -9
  9. data/gemfiles/5.0.gemfile +3 -1
  10. data/gemfiles/5.1.gemfile +8 -0
  11. data/gemfiles/5.2.gemfile +9 -0
  12. data/gemfiles/6.0.gemfile +10 -0
  13. data/gemfiles/6.1.gemfile +11 -0
  14. data/gemfiles/7.0.gemfile +11 -0
  15. data/generators/wicked_pdf/templates/wicked_pdf.rb +14 -5
  16. data/lib/generators/wicked_pdf_generator.rb +5 -9
  17. data/lib/wicked_pdf/binary.rb +65 -0
  18. data/lib/wicked_pdf/middleware.rb +1 -1
  19. data/lib/wicked_pdf/option_parser.rb +230 -0
  20. data/lib/wicked_pdf/pdf_helper.rb +35 -42
  21. data/lib/wicked_pdf/progress.rb +33 -0
  22. data/lib/wicked_pdf/railtie.rb +6 -44
  23. data/lib/wicked_pdf/tempfile.rb +33 -3
  24. data/lib/wicked_pdf/version.rb +1 -1
  25. data/lib/wicked_pdf/wicked_pdf_helper/assets.rb +200 -17
  26. data/lib/wicked_pdf/wicked_pdf_helper.rb +2 -1
  27. data/lib/wicked_pdf.rb +64 -275
  28. data/test/fixtures/database.yml +4 -0
  29. data/test/fixtures/manifest.js +3 -0
  30. data/test/functional/pdf_helper_test.rb +71 -0
  31. data/test/functional/wicked_pdf_helper_assets_test.rb +61 -0
  32. data/test/test_helper.rb +13 -8
  33. data/test/unit/wicked_pdf_binary_test.rb +26 -0
  34. data/test/unit/wicked_pdf_option_parser_test.rb +133 -0
  35. data/test/unit/wicked_pdf_test.rb +25 -146
  36. data/test/unit/wkhtmltopdf_location_test.rb +48 -0
  37. data/wicked_pdf.gemspec +20 -13
  38. metadata +79 -36
  39. data/.travis.yml +0 -57
  40. data/gemfiles/2.3.gemfile +0 -10
  41. data/gemfiles/3.0.gemfile +0 -12
  42. data/gemfiles/3.1.gemfile +0 -13
  43. data/gemfiles/3.2.gemfile +0 -12
  44. data/gemfiles/4.0.gemfile +0 -6
  45. data/gemfiles/4.1.gemfile +0 -6
  46. data/gemfiles/4.2.gemfile +0 -6
  47. data/gemfiles/rails_edge.gemfile +0 -6
@@ -0,0 +1,230 @@
1
+ class WickedPdf
2
+ class OptionParser
3
+ BINARY_VERSION_WITHOUT_DASHES = Gem::Version.new('0.12.0')
4
+
5
+ attr_reader :binary_version, :hf_tempfiles
6
+
7
+ def initialize(binary_version = WickedPdf::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 valid_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
+
40
+ options[:extra]
41
+ end
42
+
43
+ def parse_basic_auth(options)
44
+ if options[:basic_auth]
45
+ user, passwd = Base64.decode64(options[:basic_auth]).split(':')
46
+ ['--username', user, '--password', passwd]
47
+ else
48
+ []
49
+ end
50
+ end
51
+
52
+ def parse_header_footer(options)
53
+ r = []
54
+ unless options.blank?
55
+ %i[header footer].collect do |hf|
56
+ next if options[hf].blank?
57
+
58
+ opt_hf = options[hf]
59
+ r += make_options(opt_hf, %i[center font_name left right], hf.to_s)
60
+ r += make_options(opt_hf, %i[font_size spacing], hf.to_s, :numeric)
61
+ r += make_options(opt_hf, [:line], hf.to_s, :boolean)
62
+ if options[hf] && options[hf][:content]
63
+ @hf_tempfiles = [] unless defined?(@hf_tempfiles)
64
+ @hf_tempfiles.push(tf = File.new(Dir::Tmpname.create(["wicked_#{hf}_pdf", '.html']) {}, 'w'))
65
+ tf.write options[hf][:content]
66
+ tf.flush
67
+ options[hf][:html] = {}
68
+ options[hf][:html][:url] = "file:///#{tf.path}"
69
+ end
70
+ unless opt_hf[:html].blank?
71
+ r += make_option("#{hf}-html", opt_hf[:html][:url]) unless opt_hf[:html][:url].blank?
72
+ end
73
+ end
74
+ end
75
+ r
76
+ end
77
+
78
+ def parse_cover(argument)
79
+ arg = argument.to_s
80
+ return [] if arg.blank?
81
+
82
+ # Filesystem path or URL - hand off to wkhtmltopdf
83
+ if argument.is_a?(Pathname) || (arg[0, 4] == 'http')
84
+ [valid_option('cover'), arg]
85
+ else # HTML content
86
+ @hf_tempfiles ||= []
87
+ @hf_tempfiles << tf = WickedPdf::Tempfile.new('wicked_cover_pdf.html')
88
+ tf.write arg
89
+ tf.flush
90
+ [valid_option('cover'), tf.path]
91
+ end
92
+ end
93
+
94
+ def parse_toc(options)
95
+ return [] if options.nil?
96
+
97
+ r = [valid_option('toc')]
98
+ unless options.blank?
99
+ r += make_options(options, %i[font_name header_text], 'toc')
100
+ r += make_options(options, [:xsl_style_sheet])
101
+ r += make_options(options, %i[depth
102
+ header_fs
103
+ text_size_shrink
104
+ l1_font_size
105
+ l2_font_size
106
+ l3_font_size
107
+ l4_font_size
108
+ l5_font_size
109
+ l6_font_size
110
+ l7_font_size
111
+ level_indentation
112
+ l1_indentation
113
+ l2_indentation
114
+ l3_indentation
115
+ l4_indentation
116
+ l5_indentation
117
+ l6_indentation
118
+ l7_indentation], 'toc', :numeric)
119
+ r += make_options(options, %i[no_dots
120
+ disable_links
121
+ disable_back_links], 'toc', :boolean)
122
+ r += make_options(options, %i[disable_dotted_lines
123
+ disable_toc_links], nil, :boolean)
124
+ end
125
+ r
126
+ end
127
+
128
+ def parse_outline(options)
129
+ r = []
130
+ unless options.blank?
131
+ r = make_options(options, [:outline], '', :boolean)
132
+ r += make_options(options, [:outline_depth], '', :numeric)
133
+ end
134
+ r
135
+ end
136
+
137
+ def parse_margins(options)
138
+ make_options(options, %i[top bottom left right], 'margin', :numeric)
139
+ end
140
+
141
+ def parse_global(options)
142
+ r = []
143
+ unless options.blank?
144
+ r += make_options(options, %i[orientation
145
+ dpi
146
+ page_size
147
+ page_width
148
+ title
149
+ log_level])
150
+ r += make_options(options, %i[lowquality
151
+ grayscale
152
+ no_pdf_compression
153
+ quiet], '', :boolean)
154
+ r += make_options(options, %i[image_dpi
155
+ image_quality
156
+ page_height], '', :numeric)
157
+ r += parse_margins(options.delete(:margin))
158
+ end
159
+ r
160
+ end
161
+
162
+ def parse_others(options)
163
+ r = []
164
+ unless options.blank?
165
+ r += make_options(options, %i[proxy
166
+ username
167
+ password
168
+ encoding
169
+ user_style_sheet
170
+ viewport_size
171
+ window_status
172
+ allow])
173
+ r += make_options(options, %i[cookie
174
+ post], '', :name_value)
175
+ r += make_options(options, %i[redirect_delay
176
+ zoom
177
+ page_offset
178
+ javascript_delay], '', :numeric)
179
+ r += make_options(options, %i[book
180
+ default_header
181
+ disable_javascript
182
+ enable_plugins
183
+ disable_internal_links
184
+ disable_external_links
185
+ keep_relative_links
186
+ print_media_type
187
+ disable_local_file_access
188
+ enable_local_file_access
189
+ disable_smart_shrinking
190
+ use_xserver
191
+ no_background
192
+ images
193
+ no_images
194
+ no_stop_slow_scripts], '', :boolean)
195
+ end
196
+ r
197
+ end
198
+
199
+ def make_options(options, names, prefix = '', type = :string)
200
+ return [] if options.nil?
201
+
202
+ names.collect do |o|
203
+ if options[o].blank?
204
+ []
205
+ else
206
+ make_option("#{prefix.blank? ? '' : prefix + '-'}#{o}",
207
+ options[o],
208
+ type)
209
+ end
210
+ end
211
+ end
212
+
213
+ def make_option(name, value, type = :string)
214
+ return value.collect { |v| make_option(name, v, type) } if value.is_a?(Array)
215
+
216
+ if type == :name_value
217
+ parts = value.to_s.split(' ')
218
+ ["--#{name.tr('_', '-')}", *parts]
219
+ elsif type == :boolean
220
+ if value
221
+ ["--#{name.tr('_', '-')}"]
222
+ else
223
+ []
224
+ end
225
+ else
226
+ ["--#{name.tr('_', '-')}", value.to_s]
227
+ end
228
+ end
229
+ end
230
+ end
@@ -1,17 +1,5 @@
1
1
  class WickedPdf
2
2
  module PdfHelper
3
- def self.included(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
-
8
- base.class_eval do
9
- alias_method_chain :render, :wicked_pdf
10
- alias_method_chain :render_to_string, :wicked_pdf
11
- after_filter :clean_temp_files
12
- end
13
- end
14
-
15
3
  def self.prepended(base)
16
4
  # Protect from trying to augment modules that appear
17
5
  # as the result of adding other gems.
@@ -19,56 +7,55 @@ class WickedPdf
19
7
 
20
8
  base.class_eval do
21
9
  after_action :clean_temp_files
22
-
23
- alias_method :render_without_wicked_pdf, :render
24
- alias_method :render_to_string_without_wicked_pdf, :render_to_string
25
-
26
- def render(options = nil, *args, &block)
27
- render_with_wicked_pdf(options, *args, &block)
28
- end
29
-
30
- def render_to_string(options = nil, *args, &block)
31
- render_to_string_with_wicked_pdf(options, *args, &block)
32
- end
33
10
  end
34
11
  end
35
12
 
36
- def render_with_wicked_pdf(options = nil, *args, &block)
13
+ def render(*args)
14
+ options = args.first
37
15
  if options.is_a?(Hash) && options.key?(:pdf)
38
- log_pdf_creation
39
- options[:basic_auth] = set_basic_auth(options)
40
- make_and_send_pdf(options.delete(:pdf), (WickedPdf.config || {}).merge(options))
16
+ render_with_wicked_pdf(options)
41
17
  else
42
- render_without_wicked_pdf(options, *args, &block)
18
+ super
43
19
  end
44
20
  end
45
21
 
46
- def render_to_string_with_wicked_pdf(options = nil, *args, &block)
22
+ def render_to_string(*args)
23
+ options = args.first
47
24
  if options.is_a?(Hash) && options.key?(:pdf)
48
- log_pdf_creation
49
- options[:basic_auth] = set_basic_auth(options)
50
- options.delete :pdf
51
- make_pdf((WickedPdf.config || {}).merge(options))
25
+ render_to_string_with_wicked_pdf(options)
52
26
  else
53
- render_to_string_without_wicked_pdf(options, *args, &block)
27
+ super
54
28
  end
55
29
  end
56
30
 
57
- private
31
+ def render_with_wicked_pdf(options)
32
+ raise ArgumentError, 'missing keyword: pdf' unless options.is_a?(Hash) && options.key?(:pdf)
33
+
34
+ options[:basic_auth] = set_basic_auth(options)
35
+ make_and_send_pdf(options.delete(:pdf), (WickedPdf.config || {}).merge(options))
36
+ end
37
+
38
+ def render_to_string_with_wicked_pdf(options)
39
+ raise ArgumentError, 'missing keyword: pdf' unless options.is_a?(Hash) && options.key?(:pdf)
58
40
 
59
- def log_pdf_creation
60
- logger.info '*' * 15 + 'WICKED' + '*' * 15 if logger && logger.respond_to?(:info)
41
+ options[:basic_auth] = set_basic_auth(options)
42
+ options.delete :pdf
43
+ make_pdf((WickedPdf.config || {}).merge(options))
61
44
  end
62
45
 
46
+ private
47
+
63
48
  def set_basic_auth(options = {})
64
49
  options[:basic_auth] ||= WickedPdf.config.fetch(:basic_auth) { false }
65
50
  return unless options[:basic_auth] && request.env['HTTP_AUTHORIZATION']
51
+
66
52
  request.env['HTTP_AUTHORIZATION'].split(' ').last
67
53
  end
68
54
 
69
55
  def clean_temp_files
70
56
  return unless defined?(@hf_tempfiles)
71
- @hf_tempfiles.each(&:close!)
57
+
58
+ @hf_tempfiles.each(&:close)
72
59
  end
73
60
 
74
61
  def make_pdf(options = {})
@@ -76,8 +63,10 @@ class WickedPdf
76
63
  :template => options[:template],
77
64
  :layout => options[:layout],
78
65
  :formats => options[:formats],
79
- :handlers => options[:handlers]
66
+ :handlers => options[:handlers],
67
+ :assigns => options[:assigns]
80
68
  }
69
+ render_opts[:inline] = options[:inline] if options[:inline]
81
70
  render_opts[:locals] = options[:locals] if options[:locals]
82
71
  render_opts[:file] = options[:file] if options[:file]
83
72
  html_string = render_to_string(render_opts)
@@ -97,8 +86,10 @@ class WickedPdf
97
86
  :layout => options[:layout],
98
87
  :formats => options[:formats],
99
88
  :handlers => options[:handlers],
89
+ :assigns => options[:assigns],
100
90
  :content_type => 'text/html'
101
91
  }
92
+ render_opts[:inline] = options[:inline] if options[:inline]
102
93
  render_opts[:locals] = options[:locals] if options[:locals]
103
94
  render_opts[:file] = options[:file] if options[:file]
104
95
  render(render_opts)
@@ -112,16 +103,18 @@ class WickedPdf
112
103
  # Given an options hash, prerenders content for the header and footer sections
113
104
  # to temp files and return a new options hash including the URLs to these files.
114
105
  def prerender_header_and_footer(options)
115
- [:header, :footer].each do |hf|
106
+ %i[header footer].each do |hf|
116
107
  next unless options[hf] && options[hf][:html] && options[hf][:html][:template]
108
+
117
109
  @hf_tempfiles = [] unless defined?(@hf_tempfiles)
118
- @hf_tempfiles.push(tf = WickedPdfTempfile.new("wicked_#{hf}_pdf.html"))
110
+ @hf_tempfiles.push(tf = WickedPdf::Tempfile.new("wicked_#{hf}_pdf.html"))
119
111
  options[hf][:html][:layout] ||= options[:layout]
120
112
  render_opts = {
121
113
  :template => options[hf][:html][:template],
122
114
  :layout => options[hf][:html][:layout],
123
115
  :formats => options[hf][:html][:formats],
124
- :handlers => options[hf][:html][:handlers]
116
+ :handlers => options[hf][:html][:handlers],
117
+ :assigns => options[hf][:html][:assigns]
125
118
  }
126
119
  render_opts[:locals] = options[hf][:html][:locals] if options[hf][:html][:locals]
127
120
  render_opts[:file] = options[hf][:html][:file] if options[:file]
@@ -0,0 +1,33 @@
1
+ class WickedPdf
2
+ module Progress
3
+ require 'pty' if RbConfig::CONFIG['target_os'] !~ /mswin|mingw/ && RUBY_ENGINE != 'truffleruby' # no support for windows and truffleruby
4
+ require 'English'
5
+
6
+ def track_progress?(options)
7
+ options[:progress] && !(on_windows? || RUBY_ENGINE == 'truffleruby')
8
+ end
9
+
10
+ def invoke_with_progress(command, options)
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
+ options[:progress].call(line) if options[:progress]
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
@@ -3,53 +3,15 @@ require 'wicked_pdf/wicked_pdf_helper'
3
3
  require 'wicked_pdf/wicked_pdf_helper/assets'
4
4
 
5
5
  class WickedPdf
6
- if defined?(Rails)
7
-
8
- if Rails::VERSION::MAJOR >= 5
9
-
10
- class WickedRailtie < Rails::Railtie
11
- initializer 'wicked_pdf.register' do |_app|
12
- ActionController::Base.send :prepend, PdfHelper
13
- ActionView::Base.send :include, WickedPdfHelper::Assets
14
- end
15
- end
16
-
17
- elsif Rails::VERSION::MAJOR == 4
18
-
19
- class WickedRailtie < Rails::Railtie
20
- initializer 'wicked_pdf.register' do |_app|
21
- ActionController::Base.send :include, PdfHelper
22
- ActionView::Base.send :include, WickedPdfHelper::Assets
23
- end
24
- end
25
-
26
- elsif Rails::VERSION::MAJOR == 3
27
-
28
- class WickedRailtie < Rails::Railtie
29
- initializer 'wicked_pdf.register' do |_app|
30
- ActionController::Base.send :include, PdfHelper
31
- if Rails::VERSION::MINOR > 0 && Rails.configuration.assets.enabled
32
- ActionView::Base.send :include, WickedPdfHelper::Assets
33
- else
34
- ActionView::Base.send :include, WickedPdfHelper
35
- end
36
- end
37
- end
38
-
39
- elsif Rails::VERSION::MAJOR == 2
40
-
41
- unless ActionController::Base.instance_methods.include? 'render_with_wicked_pdf'
42
- ActionController::Base.send :include, PdfHelper
6
+ if defined?(Rails.env)
7
+ class WickedRailtie < Rails::Railtie
8
+ initializer 'wicked_pdf.register', :after => 'remotipart.controller_helper' do |_app|
9
+ ActiveSupport.on_load(:action_controller) { ActionController::Base.send :prepend, PdfHelper }
10
+ ActiveSupport.on_load(:action_view) { include WickedPdfHelper::Assets }
43
11
  end
44
- unless ActionView::Base.instance_methods.include? 'wicked_pdf_stylesheet_link_tag'
45
- ActionView::Base.send :include, WickedPdfHelper
46
- end
47
-
48
12
  end
49
13
 
50
- if Mime::Type.lookup_by_extension(:pdf).nil?
51
- Mime::Type.register('application/pdf', :pdf)
52
- end
14
+ Mime::Type.register('application/pdf', :pdf) if Mime::Type.lookup_by_extension(:pdf).nil?
53
15
 
54
16
  end
55
17
  end
@@ -1,13 +1,43 @@
1
1
  require 'tempfile'
2
+ require 'stringio'
2
3
 
3
4
  class WickedPdf
4
- class WickedPdfTempfile < Tempfile
5
- # ensures the Tempfile's filename always keeps its extension
5
+ class Tempfile < ::Tempfile
6
6
  def initialize(filename, temp_dir = nil)
7
7
  temp_dir ||= Dir.tmpdir
8
8
  extension = File.extname(filename)
9
- basename = File.basename(filename, extension)
9
+ basename = File.basename(filename, extension)
10
10
  super([basename, extension], temp_dir)
11
11
  end
12
+
13
+ def write_in_chunks(input_string)
14
+ binmode
15
+ string_io = StringIO.new(input_string)
16
+ write(string_io.read(chunk_size)) until string_io.eof?
17
+ close
18
+ self
19
+ rescue Errno::EINVAL => e
20
+ raise e, file_too_large_message
21
+ end
22
+
23
+ def read_in_chunks
24
+ rewind
25
+ binmode
26
+ output_string = ''
27
+ output_string << read(chunk_size) until eof?
28
+ output_string
29
+ rescue Errno::EINVAL => e
30
+ raise e, file_too_large_message
31
+ end
32
+
33
+ private
34
+
35
+ def chunk_size
36
+ 1024 * 1024
37
+ end
38
+
39
+ def file_too_large_message
40
+ 'The HTML file is too large! Try reducing the size or using the return_file option instead.'
41
+ end
12
42
  end
13
43
  end
@@ -1,3 +1,3 @@
1
1
  class WickedPdf
2
- VERSION = '1.1.0'.freeze
2
+ VERSION = '2.8.0'.freeze
3
3
  end