Wicked_pdf: Merging PDFs

Created on 1 Oct 2014  路  9Comments  路  Source: mileszs/wicked_pdf

The controller is correctly configuring the layout, header, footer, and templates to render as PDF, but how do you go about concatenating multiple HTML templates into one, correctly paginated PDF file?

Most helpful comment

I can think of another way you could accomplish this, though it will still probably require some work on your part.

wkhtmltopdf can take multiple pdf files (or urls), and render them all as one pdf with a command similar to this:

wkhtmltopdf tmp/tempfile1.html tmp/tempfile2.html tmp/output.pdf

Knowing this, you could prerender your templates, with layouts and everything out to HTML strings, then pass them into wkhtmltopdf something like this:

    # app/models/concerns/multipage_pdf_renderer.rb
    require 'open3'
    class MultipagePdfRenderer
      def self.combine(documents)
        outfile = WickedPdfTempfile.new('multipage_pdf_renderer.pdf')

        tempfiles = documents.each_with_index.map do |doc, index|
          file = WickedPdfTempfile.new("multipage_pdf_doc_#{index}.html")
          file.binmode
          file.write(doc)
          file.rewind
          file
        end

        filepaths = tempfiles.map{ |tf| tf.path.to_s }

        binary = WickedPdf.new.send(:find_wkhtmltopdf_binary_path)

        command = [binary, '-q']
        filepaths.each { |fp| command << fp }
        command << outfile.path.to_s
        err = Open3.popen3(*command) do |stdin, stdout, stderr|
          stderr.read
        end

        raise "Problem generating multipage pdf: #{err}" if err.present?
        return outfile.read
      ensure
        tempfiles.each(&:close!)
      end
    end

And call in your controller something like this:

    def fancy_report
      respond_to do |format|
        format.pdf do
          doc1 = render_to_string(template: 'pages/_page1')
          doc2 = render_to_string(template: 'pages/_page2')
          pdf_file = MultipagePdfRenderer.combine([doc1, doc2])
          send_data pdf_file, type: 'application/pdf', disposition: 'inline'
        end
      end
    end

However, this only covers the simplest of cases, you'll have to do the work of rendering the headers and footers if you need them, parsing (or adding) any options you might need.

To proof-of-concept this code, I put it in the wicked_pdf_issues project in this commit.

Let me know how it goes!

All 9 comments

You can render each html template as pdf and then use various programs to merge this, for example ghostscript, imagemagick, pdftk. I'm currently using ghostscript. You could use imagemagick for this too, but be careful not to use rmagick and sidekiq together, as they dont play well together.

As for the pagination, you can use the --page-offset option in the wkhtmltopdf process

I can think of another way you could accomplish this, though it will still probably require some work on your part.

wkhtmltopdf can take multiple pdf files (or urls), and render them all as one pdf with a command similar to this:

wkhtmltopdf tmp/tempfile1.html tmp/tempfile2.html tmp/output.pdf

Knowing this, you could prerender your templates, with layouts and everything out to HTML strings, then pass them into wkhtmltopdf something like this:

    # app/models/concerns/multipage_pdf_renderer.rb
    require 'open3'
    class MultipagePdfRenderer
      def self.combine(documents)
        outfile = WickedPdfTempfile.new('multipage_pdf_renderer.pdf')

        tempfiles = documents.each_with_index.map do |doc, index|
          file = WickedPdfTempfile.new("multipage_pdf_doc_#{index}.html")
          file.binmode
          file.write(doc)
          file.rewind
          file
        end

        filepaths = tempfiles.map{ |tf| tf.path.to_s }

        binary = WickedPdf.new.send(:find_wkhtmltopdf_binary_path)

        command = [binary, '-q']
        filepaths.each { |fp| command << fp }
        command << outfile.path.to_s
        err = Open3.popen3(*command) do |stdin, stdout, stderr|
          stderr.read
        end

        raise "Problem generating multipage pdf: #{err}" if err.present?
        return outfile.read
      ensure
        tempfiles.each(&:close!)
      end
    end

