Can't generate proper PDF files with PDFtk - ruby-on-rails

I am developing a web app using Ruby on Rails 3. One of the features of the app is to use data from a MySQL database to fill PDF template forms that have been designed in Adobe LiveCycle Designer.
I am using the technique of generating an XFDF file with the data and use it to fill in the actual PDF file. I am using PDFtk to do this and if I run it from my command prompt (Windows 7 64bit) it works fine.
I used code by Greg Lappen at http://bleep.lapcominc.com/2012/02/07/filling-pdf-forms-with-ruby-and-pdftk/ to implement this process in my Rails app, but it does not seem to work
The output PDF cannot be opened in Acrobat as it states the file has been damaged. If I open it using a normal text editor all it contains is #<StringIO:0x5958f30> with the HEX value changing after each output.
The code generating the XML data is correct. I was able to save it to a file and run it through the command prompt myself.
def self.generate_xfdf(fields, filename)
xml = Builder::XmlMarkup.new
xml.instruct!
xml.xfdf("xmlns" => "http://ns.adobe.com/xfdf/", "xml:space" => "preserve") {
xml.f :href => filename
xml.fields {
fields.each do |field, value|
xml.field(:name => field) {
if value.is_a? Array
value.each {|item| xml.value(item.to_s) }
else
xml.value(value.to_s)
end
}
end
}
}
xml.target!
end
I suspect the real problem is in either of the two code snippets below. I just started learning Ruby on Rails and I am unable to debug this. I have tried various different methods but no success so far. I would really appreciate any help.
def self.stamp(input_pdf, fields)
stdin, stdout, stderr = Open3.popen3("pdftk #{input_pdf} fill_form - output - flatten")
stdin << generate_xfdf(fields, File.basename(input_pdf))
stdin.close
yield stdout
stdout.close
stderr.close
end
PdfStamper.stamp('C:/clean-it-template.pdf', { 'LastName' => "Test Last Name", 'FirstName' => "Test First Name" }) do |pdf_io|
pdf_content = StringIO.new
pdf_content << pdf_io.read
send_data pdf_content.string, :filename=>'output.pdf', :disposition=>'inline', :type=>'application/pdf'
end
This is the full code in my controller class
require 'pdf_stamper'
class FormPagesController < ApplicationController
def pdftest
PdfStamper.stamp('C:/clean-it-template.pdf', { 'LastName' => "Test Last Name", 'FirstName' => "Test First Name" }) do |pdf_io|
pdf_content = StringIO.new
pdf_content << pdf_io.read
send_data pdf_content.string, :filename=>'output.pdf', :disposition=>'inline', :type=>'application/pdf'
end
end
end
This is the full code for the pdf_stamper class I am using
require 'builder'
require 'open3'
class PdfStamper
def self.stamp(input_pdf, fields)
stdin, stdout, stderr = Open3.popen3("pdftk #{input_pdf} fill_form - output - flatten")
stdin << generate_xfdf(fields, File.basename(input_pdf))
stdin.close
yield stdout
stdout.close
stderr.close
end
def self.generate_xfdf(fields, filename)
xml = Builder::XmlMarkup.new
xml.instruct!
xml.xfdf("xmlns" => "http://ns.adobe.com/xfdf/", "xml:space" => "preserve") {
xml.f :href => filename
xml.fields {
fields.each do |field, value|
xml.field(:name => field) {
if value.is_a? Array
value.each {|item| xml.value(item.to_s) }
else
xml.value(value.to_s)
end
}
end
}
}
xml.target!
#file = File.new("C:/debug.xml", "w+")
#file.write(xml_data)
#file.close
end
end
UPDATE #1:
I ran the web app on Ubuntu and I still get the same errors. After digging around on the web I changed the code in my controller to this:
def pdftest
PdfStamper.stamp('/home/nikolaos/clean-it-template.pdf', { 'LastName' => "Test Last Name", 'FirstName' => "Test First Name" }) do |pdf_io|
pdf_content = StringIO.new("", 'wb')
pdf_content << pdf_io.read
send_data pdf_content.string, :filename=>'output.pdf', :disposition=>'inline', :type=>'application/pdf'
end
end
I changed StringIO to be in binary write mode and it works in Ubuntu! The PDF opens correctly with all the fields filled in. I opened the same file on Windows using Acrobat and no problems, BUT if I run the web app on Windows, it still produces damaged PDF files.
Does anyone have any solutions on how to get this working in Windows? I am guessing it has something to do with the way Windows and Linux interpret newlines or something similar to that?

