#!/usr/bin/perl -w
# gstoraster	CUPS filter to convert PostScript or PDF to cups raster format
# ----------------------------------------------------------------------------
# Copyright 2010 Helge Blischke
# 1.00 - 2010-12-15/Bl
#	initial implementation
# 1.01 - 2011-05-14/Bl
#	Insert bounding box comment into probe PostScript program
# 1.02 - 2011-05-16/Bl
#	Fixed dumping PostScript strings in dump_obj
#	Fixed landscape handling bug
#	Added "-dNOMEDIAATTRS" to Ghostscript options
# 1.03 - 2011-05-20/Bl
#	Preserve pure CMYK black.
#	Ghostscript 9.xy and higher do all color handling through ICC profiles,
#	and the default CMKY profile used for /DeviceCMYK colorspace does not
#	preserve pure black (at least for 9.00 to 9.02), and this is dicussed
#	controversely among ghe Ghostscript developers.
#	We therefore transform pure CMYK black to an equivalent gray value
#	if the used PPD specifies 
#	*ColorDevice: False
# 1.04 - 2011-08-02/Bl
#	Sanitize command line options of type string if they contain spaces
#	for pstops | gs call.
#
#  This program is free software; you can redistribute it and/or modify it
#  under the terms of the GNU General Public License as published by the
#  Free Software Foundation; either version 2 of the License, or (at your
#  option) any later version.
#
# Description
# -----------
#	This filter is designed to replace both the pstoraster and the pdftoraster
#	filter. It operates in two steps:
#	(1) A probe PostScript program is fed through the standard pstops filter to 
#	    collect all the features stemming from the PPD defaults and job attributes
#	    referring to PPD choices. The output from the pstops filter is then processed
#	    by Ghostscript to set up command line options and a page device dictionary for
#	    the second step, in which the actual input file is processed.
#	(2) Ghostscript is then called with the command line options established by (1) above
#	    and processes 
#	    a)  a special initialization file which sets up the pagedevice parameters set up
#		by (1) above and redefines some procedures/operators (namely, setpagedevice
#		to handle landscape vs portrait oriented pages (especially if the input is PDF)
#		and graphicsbeginpage to cancel the page content clipping done by ghostscript's
#		PDF interpreter), and, as of version 1.03, redefines setcolorspace, setcolor,
#		and setcmykcolor render cmyk colors, the only nonzero component is black, as
#		the equivalent gray value. This is a workaround to the weird ICC profile used
#		by Ghostscript >= 9.00 for /DeviceCMYK color space, iff the used PPF specifies
#		 *ColorDevice: False,
#	    b)	the original input file.
#	    The output is written to stdout as usual.
#	This filter honours the *LandscapeOrientation: Plus90 | Minus90 | Any
#	PPD directive (Any interpreted as Plus90) and does the necessary transformation
#	by ordinary PostScript means, thus bypassing the special CTM handling by Ghostscript's
#	cups device (which is equivalent to Minus90).
#
# Restrictions:
# -------------
#	Currently, only the Landscape/Portrait orientation is handled, forcing the output to
#	the orientation specified by the PPD default page size. Any fit-to-page scaling is not
#	yet implemented.
#
use bytes;
use File::Copy;