And call in your controller something like this:

    def fancy_report
      respond_to do |format|
        format.pdf do
          doc1 = render_to_string(template: 'pages/_page1')
          doc2 = render_to_string(template: 'pages/_page2')
          pdf_file = MultipagePdfRenderer.combine([doc1, doc2])
          send_data pdf_file, type: 'application/pdf', disposition: 'inline'
        end
      end
    end

However, this only covers the simplest of cases, you'll have to do the work of rendering the headers and footers if you need them, parsing (or adding) any options you might need.

To proof-of-concept this code, I put it in the wicked_pdf_issues project in this commit.

Let me know how it goes!

@unixmonkey thank you for the amazing sample code, I've been fiddling around with it all morning. That was a perfect execution of using the binary to concatenate the files into one PDF. Some things I cant make sense of are:

1) It's rendering the specific layout correctly, but I can't seem to send in certain options when I'm rendering the PDF, such as changing margins, setting a header/footer, page height, page width. Will I have to use the layout to set the header and footer instead of passing them in as options?

2) Assigning a file name in the pdf options causes it to render as a garbled mess. I don't think this is important (I'm not really sure the purpose of assigning a file name when I'm rendering it in the view)

** This feature aside, any reason you put the MultiPageRenderer class in /concerns and not a different folder?

1) As I mentioned, this won't handle any other wicked_pdf options, and I didn't include them because I thought it would be a lot of work, but I'm starting to think you may be able to make this work with a few small changes:

Change the method to accept wicked_pdf-style options as a second parameter

def self.combine(documents, options)
  # other stuff

add the middle line here to parse out the options with WickedPdf:

command = [binary, '-q']
command += WickedPdf.send(:parse_options, options)
filepaths.each { |fp| command << fp }

Then change your controller call to add those options:

options = { header: { html: '<h1>header</h1>' }, margin: { top: 10 } }
pdf_file = MultipagePdfRenderer.combine([doc1, doc2], options)

2) This is related to (1), normal calls to render_to_string won't take wicked_pdf-style options.

** Not really, /app/models/concerns is autoloaded by default, so it was an easy place to drop this class, and still have it do the Rails magic of class reloading while testing things out. If this were a little better structured, I might have added a /app/services directory to the load path and name it render_multipage_pdf.rb to make it service-sounding. I might have also broken the class up into a bunch of small methods instead of one giant one, but I wanted to throw out something quick & simple instead of something perfect & beautiful.

Let me know how it goes.

Great news!! Managed to produce PDFs with the options just like you thought would happen
Bad news: I'm probably screwing this up, but I was only able to call the binary successfully by making a system call: %x(binary -q [options] [file1] [file2] [outfile]

I think open3 interprets the result of WickedPdf.new.send(:parse_options, options) with quotes, which results in: RuntimeError (Problem generating multipage pdf: Unknown long argument --margin-top 10 for options = { margin { top: 10 } }

What are your thoughts?

Yes, if you are using the current gem version of wicked_pdf, the options are parsed differently than in the above code examples.

WickedPdf has only recently moved away from using a system call itself, but I haven't released that version yet because it introduces some backwards-incompatibility concerns that I'm trying to get ironed out.

If you want to try out the new way (that would be compatible with what I posted above instead of the system call), you can try updating your Gemfile with:

gem 'wicked_pdf', github: 'mileszs/wicked_pdf'

Or you could stick with what you are doing. Either way works, though the newer way is a little safer, since the calls to wkhtmltopdf are passed to the bash interpreter of the server.

im very grateful, thank you very much

Hi @unixmonkey thanks for sharing great answer.
I am getting this error even I added require 'wicked_pdf/tempfile' in the model.

uninitialized constant MultipagePdfRenderer::WickedPdfTempfile

Do you have any idea to fix it?

Thank you.

@codemicky looks like a constant lookup issue.
Try prefixing the module namespace like this:

::WickedPdf::WickedPdfTempfile

Was this page helpful?
0 / 5 - 0 ratings

Related issues

chaudard picture chaudard  路  5Comments

lafeber picture lafeber  路  5Comments

lsarni picture lsarni  路  5Comments

amerritt14 picture amerritt14  路  4Comments

kavitakanojiya picture kavitakanojiya  路  5Comments