wicked_pdf 1.1.0 → 2.8.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.
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