Monday, February 9, 2009

Forms: FDF, PDF, and PHP

I have always been somewhat opposed to PDF documents. While they are great for cross-platform viewing and printing, the inability to edit them without buying special software was philosophically unacceptable. Also, not being able to save forms you fill out is really stupid. While there are some free alternatives now, they are really a kind of pain in the ass compared to Adobe Professional. So, unfortunately, that is what I used to get this to work quickly and easily. Thanks to Justin Koivisto, who's tutorial really helped.

For those of you that don't know (as I didn't before starting this project), FDF files are used to save filled out PDF forms, however they don't store the original PDF document. Instead the FDF file just has a pointer to the PDF document which can either be on the local computer or on the web.

So here is the scenario:

1) User fills out an online form.
2) PHP parses the form, and creates a VCard as well as an FDF.
3) PHP emails the files as attachments, and displays the filled out PDF for the user to print.

This is very useful for populating PDF forms that already exist, and may be in use in paper form. This makes it easy to transition to an electronic system.


The first step is a standard HTML form that will submit the data to a PHP script (application.html):

<html>
<body>
<form method="post" action="./submit.php">
<table>
<tr><td>First Name</td><td><input type="text" name="firstName" /></td></tr>
<tr><td>Last Name</td><td><input type="text" name="lastName" /></td></tr>
<tr><td>Mobile Phone Number</td><td><input type="text" name="mobilePhone" /></td></tr>
<tr><td>Home Phone Number</td><td><input type="text" name="homePhone" /></td></tr>
<tr><td>E-Mail</td><td><input type="text" name="email" /></td></tr>
<tr><td>Date of Birth (MM-DD-YYYY)</td><td><input type="text" name="dobMonth" maxlength="2" size="2"/>-<input type="text" name="dobDay" maxlength="2" size="2"/>-<input type="text" name="dobYear" maxlength="4" size="4"/></td></tr>
<tr><td>Address</td><td><input type="text" name="address" /></td></tr>
<tr><td>City</td><td><input type="text" name="city" /></td></tr>
<tr><td>State</td><td><input type="text" name="state" /></td></tr>
<tr><td>Zip Code</td><td><input type="text" name="zip" /></td></tr>
<!--<tr><td></td><td><input type="text" name="" /></td></tr>-->
<tr><td>Age</td><td><input type="text" name="age" /></td></tr>
</table>
<input type="submit" value="Submit" />
</form>
</body>
</html>



Next is the PHP script to create the VCard and FDF, emails them, and displays the FDF (submit.php):

<?php

include("./vcard.php");
include("./fdf.php");
include ("./mail_attachment.php");

$emailto = "someone@example.com";

//The pdf file that the fdf file points to:
$pdf_doc='http://example.com/application.pdf';


//We need to check POST variables to make sure the form was submitted correctly, we should also make sure they don't enter more than one email address, and that they enter an email address
/*
if(isset($_POST["firstName"])) {
echo $_POST["firstName"];
}
else
echo "BAD!!!!";
*/
$v = new vCard();

$v->setPhoneNumber($_POST["mobilePhone"], "PREF;CELL;VOICE");
$v->setPhoneNumber($_POST["homePhone"], "PREF;HOME;VOICE");
$v->setName($_POST["lastName"], $_POST["firstName"], "", "");
$v->setBirthday($_POST["dobYear"]."-".$_POST["dobMonth"]."-".$_POST["dobDat"]);
$v->setAddress("", "", $_POST["address"], $_POST["city"], $_POST["state"], $_POST["zip"], "US");
$v->setEmail($_POST["email"]);
//$v->setNote("You can take some notes here.\r\nMultiple lines are supported via \\r\\n.");
//$v->setURL("http://www.thomas-mustermann.de", "WORK");
/*
$output = $v->getVCard();
$filename = $v->getFileName();

Header("Content-Disposition: attachment; filename=$filename");
Header("Content-Length: ".strlen($output));
Header("Connection: close");
Header("Content-Type: text/x-vCard; name=$filename");

echo $output;*/

/*$outfdf = fdf_create();
fdf_set_value($outfdf, "volume", $volume, 0);

fdf_set_file($outfdf, "http:/testfdf/resultlabel.pdf");
fdf_save($outfdf, "outtest.fdf");
fdf_close($outfdf);
Header("Content-type: application/vnd.fdf");
$fp = fopen("outtest.fdf", "r");
fpassthru($fp);
unlink("outtest.fdf");*/

