#!/usr/bin/perl -w
# pdftops.pl - wrapper script for xpdf's pdftops utility to act as a CUPS filter
# ==============================================================================
# 1.00 - 2004-10-05/Bl
#	Initial implementation
# 1.10 - 2006-09-27/Bl
#	Alternatively, use Adobe Reader in place of Xpdf's pdftops
# 1.20 - 2007-12-03/Bl
#	Safe temp file creation (fix gentoo bug # 201042)
#
# Copyright: Helge Blischke / SRZ Berlin 2004-2006
# This program is free seoftware and governed by the GNU Public License Version 2.
#
# Description:
# ------------
#	This program wraps the pdftops utility from the xpdf 3.00 (and higher) suite
#	to behave as a CUPS filter as a replacement for the original pdftops filter.
#	As an alternative the Adobe Reader may be used.
#
#	The main purpose of this approach is to keep the properties of a PDF to be
#	printed as undesturbed as possible, especially with respect to page size,
#	scaling, and positioning.
#
#	The pdftops utility reads a configuration file 'pdftops.conf' or 'acroread.conf',
#	respectively, in the CUPS_SERVERROOT directory, which must exist but may be empty. 
#	The sample configuration file accompanying this program sets the defaults which
#	seem plausible to me with respect to high end production printers.
#
#	To give the user highest possible flexibility, this program accepts and
#	evaluates a set of job attributes special to this filter, which are 
#	described below:
#	
#		pdf-pages=<f>,<l>
#				expands to the -f and -l options of pdftops
#				or the -start and -end options of acroread, respectively
#				to select a page range to process. This is independent
#				of the page-ranges attribute and may significantly
#				increase throughput when printing page ranges.
#				Either of these numbers may be omitted.
#
#		pdf-paper=<name>
#				For pdftops, <name> may be one of "letter", "legal",
#				"A4", "A3", or "match"; for acroread, the permetted values
#				are "letter", "legal", "tabloid", "ledger", "executive",
#				"a3", "a4", "a5", "b4", "b5", respectively (without the
#				quotes; the names are treated case independent).
#				In case of acroread, no paper specification is equivalent
#				to pdsftops's "match".
#		pdf-paper=<width>x<height>
#				<name> may be one of letter, legal , A4, A3, or match;
#				<width> and <height> are the paper width and height
#				in printers points (1/72 inch). This expands to
#				either the -paper or the -paperh and -paperw options
#				of pdftops or the -size option of acroread.
#
#		pdf-opw=<password>
#		pdf-upw=<password>
#				expand to the -opw and -upw options of pdftops,
#				respectively and permit printing of password
#				protected PDFs.
#
#		pdf-<option>	where <option> is one of
#				level1, level1sep, level2, level2sep, level3, level3sep,
#				opi, nocrop, expand, noshrink, nocenter.
#				See the pdftops manpage for a detailed description of
#				the respective options.
#				In case of acroread, the options level1, level?sep, opi,
#				nocrop, noshrink, and nocenter are silently ignored.
#
#	All other pdftops commandline options are refused.
#
#	The return code of the pdftops utility or acroread, if nonzero, is used as the exit code
#	of this program; error messages of the pdftops utility are only visible
#	if 'debug' is specified as LogLevel in cupsd.conf.
#
#	NOTE:
#	-----
#	This wrapper script has been initially designed to use the original pdftops utility
#	as a CUPS filter and now extended to alternately use acroread.
#	But there are situations where you need to be able to select either variant, thus it
#	is possible to configure the wrapper to both programs but select one of them by default;
#	the other one then may be selected by command line option:
#
#	use-pdftops		selects pdftops
#	use-acroread		selects acroread
#
#	if both are configured (by defining the appropriate configuration file).
#

#
# Site specific parameters - modify as needed
# ----------------------------------------------------------------------------------
$pdftops_path = "/usr/bin/pdftops";		# path to the xpdf utility
$acroread_path = "/opt/bin/acroread";		# path to Adobe Reader
$default_app = 'use-pdftops';			# the default if both are configured
$use_pdftops = 1;				# default on gentoo and highly recommended
$use_acroread = 0;				# not supported/working on gentoo yet, use at your own risk
# ----------------------------------------------------------------------------------