# -------------------------- configuration parameters --------------------------
@paths = ('/bin', '/usr/bin', '/usr/local/bin');
$cmdlpgsz = 1;			# set to 1 in order to set page size by commandline option
$ignore_gserror = 0;		# set to 1 in order to ignore Ghostscript errors
$gsname = 'gs';			# the plain name of the Ghostscript executable
$log = 1;			# set to 0 to suppress NOTICE messages
# ------------------------------------------------------------------------------
$filterdir = $ENV{CUPS_SERVERBIN};
if (! defined $filterdir || ! $filterdir)
{
	chomp ($filterdir = `cups-config --serverbin`);
}
$filterdir .= "/filter";
$gs = lookup ($gsname);
$pstops = (-f "$filterdir/pstops" && -x "$filterdir/pstops") ? "$filterdir/pstops" : undef;
if (scalar @ARGV != 5 && scalar @ARGV != 6)
{
	warn "ERROR: gstoraster jobid username jobname copies options [filename]\n";
	exit 1;
}
if (! defined $gs)
{
	warn "ERROR: Ghostscript not found\n";
	exit 1;
}
if (! defined $pstops)
{
	warn "ERROR: cups filter \"pstops\" not found\n";
	exit 1;
}
$landscape = 'Plus90';			# the default
$myppd = $ENV{PPD};
if (! defined $myppd || ! $myppd)
{
	warn "ERROR: no PPD path provided\n";
	exit 1;
}
if (open (PPD, "$myppd"))
{
	my @ppd = <PPD>;
	close (PPD);
	chomp @ppd;
	($landscape) = grep (/^\*LandscapeOrientation:/, @ppd);
	$landscape = "" if (! defined $landscape);
	$landscape = ($landscape =~/^\*LandscapeOrientation:\s+(Plus90|Minus90)/) ? $1 : 'Plus90';
	# Check if the destination is a black/white device (this is a preliminary hack)
	$is_b_w = grep (/^\*ColorDevice:\s+False/, @ppd);
}
else
{
	warn "ERROR: $myppd: $!\n";
	exit 1;
}
$requestdir = $ENV{CUPS_REQUEST_ROOT};
$requestdir = $ENV{REQUEST_ROOT} if (! defined $requestdir || ! $requestdir);	# in case of CUPS 1.1.x
$tempdir = (defined $requestdir && $requestdir) ? "$requestdir/tmp" : "/var/tmp";


sub lookup
{
	my ($name) = @_;
	my $path = undef;
	foreach my $item (@paths)
	{
		my $temp = "$item/$name";
		if (-f $temp && -x $temp)
		{
			$path = $temp;
			last;
		}
	}
	$path;
}

sub sanitize
{
	my ($string) = @_;
	$string =~ s/ /\\ /g;
	$string;
}

#@args = @ARGV[0 .. 3];			# save argument for calline pstops filter (whithout filename)
$job_id = $username = $job_name = $num_copies = $filename = undef;	# prevent complaints
$job_id = shift;
$username = shift;
$job_name = shift;
$num_copies = shift;
$options = shift;
$filename = shift;		# optional file name
# Set up the sanitized command line options for calling the pstops filter by escaping
# space characters in string type options.
@args = ($job_id);
push @args, sanitize ($username);
push @args, sanitize ($job_name);
push @args, $num_copies;
# Force the options to be interpreted by the shell as only one parameter if not empty!
push @args, ($options ne '') ? "\"$options\"" : "number-up=1";	# options must not be empty (shell!)

# Feed the probe PostScript job through pstops to get the Ghostscript command line options
# and the page device statements for the final conversion

$psprobe = '%!PS-Adobe-3.0
%%Title: label printing configuration file
%%BoundingBox: (atend)
%%Pages: 0
%%EndComments
%%BeginProlog
% In this probe PostScript program, we redefine setpagedevice and currentpagedevice as local
% procedures used only to collect the parameters, as tests have shown that (at least) 
% espgs 815.02 does not honour the /PageSize key with the cups device.
/FAKEdict 16 dict def
/setpagedevice{FAKEdict copy userdict/FAKEdict 3 -1 roll put}bind def
/currentpagedevice{FAKEdict dup length dict copy}bind def

/resstr 0 string def		% the result string
/write_str			% <string> <nl_or_not> write_str -
{
	{(\n)concatstrings}if
	resstr exch concatstrings /resstr exch def
}bind def

/dump_obj			% <collected-str> <any> dump_obj <updated-str>
{
	dup type /arraytype eq
	{dump_array}
	{dup type /dicttype eq
		{dump_dict}
		{dup type /nametype eq
			{=string cvs (/)exch concatstrings}
			{dup type /stringtype ne
				{=string cvs}
				{(\()exch concatstrings(\))concatstrings}ifelse
			}ifelse
			1 index length 0 ne
			{exch ( )concatstrings exch}if
			concatstrings
		}ifelse
	}ifelse
}bind def