After some more searching through the Ruby documentation I managed to solve my problem. Now my app is able to produce valid PDF files on Windows. Here is my solution for anyone that is experiencing the same problem.
The solution was to use IO instead of StringIO in the controller.
My FormPages controller code
require 'pdf_stamper'
class FormPagesController < ApplicationController
def pdftest
PdfStamper.stamp('C:/clean-it-template.pdf', { 'LastName' => "Bukas", 'FirstName' => "Nikolaos" }) do |pdf_io|
pdf_content = IO.new(pdf_io.to_i, "r+b")
pdf_content.binmode
send_data pdf_content.read, :filename=>'output.pdf', :disposition=>'inline', :type=>'application/pdf'
end
end
end
The pdf_stamper class in charge of filling and generating the PDF using PDFtk
require 'builder'
require 'open3'
class PdfStamper
def self.stamp(input_pdf, fields)
Open3.popen3("pdftk #{input_pdf} fill_form - output -") do |stdin, stdout, stderr|
stdin << generate_xfdf(fields, File.basename(input_pdf))
stdin.close
yield stdout
stdout.close
stderr.close
end
end
def self.generate_xfdf(fields, filename)
xml = Builder::XmlMarkup.new
xml.instruct!
xml.xfdf("xmlns" => "http://ns.adobe.com/xfdf/", "xml:space" => "preserve") {
xml.f :href => filename
xml.fields {
fields.each do |field, value|
xml.field(:name => field) {
if value.is_a? Array
value.each {|item| xml.value(item.to_s) }
else
xml.value(value.to_s)
end
}
end
}
}
xml.target!
end
end

Related

Save docx render with a specific path