foreach($_POST as $name => $val) {
$data[$name] = $val;
//echo $name."<br/>";
}


// generate the file content
$fdf_data=createFDF($pdf_doc,$data);
$attachments[0]['data']=$v->getVCard();
$attachments[0]['filename']=$_POST['firstName'].$_POST['lastName'].".vcf";
$attachments[0]['mime']="text/x-vcard";

$attachments[1]['data']=$fdf_data;
$attachments[1]['filename']=$_POST['firstName'].$_POST['lastName'].".fdf";
$attachments[1]['mime']="application/vnd.fdf";

$from=$_POST['firstName']." ".$_POST['lastName']." <".$_POST['email'].">";

mail_attachment($from,$emailto,"Rental Application", "Lease: from ".$_POST['leaseStart']." to ".$_POST['leaseEnd'], $attachments);
Header("Content-type: application/vnd.fdf");
echo $fdf_data;

?>

This shows an example of how to use the multi-attachment email script shown below. We should be checking the POST inputs to make sure someone isn't trying to do something nasty, there could be a security problem there.


Notably, there included files that you need. The first is the VCard creator (vcard.php):

<?

/***************************************************************************



PHP vCard class v2.0

(c) Kai Blankenhorn

www.bitfolge.de/en

kaib@bitfolge.de





This program is free software; you can redistribute it and/or

modify it under the terms of the GNU General Public License

as published by the Free Software Foundation; either version 2

of the License, or (at your option) any later version.



This program is distributed in the hope that it will be useful,

but WITHOUT ANY WARRANTY; without even the implied warranty of

MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

GNU General Public License for more details.



You should have received a copy of the GNU General Public License

along with this program; if not, write to the Free Software

Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.



***************************************************************************/





function encode($string) {

return escape(quoted_printable_encode($string));

}



function escape($string) {

return str_replace(";","\;",$string);

}



// taken from PHP documentation comments

function quoted_printable_encode($input, $line_max = 76) {

$hex = array('0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F');

$lines = preg_split("/(?:\r\n|\r|\n)/", $input);

$eol = "\r\n";

$linebreak = "=0D=0A";

$escape = "=";

$output = "";



for ($j=0;$j<count($lines);$j++) {

$line = $lines[$j];

$linlen = strlen($line);

$newline = "";

for($i = 0; $i < $linlen; $i++) {

$c = substr($line, $i, 1);

$dec = ord($c);

if ( ($dec == 32) && ($i == ($linlen - 1)) ) { // convert space at eol only

$c = "=20";

} elseif ( ($dec == 61) || ($dec < 32 ) || ($dec > 126) ) { // always encode "\t", which is *not* required

$h2 = floor($dec/16); $h1 = floor($dec%16);

$c = $escape.$hex["$h2"].$hex["$h1"];

}

if ( (strlen($newline) + strlen($c)) >= $line_max ) { // CRLF is not counted

$output .= $newline.$escape.$eol; // soft line break; " =\r\n" is okay

$newline = " ";

}

$newline .= $c;

} // end of for

$output .= $newline;

if ($j<count($lines)-1) $output .= $linebreak;

}

return trim($output);

}



