#! /usr/bin/perl -w
# Created	: Mon 03 Oct 2005 11:22:20 PM CDT
# Modified	: Wed 05 Mar 2008 12:41:09 PM PST
# Author	: Gautam Iyer <gautam@math.uchicago.edu>

use strict;
use Getopt::Long qw(:config no_ignore_case bundling);
use Term::ANSIColor qw(:constants);

sub print_help;
sub print_usage;
sub get_dims;
sub min;
sub max;
sub toPsPoints;
sub getMargins;

# Terminal colors
my ($BD,$UL,$IT,$ER,$RE)=(RESET.CYAN,RESET.GREEN,RESET.YELLOW,RESET.RED,RESET);

my ($swidth, $sheight) = get_dims( "letter");	# Default to US letter
my ($dwidth, $dheight);
my $format = "2b";

# Default tex margins (for letter paper) are
# left/right:1.77in, top: ~1.65 (to title), ~1.35 to writing, bottom: ~1.3
my ($topmargin, $botmargin) = ("2.75cm", "2.25cm");
my $symmargin;
my $print_help	= 0;
my $dryrun	= 0;
my $srcpaper;
my $dstpaper	= "letter";
my $bbox	= undef;
my $bbox_pad	= "1cm";
my $valign	= "center";
my $get_bbox	= undef;

my $cmd = "";
my ($infile, $outfile);

my ( $xoffset1, $xoffset2, $xoffset3, $xoffset4);
my ( $yoffset1, $yoffset2, $yoffset3, $yoffset4);
my $factor;

GetOptions(
    "src-paper|p=s"	=> \$srcpaper,
    "src-height|h=s"	=> \$sheight,	# Paper height
    "src-width|w=s"	=> \$swidth,	# Paper width
    "dst-paper|P=s"	=> \$dstpaper,
    # "dst-height|H=s"	=> \$dheight,	# Paper height
    # "dst-width|W=s"	=> \$dwidth,	# Paper width
    "format|f=s"	=> \$format,	# "2", "2d", "2b", "4", "4b"
    "top-margin|t=s"	=> \$topmargin,	# Margin you want to crop from
    "bottom-margin|b=s"	=> \$botmargin,	# Bottom margin (if different)
    "margin|m=s"	=> \$symmargin, # Symmetric margins
    "bbox|B=s"		=> \$bbox,	# Bounding box
    "bbox-pad|d=s"	=> \$bbox_pad,	# Amt to enlarge the bbox by
    "bbox-valign|v=s"	=> \$valign,	# Vertical alignment (t, c, b)
    "get-bbox-from|g=s"	=> \$get_bbox,	# Pages to get the bbox from.
    "dryrun|N"		=> \$dryrun,
    "help"		=> \$print_help
) or print_usage();

if( $format !~ m/^(?:2[bd]?|4(bc?)?)$/)
{
    die "${ER}ERROR:$RE Unrecognized format $format. Use $UL--help$RE for help.\n";
}

if( $print_help )
{
    print_usage();
}