I want to save a docx that i create. I use the gem htmltoword.
The render is a function in the gem I think.
In my controller (but doesn't work) :
respond_to do |format|
format.html
format.docx do
#filepath = "#{Rails.root}/app/template/#{#cvmodif.nom}.docx"
render docx: 'show', filename: 'show.docx'
send_file(#filepath, :type => 'application/docx', :disposition => 'attachment')
end
end
I have a link. When i click on it, the docx is downloaded corectly. But i want to save it too in a custom path.
<%= link_to 'WORD', cv_path(#cvmodif, :format => 'docx') %>
How can I do that?
Both render and send_file do the same thing: generate a document and send it as an attachment.
If you want to save the document you have to do it manually before sending:
respond_to do |format|
format.docx do
# Generate the document
my_html = '<html><head></head><body><p>Hello</p></body></html>'
file_path = "test-#{Time.now.sec}.docx"
document = Htmltoword::Document.create(my_html)
# Save it in the custom file
File.open(file_path, "wb") do |out|
out << document
end
# Send the custom file
send_file(file_path, :type => 'application/docx', :disposition => 'attachment')
end
end
P.S. According to Htmltodoc source code in the version 0.4.4 there is a function create_and_save, but in the currently distributed gem this function is missing. If this scenario is often used in your application I'd recommend you to create a common method for this purposes.
UPDATE
Then there is no straightforward solution, because in this case sending of a file is a part of rendering process which is the last step of page's loading and runs deeply inside Htmltoword.
The most correct solution is to make this a Htmltoword's feature. (Create feature request or even implement it by yourself).
But for the moment you can take renderer of *.docx files from the library and add minimal changes to achieve your goals.
Create a file RailsApp/config/initializers/application_controller.rb.
Add this code of docx renderer taken from github
ActionController::Renderers.add :docx do |filename, options|
formats[0] = :docx unless formats.include?(:docx) || Rails.version < '3.2'
# This is ugly and should be solved with regular file utils
if options[:template] == action_name
if filename =~ %r{^([^\/]+)/(.+)$}
options[:prefixes] ||= []
options[:prefixes].unshift $1
options[:template] = $2
else
options[:template] = filename
end
end
# disposition / filename
disposition = options.delete(:disposition) || 'attachment'
if file_name = options.delete(:filename)
file_name += '.docx' unless file_name =~ /\.docx$/
else
file_name = "#{filename.gsub(/^.*\//, '')}.docx"
end
# other properties
save_to = options.delete(:save_to)
word_template = options.delete(:word_template) || nil
extras = options.delete(:extras) || false
# content will come from property content unless not specified
# then it will look for a template.
content = options.delete(:content) || render_to_string(options)
document = Htmltoword::Document.create(content, word_template, extras)
File.open(save_to, "wb") { |out| out << document } if save_to
send_data document, filename: file_name, type: Mime::DOCX, disposition: disposition
end
If you compare this file to the source one you'll find that I've added save_to option and when this option is set, renderer saves a document to the given location.
Usage in the controller:
format.docx do
render docx: 'my_view', filename: 'my_file.docx', save_to: "test-#{Time.now.sec}.docx"
end

Prawn PDFs not formatting correctly when using pdf = pdfname.new in Rails 3.2

I have a list of attendances at different teaching events stored in the attendance model, and I want to create certificates based upon this data.
My problem is that none of the prawn formatting seems to work. I cannot change text size, weight, position, use a template etc. Nothing seems to work correctly.
My code is as follows:
show method in attendances_controller
def show
#attendance = Attendance.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.json { render json: #attendance }
format.pdf do
pdf = CertificatePdf.new(#attendance)
send_data pdf.render, type: "application/pdf", disposition: "inline", filename: "Certificate"
end
end
end
certificate_pdf.rb
class CertificatePdf < Prawn::Document
def initialize(attendance)
start_new_page(:template => "/pdfs/certificate_template.pdf")
#attendance = attendance
attendance_info
end
def attendance_info
move_down(70)
text "This is to certify that"
text "#{#attendance.student.fname}" + " " + "#{#attendance.student.lname}", :size => 24
text "Attended the " "#{#attendance.teaching_session.title}" + " " + "#{#attendance.teaching_session.teaching_format.format}"
text "On " "#{#attendance.teaching_session.date}"
end
end
If anyone could please advise I would be immensely grateful.
Best Wishes,
Mike
You're overriding the initialize method, so the initialize method from Prawn::Document (where all of the document setup happens) is not being executed.
You should call super in your overridden initialize method. Note that you'll probably need an empty set of params (using super()), otherwise your attendance object will be passed to Prawn::Document.new and it won't know what to do with it:
def initialize(attendance)
super()
start_new_page(:template => "/pdfs/certificate_template.pdf")
#attendance = attendance
attendance_info
end
What version of prawn are you using?
I think in the latest release they moved to a formatted_text method that you'd call like so:
formatted_text [ {:text => "Text you want to format.", :size => 24 } ]
See the prawn manual section on text/formatted_text.rb:
http://prawn.majesticseacreature.com/manual.pdf

How can I save the response created by my Rails application?

There is CSV-export of some objects (such as tasks, contacts, etc) in my application. It just renders CSV-file like this:
respond_to do |format|
format.html
format.csv { render text: Task.to_csv } # I have self.to_csv def in model
end
It generates a CSV file when I go to '/tasks.csv' without a problem.
Now I want to export all the objects and zip them. I'm using rubyzip gem to create zip-files. Now my code for creating zip-file with all the CSVs looks like that:
Zip::ZipFile.open("#{path_to_file}.zip", Zip::ZipFile::CREATE) do |zipfile|
zipfile.file.open("tasks.csv", "w") { |f| f << open("http://#{request.host}:#{request.port.to_s}/tasks.csv").read }
# the same lines for contacts and other objects
end
But it seems that there is something wrong with it because it's executing for a long time (I'm getting Timeout::Error even if there is just one line in CSV) and the resulting zip-archive contains something broken.
How can I save my "/tasks.csv", "/contacts.csv", etc as a file on server (inside of zip-archive in this case)?
I did it! The code is:
Zip::ZipFile.open("#{path_to_file}.zip", Zip::ZipFile::CREATE) do |zipfile|
zipfile.file.open("tasks.csv", "w") do |f|
CSV.open(f, "w") do |csv|
CSV.parse(Task.to_csv) { |row| csv << row }
end
end
end

Rails 3.1 active record query to an array of arrays for CSV export via FastCSV

I'm attempting to DRY up a method I've been using for a few months:
def export(imagery_requests)
csv_string = FasterCSV.generate do |csv|
imagery_requests.each do |ir|
csv << [ir.id, ir.service_name, ir.description, ir.first_name, ir.last_name, ir.email,
ir.phone_contact, ir.region, ir.imagery_type, ir.file_type, ir.pixel_type,
ir.total_images, ir.tile_size, ir.progress, ir.expected_date, ir.high_priority,
ir.priority_justification, ir.raw_data_location, ir.service_overviews,
ir.is_def, ir.isc_def, ir.special_instructions, ir.navigational_path,
ir.fyqueue, ir.created_at, ir.updated_at]
end
end
# send it to the browser with proper headers
send_data csv_string,
:type => 'text/csv; charset=iso-8859-1; header=present',
:disposition => "attachment; filename=requests_as_of-#{Time.now.strftime("%Y%m%d")}.csv"
end
I figured it would be a LOT better if instead of specifying EVERY column manually, I did something like this:
def export(imagery_requests)
csv_string = FasterCSV.generate do |csv|
line = []
imagery_requests.each do |ir|
csv << ir.attributes.values.each do |i|
line << i
end
end
end
# send it to the browser with proper headers
send_data csv_string,
:type => 'text/csv; charset=iso-8859-1; header=present',
:disposition => "attachment; filename=requests_as_of-#{Time.now.strftime("%Y%m%d")}.csv"
end
That should be creating an array of arrays. It works just fine in the Rails console. But in the production environment, it just produces garbage output. I'd much rather make this method extensible so I can add more fields to the ImageryRequest model at a later time. Am I going about this all wrong?
I'm guessing that it probably works in the console when you do it for just one imagery_request, yes?
But when you do multiple it fails?
Again I'm guessing that's because you never reset line to be an empty array again. So you're continually filling a single array.
Try the simple way first, to check it works, then start going all << on it then:
csv_string = FasterCSV.generate do |csv|
imagery_requests.each do |ir|
csv << ir.attributes.values.clone
end
end
PS - in the past I've even used clone on my line-by-line array, just to be sure I wasn't doing anything untoward with persisted stuff...

How to edit docx with nokogiri and rubyzip

I'm using a combination of rubyzip and nokogiri to edit a .docx file. I'm using rubyzip to unzip the .docx file and then using nokogiri to parse and change the body of the word/document.xml file but ever time I close rubyzip at the end it corrupts the file and I can't open it or repair it. I unzip the .docx file on desktop and check the word/document.xml file and the content is updated to what I changed it to but all the other files are messed up. Could someone help me with this issue? Here is my code:
require 'rubygems'
require 'zip/zip'
require 'nokogiri'
zip = Zip::ZipFile.open("test.docx")
doc = zip.find_entry("word/document.xml")
xml = Nokogiri::XML.parse(doc.get_input_stream)
wt = xml.root.xpath("//w:t", {"w" => "http://schemas.openxmlformats.org/wordprocessingml/2006/main"}).first
wt.content = "New Text"
zip.get_output_stream("word/document.xml") {|f| f << xml.to_s}
zip.close
I ran into the same corruption problem with rubyzip last night. I solved it by copying everything to a new zip file, replacing files as necessary.
Here's my working proof of concept:
#!/usr/bin/env ruby
require 'rubygems'
require 'zip/zip' # rubyzip gem
require 'nokogiri'
class WordXmlFile
def self.open(path, &block)
self.new(path, &block)
end
def initialize(path, &block)
#replace = {}
if block_given?
#zip = Zip::ZipFile.open(path)
yield(self)
#zip.close
else
#zip = Zip::ZipFile.open(path)
end
end
def merge(rec)
xml = #zip.read("word/document.xml")
doc = Nokogiri::XML(xml) {|x| x.noent}
(doc/"//w:fldSimple").each do |field|
if field.attributes['instr'].value =~ /MERGEFIELD (\S+)/
text_node = (field/".//w:t").first
if text_node
text_node.inner_html = rec[$1].to_s
else
puts "No text node for #{$1}"
end
end
end
#replace["word/document.xml"] = doc.serialize :save_with => 0
end
def save(path)
Zip::ZipFile.open(path, Zip::ZipFile::CREATE) do |out|
#zip.each do |entry|
out.get_output_stream(entry.name) do |o|
if #replace[entry.name]
o.write(#replace[entry.name])
else
o.write(#zip.read(entry.name))
end
end
end
end
#zip.close
end
end
if __FILE__ == $0
file = ARGV[0]
out_file = ARGV[1] || file.sub(/\.docx/, ' Merged.docx')
w = WordXmlFile.open(file)
w.force_settings
w.merge('First_Name' => 'Eric', 'Last_Name' => 'Mason')
w.save(out_file)
end
I stumbled accross the post and know nothing about ruby or nokogiri but ...
It looks like you are reziping the new content incorrectly.
I don't know about rubyzip, but you need a way to tell it to update the entry word/document.xml
and then resave/rezip the file.
It looks like you are just overwriting the entry with new data wich of course is going to be a different size and totally screw up the rest of the zip file.
I give an example for excel in this post Parse text file and create an excel report
which may be of use even though i am using a different zip library and VB (Im still doing exactly what you are trying to do, my code is about half way down)
here is the part that applies
Using z As ZipFile = ZipFile.Read(xlStream.BaseStream)
'Grab Sheet 1 out of the file parts and read it into a string.
Dim myEntry As ZipEntry = z("xl/worksheets/sheet1.xml")
Dim msSheet1 As New MemoryStream
myEntry.Extract(msSheet1)
msSheet1.Position = 0
Dim sr As New StreamReader(msSheet1)
Dim strXMLData As String = sr.ReadToEnd
'Grab the data in the empty sheet and swap out the data that I want
Dim str2 As XElement = CreateSheetData(tbl)
Dim strReplace As String = strXMLData.Replace("<sheetData/>", str2.ToString)
z.UpdateEntry("xl/worksheets/sheet1.xml", strReplace)
'This just rezips the file with the new data it doesnt save to disk
z.Save(fiRet.FullName)
End Using
According to the official Github documentation, you should Use write_buffer instead open. There's also a code example at the link.

Resources