/dump_array
{
	exch ( [)concatstrings exch
	{dump_obj}forall
	( ])concatstrings
}bind def


/dump_dict
{
	exch (<<)concatstrings exch
	{3 1 roll dump_obj exch dump_obj}forall
	( >>)concatstrings
}bind def

/barf	% <value> <keyword> barf -
{
	1 index type /stringtype eq
	{1 index length 0 ne{true}{false}ifelse}
	{1 index dup type /integertype eq exch type /realtype eq or
		{1 index 0 ne{true}{false}ifelse}
		{1 index null ne{true}{false}ifelse}ifelse
	}ifelse
	{
		() exch dump_obj exch dump_obj true write_str
	}
	{pop pop}ifelse
}bind def

/belch	% <number> belch <string>
{
	dup type /realtype eq{0.5 add cvi}if 16 string cvs
}bind def
%%EndProlog
%%BeginSetup
% Evaluate command line options for Ghostscript from page device parameters
%currentpagedevice{exch ==only ( )print ==}forall flush
currentpagedevice
dup/MediaClass .knownget{/MediaClass barf}if
dup/MediaColor .knownget{/MediaColor barf}if
dup/MediaType  .knownget{/MediaType barf}if
dup/OutputType .knownget{/OutputType barf}if
dup/AdvanceDistance .knownget{/AdvanceDistance barf}if
dup/AdvanceMedia .knownget{/AdvanceMedia barf}if
dup/Collate .knownget{/Collate barf}if
dup/CutMedia .knownget{/CutMedia barf}if
dup/Duplex .knownget{/Duplex barf}if
dup/HWResolution .knownget
	{
		aload pop belch exch belch
		(-r)exch concatstrings(x)concatstrings exch concatstrings
		(\n)concatstrings print flush
	}if
dup/InsertSheet .knownget{/InsertSheet barf}if
dup/Jog .knownget{/Jog barf}if
dup/LeadingEdge .knownget{/LeadingEdge barf}if
dup/ManualFeed .knownget{/ManualFeed barf}if
dup/MediaPosition .knownget{/MediaPosition barf}if
dup/MediaWeight .knownget{/MediaWeight barf}if
dup/MirrorPrint .knownget{/MirrorPrint barf}if
dup/NegativePrint .knownget{/NegativePrint barf}if
dup/NumCopies .knownget{dup 1 ne{/NumCopies barf}{pop}ifelse}if
dup/Orientation .knownget{/Orientation barf}if
dup/OutputFaceUp .knownget{/OutputFaceUp barf}if
/PGSZ where not
{
	dup/PageSize .knownget{/PageSize barf}if	% set page size per setpagedevice
}
{
	pop						% pop the dict where PGSZ is defined
	dup/PageSize .knownget				% set page size per command line options
	{
		aload pop belch exch belch		% force rounded integer decimals
		(-dDEVICEWIDTHPOINTS=) exch concatstrings true write_str
		(-dDEVICEHEIGHTPOINTS=) exch concatstrings true write_str
	}if
}ifelse
dup/Separations .knownget{/Separations barf}if
dup/TraySwitch .knownget{/TraySwitch barf}if
dup/Tumble .knownget{/Tumble barf}if
dup/cupsMediaType .knownget{/cupsMediaType barf}if
dup/cupsBitsPerColor .knownget{/cupsBitsPerColor barf}if
dup/cupsColorOrder .knownget{/cupsColorOrder barf}if
dup/cupsColorSpace .knownget{/cupsColorSpace barf}if
dup/cupsCompression .knownget{/cupsCompression barf}if
dup/cupsRowCount .knownget{/cupsRowCount barf}if
dup/cupsRowFeed .knownget{/cupsRowFeed barf}if
dup/cupsRowStep .knownget{/cupsRowStep barf}if
dup/cupsBorderlessScalingFactor .knownget{/cupsBorderlessScalingFactor barf}if
(cupsInteger)
0 1 15{
	2 string cvs 1 index exch concatstrings
	2 index 1 index .knownget
	{(/) 3 -1 roll concatstrings ( )concatstrings cvn barf}
	{pop}ifelse
}for
pop (cupsReal)
0 1 15{
	2 string cvs 1 index exch concatstrings
	2 index 1 index .knownget
	{(/) 3 -1 roll concatstrings ( )concatstrings cvn barf}
	{pop}ifelse
}for
pop (cupsString)
0 1 15{
	2 string cvs 1 index exch concatstrings
	2 index 1 index .knownget
	{(/) 3 -1 roll concatstrings ( )concatstrings cvn barf}
	{pop}ifelse
}for
pop
dup/cupsMarkerType .knownget{/cupsMarkerType barf}if
dup/cupsRenderingIntent .knownget{/cupsRenderingIntent barf}if
dup/cupsPageSizeName .knownget{/cupsPageSizeName barf}if
pop
/resstr load print flush
%%EndSetup
%%EOF';