use File::Temp qw( tempfile );

#
# Check which app to use - pdftops or acroread
#
$rootdir = $ENV{CUPS_SERVERROOT} || die ("ERROR: CUPS server root directory undefined\n");
$use_both = $use_pdftops && $use_acroread;

#
# Check the arguments
#
die ("ERROR: wrong number of arguments\n") if (scalar @ARGV < 5);

$jobid = $username = $title = $copies = undef;
$jobid = shift;					# Job ID
$username = shift;				# Job requesting user name
$title = shift;					# Job title
$copies = shift;				# Number of requested copies
$options = shift;				# Textual representation of job attributes
$pdffile = shift;				# Pathname of PDF file to process

if (defined $use_both && $use_both)
{
	my $optstr = " $options ";
	my $to_use = '';
	if ($optstr =~ /\s+(use-acroread|use-pdftops)\s+/)
	{
		$to_use = $1;
	}
	else
	{
		$to_use = $default_app;
	}
	if ($to_use eq 'use-acroread')
	{
		undef $use_pdftops;
	}
	elsif ($to_use eq 'use-pdftops')
	{
		undef $use_acroread;
	}
	else
	{
		die ("ERROR: cannot use both pdftops and acroread simultaneously\n");
	}
}


if (defined $use_pdftops)
{
	# If we are reading from STDIN, we must copy the input to a temporary file
	# as the PDF consumer needs a seekable input.
	if (! defined $pdffile)
	{
		my $template = "pdfinXXXXXX";
		my $tmpdir = $ENV{TMPDIR};
		my ($bytes, $buffer);
		my ($tmpfh, $tmpfile) = tempfile ($template, OPEN => 1, DIR => $tmpdir, UNLINK => 0, SUFFIX => '.tmp');
		while (($bytes = read (STDIN, $buffer, 1024)) > 0)
		{
			print $tmpfh "$buffer";
		}
		if ($bytes < 0)
		{
			close ($tmpfh);
			unlink $tmpfile;
			die ("ERROR: pdftops wrapper: $tmpfile: $!\n");
		}
		close ($tmpfh);
		$pdffile = $tmpfile;
		$delete_input = 1;			# for deleting the temp file after converting
	}
}
		