class vCard {

var $properties;

var $filename;



function setPhoneNumber($number, $type="") {

// type may be PREF | WORK | HOME | VOICE | FAX | MSG | CELL | PAGER | BBS | CAR | MODEM | ISDN | VIDEO or any senseful combination, e.g. "PREF;WORK;VOICE"

$key = "TEL";

if ($type!="") $key .= ";".$type;

$key.= ";ENCODING=QUOTED-PRINTABLE";

$this->properties[$key] = quoted_printable_encode($number);

}



// UNTESTED !!!

function setPhoto($type, $photo) { // $type = "GIF" | "JPEG"

$this->properties["PHOTO;TYPE=$type;ENCODING=BASE64"] = base64_encode($photo);

}



function setFormattedName($name) {

$this->properties["FN"] = quoted_printable_encode($name);

}



function setName($family="", $first="", $additional="", $prefix="", $suffix="") {

$this->properties["N"] = "$family;$first;$additional;$prefix;$suffix";

$this->filename = "$first%20$family.vcf";

if ($this->properties["FN"]=="") $this->setFormattedName(trim("$prefix $first $additional $family $suffix"));

}



function setBirthday($date) { // $date format is YYYY-MM-DD

$this->properties["BDAY"] = $date;

}



function setAddress($postoffice="", $extended="", $street="", $city="", $region="", $zip="", $country="", $type="HOME;POSTAL") {

// $type may be DOM | INTL | POSTAL | PARCEL | HOME | WORK or any combination of these: e.g. "WORK;PARCEL;POSTAL"

$key = "ADR";

if ($type!="") $key.= ";$type";

$key.= ";ENCODING=QUOTED-PRINTABLE";

$this->properties[$key] = encode($name).";".encode($extended).";".encode($street).";".encode($city).";".encode($region).";".encode($zip).";".encode($country);



if ($this->properties["LABEL;$type;ENCODING=QUOTED-PRINTABLE"] == "") {

//$this->setLabel($postoffice, $extended, $street, $city, $region, $zip, $country, $type);

}

}



function setLabel($postoffice="", $extended="", $street="", $city="", $region="", $zip="", $country="", $type="HOME;POSTAL") {

$label = "";

if ($postoffice!="") $label.= "$postoffice\r\n";

if ($extended!="") $label.= "$extended\r\n";

if ($street!="") $label.= "$street\r\n";

if ($zip!="") $label.= "$zip ";

if ($city!="") $label.= "$city\r\n";

if ($region!="") $label.= "$region\r\n";

if ($country!="") $country.= "$country\r\n";



$this->properties["LABEL;$type;ENCODING=QUOTED-PRINTABLE"] = quoted_printable_encode($label);

}



function setEmail($address) {

$this->properties["EMAIL;INTERNET"] = $address;

}



function setNote($note) {

$this->properties["NOTE;ENCODING=QUOTED-PRINTABLE"] = quoted_printable_encode($note);

}



function setURL($url, $type="") {

// $type may be WORK | HOME

$key = "URL";

if ($type!="") $key.= ";$type";

$this->properties[$key] = $url;

}



function getVCard() {

$text = "BEGIN:VCARD\r\n";

$text.= "VERSION:2.1\r\n";

foreach($this->properties as $key => $value) {

$text.= "$key:$value\r\n";

}

$text.= "REV:".date("Y-m-d")."T".date("H:i:s")."Z\r\n";

//$text.= "MAILER:PHP vCard class\r\n";

$text.= "END:VCARD\r\n";

return $text;

}



function getFileName() {

return $this->filename;

}

}





// USAGE EXAMPLE

/*

$v = new vCard();



$v->setPhoneNumber("+49 23 456789", "PREF;HOME;VOICE");

$v->setName("Mustermann", "Thomas", "", "Herr");

$v->setBirthday("1960-07-31");

$v->setAddress("", "", "Musterstrasse 20", "Musterstadt", "", "98765", "Deutschland");

$v->setEmail("thomas.mustermann@thomas-mustermann.de");

$v->setNote("You can take some notes here.\r\nMultiple lines are supported via \\r\\n.");

$v->setURL("http://www.thomas-mustermann.de", "WORK");



$output = $v->getVCard();

$filename = $v->getFileName();



Header("Content-Disposition: attachment; filename=$filename");

Header("Content-Length: ".strlen($output));

Header("Connection: close");

Header("Content-Type: text/x-vCard; name=$filename");



echo $output;*/

?>


Next is the FDF creator (fdf.php):

<?php

/*

KOIVI HTML Form to FDF Parser for PHP (C) 2004 Justin Koivisto

Version 2.1.2

Last Modified: 9/12/2005



This library is free software; you can redistribute it and/or modify it

under the terms of the GNU Lesser General Public License as published by

the Free Software Foundation; either version 2.1 of the License, or (at

your option) any later version.



This library is distributed in the hope that it will be useful, but

WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY

or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public

License for more details.



You should have received a copy of the GNU Lesser General Public License

along with this library; if not, write to the Free Software Foundation,

Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA



Full license agreement notice can be found in the LICENSE file contained

within this distribution package.



Justin Koivisto

justin.koivisto@gmail.com

http://koivi.com

*/



