Well, it has been a few days since I posted (nearly a 10!) but here I am with another Ruby and Rails post that will hopefully help all of you out. What I intend to cover is the use of the gsub function (used for regular expression matching on Strings) and the send_data feature that you can use in your rails applications. After reviewing both of those usefull tools I will share a recent Model I developed for creating, saving, and modifying FDFs (Forms Data File? - used to fill in PDF text fields). So, lets begin!
First on my list is the String.gsub method (of, you guessed it, the String class). It is a Ruby method that allows you to run a regular expression global substitution on a string. So how does it work? Simply tack .gsub on the end of your string and use it like so:
@mystring.gsub(/a/ , 'b')
This of course is a very simplistic example - but easy to reference. 'a' (needle) is the string you are searching for in @mystring (haystack) - and 'b' is what you want to replace the needle with. An example from the manual:
"hello".gsub(/[aeiou]/, '*') #=> "h*ll*"
Its that simple! And who doesnt love working with Regular Expressions? To learn more about gsub go to Ruby's Class reference on Strings.
Now on to the second part of this post - the send_data function that is part of the Ruby on Rails framework. I covered send_file in an earlier post so it is only fitting that I give send_data a spin. Send_data allows you to transfer data as files straight to any visitor. Placing send_data in a particular action causes that action not to render a view (so you better have a redirect_to :action => "anotheraction" if you want to keep your visitors around!). So what is the syntax? Straight out of one of my controllers:
send_data(@myfdf, {:type => "application/vnd.fdf", :stream => false, :filename => "thefile.fdf"})
Lets quickly dissect this function:
@myfdf is the data you are sending, then inside the array we have -- :type => "application/vnd.fdf" where you set the content type of the data you are sending (which in alot of cases doesn't really do anything). :stream => false sets the stream option to off (true by default - however my server has had problems streaming files... ). And finally :filename => "thefile.fdf" sets the name that your visitor's browser will try to save the file as.
So in the example above we are sending data to our visitor to save as an fdf file... and - as you already know, creating fdfs is what I am going to cover next.
You can find some information about FDF files on the Rails Wiki so be sure to check that out. In any case, I want to cover the basics of a FDF Model that allows us to save FDF data to the database and then extract, modify and send that data to our users.
So lets first establish a very simple database table to hold what little we actually want to store:
CREATE TABLE `fdfs` (
`id` int(11) NOT NULL auto_increment,
`data` longtext NOT NULL,
`url` varchar(255) NOT NULL default '',
PRIMARY KEY (`id`)
);
I plan on only storing the data fields in the database along with a default URL that my FDF will use to open its corresponding PDF.
Now we have established the database table lets get our model together. I originally thought about sectioning this out into snippets to explain - but instead I will just paste it all here (for easy copying) and then explain the methods below:
class Fdf < ActiveRecord::Base
def add_data(fdfdata)
return self.data += format_data(fdfdata)
end
def hide_data(field)
return self.data = self.data.gsub(/(<<\/T\(#{field}\)\/V\()(\w*\)>>\s)/, '')
end
def create_header
return "%FDF-1.2\x0d%\xe2\xe3\xcf\xd3\x0d\x0a 1 0 obj\x0d<< \x0d/FDF << /Fields [ "
end
def create_footer
return "] \x0d >> \x0d >> \x0dendobj\x0d trailer\x0d<<\x0d/Root 1 0 R \x0d\x0d>>\x0d %%EOF\x0d\x0a"
end
def specify_pdf(pdf_url)
return "] \x0d /F (" << pdf_url << ") /ID [ <" << Digest::SHA1.hexdigest(Time.now.to_i.to_s) << ">"
end
def format_data(fdfdata)
data = ''
fdfdata.each { |key,value|
if value.class == Hash
value.each { |sub_key,sub_value|
data += '<</T(' + key + '_' + sub_key + ')'
data += '/V(' + escape_data(sub_value) + ')>> '
}
else
data += '<</T(' + key + ')/V(' + escape_data(value) + ')>> '
end
}
return data
end
def escape_data(fdfdata)
fdfdata = fdfdata.to_s.strip.gsub(/[\(]/, '\\(')
fdfdata = fdfdata.to_s.gsub(/[\)]/, '\\)')
return fdfdata
end
end
Essentially - we have 7 methods, 3 of which we will interact with to create our FDF file and 2 we will call to finalize the fdf before we send it. So, now that we have the model - what do all these methods do?
add_data(fdfdata) --- allows us to add data to our FDF object, we simply pass the data in as a hash
hide_data(fdfdata) --- allows us to hide certain fields - simply pass a field name to the hide_data method to remove both the field and the its value from your FDF
create_header / footer --- prepares the top and bottom necessary portions for our FDF files
specify_pdf --- allows us to define the PDF that our FDF will put the data in
format_data and escape_data are used by the other methods - and simply prepare the data for saving
To make things easy let me give you the example from my test controller that has one action that saves an FDF object to the database, then another action that extracts that FDF object, modifies it and sends it out.
class TestController < ApplicationController
def index
@fdfobj = Fdf.new
@fdfobj.add_data({"field1","value1","field2","value2"})
@fdfobj.url = "http://www.testurl.com"
@fdfobj.save
end
def getfdf
@getit = Fdf.find(1)
@getit.add_data({"field3","valu(e3)","field4","value4"})
@getit.hide_data("field2")
myfdf = @getit.create_header
myfdf += @getit.data
myfdf += @getit.specify_pdf(@getit.url)
myfdf += @getit.create_footer
send_data(myfdf, {:type => "application/vnd.fdf", :stream => false, :filename => "thefile.fdf"})
end
end
So in this test controller we use the index method simply to instantiate a new FDF object. After it is created we add some data to it (using the add_data method), assign a default url, and save it to our database.
In the getfdf method we Find the first FDF object and assign it to @getit. We then decide that we want to add some additional data to this FDF today (maybe a new date field, or new instructions). So we pass it a hash {"field3","valu(e3)","field4","value4"} to the add_data field, which simply appends this new data to our FDF object.
But since we added some data - we also decided that some of the FDF data should not be visible for this person (or maybe some of the data is old, and should just be removed) - so we use the hide_data method - and simply pass it the field name we want removed. I plan on updating this soon to also support a hash (to remove multiple fields at once) - but will leave that for a side project.
And to prepare the FDF file for distrobution we simply assign the values of our FDF object (along with the header and footer information) to the variable myfdf before we use that new send_data function I briefly reviewed earlier.
That does it! Now when you goto the getfdf action of the test controller it will prompt you to download a file - and it just so happens to be an FDF file... happy coding =)

RR said:
Thanks for the post, I used your code but found that the value generator in format_data needed the '[' and ']' removed in order for the FDF to properly fill with pdftk (1.12, OSX). I referenced Adobe API's FDF samples and they defined the values as "/V(your_val_here)".
2007-03-06 15:53:57 UTCCharles said:
Hi RR. Sorry, didn't see your comment - Exim is supposed to email these to me, but it appears my ISP has started blocking all my outgoing mail... go figure.
Anyway - Thanks for the bug fix - I wrote this some time ago with the intention of putting it into production, and it appeared to work fine for me (i tested it with a currently running app). I didn't come across this problem, but I can see what you are pointing out.
Thanks again, ill update the article with your input.
2007-03-11 23:48:23 UTCEckhart said:
Are you able to loop through multiple db records with fdf to batch process your db entries either into multiple output.pdf or a single multipage output.pdf ?
If so, would you be so kind to add an example ?
Thank you
2007-07-02 09:57:12 UTC