#!/usr/bin/perl -w # Convert tbl-style input to Encapsulated PostScript # Author: Russ Cox, September 2007 # Version 1e-10 # TODO: u for staggered columns # TODO: w for width specifiers # TODO: z for zero-width items # TODO: repeated characters \Rx # TODO: text blocks T{ ... T} ??? # TODO: multipage tables, headings with .TH # TODO: options: # doublebox looks awful # linesize(x) delim(xy) # center, expand intentionally unimplemented # TODO: parse units in TS line, width specifier # Extensions to tbl: # format modifier ~ puts lines around entry use PostScript::FontMetrics; %fontfile = ( # Use .afm files installed by LaTeX - everyone has those! 'Times-Roman' => 'times/ptmr8a.afm', 'Times-Italic' => 'times/ptmri8a.afm', 'Times-Bold' => 'times/ptmb8a.afm', 'Times-BoldItalic' => 'times/ptmbi8a.afm' ); %fontalias = ( 'R' => 'Times-Roman', 'B' => 'Times-Bold', 'I' => 'Times-Italic', ); %fontcache = (); $table = []; $hline = [""]; $width = 0; $PS = 10; $VS = 12; $en = $PS/2; $maxcol = 0; $tabchar = "\t"; %options = ( "box" => "" ); $linewidth = 0.25; my $currentfont = "XXX"; my $currentsize = -1; my $ps = ""; my $y = 0; sub max { my $m = shift @_; while (@_ > 0){ my $n = shift @_; $m = $n if !defined($m) || (defined($n) && $n > $m); } return $m; } sub readinput() { my $intable = 0; my @fmts = (); my $tabpattern = $tabchar; $tabpattern =~ s/([^a-zA-Z0-9_])/\\$1/g; while(<>){ chomp; s/\s*$//; if(/^\.TS/){ $intable = 1; if(/^\.TS ([0-9.]+)$/){ $width = $1 * 72; }elsif(/^\.TS$/){ # nothing }else{ print STDERR "? $_\n"; } next; } if(/^\.TE/){ $intable = 0; dotable(); next; } next if !$intable; if(/\.ps (\d+)/){ $PS = $1; next; } if(/\.vs (\d+)/){ $VS = $1; next; } if($intable == 1){ # perhaps options $intable = 2; if(/(.*);$/){ my @opts = split(/,\s*|\s+/, $1); foreach my $opt (@opts) { if($opt =~ /^tab\((.*)\)/){ $tabchar = $1; $tabpattern = $tabchar; $tabpattern =~ s/([^a-zA-Z0-9_])/\\$1/g; next; } if($opt eq "box" || $opt eq "doublebox" || $opt eq "allbox"){ $options{"box"} = $opt; next; } print STDERR "warning: ignoring option $opt\n"; } next; } } if(/\.T\&/){ $intable = 2; @fmts = (); next; } if($intable == 2){ # expect format specifiers if(/\.$/){ $intable = 3; } s/\.$//; my @a = split(/,/); foreach my $a (@a) { push @fmts, parsefmt($a); } next; } if($intable == 3){ # expect actual table data if(/^[=_]$/){ $hline->[@$table] = $_; next; } if(/^\.sp (.+)/){ my $space = $1 * $VS; my $r = @$table; my $row = []; for($i=0; $i<=$maxcol; $i++){ my $c = {}; $c->{'row'} = $r; $c->{'col'} = $c; $c->{'fmt'} = lfmt(); $c->{'height'} = $space; $c->{'text'} = ""; $row->[$i] = $c; } push @$table, $row; $hline->[@$table] = ""; next; } my @f = @{$fmts[0]}; shift @fmts if @fmts > 1; my @a = split($tabpattern); if(@a > $maxcol){ print STDERR "warning: more text than format: $a[$maxcol]...\n"; @a = @a[0..$maxcol-1]; } my $row = []; my $r = @$table; for($i=0; $i<=$maxcol; $i++){ # make list of cells from format and text. my $c = {}; $c->{'row'} = $r; $c->{'col'} = $i; $c->{'height'} = 0; $row->[$i] = $c; my $a = $i < @a ? $a[$i] : ""; my $f = $i < @f ? $f[$i] : lfmt(); if($i == $maxcol){ $f->{'align'} = "dead"; } if($f->{'align'} =~ /[\^_]/){ print STDERR "Warning: ignoring text $a\n" if $a ne ""; $a = ""; } $c->{'fmt'} = $f; $c->{'text'} = $a; if($f->{'align'} eq "s"){ my $cc = $row->[$i-1]; if(!$cc->{'span'}){ $cc->{'span'} = [$cc]; } push @{$cc->{'span'}}, $c; $c->{'span'} = $cc->{'span'}; } if($f->{'align'} eq "^" || $a eq "\\^"){ $c->{'text'} = ""; my $cc = $table->[$r-1]->[$i]; if(!$cc->{'vspan'}){ $cc->{'vspan'} = [$cc]; } push @{$cc->{'vspan'}}, $c; $c->{'vspan'} = $cc->{'vspan'}; } } push @$table, $row; $hline->[@$table] = ""; next; } } dotable() if $intable; } sub lfmt() { my $F = {}; $F->{'align'} = 'l'; $F->{'vert'} = ""; $F->{'sep'} = 3*$en; $F->{'valign'} = "c"; $F->{'equal'} = 0; $F->{'ps'} = $PS; $F->{'vs'} = $VS; $F->{'font'} = "Times-Roman"; $F->{'lines'} = 0; return $F; } sub parsefmt($) { my ($fmt) = @_; my @a = (); my $sep = 3*$en; my $ncol = 0; my $vert = ""; while($fmt ne ""){ $fmt =~ s/^\s+//; last if $fmt eq ""; if($fmt =~ /^(\|+)(.*)/){ $vert = $1; $fmt = $2; next; } if($fmt =~ /^([lrcnas\^LRCNAS_])\s*(.*)/){ my $op = $1; $fmt = $2; my $F = lfmt(); push @a, $F; $op =~ y/A-Z/a-z/; $F->{'align'} = $op; $F->{'lo'} = $ncol++; $F->{'vert'} = $vert; $F->{'sep'} = $sep; $sep = 3*$en; $vert = ""; for(;;){ if($fmt =~ /^([0-9]+)\s*(.*)/){ $sep = $1 * $en; $fmt = $2; next; } if($fmt =~ /^([tTeEuUzZ])\s*(.*)/){ my $flag = $1; $fmt = $2; $flag =~ y/A-Z/a-z/; if($flag eq "t"){ $F->{'valign'} = "t"; }elsif($flag eq "e"){ $F->{'equal'} = 1; }elsif($flag eq "z"){ $F->{'zero'} = 1; } next; } if($fmt =~ /^([bBiI])\s*(.*)/){ my $f = $1; $fmt = $2; $f =~ y/a-z/A-Z/; $F->{'font'} = $f; next; } if($fmt =~ /^[fF]([a-zA-Z])(\s|$)\s*(.*)/){ $F->{'font'} = $1; $fmt = $3; next; } if($fmt =~ /^[fF]([a-zA-Z][a-zA-Z])\s*(.*)/){ $F->{'font'} = $1; $fmt = $2; next; } if($fmt =~ /^[fF]\(([^()]*)\)\s*(.*)/){ $F->{'font'} = $1; $fmt = $2; next; } if($fmt =~ /^[pP]([+-]?[0-9]+)\s*(.*)/){ my $ps = $1; $fmt = $2; $ps += $F->{'ps'} if $ps =~ /^[+-]/; $F->{'ps'} = $ps; next; } if($fmt =~ /^[vV]([+-]?[0-9]+)\s*(.*)/){ my $vs = $1; $fmt = $2; $vs += $F->{'vs'} if $vs =~ /^[+-]/; $F->{'vs'} = $vs; next; } if($fmt =~ /^[wW]\(([^()]*)\)\s*(.*)/){ $F->{'width'} = $1; $fmt = $2; next; } if($fmt =~ /^~\s*(.*)/){ $F->{'lines'} = 1; $fmt = $1; next; } last; } next; } print STDERR "Unknown fmt: $fmt\n"; last; } my $F = lfmt(); $F->{'vert'} = $vert; push @a, $F; $maxcol = max($maxcol, $ncol); return \@a; } sub loadfont($){ my ($fontname) = @_; $fontname = $fontalias{$fontname} || $fontname; if(defined($fontcache{$fontname})){ return $fontcache{$fontname}; } if(!$fontfile{$fontname}){ print STDERR "No font file for $fontname; using Times-Roman.\n"; $fontname = "Times-Roman"; } my $file = `locate $fontfile{$fontname} | sed 1q`; chomp $file; if($file =~ /^$/){ print STDERR "Cannot find $fontfile{$fontname}.\n"; exit 1; } $fontcache{$fontname} = new PostScript::FontMetrics($file); return $fontcache{$fontname}; } sub textwidth($$$) { my ($text, $fontname, $size) = @_; my $font = loadfont($fontname); my $width = 0; my @pieces = split(/(\\s[+-]?[0-9]|\\f.)/, $text); foreach my $p (@pieces){ if($p =~ /^\\s([0-9])/){ $size = $1; }elsif($p =~ /^\\s([+-][0-9])/){ $size += $1; }elsif($p =~ /^\\f(.)/){ $font = loadfont($1); }else{ $p =~ s/\\-/\261/g; $p =~ s/\\\(mi/\261/g; $p =~ s/\\e/\\/g; $width += $font->stringwidth($p) * $size / 1000; } } return $width; } sub pstext($$$) { my ($text, $fontname, $size) = @_; my $font = loadfont($fontname); my $width = 0; my @pieces = split(/(\\s[+-]?[0-9]|\\f.)/, $text); foreach my $p (@pieces){ if($p =~ /^\\s([0-9])/){ $size = $1; }elsif($p =~ /^\\s([+-][0-9])/){ $size += $1; }elsif($p =~ /^\\f(.)/){ $font = loadfont($1); }else{ $p =~ s/\\-/\261/g; $p =~ s/\\\(mi/\261/g; $p =~ s/\\e/\\/g; switchfont($font, $size); my $clean = $p; $clean =~ s/([()\\])/\\$1/g; $ps .= "($clean) show\n"; } } } sub format1($){ my ($cell) = @_; my $fmt = $cell->{'fmt'}; my $text = $cell->{'text'}; my $font = $fmt->{'font'}; my $fontsize = $fmt->{'ps'}; my $height = $cell->{'height'}; my ($left, $right, $center) = (0, 0, 0); my $align = $fmt->{'align'}; my $alignwidth = 0; if($fmt->{'align'} eq "_"){ $cell->{'text'} = $text = "_"; } if($text eq "_" || $text eq "\\_"){ $height = 1; goto OUT; } if($text eq "="){ $height = 2; goto OUT; } if($text eq ""){ goto OUT; } $height = $VS; if($align eq "n"){ # numeric if($text =~ /^(.*)\\\&(.*)/ || $text =~ /^(.*)(((?<=[0-9])\.|\.(?=[0-9])).*)/ || $text =~ /(.*[0-9])(.*)/){ my ($l, $r) = ($1, $2); $text = $l . $r; $left = textwidth($l, $font, $fontsize); $right = textwidth($r, $font, $fontsize); }else{ $center = textwidth($text, $font, $fontsize); } }elsif($align eq "a" || $align eq "l"){ $right = textwidth($text, $font, $fontsize); if($align eq "a"){ $alignwidth = $right; } }elsif($align eq "r"){ $left = textwidth($text, $font, $fontsize); }elsif($align eq "c"){ $center = textwidth($text, $font, $fontsize); }elsif($align eq "z"){ $left = $right = $center = 0; }elsif($align eq "s" || $align eq "dead" || $align eq "^"){ # }else{ print STDERR "Unknown alignment in format1: $align\n"; } OUT: $cell->{'text'} = $text; $cell->{'left'} = $left; $cell->{'right'} = $right; $cell->{'center'} = $center; $cell->{'need'} = max($left+$right, $center); $cell->{'sep'} = $fmt->{'sep'}; $cell->{'height'} = $height; $cell->{'alignwidth'} = $alignwidth; } sub mkgrid() { my @grid = (); for(my $r=0; $r<@$table; $r++){ my @row = (); for(my $c=0; $c<=$maxcol; $c++){ push @row, ""; } push @grid, \@row; } if($options{"box"} eq "box" || $options{"box"} eq "allbox"){ for(my $r=0; $r<@grid; $r++){ $grid[$r]->[0] = $grid[$r]->[$maxcol] = "|"; } }elsif($options{"box"} eq "doublebox"){ for(my $r=0; $r<@$table; $r++){ $grid[$r]->[0] = $grid[$r]->[$maxcol] = "||"; } } return \@grid; } sub changey1($$) { my ($r, $y1) = @_; return if $r < 0; my $row = $table->[$r]; for(my $c=0; $c<@$row; $c++){ my $cell = $row->[$c]; $cell->{'y1'} = $y1; } } sub isline($$){ my ($r, $c) = @_; return 0 if $c < 0; return $table->[$r]->[$c]->{'height'} == 1; } sub rowmiddle($){ my ($r) = @_; return ($table->[$r]->[0]->{'y0'} + $table->[$r]->[0]->{'y1'}) / 2; } sub dotable() { # Step 1. Calculate cell, individual column widths. # also make grid my (@left, @right, @center, @align, @width, @sep, @extra, @equal, @rowheight); my $grid = mkgrid(); for(my $r=0; $r<@$table; $r++){ $rowheight[$r] = 0; my $row = $table->[$r]; my $gridrow = $grid->[$r]; for(my $c=0; $c<@$row; $c++){ my $cell = $row->[$c]; my $fmt = $cell->{'fmt'}; format1($cell); if($cell->{'height'} >= $rowheight[$r]){ $rowheight[$r] = $cell->{'height'}; } if($c >= @left){ $left[$c] = $sep[$c] = $right[$c] = $center[$c] = $extra[$c] = $width[$c] = $equal[$c] = 0; } $equal[$c] += $fmt->{'equal'}; $sep[$c] = max($sep[$c], $cell->{'sep'}); if($fmt->{'vert'}){ $grid->[$r]->[$c] = $fmt->{'vert'}; }elsif($options{"box"} eq "allbox" && $c+1 < @$row && $fmt->{'align'} ne "s"){ if($c+1 < @$row && $row->[$c+1]->{'fmt'}->{'align'} ne "s"){ $grid->[$r]->[$c] = "|"; } } $grid->[$r]->[$c] = $fmt->{'vert'} if $fmt->{'vert'}; next if $cell->{'span'} || $fmt->{'align'} eq "dead"; $left[$c] = max($left[$c], $cell->{'left'}); $right[$c] = max($right[$c], $cell->{'right'}); $center[$c] = max($center[$c], $cell->{'center'}); $width[$c] = max($left[$c]+$right[$c], $center[$c]); $align[$c] = max($align[$c], $cell->{'alignwidth'}); } if($rowheight[$r] == 0){ $rowheight[$r] = $VS; # blank line } } # Step 2. Assign spanned widths. for(my $r=0; $r<@$table; $r++){ my $row = $table->[$r]; for(my $c=0; $c<@$row; $c++){ my $cell = $row->[$c]; my $span = $cell->{'span'}; next if !$span || $span->[0] != $cell; my $w = -$sep[$c]; foreach my $cellx (@$span){ my $cx = $cellx->{'col'}; $w += $sep[$cx] + $width[$cx]; } next if $cell->{'need'} < $w; my $bump = ($cell->{'need'} - $w) / @$span; foreach my $cellx (@$span){ $width[$cellx->{'col'}] += $bump; } } } # Step 3. Equalize the equalizers. my $w = 0; for(my $c=0; $c<@equal; $c++){ next if !$equal[$c]; $w = max($w, $width[$c]); } for(my $c=0; $c<@equal; $c++){ next if !$equal[$c]; $width[$c] = $w; } # Step 3. Assign column starting offsets. my @start = (); my $x = -$sep[0]/2; for (my $i=0; $i<@width; $i++){ $x += $sep[$i]; $start[$i] = $x; $x += $width[$i]; } $x -= $sep[$#width]/2; if($width == 0){ $width = $x; } $xpad = ($width - $x) / 2; # print STDERR "Table too wide: $x pt >= $width pt\n" if $xpad < 0; # print STDERR "Column widths: " . join(' ', @width) . "\n"; # print STDERR "Starts: " . join(' ', @start) . "\n"; # Step 4. Layout. # Invariant: y points at top of row. $y = 0; if($options{'box'} eq "box" || $options{'box'} eq "allbox"){ $hline->[0] = "_"; $hline->[@$table] = "_"; }elsif($options{'box'} eq "doublebox"){ $hline->[0] = "="; $hline->[@$table] = "="; } for(my $r=0; $r<@$table; $r++){ my $row = $table->[$r]; $y0 = $y; if($hline->[$r] eq "="){ doubleline($r); $y0 = $y; }elsif($hline->[$r] eq "_" || ($r>0 && $options{'box'} eq "allbox")){ singleline($r); $y0 = $y+1; } $y -= $rowheight[$r]; $y1 = $y; $y += ($VS - $PS)*2; my $sep0 = 3*$en; my $sep1 = 3*$en; for(my $c=0; $c<@$row; $c++){ my $cell = $row->[$c]; my $fmt = $cell->{'fmt'}; my $x = $start[$c]; my $width = $width[$c]; my $yadjust = 0; my $vspan = $cell->{'vspan'}; my $oldcell; $cell->{'y0'} = $y0; $cell->{'y1'} = $y1; if($vspan){ next if $vspan->[@$vspan-1] != $cell; my $oldcell = $cell; $cell = $vspan->[0]; $yadjust = $cell->{'y0'} - $oldcell->{'y0'}; if($fmt->{'valign'} ne "t"){ $yadjust /= 2; } } my $span = $cell->{'span'}; $sep0 = $sep1; $sep1 = $cell->{'sep'}; my $spanlength = 1; if($span){ next if $span->[0] != $cell; $spanlength = @$span; my $cell1 = $span->[$spanlength-1]; my $last = $cell1->{'col'}; $width = $start[$last] + $width[$last] - $x; $sep1 = $cell1->{'sep'}; } if($cell->{'text'} eq "_"){ my $y = ($y0+$y1)/2; my $x0 = $x - $sep0/2; my $x1 = $x + $width + $sep1/2; if($grid->[$r]->[$c] eq "||"){ $x0 -= 1; } if($grid->[$r]->[$c+$spanlength] eq "||"){ $x1 += 1; } $ps .= "$x0 $y moveto $x1 $y lineto stroke\n"; next; } next if $cell->{'text'} eq ""; my $lr = $left[$c] + $right[$c]; my $lt = $left[$c]; my $center = $cell->{'center'}; my $left = $cell->{'left'}; my $right = $cell->{'right'}; if($fmt->{'align'} eq "l" || $fmt->{'align'} eq "z"){ # nothing to do }elsif($fmt->{'align'} eq "a"){ print STDERR "ALIGN: width=$width a=$align[$c]\n"; $x += ($width-$align[$c])/2; }elsif($fmt->{'align'} eq "r"){ $x += $width - $left; # [sic] }elsif($center > 0){ $x += ($width-$center)/2; }else{ $x += ($width-$lr)/2+$lt-$left; } showtext($x, $yadjust, $cell); if($fmt->{'lines'}){ linesaround($start[$c], $x, $x + $cell->{'need'}, $start[$c] + $width, $yadjust); } } $y = $y1; } if($options{'box'} eq "box" || $options{'box'} eq "allbox"){ singleline(scalar(@$table)); }elsif($options{'box'} eq "doublebox"){ doubleline(scalar(@$table)); } # Step 5. Draw vertical lines. for(my $c=0; $c<=$maxcol; $c++){ for(my $r=0; $r<@$table; $r++){ my $vert = $grid->[$r]->[$c]; next if !$vert; # print STDERR "$r $c $vert\n"; my $r0 = $r; while($r+1<@$table && $grid->[$r+1]->[$c] eq $vert){ $r++; } my $r1 = $r; my $x = $start[$c] - $sep[$c]/2; my $y0 = $table->[$r0]->[$c]->{'y0'}; my $y1 = $table->[$r1]->[$c]->{'y1'}; if($r0 > 0 && (isline($r0-1, $c) || isline($r0-1, $c-1))){ $y0 = rowmiddle($r0-1); # $y0 += 0.5 * $linewidth; }elsif(isline($r0, $c) || isline($r0, $c-1)){ $y0 = rowmiddle($r0); # $y0 += 0.5 * $linewidth; } if($r1+1 < @$table && (isline($r1+1, $c) || isline($r1+1, $c-1))){ $y1 = rowmiddle($r1+1); # $y1 -= 0.5 * $linewidth; }elsif(isline($r1, $c) || isline($r1, $c-1)){ $y1 = rowmiddle($r1); # $y1 -= 0.5 * $linewidth; } if($vert eq "|"){ $ps .= "$x $y0 moveto $x $y1 lineto stroke\n"; }else{ $x--; $ps .= "$x $y0 moveto $x $y1 lineto stroke\n"; $x += 2; $ps .= "$x $y0 moveto $x $y1 lineto stroke\n"; } } } } sub switchfont($$) { my ($font, $size) = @_; my $name = $font->FontName(); if($name ne $currentfont || $size != $currentsize){ $ps .= "/$name findfont $size scalefont setfont\n"; $currentfont = $name; $currentsize = $size; } } sub showtext($$$){ my ($x, $yadjust, $cell) = @_; my $yy = $y + $yadjust; $ps .= "$x $yy moveto "; pstext($cell->{'text'}, $cell->{'fmt'}->{'font'}, $cell->{'fmt'}->{'ps'}); } sub linesaround($$$$$){ my ($x0, $x1, $x2, $x3, $yadjust) = @_; $x1 -= 2; $x2 += 2; my $yy = $y + $yadjust + $PS/3; if($x1 > $x0){ $ps .= "$x1 $yy moveto $x0 $yy lineto stroke\n"; } if($x3 > $x2){ $ps .= "$x3 $yy moveto $x2 $yy lineto stroke\n"; } } sub newline(){ $y -= $VS; } sub doubleline($) { my ($r) = @_; $ps .= "0 $y moveto $width 0 rlineto stroke\n"; $y -= 2; $ps .= "0 $y moveto $width 0 rlineto stroke\n"; } sub singleline() { my ($r) = @_; $y--; changey1($r-1, $y) if $r > 0; $ps .= "0 $y moveto $width 0 rlineto stroke\n"; $y--; } readinput(); if(@$table == 0){ print STDERR "no table in input.\n"; exit 1; } print "%!PS-Adobe-2.0-EPSF\n"; $ht = -$y; $w = 36 + $width + 1; $h = 36 + $ht + 1; print "%%BoundingBox: 35 35 $w $h\n"; print "36 36 translate\n"; print "2 setlinecap\n"; print "$xpad $ht translate\n"; print "$linewidth setlinewidth\n"; print $ps; print "showpage\n";