# 
# Check the options string for options to modify the bahaviour of the pdftops utility:
#
@optarr = split (/\s+/, $options);
if (defined $use_pdftops)
{
	$cmdopt = ""; # do not pass the -cfg argument to the poppler pdftops util
	# The following are the (parameterless) command line options that may be used to change the 
	# defaults defiend by pdftops.conf
	$simple = 'level1|level1sep|level2|level2sep|level3|level3sep|opi|nocrop|expand|noshrink|nocenter';
	%papernames = (
		'letter'	=>	'-paper letter',
		'tabloid'	=>	'-paperw 792 -paperh 1224',
		'ledger'	=>	'-paperw 1224 -paperh 792',
		'legal'		=>	'-paper legal',
		'executive'	=>	'-paperw 756 -paperh 522',
		'a3'		=>	'-paper A3',
		'a4'		=>	'-paper A4',
		'a5'		=>	'-paperw 421 -paperh 595',
		'b4'		=>	'-paperw 709 -paperh 1002',
		'b5'		=>	'-paperw 501 -paperh 709',
		'match'		=>	'-paper match'
	);
}
else
{
	open (CFG, "<$rootdir/acroread.conf") || die ("ERROR: acroread.conf: $!\n");
	$cmdopt = '-toPostScript';
	while (<CFG>)
	{
		chomp;
		next if (/^\s*#/);			# skip comment lines
		next if (/^\s*$/);			# skip blank lines
		s/^-\s*//;				# discard leading '-' and white space, as it will be generated later
		s/\s+$//;				# discard trailing white space
		$cmdopt .= " -$_";
	}
	close (CFG);
	$simple = 'level1|level1sep|level2|level2sep|level3|level3sep|opi|nocrop|expand|noshrink|nocenter';
	%papernames = (
		'letter'	=>	'-size letter',
		'tabloid'	=>	'-size tabloid',
		'ledger'	=>	'-size ledger',
		'legal'		=>	'-size legal',
		'executive'	=>	'-size executive',
		'a3'		=>	'-size a3',
		'a4'		=>	'-size a4',
		'a5'		=>	'-size a5',
		'b4'		=>	'-size b4',
		'b5'		=>	'-size b5',
		'match'		=>	''		# this is the default with acroread
	);
}

foreach my $option (@optarr)
{
	if ($option =~ /^pdf-(.+)$/)
	{	# We assume this is an option to evaluate
		my $optkey = $1;		# possible pdftops option
		if ($optkey =~ /^pages=(\d*),(\d*)$/)
		{
			# We do this hack here to avoid clashes with the page-ranges atrribute
			# which is handled by the pstops filter. And we allow one of the numbers
			# to be omitted.
			my $first = $1;
			my $lastp = $2;
			if (defined $use_pdftops)
			{
				$cmdopt .= " -f $1" if ($1);		# first page
				$cmdopt .= " -l $2" if ($2);		# last page
			}
			else
			{
				$cmdopt .= " -start $1" if ($1);	# first page
				$cmdopt .= " -end $2" if ($2);		# last page
			}
		}
		elsif ($optkey =~ /^paper=(letter|tabloid|ledger|legal|[Aa]3|[Aa]4|[Aa]5|[Bb]4|[Bb]5|match)$/)
		{
			# evaluate paper name
			my $paper = $1;
			$paper =~ tr/A-Z/a-z/;
			my $value = $papernames{$paper};
			$cmdopt .= " $value" if ($value);
		}
		elsif ($optkey =~ /^paper=(\d+)x(\d+)$/)
		{
			# evaluate paper dimensions
			if (defined $use_pdftops)
			{
				$cmdopt .= " -paperw $1 -paperh $2";
			}
			else
			{
				$cmdopt .= " -size $1" . 'x' . "$2";
			}
		}
		elsif ($optkey =~ /^(o|u)pw=(\S+)$/)
		{
			$cmdopt .= " $1" . 'pw ' . $2 if (defined $use_pdftops);	# owner/user password
		}
		elsif ($optkey =~ /^($simple)$/)
		{
			my $thisopt = $1;
			if (defined $use_pdftops)
			{
				$cmdopt .= ' -' . $1;				# allowed simple options
			}
			else
			{
				$thisopt =~ s/sep$//;				# ignore the ...sep suffix
				if ($thisopt =~ /level1|opi|nocrop|nocenter/)
				{
					$thisopt = '';
				}
				elsif ($thisopt eq 'noshrink')
				{
					$thisopt = '';
					$cmdopt =~ s/ -shrink//;
				}
				$cmdopt .= " -" . $thisopt if ($thisopt);
			}
		}
		else
		{
			warn ("ERROR: pdftops wrapper: illegal attribute \"pdf-$optkey\"\n");
		}
	}
	# All other attributes are processed elsewhere
}
#
# Complete the command
#
if (defined $use_pdftops)
{
	warn ("ERROR: pdftops-options: $cmdopt\n");
}
else
{
	warn ("ERROR: acroread-options: $cmdopt\n");
}
if (defined $use_pdftops)
{
	$rc = system ("$pdftops_path $cmdopt $pdffile -");
}
else
{
	if (defined $pdffile && $pdffile)
	{
		$rc = system ("$acroread_path $cmdopt < $pdffile")
	}
	else
	{
		$rc = system ("$acroread_path $cmdopt");
	}
}
if ($rc)
{
	$ir = $rc & 127;
	$rc >>= 8;
	my $temp = (defined $use_pdftops) ? $pdftops_path : $acroread_path;
	warn ("ERROR: $temp exited with ", ($ir) ? "signal $ir, " : " exit code $rc", "\n");
	exit $rc;
}
unlink ($pdffile) if (defined $delete_input);		# Delete the temp file if any
exit 0;
