Hackers Corner: Proportional Tables in PostScript
Vector 8:1, page 92
Transcribed from the printed issue; not yet reviewed. Read it in the PDF of the issue, page 92
My immediate problem was to output a decent looking table of figures from an APL application to a PostScript printer. Rather than chickening out and just using Courier (and hence falling into the category of someone who thinks WordPerfect is a wordprocessor), I thought it would be more fun to get APL to write a little PostScript program to do the hard work.
Here is the kind of output I was after:
Hello from TEST.TXT … the following lines are tabbed
Name Age Notes Adrian 38 Well known troublemaker Gill 38 Wife of well-known troublemaker Richard 7 Generally a good child Tim 5 Generally a bad child The end.
The two problems to be solved were the simple one of lining up the columns in a proportional font, and the secondary issue of getting right justification on the numeric data.
The APL code to do this is mostly just a few globals to set up font name, size and pitch, as well as the page layout and tab positions. All the serious processing is done with a few PostScript routines, which go as a prologue on the front of the file:
%!PS-Adobe-2.0
%% Prologue for PostScript print
/inch {72 mul} def
/LM 1.00 inch def /BM 1.00 inch def /TM 1.00 inch def
/PW 11.25 inch def /PH 8.6 inch def
The ‘%’ is simply a comment, and the first line is a useful convention … several printers such as the Qume PS range will switch automatically between Epson, LaserJet and PostScript emulation … they recognise a PostScript file by the ‘%!’ at the beginning. The subsequent lines define a trivial function to convert from inches to points, and a bunch of globals to define the page size and margins.
Next we set up some tabs, measured in inches from the left margin. My simple convention is to make right tabs negative … if you want centred or decimal tabs you will have to devise something better!
Define Top Of Form as the Page Height less the Top Margin, and a null function to Orient the page (not required if we are in portrait mode, but essential for landscape … see below). Then define FormFeed as a function to print the current page, orient it and go to the top …
… and NewLine to set the typeball to the next tab, output any pending text, reset the tab bar to the first tab, and drop down Pitch positions. It checks if we have overstepped the bottom margin … if so we need a FormFeed:
Now for the hard bit! The PostScript ‘get’ primitive is essentially APL’s array indexing, so read the first line of TAB as tb←TABS[TB] (zero origin, of course). The next line reads moveto (LM+inch |tb),VPOS (remember that negative tabs are right aligned, so the sign must be ignored at this stage); then we check that we have not fallen off the end of the tab rack, and if there is another tab to go, we increment the tab counter TB. (length is the PostScript equivalent of ⍴, but because we are in ⎕IO←0, we need to subtract 1.) Finally, we check the sign of the tab and either display the preceeding text directly (using show) or right align it (using our own rshow function)
/TAB { TABS TB get /tb exch def
tb abs inch LM add VPOS moveto
TABS length 1 sub TB gt { /TB TB 1 add def } if
tb 0 lt {rshow} {show} ifelse } def
/rshow { dup stringwidth pop neg 0 rmoveto show } def
If the user has requested landscape printing, we can throw in a little function to rotate the paper and shift origin appropriately:
And finally, we select the requested font and display the data. Note that APL needs to make no allowance for pagination … all the hard work is done by the PostScript NL function which will allow for the specified margins and throw pages accordingly.
/Helvetica findfont 18 scalefont setfont
(Hello from TEST.TXT ... the following lines are tabbed) NL
() TAB (Name) TAB (Age) TAB (Notes) NL
() TAB (Adrian) TAB (38) TAB (Well known troublemaker) NL
() TAB (Gill) TAB (38) TAB (Wife of well-known troublemaker) NL
() TAB (Richard) TAB (7) TAB (Generally a good child) NL
() TAB (Tim) TAB (5) TAB (Generally a bad child) NL
() NL
(The end.) NL
FF
Note the final FormFeed to throw out the last page!
Simple lines are just wrapped up in an opening parenthesis and a ‘) NL’; anything between parentheses is treated as a text string, and hence is the argument to the NL function (remember that NL calls TAB to do the output!). Where we would logically insert tab characters in the text, we instead insert the string ‘) TAB (’. This conveniently takes account of the first tab at position zero (i.e. the left margin) by inserting a null string at the first tab … subsequent tabbed sections are output by the TAB function at the appropriate position on the page.
I’m afraid I wrote all this in a boring old first generation APL, and simply hard-coded the PostScript into my reporting function. The correct solution is obviously to write a general-purpose formatter that will take a nested argument (in the example a 3-element array consisting of two text vectors separated by a numeric vector) and the required tab positions, and will automatically generate the PostScript file ready to send to the printer.
It could even use a table of character widths to work out the optimum tab positions for itself (automatically left and right aligning text and numbers); this one is definitely left as an exercise for the reader … I will stick to the trial-and-error approach until I have a real need for something better.