Day 12 - Producing PDFs with Template::Latex
Using Template::Latex to produce PDFs in Catalyst
LaTeX
LaTeX (pronounced "lay-tek" by those in the know) is a typesetting language that can be used to produce DVI ("Device Independent"), Postscript, or PDF files. However the main problem is that LaTeX (and TeX, upon which LaTeX is based) is one of those "easy to learn, hard to master languages". If you don't know LaTeX and are interested, there are a numerous tutorials around the web - hit Google. For the sake of completeness I will show a minimal example of a LaTeX file:
\documentclass[a4paper]{article} \begin{document} Look at me! I was typeset using \LaTeX And I'm a second paragraph that is split over two source lines. \end{document}
If you run pdflatex
(assuming you have it installed) and type those
five lines in, you will end up with a small, beautifully presented PDF
file. Easy wasn't it?
Template::Latex
So now that you know LaTeX (*cough*) we can go on and produce PDF documents
from inside Catalyst. You could simply do a system('pdflatex')
call, except
for the fact that TeX and LaTeX really don't like taking data from STDIN -
I guess STDIN wasn't so common back in the 1970's when TeX was first written.
So rather than going through the hassle of creating a temporary directory, saving the .tex file, parsing the output log and getting the output file ourselves, we will use the Template::Latex module by Andy Wardley which handles all of this for us. Great!
Once you've installed Template::Latex (and the LaTeX system itself if you don't already have it installed) it's time to create a Catalyst application:
$ catalyst.pl PDFTest $ cd PDFTest $ perl ./script/pdftest_create.pl view TT TT
Now we need to create our action that will generate the PDF file. In
lib/PDFTest/Controller/Root.pm
add the following code:
sub pdf : Local { my ($self, $c) = @_; if ($c->forward( 'PDFTest::View::TT' ) ) { # Only set the content type if we sucessfully processed the template $c->response->content_type('application/pdf'); $c->response->header('Content-Disposition', "attachment; filename=PDFTest.pdf") } }
Now let's create the template. Create a new file in your editor of
choice, and save it as root/pdf
with the following contents:
[%- USE Latex; FILTER latex("pdf") %] \documentclass{article} \begin{document} I'm a doument created from [% name %] \end{document} [% END -%]
(If you are using this in an existing application and you are using a wrapper, make sure that this template isn't wrapped, or you will get the binary PDF data nicely placed inside your wrapper.)
Testing It Out
We've created a template that uses the LaTeX filter, and an action that will cause it to be rendered. Let's test it!
$ perl ./script/pdftest_server.pl <output snipped> You can connect to your server at http://localhost:3000
Since we called our action pdf, let's visit http://localhost:3000/pdf. Now with any luck your browser should display the PDF inline or prompt you to save what to do with a file called "PDFTest.pdf". However if you are using Catalyst::Runtime version 5.7006 (which is the latest at the time of writing) then you will be seeing an error like the following:
Couldn't render template "latex error - pdflatex exited with errors: "
This is caused by a bug in the standalone server only (version 5.7006 - this will be fixed in newer versions) where all system calls return -1. Hmm, so we could use one of the other server modes, but they make developing harder. So instead let's use a dirty hack to get round the problem.
WARNING: This is very dirty and prone to break for different versions, but it worked for me with pdfeTeX 3.141592-1.21a-2.2 (Web2C 7.5.4) and Template::Latex 2.17. Don't use this in production code. Use it for development only. You have been warned.
The way this hack works is be defining a system
sub in the
Template::Latex
package that will get called instead of Perl's built-in
system. What this 'replacement' system call does is first call the built-in
system
, and it then examines the last line of the log file to see if it
says "Output written on filename.pdf", in which case it returns 0, else it
returns the return value of the built-in system call.
Rather than explaining how to go about this hack, I will just include
what the pdf
sub should look like:
sub pdf : Local { my ($self, $c) = @_; { no warnings 'redefine'; # right here, you can tell bad things will happen local *Template::Latex::system = sub { my $ret = system(@_); my ($filename) = $_[0] =~ m[\\input{(.*?)}] ; my $fh = new IO::File "${filename}.log" or die "Unable to open pdflatex logfile ${filename}.log: $!"; my $line; while ( defined($_ = $fh->getline) ) { $line = $_; } return 0 if $line =~ /^Output written on ${filename}.pdf \(\d+ pages?, \d+ bytes?\).$/; return $ret; } if $c->engine =~ /^Catalyst::Engine::HTTP/; if ($c->forward( 'PDFTest::View::TT')) { $c->response->content_type('application/pdf'); $c->response->header('Content-Disposition', "attachment; filename=PDFTest.pdf"); } } }
Caveat
Hacks like the one detailed above are generally a really bad idea, because you are messing with the internals of another package. This is asking for trouble. It's included so that you can make a working example with the development server under Catalyst 5.7006 - the bug should be fixed in subsequent versions.
AUTHOR
Ash Berlin <ash@cpan.org>
COPYRIGHT
Copyright 2006 Ash Berlin. This document can be freely redistributed and can be modified and re-distributed under the same conditions as Perl itself.