# wkhtml2pdf Ruby interface # http://wkhtmltopdf.org/ require 'logger' require 'digest/md5' require 'rbconfig' require 'open3' require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/object/blank' require 'wicked_pdf/version' require 'wicked_pdf/railtie' require 'wicked_pdf/tempfile' require 'wicked_pdf/middleware' require 'wicked_pdf/progress' class WickedPdf DEFAULT_BINARY_VERSION = Gem::Version.new('0.9.9') BINARY_VERSION_WITHOUT_DASHES = Gem::Version.new('0.12.0') EXE_NAME = 'wkhtmltopdf'.freeze @@config = {} cattr_accessor :config attr_accessor :binary_version include Progress def initialize(wkhtmltopdf_binary_path = nil) @exe_path = wkhtmltopdf_binary_path || find_wkhtmltopdf_binary_path raise "Location of #{EXE_NAME} unknown" if @exe_path.empty? raise "Bad #{EXE_NAME}'s path: #{@exe_path}" unless File.exist?(@exe_path) raise "#{EXE_NAME} is not executable" unless File.executable?(@exe_path) retrieve_binary_version end def pdf_from_html_file(filepath, options = {}) pdf_from_url("file:///#{filepath}", options) end def pdf_from_string(string, options = {}) options = options.dup options.merge!(WickedPdf.config) { |_key, option, _config| option } string_file = WickedPdfTempfile.new('wicked_pdf.html', options[:temp_path]) string_file.binmode string_file.write(string) string_file.close pdf = pdf_from_html_file(string_file.path, options) pdf ensure string_file.close! if string_file end def pdf_from_url(url, options = {}) # merge in global config options options.merge!(WickedPdf.config) { |_key, option, _config| option } generated_pdf_file = WickedPdfTempfile.new('wicked_pdf_generated_file.pdf', options[:temp_path]) command = [@exe_path] command += parse_options(options) command << url command << generated_pdf_file.path.to_s print_command(command.inspect) if in_development_mode? if track_progress?(options) invoke_with_progress(command, options) else err = Open3.popen3(*command) do |_stdin, _stdout, stderr| stderr.read end end if options[:return_file] return_file = options.delete(:return_file) return generated_pdf_file end generated_pdf_file.rewind generated_pdf_file.binmode pdf = generated_pdf_file.read raise "Error generating PDF\n Command Error: #{err}" if options[:raise_on_all_errors] && !err.empty? raise "PDF could not be generated!\n Command Error: #{err}" if pdf && pdf.rstrip.empty? pdf rescue StandardError => e raise "Failed to execute:\n#{command}\nError: #{e}" ensure generated_pdf_file.close! if generated_pdf_file && !return_file end private def in_development_mode? return Rails.env == 'development' if defined?(Rails.env) RAILS_ENV == 'development' if defined?(RAILS_ENV) end def on_windows? RbConfig::CONFIG['target_os'] =~ /mswin|mingw/ end def print_command(cmd) Rails.logger.debug '[wicked_pdf]: ' + cmd end def retrieve_binary_version _stdin, stdout, _stderr = Open3.popen3(@exe_path + ' -V') @binary_version = parse_version(stdout.gets(nil)) rescue StandardError DEFAULT_BINARY_VERSION end def parse_version(version_info) match_data = /wkhtmltopdf\s*(\d*\.\d*\.\d*\w*)/.match(version_info) if match_data && (match_data.length == 2) Gem::Version.new(match_data[1]) else DEFAULT_BINARY_VERSION end end def parse_options(options) [ parse_extra(options), parse_others(options), parse_global(options), parse_outline(options.delete(:outline)), parse_header_footer(:header => options.delete(:header), :footer => options.delete(:footer), :layout => options[:layout]), parse_cover(options.delete(:cover)), parse_toc(options.delete(:toc)), parse_basic_auth(options) ].flatten end def parse_extra(options) return [] if options[:extra].nil? return options[:extra].split if options[:extra].respond_to?(:split) options[:extra] end def parse_basic_auth(options) if options[:basic_auth] user, passwd = Base64.decode64(options[:basic_auth]).split(':') ['--username', user, '--password', passwd] else [] end end def make_option(name, value, type = :string) if value.is_a?(Array) return value.collect { |v| make_option(name, v, type) } end if type == :name_value parts = value.to_s.split(' ') ["--#{name.tr('_', '-')}", *parts] elsif type == :boolean if value ["--#{name.tr('_', '-')}"] else [] end else ["--#{name.tr('_', '-')}", value.to_s] end end def valid_option(name) if binary_version < BINARY_VERSION_WITHOUT_DASHES "--#{name}" else name end end def make_options(options, names, prefix = '', type = :string) return [] if options.nil? names.collect do |o| if options[o].blank? [] else make_option("#{prefix.blank? ? '' : prefix + '-'}#{o}", options[o], type) end end end def parse_header_footer(options) r = [] unless options.blank? [:header, :footer].collect do |hf| next if options[hf].blank? opt_hf = options[hf] r += make_options(opt_hf, [:center, :font_name, :left, :right], hf.to_s) r += make_options(opt_hf, [:font_size, :spacing], hf.to_s, :numeric) r += make_options(opt_hf, [:line], hf.to_s, :boolean) if options[hf] && options[hf][:content] @hf_tempfiles = [] unless defined?(@hf_tempfiles) @hf_tempfiles.push(tf = WickedPdfTempfile.new("wicked_#{hf}_pdf.html")) tf.write options[hf][:content] tf.flush options[hf][:html] = {} options[hf][:html][:url] = "file:///#{tf.path}" end unless opt_hf[:html].blank? r += make_option("#{hf}-html", opt_hf[:html][:url]) unless opt_hf[:html][:url].blank? end end end r end def parse_cover(argument) arg = argument.to_s return [] if arg.blank? # Filesystem path or URL - hand off to wkhtmltopdf if argument.is_a?(Pathname) || (arg[0, 4] == 'http') [valid_option('cover'), arg] else # HTML content @hf_tempfiles ||= [] @hf_tempfiles << tf = WickedPdfTempfile.new('wicked_cover_pdf.html') tf.write arg tf.flush [valid_option('cover'), tf.path] end end def parse_toc(options) return [] if options.nil? r = [valid_option('toc')] unless options.blank? r += make_options(options, [:font_name, :header_text], 'toc') r += make_options(options, [:xsl_style_sheet]) r += make_options(options, [:depth, :header_fs, :text_size_shrink, :l1_font_size, :l2_font_size, :l3_font_size, :l4_font_size, :l5_font_size, :l6_font_size, :l7_font_size, :level_indentation, :l1_indentation, :l2_indentation, :l3_indentation, :l4_indentation, :l5_indentation, :l6_indentation, :l7_indentation], 'toc', :numeric) r += make_options(options, [:no_dots, :disable_links, :disable_back_links], 'toc', :boolean) r += make_options(options, [:disable_dotted_lines, :disable_toc_links], nil, :boolean) end r end def parse_outline(options) r = [] unless options.blank? r = make_options(options, [:outline], '', :boolean) r += make_options(options, [:outline_depth], '', :numeric) end r end def parse_margins(options) make_options(options, [:top, :bottom, :left, :right], 'margin', :numeric) end def parse_global(options) r = [] unless options.blank? r += make_options(options, [:orientation, :dpi, :page_size, :page_width, :title, :log_level]) r += make_options(options, [:lowquality, :grayscale, :no_pdf_compression, :quiet], '', :boolean) r += make_options(options, [:image_dpi, :image_quality, :page_height], '', :numeric) r += parse_margins(options.delete(:margin)) end r end def parse_others(options) r = [] unless options.blank? r += make_options(options, [:proxy, :username, :password, :encoding, :user_style_sheet, :viewport_size, :window_status]) r += make_options(options, [:cookie, :post], '', :name_value) r += make_options(options, [:redirect_delay, :zoom, :page_offset, :javascript_delay], '', :numeric) r += make_options(options, [:book, :default_header, :disable_javascript, :enable_plugins, :disable_internal_links, :disable_external_links, :print_media_type, :disable_smart_shrinking, :use_xserver, :no_background, :images, :no_images, :no_stop_slow_scripts], '', :boolean) end r end def find_wkhtmltopdf_binary_path possible_locations = (ENV['PATH'].split(':') + %w[/usr/bin /usr/local/bin]).uniq possible_locations += %w[~/bin] if ENV.key?('HOME') exe_path ||= WickedPdf.config[:exe_path] unless WickedPdf.config.empty? exe_path ||= begin detected_path = (defined?(Bundler) ? Bundler.which('wkhtmltopdf') : `which wkhtmltopdf`).chomp detected_path.present? && detected_path rescue StandardError nil end exe_path ||= possible_locations.map { |l| File.expand_path("#{l}/#{EXE_NAME}") }.find { |location| File.exist?(location) } exe_path || '' end end