/*

* createFDF

*

* Takes values submitted via an HTML form and fills in the corresponding

* fields into an FDF file for use with a PDF file with form fields.

*

* @param $file The pdf file that this form is meant for. Can be either

* a url or a file path.

* @param $info The submitted values in key/value pairs. (eg. $_POST)

* @result Returns the FDF file contents for further processing.

*/

function createFDF($file,$info){

$data="%FDF-1.2\n%????\n1 0 obj\n<< \n/FDF << /Fields [ ";

foreach($info as $field => $val){

if(is_array($val)){

$data.='<</T('.$field.')/V[';

foreach($val as $opt)

$data.='('.trim($opt).')';

$data.=']>>';

}else{

$data.='<</T('.$field.')/V('.trim($val).')>>';

}

}

$data.="] \n/F (".$file.") /ID [ <".md5(time()).">\n] >>".

" \n>> \nendobj\ntrailer\n".

"<<\n/Root 1 0 R \n\n>>\n%%EOF\n";

return $data;

}

?>

There is an official PHP FDF creator, but it uses some ridiculous dll file that you need to install. PHP is great at string manipulation, why the heck do we need a dll? Anyway, thanks a lot Justin Koivisto.

Finally a custom email script I wrote to email multiple attachments (mail_attachment.php):

<?php

function mail_attachment ($from , $to, $subject, $message, $attachments){

$email_from = $from; // Who the email is from

$email_subject = $subject; // The Subject of the email

$email_txt = $message; // Message that the email has in it



$email_to = $to; // Who the email is to

$headers = "From: ".$email_from;


$semi_rand = md5(time());

$mime_boundary = "==Multipart_Boundary_x{$semi_rand}x";



$headers .= "\nMIME-Version: 1.0\n" .

"Content-Type: multipart/mixed;\n" .

" boundary=\"{$mime_boundary}\"";



$email_message .= "This is a multi-part message in MIME format.\n\n" .

"--{$mime_boundary}\n" .

"Content-Type:text/html; charset=\"iso-8859-1\"\n" .

"Content-Transfer-Encoding: 7bit\n\n" . $email_txt . "\n\n";



foreach ($attachments as $a) {

$data = chunk_split(base64_encode($a['data']));



$email_message .= "--{$mime_boundary}\n" .

"Content-Type: {$a['mime']};\n" .

" name=\"{$a['filename']}\"\n" .

"Content-Transfer-Encoding: base64\n\n" .

$data . "\n\n";

}



$email_message .= "--{$mime_boundary}--\n";



$ok = @mail($email_to, $email_subject, $email_message, $headers);



if($ok) {

} else {

die("There was a problem sending the email. Please check your inputs.");

}

}



?>



Thank you about.com for the HTML Converter.

21 comments:

Graham said...

Thanks so much for this - it's a fantastic piece of work. I have noticed one small bug - in the vCard properties in submit.php, you have the first name and last name post fields the wrong way round.

Thanks again!

Clayton Shepard said...

Glad it helped :)

This was not a well explained post at all. I just posted it for future reference, and just in case anyone else could use it.

Thanks a lot for letting me know about the bug, I hadn't even noticed it yet :O (although the people I did this for haven't deployed it yet, which makes it even more useful :) )

export00 said...

Thank you for this! Have been trying to implement a similar system using Justin's FDF class, your post has been very informative and has helped me out a lot. Thanks.

cameron said...

so ive got everything loaded and adjusted properly but i get

%FDF-1.2 %???? 1 0 obj << /FDF << /Fields [ <><><><><><><><><><><>] /F (./test.pdf) /ID [ ] >> >> endobj trailer << /Root 1 0 R >> %%EOF

any suggestions?

Clayton Shepard said...

@cameron

It looks like it *could* be a valid (empty) fdf file... are you setting the header properly?

cameron said...
This comment has been removed by the author.
cameron said...

I guess I'd have to say that I copied your stuff and used it, I obviously generated my own pdf and custom textfields of course....

C

So when you say set the header....I cant answer other then I used what you had

Clayton Shepard said...

You will notice the second to last line of code in submit.php says:

Header("Content-type: application/vnd.fdf");

This line is what tells the browser what kind of information is being sent to it, and, thus, how to render it. If this line isn't there or malfunctioning (as it will do if you echo/print something before you get to this line), then the browser will display the fdf file rather than use adobe to open it.

