Dynamic Paper Pages
Dump That Clumsy Commercial Software
Moving from Windows to Unix is not about replacing Excel with Gnumeric, it's mental shift that rids itself of the notion that software is magic. For some this kind of transition starts even nder Windows when you first learn to write a CGI script in Perl or an application server in Python instead of using ASP under IIS. Not only is ASP not magic, but it actually limits your application development by locking you into a few clumsy paradigms.
Microsoft ideas fail to impress most web developers because anyone who programs for the web understands that they don't need artificial abstraction and a Microsoft API, they just need the tools. This is not only true for web, it's true almost everywhere. This tutorial is intended to demonstrate how to think about and employ open-source tools to implement a solution that produces output on a device that we all use, but rarely program for: the laser printer.
Generating PostScript
The World Wide Web is based on a few document standards (HTML, CSS) and a few protocols (DNS, HTTP, HTTPS). Historically the printed content is (or should be) described by a language called PostScript. Only high-end ink-jets support PS, but it's common for lasers to have an interpreter built in. The reason PostScript is so important is because it's a standardized way to producing printed documents. Unlike HTML, PS is an interpreted programming language, and it produces exact results.
So how do we generate PostScript?
| Web Browser | This is what most web developers do--generate an HTML report that is roughly formatted for a page, and have the user go to File -> Print. This is bad because it generally produces unprofessional output, and it makes the user click, click click for every section that needs printed. |
| TeX | TeX is a macro language available on UNIX systems since the beginning of time: it was put into production on the PDP-10. Features excellent typesetting capabilities, but is very hard to script for. Unlike XML, the generation logic required to produce .tex files must handle many peculiar exceptions, and has requires case-by-case scring translations to make input sane in each particular context. |
| Docbook XML | Popular among open-source projects such as FreeBSD, KDE, GNOME, etc. Oddly enough it's better at producing HTML than PS. Why? Because like most document system, it relies on LaTeX/teTeX to generate PS, and current DocBook only understands a few formating constructs. Enough to build a book, but not nearly flexible enough make sense for reporting. |
| C API | LASi is one such C++ library that uses a stream processor to to generate PostScript files from a program. |
| Proprietary Solutions | There are commercial applications that we won't waste our time on here. |
| Lout | Designed for the generation of page generation and PS output only. Simple, consistent API that is easy to build using almost any generic (non-XML) templating system. This is the method I use most often. |
| Nonpareil | A project to implement a functional programming language with macros aimed at generating printed documents. Language specification and alpha compiler have been produced, but it's not finished yet. |
Including Graphics
Any valid EPS file can be included in a Lout document, and any application that can print to file can be used to generate a PS file. After printing use eps2eps to convert this to a PS file that can be embedded:
% eps2eps page_header.ps page_header.eps
Then run gs to find the real bounds of the graphic:
% gs -sDEVICE=bbox -dNOPAUSE -dBATCH page_header.eps %%BoundingBox: 65 659 512 758 %%HiResBoundingBox: 65.183496 659.745015 511.434507 757.700656
Plug these figures in at the top of the .eps file and it will now be sized correctly for inclusion in another document.
Alternatively use the ps2epsi utility included with GhostScript:
% ps2epsi page_header.ps page_header.eps
Similarly, ImageMagick's convert command can be used to convert a .png file retreaved from the web:
% wget http://us300-ob0.teisprint.net/mrtg/72.20.214.6_fa0_6-week.png -O this_week.png % convert this_week.png this_week.ps
Building a Basic Lout Document
Now that I've worked out the means for obtaining and including some of my graphic content, I draft and test a static document.
@SysInclude{ doc }
@SysInclude { tbl }
@Document
//
@Text @Begin
@IncludeGraphic "page_header.eps"
@CD
@Heading "Monthly bandwidth graph for December"
@CD
0.7 @Scale @IncludeGraphic "this_week.eps"
@CD
@Heading "Charges"
@CD 10c @Wide @Tbl
rule{yes}
aformat { @Cell A | @Cell B }
{
@Rowa A { Colocation, 2U: } B { $50.00 }
@Rowa A { Bandwidth: } B { $95.00 }
@Rowa A { Backup Services: } B { $15.00 }
@Rowa A { @B Total: } B { @B $160.00 }
}
@End @Text
Interpret and test:
% lout report.lout > report.ps && gv report.ps
Dynamic Page Generation
Once the layout for the presentation looks good copy the static document and plug in some logic and values with your favorite templating language. This is using eRuby:
<% total = 0 %>
<% for service in services %>
@Rowa A { <%= service['description'] %> } B { <%= service['price'] %> }
<% total = total + service['price'] %>
<% end %>
@Rowa A { @B Total: } B { @B <%= total %> }
The Ruby script that imports this template is tasked with pulling the right information from the database, downloading the correct graph for that customer, running Lout to product the PostScript output, and finally with calling lpr to print the document.
First import the template, cgi, and database support:
require "eruby" require "cgi" require "dbi"
Next instantate a CGI class for parsing arguments and pull the relavant recods from the database:
cgi = CGI.new
id = Integer(cgi['id'])
dbh = DBI.connect('DBI:Pg:system')
conn = dbh.prepare("SELECT * FROM services(?)")
conn.execute(id)
services = conn.fetch_all
Finally generate the doucment and print it!
puts "Content-Type: text/plain"
puts
puts "Processing customer (#{id})"
puts
puts " Building Template... [eruby]"
`wget http://us300-ob0.teisprint.net/mrtg/#{service['graph'} -O this_week.png`
`convert this_week.png this_week.ps`
saved_stdout = $stdout
output = File.open("envalope.l","w")
$stdout = output
ERuby::import('report.rl')
output.close
$stdout = saved_stdout
puts " Rendering... [lout]"
`/usr/pkg/bin/lout report.l > report.ps`
`lpr -P8440 -h envalope.ps`
puts
puts "Done."