# Get input and output files
for( my $i=0; $i <= $#ARGV; $i++)
{
    my $arg = $ARGV[$i];

    if( !defined( $arg) || $arg =~ m/^-./)
    {
	next;
    };

    # On getting an input file, we either store it in $infile, or we set $cmd
    # to convert it to a PS file.
    if( !defined( $infile) )		#arg is the input file
    {
	#die "${ER}ERROR:$RE Unable to read $i\n" if( ! -r $i );

	# Convert input files to ps
	if( $arg =~ m/\.dvi$/i )
	{
	    $cmd = "dvips -t$dstpaper -f $arg | ";
	    $infile = $arg; # We'll replace this with "-" later
	}
	elsif( $arg =~ m/\.pdf$/i )
	{
	    #$cmd = "pdf2ps -sPAPERSIZE=$dstpaper $arg - | ";
	    $cmd = "pdftops -paper $dstpaper $arg - | ";
	    $infile = $arg;
	}
	elsif( $arg =~ m/\.ps$|^-$/i )
	{
	    $infile = $arg;
	}
	else
	{
	    die "${ER}ERROR:$RE Input file $arg is not of a known type. Known types are DVI, PDF and PS.\n"
	}

	# Remove infile from ARGV
	splice( @ARGV, $i, 1);
	redo if( $i <= $#ARGV);
    }
    elsif( !defined( $outfile) )
    {
	$outfile = $arg;

	# Remove outfile from ARGV
	splice( @ARGV, $i, 1);
	redo if( $i <= $#ARGV);
    }
    else
    {
	die "${ER}ERROR:$RE Too many files. Use ${UL}--help$RE for help\n";
    }
}

if( !defined( $outfile) && defined( $infile) )
{
    $outfile = $infile;
    if( $format =~ m/b/)
    {
	$outfile =~ s/\.(?:dvi|pdf|ps)$/_book\.ps/i;
    } elsif( $infile =~ m/\.(?:dvi|pdf)$/i )
    {
	$outfile =~ s//\.ps/;
    } else
    {
	$outfile =~ s/\.ps$/_${format}up.ps/i;
    }
}

if( $cmd eq "" && defined( $infile))
{
    $cmd = "cat $infile | "
};
# $infile = "-" if( defined( $infile));

if( !defined( $outfile) || $outfile eq "-")
{
    $outfile = "";
}
else
{
    $outfile = " > $outfile";
}

# $cmd should now be set so that the input gets piped on the std input.

# Get source and destination paper dimensions
($swidth, $sheight) = get_dims( $srcpaper) if( defined( $srcpaper));
($dwidth, $dheight) = get_dims( $dstpaper);

# Convert all dimensions to ps points.
toPsPoints( $sheight, $swidth, $dheight, $dwidth, $topmargin, $botmargin,
	    $symmargin, $bbox_pad );

if( defined( $get_bbox ) )
{
    # Attempt to get the bounding box of the text using gs.
    my $gsoutput;
    my ($left, $bot, $right, $top);

    print( "Getting bounding box ... " );
    $gsoutput = `${cmd} psselect -q -p$get_bbox  | \
			gs -q -sDEVICE=bbox -dNOPAUSE -dBATCH - 2>&1`;
    
    while( $gsoutput =~ m/HiResBoundingBox:\s+(.*)$/g )
    {
	my ($lleft, $lbot, $lright, $ltop) = split( /\s/, $1 );
	if (
	      !defined( $lleft ) || !defined( $lbot )
	      || !defined( $lright ) || !defined( $ltop )
	   )
	{
	    # print( "${ER}Warning$RE: Badly formed bounding box '$1'\n");
	    next;
	}

	$left	= defined( $left  ) ? min( $lleft,  $left  ) : $lleft;
	$bot	= defined( $bot   ) ? min( $lbot,   $bot   ) : $lbot;
	$right	= defined( $right ) ? max( $lright, $right ) : $lright;
	$top	= defined( $top   ) ? max( $ltop,   $top   ) : $ltop;
    }

    if (
	  !defined( $left ) || !defined( $right )
	  || !defined( $top ) || !defined( $bot )
       )
    {
	die( "${ER}failed.${RE}\n" );
    }
    else
    {
	$bbox = "$left $bot $right $top";
	print( "${UL}OK$RE (bbox $bbox)\n" );
    }
}


# Compute dimensions of the cropped page
if( defined( $bbox ) )
{
    # Compute top and bottom margins using the bounding box
    # ($topmargin, $botmargin) = getMargins( $bbox, $bbox_pad, \$cmd,
    #     				    $sheight, $swidth,
    #     				    $dheight, $dwidth);
    ( $topmargin, $botmargin ) = getMargins();
    print( "Got topmargin=$topmargin and botmargin=$botmargin\n" );
}
elsif ( defined( $symmargin ) )
{
    $topmargin = $botmargin = $symmargin
};

# Compute offsets of each of the 2uped pages.
getOffsets( $sheight, $swidth, $dheight, $dwidth, $topmargin, $botmargin);

# Build the pstops command line
my $paperopt = ( defined( $dstpaper) ? "-p$dstpaper" : "");

if( $format eq "2" || $format =~ m/^4c?$/ )
{
    # Just 2up. No booklet junk required
    $cmd .= "pstops $paperopt \"2:0\@${factor}L(${xoffset1},${yoffset1})+1\@${factor}L(${xoffset2},${yoffset2})\"";
}
else
{
    $cmd .= "psbook | " if( $format =~ m/b/);
    $cmd .= "pstops $paperopt \"4:0\@${factor}L(${xoffset1},${yoffset1})+1\@${factor}L(${xoffset2},${yoffset2}),3\@${factor}R(${xoffset3},${yoffset3})+2\@${factor}R(${xoffset4},${yoffset4})\"";

    # Booklet printing. This doesnt' work too well. psbook works better.
    # $sizeopt = "\"4:-3\@${factor}L(${xoffset1},${yoffset1})+0\@${factor}L(${xoffset2},${yoffset2}),-2\@${factor}R(${xoffset3},${yoffset3})+1\@${factor}R(${xoffset4},${yoffset4})\"";
}

if( $format =~ m/^4/ )
{
    # 4up printing. Re-2up the ps file (with no margins)
    my ($scaled_height, $scaled_width) = ($dwidth, $dheight / 2);
    my $factor = min( $scaled_height / $dheight, $scaled_width / $dwidth);
    my ($lcoff, $rcoff) = (0, $dwidth);
    my $ymid = $dheight / 2;

    if( $format =~ m/^4$|c/ )
    {
	$lcoff = ($dwidth - $factor * $dheight) / 2;
	$rcoff = $dwidth - $lcoff;
    }

    if( $format =~ m/b/)
    {
	$cmd .= " | psbook | pstops $paperopt \"4:1\@${factor}R(${lcoff},${dheight})+0\@${factor}L(${rcoff},0cm),2L@.647(${rcoff},${ymid})+3R@.647(${lcoff},${ymid})\""
    }
    else
    {
	$cmd .= " | pstops $paperopt \"2:0\@${factor}R(${lcoff},${dheight})+1\@${factor}R(${lcoff},${ymid})\""
    }
}


# Call pstops and exit
if( $dryrun)
{
    print "${cmd} @ARGV $outfile\n";
}
else
{
    exec "${cmd} @ARGV $outfile";
};

# ---------------------------------------------------------------------------- #
#
#			       BEGIN SUBROUTINES
#
# ---------------------------------------------------------------------------- #

# {{{1 getMargins( $bbox ): Returns top and bottom margins given the bbox
sub getMargins
{
    # my ($bbox, $bbox_pad, $cmd, $sheight, $swidth, $dheight, $dwidth) = @_;
    my ($left, $bot, $right, $top) = split( /\s|,/, $bbox, 4 );
    my ($scaled_height, $scaled_width)   = ($dwidth, $dheight / 2);
    my ($tguess, $bguess);

    toPsPoints( $left, $bot, $right, $top );
    if (
	  !defined( $left ) || !defined( $bot )
	  || !defined( $right ) || !defined( $top )
       )
    {
	die "${ER}ERROR:$RE Badly formed bounding box $bbox\n"
    };

    # Center the margins if necessary
    if( $cmd ne "" && abs( $left - ($swidth - $right) ) > 5 )
    {
        $right = $swidth - $right;
        print( "Centering margins (left=$left, right=$right)\n" );
	if( !system( 'which pscenter > /dev/null' ) )
	{
	    $cmd .= 'pscenter'
	}
	elsif( !system( qw(which pscenter.pl) ) )
	{
	    $cmd .= 'pscenter.pl'
	}
	else
	{
	    die( "Could not find pscenter.pl" );
	}
        $cmd .= " -l $left -r $right | ";

	# Set left / right to be the centered margins.
        $left = ( $left + $right ) / 2;
        $right = $swidth - $left;
    }

    # Make right and bot the right and bot margins respectively.
    $right	= $swidth - $right;
    $top	= $sheight - $top;

    # Enlarge the bbox $bbox_pad
    for ( $left, $bot, $right, $top )
    {
	# $_ = max( $_ - $bbox_pad, 0 );
	$_ = $_ - $bbox_pad;
    };

    if (
	  ($sheight - $top - $bot) / ($swidth - $left - $right)
		> $scaled_height / $scaled_width
       )
    {
	# Source text is narrower than desired output size.
	return ( $top, $bot );
    }
    else
    {
	# Source text is wider. Correct the top and bottom margins.
	my $newheight = ( $swidth - $left - $right ) *
					$scaled_height / $scaled_width;
	# my $totalmargin = abs($top) + abs($bot);
	my $extraheight = $newheight - ($sheight - $top - $bot);

	if( $valign =~ m/^c(?:enter)?$/ )
	{
	    # Center output
	    for ($top, $bot)
	    {
	        # $_ = max( 0, $_ - $extraheight * $_ / $totalmargin );
	        # $_ = $_ - $extraheight * abs($_) / $totalmargin;
	        $_ -= $extraheight / 2;
	    };
	}
	elsif( $valign =~ m/^b(?:ot(?:tom)?)?$/ )
	{
	    # Flush bottom
	    $top -= $extraheight;
	}
	else
	{
	    if( $valign !~ m/^t(?:op)?$/ )
	    {
		print( "${ER}Warning:${RE} Unrecognized format for ",
		       "${BD}--bbox-valign${RE}.\n" );
	    }

	    # Flush top
	    $bot -= $extraheight;
	}

	return ( $top, $bot );
    }
}

# {{{1 toPsPoints( ... ): Convert dimensions to ps points.
sub toPsPoints()
{
    my $number = qr/[+-]?(?:\d+|\d*(?:\.\d+))/o;

    FOR: for (@_)
    {
	if( !defined( $_ ) )
	{
	    next FOR;
	};

	m/^(${number})in/ && do
	{
	    $_ = 72 * $1;
	    next FOR;
	};

	m/^(${number})cm$/ && do
	{
	    $_ = (72 / 2.54) * $1;
	    next FOR;
	};
    }
}

# {{{1 getOffsets( $sheight, $swidth, $dheight, $dwidth, $topmargin, $botmargin)
# Set $factor, and $xoffset1,2,3,4, $yoffset1,2,3,4
sub getOffsets {
    my ($sheight, $swidth, $dheight, $dwidth, $topmargin, $botmargin) = @_;

    # Dimensions after rotating and scaling the image
    my ($scaled_height, $scaled_width)   = ($dwidth, $dheight / 2);

    # $botmargin = $topmargin unless( defined( $botmargin));
    my $newheight = $sheight - $topmargin - $botmargin;
    my $newwidth  = ($newheight / $scaled_height) * $scaled_width;

    my $leftmargin = ($swidth - $newwidth)/2;
    print STDERR "${ER}Left margin negative (${leftmargin}).$RE\n"
	if( $leftmargin < 0);

    # Compute shrink factor
    $factor = $scaled_height / $newheight;

    # Compute page offsets
    $xoffset1 = $dwidth + $botmargin * $factor;
    $xoffset2 = $xoffset1;

    $yoffset1 = -$leftmargin * $factor;
    $yoffset2 = $dheight/2 + $yoffset1;

    $yoffset3 = ($newwidth + $leftmargin) * $factor;
    $yoffset4 = $dheight/2 + $yoffset3;

    $xoffset3 = -$botmargin*$factor;
    $xoffset4 = $xoffset3;
}


# min {{{1
sub min
{
    return $_[0] < $_[1] ? $_[0] : $_[1];
}

sub max
{
    return $_[0] > $_[1] ? $_[0] : $_[1];
}


# Getdims {{{1
sub get_dims {
    $_ = shift;

    if( $_ eq "letter") {
	return ( "8.5in", "11in" );
    } elsif( $_ eq "legal") {
	return ( "8.5in", "14in" );
    } elsif( $_ eq "tabloid") {
	return ( "11in", "17in" );
    } elsif( $_ eq "a3" ) {
	return ( "29.7cm", "42cm");
    } elsif( $_ eq "a4" ) {
	return (  "21cm", "29.7cm" );
    } elsif( $_ eq "a5" ) {
	return ( "14.8cm", "21cm");
    # } elsif( $_ eq "b4" ) {
    #     return ( 25.7, 36.4);
    } elsif( $_ eq "b5" ) {
	return ( "18.2cm", "25.7cm");
    } else {
	die "${ER}ERROR:$RE Unsupported paper type. Known types are ${UL}letter$RE, ${UL}legal$RE, ${UL}tabloid$RE, ${UL}a3$RE, ${UL}a4$RE, ${UL}a5$RE, ${UL}b5$RE\n";
    }
}

# Print usage{{{1
sub print_usage {
    print << "EOF";
Usage: ${BD}mkbooklet.pl$RE [${UL}options$RE] [${IT}infile$RE [${IT}outfile$RE]] [$UL-- pstops_options$RE]

Eco friendly printing :). Makes a 2up / 4up PS file with trimmed margins. Use
${UL}perldoc mkbooklet.pl$RE for complete help.

${BD}OPTIONS$RE
    $UL-m ${IT}margin$RE	Top and bottom margins to clip.
    $UL-t ${IT}margin$RE	Top margin to clip.
    $UL-b ${IT}margin$RE	Bottom margin to clip.
    $UL-B ${IT}bbox$RE	Bounding box of the text
    $UL-d ${IT}pad$RE	Amount to enlarge the bounding box by
    $UL-v ${IT}align$RE	Vertical alignment of output (${BD}top$RE, ${BD}botom$RE, ${BD}center$RE)
    $UL-g ${IT}pages$RE	Page list to get bounding box from.
    $UL-h ${IT}height$RE	Height of the source paper.
    $UL-w ${IT}width$RE	Width of the source paper.
    $UL-p ${IT}paper$RE	Source paper type.
    $UL-P ${IT}paper$RE	Destination paper type.
    $UL-f ${IT}format$RE	PS Output file format: ${BD}2$RE, ${BD}2d$RE, ${BD}2b$RE, ${BD}4$RE, ${BD}4b$RE, ${BD}4bc$RE
    $UL-N$RE		Dryrun. Just print the pstops command.
EOF
    exit 1;
}
# }}}1

# ---------------------------------------------------------------------------- #
#
#			    BEGIN POD DOCUMENTATION
#
# ---------------------------------------------------------------------------- #

# POD documentation {{{1

=head1 NAME

mkbooklet.pl - Make 2up/4up PS files from DVI/PDF/PS inputs.

=head1 SYNOPSIS

B<mkbooklet.pl> [I<options>] [F<infile> [F<outfile>]] [I<-- pstops_options>]

B<mkbooklet.pl> I<--help>

=head1 DESCRIPTION

B<mkbooklet.pl> takes a PS/PDF/DVI file, and 2up's (i.e. rotates and puts two pages on the same page). It can also 4up files, and re-order pages to make a 2up'd or 4up'd booklet. The default behavior is to make a 2uped booklet. It is similar to psnup(1), except it cuts out (user specified) margins making the end result have larger, more readable text.

This script is a front end for pstops(1), supplied with psutils. You can obtain psutils from http://www.tardis.ed.ac.uk/~ajcd/psutils.

=head1 OPTIONS

=over

=item I<-m> B<margin>, I<--margin> B<margin>

Specify the top and bottom margins to clip. The margins are in post script points but can be suffixed by "cm" or "in" to treat them as centimeters or inches respectively. Based on this the left and right margins are computed (keeping the aspect ratio fixed). The remaining visible area is then shrunk and rotated to produce your 2uped ps file. You can also specify the top and bottom margins differently using I<-t> and I<-b>. The default is to use asymetric margins.

=item I<-t> B<margin>, I<--top-margin> B<margin>

=item I<-b> B<margin>, I<--bottom-margin> B<margin>

Specify the top/bottom margins. Default 2.75cm (top) and 2.25cm (bottom). (If you use create a document using (AMS)LaTeX with default margins on letter paper, this is optimal.)

=item I<-B> B<bbox>, I<--bbox> B<bbox>

Specify bounding box of the text. Instead of guessing the top and bottom margins for clipping, you can specify the bounding box of the text. You can either find this out using

    gs -q -sDEVICE=bbox -dNOPAUSE -dBATCH

or by using the ruler in gv. If the bounding box indicates that the left and right text margins are not equal, then the document is centered using pscenter.pl.

The B<bbox> argument of the form I<left bot right top> as output by ghost script. The dimensions are is PS points, but you can suffix them with "in" or "cm" for inches / centimeters. Alternately you can also separate dimensions with a "," instead of a S<" ">.

=item I<-g> B<pages>, I<--get-bbox-from> B<pages>

Use ghostscript to get the bounding box of the text in B<pages> and use this as the value of the I<-B> option. This option does not work if B<mkbooklet.pl> is running as a filter, and is quite slow if the input file is a PDF.

The argument B<pages> is a page list in a format understood by psselect(1). Generally you should specify about two (consecutive) pages which take up the entire text area (i.e. don't specify pages with a large space on the top or something). Remember that the page numbers are counted sequentially from the first page, and have nothing to do with the page numbers in your original file.

=item I<-d> B<pad>, I<--bbox-pad> B<pad>

Amount to increase the bounding box by (default 1cm).

=item I<-v> B<valign>, I<--bbox-valign> B<valign>

Specify the vertical alignment of output. This option has no effect UNLESS you specified a bounding box (with either the I<-B> or the I<-g> options). B<valign> can be one of I<top>, I<bottom>, or I<center> (default).

=item I<-h> B<height>, I<--height> B<height>

=item I<-w> B<width>, I<--width> B<width>

Speficy source paper height or width.

=item I<-p> B<paper>, I<--src-paper> B<paper>

=item I<-P> B<paper>, I<--src-paper> B<paper>

Specify the paper type of the source / destination. Paper can be one of B<letter> (default), B<a3>, B<a4>, B<a5> or B<b5>.

=item I<-f> B<format>, I<--format> B<format>

Speficy the format. B<format> can be one of

=over

=item B<2>

2up pages. If you print on a duplex printer, you will have to flip pages on the long side (ugh) to read the back.

=item B<2d>

2up pages for duplex printing. After printing on a duplex printer, you'll have to flip pages on the short side to read the back (as it should be).

=item B<2b>

2up booklets. Print this on a duplex printer, fold in half (or cut down the middle) to get a little booklet. If you don't have a duplex printer, use psmandup(1). This is the default behaviour.

=item B<4>

4up pages.

=item B<4b>

4up booklets. First cut horizontaly in half. Invert the bottom half and place under the top. Cut off the margins on the right. Fold / cut in half, and you have a wallet sized booklet.

=item B<4bc>

Similar to B<4b>, but leaves a symmetric margin on left and right. This involves more work if you plan on cutting off the extra.

=back

=item I<-N>

Dryrun. Don't do anything, and only print the pstops command.

=item I<--help>

Print a brief summary of options.

=back

=head1 SEE ALSO

pstops(1), psbook(1), psnup(1)

=head1 AUTHOR

Gautam Iyer <gautam@math.uchicago.edu>

=cut