If Adobe Acrobat is not installed properly then the browser may not know what to do with a fdf file even if it gets this line, and may just display the fdf as text.

After you have corrected both of these issues then you may also have a problem with adobe finding the original pdf file, since it appears that you have it referenced simply by "./test.pdf".

Good luck.

cameron said...

recopied and put a space before the header and this is what I got.

Warning: Cannot modify header information - headers already sent by (output started at /home/planetx2/public_html/phpformtest/fdf.php:9) in /home/planetx2/public_html/phpformtest/submit.php on line 64
%FDF-1.2 %???? 1 0 obj << /FDF << /Fields [ <><><><><><><><><><><>] /F (http://69.175.12.154/~planetx2/phpformtest/test.pdf) /ID [ ] >> >> endobj trailer << /Root 1 0 R >> %%EOF

cameron said...

i used the root, but I put in the entire pdf url this time (building on a server backend)

Clayton Shepard said...

As I just mentioned, if you try to output anything before the Header() call then it won't work. (PHP has to send header information before it outputs anything, so if you don't specify a header before creating output then it will create one for you.)

Stop outputting anything before that header call and make sure you have adobe acrobat installed correctly.

cameron said...

could I send you my submit.php and fdf.php file via email? Ive been working on this for like flippin two days and im going to explode. I could really use a hand.

Thanks regardless,

Clayton Shepard said...

If you pastebin it I may try to take a look when I have some time.

You really should try to figure it out on your own though; I know its painful, but thats how you learn ;p

Well, sometimes the only thing you learn is trivial, like that you missed a ';' somewhere, but it sounds like you could probably learn a little bit more than that =)

Mitchell said...

Thank you so much!! I've been fighting with KOIVI's tutorial for a few days now and after going through yours it just simply works!! You've made me and more importantly my client very happy!

mtv said...

Hello Clayton, thanks for the tutorial. how ever iam stating in php and there thing that I dont understand yet, I need to build a small application for my current job and they are asking me for that and iam in a hurry,I dont want to get fire. could you please help me out to do this app? we can talk about it and price on skype: logigen2, or mtaisigue@gmail.com.

Thank you
Regards

Mark said...

Clayton,

Much thanks, this tutorial really clarified how to handle the process of creating FDFs with PHP.

b!Vd said...
This comment has been removed by the author.
b!Vd said...

Hi Clayton
Thanks for this wonderful tutorial.

I have a small problem which I am hoping you can help me with it.
I used your exact code and just changed the email address and link of the final destination pdf. As I had seen in this comments section earlier, I had got a warning saying that


Cannot modify header information - headers already sent by (output started at ./fdf.php:2)



SO I moved this header tag to the top of the submit.php


Header("Content-type: application/vnd.fdf");


Now the pdf file opens in the browser but there is no data on it. Also I get an email with the two attachments and both those attachments are perfect( as in locally when I try doing it , the pdf is populated)

Any ideas as to where I am going wrong?

Thanks again

b!Vd said...

What I was trying to say earlier was that I was able to open both the attachments in my email and they are perfectly populated.

Clayton Shepard said...

@b!Vd

As soon as something creates output (e.g. print/echo/error/warning) then php *has* to send the HTTP header back to the client. Thus if you try to call the header() function after output has been created it doesn't work, since the header has already been sent.

In your case, for some reason fdf.php is creating output on line 2. This should not happen, so I would check fdf.php and make sure it hasn't been modified.

I don't remember this setup that well, but I have a lot of example code commented out. You may want to test just the fdf/browser part. If you create your html/pdf form values correctly, then you should just need something like:

<?php
include("./fdf.php");
foreach($_POST as $name => $val) {
$data[$name] = $val;
}
$fdf_data=createFDF('http://example.com/application.pdf',$data);
Header("Content-type: application/vnd.fdf");
echo $fdf_data;
?>


which may help you debug (note that this was just off the top of my head, and may have issues...). The fdf.php output you were experiencing may be the cause of the problem, but you'll have to your own debugging.

Good luck.


P.S. It appears I was missing a semi-colon on that echo $fdf_data. I will fix it.

Brandon Kem said...

This is the first time I've actually been able to have the fdf file automatically attached to an email.
Is there a way to remove the vcard function?
I'm also needing to send the data to a mysql database or access file for record keeping.
Any thoughts?
Brandon