# We use -sDEVICE=cups to guarantee that Ghostscript does not refuse any of the page device
# parameters specified by the PPD. As the probe file contains no page(s), no output is written.
# In case $cmdlpgsz is defined and not null, force the page size to be passed as commandline
# options, not as setpagedevice statement.
$opts = (defined $cmdlpgsz && $cmdlpgsz) ? '-dPGSZ' : '';
$pstopscmd = "echo \"$psprobe\" | $pstops " . join (' ', @args) . " | $gs -q $opts -sDEVICE=cups -f -_ -c quit";
# warn "$pstopscmd\n";
@results = `$pstopscmd 2>&1`;
chomp @results;
$gssetpg = "";					# holds the serpagedevice parameters
@gsopts = (
	"$gs",
	"-q",
	"-dDELAYBIND",
	"-dNOPAUSE",
	"-dBATCH",
	"-dNOMEDIAATTRS",
	"-sPPD_LANDSCAPE=$landscape",		# initial value from PPD
	"-sDEVICE=cups",
	'-sstdout=%stderr',
	'-sOutputFile=%stdout',
	);
push @gsopts, "-dB_W_DEVICE" if (defined $is_b_w && $is_b_w);	# pass black/white device flag to Ghostscript
$linepref = 'DEBUG:';
foreach my $line (@results)
{
	warn "NOTICE:$line\n" if (defined $log && $log);
	$line =~ s/^\000//;
	if ($line =~ /^-/)			# Ghostscript option from pagedevice
	{
		push @gsopts, $line;
	}
	elsif ($line =~ /^\//)
	{
		$gssetpg .= ($gssetpg eq "") ? "<<$line" : " $line";
	}
	elsif ($line =~ /^(DEBUG:|NOTICE:|WARNING:|ERROR:|INFO:)/)
	{
		warn "$line\n";
	}
	else
	{
		$linepref = 'ERROR:' if ($line =~ /Error:/);	# Ghostscript error
		warn "$linepref$line\n";
	}
}
if ($gssetpg)
{
	my $optstr = ' ' . $options . ' ';
	if ($optstr =~ /\s+profile=(\S+)/)
	{
		my $profile = "/cupsProfile ($1)";
		warn "NOTICE:$profile\n" if (defined $log && $log);
		$gssetpg .= " $profile";
	}
	$gssetpg .= ">>setpagedevice\n";
	# warn "NOTICE:$gssetpg\n";
}

#foreach my $line (@gsopts){warn "$line\n";}
#
# Now set up what we need to process the input, especially in case of PDF
#
$gsprolog = "%%BeginFile: gs_prolog 1.0 0
%%Creator: Helge Blischke
%%CreationDate: 2010-12-07
% NOTE: Ghostscript needs to be called with the option -DELAYBIND
% Save the original pagedevice operators
/sys_setpagedevice /setpagedevice load def
/sys_currentpagedevice /currentpagedevice load def
currentglobal true setglobal
GS_PDF_ProcSet /sys_graphicsbeginpage GS_PDF_ProcSet/graphicsbeginpage get .forceput
setglobal
/B_W_DEVICE dup where{exch get}{pop false}ifelse
{
	/sys_setcolorspace /setcolorspace load def
	/sys_setcolor /setcolor load def
	/sys_setcmykcolor /setcmykcolor load def
	userdict/is_cmyk false put	% initialization
	/setcolorspace
	{
		sys_setcolorspace
		currentcolorspace 0 get /DeviceCMYK eq
		userdict/is_cmyk 3 -1 roll put
	}bind def
	/setcolor
	{
		userdict/is_cmyk get not
		{sys_setcolor}
		{
			4 copy pop	% c m y
			0 eq exch 0 eq and exch 0 eq and
			{1 exch sub setgray 3{pop}repeat}
			{/DeviceCMYK sys_setcolorspace sys_setcolor}ifelse
		}ifelse
	}bind def
	/setcmykcolor
	{
		4 copy pop      % c m y
		0 eq exch 0 eq and exch 0 eq and
		{1 exch sub setgray 3{pop}repeat}
		{sys_setcmykcolor}ifelse
	}bind def
}if
% Set up the final page device dictionary
$gssetpg
% Save page device properties we need to compute rotating and scaling
/sys_PageSize sys_currentpagedevice/PageSize get def
/Portrait? sys_PageSize aload pop le def
/Plus90 {90 rotate 0 //sys_PageSize 0 get neg translate} def
/Minus90 {90 neg rotate //sys_PageSize 1 get neg 0 translate} def
% Define our landscape orientation, depending on PPD_LANDSCAPE:
/Landscape /PPD_LANDSCAPE dup where{exch get}{pop (Plus90)}ifelse (Plus90) eq {//Plus90}{//Minus90}ifelse def
% Set up the fake operators
currentglobal true setglobal
GS_PDF_ProcSet
/graphicsbeginpage
{
	currentdict/ClipRect undef	% ClipRect stems from /CropBox, if present
	sys_graphicsbeginpage
}bind .forceput
setglobal
/setpagedevice
{
	% Currently, we do not implement any scale to fit operation as we suppose
	% the page size(s) specified by the input data only differ from the PPD default
	% by the orientation.
	dup/PageSize .knownget
	{
		aload pop le Portrait? xor
		{dup/BeginPage{pop //Landscape exec}put}if
		dup/PageSize undef				% remove /PageSize key - value pair
	}if
	dup/Orientation undef
%	dup/Install undef
	sys_setpagedevice					% do not ignore the rest
}bind def

% As we need to intercept the setpagedevice operator even when processing PDF, we call
% Ghostscript with the DELAYBIND option (because of setpagedevice called in pdfshowpage)
% and now can perform the bind safely.
.bindnow
%%EndFile
";

# As we must fed two different files (the above prolog and the original input) into Ghostscript,
# and we cannot do both from Ghostscript's stdin (the original input may be PDF), we save both 
# to a temporary file in CUPS' tmp directory.

$prolfile = "$tempdir/gsprolog.$$.ps";
if (open (TMP, ">$prolfile"))
{
	print TMP "$gsprolog";
	close (TMP);
}
else
{
	warn "ERROR: $prolfile: $!\n";
	exit 1;
}
if (! defined $filename || ! $filename)
{						# input is from stdin
	$tempfile = "$tempdir/gsinput.$$.tmp";
	copy (\*STDIN, $tempfile);
	if ($!)
	{
		warn "ERROR: $tempfile: $!\n";
		exit 1;
	}
	$gsinput = $tempfile;
}
else
{
	$gsinput = $filename;
}

#push @gsopts, "-c save pop";
push @gsopts, "-f";
push @gsopts, "$prolfile";
push @gsopts, "$gsinput";
push @gsopts, "-c quit";

# warn "NOTICE: ", join (' ', @gsopts), "\n";
system @gsopts;
if ($?)
{
	warn "ERROR: Ghostscript exited with value ", $? >> 8, "\n";
	exit 1 unless (defined $ignore_gserror && $ignore_gserror);
}
unlink $prolfile;
unlink $tempfile if ($tempfile);
