#!/usr/bin/perl
# The above Perl path may vary on your system; fix it!!! -*- perl -*-

use strict;
use POSIX;
use Cwd;

my $ripversion='$Revision: 3.43.2.12 $';
#'# Fix emacs syntax highlighting

# foomatic-rip is a spooler-independent filter script which takes
# PostScript as standard input and generates the printer's page
# description language (PDL)/raster format as standard output. This
# kind of filter is usually called Raster Image Processor (RIP),
# therefore the name "foomatic-rip".

# Save it in one of the directories of your $PATH, so that it gets
# found when called from the command line (for spooler-less printing),
# link it to spooler-specific directories when you use CUPS or PPR:

#    ln -s /usr/bin/foomatic-rip /usr/lib/cups/filter/
#    ln -s /usr/bin/foomatic-rip /usr/lib/ppr/lib/
#    ln -s /usr/bin/foomatic-rip /usr/lib/ppr/interfaces/

# Mark this filter world-readable and world-executable (note that most
# spoolers run the print filters as a special user, as "lp", not as
# "root" or as the user who sent the job).

# See http://www.linuxprinting.org/cups-doc.html
#     http://www.linuxprinting.org/lpd-doc.html
#     http://www.linuxprinting.org/ppr-doc.html
#     http://www.linuxprinting.org/pdq-doc.html
#     http://www.linuxprinting.org/direct-doc.html
#     http://www.linuxprinting.org/ppd-doc.html

# ==========================================================================
#
#    User-configurable settings, edit them if needed
#
# ==========================================================================

# What path to use for filter programs and such.  Your printer driver
# must be in the path, as must be the renderer, $enscriptcommand, and
# possibly other stuff.	 The default path is often fine on Linux, but
# may not be on other systems.
#
my $execpath = "/usr/local/bin:/usr/local/bin:/usr/bin:/bin";

# CUPS raster drivers are searched here
my $cupsfilterpath = "/usr/local/lib/cups/filter:/usr/local/lib/cups/filter:/usr/local/libexec/cups/filter:/opt/cups/filter:/usr/lib/cups/filter";

# Location of the configuration file "filter.conf", this file can be
# used to change the settings of foomatic-rip without editing
# foomatic-rip. itself. This variable must contain the full pathname 
# of the directory which contains the configuration file, usually
# "/etc/foomatic".
# Some versions of configure do not fully expand $sysconfdir
my $prefix = "/usr/local";
my $configpath = "${prefix}/etc/foomatic";

# For the stuff below, the settings in the configuration file have priority.

# Set to 1 to insert postscript code for page accounting (CUPS only).
my $ps_accounting = 1;
my $accounting_prolog = "";

# Enter here your personal command for converting non-postscript files
# (especially text) to PostScript. If you leave it blank, at first the
# line "textfilter: ..." from /etc/foomatic/filter.conf is read and
# then the commands given on the list below are tried, beginning with
# the first one.
# You can set this to "a2ps", "enscript" or "mpage" to select one of the 
# default command strings.
my $fileconverter = "";

my($kid0,$kid1,$kid2,$kid3,$kid4);
my($kidfailed,$kid3finished,$kid4finished);
my($convkidfailed,$dockidfailed,$kid0finished,$kid1finished,$kid2finished);
my($fileconverterpid,$rendererpid,$fileconverterhandle,$rendererhandle);
my($jobhasjcl);

# What 'echo' program to use.  It needs -e and -n.  Linux's builtin
# and regular echo work fine; non-GNU platforms may need to install
# gnu echo and put gecho here or something.
#
my $myecho = 'echo';

# Set debug to 1 to enable the debug logfile for this filter; it will
# appear as defined by $logfile. It will contain status from this
# filter, plus the renderer's stderr output. You can also add a line
# "debug: 1" to your /etc/foomatic/filter.conf to get all your
# Foomatic filters into debug mode.
#
# WARNING: This logfile is a security hole; do not use in production.
my $debug = 0;

# This is the location of the debug logfile (and also the copy of the
# processed PostScript data) in case you have enabled debugging above.
# The logfile will get the extension ".log", the PostScript data ".ps".
my $logfile = "/tmp/foomatic-rip";

# End interesting enduser options

# ==========================================================================
#
# foomatic-rip spooler-independent PS->Printer filter (RIP) of Foomatic
#
# Copyright 2002 - 2004 Grant Taylor <gtaylor@picante.com>
#		 & Till Kamppeter <till.kamppeter@gmx.net>
#                & Helge Blischke <h.blischke@srz.de>
#
#  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.
#
#  This program is distributed in the hope that it will be useful, but
#  WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
#  Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
#  USA.
#

my $added_lf = "\n";

# Flush everything immediately.
$|=1;



## Constants used by this filter

# Error codes, as some spooles behave different depending on the reason why
# the RIP failed, we return an error code. As I have only found a table of
# error codes for the PPR spooler. If our spooler is really PPR, these
# definitions get overwritten by the ones of the PPR version currently in
# use.

my $EXIT_PRINTED = 0;         # file was printed normally
my $EXIT_PRNERR = 1;          # printer error occured
my $EXIT_PRNERR_NORETRY = 2;  # printer error with no hope of retry
my $EXIT_JOBERR = 3;          # job is defective
my $EXIT_SIGNAL = 4;          # terminated after catching signal
my $EXIT_ENGAGED = 5;         # printer is otherwise engaged (connection 
                           # refused)
my $EXIT_STARVED = 6;         # starved for system resources
my $EXIT_PRNERR_NORETRY_ACCESS_DENIED = 7;     # bad password? bad port
                                            # permissions?
my $EXIT_PRNERR_NOT_RESPONDING = 8;            # just doesn't answer at all 
                                            # (turned off?)
my $EXIT_PRNERR_NORETRY_BAD_SETTINGS = 9;      # interface settings are invalid
my $EXIT_PRNERR_NO_SUCH_ADDRESS = 10;          # address lookup failed, may be 
                                            # transient
my $EXIT_PRNERR_NORETRY_NO_SUCH_ADDRESS = 11;  # address lookup failed, not 
                                            # transient
my $EXIT_INCAPABLE = 50;                       # printer wants (lacks) features
                                            # or resources
# Standard Unix signal names
#my SIGHUP = 1;
#my SIGINT = 2;
#my SIGQUIT = 3;
#my SIGKILL = 9;
#my SIGTERM = 15;
#my SIGUSR1 = 10;
#my SIGUSR2 = 12;
#my SIGTTIN = 21;
#my SIGTTOU = 22;

my $ESPIPE = 29;	# the errno value when seeking a pipe or socket



## Some important variables

# We don't know yet, which spooler will be used. If we don't detect
# one.  we assume that we do spooler-less printing. Supported spoolers
# are currently:

#    cups    - CUPS - Common Unix Printing System
#    lpd     - LPD - Line Printer Daemon
#    lprng   - LPRng - LPR - New Generation
#    gnulpr  - GNUlpr, an enhanced LPD (development stopped)
#    ppr     - PPR (foomatic-rip runs as a PPR RIP)
#    ppr_int - PPR (foomatic-rip runs as an interface)
#    cps     - CPS - Coherent Printing System
#    pdq     - PDQ - Print, Don't Queue (development stopped)
#    direct  - Direct, spooler-less printing

my $spooler = 'direct';

# PPD file name
my $ppdfile = "";

# Printer model
my $model = "";

# Printer queue name
my $printer = "";

# Printing options
my $optstr = "";

# Job title
my $jobtitle = "";

# Post pipe (command into which the output of this filter should be piped)
my $postpipe = "";

# Files to be printed
my @filelist = ();

# JCL prefix to put before the JCL options (Can be modified by a
# "*JCLBegin:" keyword in the PPD file):
my $jclbegin = "\033%-12345X\@PJL\n";

# JCL command to switch the printer to the PostScript interpreter (Can
# be modified by a "*JCLToPSInterpreter:" keyword in the PPD file):
my $jcltointerpreter = "";

# JCL command to close a print job (Can be modified by a "*JCLEnd:"
# keyword in the PPD file):
my $jclend = "\033%-12345X\@PJL RESET\n";

# Under which name were we called and in which directory do we reside
$0 =~ m!^(.*/)([^/]+)$!;
my $programdir = $1;
my $programname = $2;

# Filters to convert non-PostScript files
my @fileconverters = 
  (# a2ps (converts also other files than text)
   'a2ps -1 @@--medium=@@PAGESIZE@@ @@--center-title=@@JOBTITLE@@ -o -',
   # enscript
   'enscript -G @@-M @@PAGESIZE@@ @@-b "Page $%|@@JOBTITLE@@ ' .
   '--margins=36:36:36:36 --mark-wrapped-lines=arrow --word-wrap -p-',
   # mpage
   'mpage -o -1 @@-b @@PAGESIZE@@ @@-H -h @@JOBTITLE@@ -m36l36b36t36r ' .
   '-f -P- -');

# spooler-specific file converters, default for the specific spooler when
# none of the converters above is chosen. Remove weird characters from the
# command line arguments to enhance security
my @fixed_args = 
    (defined($ARGV[0])?removespecialchars($ARGV[0]):"",
     defined($ARGV[1])?removespecialchars($ARGV[1]):"",
     defined($ARGV[2])?removespecialchars($ARGV[2]):"",
     defined($ARGV[3])?removespecialchars($ARGV[3]):"",
     defined($ARGV[4])?removespecialchars($ARGV[4]):"");
my $spoolerfileconverters = {
    'cups' => "${programdir}texttops '$fixed_args[0]' '$fixed_args[1]' '$fixed_args[2]' " .
		"'$fixed_args[3]' '$fixed_args[4] page-top=36 page-bottom=36 " .
	"page-left=36 page-right=36 nolandscape cpi=12 lpi=7 " .
	"columns=1 wrap'"
    };

## Config file

# Read config file if present
my %conf = readConfFile("$configpath/filter.conf");

# Get execution path from config file
$execpath = $conf{execpath} if defined $conf{execpath};
$ENV{'PATH'} = $execpath;

# Get CUPS filter path from config file
$cupsfilterpath = $conf{cupsfilterpath} if defined $conf{cupsfilterpath};

# Set debug mode
$debug = $conf{debug} if defined $conf{debug};

# Determine which filter to use for non-PostScript files to be converted
# to PostScript
if (defined $conf{textfilter}) {
    $fileconverter = $conf{textfilter};
    $fileconverter eq 'a2ps' and $fileconverter = $fileconverters[0];
    $fileconverter eq 'enscript' and $fileconverter = $fileconverters[1];
    $fileconverter eq 'mpage' and $fileconverter = $fileconverters[2];
}



## Environment variables;

# "PPD": PPD file name for CUPS or PPR (if we run as PPR RIP)
if (defined($ENV{'PPD'})) {
    # Clean the file name from weird characters which could cause
    # unexpected behaviour
    $ppdfile = removespecialchars($ENV{'PPD'});
    # CUPS and PPR (RIP filter) use the "PPD" environment variable to 
    # make the PPD file name available (we set CUPS here preliminarily,
    # in the next step we check for PPR)
    $spooler = 'cups';
}

# "PPR_VERSION": PPR
if (defined($ENV{'PPR_VERSION'})) {
    # We have PPR
    $spooler = 'ppr';
}

# "PPR_RIPOPTS": PPR
if (defined($ENV{'PPR_RIPOPTS'})) {
    # PPR 1.5 allows the user to specify options for the PPR RIP with the 
    # "--ripopts" option on the "ppr" command line. They are provided to
    # the RIP via the "PPR_RIPOPTS" environment variable.
    # Clean the option string from weird characters which could cause
    # unexpected behaviour
    $optstr .= removespecialchars("$ENV{'PPR_RIPOPTS'} ");
    # We have PPR
    $spooler = 'ppr';
}

# "LPOPTS": Option settings for some LPD implementations (ex: GNUlpr)
if (defined($ENV{'LPOPTS'})) {
    my @lpopts = split(/,/, removespecialchars($ENV{'LPOPTS'}));
    foreach my $opt (@lpopts) {
	$opt =~ s/^\s+//;
	$opt =~ s/\s+$//;
	if ($opt =~ /\s+/) {
	    $opt = "\"$opt\"";
	}
	$optstr .= "$opt ";
    }
    # We have an LPD which accepts "-o" for options
    $spooler = 'gnulpr';
}



## Named command line options

# We do not use Getopt::Long because it does not work when between the
# option and the argument is no space ("-w80" instead of "-w 80"). This
# happens in the command line of LPRng, but also users could type in
# options this way when printing without spooler.

# Make one option string with a non-printable character as separator,
# So we can parse it more easily.

# To avoid the separator to be in the options itselves, it is filters
# out of the options. This does not break anything as having non
# printable characters in the command line options does not make sense
# nor is this needed. This way misinterpretation and even abuse is
# prevented.

my $argstr = "\x01" . 
    join("\x01", map { removeunprintables($_) } @ARGV) . "\x01";

# Debug mode activated via command line
if ($argstr =~ s/\x01--debug\x01/\x01/) {
    $debug = 1;
}

# Command line options for verbosity
my $verbose = ($argstr =~ s/\x01-v\x01/\x01/);
my $quiet = ($argstr =~ s/\x01-q\x01/\x01/);
my $show_docs = ($argstr =~ s/\x01-d\x01/\x01/);
my $do_docs;
my $cupscolorprofile;

# Where to send debugging log output to
my $logh;

if ($debug) {
    # Grotesquely unsecure; use for debugging only
    open LOG, "> ${logfile}.log";
    $logh = *LOG;

    use IO::Handle;
    $logh->autoflush(1);
} elsif (($quiet) && (!$verbose)) {
    # Quiet mode, do not log
    open LOG, "> /dev/null";
    $logh = *LOG;

    use IO::Handle;
    $logh->autoflush(1);
} else {
    # Default: log to STDERR
    $logh=*STDERR;
}



## Start debug logging
if ($debug) {
    # If we are not in debug mode, we do this later, as we must find out at
    # first which spooler is used. When printing without spooler we
    # suppress logging because foomatic-rip is called directly on the
    # command line and so we avoid logging onto the console.
    print $logh "foomatic-rip version $ripversion running...\n";
    # Print the command line only in debug mode, Mac OS X adds very many
    # options so that CUPS cannot handle the output of the command line
    # in its log files. If CUPS encounters a line with more than 1024
    # characters sent into its log files, it aborts the job with an error.
    if (($debug) || ($spooler ne 'cups')) {
	print $logh "called with arguments: '", join("', '",@ARGV), "'\n";
    }
}



## Continue with named options

# Check for LPRng first so we do not pick up bogus ppd files by the -p option
if ($argstr =~ s/\x01--lprng\x01/\x01/) {
    # We have LPRng
    $spooler = 'lprng';
}
# 'PRINTCAP_ENTRY' environment variable is : LPRng
#  the :ppd=/path/to/ppdfile printcap entry should be used
if (defined($ENV{'PRINTCAP_ENTRY'})){
	$spooler = 'lprng';
	my( @pc);
	@pc = split( /\s*:\s*/, $ENV{'PRINTCAP_ENTRY'} );
	shift @pc;
	foreach (@pc) {
		if( /^ppd=(.*)$/ or  /^ppdfile=(.*)$/ ){
			$ppdfile = removespecialchars($1) if $1;
		}
	}
} elsif ($argstr =~ s/\x01--lprng\x01/\x01/g) {
    # We have LPRng
    $spooler = 'lprng';
}


# PPD file name given via the command line
# allow duplicates, and use the last specified one
while ( ($spooler ne 'lprng') and ($argstr =~ s/\x01-p(\x01|)([^\x01]+)\x01/\x01/)) {
    $ppdfile = removeshellescapes($2);
}
while ($argstr =~ s/\x01--ppd(\x01|=|)([^\x01]+)\x01/\x01/) {
    $ppdfile = removeshellescapes($2);
}

# Check for LPD/GNUlpr by typical options which the spooler puts onto
# the filter's command line (options "-w": text width, "-l": text
# length, "-i": indent, "-x", "-y": graphics size, "-c": raw printing,
# "-n": user name, "-h": host name)
if (($argstr =~ s/\x01-w(\x01|)\d+\x01/\x01/) ||
    ($argstr =~ s/\x01-l(\x01|)\d+\x01/\x01/) || 
    ($argstr =~ s/\x01-x(\x01|)\d+\x01/\x01/) ||
    ($argstr =~ s/\x01-y(\x01|)\d+\x01/\x01/) || 
    ($argstr =~ s/\x01-i(\x01|)\d+\x01/\x01/) ||
    ($argstr =~ s/\x01-c\x01/\x01/) ||
    ($argstr =~ s/\x01-n(\x01|)[^\x01]+\x01/\x01/) ||
    ($argstr =~ s/\x01-h(\x01|)[^\x01]+\x01/\x01/)) {
    # We have LPD or GNUlpr
    if (($spooler ne 'lpd') && ($spooler ne 'gnulpr') && ($spooler ne 'lprng')) {
	$spooler = 'lpd';
    }
}

# LPRng delivers the option settings via the "-Z" argument
if ($argstr =~ s/\x01-Z(\x01|)([^\x01]+)\x01/\x01/) {
    my @lpopts = split(/,/, $2);
    foreach my $opt (@lpopts) {
	$opt =~ s/^\s+//;
	$opt =~ s/\s+$//;
	$opt = removeshellescapes($opt);
	if ($opt =~ /\s+/) {
	    $opt = "\"$opt\"";
	}
	$optstr .= "$opt ";
    }
    # We have LPRng
    $spooler = 'lprng';
}

# Job title and options for stock LPD
if ($argstr =~ s/\x01-[jJ](\x01|)([^\x01]+)\x01/\x01/) {
    # An LPD
    $jobtitle = removeshellescapes($2);
    # Classic LPD hack
    if ($spooler eq "lpd") {
	$optstr .= "$jobtitle ";
    }
}

# Check for CPS
if ($argstr =~ s/\x01--cps\x01/\x01/) {
    # We have cps
    $spooler = 'cps';
}

# Options for spooler-less printing, CPS, or PDQ
while ($argstr =~ s/\x01-o(\x01|)([^\x01]+)\x01/\x01/) {
    my $opt = $2;
    $opt =~ s/^\s+//;
    $opt =~ s/\s+$//;
    $opt = removeshellescapes($opt);
    if ($opt =~ /\s+/) {
	$opt = "\"$opt\"";
    }
    $optstr .= "$opt ";
    # If we don't print as a PPR RIP or as a CPS filter, we print without
    # spooler (we check for PDQ later)
    if (($spooler ne 'ppr') && ($spooler ne 'cps')) {
	$spooler = 'direct';
    }
}

# Printer for spooler-less printing or PDQ
if ($argstr =~ s/\x01-d(\x01|)([^\x01]+)\x01/\x01/) {
    $printer = removeshellescapes($2);
}
# Printer for spooler-less printing, PDQ, or LPRng
if ($argstr =~ s/\x01-P(\x01|)([^\x01]+)\x01/\x01/) {
    $printer = removeshellescapes($2);
}

# Were we called from a PDQ wrapper?
if ($argstr =~ s/\x01--pdq\x01/\x01/) {
    # We have PDQ
    $spooler = 'pdq';
}

# Were we called to build the PDQ driver declaration file?
# "--appendpdq=<file>" appends the data to the <file>,
# "--genpdq=<file>" creates/overwrites <file> for the data, and
# "--genpdq" writes to standard output
my $genpdqfile = "";
if (($argstr =~ s/\x01--(gen)(raw|)pdq(\x01|=|)([^\x01]*)\x01/\x01/) ||
    ($argstr =~ s/\x01--(append)(raw|)pdq(\x01|=|)([^\x01]+)\x01/\x01/)) {
    # Determine output file name
    if (!$4) {
	$genpdqfile = ">&STDOUT";
    } else {
	if ($1 eq 'gen') {
	    $genpdqfile = "> " . removeshellescapes($4);
	} else {
	    $genpdqfile = ">> " . removeshellescapes($4);
	}
    }
    # Do we want to have a PDQ driver declaration for a raw printer?
    if ($2 eq 'raw') {
	my $time = time();
	my @pdqfile =
"driver \"Raw-Printer-$time\" {
  # This PDQ driver declaration file was generated automatically by
  # foomatic-rip to allow raw (filter-less) printing.
  language_driver all {
    # We accept all file types and pass them through without any changes
    filetype_regx \"\"
    convert_exec {
      ln -s \$INPUT \$OUTPUT
    }
  }
  filter_exec {
    ln -s \$INPUT \$OUTPUT
  }
}";
	open PDQFILE, $genpdqfile or
	    rip_die("Cannot write PDQ driver declaration file",
		    $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
	print PDQFILE join('', @pdqfile);
	close PDQFILE;
	exit $EXIT_PRINTED;
    }
    # We have PDQ
    $spooler = 'pdq';
}


# remove extra spacing if running as LPRng filter
$added_lf = "" if $spooler eq 'lprng';

## Command line arguments without name

# Remaining arguments
my @rargs = split(/\x01/, $argstr);
shift @rargs;

# Load definitions for PPR error messages, check whether we run as
# PPR interface or as PPR RIP
my( $ppr_printer, $ppr_address, $ppr_options, $ppr_jobbreak, $ppr_feedback,
	$ppr_codes, $ppr_jobname, $ppr_routing, $ppr_for, $ppr_filetype,
	$ppr_filetoprint );
if ($spooler eq 'ppr') {
    # Read interface.sh so we will know the correct exit codes and
    # also signal.sh for the signal codes
    my $deffound = 0; # Did we find one of the definition files
    my @definitions;
    for my $file (("lib/interface.sh", "lib/signal.sh")) {
	
	open FILE, "< $file" || do {
	    print $logh "error opening $file.\n";
	    next;
	};
	
	$deffound = 1;
	while(my $line = <FILE>) {
	    # Translate the shell script to Perl
	    if (($line !~ m/^\s*$/) && ($line !~ m/^\s*\#/)) {
		$line =~ s/^\s*([^\#\s]*)/\$$1;/;
		push (@definitions, $line);
	    }
	}
	close FILE;
    }

    if ($deffound) {
	# Apply the definitions loaded from PPR
	eval join('',@definitions) || do {
	    print $logh "unable to evaluate definitions\n";
	    rip_die ("Error in definitions evaluation",
		     $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
	};
    }

    # Check whether we run as a PPR interface (if not, we run as a PPR RIP)
    if (($rargs[3] =~ /^\s*\d\d?\s*$/) &&
	($rargs[5] =~ /^\s*\d\d?\s*$/) &&
	(($#rargs == 10) || ($#rargs == 9) || ($#rargs == 7))) {
	# PPR calls interfaces with many command line parameters,
	# where the forth and the sixth is a small integer
	# number. In addition, we have 8 (PPR <= 1.31), 10
	# (PPR>=1.32), 11 (PPR >= 1.50) command line parameters.
	# We also check whether the current working directory is a
	# PPR directory.
	
	# Get all command line parameters
	$ppr_printer = removeshellescapes($rargs[0]);
	$ppr_address = $rargs[1];
	$ppr_options = removeshellescapes($rargs[2]);
	$ppr_jobbreak = $rargs[3];
	$ppr_feedback = $rargs[4];
	$ppr_codes = $rargs[5];
	$ppr_jobname = removeshellescapes($rargs[6]);
	$ppr_routing = removeshellescapes($rargs[7]);
	$ppr_for = $rargs[8];
	$ppr_filetype = $rargs[9];
	$ppr_filetoprint = removeshellescapes($rargs[10]);
	
	# Common job parameters
	$printer = $ppr_printer;
	$jobtitle = $ppr_jobname;
	if ((!$jobtitle) && ($ppr_filetoprint)) {
	    $jobtitle = $ppr_filetoprint;
	}
	$optstr .= "$ppr_options $ppr_routing";
	
	# Get the path of the PPD file from the queue configuration
	$ppdfile = `LANG=en_US; ppad show $ppr_printer | grep PPDFile`;
	$ppdfile = removeshellescapes($ppdfile);
	$ppdfile =~ s/PPDFile:\s+//;
	if ($ppdfile !~ m!^/!) {
	    $ppdfile = "../../share/ppr/PPDFiles/$ppdfile";
	}
	chomp($ppdfile);
	
	# We have PPR and run as an interface
	$spooler = 'ppr_int';
    }
}

# CUPS
my( $cups_jobid, $cups_user, $cups_jobtitle, $cups_copies, $cups_options,
    $cups_filename );
if ($spooler eq 'cups') {

    # Use CUPS font path ("FontPath" in /etc/cups/cupsd.conf)
    if ($ENV{'CUPS_FONTPATH'}) {
	$ENV{'GS_LIB'} = $ENV{'CUPS_FONTPATH'} .
	    ($ENV{'GS_LIB'} ? ":$ENV{'GS_LIB'}" : "");
    } else {
	if ($ENV{'CUPS_DATADIR'}) {
	    $ENV{'GS_LIB'} = "$ENV{'CUPS_DATADIR'}/fonts" .
		($ENV{'GS_LIB'} ? ":$ENV{'GS_LIB'}" : "");
	}
    }

    # Get all command line parameters
    $cups_jobid = removeshellescapes($rargs[0]);
    $cups_user = removeshellescapes($rargs[1]);
    $cups_jobtitle = removeshellescapes($rargs[2]);
    $cups_copies = removeshellescapes($rargs[3]);
    $cups_options = removeshellescapes($rargs[4]);
    $cups_filename = removeshellescapes($rargs[5]);

    # Common job parameters
    #$printer = $cups_printer;
    $jobtitle = $cups_jobtitle;
    $optstr .= $cups_options;

    # Check for and handle inputfile vs stdin
    if ((defined($cups_filename)) && ($cups_filename) &&
	($cups_filename ne '-')) {
	# We get the input from a file
	@filelist = ($cups_filename);
	print $logh "Getting input from file $cups_filename\n";
    }
}

# LPD/LPRng/GNUlpr
if (($spooler eq 'lpd') ||
    ($spooler eq 'lprng' and !$ppdfile) || 
    ($spooler eq 'gnulpr')) {

    # Get PPD file name as the last command line argument
    $ppdfile = removeshellescapes($rargs[$#rargs]);

}


# No spooler, CPS, or PDQ
if (($spooler eq 'direct') || ($spooler eq 'cps') || ($spooler eq 'pdq')) {
    # Which files do we want to print?
    @filelist = map { removeshellescapes($_) } @rargs;
}



## Additional spooler-specific preparations

# CUPS

if ($spooler eq 'cups') {

    # This piece of PostScript code (initial idea 2001 by Michael
    # Allerhand (michael.allerhand at ed dot ac dot uk, vastly
    # improved by Till Kamppeter in 2002) lets GhostScript output
    # the page accounting information which CUPS needs on standard
    # error.
    # Redesign by Helge Blischke (2004-11-17):
    # - As the PostScript job itself may define BeginPage and/or EndPage
    #   procedures, or the alternate pstops filter may have inserted
    #   such procedures, we make sure that the accounting routine 
    #   will safely coexist with those. To achieve this, we force
    #   - the accountint stuff to be inserted at the very end of the
    #     PostScript job's setup section,
    #   - the accounting stuff just using the return value of the 
    #     existing EndPage procedure, if any (and providing a default one
    #     if not).
    # - As PostScript jobs may contain calls to setpagedevice "between"
    #   pages, e.g. to change media type, do in-job stapling, etc.,
    #   we cannot rely on the "showpage count since last pagedevice
    #   activation" but instead count the physical pages by ourselves
    #   (in a global dictionary).

    if (defined $conf{ps_accounting}) {
	$ps_accounting = $conf{ps_accounting};
    }
    $accounting_prolog = $ps_accounting ? "[{
%% Code for writing CUPS accounting tags on standard error

/cupsPSLevel2 % Determine whether we can do PostScript level 2 or newer
    systemdict/languagelevel 2 copy
    known{get exec}{pop pop 1}ifelse 2 ge
def

cupsPSLevel2
{					% in case of level 2 or higher
	currentglobal true setglobal	% define a dictioary foomaticDict
	globaldict begin		% in global VM and establish a
	/foomaticDict			% pages count key there
	<<
		/PhysPages 0
	>>def
	end
	setglobal
}if

/cupsGetNumCopies { % Read the number of Copies requested for the current
		    % page
    cupsPSLevel2
    {
	% PS Level 2+: Get number of copies from Page Device dictionary
	currentpagedevice /NumCopies get
    }
    {
	% PS Level 1: Number of copies not in Page Device dictionary
	null
    }
    ifelse
    % Check whether the number is defined, if it is \"null\" use #copies 
    % instead
    dup null eq {
	pop #copies
    }
    if
    % Check whether the number is defined now, if it is still \"null\" use 1
    % instead
    dup null eq {
	pop 1
    } if
} bind def

/cupsWrite { % write a string onto standard error
    (%stderr) (w) file
    exch writestring
} bind def

/cupsFlush	% flush standard error to make it sort of unbuffered
{
	(%stderr)(w)file flushfile
}bind def

cupsPSLevel2
{				% In language level 2, we try to do something reasonable
  <<
    /EndPage
    [					% start the array that becomes the procedure
      currentpagedevice/EndPage 2 copy known
      {get}					% get the existing EndPage procedure
      {pop pop {exch pop 2 ne}bind}ifelse	% there is none, define the default
      /exec load				% make sure it will be executed, whatever it is
      /dup load					% duplicate the result value
      {					% true: a sheet gets printed, do accounting
        currentglobal true setglobal		% switch to global VM ...
        foomaticDict begin			% ... and access our special dictionary
        PhysPages 1 add			% count the sheets printed (including this one)
        dup /PhysPages exch def		% and save the value
        end					% leave our dict
        exch setglobal				% return to previous VM
        (PAGE: )cupsWrite 			% assemble and print the accounting string ...
        16 string cvs cupsWrite			% ... the sheet count ...
        ( )cupsWrite				% ... a space ...
        cupsGetNumCopies 			% ... the number of copies ...
        16 string cvs cupsWrite			% ...
        (\\n)cupsWrite				% ... a newline
        cupsFlush
      }/if load
					% false: current page gets discarded; do nothing	
    ]cvx bind				% make the array executable and apply bind
  >>setpagedevice
}
{
    % In language level 1, we do no accounting currently, as there is no global VM
    % the contents of which are undesturbed by save and restore. 
    % If we may be sure that showpage never gets called inside a page related save / restore pair
    % we might implement an hack with showpage similar to the one above.
}ifelse

} stopped cleartomark
" : "";

    # On which queue are we printing?
    # CUPS gives the PPD file the same name as the printer queue,
    # so we can get the queue name from the name of the PPD file.
    $ppdfile =~ m!^(.*/)([^/]+)\.ppd$!;
    $printer = $2;
}

# No spooler, CPS, or PDQ

if (($spooler eq 'direct') || ($spooler eq 'cps') || ($spooler eq 'pdq')) {

    # Path for personal Foomatic configuration
    my $user_default_path = "$ENV{'HOME'}/.foomatic";

    if (!$ppdfile) {
	if (!$printer) {
	    # No printer definition file selected, check whether we have a
	    # default printer defined.
	    for my $conf_file (("./.directconfig",
				"./directconfig",
				"./.config",
				"$user_default_path/direct/.config",
				"$user_default_path/direct.conf",
				"$configpath/direct/.config",
				"$configpath/direct.conf")) {
		if (open CONFIG, "< $conf_file") {
		    while (my $line = <CONFIG>) {
			chomp $line;
			if ($line =~ /^default\s*:\s*([^:\s]+)\s*$/) {
			    $printer = $1;
			    last;
			}
		    }
		    close CONFIG;
		}
		if ($printer) {
		    last;
		}
	    }
	}

	# Neither in a config file nor on the command line a printer was
	# selected.
	if (!$printer) {
	    rip_die("No printer definition (option \"-P <name>\") " .
		    "specified!", $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
	}
	
	# Search for the PPD file
	
	# Search also common spooler-specific locations, this way a printer
	# configured under a certain spooler can also be used without
	# spooler

	if (-r $printer) {
	    $ppdfile = $printer;
	# CPS can have the PPD in the spool directory
	} elsif (($spooler eq 'cps') &&
		 (-r "/var/spool/lpd/${printer}/${printer}.ppd")) {
	    $ppdfile = "/var/spool/lpd/${printer}/${printer}.ppd";
	} elsif (($spooler eq 'cps') &&
		 (-r "/var/local/spool/lpd/${printer}/${printer}.ppd")) {
	    $ppdfile = "/var/local/spool/lpd/${printer}/${printer}.ppd";
	} elsif (($spooler eq 'cps') &&
		 (-r "/var/local/lpd/${printer}/${printer}.ppd")) {
	    $ppdfile = "/var/local/lpd/${printer}/${printer}.ppd";
	} elsif (($spooler eq 'cps') &&
		 (-r "/var/spool/lpd/${printer}.ppd")) {
	    $ppdfile = "/var/spool/lpd/${printer}.ppd";
	} elsif (($spooler eq 'cps') &&
		 (-r "/var/local/spool/lpd/${printer}.ppd")) {
	    $ppdfile = "/var/local/spool/lpd/${printer}.ppd";
	} elsif (($spooler eq 'cps') &&
		 (-r "/var/local/lpd/${printer}.ppd")) {
	    $ppdfile = "/var/local/lpd/${printer}.ppd";
	} elsif (-r "${printer}.ppd") { # current dir
	    $ppdfile = "${printer}.ppd";
	} elsif (-r "$user_default_path/${printer}.ppd") { # user dir
	    $ppdfile = "$user_default_path/${printer}.ppd";
	} elsif (-r "$configpath/direct/${printer}.ppd") { # system dir
	    $ppdfile = "$configpath/direct/${printer}.ppd";
	} elsif (-r "$configpath/${printer}.ppd") { # system dir
	    $ppdfile = "$configpath/${printer}.ppd";
	} elsif (-r "/etc/cups/ppd/${printer}.ppd") { # CUPS config dir
	    $ppdfile = "/etc/cups/ppd/${printer}.ppd";
	} elsif (-r "/usr/local/etc/cups/ppd/${printer}.ppd") {
	    $ppdfile = "/usr/local/etc/cups/ppd/${printer}.ppd";
	} elsif (-r "/usr/share/ppr/PPDFiles/${printer}.ppd") { # PPR PPDs
	    $ppdfile = "/usr/share/ppr/PPDFiles/${printer}.ppd";
	} elsif (-r "/usr/local/share/ppr/PPDFiles/${printer}.ppd") {
	    $ppdfile = "/usr/local/share/ppr/PPDFiles/${printer}.ppd";
	} else {
	    rip_die ("There is no readable PPD file for the printer " .
		     "$printer, is it configured?",
		     $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
	}
    }
}



## Files to be printed (can be more than one for spooler-less printing)

# Empty file list -> print STDIN
if ($#filelist < 0) {
    @filelist = ("<STDIN>");
}

# Check file list
my $file;
my $filecnt = 0;
for $file (@filelist) {
    if ($file ne "<STDIN>") {
	if ($file =~ /^-/) {
	    rip_die ("Invalid argument: $file",
		     $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
	} elsif (! -r $file) {
	    print $logh "File $file does not exist/is not readable\n";
	    splice(@filelist, $filecnt, 1);
	    $filecnt --;
	}
    }
    $filecnt ++;
}



## When we print without spooler or with CPS do not log onto STDERR unless 
## the "-v" ('Verbose') is set or the debug mode is used
if ((($spooler eq 'direct') || ($spooler eq 'cps') || ($genpdqfile)) && 
    (!$verbose) && (!$debug)) {
    close $logh;
    open LOG, "> /dev/null";
    $logh = *LOG;

    use IO::Handle;
    $logh->autoflush(1);
}



## Start logging
if (!$debug) {
    # If we are in debug mode, we do this earlier.
    print $logh "foomatic-rip version $ripversion running...\n";
    # Print the command line only in debug mode, Mac OS X adds very many
    # options so that CUPS cannot handle the output of the command line
    # in its log files. If CUPS encounters a line with more than 1024
    # characters sent into its log files, it aborts the job with an error.
    if (($debug) || ($spooler ne 'cups')) {
	print $logh "called with arguments: '", join("', '",@ARGV), "'\n";
    }
}



## PPD file

# Load the PPD file and build a data structure for the renderer's
# command line and the options
open PPD, "< $ppdfile" || do {
    print $logh "error opening $ppdfile.\n";
    rip_die ("Unable to open PPD file $ppdfile",
	     $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
};

print $logh "Parsing PPD file ...\n";

my $dat = {};              # data structure for the options
my $currentargument = "";  # We are currently reading this argument

# If we have an old Foomatic 2.0.x PPD file, read its built-in Perl
# data structure into @datablob and the default values in %ppddefaults
# Then delete the $dat structure, replace it by the one "eval"ed from
# @datablob, and correct the default settings according to the ones of
# the main PPD structure
my @datablob;

# Parse the PPD file
sub undossify( $ );
while(<PPD>) {
    # foomatic-rip should also work with PPD file downloaded under Windows.
    $_ = undossify($_);
    # Parse keywords
    if (m!^\*NickName:\s*\"(.*)$!) {
	# "*NickName: <code>"
	my $line = $1;
	# Store the value
	# Code string can have multiple lines, read all of them
	my $cmd = "";
	while ($line !~ m!\"!) {
	    if ($line =~ m!&&$!) {
		# line continues in next line
		$cmd .= substr($line, 0, -2);
	    } else {
		# line ends here
		$cmd .= "$line\n";
	    }
	    # Read next line
	    $line = <PPD>;
	    chomp $line;
	}
	$line =~ m!^([^\"]*)\"!;
	$cmd .= $1;
	$model = unhtmlify($cmd);
    } elsif (m!^\*FoomaticIDs:\s*(\S+)\s+(\S+)\s*$!) {
	# "*FoomaticIDs: <printer ID> <driver ID>"
	my $id = $1;
	my $driver = $2;
	# Store the values
	$dat->{'id'} = $id;
	$dat->{'driver'} = $driver;
    } elsif (m!^\*FoomaticRIPPostPipe:\s*\"(.*)$!) {
	# "*FoomaticRIPPostPipe: <code>"
	my $line = $1;
	# Store the value
	# Code string can have multiple lines, read all of them
	my $cmd = "";
	while ($line !~ m!\"!) {
	    if ($line =~ m!&&$!) {
		# line continues in next line
		$cmd .= substr($line, 0, -2);
	    } else {
		# line ends here
		$cmd .= "$line\n";
	    }
	    # Read next line
	    $line = <PPD>;
	    chomp $line;
	}
	$line =~ m!^([^\"]*)\"!;
	$cmd .= $1;
	$postpipe = unhtmlify($cmd);
    } elsif (m!^\*FoomaticRIPCommandLine:\s*\"(.*)$!) {
	# "*FoomaticRIPCommandLine: <code>"
	my $line = $1;
	# Store the value
	# Code string can have multiple lines, read all of them
	my $cmd = "";
	while ($line !~ m!\"!) {
	    if ($line =~ m!&&$!) {
		# line continues in next line
		$cmd .= substr($line, 0, -2);
	    } else {
		# line ends here
		$cmd .= "$line\n";
	    }
	    # Read next line
	    $line = <PPD>;
	    chomp $line;
	}
	$line =~ m!^([^\"]*)\"!;
	$cmd .= $1;
	$dat->{'cmd'} = unhtmlify($cmd);
    } elsif (m!^\*cupsFilter:\s*\"(.*)$!) {
	# "*cupsFilter: <code>"
	my $line = $1;
	# Store the value
	# Code string can have multiple lines, read all of them
	my $cmd = "";
	while ($line !~ m!\"!) {
	    if ($line =~ m!&&$!) {
		# line continues in next line
		$cmd .= substr($line, 0, -2);
	    } else {
		# line ends here
		$cmd .= "$line\n";
	    }
	    # Read next line
	    $line = <PPD>;
	    chomp $line;
	}
	$line =~ m!^([^\"]*)\"!;
	$cmd .= $1;
	my $cupsfilterline = unhtmlify($cmd);
	if ($cupsfilterline =~ /^\s*(\S+)\s+\d+\s+(\S+)\s*$/) {
	    print $logh "*cupsFilter: \"$cupsfilterline\"\n"; 
	    # Make a hash by mime type for all CUPS filters set in this PPD
	    $dat->{'cupsfilter'}{$1} = $2;
	}
    } elsif (m!^\*CustomPageSize\s+True:\s*\"(.*)$!) {
	# "*CustomPageSize True: <code>"
	my $setting = "Custom";
	my $translation = "Custom Size";
	my $line = $1;
	# Make sure that the argument is in the data structure
	checkarg ($dat, "PageSize");
	checkarg ($dat, "PageRegion");
	# Make sure that the setting is in the data structure
	checksetting ($dat, "PageSize", $setting);
	checksetting ($dat, "PageRegion", $setting);
	$dat->{'args_byname'}{'PageSize'}{'vals_byname'}{$setting}{'comment'} = $translation;
	$dat->{'args_byname'}{'PageRegion'}{'vals_byname'}{$setting}{'comment'} = $translation;
	# Store the value
	# Code string can have multiple lines, read all of them
	my $code = "";
	while ($line !~ m!\"!) {
	    if ($line =~ m!&&$!) {
		# line continues in next line
		$code .= substr($line, 0, -2);
	    } else {
		# line ends here
		$code .= "$line\n";
	    }
	    # Read next line
	    $line = <PPD>;
	    chomp $line;
	}
	$line =~ m!^([^\"]*)\"!;
	$code .= $1;
	if ($code !~ m!^%% FoomaticRIPOptionSetting!m) {
	    $dat->{'args_byname'}{'PageSize'}{'vals_byname'}{$setting}{'driverval'} = $code;
	    $dat->{'args_byname'}{'PageRegion'}{'vals_byname'}{$setting}{'driverval'} = $code;
	}
    } elsif (m!^\*(JCL|)OpenUI\s+\*([^:]+):\s*(\S+)\s*$!) {
	# "*[JCL]OpenUI *<option>[/<translation>]: <type>"
	my $argnametrans = $2;
	my $argtype = $3;
	my $argname;
	my $translation = "";
	if ($argnametrans =~ m!^([^:/\s]+)/([^:]*)$!) {
	    $argname = $1;
	    $translation = $2;
	} else {
	    $argname = $argnametrans;
	}
	# Make sure that the argument is in the data structure
	checkarg ($dat, $argname);
	# Store the values
	$dat->{'args_byname'}{$argname}{'comment'} = $translation;
	# Set the argument type only if not defined yet, a
	# definition in "*FoomaticRIPOption" has priority
	if ( !($dat->{'args_byname'}{$argname}{'type'}) ) {
	    if ($argtype eq "PickOne") {
		$dat->{'args_byname'}{$argname}{'type'} = 'enum';
	    } elsif ($argtype eq "PickMany") {
		$dat->{'args_byname'}{$argname}{'type'} = 'pickmany';
	    } elsif ($argtype eq "Boolean") {
		$dat->{'args_byname'}{$argname}{'type'} = 'bool';
	    }
	}
	# Mark in which argument we are currently, so that we can find
	# the entries for the choices
	$currentargument = $argname;
    } elsif (m!^\*(JCL|)CloseUI:\s+\*([^:/\s]+)\s*$!) {
	# "*[JCL]CloseUI *<option>"
	my $argname = $2;
	# Unmark the current argument to do not mis-interpret any keywords
	# as choices
	$currentargument = "";
    } elsif ((m!^\*FoomaticRIPOption ([^/:\s]+):\s*(\S+)\s+(\S+)\s+(\S)\s*$!) ||
	     (m!^\*FoomaticRIPOption ([^/:\s]+):\s*(\S+)\s+(\S+)\s+(\S)\s+(\S+)\s*$!)){
	# "*FoomaticRIPOption <option>: <type> <style> <spot> [<order>]"
	# <order> only used for 1-choice enum options
	my $argname = $1;
	my $argtype = $2;
	my $argstyle = $3;
	my $spot = $4;
	my $order = $5;
	# Make sure that the argument is in the data structure
	checkarg ($dat, $argname);
	# Store the values
	$dat->{'args_byname'}{$argname}{'type'} = $argtype;
	if ($argstyle eq "PS") {
	    $dat->{'args_byname'}{$argname}{'style'} = 'G';
	} elsif ($argstyle eq "CmdLine") {
	    $dat->{'args_byname'}{$argname}{'style'} = 'C';
	} elsif ($argstyle eq "JCL") {
	    $dat->{'args_byname'}{$argname}{'style'} = 'J';
	    $dat->{'jcl'} = 1;
	} elsif ($argstyle eq "Composite") {
	    $dat->{'args_byname'}{$argname}{'style'} = 'X';
	}
	$dat->{'args_byname'}{$argname}{'spot'} = $spot;
	# $order only defined here for 1-choice enum options
	if ($order) {
	    $dat->{'args_byname'}{$argname}{'order'} = $order;
	}
    } elsif (m!^\*FoomaticRIPOptionPrototype\s+([^/:\s]+):\s*\"(.*)$!) {
	# "*FoomaticRIPOptionPrototype <option>: <code>"
	# Used for numerical and string options only
	my $argname = $1;
	my $line = $2;
	# Make sure that the argument is in the data structure
	checkarg ($dat, $argname);
	# Store the value
	# Code string can have multiple lines, read all of them
	my $proto = "";
	while ($line !~ m!\"!) {
	    if ($line =~ m!&&$!) {
		# line continues in next line
		$proto .= substr($line, 0, -2);
	    } else {
		# line ends here
		$proto .= "$line\n";
	    }
	    # Read next line
	    $line = <PPD>;
	    chomp $line;
	}
	$line =~ m!^([^\"]*)\"!;
	$proto .= $1;
	$dat->{'args_byname'}{$argname}{'proto'} = unhtmlify($proto);
    } elsif (m!^\*FoomaticRIPOptionRange\s+([^/:\s]+):\s*(\S+)\s+(\S+)\s*$!) {
	# "*FoomaticRIPOptionRange <option>: <min> <max>"
	# Used for numerical options only
	my $argname = $1;
	my $min = $2;
	my $max = $3;
	# Make sure that the argument is in the data structure
	checkarg ($dat, $argname);
	# Store the values
	$dat->{'args_byname'}{$argname}{'min'} = $min;
	$dat->{'args_byname'}{$argname}{'max'} = $max;
    } elsif (m!^\*FoomaticRIPOptionMaxLength\s+([^/:\s]+):\s*(\S+)\s*$!) {
	# "*FoomaticRIPOptionMaxLength <option>: <length>"
	# Used for string options only
	my $argname = $1;
	my $maxlength = $2;
	# Make sure that the argument is in the data structure
	checkarg ($dat, $argname);
	# Store the value
	$dat->{'args_byname'}{$argname}{'maxlength'} = $maxlength;
    } elsif (m!^\*FoomaticRIPOptionAllowedChars\s+([^/:\s]+):\s*\"(.*)$!) {
	# "*FoomaticRIPOptionAllowedChars <option>: <code>"
	# Used for string options only
	my $argname = $1;
	my $line = $2;
	# Store the value
	# Code string can have multiple lines, read all of them
	my $code = "";
	while ($line !~ m!\"!) {
	    if ($line =~ m!&&$!) {
		# line continues in next line
		$code .= substr($line, 0, -2);
	    } else {
		# line ends here
		$code .= "$line\n";
	    }
	    # Read next line
	    $line = <PPD>;
	    chomp $line;
	}
	$line =~ m!^([^\"]*)\"!;
	$code .= $1;
	# Make sure that the argument is in the data structure
	checkarg ($dat, $argname);
	# Store the value
	$dat->{'args_byname'}{$argname}{'allowedchars'} = unhtmlify($code);
    } elsif (m!^\*FoomaticRIPOptionAllowedRegExp\s+([^/:\s]+):\s*\"(.*)$!) {
	# "*FoomaticRIPOptionAllowedRegExp <option>: <code>"
	# Used for string options only
	my $argname = $1;
	my $line = $2;
	# Store the value
	# Code string can have multiple lines, read all of them
	my $code = "";
	while ($line !~ m!\"!) {
	    if ($line =~ m!&&$!) {
		# line continues in next line
		$code .= substr($line, 0, -2);
	    } else {
		# line ends here
		$code .= "$line\n";
	    }
	    # Read next line
	    $line = <PPD>;
	    chomp $line;
	}
	$line =~ m!^([^\"]*)\"!;
	$code .= $1;
	# Make sure that the argument is in the data structure
	checkarg ($dat, $argname);
	# Store the value
	$dat->{'args_byname'}{$argname}{'allowedregexp'} =
	    unhtmlify($code);
    } elsif (m!^\*OrderDependency:\s*(\S+)\s+(\S+)\s+\*([^:/\s]+)\s*$!) {
	# "*OrderDependency: <order> <section> *<option>"
	my $order = $1;
	my $section = $2;
	my $argname = $3;
	# Make sure that the argument is in the data structure
	checkarg ($dat, $argname);
	# Store the values
	$dat->{'args_byname'}{$argname}{'order'} = $order;
	$dat->{'args_byname'}{$argname}{'section'} = $section;
    } elsif (m!^\*Default([^/:\s]+):\s*([^/:\s]+)\s*$!) {
	# "*Default<option>: <value>"
	my $argname = $1;
	my $default = $2;
	# Make sure that the argument is in the data structure
	checkarg ($dat, $argname);
	# Store the value
	$dat->{'args_byname'}{$argname}{'default'} = $default;
    } elsif (m!^\*FoomaticRIPDefault([^/:\s]+):\s*([^/:\s]+)\s*$!) {
	# "*FoomaticRIPDefault<option>: <value>"
	# Used for numerical options only
	my $argname = $1;
	my $default = $2;
	# Make sure that the argument is in the data structure
	checkarg ($dat, $argname);
	# Store the value
	$dat->{'args_byname'}{$argname}{'fdefault'} = $default;
    } elsif (m!^\*$currentargument\s+([^:]+):\s*\"(.*)$!) {
	# "*<option> <choice>[/<translation>]: <code>"
	my $settingtrans = $1;
	my $line = $2;
	my $translation = "";
	my $setting = "";
	if ($settingtrans =~ m!^([^:/\s]+)/([^:]*)$!) {
	    $setting = $1;
	    $translation = $2;
	} else {
	    $setting = $settingtrans;
	}
	# Make sure that the argument is in the data structure
	checkarg ($dat, $currentargument);
	# Make sure that the setting is in the data structure (enum options)
	my $bool =
	    ($dat->{'args_byname'}{$currentargument}{'type'} eq 'bool');
	if ($bool) {
	    if (lc($setting) eq "true") {
		if (!$dat->{'args_byname'}{$currentargument}{'comment'}) {
		    $dat->{'args_byname'}{$currentargument}{'comment'} =
			$translation;
		}
		$dat->{'args_byname'}{$currentargument}{'comment_true'} =
		    $translation;
	    } else {
		$dat->{'args_byname'}{$currentargument}{'comment_false'} =
		    $translation;
	    }
	} else {
	    checksetting ($dat, $currentargument, $setting);
	    # Make sure that this argument has a default setting, even if 
	    # none is defined in this PPD file
	    if (!$dat->{'args_byname'}{$currentargument}{'default'}) {
		$dat->{'args_byname'}{$currentargument}{'default'} = $setting;
	    }
	    $dat->{'args_byname'}{$currentargument}{'vals_byname'}{$setting}{'comment'} = $translation;
	}
	# Store the value
	# Code string can have multiple lines, read all of them
	my $code = "";
	while ($line !~ m!\"!) {
	    if ($line =~ m!&&$!) {
		# line continues in next line
		$code .= substr($line, 0, -2);
	    } else {
		# line ends here
		$code .= "$line\n";
	    }
	    # Read next line
	    $line = <PPD>;
	    chomp $line;
	}
	$line =~ m!^([^\"]*)\"!;
	$code .= $1;
	if ($code !~ m!^%% FoomaticRIPOptionSetting!) {
	    if ($bool) {
		if (lc($setting) eq "true") {
		    $dat->{'args_byname'}{$currentargument}{'proto'} = $code;
		} else {
		    $dat->{'args_byname'}{$currentargument}{'protof'} = $code;
		}
	    } else {
		$dat->{'args_byname'}{$currentargument}{'vals_byname'}{$setting}{'driverval'} = $code;
	    }
	}
    } elsif ((m!^\*FoomaticRIPOptionSetting\s+([^/:=\s]+)=([^/:=\s]+):\s*\"(.*)$!) ||
	     (m!^\*FoomaticRIPOptionSetting\s+([^/:=\s]+):\s*\"(.*)$!)) {
	# "*FoomaticRIPOptionSetting <option>[=<choice>]: <code>"
	# For boolean options <choice> is not given
	my $argname = $1;
	my $setting = $2;
	my $line = $3;
	my $bool = 0;
	if (!$line) {
	    $line = $setting;
	    $bool = 1;
	}
	# Make sure that the argument is in the data structure
	checkarg ($dat, $argname);
	# Make sure that the setting is in the data structure (enum options)
	if (!$bool) {
	    checksetting ($dat, $argname, $setting);
	    # Make sure that this argument has a default setting, even if 
	    # none is defined in this PPD file
	    if (!$dat->{'args_byname'}{$argname}{'default'}) {
		$dat->{'args_byname'}{$argname}{'default'} = $setting;
	    }
	}
	# Store the value
	# Code string can have multiple lines, read all of them
	my $code = "";
	while ($line !~ m!\"!) {
	    if ($line =~ m!&&$!) {
		# line continues in next line
		$code .= substr($line, 0, -2);
	    } else {
		# line ends here
		$code .= "$line\n";
	    }
	    # Read next line
	    $line = <PPD>;
	    chomp $line;
	}
	$line =~ m!^([^\"]*)\"!;
	$code .= $1;
	if ($bool) {
	    $dat->{'args_byname'}{$argname}{'proto'} = unhtmlify($code);
	} else {
	    $dat->{'args_byname'}{$argname}{'vals_byname'}{$setting}{'driverval'} = unhtmlify($code);
	}
    } elsif (m!^\*JCL(Begin|ToPSInterpreter|End):\s*\"(.*)$!) {
	# "*JCL(Begin|ToPSInterpreter|End): <code>"
	# The printer supports PJL/JCL when there is such a line 
	$dat->{'jcl'} = 1;
	my $item = $1;
	my $line = $2;
	# Store the value
	# Code string can have multiple lines, read all of them
	my $code = "";
	while ($line !~ m!\"!) {
	    if ($line =~ m!&&$!) {
		# line continues in next line
		$code .= substr($line, 0, -2);
	    } else {
		# line ends here
		$code .= "$line\n";
	    }
	    # Read next line
	    $line = <PPD>;
	    chomp $line;
	}
	$line =~ m!^([^\"]*)\"!;
	$code .= $1;
	if ($item eq 'Begin') {
	    $jclbegin = unhexify($code);
	} elsif ($item eq 'ToPSInterpreter') {
	    $jcltointerpreter = unhexify($code);
	} elsif ($item eq 'End') {
	    $jclend = unhexify($code);
	}
    } elsif (m!^\*\% COMDATA \#(.*)$!) {
	# If we have an old Foomatic 2.0.x PPD file, collect its Perl data
	push (@datablob, $1);
    }
}
close PPD;

# If we have an old Foomatic 2.0.x PPD file use its Perl data structure
if ($#datablob >= 0) {
    print $logh "${added_lf}You are using an old Foomatic 2.0 PPD file, consider " .
	"upgrading.${added_lf}\n";
    my $VAR1;
    if (eval join('',@datablob)) {
	# Overtake default settings from the main structure of the PPD file
	for my $arg (@{$dat->{'args'}}) {
	    if ($arg->{'default'}) {
		$VAR1->{'argsbyname'}{$arg->{'name'}}{'default'} = 
		    $arg->{'default'};
	    }
	}
	undef $dat;
	$dat = $VAR1;
	$dat->{'jcl'} = $dat->{'pjl'};
    } else {
	# Perl structure broken
	print $logh "${added_lf}Unable to evaluate datablob, print job may come " .
	    "out incorrectly or not at all.${added_lf}\n";
    }
}



## We do not need to parse the PostScript job when we don't have
## any options. If we have options, we must check whether the
## default settings from the PPD file are valid and correct them
## if nexessary.

my $dontparse = 0;
if ((!defined(@{$dat->{'args'}})) ||
    ($#{$dat->{'args'}} < 0)) {
    # We don't have any options, so we do not need to parse the
    # PostScript data
    $dontparse = 1;
} else {
    # Let the default value of a boolean option being 0 or 1 instead of
    # "True" or "False", range-check the defaults of all options and
    # issue warnings if the values are not valid
    checkoptions($dat, 'default');

    # Adobe's PPD specs do not support numerical
    # options. Therefore the numerical options are mapped to
    # enumerated options in the PPD file and their characteristics
    # as a numerical option are stored in "*Foomatic..."
    # keywords. A default must be between the enumerated
    # fixed values. The default
    # value must be given by a "*FoomaticRIPDefault<option>:
    # <value>" line in the PPD file. But this value is only valid
    # if the "official" default given by a "*Default<option>:
    # <value>" line (it must be one of the enumerated values)
    # points to the enumerated value which is closest to this
    # value. This way a user can select a default value with a
    # tool only supporting PPD files but not Foomatic extensions.
    # This tool only modifies the "*Default<option>: <value>" line
    # and if the "*FoomaticRIPDefault<option>: <value>" had always
    # priority, the user's change in "*Default<option>: <value>"
    # would have no effect.

    for my $arg (@{$dat->{'args'}}) {
	if ($arg->{'fdefault'}) {
	    if ($arg->{'default'}) {
		if ($arg->{'type'} =~ /^(int|float)$/) {
		    if ($arg->{'fdefault'} < $arg->{'min'}) {
			$arg->{'fdefault'} = $arg->{'min'};
		    }
		    if ($arg->{'fdefault'} > $arg->{'max'}) {
			$arg->{'fdefault'} = $arg->{'max'};
		    }
		    my $mindiff = abs($arg->{'max'} - $arg->{'min'});
		    my $closestvalue;
		    for my $val (@{$arg->{'vals'}}) {
			if (abs($arg->{'fdefault'} - $val->{'value'}) <
			    $mindiff) {
			    $mindiff = 
				abs($arg->{'fdefault'} - $val->{'value'});
			    $closestvalue = $val->{'value'};
			}
		    }
		    if (($arg->{'default'} == $closestvalue) ||
			(abs($arg->{'default'} - $closestvalue) /
			 $closestvalue < 0.001)) {
			$arg->{'default'} = $arg->{'fdefault'};
		    }
		}
	    } else {
		$arg->{'default'} = $arg->{'fdefault'};
	    }
	}
    }
}

# Is our PPD for a CUPS raster driver
if (my $cupsfilter = $dat->{'cupsfilter'}{"application/vnd.cups-raster"}) {

    # Search filter in cupsfilterpath
    # The %Y is a placeholder for the option settings
    my $havefilter = 0;
    for (split(':', $cupsfilterpath)) {
	if (-x "$_/$cupsfilter") {
	    $havefilter=1;
	    $cupsfilter = "$_/$cupsfilter 0 '' '' 0 '%Y%X'";
	    last;
	}
    }

    if (!$havefilter) {

	# We do not have the required filter, so we assume that
	# rendering this job is supposed to be done on a remote
	# server. So we do not define a renderer command line and
	# embed only the option settings (as we had a PostScript
	# printer). This way the settings are # taken into account
	# when the job is rendered on the server.
	print $logh "${added_lf}CUPS filter for this PPD file not found " .
	    "assuming that job will be rendered on a remote server. Only " .
	    "the PostScript of the options will be inserted into the " .
	    "PostScript data stream.${added_lf}\n";

    } else {

	# use pstoraster script if available, otherwise run GhostScript
	# directly
	my $pstoraster = "pstoraster";
	my $havepstoraster = 0;
	for (split(':', $cupsfilterpath)) {
	    if (-x "$_/$pstoraster") {
		$havepstoraster=1;
		$pstoraster = "$_/$pstoraster 0 '' '' 0 '%X'";
		last;
	    }
	}

	if (!$havepstoraster) {

	    # Build GhostScript command line
	    $pstoraster = "gs -dQUIET -dDEBUG -dPARANOIDSAFER -dNOPAUSE -dBATCH -dNOMEDIAATTRS -sDEVICE=cups -sOutputFile=-%W -"
	    
	}

	# build GhostScript/CUPS driver command line
	$dat->{'cmd'} = "$pstoraster | $cupsfilter";

	# Set environment variables
	$ENV{'PPD'} = $ppdfile;
	
    }
}

# Was the RIP command line defined in the PPD file? If not, we assume a
# PostScript printer and do not render/translate the input data
if (!defined($dat->{'cmd'})) {
    $dat->{'cmd'} = "cat%A%B%C%D%E%F%G%H%I%J%K%L%M%Z";
    if ($dontparse) {
	# No command line, no options, we have a raw queue, don't check
	# whether the input is PostScript and ignore the "docs" option,
	# simply pass the input data to the backend.
	$dontparse = 2;
	$model = "Raw queue";
    }
}



## Summary for debugging
print $logh "${added_lf}Parameter Summary\n";
print $logh "-----------------${added_lf}\n";
print $logh "Spooler: $spooler\n";
print $logh "Printer: $printer\n";
print $logh "PPD file: $ppdfile\n";
print $logh "Printer model: $model\n";
# Print the options string only in debug mode, Mac OS X adds very many
# options so that CUPS cannot handle the output of the option string
# in its log files. If CUPS encounters a line with more than 1024 characters
# sent into its log files, it aborts the job with an error.
if (($debug) || ($spooler ne 'cups')) {
    print $logh "Options: $optstr\n";
}
print $logh "Job title: $jobtitle\n";
print $logh "File(s) to be printed: ${added_lf}@filelist${added_lf}\n";
print $logh "GhostScript extra search path ('GS_LIB'): $ENV{'GS_LIB'}\n"
    if $ENV{'GS_LIB'};



## Parse options from command line ($optstr)

# Before we start, save the defaults for printing documentation pages

copyoptions($dat, 'default', 'userval');


# The options are "foo='bar nut'", "foo", "nofoo", "'bar nut'", or
# "foo:'bar nut'" (when GPR was used) all with spaces between...
# In addition they can be preceeded by page ranges, separated with a
# colon.

my @opts;

# Variable for PPR's backend interface name (parallel, tcpip, atalk, ...)

my $backend = "";

# Array to collect unknown options so that they can get passed to the
# backend interface of PPR. For other spoolers we ignore them.

my @backendoptions = ();

# "foo='bar nut'"
while ($optstr =~ s!(((even|odd|[\d,-]+):|)\w+=[\'\"].*?[\'\"]) ?!!i) {
    push (@opts, $1);
}

# "foo:'bar nut'" (GPR separates option and setting with a colon ":")
while ($optstr =~ s!(((even|odd|[\d,-]+):|)\w+:[\'\"].*?[\'\"]) ?!!i) {
#while ($optstr =~ s!(\w+=[\'\"].*?[\'\"])!!i) {
    push (@opts, $1);
}

# "'bar nut'", "'foo=bar nut'", "'foo:bar nut'"
while ($optstr =~ s!([\'\"].+?[\'\"]) ?!!) {
    my $opt = $1;
    $opt =~ s/[\'\"]//g; # Make only sure that we didn't quote
                         # the option for a second time when we read
                         # rge options from the command line or
                         # environment variable
    push (@opts, $opt);
    
}

# "foo", "nofoo"
push(@opts, split(/ /,$optstr));

# Now actually process those pesky options...

for (@opts) {
    print $logh "Pondering option '$_'\n";

    # "docs" option to print help page
    if ((lc($_) =~ /^\s*docs\s*$/) ||
	(lc($_) =~ /^\s*docs\s*=\s*true\s*$/)) {
	# The second one is necessary becuase CUPS 1.1.15 or newer sees
	# "docs" as boolean option and modifies it to "docs=true"
        $do_docs = 1;
	next;
    }

    # "profile" option to supply a color correction profile to a
    # CUPS raster driver
    if (lc($_) =~ /^\s*profile=(\S+)\s*$/) {
	$cupscolorprofile=$1;
	$dat->{'cmd'} =~ s!\%X!profile=$cupscolorprofile!g;
	$dat->{'cmd'} =~ s!\%W! -c\"<</cupsProfile($cupscolorprofile)>>setpagedevice\"!g;
	next;
    }

    # Is the command line option limited to certain page ranges? If so,
    # mark the setting with a hash key containing the ranges
    my $optionset;
    if (s/^(even|odd|[\d,-]+)://i) {
	$optionset = "pages:$1";
    } else {
	$optionset = 'userval';
    }

    my $arg;
    if ((m!([^=]+)=\'?(.*)\'?!) || (m!([^=:]+):\'?(.*)\'?!)) {
        my ($aname, $avalue) = ($1, $2);

	if (($optionset =~ /pages/) &&
	    ($arg = argbyname($aname)) &&
	    ((!defined($arg->{'section'})) ||
	     ($arg->{'section'} !~ /^(Any|Page)Setup/))) {
	    print $logh "This option is not a \"PageSetup\" or " .
		"\"AnySetup\" option, so it cannot be restricted to " .
		"a page range.\n";
	    next;
	}

	# At first look for the "backend" option to determine the PPR
	# backend to use
	if (($aname =~ m!^backend$!i) && ($spooler eq 'ppr_int')) {
	    # Backend interface name
	    $backend = $avalue;
        } elsif ($aname =~ m!^media$!i) {

	    # Standard arguments?
	    # media=x,y,z
	    # sides=one|two-sided-long|short-edge

	    # Rummage around in the media= option for known media, source, 
	    # etc types.
	    # We ought to do something sensible to make the common manual
	    # boolean option work when specified as a media= tray thing.
	    # 
	    # Note that this fails miserably when the option value is in
	    # fact a number; they all look alike.  It's unclear how many
	    # drivers do that.  We may have to standardize the verbose
	    # names to make them work as selections, too.

            my @values = split(',',$avalue);
            for (@values) {
		my $val;
                if ($dat->{'args_byname'}{'PageSize'} and
                    $val=valbyname($dat->{'args_byname'}{'PageSize'},$_)) {
                    $dat->{'args_byname'}{'PageSize'}{$optionset} = 
                        $val->{'value'};
		    # Keep "PageRegion" in sync
		    if ($dat->{'args_byname'}{'PageRegion'} and
			$val=valbyname($dat->{'args_byname'}{'PageRegion'},
				       $_)) {
			$dat->{'args_byname'}{'PageRegion'}{$optionset} = 
			    $val->{'value'};
		    }
                } elsif ($dat->{'args_byname'}{'PageSize'} 
			 and /^Custom/) {
		    $dat->{'args_byname'}{'PageSize'}{$optionset} = $_;
		    # Keep "PageRegion" in sync
		    if ($dat->{'args_byname'}{'PageRegion'}) {
			$dat->{'args_byname'}{'PageRegion'}{$optionset} = 
			    $_;
		    }
                } elsif ($dat->{'args_byname'}{'MediaType'} and
                         $val=valbyname($dat->{'args_byname'}{'MediaType'},
					$_)) {
                    $dat->{'args_byname'}{'MediaType'}{$optionset} =
                        $val->{'value'};
                } elsif ($dat->{'args_byname'}{'InputSlot'} and
                         $val=valbyname($dat->{'args_byname'}{'InputSlot'},
					$_)) {
                    $dat->{'args_byname'}{'InputSlot'}{$optionset} = 
                        $val->{'value'};
                } elsif (lc($_) eq 'manualfeed') {
                    # Special case for our typical boolean manual
                    # feeder option if we didn't match an InputSlot above
                    if (defined($dat->{'args_byname'}{'ManualFeed'})) {
                        $dat->{'args_byname'}{'ManualFeed'}{$optionset} = 1;
                    }
                } else {
                    print $logh "Unknown \"media\" component: \"$_\".\n";
                }
            }
        } elsif ($aname =~ m!^sides$!i) {
            # Handle the standard duplex option, mostly
            if ($avalue =~ m!^two-sided!i) {
                if (defined($dat->{'args_byname'}{'Duplex'})) {
		    # We set "Duplex" to '1' here, the real argument setting
		    # will be done later
                    $dat->{'args_byname'}{'Duplex'}{$optionset} = '1';
		    # Check the binding: "long edge" or "short edge"
		    if ($avalue =~ m!long-edge!i) {
			if (defined($dat->{'args_byname'}{'Binding'})) {
			    $dat->{'args_byname'}{'Binding'}{$optionset} =
      $dat->{'args_byname'}{'Binding'}{'vals_byname'}{'LongEdge'}{'value'};
			} else {
			    $dat->{'args_byname'}{'Duplex'}{$optionset} = 
				'LongEdge';
			}
		    } elsif ($avalue =~ m!short-edge!i) {
			if (defined($dat->{'args_byname'}{'Binding'})) {
			    $dat->{'args_byname'}{'Binding'}{$optionset} =
      $dat->{'args_byname'}{'Binding'}{'vals_byname'}{'ShortEdge'}{'value'};
			} else {
			    $dat->{'args_byname'}{'Duplex'}{$optionset} = 
				'ShortEdge';
			}
		    }
                }
            } elsif ($avalue =~ m!^one-sided!i) {
                if (defined($dat->{'args_byname'}{'Duplex'})) {
		    # We set "Duplex" to '0' here, the real argument setting
		    # will be done later
                    $dat->{'args_byname'}{'Duplex'}{$optionset} = '0';
                }
            }

            # We should handle the other half of this option - the
            # BindEdge bit.  Also, are there well-known ipp/cups
            # options for Collate and StapleLocation?  These may be
            # here...

        } else {
            # Various non-standard printer-specific options
            if ($arg = argbyname($aname)) {
		if (defined(my $newvalue =
		    checkoptionvalue($dat, $aname, $avalue, 0))) {
		    # If the choice is valid, use it, otherwise
		    # ignore it.
		    $arg->{$optionset} = $newvalue;
		    # If this argument is PageSize or PageRegion,
		    # also set the other
		    syncpagesize($dat, $aname, $avalue, $optionset);
		} else {
		    # Invalid choice, make log entry
		    print $logh "Invalid choice $aname=$avalue.\n";
		}
            } elsif ($spooler eq 'ppr_int') {
                # Unknown option, pass it to PPR's backend interface
		push (@backendoptions, "$aname=$avalue");
            } else {
		# Unknown option, make log entry
		print $logh "Unknown option $aname=$avalue.\n";
	    }
        }
    } elsif (m!^([\d\.]+)x([\d\.]+)([A-Za-z]*)$!) {
	my ($w, $h, $u) = ($1, $2, $3);
	# Custom paper size
	if (($w != 0) && ($h != 0) &&
	    ($arg=argbyname("PageSize")) &&
	    (defined($arg->{'vals_byname'}{'Custom'}))) {
            $arg->{$optionset} = "Custom.${w}x${h}${u}";
	    # Keep "PageRegion" in sync
	    if ($dat->{'args_byname'}{'PageRegion'}) {
		$dat->{'args_byname'}{'PageRegion'}{$optionset} = 
		    $arg->{$optionset};
	    }
	}
    } elsif ((m!^\s*no(.+)\s*$!i) and ($arg=argbyname($1))) {
        # standard bool args:
        # landscape; what to do here?
        # duplex; we should just handle this one OK now?
	$arg->{$optionset} = 0;
    } elsif (m!^\s*(.+)\s*$!) {
        if ($arg=argbyname($1)) {
            $arg->{$optionset} = 1;
        } else {
            print $logh "Unknown boolean option \"$1\".\n";
        }
    }
}
$do_docs = 1 if( $show_docs );


## Were we called to build the PDQ driver declaration file?
my @pdqfile;
if ($genpdqfile) {
    @pdqfile = buildpdqdriver($dat, 'userval');
    open PDQFILE, $genpdqfile or
	rip_die("Cannot write PDQ driver declaration file",
		$EXIT_PRNERR_NORETRY_BAD_SETTINGS);
    print PDQFILE join('', @pdqfile);
    close PDQFILE;
    exit $EXIT_PRINTED;
}



## Set the $postpipe

# $postpipe when running as a PPR RIP
if ($spooler eq 'ppr') {
    # The PPR RIP sends the data output to /dev/fd/3 instead of to STDOUT
    if (-w "/dev/fd/3") {
	$postpipe = "| cat - > /dev/fd/3";
    } else {
	$postpipe = "| cat - >&3";
    }
}

# Set up PPR backend (if we run as a PPR interface).
if ($spooler eq 'ppr_int') {

    # Is the chosen backend installed and executable
    if (!-x "interfaces/$backend") {
	my $pwd = cwd;
	print $logh "The backend interface $pwd/interfaces/$backend " .
	    "does not exist/is not executable!\n";
	rip_die ("The backend interface $pwd/interfaces/$backend " .
		 "does not exist/is not executable!",
		 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
    }

    # foomatic-rip cannot use foomatic-rip as backend
    if ($backend eq "foomatic-rip") {
	print $logh "\"foomatic-rip\" cannot use itself as backend " .
	    "interface!\n";
	ppr_die ($ppr_printer,
		 "\"foomatic-rip\" cannot use itself as backend interface!",
		 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
    }

    # Put the backend interface into the $postpipe
    $postpipe = "| ( interfaces/$backend \"$ppr_printer\" ".
	"\"$ppr_address\" \"" . join(" ",@backendoptions) .
	"\" \"$ppr_jobbreak\" \"$ppr_feedback\" " .
	"\"$ppr_codes\" \"$ppr_jobname\" \"$ppr_routing\" " .
	"\"$ppr_for\" \"\" )";

}

# CUPS and PDQ have their own backends, they do not need a $postpipe
if (($spooler eq 'cups') || ($spooler eq 'pdq')) {
    # No $postpipe for CUPS or PDQ, even if one is defined in the PPD file
    $postpipe = "";
}

# CPS needs always a $postpipe, set the default one for local printing
# if none is set
if (($spooler eq 'cps') && !$postpipe) {
    $postpipe = "| cat - > \$LPDDEV";
}

if ($postpipe) {
    print $logh "${added_lf}Output will be redirected to:\n$postpipe${added_lf}\n";
}



## Print documentation page when asked for
my ($docgeneratorhandle, $docgeneratorpid,$retval);
if ($do_docs) {
    # Don't print the supplied files, STDIN will be redirected to the
    # documentation page generator
    @filelist = ("<STDIN>");
    # Start the documentation page generator
    ($docgeneratorhandle, $docgeneratorpid) =
	getdocgeneratorhandle($dat);
    if ($retval != $EXIT_PRINTED) {
	rip_die ("Error opening documentation page generator",
		 $retval);
    }
    # Read the further data from the documentation page generator and
    # not from STDIN
    if (!close STDIN && $! != $ESPIPE) {
	rip_die ("Couldn't close STDIN",
		 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
    }
    if (!open (STDIN, "<&$docgeneratorhandle")) {
	rip_die ("Couldn't dup \$docgeneratorhandle",
		 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
    }
	if( $show_docs ){
		while( <$docgeneratorhandle> ){
			print;
		}
		exit(0);
	}
}




## In debug mode save the data supposed to be fed into the
## renderer also into a file, reset the file here

if ($debug) {
    system("> ${logfile}.ps");
}


	    
## From here on we have to repeat all the rest of the program for
## every file to print

for $file (@filelist) {

    print $logh
"${added_lf}================================================\n${added_lf}".
"File: $file\n${added_lf}" .
"================================================\n${added_lf}";



    ## If we do not print standard input, open the file to print
    if ($file ne "<STDIN>") {
	if (! -r $file) {
	    print $logh "File $file missing or not readable, skipping.\n";
	    next;
	}
        close STDIN;
        open STDIN, "< $file" || do {
	    print $logh "Cannot open $file, skipping.\n";
	    next;
	}
    }



    ## Do we have a raw queue
    if ($dontparse == 2) {
	# Raw queue, simply pass the input into the $postpipe (or to STDOUT
	# when there is no $postpipe)
	print $logh "Raw printing, executing \"cat $postpipe\"${added_lf}\n";
	system("cat $postpipe");
	next;
    }



    ## First, for arguments with a default, stick the default in as
    ## the initial value for the "header" option set, this option set
    ## consists of the PPD defaults, the options specified on the
    ## command line, and the options set in the header part of the
    ## PostScript file (all before the first page begins).

    copyoptions($dat, 'userval', 'header');



    ## Next, examine the PostScript job for traces of command-line and
    ## JCL options. PPD-aware applications and spoolers stuff option
    ## settings directly into the file, they do not necessarily send
    ## PPD options by the command line. Also stuff in PostScript code
    ## to apply option settings given by the command line and to set
    ## the defaults given in the PPD file.

    # Examination strategy: read lines from STDIN until the first
    # %%Page: comment appears and save them as @psheader. This is the
    # page-independent header part of the PostScript file. The
    # PostScript interpreter (renderer) must execute this part once
    # before rendering any assortment of pages. Then pages can be
    # printed in any arbitrary selection or order. All option
    # settings we find here will be collected in the default option
    # set for the RIP command line.

    # Now the pages will be read and sent to the renderer, one after
    # the other. Every page is read into memory until the
    # %%EndPageSetup comment appears (or a certain amount of lines was
    # read). So we can get option settings only valid for this
    # page. If we have such settings we set them in the modified
    # command set for this page.

    # If the renderer is not running yet (first page) we start it with
    # the command line built from the current modified command set and
    # send the first page to it, in the end we leave the renderer
    # running and keep input and output pipes open, so that it can
    # accept further pages. If the renderer is still running from
    # the previous page and the current modified command set is the
    # same as the one for the previous page, we send the page. If
    # the command set is different, we close the renderer, re-start
    # it with the command line built from the new modified command
    # set, send the header again, and then the page.

    # After the last page the trailer (%%Trailer) is sent.

    # The output pipe of this program stays open all the time so that
    # the spooler does not assume that the job has finished when the
    # renderer is re-started.

    # Non DSC-conforming documents will be read until a certain line
    # number is reached. Command line or JCL options inserted later
    # will be ignored.

    # If options are implemented by PostScript code supposed to be
    # stuffed into the job's PostScript data we stuff the code for all
    # these options into our job data, So all default settings made in
    # the PPD file (the user can have edited the PPD file to change
    # them) are taken care of and command line options get also
    # applied. To give priority to settings made by applications we
    # insert the options's code in the beginnings of their respective
    # sections, so that sommething, which is already inserted, gets
    # executed after our code. Missing sections are automatically
    # created. In non-DSC-conforming files we insert the option code
    # in the beginning of the file. This is the same policy as used by
    # the "pstops" filter of CUPS.

    # If CUPS is the spooler, the option settings were already
    # inserted by the "pstops" filter, so we don't insert them
    # again. The only thing we do is correcting settings of numerical
    # options when they were set to a value not available as choice in
    # the PPD file, As "pstops" does not support "real" numerical
    # options, it sees these settings as an invalid choice and stays
    # with the default setting. In this case we correct the setting in
    # the first occurence of the option's code, as this one is the one
    # added by CUPS, later occurences come from applications and
    # should not be touched.

    # If the input is not PostScript (if there is no "%!" after
    # $maxlinestopsstart lines) a file conversion filter will
    # automatically be applied to the incoming data, so that we will
    # process the resulting PostScript here. This way we have always
    # PostScript data here and so we can apply the printer/driver
    # features described in the PPD file.

    # Supported file conversion filters are "a2ps", "enscript",
    # "mpage", and spooler-specific filters. All filters convert
    # plain text to PostScript, "a2ps" also other formats. The
    # conversion filter is always used when one prints the
    # documentation pages, as they are created as plain text,
    # when CUPS is the spooler "pstops" is executed after the
    # filter so that the default option settings from the PPD file
    # and CUPS-specific options as N-up get applied. On regular
    # printouts one gets always PostScript when CUPS or PPR is
    # the spooler, so the filter is only used for regular
    # printouts under LPD, LPRng, GNUlpr or without spooler.

    my $maxlines = 1000;            # Maximum number of lines to be read
                                    # when the documenent is not
                                    # DSC-conforming. "$maxlines = 0"
                                    # means that all will be read
                                    # and examined. If it is
                                    # discovered that the input file
                                    # is DSC-conforming, this will
                                    # be set to 0.

    my $maxlinestopsstart = 200;    # That many lines are allowed until the
                                    # "%!" indicating PS comes. These
                                    # additional lines in the
                                    # beginning are usually JCL
                                    # commands. The lines will be
                                    # ignored by our parsing but
                                    # passed through.

    my $maxlinesforpageoptions=200; # Unfortunately, CUPS does not bracket
                                    # "PageSetup" option with
                                    # "%%BeginPageSetup" and
                                    # "%%EndPageSetup", so the options
                                    # can simply stand after the
                                    # page header and before the
                                    # page code, without special
                                    # marking. So buffer this amount
                                    # of lines before printing the
                                    # page to check for options.

    my $maxnondsclinesinheader=1000; # If there is a block of more lines
                                    # than this in the document
                                    # header which is not in the
                                    # "%%BeginProlog...%%EndProlog"
                                    # or
                                    # "%%BeginSetup...%%EndSetup"
                                    # sections, the document is not
                                    # considered as DSC-conforming
                                    # and the rest gets passed
                                    # through to the renderer without
                                    # further parsing for options.

    my $nondsclines = 0;            # Amount of lines found which are not in
                                    # a section (see 
                                    # $maxnondsclinesinheader).

    my $nonpslines = 0;             # lines before "%!" found yet.

    my $more_stuff = 1;             # there is more stuff in stdin.

    my $linect = 0;                 # how many lines have we examined?

    my $onelinebefore = "";         # The line before the current line
                                    # (Non-DSC comments are ignored)

    my $twolinesbefore = "";        # The line two lines before the current 
                                    # line (Non-DSC comments are ignored)

    my @psheader = ();              # The header of the PostScript file, 
                                    # to be sent after each start of the
                                    # renderer

    my @psfifo = ();                # The input FIFO, data which we have
                                    # pulled from stdin for examination,
                                    # but not sent to the renderer yet.

    my $passthru = 0;               # 0: write data into @psfifo; 1: pass
                                    # data directly to the renderer

    my $isdscjob = 0;               # Is the job DSC conforming

    my $inheader = 1;               # Are we still in the header, before
                                    # first "%%Page:" comment?

    my $optionset = 'header';       # Where do the option settings, which 
                                    # we have found, go?

    my $optionsalsointoheader = 0;  # 1: We are in a "%%BeginSetup...
                                    # %%EndSetup" section after the first
                                    # "%%Page:..." line (OpenOffice.org
                                    # does this and intends the options here
                                    # apply to the whole document and not
                                    # only to the current page). We have to
                                    # add all lines also to the end of the
                                    # @psheader now and we have to set
                                    # non-PostScript options also in the
                                    # "header" optionset. 0: otherwise.

    my $nestinglevel = 0;           # Are we in the main document (0) or
                                    # in an embedded document bracketed by
                                    # "%%BeginDocument" and "%%EndDocument"
                                    # (>0) We do not parse the PostScript
                                    # in an embedded document.

    my $inpageheader = 0;           # Are we in the header of a page,
                                    # between "%%BeginPageSetup" and
                                    # "%%EndPageSetup" (1) or not (0).

    my $lastpassthru = 0;           # State of $passthru in previous line
                                    # (to allow debug output when $passthru
                                    # switches.

    my $ignorepageheader = 0;       # Will be set to 1 as soon as active 
                                    # code (not between "%%BeginPageSetup" 
                                    # and "%%EndPageSetup") appears after a
                                    # "%%Page:" comment. In this case
                                    # "%%BeginPageSetup" and
                                    # "%%EndPageSetup" is not allowed any 
                                    # more on this page and will be ignored.
                                    # Will be set to 0 when a new "%%Page:" 
                                    # comment appears.

    my $printprevpage = 0;          # We set this when encountering
                                    # "%%Page:" and the previous page is not
                                    # printed yet. Then it will be printed and 
                                    # the new page will be prepared in the
                                    # next run of the loop (we don't read a
                                    # new line and don't increase the
                                    # $linect then).

    $fileconverterhandle = undef;   # File handle to the fileconverter process

    $fileconverterpid = 0;          # PID of the fileconverter process

    $rendererhandle = undef;        # File handle to the renderer process

    $rendererpid = 0;               # PID of the renderer process

    my $prologfound = 0;            # Did we find the
                                    # "%%BeginProlog...%%EndProlog" section?

    my $setupfound = 0;             # Did we find the
                                    # "%%BeginSetup...%%EndSetup" section?

    my $pagesetupfound = 0;         # special page setup handling needed

    my $inprolog = 0;               # We are between "%%BeginProlog" and
                                    # "%%EndProlog".

    my $insetup = 0;                # We are between "%%BeginSetup" and
                                    # "%%EndSetup".

    my $infeature = 0;              # We are between "%%BeginFeature" and
                                    # "%%EndFeature".

    my $postscriptsection = 'jclsetup'; # In which section of the PostScript
                                    # file are we currently?

    $nondsclines = 0;            # Number of subsequent lines found which
                                    # are at a non-DSC-conforming place,
                                    # between the sections of the header.

    my $optionreplaced = 0;         # Will be set to 1 when we are in an
                                    # option ("%%BeginFeature...
                                    # %%EndFeature") which we have replaced.

    $jobhasjcl = 0;                 # When the job does not start with
                                    # PostScript directly, but is a
                                    # PostScript job, we set this to 1
                                    # to avoid adding the JCL options
                                    # for the second time.

    my $insertoptions = 1;          # If we find out that a file with
                                    # a DSC magic string
                                    # ("%!PS-Adobe-") is not really
                                    # DSC-conforming, we insert the
                                    # options directly after the line
                                    # with the magic string. We use
                                    # this variable to store the
                                    # number of the line with the
                                    # magic string.

    my $currentpage = 0;            # The page which we are currently
                                    # printing.

    my $ooo110 = 0;                 # Flag to work around an application 
                                    # bug.

    if ($dontparse) {
	# We do not parse the PostScript to find Foomatic options, we check
	# only whether we have PostScript.
	$maxlines = 1;
    }

    print $logh "Reading PostScript input ...\n";

    my $line;                       # Line to be read from stdin
    do {
	my $ignoreline = 0;         # Comment line to be ignored when
	                            # determining the last active line 
	                            # and the one before the last

	if (($printprevpage) || ($line=<STDIN>)) {

	    if ($linect == $nonpslines) {
		# In the beginning should be the postscript leader,
                # sometimes after some JCL commands
		if ($line !~ m/^.?%!/) { # There can be a Windows control 
		                         # character before "%!"
		    $nonpslines ++;
		    if ($maxlines == $nonpslines) {
			$maxlines ++;
		    }
		    $jobhasjcl = 1;
		    if ($nonpslines > $maxlinestopsstart) {
			# This is not a PostScript job, we must convert it
			print $logh "${added_lf}Job does not start with \"%!\", " . 
			     "is it PostScript?\n" .
			     "Starting file converter\n";
			# Reset all variables but conserve the data which
			# we have already read.
			$jobhasjcl = 0;
			$linect =  0;
			$nonpslines = 1; # Take into account that the line
			                 # of this run of the loop will be
			                 # put into @psheader, so the
			                 # first line read by the file
			                 # converter is already the second
			                 # line.
			$maxlines = 1001;
			$onelinebefore = "";
			$twolinesbefore = "";
			my $alreadyread = join('', @psheader, @psfifo) . 
			    $line;
			$line = "";
			@psheader = ();
			@psfifo = ();
			# Start the file conversion filter
			if (!$fileconverterpid) {
			    ($fileconverterhandle, $fileconverterpid) =
				getfileconverterhandle
				($dat, $alreadyread);
			    if ($retval != $EXIT_PRINTED) {
				rip_die ("Error opening file converter",
					 $retval);
			    }
			} else {
			    rip_die("File conversion filter probably " .
				    "crashed",
				    $EXIT_JOBERR);
			}
			# Read the further data from the file converter and
			# not from STDIN
			if (!close STDIN && $! != $ESPIPE) {
			    rip_die ("Couldn't close STDIN",
				     $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
			}
			if (!open (STDIN, "<&$fileconverterhandle")) {
			    rip_die ("Couldn't dup \$fileconverterhandle",
				     $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
			}
		    }
		} else {
		    # Do we have a DSC-conforming document?
		    if ($line =~ m/^.?%!PS-Adobe-/) {
			# Do not stop parsing the document
			if (!$dontparse) {
			    $maxlines = 0;
			    $isdscjob = 1;
			    $insertoptions = $linect + 1;
			    # We have written into @psfifo before,
			    # now we continue in @psheader and move
			    # over the data which is already in @psfifo
			    push (@psheader, @psfifo);
			    @psfifo = ();
			}
			print $logh 
			    "--> This document is DSC-conforming!\n";
		    } else {
			# Job is not DSC-conforming, stick in all PostScript
			# option settings in the beginning
			$line .= makeprologsection($dat, $optionset, 1);
			$line .= makesetupsection($dat, $optionset, 1);
			$line .= makepagesetupsection($dat, $optionset, 1);
			$prologfound = 1;
			$setupfound = 1;
			$pagesetupfound = 1;
		    }
		}
	    } else {
		if ($line =~ m/^\s*\%\%BeginDocument[: ]/) {
		    # Beginning of an embedded document
		    # Note that Adobe Acrobat has a bug and so uses
		    # "%%BeginDocument " instead of "%%BeginDocument:"
		    $nestinglevel ++;
		    print $logh "Embedded document, " .
			"nesting level now: $nestinglevel\n";
		} elsif (($line =~ m/^\s*\%\%EndDocument/) &&
			 ($nestinglevel > 0)) {
		    # End of an embedded document
		    $nestinglevel --;
		    print $logh "End of Embedded document, " .
			"nesting level now: $nestinglevel\n";
		} elsif (($line =~ m/^\s*\%\%Creator[: ](.*)$/) &&
			 ($nestinglevel == 0)) {
		    # Here we set flags to treat particular bugs of the
		    # PostScript produced by certain applications
		    my $creator = $1;
		    if ($creator =~ /^\s*OpenOffice.org\s+1.1.\d+\s*$/) {
			# OpenOffice.org 1.1.x
			# The option settings supposed to affect the
			# whole document are put into the "%%PageSetup"
			# section of the first page
			print $logh "Document created with " .
			    "OpenOffice.org 1.1.x\n";
			$ooo110 = 1;
		    }
		} elsif (($line =~ m/^\%\%BeginProlog/) &&
			 ($nestinglevel == 0)) {
		    # Note: Below is another place where a "Prolog"
		    # section start will be considered. There we assume
		    # start of the "Prolog" if the job is DSC-Conformimg,
		    # but an arbitrary comment starting with "%%Begin", but
		    # not a comment explicitly treated here, is found. This
		    # is done because many "dvips" (TeX/LaTeX) files miss
		    # the "%%BeginProlog" comment.
		    # Beginning of Prolog
		    print $logh "${added_lf}-----------\nFound: \%\%BeginProlog\n";
		    $inprolog = 1;
		    $postscriptsection = 'prolog' if $inheader;
		    $nondsclines = 0;
		    # Insert options for "Prolog"
		    if (!$prologfound) {
			$line .= makeprologsection($dat, $optionset, 0);
		    }
		    $prologfound = 1;
		} elsif (($line =~ m/^\%\%EndProlog/) &&
			 ($nestinglevel == 0)) {
		    # End of Prolog
		    print $logh "Found: \%\%EndProlog\n";
		    $inprolog = 0;
		    $insertoptions = $linect + 1;
		} elsif (($line =~ m/^\%\%BeginSetup/) &&
			 ($nestinglevel == 0)) {
		    # Beginning of Setup
		    print $logh "${added_lf}-----------\nFound: \%\%BeginSetup\n";
		    $insetup = 1;
		    # We need to distinguish with the $inheader variable
		    # here whether we are in the header or on a page, as
		    # OpenOffice.org inserts a "%%BeginSetup...%%EndSetup"
		    # section after the first "%%Page:..." line and assumes
		    # this section to be valid for all pages.
		    $postscriptsection = 'setup' if $inheader;
		    $nondsclines = 0;
		    if ($inheader) {
			# If there was no "Prolog" but there are
			# options for the "Prolog", push a "Prolog"
			# with these options onto the @psfifo here
			if (!$prologfound) {
			    # "Prolog" missing, insert it here
			    $line = makeprologsection($dat, $optionset, 1) .
				$line;
			    # Now we have a "Prolog"
			    $prologfound = 1;
			}
			# Insert options for "DocumentSetup" or "AnySetup"
			if ($spooler ne 'cups') {
				# For non-CUPS spoolers or no spooler at all, we leave
				# everything as it is.
				if (!$setupfound) {
				    $line .= makesetupsection($dat, $optionset, 0);
				}
				$setupfound = 1;
			}
		    } else {
			# Found option settings must be stuffed into both
			# the header and the currrent page now. They will
			# be written into both the "header" and the
			# "currentpage" optionsets and the PostScript code
			# lines of this section will not only go into the
			# output stream, but also added to the end of the
			# @psheader, so that they get repeated (to preserve
			# the embedded PostScript option settings) on a 
			# restart of the renderer due to command line 
			# option changes
			$optionsalsointoheader = 1;
			print $logh "\"%%BeginSetup\" in page header\n";
		    }
		} elsif (($line =~ m/^\%\%EndSetup/) &&
			 ($nestinglevel == 0)) {
		    # End of Setup
		    print $logh "Found: \%\%EndSetup\n";
		    $insetup = 0;
		    if ($inheader) {
		        if ($spooler eq 'cups') {
			    # In case of CUPS, we must insert the
			    # accounting stuff just before the
			    # %%EndSetup comment in order to leave any
			    # EndPage procedures that have been
			    # defined by either the pstops filter or
			    # the PostScript job itself fully
			    # functional.
			    if (!$setupfound) {
				$line = makesetupsection($dat, 
				                         $optionset, 0) . 
	                                $line;	 
			    }
			    $setupfound = 1;
		        }
			$insertoptions = $linect + 1;
		    } else {
			# The "%%BeginSetup...%%EndSetup" which
			# OpenOffice.org has inserted after the first
			# "%%Page:..." line ends here, so the following
			# options go only onto the current page again
			$optionsalsointoheader = 0;
		    }
		} elsif (($line =~ m/^\%\%Page:(.*)$/) &&
			 ($nestinglevel == 0)) {
		    if ((!$lastpassthru) && (!$inheader)) {
			# In the last line we were not in passthru mode,
			# so the last page is not printed. Prepare to do
			# it now.
			$printprevpage = 1;
			# Print the previous page
			$passthru = 1;
			print $logh "New page found but previous not " . 
			    "printed, print it now.\n";
		    } else {
			# The previous page is printed, so we can prepare
			# the current one
			$printprevpage = 0;
			print $logh "${added_lf}-----------\nNew page: $1\n";
			# Count pages
			$currentpage ++;
			# We consider the beginning of the page already as
			# page setup section, as some apps do not use
			# "%%PageSetup" tags.
			$postscriptsection = 'pagesetup';
			# Save PostScript state before beginning the page
			#$line .= "/foomatic-saved-state save def\n";
			# Here begins a new page
			if ($inheader) {
			    # Here we add some stuff which still belongs
			    # into the header
			    my $stillforheader;
			    # If there was no "Setup" but there are
			    # options for the "Setup", push a "Setup"
			    # with these options onto the @psfifo here
			    if (!$setupfound) {
				# "Setup" missing, insert it here
				$stillforheader = 
				    makesetupsection($dat, $optionset, 1) .
				    $stillforheader;
				# Now we have a "Setup"
				$setupfound = 1;
			    }
			    # If there was no "Prolog" but there are
			    # options for the "Prolog", push a "Prolog"
			    # with these options onto the @psfifo here
			    if (!$prologfound) {
				# "Prolog" missing, insert it here
				$stillforheader = 
				    makeprologsection($dat, $optionset, 1) .
				    $stillforheader;
				# Now we have a "Prolog"
				$prologfound = 1;
			    }
			    # Now we push this onto the header
			    push (@psheader, $stillforheader);
			    # The first page starts, so the header ends
			    $inheader = 0;
			    $nondsclines = 0;
			    # Option setting should go into the
			    # page-specific option set now
			    $optionset = 'currentpage';
			} else {
			    # Restore PostScript state after completing the
			    # previous page:
			    # 
			    #   foomatic-saved-state restore
			    #   %%Page: ...
			    #   /foomatic-saved-state save def
			    #
			    # Print this directly, so that if we need to
			    # restart the renderer for this page due to
			    # a command line change this is done under the
			    # old instance of the renderer
			    #print $rendererhandle
			    #    "foomatic-saved-state restore\n";

			    # Save the option settings of the previous page
			    copyoptions($dat, 'currentpage',
					'previouspage');
			    deleteoptions($dat, 'currentpage');
			}
			# Initialize the option set
			copyoptions($dat, 'header', 'currentpage');
			# Set command line options which apply only
			# given pages
			setoptionsforpage($dat, 'currentpage', $currentpage);
			$pagesetupfound = 0;
			if ($spooler eq 'cups') {
			    # Remove the "notfirst" flag from all options
			    # forseen for the "PageSetup" section, because
			    # when these are numerical options for CUPS.
			    # they have to be set to the correct value
			    # for every page
			    for my $arg (@{$dat->{'args'}}) {
				if (($arg->{'section'} eq 'PageSetup') &&
				    (defined($arg->{'notfirst'}))) {
				    delete($arg->{'notfirst'});
				}
			    }
			}
			# Insert PostScript option settings
			# (options for section "PageSetup".
			if ($isdscjob) {
			    $line .= 
				makepagesetupsection($dat, $optionset,
						     0);
			    $pagesetupfound = 1;
			}
			# Now the page header comes, so buffer the data,
			# because we must perhaps shut down and restart 
			# the renderer
			$passthru = 0;
			$ignorepageheader = 0;
			$optionsalsointoheader = 0;
		    }
		} elsif (($line =~ m/^\%\%BeginPageSetup/) &&
			 ($nestinglevel == 0) &&
			 (!$ignorepageheader))  {
		    # Start of the page header, up to %%EndPageSetup
		    # nothing of the page will be drawn, page-specific
		    # option settngs (as letter-head paper for page 1)
		    # go here
		    print $logh "${added_lf}Found: \%\%BeginPageSetup\n";
		    $passthru = 0;
		    $inpageheader = 1;		
		    $postscriptsection = 'pagesetup';
		    if (($ooo110) && ($currentpage == 1)) {
			$optionsalsointoheader = 1;
		    } else {
			$optionsalsointoheader = 0;
		    }
		} elsif (($line =~ m/^\%\%EndPageSetup/) &&
			 ($nestinglevel == 0) &&
			 (!$ignorepageheader)) {
		    # End of the page header, the page is ready to be
		    # printed
		    print $logh "Found: \%\%EndPageSetup\n";
		    print $logh "End of page header\n";
		    # We cannot for sure say that the page header ends here
		    # OpenOffice.org puts (due to a bug) a "%%BeginSetup...
		    # %%EndSetup" section after the first "%%Page:...". It
		    # is possible that CUPS inserts a "%%BeginPageSetup...
		    # %%EndPageSetup" before this section, which means that
		    # the options in the "%%BeginSetup...%%EndSetup" section
		    # are after the "%%EndPageSetup", so we continue for
		    # searching options up to the buffer size limit
		    # $maxlinesforpageoptions.
		    $passthru = 0;
		    $inpageheader = 0;
		    $optionsalsointoheader = 0;
		} elsif ((($line =~ m/^\%\%(BeginFeature):\s*\*?([^\*\s=]+)\s+()(\S[^\r\n]*)\r?\n?$/) ||
			  ($line =~ m/^\s*\%\%\s*(FoomaticRIPOptionSetting):\s*([^\*\s=]+)\s*=\s*(\@?)([^\@\s][^\r\n]*)\r?\n?$/)) &&
			 ($nestinglevel == 0) &&
			 (!$optionreplaced) &&
			 ((!$passthru) || (!$isdscjob))) {
		    my ($linetype, $option, $fromcomposite, $value) = 
			($1, $2, $3, $4);

		    # Mark that we are in a "Feature" section
		    if ($linetype eq 'BeginFeature') {
			$infeature = 1;
		    }
		    
		    # OK, we have an option.  If it's not a
		    # *ostscript-style option (ie, it's command-line or
		    # JCL) then we should note that fact, since the
		    # attribute-to-filter option passing in CUPS is kind of
		    # funky, especially wrt boolean options.  

		    print $logh "Found: $line";
		    if (my $arg=argbyname($option)) {
			print $logh "   Option: $option=" .
			    ($fromcomposite ? "From" : "") . $value;
			if (($spooler eq 'cups') &&
			    ($linetype eq 'BeginFeature') &&
			    (!defined($arg->{'notfirst'})) &&
			    ($arg->{$optionset} ne $value) &&
			    (($inheader) ||
			     ($arg->{section} eq 'PageSetup'))) {
			    # We have the first occurence of an
			    # option setting and the spooler is CUPS,
			    # so this setting is inserted by "pstops".
			    # The value from the command line was not
			    # inserted by "pstops" so it seems to be
			    # not under the choices in the PPD.
			    # Possible reasons:
			    #
			    # - "pstops" ignores settings of numerical
			    #   or string options which are not one of 
			    #   the choices in the PPD file, and inserts
			    #   the default value instead.
			    #
			    # - On the command line an option was applied
			    #   only to selected pages:
			    #    "-o <page ranges>:<option>=<values>
			    #   This is not supported by CUPS, so not
			    #   taken care of by "pstops".
			    #
                            # We must fix this here by replacing the setting
			    # inserted by "pstops" with the exact setting
			    # given on the command line.
			    
			    # $arg->{$optionset} is already 
			    # range-checked, so do not check again here
			    # Insert DSC comment
			    my $dest = ((($inheader) && ($isdscjob)) ?
					\@psheader : \@psfifo);
			    push(@{$dest},
				 "%%BeginFeature: " .
				 "*$option $arg->{$optionset}\n");
			    my $val;
			    if ($arg->{'style'} eq 'G') {
				# PostScript option, insert the code
				if ($arg->{'type'} eq 'bool') {
				    # Boolean option
				    if (defined($arg->{$optionset}) && 
					$arg->{$optionset} == 1) {
					push(@{$dest}, $arg->{'proto'} . "\n");
				    } elsif ($arg->{'protof'}) {
					push(@{$dest}, $arg->{'protof'}. "\n");
				    }
				} elsif ((($arg->{'type'} eq 'enum') ||
				     ($arg->{'type'} eq 'string') ||
				     ($arg->{'type'} eq 'password')) &&
				    (defined($val =
					     $arg->{'vals_byname'}{$arg->{$optionset}}))) {
				    # Enumerated choice of string or enum 
				    # option
				    push(@{$dest}, $val->{'driverval'} . "\n");
				} elsif ((($arg->{'type'} eq 'string') ||
				     ($arg->{'type'} eq 'password')) &&
				    ($arg->{$optionset} eq 'None')) {
				    # 'None' is mapped to the empty string in
				    # string options
				    my $driverval = $arg->{'proto'};
				    $driverval =~ s/\%s//g;
				    push(@{$dest}, $driverval . "\n");
				} else {
				    # Setting for numerical or string option
				    # which is not under the enumerated choices
				    my $sprintfproto = $arg->{'proto'};
				    $sprintfproto =~ s/\%(?!s)/\%\%/g;
				    push(@{$dest},
					 sprintf($sprintfproto,
						 $arg->{$optionset}) .
					 "\n");
				}
			    } else {
				# Command line or JCL option
				push(@{$dest},
				     "%% FoomaticRIPOptionSetting: " .
				     "$option=$arg->{$optionset}\n");
			    }
			    print $logh " --> Correcting numerical/string " .
				"option to $option=$arg->{$optionset}" .
				" (Command line argument)\n";
			    # We have replaced this option on the 
			    # FIFO
			    $optionreplaced = 1;
			}
			# Mark that we have already found this option
			$arg->{'notfirst'} = 1;
			if (!$optionreplaced) {
			    if ($arg->{'style'} ne 'G') {
				# "Controlled by '<Composite>'" setting of
				# a member option of a composite option
				if ($fromcomposite) {
				    $value = "From$value";
				}
				# Non-PostScript option
				# Check whether it is valid
				if (defined(my $newvalue =
					    checkoptionvalue($dat, $option,
							     $value, 0))) {
				    print $logh " --> Setting option\n";
				    # Valid choice, set it.
				    $arg->{$optionset} = $newvalue;
				    if ($optionsalsointoheader) {
					$arg->{'header'} = $newvalue;
				    }
				    if (($arg->{'type'} eq 'enum') &&
					(($option eq 'PageSize') ||
					 ($option eq 'PageRegion')) &&
					($newvalue =~ /^Custom/) &&
					($linetype eq 
					 'FoomaticRIPOptionSetting')) {
					# Custom page size
					$twolinesbefore =~
					    /^\s*([\d\.]+)\s+([\d\.]+)\s+([\d\.]+)\s+([\d\.]+)\s+([\d\.]+)\s*$/;
					my ($w, $h) = ($1, $2);
					if (($w) && ($h) && 
					    ($w != 0) && ($h != 0)) {
					    $newvalue = "$newvalue.${w}x$h";
					    $arg->{$optionset} = $newvalue;
					    if ($optionsalsointoheader) {
						$arg->{'header'} =
						    $newvalue;
					    }
					}
				    }
				    # For a composite option insert the
				    # code from the member options with
				    # current setting "From<composite>"
				    # The code from the member options
				    # is chosen according to the setting 
				    # of the composite option.
				    if (($arg->{'style'} eq 'X') &&
					($linetype eq 
					 'FoomaticRIPOptionSetting')) {
					buildcommandline($dat, $optionset);
					$line .= $arg->{$postscriptsection};
				    }
				    # If this argument is PageSize or 
				    # PageRegion, also set the other
				    syncpagesize($dat, $option, $newvalue, 
						 $optionset);
				    if ($optionsalsointoheader) {
					syncpagesize($dat, $option, 
						     $newvalue, 'header');
				    }
				} else {
				    # Invalid option, log it.
				    print $logh " --> Invalid option " .
					"setting found in job\n";
				}
			    } elsif ($fromcomposite) {
				# PostScript option, but we have to look up
				# the PostScript code to be inserted from
				# the setting of a composite option, as this
				# option is set to "Controlled by 
				# '<Composite>'".
				# Set the option
				if (defined(my $newvalue =
					    checkoptionvalue
					    ($dat, $option,
					     "From$value", 0))) {
				    print $logh " --> Looking up setting " .
					"in composite option '$value'\n";
				    # Valid choice, set it.
				    $arg->{$optionset} = $newvalue;
				    if ($optionsalsointoheader) {
					$arg->{'header'} = $newvalue;
				    }
				    # Update composite options
				    buildcommandline($dat, $optionset);
				    # Substitute PostScript comment by
				    # the real code
				    $line = $arg->{'compositesubst'};
				} else {
				    # Invalid option, log it.
				    print $logh " --> Invalid option " .
					"setting found in job\n";
				}
			    } else {
				# it is a PostScript style option with
				# the code readily inserted, no option
				# for the renderer command line/JCL to set,
				# no lookup of a composite option needed,
				# so nothing to do here...
				print $logh 
				    " --> Option will be set by " .
				    "PostScript interpreter\n";
			    }
			}
		    } else {
			# This option is unknown to us.  WTF?
			print $logh "Unknown option $option=$value found " .
			    "in the job\n";
		    }
		} elsif (($line =~ m/^\%\%EndFeature/) &&
			 ($nestinglevel == 0)) {
		    # End of Feature
		    $infeature = 0;
		    # If the option setting was replaced, it ends here, too,
		    # end the next option is not necessarily also replaced.
		    $optionreplaced = 0;
		} elsif (($line =~ m/^\%\%Begin/) &&
			 ($isdscjob) &&
			 (!$prologfound) &&
			 ($nestinglevel == 0)) {
		    # In some PostScript files (especially when generated
		    # by "dvips" of TeX/LaTeX) the "%%BeginProlog" is
		    # missing, so assume that it was before the current
		    # line (the first line starting with "%%Begin".
		    print $logh "Job claims to be DSC-conforming, but " . 
			"\"%%BeginProlog\" was missing before first " .
			"line with another \"%%Begin...\" comment " .
			"(is this a TeX/LaTeX/dvips-generated PostScript " .
			"file?). Assuming start of \"Prolog\" here.\n";
		    # Beginning of Prolog
		    $inprolog = 1;
		    $nondsclines = 0;
		    # Insert options for "Prolog" before the current line
		    if (!$prologfound) {
			$line =
			    "%%BeginProlog\n" .
			    makeprologsection($dat, $optionset, 0) .
			    $line;
		    }
		    $prologfound = 1;
		} elsif (($line =~ m/^\s*\%/) || ($line =~ m/^\s*$/)) {
		    # This is an unknown PostScript comment or a blank line,
		    # no active code
		    $ignoreline = 1;
		} else {
		    # This line is active PostScript code
		    if ($inheader) {
			if ((!$inprolog) && (!$insetup)) {
			    # Outside the "Prolog" and "Setup" section
			    # a correct DSC-conforming document has no
			    # active PostScript code, so consider the
			    # file as non-DSC-conforming when there are
			    # too many of such lines.
			    $nondsclines ++;
			    if ($nondsclines > $maxnondsclinesinheader) {
				# Consider document as not DSC-conforming
				print $logh "This job seems not to be " .
				    "DSC-conforming, DSC-comment for " .
				    "next section not found, stopping " .
				    "to parse the rest, passing it " .
				    "directly to the renderer.\n";
				# Stop scanning for further option settings
				$maxlines = 1;
				$isdscjob = 0;
				# Insert defaults and command line settings
				# in the beginning of the job or after the
				# last valid section
				splice(@psheader, $insertoptions, 0,
				       ($prologfound ? () :
					makeprologsection($dat, $optionset, 
							  1)),
				       ($setupfound ? () :
					makesetupsection($dat, $optionset,
							 1)),
				       ($pagesetupfound ? () :
					makepagesetupsection($dat,
							     $optionset, 
							     1)));
				$prologfound = 1;
				$setupfound = 1;
				$pagesetupfound = 1;
			    }
			}
		    } else {
			if (!$inpageheader) {
			    # PostScript code inside a page, but not between
			    # "%%BeginPageSetup" and "%%EndPageSetup", so 
			    # we are perhaps already drawing onto a page now
			    if ($onelinebefore =~ m/^\%\%Page:/) {
				print $logh "No page header or page " .
				    "header not DSC-conforming\n";
			    }
			    # Stop buffering lines to search for options 
			    # placed not DSC-conforming
			    if (scalar(@psfifo) >= 
				$maxlinesforpageoptions) {
				print $logh "Stopping search for " .
				    "page header options\n";
				$passthru = 1;
				# If there comes a page header now, ignore 
				# it
				$ignorepageheader = 1;
				$optionsalsointoheader = 0;
			    }
			}
		    }
		}
	    }
	    
	    # Debug info
	    if ($lastpassthru != $passthru) {
		if ($passthru) {
		    print $logh "Found:\n   $line" . 
			" --> Output goes directly to the renderer now.\n${added_lf}";
		} else {
		    print $logh "Found:\n   $line" . 
			" --> Output goes to the FIFO buffer now.${added_lf}\n";
		}
	    }

	    # We are in an option which was replaced, do not output
	    # the current line.
	    if ($optionreplaced) {
		$line = "";
	    }

	    # If we are in a "%%BeginSetup...%%EndSetup" section after
	    # the first "%%Page:..." and the current line belongs to
	    # an option setting, we have to copy the line also to the
	    # @psheader.
	    if (($optionsalsointoheader) && 
		(($infeature) || ($line =~ m/^\%\%EndFeature/))) {
		push (@psheader, $line);
	    }

	    # Store or send the current line
	    if (($inheader) && ($isdscjob)) {
		# We are still in the PostScript header, collect all lines 
		# in @psheader
		push (@psheader, $line);
	    } else {
		if (($passthru) && ($isdscjob)) {
		    if (!$lastpassthru) {
			# We enter passthru mode with this line, so the
			# command line can have changed, check it and
			# close the renderer if needed
			if (($rendererpid) &&
			    (!optionsequal($dat, 'currentpage',
					   'previouspage', 0))) {
			    print $logh "Command line/JCL options " .
				"changed, restarting renderer\n";
			    $retval = closerendererhandle
				($rendererhandle, $rendererpid);
			    if ($retval != $EXIT_PRINTED) {
				rip_die ("Error closing renderer",
					 $retval);
			    }
			    $rendererpid = 0;
			}
		    }
		    # Flush @psfifo and send line directly to the renderer
		    if (!$rendererpid) {
			# No renderer running, start it
			($rendererhandle, $rendererpid) =
			    getrendererhandle
			    ($dat, join('', @psheader, @psfifo));
			if ($retval != $EXIT_PRINTED) {
			    rip_die ("Error opening renderer",
				     $retval);
			}
			# @psfifo is sent out, flush it.
			@psfifo = ();
		    }
		    if ($#psfifo >= 0) {
			# Send @psfifo to renderer
			print $rendererhandle join('', @psfifo);
			# flush @psfifo
			@psfifo = ();
		    }
		    # Send line to renderer
		    if (!$printprevpage) {
			print $rendererhandle $line;
		    }
		} else {
		    # Push the line onto the stack for later spitting up...
		    push (@psfifo, $line);
		}
	    }
	    
	    if (!$printprevpage) {
		$linect++;
	    }

	} else {
	    # EOF!
	    $more_stuff = 0;
	    # No PostScript header in the whole file? Then it's not
	    # PostScript, convert it.
	    # We open the file converter here when the file has less
	    # lines than the amount which we search for the PostScript
	    # header ($maxlinestopsstart).
	    if ($linect <= $nonpslines) {
		# This is not a PostScript job, we must convert it
		print $logh "${added_lf}Job does not start with \"%!\", " . 
		    "is it PostScript?\n" .
		    "Starting file converter\n";
		# Reset all variables but conserve the data which
		# we have already read.
		$jobhasjcl = 0;
		$linect = 0;
		$nonpslines = 0;
		$maxlines = 1000;
		$onelinebefore = "";
		$twolinesbefore = "";
		my $alreadyread = join('', @psheader, @psfifo);
		@psheader = ();
		@psfifo = ();
		$line = "";
		# Start the file conversion filter
		if (!$fileconverterpid) {
		    ($fileconverterhandle, $fileconverterpid) =
			getfileconverterhandle($dat, $alreadyread);
		    if ( defined($retval) and $retval != $EXIT_PRINTED) {
			rip_die ("Error opening file converter",
				 $retval);
		    }
		} else {
		    rip_die("File conversion filter probably " .
			    "crashed",
			    $EXIT_JOBERR);
		}
		# Read the further data from the file converter and
		# not from STDIN
		if (!close STDIN && $! != $ESPIPE) {
		    rip_die ("Couldn't close STDIN",
			     $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
		}
		if (!open (STDIN, "<&$fileconverterhandle")) {
		    rip_die ("Couldn't dup \$fileconverterhandle",
			     $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
		}
		# Now we have new (converted) stuff in STDIN, so
		# continue in the loop
		$more_stuff = 1;
	    }
	}

	$lastpassthru = $passthru;
	
	if ((!$ignoreline) && (!$printprevpage)) {
	    $twolinesbefore = $onelinebefore;
	    $onelinebefore = $line;
	}

    } while ((($maxlines == 0) or ($linect < $maxlines)) and
	     ($more_stuff != 0));

    # Some buffer still containing data? Send it out to the renderer.
    if (($more_stuff != 0) || ($inheader) || ($#psfifo >= 0)) {
	# Flush @psfifo and send the remaining data to the renderer, this
	# only happens with non-DSC-conforming jobs or non-Foomatic PPDs
	if ($more_stuff) {
	    print $logh "Stopped parsing the PostScript data, ".
		"sending rest directly to renderer.\n";
	} else {
	    print $logh "Flushing FIFO.\n";
	}
	if ($inheader) {
	    # No page initialized yet? Copy the "header" option set into the
	    # "currentpage" option set, so that the renderer will find the
	    # options settings.
	    copyoptions($dat, 'header', 'currentpage');
	    $optionset = 'currentpage';
	    # If not done yet, insert defaults and command line settings
	    # in the beginning of the job or after the last valid section
	    splice(@psheader, $insertoptions, 0,
		   ($prologfound ? () :
		    makeprologsection($dat, $optionset, 1)),
		   ($setupfound ? () :
		    makesetupsection($dat, $optionset, 1)),
		   ($pagesetupfound ? () :
		    makepagesetupsection($dat, $optionset, 1)));
	    $prologfound = 1;
	    $setupfound = 1;
	    $pagesetupfound = 1;
	}
	if (($rendererpid) &&
	    (!optionsequal($dat, 'currentpage',
			   'previouspage', 0))) {
	    print $logh "Command line/JCL options " .
		"changed, restarting renderer\n";
	    $retval = closerendererhandle
		($rendererhandle, $rendererpid);
	    if ($retval != $EXIT_PRINTED) {
		rip_die ("Error closing renderer",
			 $retval);
	    }
	    $rendererpid = 0;
	}
	if (!$rendererpid) {
	    ($rendererhandle, $rendererpid) =
		getrendererhandle($dat, join('', @psheader, @psfifo));
	    if ($retval != $EXIT_PRINTED) {
		rip_die ("Error opening renderer",
			 $retval);
	    }
	    # We have sent @psfifo now
	    @psfifo = ();
	}
	if ($#psfifo >= 0) {
	    # Send @psfifo to renderer
	    print $rendererhandle join('', @psfifo);
	    # flush @psfifo
	    @psfifo = ();
	}
	# Print the rest of the input data
	if ($more_stuff) {
	    while (<STDIN>) {
		print $rendererhandle $_;
	    }
	}
    }

    # At every "%%Page:..." comment we have saved the PostScript state
    # and we have increased the page number. So if the page number is
    # non-zero we had at least one "%%Page:..." comment and so we have
    # to give a restore the PostScript state.
    #if ($currentpage > 0) {
    #    print $rendererhandle "foomatic-saved-state restore\n";
    #}
    
    # Close the renderer
    if ($rendererpid) {
	$retval = closerendererhandle ($rendererhandle, $rendererpid);
	if ($retval != $EXIT_PRINTED) {
	    rip_die ("Error closing renderer",
		     $retval);
	}
	$rendererpid = 0;
    }

    # Close the file converter (if it was used)
    if ($fileconverterpid) {
	$retval = closefileconverterhandle
	    ($fileconverterhandle, $fileconverterpid);
	if ($retval != $EXIT_PRINTED) {
	    rip_die ("Error closing file converter",
		     $retval);
	}
	$fileconverterpid = 0;
    }
}


## Close the documentation page generator
if ($docgeneratorpid) {
    $retval = closedocgeneratorhandle
	($docgeneratorhandle, $docgeneratorpid);
    if ($retval != $EXIT_PRINTED) {
	rip_die ("Error closing documentation page generator",
		 $retval);
    }
    $docgeneratorpid = 0;
}



## Close last input file
close STDIN;



## Only for debugging
if ($debug && 1) {
    use Data::Dumper;
    local $Data::Dumper::Purity=1;
    local $Data::Dumper::Indent=1;
    print $logh Dumper($dat);
}



## The End
print $logh "${added_lf}Closing foomatic-rip.\n";
close $logh;

exit $retval;



## Functions to let foomatic-rip fork to do several tasks in parallel.

# To do the filtering without loading the whole file into memory we work
# on a data stream, we read the data line by line analyse it to decide what
# filters to use and start the filters if we have found out which we need.
# We buffer the data only as long as we didn't determing which filters to
# use for this piece of data and with which options. There are no temporary
# files used.

# foomatic-rip splits into up to 6 parallel processes to do the whole
# filtering (listed in the order of the data flow):

#    KID0: Generate documentation pages (only jobs with "docs" option)
#    KID2: Put together already read data and current input stream for
#          feeding into the file conversion filter (only non-PostScript
#          and "docs" jobs)
#    KID1: Run the file conversion filter to convert non-PostScript
#          input into PostScript (only non-PostScript and "docs" jobs)
#    MAIN: Prepare the job auto-detecting the spooler, reading the PPD,
#          extracting the options from the command line, and parsing
#          the job data itself. It analyses the job data to check
#          whether it is PostScript and starts KID1/KID2 if not, it
#          also stuffs PostScript code from option settings into the
#          PostScript data stream. It starts the renderer (KID3/KID4)
#          as soon as it knows its command line and restarts it when
#          page-specific option settings need another command line
#          or different JCL commands.
#    KID3: The rendering process. In most cases GhostScript, "cat"
#          for native PostScript printers with their manufacturer's
#          PPD files.
#    KID4: Put together the JCL commands and the renderer's output
#          and send all that either to STDOUT or pipe it into the
#          command line defined with $postpipe.

## This function runs the renderer command line (and if defined also
## the postpipe) and returns a file handle for stuffing in the
## PostScript data.
sub getrendererhandle {

    my ($dat, $prepend) = @_;

    print $logh "${added_lf}Starting renderer\n";

    # Catch signals
    $retval = $EXIT_PRINTED;
    use sigtrap qw(handler set_exit_prnerr USR1 
		   handler set_exit_prnerr_noretry USR2
		   handler set_exit_engaged TTIN);

    # Variables for the kid processes reporting their state

    # Set up a pipe for the kids to pass their exit stat to the main process
    pipe KID_MESSAGE, KID_MESSAGE_IN;

    # When one kid fails put the exit stat here
    $kidfailed = 0;

    # When a kid exits successfully, mark it here
    $kid3finished = 0;
    $kid4finished = 0;

    # Build the command line and get the JCL commands
    buildcommandline($dat, 'currentpage');
    my $commandline = $dat->{'currentcmd'};
    my @jclprepend = @{$dat->{'jclprepend'}} if defined $dat->{'jclprepend'};
    my @jclappend  = @{$dat->{'jclappend'}}  if defined $dat->{'jclappend'};

    use IO::Handle;
    pipe KID3_IN, KID3;
    KID3->autoflush(1);
    $kid3 = fork();
    if (!defined($kid3)) {
	close KID3;
	close KID3_IN;
        print $logh "$0: cannot fork for kid3!\n";
	rip_die ("can't fork for kid3",
		 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
    }
    if ($kid3) {

        # we are the parent; return a glob to the filehandle
        close KID3_IN;

	# Feed in the PostScript header and the FIFO contents
	print KID3 $prepend;

        KID3->flush();
        return ( *KID3, $kid3 );

    } else {
        close KID3;

        pipe KID4_IN, KID4;
	KID4->autoflush(1);
        $kid4 = fork();
        if (!defined($kid4)) {
	    close KID4;
	    close KID4_IN;
            print $logh "$0: cannot fork for kid4!\n";
	    close KID_MESSAGE;
	    print KID_MESSAGE_IN "3 $EXIT_PRNERR_NORETRY_BAD_SETTINGS\n";
	    close KID_MESSAGE_IN;
	    rip_die ("can't fork for kid4",
		     $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
        }
        
        if ($kid4) {
            # parent, child of primary task; we are |commandline|
            close KID4_IN;

            print $logh "renderer PID kid4=$kid4\n";
	    print $logh "renderer command: $commandline\n";
            
            if (!close STDIN && $! != $ESPIPE) {
		close KID3_IN;
		close KID4;
		close KID_MESSAGE;
		print KID_MESSAGE_IN
		    "3 $EXIT_PRNERR_NORETRY_BAD_SETTINGS\n";
		close KID_MESSAGE_IN;
		rip_die ("Couldn't close STDIN in $kid4",
			 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
	    }
            if (!open (STDIN, "<&KID3_IN")) {
		close KID3_IN;
		close KID4;
		close KID_MESSAGE;
		print KID_MESSAGE_IN
		    "3 $EXIT_PRNERR_NORETRY_BAD_SETTINGS\n";
		close KID_MESSAGE_IN;
		rip_die ("Couldn't dup KID3_IN",
			 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
	    }
            if (!close STDOUT) {
		close KID3_IN;
		close KID4;
		close KID_MESSAGE;
		print KID_MESSAGE_IN
		    "3 $EXIT_PRNERR_NORETRY_BAD_SETTINGS\n";
		close KID_MESSAGE_IN;
		rip_die ("Couldn't close STDOUT in $kid4",
			 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
	    }
            if (!open (STDOUT, ">&KID4")) {
		close KID3_IN;
		close KID4;
		close KID_MESSAGE;
		print KID_MESSAGE_IN
		    "3 $EXIT_PRNERR_NORETRY_BAD_SETTINGS\n";
		close KID_MESSAGE_IN;
		rip_die ("Couldn't dup KID4",
			 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
	    }
	    if ($debug) {
		if (!open (STDERR, ">&$logh")) {
		    close KID3_IN;
		    close KID4;
		    close KID_MESSAGE;
		    print KID_MESSAGE_IN
			"3 $EXIT_PRNERR_NORETRY_BAD_SETTINGS\n";
		    close KID_MESSAGE_IN;
		    rip_die ("Couldn't dup logh to stderr",
			     $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
		}
	    }

	    # Massage commandline to execute foomatic-gswrapper
	    my $havewrapper = 0;
	    for (split(':', $ENV{'PATH'})) {
		if (-x "$_/foomatic-gswrapper") {
		    $havewrapper=1;
		    last;
		}
	    }
	    if ($havewrapper) {
		$commandline =~ s!^\s*gs\s!foomatic-gswrapper !g;
		$commandline =~ s!(\|\s*)gs\s!\|foomatic-gswrapper !g;
		$commandline =~ s!(;\s*)gs\s!; foomatic-gswrapper !g;
	    }

	    # If the renderer command line contains the "echo"
	    # command, replace the "echo" by the user-chosen $myecho
	    # (important for non-GNU systems where GNU echo is in a
	    # special path
	    $commandline =~ s!^\s*echo\s!$myecho !g;
	    $commandline =~ s!(\|\s*)echo\s!\|$myecho !g;
	    $commandline =~ s!(;\s*)echo\s!; $myecho !g;

	    # In debug mode save the data supposed to be fed into the
	    # renderer also into a file
	    if ($debug) {
		$commandline = "tee -a ${logfile}.ps | ( $commandline )";
	    }
	    
	    # Actually run the thing...
	    system("$commandline");
            if ($? != 0) {
		my $rendererretval = $? >> 8;
		print $logh "renderer return value: $rendererretval\n";
		my $renderersignal = $? & 127;
		print $logh "renderer received signal: $rendererretval\n";
		close STDOUT;
		close KID4;
		close STDIN;
		close KID3_IN;
		# Handle signals
		if ($renderersignal == SIGUSR1) {
		    $retval = $EXIT_PRNERR;
		} elsif ($renderersignal == SIGUSR2) {
		    $retval = $EXIT_PRNERR_NORETRY;
		} elsif ($renderersignal == SIGTTIN) {
		    $retval = $EXIT_ENGAGED;
		}
		if ($retval != $EXIT_PRINTED) {
		    close KID_MESSAGE;
		    print KID_MESSAGE_IN "3 $retval\n";
		    close KID_MESSAGE_IN;
		    exit $retval;
		}
		# Evaluate renderer result
		if ($rendererretval == 0) {
		    # Success, exit with 0 and inform main process
		    close KID_MESSAGE;
		    print KID_MESSAGE_IN "3 $EXIT_PRINTED\n";
		    close KID_MESSAGE_IN;
		    exit $EXIT_PRINTED;
		} elsif ($rendererretval == 1) {
		    # Syntax error? PostScript error?
		    close KID_MESSAGE;
		    print KID_MESSAGE_IN "3 $EXIT_JOBERR\n";
		    close KID_MESSAGE_IN;
		    rip_die ("Possible error on renderer command line or PostScript error. Check options.",
			     $EXIT_JOBERR);
		} elsif ($rendererretval == 139) {
		    # Seems to indicate a core dump
		    close KID_MESSAGE;
		    print KID_MESSAGE_IN "3 $EXIT_JOBERR\n";
		    close KID_MESSAGE_IN;
		    rip_die ("The renderer may have dumped core.",
			     $EXIT_JOBERR);
		} elsif ($rendererretval == 141) {
		    # Broken pipe, presumably additional filter interface
		    # exited.
		    close KID_MESSAGE;
		    print KID_MESSAGE_IN "3 $EXIT_PRNERR\n";
		    close KID_MESSAGE_IN;
		    rip_die ("A filter used in addition to the renderer" .
			     " itself may have failed.",
			     $EXIT_PRNERR);
		} elsif (($rendererretval == 243) || ($retval == 255)) {
		    # PostScript error?
		    close KID_MESSAGE;
		    print KID_MESSAGE_IN "3 $EXIT_JOBERR\n";
		    close KID_MESSAGE_IN;
		    exit $EXIT_JOBERR;
		} else {
		    # Unknown error
		    close KID_MESSAGE;
		    print KID_MESSAGE_IN "3 $EXIT_PRNERR\n";
		    close KID_MESSAGE_IN;
		    rip_die ("The renderer command line returned an" .
			     " unrecognized error code $rendererretval.",
			     $EXIT_PRNERR);
		}
	    }
	    close STDOUT;
	    close KID4;
	    close STDIN;
	    close KID3_IN;
	    # When arrived here the renderer command line was successful
	    # So exit with zero exit value here and inform the main process
	    close KID_MESSAGE;
	    print KID_MESSAGE_IN "3 $EXIT_PRINTED\n";
	    close KID_MESSAGE_IN;
	    # Wait for postpipe/output child
	    waitpid($kid4, 0);
	    print $logh "KID3 finished\n";
	    exit $EXIT_PRINTED;
        } else {
            # child, trailing task on the pipe; we write jcl stuff
            close KID4;
	    close KID3_IN;

            my $fileh = *STDOUT;

	    # Do we have a $postpipe, if yes, launch the command(s) and
	    # point our output into it/them
            if ($postpipe) {
                if (!open PIPE,$postpipe) {
		    close KID4_IN;
                    close KID_MESSAGE;
                    print KID_MESSAGE_IN
			"4 $EXIT_PRNERR_NORETRY_BAD_SETTINGS\n";
		    close KID_MESSAGE_IN;
                    rip_die ("cannot execute postpipe $postpipe",
                             $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
                }
                $fileh = *PIPE;
            }

	    # Debug output
	    print $logh "JCL: " . join("", @jclprepend) . "<job data> ${added_lf}" .
		join("", @jclappend) . "\n";

            # wrap the JCL around the job data, if there are any
            # options specified...
	    # Should the driver already have inserted JCL commands we merge
	    # our JCL header with the one from the driver
	    my $driverjcl = 0;
	    if ( @jclprepend > 1 ) {
		# JCL header read from renderer output
		my @jclheader = ();
		# Determine magic string of JCL in use (usually "@PJL")
		# For that we take the first part of the second JCL line up
		# to the first space
		if ($jclprepend[1] =~ /^(\S+)/) {
		    my $jclstr = $1;
		    # Read from the renderer output until the first non-JCL
		    # line appears
		    while (my $line = <KID4_IN>) {
			push(@jclheader, $line);
			last if ($line !~ /$jclstr/);
		    }
		    # If we had read at least two lines, at least one is
		    # a JCL header, so do the merging
		    if (@jclheader > 1) {
			$driverjcl = 1;
			# Discard the first and the last entry of the
			# @jclprepend array, we only need the option settings
			# to merge them in
			pop(@jclprepend);
			shift(@jclprepend);
			# Line after which we insert new JCL commands in the
			# JCL header of the job
			my $insert = 1;
			# Go through every JCL command in @jclprepend
			for my $line (@jclprepend) {
			    # Search the command in the JCL header from the
			    # driver. As search term use only the string from
			    # the beginning of the line to the "=", so the
			    # command will also be found when it has another
			    # value
			    $line =~ /^([^=]+)/;
			    my $cmd = $1;
			    my $cmdfound = 0;
			    for (@jclheader) {
				# If the command is there, replace it
				$_ =~ s/$cmd\b.*(\r\n|\n|\r)/$line/ and 
				    $cmdfound = 1;
			    }
			    if (!$cmdfound) {
				# If the command is not found, insert it
				if (@jclheader > 2) {
				    # @jclheader has more than one line,
				    # insert the new command beginning
				    # right after the first line and continuing
				    # after the previous inserted command
				    splice(@jclheader, $insert, 0, $line);
				    $insert ++;
				} else {
				    # If we have only one line of JCL it
				    # is probably something like the
				    # "@PJL ENTER LANGUAGE=..." line
				    # which has to be in the end, but
				    # it also contains the
				    # "<esc>%-12345X" which has to be in the
				    # beginning of the job. So we split the
				    # line right before the $jclstr and
				    # append our command to the end of the
				    # first part and let the second part
				    # be a second JCL line.
				    $jclheader[0] =~ 
					/^(.*?)($jclstr.*(\r\n|\n|\r))/;
				    my $first = "$1$line";
				    my $second = "$2";
				    my $third = $jclheader[1];
				    @jclheader = ($first, $second, $third);
				}
			    }
			}
			# Now pass on the merged JCL header
			print $fileh @jclheader;
		    } else {
			# The driver didn't create a JCL header, simply
			# prepend ours and then pass on the line which we
			# already have read
			print $fileh @jclprepend, @jclheader;
		    }
		} else {
		    # No merging of JCL header possible, simply prepend it
		    print $fileh @jclprepend;
		}
	    }

	    # The rest of the job data
            while (<KID4_IN>) {
                print $fileh $_;
            }

	    # A JCL trailer
	    if (( @jclprepend > 1 ) && (!$driverjcl)) {
		print $fileh @jclappend;
	    }
            
            if (!close $fileh) {
		close KID4_IN;
		close KID_MESSAGE;
		print KID_MESSAGE_IN
		    "4 $EXIT_PRNERR_NORETRY_BAD_SETTINGS\n";
		close KID_MESSAGE_IN;
		rip_die ("error closing $fileh",
			 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
	    }
	    close KID4_IN;

            print $logh "tail process done writing data to STDOUT\n";

	    # Handle signals of the backend interface
	    if ($retval != $EXIT_PRINTED) {
		close KID_MESSAGE;
		print KID_MESSAGE_IN "4 $retval\n";
		close KID_MESSAGE_IN;
		exit $retval;
	    }

	    # Successful exit, inform main process
	    close KID_MESSAGE;
	    print KID_MESSAGE_IN "4 $EXIT_PRINTED\n";
	    close KID_MESSAGE_IN;

	    print $logh "KID4 finished\n";
            exit($EXIT_PRINTED);
        }
    }
}



## Close the renderer process and wait until all kid processes finish.

sub closerendererhandle {

    my ($rendererhandle, $rendererpid) = @_;

    print $logh "${added_lf}Closing renderer\n";

    # Do it!
    close $rendererhandle;

    # Wait for all kid processes to finish or one kid process to fail
    close KID_MESSAGE_IN;
    while ((!$kidfailed) &&
	   !(($kid3finished) &&
	     ($kid4finished))) {
	my $message = <KID_MESSAGE>;
	chomp $message;
	if ($message =~ /(\d+)\s+(\d+)/) {
	    my $kid_id = $1;
	    my $exitstat = $2;
	    print $logh "KID$kid_id exited with status $exitstat\n";
	    if ($exitstat > 0) {
		$kidfailed = $exitstat;
	    } elsif ($kid_id == 3) {
		$kid3finished = 1;
	    } elsif ($kid_id == 4) {
		$kid4finished = 1;
	    }
	}
    }

    close KID_MESSAGE;

    # If a kid failed, return the exit stat of this kid
    if ($kidfailed != 0) {
	$retval = $kidfailed;
    }

    print $logh "Renderer exit stat: $retval\n";
    # Wait for renderer child
    waitpid($rendererpid, 0);
    print $logh "Renderer process finished\n";
    return ($retval);
}



## This function is only used when the input data is not
## PostScript. Then it runs a filter which converts non-PostScript
## files into PostScript. The user can choose which filter he wants
## to use. The filter command line is provided by $fileconverter.

sub getfileconverterhandle {

    # Already read data must be converted, too
    my ($dat, $alreadyread) = @_;

    print $logh "${added_lf}Starting converter for non-PostScript files\n";

    # Determine with which command non-PostScript files are converted
    # to PostScript
    if ($fileconverter eq "") {
	if ($spoolerfileconverters->{$spooler}) {
	    $fileconverter = $spoolerfileconverters->{$spooler};
	} else {
	    for my $c (@fileconverters) {
		($c =~ m/^\s*(\S+)\s+/) || ($c = m/^\s*(\S+)$/);
		my $command = $1;
		if( -x $command ){
		    $fileconverter = $command;
		} else {
		for (split(':', $ENV{'PATH'})) {
		    if (-x "$_/$command") {
			$fileconverter = $c;
			last;
		    }
		}
		}
		if ($fileconverter ne "") {
		    last;
		}
	    }
	}
	if ($fileconverter eq "") {
	    $fileconverter = "echo \"Cannot convert file to " .
		"PostScript!\" 1>&2";
	}
    }

    # Insert the page size into the $fileconverter
    if ($fileconverter =~ /\@\@([^@]+)\@\@PAGESIZE\@\@/) {
	# We always use the "header" option swt here, with a
	# non-PostScript file we have no "currentpage"
	my $optstr = $1;
	my $arg;
	my $sizestr = (($arg = $dat->{'args_byname'}{'PageSize'})
		       ? $arg->{'header'}
		       : "");
	if ($sizestr) {
	    # Use wider margins so that the pages come out completely on
	    # every printer model (especially HP inkjets)
	    if ($fileconverter =~ /^\s*(a2ps)\s+/) {
		if (lc($sizestr) eq "letter") {
		    $sizestr = "Letterdj";
		} elsif (lc($sizestr) eq "a4") {
		    $sizestr = "A4dj";
		}
	    }
	    $optstr .= $sizestr;
	} else {
	    $optstr = "";
	}
	$fileconverter =~ s/\@\@([^@]+)\@\@PAGESIZE\@\@/$optstr/;
    }

    # Insert the job title into the $fileconverter
    if ($fileconverter =~ /\@\@([^@]+)\@\@JOBTITLE\@\@/) {
	if ($do_docs) {
	    $jobtitle =
		"Documentation for the $model";
	}
	my $titlearg = $1;
	my ($arg, $optstr);
	($arg = $jobtitle) =~ s/\"/\\\"/g;
	if (($titlearg =~ /\"/) || $arg) {
	    $optstr = $titlearg . ($titlearg =~ /\"/ ? '' : '"') .
		($arg ? "$arg\"" : '"');
	} else {
	    $optstr = "";
	}
	$fileconverter =~ s/\@\@([^@]+)\@\@JOBTITLE\@\@/$optstr/;
    }

    # Apply "pstops" when having used a file converter under CUPS, so
    # CUPS can stuff the default settings into the PostScript output
    # of the file converter (so all CUPS settings get also applied when
    # one prints the documentation pages (all other files we get
    # already converted to PostScript by CUPS.
    if ($spooler eq 'cups') {
	$fileconverter .=
	    " | ${programdir}pstops '$rargs[0]' '$rargs[1]' '$rargs[2]' " .
	    "'$rargs[3]' '$rargs[4]'";
    }

    # Variables for the kid processes reporting their state

    # Set up a pipe for the kids to pass their exit stat to the main process
    pipe KID_MESSAGE_CONV, KID_MESSAGE_CONV_IN;

    # When one kid fails put the exit stat here
    $convkidfailed = 0;

    # When a kid exits successfully, mark it here
    $kid1finished = 0;
    $kid2finished = 0;

    use IO::Handle;
    pipe KID1_IN, KID1;
    KID1->autoflush(1);
    my $kid1 = fork();
    if (!defined($kid1)) {
	close KID1;
	close KID1_IN;
        print $logh "$0: cannot fork for kid1!\n";
	rip_die ("can't fork for kid1",
		 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
    }

    if ($kid1) {

        # we are the parent; return a glob to the filehandle
        close KID1;

        return ( *KID1_IN, $kid1 );

    } else {
	# We go on reading the job data and stuff it into the file
	# converter
        close KID1_IN;

        pipe KID2_IN, KID2;
	KID2->autoflush(1);
        $kid2 = fork();
        if (!defined($kid2)) {
            print $logh "$0: cannot fork for kid2!\n";
	    close KID1;
	    close KID2;
	    close KID2_IN;
	    close KID_MESSAGE_CONV;
	    print KID_MESSAGE_CONV_IN 
		"1 $EXIT_PRNERR_NORETRY_BAD_SETTINGS\n";
	    rip_die ("can't fork for kid2",
		     $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
        }
        
        if ($kid2) {
            # parent, child of primary task; we are |$fileconverter|
            close KID2;

            print $logh "file converter PID kid2=$kid2\n";
	    if (($debug) || ($spooler ne 'cups')) {
		print $logh "file converter command: $fileconverter\n";
	    }
            
            if (!close STDIN && $! != $ESPIPE) {
		close KID1;
		close KID2_IN;
		close KID_MESSAGE_CONV;
		print KID_MESSAGE_CONV_IN 
		    "1 $EXIT_PRNERR_NORETRY_BAD_SETTINGS\n";
		close KID_MESSAGE_CONV_IN;
		rip_die ("Couldn't close STDIN in $kid2",
			 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
	    }
            if (!open (STDIN, "<&KID2_IN")) {
		close KID1;
		close KID2_IN;
		close KID_MESSAGE_CONV;
		print KID_MESSAGE_CONV_IN 
		    "1 $EXIT_PRNERR_NORETRY_BAD_SETTINGS\n";
		close KID_MESSAGE_CONV_IN;
		rip_die ("Couldn't dup KID2_IN",
			 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
	    }
            if (!close STDOUT) {
		close KID1;
		close KID2_IN;
		close KID_MESSAGE_CONV;
		print KID_MESSAGE_CONV_IN
		    "1 $EXIT_PRNERR_NORETRY_BAD_SETTINGS\n";
		close KID_MESSAGE_CONV_IN;
		rip_die ("Couldn't close STDOUT in $kid2",
			 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
	    }
            if (!open (STDOUT, ">&KID1")) {
		close KID1;
		close KID2_IN;
		close KID_MESSAGE_CONV;
		print KID_MESSAGE_CONV_IN
		    "1 $EXIT_PRNERR_NORETRY_BAD_SETTINGS\n";
		close KID_MESSAGE_CONV_IN;
		rip_die ("Couldn't dup KID1",
			 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
	    }
	    if ($debug) {
		if (!open (STDERR, ">&$logh")) {
		    close KID1;
		    close KID2_IN;
		    close KID_MESSAGE_CONV;
		    print KID_MESSAGE_CONV_IN
			"1 $EXIT_PRNERR_NORETRY_BAD_SETTINGS\n";
		    close KID_MESSAGE_CONV_IN;
		    rip_die ("Couldn't dup logh to stderr",
			     $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
		}
	    }

	    # Actually run the thing...
	    system("$fileconverter");
            if ($? != 0) {
		my $fileconverterretval = $? >> 8;
		print $logh "file converter return value: " .
		    "$fileconverterretval\n";
		my $fileconvertersignal = $? & 127;
		print $logh "file converter received signal: ".
		    "$fileconverterretval\n";
		close STDOUT;
		close KID1;
		close STDIN;
		close KID2_IN;
		# Handle signals
		if ($fileconvertersignal == SIGUSR1) {
		    $retval = $EXIT_PRNERR;
		} elsif ($fileconvertersignal == SIGUSR2) {
		    $retval = $EXIT_PRNERR_NORETRY;
		} elsif ($fileconvertersignal == SIGTTIN) {
		    $retval = $EXIT_ENGAGED;
		}
		if ($retval != $EXIT_PRINTED) {
		    close KID_MESSAGE_CONV;
		    print KID_MESSAGE_CONV_IN "1 $retval\n";
		    close KID_MESSAGE_CONV_IN;
		    exit $retval;
		}
		# Evaluate fileconverter result
		if ($fileconverterretval == 0) {
		    # Success, exit with 0 and inform main process
		    close KID_MESSAGE_CONV;
		    print KID_MESSAGE_CONV_IN "1 $EXIT_PRINTED\n";
		    close KID_MESSAGE_CONV_IN;
		    exit $EXIT_PRINTED;
		} else {
		    # Unknown error
		    close KID_MESSAGE_CONV;
		    print KID_MESSAGE_CONV_IN "1 $EXIT_PRNERR\n";
		    close KID_MESSAGE_CONV_IN;
		    rip_die ("The file converter command line returned " . 
			     "an unrecognized error code " .
			     "$fileconverterretval.",
			     $EXIT_PRNERR);
		}
	    }
	    close STDOUT;
	    close KID1;
	    close STDIN;
	    close KID2_IN;
	    # When arrived here the fileconverter command line was
	    # successful.
	    # So exit with zero exit value here and inform the main process
	    close KID_MESSAGE_CONV;
	    print KID_MESSAGE_CONV_IN "1 $EXIT_PRINTED\n";
	    close KID_MESSAGE_CONV_IN;
	    # Wait for input child
	    waitpid($kid1, 0);
	    print $logh "KID1 finished\n";
	    exit $EXIT_PRINTED;
        } else {
            # child, first part of the pipe, reading in the data from
	    # standard input and stuffing it into the file converter
	    # after putting in the already read data (in $alreadyread)
            close KID1;
	    close KID2_IN;

	    # At first pass the data which we have already read to the
	    # filter
	    print KID2 $alreadyread;
	    # Then read the rest from standard input
	    while (<STDIN>) { 
		print KID2 $_; 
	    }

            if (!close STDIN && $! != $ESPIPE) {
		close KID2;
		close KID_MESSAGE_CONV;
		print KID_MESSAGE_CONV_IN
		    "2 $EXIT_PRNERR_NORETRY_BAD_SETTINGS\n";
		close KID_MESSAGE_CONV_IN;
		rip_die ("error closing STDIN",
			 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
	    }
	    close KID2;

            print $logh "tail process done reading data from STDIN\n";

	    # Successful exit, inform main process
	    close KID_MESSAGE_CONV;
	    print KID_MESSAGE_CONV_IN "2 $EXIT_PRINTED\n";
	    close KID_MESSAGE_CONV_IN;

	    print $logh "KID2 finished\n";
            exit($EXIT_PRINTED);
        }
    }
}



## Close the file conversion process and wait until all kid processes
## finish.

sub closefileconverterhandle {

    my ($fileconverterhandle, $fileconverterpid) = @_;

    print $logh "${added_lf}Closing file converter\n";

    # Do it!
    close $fileconverterhandle;

    # Wait for all kid processes to finish or one kid process to fail
    close KID_MESSAGE_CONV_IN;
    while ((!$convkidfailed) &&
	   !(($kid1finished) &&
	     ($kid2finished))) {
	my $message = <KID_MESSAGE_CONV>;
	chomp $message;
	if ($message =~ /(\d+)\s+(\d+)/) {
	    my $kid_id = $1;
	    my $exitstat = $2;
	    print $logh "KID$kid_id exited with status $exitstat\n";
	    if ($exitstat > 0) {
		$convkidfailed = $exitstat;
	    } elsif ($kid_id == 1) {
		$kid1finished = 1;
	    } elsif ($kid_id == 2) {
		$kid2finished = 1;
	    }
	}
    }

    close KID_MESSAGE_CONV;

    # If a kid failed, return the exit stat of this kid
    if ($convkidfailed != 0) {
	$retval = $convkidfailed;
    }

    print $logh "File converter exit stat: $retval\n";
    # Wait for fileconverter child
    waitpid($fileconverterpid, 0);
    print $logh "File converter process finished\n";
    return ($retval);
}



## Generate the documentation page and return a filehandle to get it

sub getdocgeneratorhandle {

    # The data structure with the options
    my ($dat) = @_;

    print $logh "${added_lf}Generating documentation page for the $model\n";

    # Printer queue name
    my $printerstr;
    if ($printer) {
	$printerstr = $printer;
    } else {
	$printerstr = "<printer>";
    }
	
    # Spooler-specific differences
    my ($command,
	$enumopt, $enumoptleft, $enumoptequal, $enumoptright,
	$boolopt, $booloptfalseprefix, $booloptleft, $booloptequal,
	$booloptright,
	$numopt, $numoptleft, $numoptequal, $numoptright,
	$stropt, $stroptleft, $stroptequal, $stroptright,
	$optsep, $trailer, $custompagesize);
    if ($spooler eq 'cups') {
	($command,
	 $enumopt, $enumoptleft, $enumoptequal, $enumoptright,
	 $boolopt, $booloptfalseprefix, $booloptleft, $booloptequal,
	 $booloptright,
	 $numopt, $numoptleft, $numoptequal, $numoptright,
	 $stropt, $stroptleft, $stroptequal, $stroptright,
	 $optsep, $trailer, $custompagesize) =
	     ("lpr -P $printerstr ",
	      "-o ", "", "=", "",
	      "-o ", "no", "", "=", "",
	      "-o ", "", "=", "",
	      "-o ", "", "=", "",
	      " "," <file>",
	      "\n  Custom size: -o PageSize=Custom." .
	      "<width>x<height>[<unit>]\n" .
	      "               Units: pt (default), in, cm, mm\n" .
	      "  Example: -o PageSize=Custom.4.0x6.0in\n");
    } elsif ($spooler eq 'lpd') {
	($command,
	 $enumopt, $enumoptleft, $enumoptequal, $enumoptright,
	 $boolopt, $booloptfalseprefix, $booloptleft, $booloptequal,
	 $booloptright,
	 $numopt, $numoptleft, $numoptequal, $numoptright,
	 $stropt, $stroptleft, $stroptequal, $stroptright,
	 $optsep, $trailer, $custompagesize) =
	     ("lpr -P $printerstr -J \"",
	      "", "", "=", "",
	      "", "", "", "=", "",
	      "", "", "=", "",
	      "", "", "=", "",
	      " ", "\" <file>",
	      "\n  Custom size: PageSize=Custom." .
	      "<width>x<height>[<unit>]\n" .
	      "               Units: pt (default), in, cm, mm\n" .
	      "  Example: PageSize=Custom.4.0x6.0in\n");
    } elsif ($spooler eq 'gnulpr') {
	($command,
	 $enumopt, $enumoptleft, $enumoptequal, $enumoptright,
	 $boolopt, $booloptfalseprefix, $booloptleft, $booloptequal,
	 $booloptright,
	 $numopt, $numoptleft, $numoptequal, $numoptright,
	 $stropt, $stroptleft, $stroptequal, $stroptright,
	 $optsep, $trailer, $custompagesize) =
	     ("lpr -P $printerstr ",
	      "-o ", "", "=", "",
	      "-o ", "", "", "=", "",
	      "-o ", "", "=", "",
	      "-o ", "", "=", "",
	      " "," <file>",
	      "\n  Custom size: -o PageSize=Custom." .
	      "<width>x<height>[<unit>]\n" .
	      "               Units: pt (default), in, cm, mm\n" .
	      "  Example: -o PageSize=Custom.4.0x6.0in\n");
    } elsif ($spooler eq 'lprng') {
	($command,
	 $enumopt, $enumoptleft, $enumoptequal, $enumoptright,
	 $boolopt, $booloptfalseprefix, $booloptleft, $booloptequal,
	 $booloptright,
	 $numopt, $numoptleft, $numoptequal, $numoptright,
	 $stropt, $stroptleft, $stroptequal, $stroptright,
	 $optsep, $trailer, $custompagesize) =
	     ("lpr -P $printerstr ",
	      "-Z ", "", "=", "",
	      "-Z ", "", "", "=", "",
	      "-Z ", "", "=", "",
	      "-Z ", "", "=", "",
	      " "," <file>",
	      "\n  Custom size: -Z PageSize=Custom." .
	      "<width>x<height>[<unit>]\n" .
	      "               Units: pt (default), in, cm, mm\n" .
	      "  Example: -Z PageSize=Custom.4.0x6.0in\n");
    } elsif ($spooler eq 'ppr') {
	($command,
	 $enumopt, $enumoptleft, $enumoptequal, $enumoptright,
	 $boolopt, $booloptfalseprefix, $booloptleft, $booloptequal,
	 $booloptright,
	 $numopt, $numoptleft, $numoptequal, $numoptright,
	 $stropt, $stroptleft, $stroptequal, $stroptright,
	 $optsep, $trailer, $custompagesize) =
	     ("ppr -d $printerstr --ripopts \"",
	      "", "", "=", "",
	      "", "", "", "=", "",
	      "", "", "=", "",
	      "", "", "=", "",
	      " ","\" <file>",
	      "\n  Custom size: PageSize=Custom." .
	      "<width>x<height>[<unit>]\n" .
	      "               Units: pt (default), in, cm, mm\n" .
	      "  Example: PageSize=Custom.4.0x6.0in\n");
    } elsif ($spooler eq 'ppr-int') {
	($command,
	 $enumopt, $enumoptleft, $enumoptequal, $enumoptright,
	 $boolopt, $booloptfalseprefix, $booloptleft, $booloptequal,
	 $booloptright,
	 $numopt, $numoptleft, $numoptequal, $numoptright,
	 $stropt, $stroptleft, $stroptequal, $stroptright,
	 $optsep, $trailer, $custompagesize) =
	     ("ppr -d $printerstr -i \"",
	      "", "", "=", "",
	      "", "", "", "=", "",
	      "", "", "=", "",
	      "", "", "=", "",
	      " ","\" <file>",
	      "\n  Custom size: PageSize=Custom." .
	      "<width>x<height>[<unit>]\n" .
	      "               Units: pt (default), in, cm, mm\n" .
	      "  Example: PageSize=Custom.4.0x6.0in\n");
    } elsif ($spooler eq 'cps') {
	($command,
	 $enumopt, $enumoptleft, $enumoptequal, $enumoptright,
	 $boolopt, $booloptfalseprefix, $booloptleft, $booloptequal,
	 $booloptright,
	 $numopt, $numoptleft, $numoptequal, $numoptright,
	 $stropt, $stroptleft, $stroptequal, $stroptright,
	 $optsep, $trailer, $custompagesize) =
	     ("lpr -P $printerstr ",
	      "-o ", "", "=", "",
	      "-o ", "", "", "=", "",
	      "-o ", "", "=", "",
	      "-o ", "", "=", "",
	      " "," <file>",
	      "\n  Custom size: -o PageSize=Custom." .
	      "<width>x<height>[<unit>]\n" .
	      "               Units: pt (default), in, cm, mm\n" .
	      "  Example: -o PageSize=Custom.4.0x6.0in\n");
    } elsif ($spooler eq 'direct') {
	($command,
	 $enumopt, $enumoptleft, $enumoptequal, $enumoptright,
	 $boolopt, $booloptfalseprefix, $booloptleft, $booloptequal,
	 $booloptright,
	 $numopt, $numoptleft, $numoptequal, $numoptright,
	 $stropt, $stroptleft, $stroptequal, $stroptright,
	 $optsep, $trailer, $custompagesize) =
	     ("$programname -P $printerstr ",
	      "-o ", "", "=", "",
	      "-o ", "", "", "=", "",
	      "-o ", "", "=", "",
	      "-o ", "", "=", "",
	      " "," <file>",
	      "\n  Custom size: -o PageSize=Custom." .
	      "<width>x<height>[<unit>]\n" .
	      "               Units: pt (default), in, cm, mm\n" .
	      "  Example: -o PageSize=Custom.4.0x6.0in\n");
    } elsif ($spooler eq 'pdq') {
	($command,
	 $enumopt, $enumoptleft, $enumoptequal, $enumoptright,
	 $boolopt, $booloptfalseprefix, $booloptleft, $booloptequal,
	 $booloptright,
	 $numopt, $numoptleft, $numoptequal, $numoptright,
	 $stropt, $stroptleft, $stroptequal, $stroptright,
	 $optsep, $trailer, $custompagesize) =
	     ("pdq -P $printerstr ",
	      "-o", "", "_", "",
	      "-o", "no", "", "_", "",
	      "-a", "", "=", "",
	      "-a", "", "=", "",
	      " "," <file>",
	      "\n" .
	      "Option 'PageWidth':\n". 
	      "  Page Width (for \"Custom\" page size)\n" .
	      "  A floating point number argument\n" .
	      "  Range: 0 <= x <= 100000\n" .
	      "  Example: -aPageWidth=123.4\n" .
	      "\n" .
	      "Option 'PageHeight':\n" .
	      "  Page Height (for \"Custom\" page size)\n" .
	      "  A floating point number argument\n" .
	      "  Range: 0 <= x <= 100000\n" .
	      "  Example: -aPageHeight=234.5\n" .
	      "\n" .
	      "Option 'PageSizeUnit':\n" .
	      "  Unit (for \"Custom\" page size)\n" .
	      "  An enumerated choice argument\n" .
	      "  Possible choices:\n" .
	      "   o -oPageSizeUnit_pt: Points (1/72 inch)\n" .
	      "   o -oPageSizeUnit_in: Inches\n" .
	      "   o -oPageSizeUnit_cm: cm\n" .
	      "   o -oPageSizeUnit_mm: mm\n" .
	      "  Example: -oPageSizeUnit_mm\n");
    }

    # Variables for the kid processes reporting their state

    # Set up a pipe for the kids to pass their exit stat to the main process
    pipe KID_MESSAGE_DOC, KID_MESSAGE_DOC_IN;

    # When the kid fails put the exit stat here
    $dockidfailed = 0;

    # When the kid exits successfully, mark it here
    $kid0finished = 0;

    use IO::Handle;
    pipe KID0_IN, KID0;
    KID0->autoflush(1);
    my $kid0 = fork();
    if (!defined($kid0)) {
	close KID0;
	close KID0_IN;
        print $logh "$0: cannot fork for kid0!\n";
	rip_die ("can't fork for kid0",
		 $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
    }

    if ($kid0) {
        # we are the parent; return a glob to the filehandle
        close KID0;
	print $logh "Documentation page generator PID kid0=$kid0\n";
        return ( *KID0_IN, $kid0 );
    }

    # we are the kid; we generate the documentation page

    close KID0_IN;

    # Kill data on STDIN to satisfy PPR
    if (($spooler eq 'ppr_int') || ($spooler eq 'ppr')) {
	while (my $dummy = <STDIN>) {};
    }
    close STDIN
	or print $logh "Error closing STDIN for docs print\n";

    # write the job into KID0
    select KID0;

    print "\nInvokation summary for the $model\n\n";
    print "Use the following command line:\n\n";
    if ($booloptfalseprefix) {
	# I think that what you want to indicate is that the prefix for a false
	# boolean has this form:  xxx [no]<switch> or something similar
	print "   ${command}${enumopt}${enumoptleft}<option>" .
	    "${enumoptequal}<choice>${enumoptright}${optsep}" .
	    "${boolopt}${booloptleft}\[${booloptfalseprefix}\]<switch>" .
	    "${booloptright}${optsep}" .
	    "${numopt}${numoptleft}<num. option>${numoptequal}" .
	    "<value>${numoptright}${optsep}" .
	    "${stropt}${stroptleft}<string option>${stroptequal}" .
	    "<string>${stroptright}" .
	    "${trailer}\n\n";
    } else {
	print "   ${command}${enumopt}${enumoptleft}<option>" .
	    "${enumoptequal}<choice>${enumoptright}${optsep}" .
	    "${boolopt}${booloptleft}<switch>${booloptequal}" .
	    "<True/False>${booloptright}${optsep}" .
	    "${numopt}${numoptleft}<num. option>${numoptequal}" .
	    "<value>${numoptright}${optsep}" .
	    "${stropt}${stroptleft}<string option>${stroptequal}" .
	    "<string>${stroptright}" .
	    "${trailer}\n\n";
    }
	
    print "The following options are available for this printer:\n\n";

    for my $arg (@{$dat->{'args'}}) {
        my ($name,
            $type,
            $comment,
            $spot,
            $default) = ($arg->{'name'},
                         $arg->{'type'},
                         $arg->{'comment'},
                         $arg->{'spot'},
                         $arg->{'default'});

	# Is this really an option? Otherwise skip it.
	next if (!$type);

	# We don't need "PageRegion", we have "PageSize"
	next if ($name eq "PageRegion");

	# Skip enumerated choice options with only one choice
	next if (($type eq 'enum') && ($#{$arg->{'vals'}} < 1));

	my $commentstr = "";
	if ($comment) {
	    $commentstr = "  $comment\n";
	}

	my $typestr;
        if ($type eq "enum") {
	    $typestr = "An enumerated choice";
	} elsif ($type eq "bool") {
	    $typestr = "A boolean";
	} elsif ($type eq "int") {
	    $typestr = "An integer number";
	} elsif ($type eq "float") {
	    $typestr = "A floating point number";
	} elsif (($type eq "string") || ($type eq "password")) {
	    $typestr = "A string";
	}

        print "Option '$name':\n$commentstr  $typestr argument\n";
        print "  This options corresponds to a PJL command\n" if ($arg->{'style'} eq 'J');
        
        if ($type eq 'bool') {
            print "  Possible choices:\n";
	    if ($booloptfalseprefix) {
		print "   o $name: $arg->{'comment_true'}\n";
		print "   o $booloptfalseprefix$name: " .
		    "$arg->{'comment_false'}\n";
		if (defined($default)) {
		    my $defstr = ($default ? "" : "$booloptfalseprefix");
		    print "  Default: $defstr$name\n";
		}
		print "  Example: ${boolopt}${booloptleft}${name}" .
		    "${booloptright}\n";
	    } else {
		print "   o True: $arg->{'comment_true'}\n";
		print "   o False: $arg->{'comment_false'}\n";
		if (defined($default)) {
		    my $defstr = ($default ? "True" : "False");
		    print "  Default: $defstr\n";
		}
		print "  Example: ${boolopt}${booloptleft}${name}" .
		    "${booloptequal}True${booloptright}\n";
	    }
        } elsif ($type eq 'enum') {
            print "  Possible choices:\n";
            my $exarg;
	    my $havecustomsize = 0;
            for (@{$arg->{'vals'}}) {
                my ($choice, $comment) = ($_->{'value'}, $_->{'comment'});
                print "   o $choice: $comment\n";
		if (($name eq "PageSize") && ($choice eq "Custom")) {
		    $havecustomsize = 1;
		}
                $exarg=$choice;
            }
            if (defined($default)) {
                print "  Default: $default\n";
            }
            print "  Example: ${enumopt}${enumoptleft}${name}" .
		"${enumoptequal}${exarg}${enumoptright}\n";
	    if ($havecustomsize) {
		print $custompagesize;
	    }
        } elsif ($type eq 'int' or $type eq 'float') {
            my ($max, $min) = ($arg->{'max'}, $arg->{'min'});
            my $exarg;
            if (defined($max)) {
                print "  Range: $min <= x <= $max\n";
                $exarg=$max;
            }
            if (defined($default)) {
                print "  Default: $default\n";
                $exarg=$default;
            }
            if (!$exarg) { $exarg=0; }
            print "  Example: ${numopt}${numoptleft}${name}" .
		"${numoptequal}${exarg}${numoptright}\n";
        } elsif ($type eq 'string' or $type eq 'password') {
            my $maxlength = $arg->{'maxlength'};
            if (defined($maxlength)) {
                print "  Maximum length: $maxlength characters\n";
            }
            if (defined($default)) {
                print "  Default: $default\n";
            }
            print "  Examples/special settings:\n";
            for (@{$arg->{'vals'}}) {
                my ($value, $comment, $driverval, $proto) = 
		    ($_->{'value'}, $_->{'comment'}, $_->{'driverval'},
		     $arg->{'proto'});
		# Retrieve the original string from the prototype
		# and the driverval
		my $string;
		if ($proto) {
		    my $s = index($proto, '%s');
		    my $l = length($driverval) - length($proto) + 2;
		    if (($s < 0) || ($l < 0)) {
			$string = $driverval;
		    } else {
			$string = substr($driverval, $s, $l);
		    }
		} else {
		    $string = $driverval;
		}
		print "   o ${stropt}${stroptleft}${name}" .
		    "${stroptequal}${value}${stroptright}";
		if (($value ne $string) || ($comment ne $value)) {
		    print " (";
		}
		if ($value ne $string) {
		    if ($string eq '') {
			print "blank string";
		    } else {
			print "\"$string\"";
		    }
		}
		if (($value ne $string) && ($comment ne $value)) {
		    print ", ";
		}
		if ($value ne $comment) {
		    print "$comment";
		}
		if (($value ne $string) || ($comment ne $value)) {
		    print ")";
		}
		print "\n";
            }
        }

        print "\n";
    }
    
    select STDOUT;
    close KID0 
        or print $logh "Error closing KID0 for docs print\n";
    close STDOUT
        or print $logh "Error closing STDOUT for docs print\n";

    # Finished successfully, inform main process
    close KID_MESSAGE_DOC;
    print KID_MESSAGE_DOC_IN "0 $EXIT_PRINTED\n";
    close KID_MESSAGE_DOC_IN;

    print $logh "KID0 finished\n";
    exit($EXIT_PRINTED);

}



## Close the documentation page generation process and wait until the
## kid process finishes.

sub closedocgeneratorhandle {

    my ($handle, $pid) = @_;

    print $logh "${added_lf}Closing documentation page generator\n";

    # Do it!
    close $handle;

    # Wait for the kid process to finish or the kid process to fail
    close KID_MESSAGE_DOC_IN;
    while ((!$dockidfailed) &&
	   (!$kid0finished)) {
	my $message = <KID_MESSAGE_DOC>;
	chomp $message;
	if ($message =~ /(\d+)\s+(\d+)/) {
	    my $kid_id = $1;
	    my $exitstat = $2;
	    print $logh "KID$kid_id exited with status $exitstat\n";
	    if ($exitstat > 0) {
		$dockidfailed = $exitstat;
	    } elsif ($kid_id eq "0") {
		$kid0finished = 1;
	    }
	}
    }

    close KID_MESSAGE_DOC;

    # If the kid failed, return the exit stat of the kid
    if ($dockidfailed != 0) {
	$retval = $dockidfailed;
    }

    print $logh "Documentation page generator exit stat: $retval\n";
    # Wait for fileconverter child
    waitpid($pid, 0);
    print $logh "Documentation page generator process finished\n";
    return ($retval);
}



# Find an argument by name in a case-insensitive way
sub argbyname {
    my $name = $_[0];

    for my $arg (@{$dat->{'args'}}) {
        return $arg if (lc($name) eq lc($arg->{'name'}));
    }

    return undef;
}

sub valbyname {
    my ($arg,$name) = @_;

    for my $val (@{$arg->{'vals'}}) {
        return $val if (lc($name) eq lc($val->{'value'}));
    }

    return undef;
}

# Write a Good-Bye letter and clean up before committing suicide (send
# error message to caller)

sub rip_die {
    my ($message, $exitstat) = @_;
    my $errmsg = "$!";
    my $errcod = $! + 0;

    # Close the documentation page generator (if it was used)
    if ($docgeneratorpid) {
	if ($kid0) {
	    print $logh "Killing process $kid0 (KID0)\n";
	    kill(9, $kid0);
	}
	$docgeneratorpid = 0;
    }

    # Close the file converter (if it was used)
    if ($fileconverterpid) {
	if ($kid2) {
	    print $logh "Killing process $kid2 (KID2)\n";
	    kill(9, $kid2);
	}
	if ($kid1) {
	    print $logh "Killing process $kid1 (KID1)\n";
	    kill(9, $kid1);
	}
	$fileconverterpid = 0;
    }

    # Close the renderer
    if ($rendererpid) {
	if ($kid4) {
	    print $logh "Killing process $kid4 (KID4)\n";
	    kill(9, $kid4);
	}
	if ($kid3) {
	    print $logh "Killing process $kid3 (KID3)\n";
	    kill(9, $kid3);
	}
	$rendererpid = 0;
    }

    print $logh "Process dying with \"$message\", exit stat: $exitstat\n\terror: $errmsg ($errcod)\n";
    if ($spooler eq 'ppr_int') {
	# Special error handling for PPR intefaces
	$message =~ s/\\/\\\\/;
	$message =~ s/\"/\\\"/;
	my @messagelines = split("\n", $message);
	my $firstline = "TRUE";
	for my $line (@messagelines) {
	    system("lib/alert $printer $firstline \"$line\"");
	    $firstline = "FALSE";
	}
    } else {
	print STDERR $message . "\n";
    }
    exit $exitstat;
}

# Signal handling routines

sub set_exit_prnerr {
    $retval = $EXIT_PRNERR;
}

sub set_exit_prnerr_noretry {
    $retval = $EXIT_PRNERR_NORETRY;
}

sub set_exit_engaged {
    $retval = $EXIT_ENGAGED;
}

# Read the config file

sub readConfFile {
    my ($file) = @_;

    my %conf;
    # Read config file if present
    if (open CONF, "< $file") {
	while (<CONF>)
	{
	    $conf{$1}="$2" if (m/^\s*([^\#]\S*)\s*:\s*(.*?)\s*$/);
	}
	close CONF;
    }

    return %conf;
}

sub removeunprintables {
    # Remove unprintable characters
    my $str = $_[0];
    $str =~ s/[\x00-\x1f]//g;
    return $str;
}

sub removeshellescapes {
    # Remove shell escape characters
    my $str = $_[0];
    $str =~ s/[\|<>&!\$\'\"\#\*\?\(\)\[\]\{\}]//g;
    return $str;
}

sub removespecialchars {
    # Remove unprintable and shell escape characters
    return removeshellescapes(removeunprintables($_[0]));
}

sub unhtmlify {
    # Replace HTML/XML entities by the original characters
    my $str = $_[0];
    $str =~ s/\&apos;/\'/g;
    $str =~ s/\&quot;/\"/g;
    $str =~ s/\&gt;/\>/g;
    $str =~ s/\&lt;/\</g;
    $str =~ s/\&amp;/\&/g;
    return $str;
}

sub unhexify {
    # Replace hex notation for unprintable characters in PPD files
    # by the actual characters ex: "<0A>" --> chr(hex("0A"))
    my ($input) = @_;
    my $output = "";
    my $hexmode = 0;
    my $firstdigit = "";
    for (my $i = 0; $i < length($input); $i ++) {
	my $c = substr($input, $i, 1);
	if ($hexmode) {
	    if ($c eq ">") {
		# End of hex string
		$hexmode = 0;
	    } elsif ($c =~ /^[0-9a-fA-F]$/) {
		# Hexadecimal digit, two of them give a character
		if ($firstdigit ne "") {
		    $output .= chr(hex("$firstdigit$c"));
		    $firstdigit = "";
		} else {
		    $firstdigit = $c;
		}
	    }
	} else {
	    if ($c eq "<") {
		# Beginning of hex string
		$hexmode = 1;
	    } else {
		# Normal character
		$output .= $c;
	    }
	}
    }
    return $output;
}

sub undossify( $ ) {
    # Remove "dossy" line ends ("\r\n") from a string
    my $str = $_[0];
    $str =~ s/\r\n/\n/gs;
    $str =~ s/\r$//s;
    return( $str );
}

sub checkarg {
    # Check if there is already an argument record $argname in $dat, if not,
    # create one
    my ($dat, $argname) = @_;
    return if defined($dat->{'args_byname'}{$argname});
    # argument record
    my $rec;
    $rec->{'name'} = $argname;
    # Insert record in 'args' array for browsing all arguments
    push(@{$dat->{'args'}}, $rec);
    # 'args_byname' hash for looking up arguments by name
    $dat->{'args_byname'}{$argname} = $dat->{'args'}[$#{$dat->{'args'}}];
    # Default execution style is 'G' (PostScript) since all arguments for
    # which we don't find "*Foomatic..." keywords are usual PostScript
    # options
    $dat->{'args_byname'}{$argname}{'style'} = 'G';
    # Default prototype for code to insert, used by enum options
    $dat->{'args_byname'}{$argname}{'proto'} = '%s';
    # stop Perl nattering about undefined to string comparisons
    $dat->{'args_byname'}{$argname}{'type'} = '';
    print $logh "Added option $argname\n";
}

sub checksetting {
    # Check if there is already an choice record $setting in the $argname
    # argument in $dat, if not, create one
    my ($dat, $argname, $setting) = @_;
    return if 
	defined($dat->{'args_byname'}{$argname}{'vals_byname'}{$setting});
    # setting record
    my $rec;
    $rec->{'value'} = $setting;
    # Insert record in 'vals' array for browsing all settings
    push(@{$dat->{'args_byname'}{$argname}{'vals'}}, $rec);
    # 'vals_byname' hash for looking up settings by name
    $dat->{'args_byname'}{$argname}{'vals_byname'}{$setting} = 
	$dat->{'args_byname'}{$argname}{'vals'}[$#{$dat->{'args_byname'}{$argname}{'vals'}}];
}

sub removearg {
    # remove the argument record $argname from $dat
    my ($dat, $argname) = @_;
    return if !defined($dat->{'args_byname'}{$argname});
    # Remove 'args_byname' hash for looking up arguments by name
    delete $dat->{'args_byname'}{$argname};
    # Remove argument itself
    for (my $i = 0; $i <= $#{$dat->{'args'}}; $i ++) {
	if ($dat->{'args'}[$i]{'name'} eq $argname) {
	    print $logh "Removing option " .
		$argname . "\n";
	    splice(@{$dat->{'args'}}, $i, 1);
	    last;
	}
    }
}

sub removepsargs {
    # remove all records of PostScript arguments from $dat
    my ($dat) = @_;
    return if !defined($dat);
    for (my $i = 0; $i <= $#{$dat->{'args'}}; $i ++) {
	if ($dat->{'args'}[$i]{'style'} eq 'G') {
	    print $logh "Removing PostScript option " .
		$dat->{'args'}[$i]{'name'} . "\n";
	    # Remove 'args_byname' hash for looking up arguments by name
	    delete $dat->{'args_byname'}{$dat->{'args'}[$i]{'name'}};
	    # Remove argument itself
	    splice(@{$dat->{'args'}}, $i, 1);
	    $i --;
	}
    }
}

sub checkoptionvalue {

    ## This function checks whether a given value is valid for a given
    ## option. If yes, it returns a cleaned value (e. g. always 0 or 1
    ## for boolean options), otherwise "undef". If $forcevalue is set,
    ## we always determine a corrected value to insert (we never return
    ## "undef").

    # Is $value valid for the option named $argname?
    my ($dat, $argname, $value, $forcevalue) = @_;

    # Record for option $argname
    my $arg = $dat->{'args_byname'}{$argname};
    $arg->{'type'} = '' if not defined $arg->{'type'};

    if ($arg->{'type'} eq 'bool') {
	my $lcvalue = lc($value);
	if ((($lcvalue) eq 'true') ||
	    (($lcvalue) eq 'on') ||
	    (($lcvalue) eq 'yes') ||
	    (($lcvalue) eq '1')) {
	    return 1;
	} elsif ((($lcvalue) eq 'false') ||
		 (($lcvalue) eq 'off') ||
		 (($lcvalue) eq 'no') ||
		 (($lcvalue) eq '0')) {
	    return 0;
	} elsif ($forcevalue) {
	    # This maps Unknown to mean False.  Good?  Bad?
	    # It was done so in Foomatic 2.0.x, too.
	    my $name = $arg->{'name'};
	    print $logh 
		"The value $value for $name is not a " .
		"choice!\n" .
		" --> Using False instead!\n";
	    return 0;
	}
    } elsif ($arg->{'type'} eq 'enum') {
	if ($value =~ /^None$/i) {
	    return 'None';
	} elsif (defined($arg->{'vals_byname'}{$value})) {
	    return $value;
	} elsif ((($arg->{'name'} eq "PageSize") ||
		  ($arg->{'name'} eq "PageRegion")) &&
		 (defined($arg->{'vals_byname'}{'Custom'})) &&
		 ($value =~ m!^Custom\.([\d\.]+)x([\d\.]+)([A-Za-z]*)$!)) {
	    # Custom paper size
	    return $value;
	} elsif ($forcevalue) {
	    # wtf!?  that's not a choice!
	    my $name = $arg->{'name'};
	    # Return the first entry of the list
	    my $firstentry = $arg->{'vals'}[0]{'value'};
	    print $logh 
		"The value $value for $name is not a " .
		"choice!\n" .
		" --> Using $firstentry instead!\n";
	    return $firstentry;
	}
    } elsif (($arg->{'type'} eq 'int') ||
	     ($arg->{'type'} eq 'float')) {
	if (($value <= $arg->{'max'}) &&
	    ($value >= $arg->{'min'})) {
	    return $value;
	} elsif ($forcevalue) {
	    my $name = $arg->{'name'};
	    my $newvalue;
	    if ($value > $arg->{'max'}) {
		$newvalue = $arg->{'max'}
	    } elsif ($value < $arg->{'min'}) {
		$newvalue = $arg->{'min'}
	    }
	    print $logh 
		"The value $value for $name is out of " .
		"range!\n" .
		" --> Using $newvalue instead!\n";
	    return $newvalue;
	}
    } elsif (($arg->{'type'} eq 'string') ||
	     ($arg->{'type'} eq 'password')) {
	if (defined($arg->{'vals_byname'}{$value})) {
	    my $name = $arg->{'name'};
	    print $logh 
		"The value $value for $name is a predefined choice\n";
	    return $value;
	} elsif (stringvalid($dat, $argname, $value)) {
	    # Check whether the string is one of the enumerated choices
	    my $sprintfproto = $arg->{'proto'};
	    $sprintfproto =~ s/\%(?!s)/\%\%/g;
	    my $driverval = sprintf($sprintfproto, $value);
	    for my $val (@{$arg->{'vals'}}) {
		if (($val->{'driverval'} eq $driverval) ||
		    ($val->{'driverval'} eq $value)) {
		    my $name = $arg->{'name'};
		    print $logh 
			"The string $value for $name is the predefined " .
			"choice $val->{value}\n";
		    return $val->{value};
		}
	    }
	    # "None" is mapped to the empty string
	    if ($value eq 'None') {
		my $name = $arg->{'name'};
		print $logh 
		    "Option $name: 'None' is the mapped to the " .
		    "empty string\n";
		return '';
	    }
	    # No matching choice? Return the original string
	    return $value;
	} elsif ($forcevalue) {
	    my $name = $arg->{'name'};
	    my $str = substr($value, 0, $arg->{'maxlength'});
	    if (stringvalid($dat, $argname, $str)) {
		print $logh 
		    "The string $value for $name is longer than " .
		    "$arg->{'maxlength'}, string shortened to $str\n";
		return $str;
	    } elsif ($#{$arg->{'vals'}} >= 0) {
		# First list item
		my $firstentry = $arg->{'vals'}[0]{'value'};
		print $logh 
		    "The string $value for $name contains forbidden " .
		    "characters or does not match the regular expression " .
		    "defined for this option, using predefined choice " .
		    "$firstentry instead\n";
		return $firstentry;
	    } else {
		# We should not get here
		rip_die("Option $name incorrectly defined in the " .
			"PPD file!\n", $EXIT_PRNERR_NORETRY_BAD_SETTINGS);
	    }
	}
    }
    return undef;
}

sub stringvalid {

    ## Checks whether a user-supplied value for a string option is valid
    ## It must be within the length limit, should only contain allowed
    ## characters and match the given regexp

    # Option and string
    my ($dat, $argname, $value) = @_;

    my $arg = $dat->{'args_byname'}{$argname};

    # Maximum length
    return 0 if (defined($arg->{'maxlength'}) &&
		 (length($value) > $arg->{'maxlength'}));

    # Allowed characters
    if ($arg->{'allowedchars'}) {
	my $chars = $arg->{'allowedchars'};
	# Quote the slashes (if a slash is preceeded by an even number of
	# backslashes, it is not already quoted)
	$chars =~ s/(?<!\\)((\\\\)*)\//$2\\\//g;
	return 0 if $value !~ /^[$chars]*$/;
    }

    # Regular expression
    if ($arg->{'allowedregexp'}) {
	my $regexp = $arg->{'allowedregexp'};
	# Quote the slashes (if a slash is preceeded by an even number of
	# backslashes, it is not already quoted)
	$regexp =~ s/(?<!\\)((\\\\)*)\//$2\\\//g;
	return 0 if $value !~ /$regexp/;
    }

    # All checks passed
    return 1;
}

sub checkoptions {

    ## Let the values of a boolean option being 0 or 1 instead of
    ## "True" or "False", range-check the defaults of all options and
    ## issue warnings if the values are not valid

    # Option set to be examined
    my ($dat, $optionset) = @_;

    for my $arg (@{$dat->{'args'}}) {
	if (defined($arg->{$optionset})) {
	    $arg->{$optionset} =
		checkoptionvalue
		($dat, $arg->{'name'}, $arg->{$optionset}, 1);
	}
    }

    # If the settings for "PageSize" and "PageRegion" are different,
    # set the one for "PageRegion" to the one for "PageSize" and issue
    # a warning.
    if ($dat->{'args_byname'}{'PageSize'}{$optionset} ne
	$dat->{'args_byname'}{'PageRegion'}{$optionset}) {
	print $logh "Settings for \"PageSize\" and \"PageRegion\" are " .
	    "different:\n" .
	    "   PageSize: $dat->{'args_byname'}{'PageSize'}{$optionset}\n" .
	    "   PageRegion: ".
	    "$dat->{'args_byname'}{'PageRegion'}{$optionset}\n" .
	    "Using the \"PageSize\" value " .
	    "\"$dat->{'args_byname'}{'PageSize'}{$optionset}\"," .
	    " for both.\n";
	$dat->{'args_byname'}{'PageRegion'}{$optionset} =
	    $dat->{'args_byname'}{'PageSize'}{$optionset};
    }
}

# If the PageSize or PageRegion was changed, also change the other

sub syncpagesize {
    
    # Name and value of the option we set, and the option set where we
    # did the change
    my ($dat, $name, $value, $optionset) = @_;

    # Don't do anything if we were called with an option other than
    # "PageSize" or "PageRegion"
    return if (($name ne "PageSize") && ($name ne "PageRegion"));
    
    # Don't do anything if not both "PageSize" and "PageRegion" exist
    return if ((!defined($dat->{'args_byname'}{'PageSize'})) ||
	       (!defined($dat->{'args_byname'}{'PageRegion'})));
    
    my $dest;
    
    # "PageSize" --> "PageRegion"
    if ($name eq "PageSize") {
	$dest = "PageRegion";
    }
    
    # "PageRegion" --> "PageSize"
    if ($name eq "PageRegion") {
	$dest = "PageSize";
    }
    
    # Do it!
	my $val;
    if ($val=valbyname($dat->{'args_byname'}{$dest}, $value)) {
	# Standard paper size
	$dat->{'args_byname'}{$dest}{$optionset} = $val->{'value'};
    } elsif ($val=valbyname($dat->{'args_byname'}{$dest}, "Custom")) {
	# Custom paper size
	$dat->{'args_byname'}{$dest}{$optionset} = $value;
    }
}

sub copyoptions {

    ## Copy one option set into another one

    # Source and destination option sets
    my ($dat, $srcoptionset, $destoptionset) = @_;

    for my $arg (@{$dat->{'args'}}) {
	if (defined($arg->{$srcoptionset})) {
	    $arg->{$destoptionset} = $arg->{$srcoptionset};
	}
    }
}

sub deleteoptions {

    ## Delete an option set

    # option set to be removed
    my ($dat, $optionset) = @_;

    for my $arg (@{$dat->{'args'}}) {
	if (defined($arg->{$optionset})) {
	    delete($arg->{$optionset});
	}
    }
}

sub optionsequal {

    ## Compare two option sets, if they are equal, return 1, otherwise 0

    # Option sets to be compared, flag to compare only command line and JCL
    # options
    my ($dat, $firstoptionset, $secondoptionset, $exceptPS) = @_;

    for my $arg (@{$dat->{'args'}}) {
	next if ($exceptPS && ($arg->{'style'} eq 'G'));
	if ((defined($arg->{$firstoptionset})) &&
	    (defined($arg->{$secondoptionset}))) {
	    # Both entries exist
	    return 0 if $arg->{$firstoptionset} ne $arg->{$secondoptionset};
	} elsif ((defined($arg->{$firstoptionset})) ||
		 (defined($arg->{$secondoptionset}))) {
	    # One entry exists
	    return 0;
	}
	# If no entry exists, the non-existing entries are considered as
	# equal
    }
    return 1;
}

sub makeprologsection {

    # option set to be used,
    # $comments = 1: Add "%%BeginProlog...%%EndProlog"
    my ($dat, $optionset, $comments) = @_;
    
    # Collect data to be inserted here
    my @output;

    # Start comment
    if ($comments) {
	print $logh "\"Prolog\" section is missing, inserting it.\n";
	push(@output, "%%BeginProlog\n");
    }

    # Generate the option code (not necessary when CUPS is spooler)
    if ($spooler ne 'cups') {
	print $logh "Inserting option code into \"Prolog\" section.\n";
	buildcommandline ($dat, $optionset);
	push(@output, @{$dat->{'prologprepend'}});
    }

    # End comment
    if ($comments) {
	push(@output, "%%EndProlog\n");
    }

    return join('', @output);
}

sub makesetupsection {

    # option set to be used, $comments = 1: Add "%%BeginSetup...%%EndSetup"
    my ($dat, $optionset, $comments) = @_;
    
    # Collect data to be inserted here
    my @output;

    # Start comment
    if ($comments) {
	print $logh "\"Setup\" section is missing, inserting it.\n";
	push(@output, "%%BeginSetup\n");
    }

    # PostScript code to generate accounting messages for CUPS
    if ($spooler eq 'cups') {
	print $logh "Inserting PostScript code for CUPS' page accounting\n";
	push(@output, $accounting_prolog);
    }

    # Generate the option code (not necessary when CUPS is spooler)
    if ($spooler ne 'cups') {
	print $logh "Inserting option code into \"Setup\" section.\n";
	buildcommandline ($dat, $optionset);
	push(@output, @{$dat->{'setupprepend'}});
    }

    # End comment
    if ($comments) {
	push(@output, "%%EndSetup\n");
    }

    return join('', @output);
}

sub makepagesetupsection {

    # option set to be used,
    # $comments = 1: Add "%%BeginPageSetup...%%EndPageSetup"
    my ($dat, $optionset, $comments) = @_;
    
    # Collect data to be inserted here
    my @output;

    # Start comment
    if ($comments) {
	push(@output, "%%BeginPageSetup\n");
	print $logh "\"PageSetup\" section is missing, inserting it.\n";
    }

    # Generate the option code (not necessary when CUPS is spooler)
    print $logh "Inserting option code into \"PageSetup\" section.\n";
    buildcommandline ($dat, $optionset);
    if ($spooler ne 'cups') {
	push(@output, @{$dat->{'pagesetupprepend'}});
    } else {
	push(@output, @{$dat->{'cupspagesetupprepend'}});
    }

    # End comment
    if ($comments) {
	push(@output, "%%EndPageSetup\n");
    }

    return join('', @output);
}

sub parsepageranges {

    ## Parse a string containing page ranges and either check whether a
    ## given page is in the ranges or, if the given page number is zero,
    ## determine the score how specific this page range string is.

    # String with page ranges and number of current page (0 for score)
    my ($ranges, $page) = @_;
    
    my $currentnumber = 0;
    my $rangestart = 0;
####### Question: is rangeend ever used?
    my $rangeend = 0;
    my $currentkeyword = '';
    my $invalidrange = 0;
    my $totalscore = 0;
    my $pageinside = 0;
    my $currentrange = '';

    my $evaluaterange = sub {
	# evaluate the current range: determine its score and whether the
	# current page is member of it.
	if ($invalidrange) {
	    # Range is invalid, issue a warning
	    print $logh "   Invalid range: $currentrange\n";
	} else {
	    # We have a valid range, evaluate it
	    if ($currentkeyword) {
		if ($currentkeyword =~ /^even/i) {
		    # All even-numbered pages
		    $totalscore += 50000;
		    $pageinside = 1 if (($page % 2) == 0);
		} elsif ($currentkeyword =~ /^odd/i) {
		    # All odd-numbered pages
		    $totalscore += 50000;
		    $pageinside = 1 if (($page % 2) == 1);
		} else {
		    # Invalid range
		    print $logh "   Invalid range: $currentrange\n";
		}
	    } elsif (($rangestart == 0) && ($currentnumber > 0)) {
		# Page range is a single page
		$totalscore += 1;
		$pageinside = 1 if ($page == $currentnumber);
	    } elsif (($rangestart > 0) && ($currentnumber > 0)) {
		# Page range is a sequence of pages
		$totalscore += (abs($currentnumber - $rangestart) + 1);
		if ($currentnumber < $rangestart) {
		    my $tmp = $currentnumber;
		    $currentnumber = $rangestart;
		    $rangestart = $tmp;
		}
		$pageinside = 1 if (($page <= $currentnumber) &&
				    ($page >= $rangestart));
	    } elsif ($rangestart > 0) {
		# Page range goes to the end of the document
		$totalscore += 100000;
		$pageinside = 1 if ($page >= $rangestart);
	    } else {
		# Invalid range
		print $logh "   Invalid range: $currentrange\n";
	    }
	}
	# Range is evaluated, remove all recordings of the current range
	$rangestart = 0;
	$currentnumber = 0;
	$currentkeyword = '';
	$invalidrange = 0;
	$currentrange = '';
    };

    for (my $i = 0; $i < length($ranges); $i ++) {
	my $c = substr($ranges, $i, 1);
	if (!$invalidrange) {
	    if ($c =~ /\d/) {
		# Digit
		if ($currentkeyword) {
		    # Add to keyword
		    $currentkeyword .= $c;
		} else {
		    # Build a page number
		    $currentnumber *= 10;
		    $currentnumber += $c;
		}
	    } elsif ($c =~ /[a-z_]/i) {
		# Letter or underscore
		if (($rangestart > 0) || ($rangeend > 0) ||
		    ($currentnumber > 0)) {
		    # Keyword not allowed after a page number or a
		    # page range
		    $invalidrange = 1;
		} else {
		    # Build a keyword
		    $currentkeyword .= $c;
		}
	    } elsif ($c eq '-') {
		# Page range 
		if (($rangestart > 0) || ($currentkeyword)) {
		    # Keyword or two '-' not allowed in page range
		    $invalidrange = 1;
		} else {
		    # Save start of range, reset page number
		    $rangestart = $currentnumber;
		    if ($rangestart == 0) {
			$rangestart = 1;
		    }
		    $currentnumber = 0;
		}
	    } 
	}
	if ($c eq ',') {
	    # End of a range
	    &$evaluaterange();
	} else {
	    # Make a string of the current range, for warnings
	    $currentrange .= $c;
	}
    }
    # End of input string
    &$evaluaterange();
    # Return value
    if (($page == 0) || ($pageinside)) {
	return $totalscore;
    } else {
	return 0;
    }
}

sub setoptionsforpage {

    ## Set the options for a given page

    # Foomatic data, name of the option set where to apply the options, and
    # number of the page
    my ($dat, $optionset, $page) = @_;

    my $bestscore = 10000000;
    my $value;
    for my $arg (@{$dat->{'args'}}) {
	$value = '';
	for my $key (keys %{$arg}) {
	    next if $key !~ /^pages:(.*)$/;
	    my $pageranges = $1;
	    if (my $score = parsepageranges($pageranges, $page)) {
		if ($score <= $bestscore) {
		    $bestscore = $score;
		    $value = $arg->{$key};
		}
	    }
	}
	if ($value) {
	    $arg->{$optionset} = $value;
	}
    }
}

sub buildcommandline {

    ## Build a renderer command line, based on the given option set

    # Foomatic data and name of the option set to apply
    my ($dat, $optionset) = @_;

    # Construct the proper command line.
    $dat->{'currentcmd'} = $dat->{'cmd'};
    my @prologprepend;
    my @setupprepend;
    my @pagesetupprepend;
    my @cupspagesetupprepend;
    my @jclprepend;
    my @jclappend;

    # At first search for composite options and determine how they
    # set their member options
    for my $arg (@{$dat->{'args'}}) { $arg->{'order'} = 0 if !defined $arg->{'order'}; }
    for my $arg (sort { $a->{'order'} <=> $b->{'order'} } 
	      @{$dat->{'args'}}) {

	# Here we are only interested in composite options, skip the others
	next if $arg->{'style'} ne 'X';
	
	my $name = $arg->{'name'};
	# Check whether this composite option is controlled by another
	# composite option, so nested composite options are possible.
	my $userval = ($arg->{'fromcomposite'} ?
		       $arg->{'fromcomposite'} : $arg->{$optionset});

	# Get the current setting
	my $v = $arg->{'vals_byname'}{$userval};
	my @settings = split(/\s+/s, $v->{'driverval'});
	for my $s (@settings) {
	    my ($key, $value);
	    if ($s =~ /^([^=]+)=(.+)$/) {
		$key = $1;
		$value = $2;
	    } elsif ($s =~ /^no([^=]+)$/) {
		$key = $1;
		$value = 0;
	    } elsif ($s =~ /^([^=]+)$/) {
		$key = $1;
		$value = 1;
	    }
	    $a = $dat->{'args_byname'}{$key};
	    if ($a->{$optionset} eq "From$name") {
		# We must set this option according to the
		# composite option
		$a->{'fromcomposite'} = $value;
		# Mark the option telling by which composite option
		# it is controlled
		$a->{'controlledby'} = $name;
	    } else {
		$a->{'fromcomposite'} = "";
	    }
	}
	# Remove PostScript code to be inserted after an appearance of the
	# Composite option in the PostScript code.
	undef $arg->{'jclsetup'};
	undef $arg->{'prolog'};
	undef $arg->{'setup'};
	undef $arg->{'pagesetup'};
    }

    for my $arg (sort { $a->{'order'} <=> $b->{'order'} } 
	      @{$dat->{'args'}}) {
	
	# Composite options have no direct influence on the command
	# line, skip them here
	next if $arg->{'style'} eq 'X';

	my $name = $arg->{'name'};
	my $spot = $arg->{'spot'};
	my $cmd = $arg->{'proto'};
	my $cmdf = $arg->{'protof'};
	my $type = ($arg->{'type'} || "");
	my $section = $arg->{'section'};
	my $userval = ($arg->{'fromcomposite'} ?
		       $arg->{'fromcomposite'} : $arg->{$optionset});
	my $cmdvar = "";

	# If we have both "PageSize" and "PageRegion" options, we kept
	# them all the time in sync, so we don't need to insert the settings
	# of both options. So skip "PageRegion".
	next if (($name eq "PageRegion") &&
		 (defined($dat->{'args_byname'}{'PageSize'})) &&
		 (defined($dat->{'args_byname'}{'PageRegion'})));

	# Build the command line snippet/PostScript/JCL code for the current
	# option
	if ($type eq 'bool') {

	    # If true, stick the proto into the command line, if false
	    # and we have a proto for false, stick that in
	    if (defined($userval) && $userval == 1) {
		$cmdvar = $cmd;
	    } elsif ($cmdf) {
		$userval = 0;
		$cmdvar = $cmdf;
	    }

	} elsif ($type eq 'int' or $type eq 'float') {

	    # If defined, process the proto and stick the result into
	    # the command line or postscript queue.
	    if (defined($userval)) {
		my $min = $arg->{'min'};
		my $max = $arg->{'max'};
		# We have already range-checked, correct only
		# floating point inaccuricies here
		if ($userval < $min) {
		    $userval = $min;
		}
		if ($userval > $max) {
		    $userval = $max;
		}
		my $sprintfcmd = $cmd;
		$sprintfcmd =~ s/\%(?!s)/\%\%/g;
		$cmdvar = sprintf($sprintfcmd,
				  ($type eq 'int' 
				   ? sprintf("%d", $userval)
				   : sprintf("%f", $userval)));
	    } else {
		$userval = 'None';
	    }

	} elsif ($type eq 'enum') {

	    # If defined, stick the selected value into the proto and
	    # thence into the commandline
	    if (defined($userval)) {
		# CUPS assumes that options with the choices "Yes", "No",
		# "On", "Off", "True", or "False" are boolean options and
		# maps "-o Option=On" to "-o Option" and "-o Option=Off"
		# to "-o noOption", which foomatic-rip maps to "0" and "1".
		# So when "0" or "1" is unavailable in the option, we try
		# "Yes", "No", "On", "Off", "True", and "False".
		my $val;
		my $found = 0;
		if ($val=valbyname($arg,$userval)) {
		    $found = 1;
		} elsif ($userval =~ /^Custom\.[\d\.]+x[\d\.]+[A-Za-z]*$/) {
		    # Custom paper size
		    $val = valbyname($arg,"Custom");
		    $found = 1;
		} elsif ($userval eq '0') {
		    foreach (qw(No Off False None)) {
			if ($val=valbyname($arg,$_)) {
			    $userval = $_;
			    $arg->{$optionset} = $userval;
			    $found = 1;
			    last;
			}
		    }
		} elsif ($userval eq '1') {
		    foreach (qw(Yes On True)) {
			if ($val=valbyname($arg,$_)) {
			    $userval = $_;
			    $arg->{$optionset} = $userval;
			    $found = 1;
			    last;
			}
		    }
		} elsif ($userval eq 'LongEdge') {
		    # Handle different names for the choices of the
		    # "Duplex" option
		    foreach (qw(LongEdge DuplexNoTumble)) {
			if ($val=valbyname($arg,$_)) {
			    $userval = $_;
			    $arg->{$optionset} = $userval;
			    $found = 1;
			    last;
			}
		    }
		} elsif ($userval eq 'ShortEdge') {
		    foreach (qw(ShortEdge DuplexTumble)) {
			if ($val=valbyname($arg,$_)) {
			    $userval = $_;
			    $arg->{$optionset} = $userval;
			    $found = 1;
			    last;
			}
		    }
		}
		if ($found) {
		    my $sprintfcmd = $cmd;
		    $sprintfcmd =~ s/\%(?!s)/\%\%/g;
		    $cmdvar = sprintf($sprintfcmd,
				      (defined($val->{'driverval'})
				       ? $val->{'driverval'}
				       : $val->{'value'}));
		    # Custom paper size
		    if ($userval =~ /^Custom\.([\d\.]+)x([\d\.]+)([A-Za-z]*)$/) {
			my $width = $1;
			my $height = $2;
			my $unit = $3;
			# convert width and height to PostScript points
			if (lc($unit) eq "in") {
			    $width *= 72.0;
			    $height *= 72.0;
			} elsif (lc($unit) eq "cm") {
			    $width *= (72.0/2.54);
			    $height *= (72.0/2.54);
			} elsif (lc($unit) eq "mm") {
			    $width *= (72.0/25.4);
			    $height *= (72.0/25.4);
			}
			# Round width and height
			$width =~ s/\.[0-4].*$// or
			    $width =~ s/\.[5-9].*$// and $width += 1;
			$height =~ s/\.[0-4].*$// or
			    $height =~ s/\.[5-9].*$// and $height += 1;
			# Insert width and height into the prototype
			if ($cmdvar =~ /^\s*pop\W/s) {
			    # Custom page size for PostScript printers
			    $cmdvar = "$width $height 0 0 0\n$cmdvar";
			} else {
			    # Custom page size for Foomatic/Gimp-Print
			    $cmdvar =~ s/\%0/$width/ or
				$cmdvar =~ s/(\W)0(\W)/$1$width$2/ or
				$cmdvar =~ s/^0(\W)/$width$1/m or
				$cmdvar =~ s/(\W)0$/$1$width/m or
				$cmdvar =~ s/^0$/$width/m;
			    $cmdvar =~ s/\%1/$height/ or
				$cmdvar =~ s/(\W)0(\W)/$1$height$2/ or
				$cmdvar =~ s/^0(\W)/$height$1/m or
				$cmdvar =~ s/(\W)0$/$1$height/m or
				$cmdvar =~ s/^0$/$height/m;
			}
		    }
		} else {
		    # User gave unknown value?
		    $userval = 'None';
		    print $logh "Value $userval for $name is not a valid choice.\n";
		}
	    } else {
		$userval = 'None';
	    }

	} elsif (($type eq 'string') || ($type eq 'password')) {
	    # Stick the entered value into the proto and
	    # thence into the commandline
	    if (defined($userval)) {
		my $val;
		if ($val=valbyname($arg,$userval)) {
		    $userval = $val->{'value'};
		    $cmdvar = (defined($val->{'driverval'})
				       ? $val->{'driverval'}
				       : $val->{'value'});
		} else {
		    my $sprintfcmd = $cmd;
		    $sprintfcmd =~ s/\%(?!s)/\%\%/g;
		    $cmdvar = sprintf($sprintfcmd, $userval);
		}
	    } else {
		$userval = 'None';
	    }

	} else {
	    # Ignore unknown option types silently
	}
        
	# Insert the built snippet at the correct place
	if ($arg->{'style'} eq 'G') {
	    # Place this Postscript command onto the prepend queue
	    # for the appropriate section.
	    if ($cmdvar) {
		my $open = "[{\n%%BeginFeature: *$name $userval\n";
		my $close = "\n%%EndFeature\n} stopped cleartomark\n";
		if ($section eq "Prolog") {
		    push (@prologprepend, "$open$cmdvar$close");
		    my $a = $arg;
		    while ($a->{'controlledby'}) {
			# Collect option PostScript code to be inserted when
			# the composite option which controls this option
			# is found in the PostScript code
			$a = $dat->{'args_byname'}{$a->{'controlledby'}};
			$a->{'prolog'} .= "$cmdvar\n";
		    }
		} elsif ($section eq "AnySetup") {
		    if ($optionset ne 'currentpage') {
			push (@setupprepend, "$open$cmdvar$close");
		    } elsif ($arg->{'header'} ne $userval) {
			push (@pagesetupprepend, "$open$cmdvar$close");
			push (@cupspagesetupprepend, "$open$cmdvar$close");
		    }
		    my $a = $arg;
		    while ($a->{'controlledby'}) {
			# Collect option PostScript code to be inserted when
			# the composite option which controls this option
			# is found in the PostScript code
			$a = $dat->{'args_byname'}{$a->{'controlledby'}};
			$a->{'setup'} .= "$cmdvar\n";
			$a->{'pagesetup'} .= "$cmdvar\n";
		    }
		} elsif ($section eq "DocumentSetup") {
		    push (@setupprepend, "$open$cmdvar$close");
		    my $a = $arg;
		    while ($a->{'controlledby'}) {
			# Collect option PostScript code to be inserted when
			# the composite option which controls this option
			# is found in the PostScript code
			$a = $dat->{'args_byname'}{$a->{'controlledby'}};
			$a->{'setup'} .= "$cmdvar\n";
		    }
		} elsif ($section eq "PageSetup") {
		    push (@pagesetupprepend, "$open$cmdvar$close");
		    my $a = $arg;
		    while ($a->{'controlledby'}) {
			# Collect option PostScript code to be inserted when
			# the composite option which controls this option
			# is found in the PostScript code
			$a = $dat->{'args_byname'}{$a->{'controlledby'}};
			$a->{'pagesetup'} .= "$cmdvar\n";
		    }
		} elsif ($section eq "JCLSetup") {
		    # PJL/JCL argument
		    $dat->{'jcl'} = 1;
		    push (@jclprepend, unhexify($cmdvar));
		    my $a = $arg;
		    while ($a->{'controlledby'}) {
			# Collect option PostScript code to be inserted when
			# the composite option which controls this option
			# is found in the PostScript code
			$a = $dat->{'args_byname'}{$a->{'controlledby'}};
			$a->{'jclsetup'} .= "$cmdvar\n";
		    }
		} else {
		    push (@setupprepend, "$open$cmdvar$close");
		    my $a = $arg;
		    while ($a->{'controlledby'}) {
			# Collect option PostScript code to be inserted when
			# the composite option which controls this option
			# is found in the PostScript code
			$a = $dat->{'args_byname'}{$a->{'controlledby'}};
			$a->{'setup'} .= "$cmdvar\n";
		    }
		}
	    }
	    # Do we have an option which is set to "Controlled by 
	    # '<Composite>'"? Then make PostScript code available
	    # for substitution of "%% FoomaticRIPOptionSetting: ..." 
	    if ($arg->{'fromcomposite'}) {
		$arg->{'compositesubst'} = "$cmdvar\n";
	    }
	} elsif ($arg->{'style'} eq 'J') {
	    # JCL argument
	    $dat->{'jcl'} = 1;
	    # put JCL commands onto JCL stack...
	    push (@jclprepend, "\@PJL $cmdvar\n") if $cmdvar;
	} elsif ($arg->{'style'} eq 'C') {
	    # command-line argument

	    # Insert the processed argument in the commandline
	    # just before every occurance of the spot marker.
	    $dat->{'currentcmd'} =~ s!\%$spot!$cmdvar\%$spot!g;
	}
	# Insert option into command line of CUPS raster driver
	if ($dat->{'currentcmd'} =~ m!\%Y!) {
	    next if !defined($userval) or $userval eq "";
	    $dat->{'currentcmd'} =~ s!\%Y!$name=$userval \%Y!g;
	}
	# Remove the marks telling that this option is currently controlled
	# by a composite option (setting "From<composite>")
        undef $arg->{'fromcomposite'};
	undef $arg->{'controlledby'};
    }
    

    ### Tidy up after computing option statements for all of P, J, and
    ### C types:

    ## C type finishing
    # Pluck out all of the %n's from the command line prototype
    my @letters = qw/A B C D E F G H I J K L M W X Y Z/;
    for my $spot (@letters) {
	# Remove the letter markers from the commandline
	$dat->{'currentcmd'} =~ s!\%$spot!!g;
    }

    ## J type finishing
    # Compute the proper stuff to say around the job

    if ((defined($dat->{'jcl'})) && (!$jobhasjcl)) {

	# Stick beginning of job cruft on the front of the jcl stuff...
	unshift (@jclprepend, $jclbegin);

	# Command to switch to the interpreter
	push (@jclprepend, $jcltointerpreter);
	
	# Arrange for JCL RESET command at end of job
	push (@jclappend, $jclend);

	# Put the JCL stuff into the data structure
	@{$dat->{'jclprepend'}} = @jclprepend;
	@{$dat->{'jclappend'}} = @jclappend;
    }

    ## G type finishing
    # Save PostScript options
    @{$dat->{'prologprepend'}} = @prologprepend;
    @{$dat->{'setupprepend'}} = @setupprepend;
    @{$dat->{'pagesetupprepend'}} = @pagesetupprepend;
    @{$dat->{'cupspagesetupprepend'}} = @cupspagesetupprepend;
}

sub buildpdqdriver {

    # Build a PDQ driver description file to use the given PPD file
    # together with foomatic-rip with the PDQ printing system

    # Foomatic data and name of the option set for the default settings
    my ($dat, $optionset) = @_;

    # Construct structure with driver information
    my @pdqdriver = ();

    # Construct option list
    my @driveropts = ();

    # Do we have a "Custom" setting for the page size?
    # Then we have to insert the following into the "filter_exec" script.
    my @setcustompagesize = ();

    # Fata for a custom page size, to allow a custom size as default
    my $pagewidth = 612;
    my $pageheight = 792;
    my $pageunit = "pt";



    ## First, compute the various option/value clauses
    for my $arg (@{$dat->{'args'}}) {

	if ($arg->{'type'} eq "enum") {
	    
	    # Option with only one choice, omit it, foomatic-rip will set 
	    # this choice anyway.
	    next if ($#{$arg->{'vals'}} < 1);

	    my $nam = $arg->{'name'};

	    # Omit "PageRegion" option, it does the same as "PageSize".
	    next if $nam eq "PageRegion";

	    my $com = $arg->{'comment'};

	    # Assure that the comment is not empty
	    if (!$com) {
		$com = $nam;
	    }

	    my $def = $arg->{$optionset};
	    $arg->{'varname'} = "$nam";
	    $arg->{'varname'} =~ s![\-\/\.]!\_!g;
	    my $varn = $arg->{'varname'};

	    # 1, if setting "PageSize=Custom" was found
	    # Then we must add options for page width and height
	    my $custompagesize = 0;

	    # If the default is a custom size we have to set also
	    # defaults for the width, height, and units of the page
	    if (($nam eq "PageSize") &&
		($def =~ /^Custom\.([\d\.]+)x([\d\.]+)([A-Za-z]*)$/)) {
		$def = "Custom";
		$pagewidth = $1;
		$pageheight = $2;
		$pageunit = $3;
	    }

	    # No quotes, thank you.
	    $com =~ s!\"!\\\"!g;
	    
	    push(@driveropts,
		 "  option {\n",
		 "    var = \"$varn\"\n",
		 "    desc = \"$com\"\n");
	    
	    # get enumeration values for each enum arg
	    my ($ev, @vals, @valstmp);
	    for $ev (@{$arg->{'vals'}}) {
		my $choiceshortname = $ev->{'value'};
		my $choicename = "${nam}_${choiceshortname}";
		my $val = " -o ${nam}=${choiceshortname}";
		my $com = $ev->{'comment'};

		# Assure that the comment is not empty
		if (!$com) {
		    $com = $choiceshortname;
		}

		# stick another choice on driveropts
		push(@valstmp,
		     "    choice \"$choicename\" {\n",
		     "      desc = \"$com\"\n",
		     "      value = \"$val\"\n",
		     "    }\n");
		if (($nam eq "PageSize") && 
		    ($choiceshortname eq "Custom")) {
		    $custompagesize = 1;
		    if ($#setcustompagesize < 0) {
			push(@setcustompagesize,
			     "      # Custom page size settings\n",
			     "      # We aren't really checking for " .
			     "legal vals.\n",
			     "      if [ \"x\${$varn}\" == 'x$val' ]; " .
			     "then\n",
			     "        $varn=\"\${$varn}.\${PageWidth}" .
			     "x\${PageHeight}\${PageSizeUnit}\"\n",
			     "      fi\n\n");
		    }
		}
	    }

	    push(@driveropts,
		 "    default_choice \"" . $nam . "_" . $def . "\"\n",
		 @valstmp,
		 "  }\n\n");

	    if ($custompagesize) {
		# Add options to set the custom page size
		push(@driveropts,
		     "  argument {\n",
		     "    var = \"PageWidth\"\n",
		     "    desc = \"Page Width (for \\\"Custom\\\" page " .
		     "size)\"\n",
		     "    def_value \"$pagewidth\"\n",
		     "    help = \"Minimum value: 0, Maximum value: " .
		     "100000\"\n",
		     "  }\n\n",
		     "  argument {\n",
		     "    var = \"PageHeight\"\n",
		     "    desc = \"Page Height (for \\\"Custom\\\" page " .
		     "size)\"\n",
		     "    def_value \"$pageheight\"\n",
		     "    help = \"Minimum value: 0, Maximum value: " .
		     "100000\"\n",
		     "  }\n\n",
		     "  option {\n",
		     "    var = \"PageSizeUnit\"\n",
		     "    desc = \"Unit (for \\\"Custom\\\" page size)\"\n",
		     "    default_choice \"PageSizeUnit_$pageunit\"\n",
		     "    choice \"PageSizeUnit_pt\" {\n",
		     "      desc = \"Points (1/72 inch)\"\n",
		     "      value = \"pt\"\n",
		     "    }\n",
		     "    choice \"PageSizeUnit_in\" {\n",
		     "      desc = \"Inches\"\n",
		     "      value = \"in\"\n",
		     "    }\n",
		     "    choice \"PageSizeUnit_cm\" {\n",
		     "      desc = \"cm\"\n",
		     "      value = \"cm\"\n",
		     "    }\n",
		     "    choice \"PageSizeUnit_mm\" {\n",
		     "      desc = \"mm\"\n",
		     "      value = \"mm\"\n",
		     "    }\n",
		     "  }\n\n");		
	    }
	    
	} elsif ($arg->{'type'} eq 'int' or $arg->{'type'} eq 'float') {
	    
	    my $nam = $arg->{'name'};
	    my $com = $arg->{'comment'};

	    # Assure that the comment is not empty
	    if (!$com) {
		$com = $nam;
	    }

	    my $def = $arg->{$optionset};
	    my $max = $arg->{'max'};
	    my $min = $arg->{'min'};
	    $arg->{'varname'} = "$nam";
	    $arg->{'varname'} =~ s![\-\/\.]!\_!g;
	    my $varn = $arg->{'varname'};
	    my $legal = $arg->{'legal'} = 
		"Minimum value: $min, Maximum value: $max";
	    
	    my $defstr = "";
	    if ($def) {
		$defstr = sprintf("    def_value \"%s\"\n", $def);
	    }
	    
	    push(@driveropts,
		 "  argument {\n",
		 "    var = \"$varn\"\n",
		 "    desc = \"$com\"\n",
		 $defstr,
		 "    help = \"$legal\"\n",
		 "  }\n\n");
	    
	} elsif ($arg->{'type'} eq 'bool') {
	    
	    my $nam = $arg->{'name'};
	    my $com = $arg->{'comment'};

	    # Assure that the comment is not empty
	    if (!$com) {
		$com = $nam;
	    }

	    my $tcom = $arg->{'comment_true'};
	    my $fcom = $arg->{'comment_false'};
	    my $def = $arg->{$optionset};
	    $arg->{'legal'} = "Value is a boolean flag";
	    $arg->{'varname'} = "$nam";
	    $arg->{'varname'} =~ s![\-\/\.]!\_!g;
	    my $varn = $arg->{'varname'};
	    
	    my $defstr = "";
	    if ($def) {
		$defstr = sprintf("    default_choice \"%s\"\n", 
				  $def ? "$nam" : "no$nam");
	    } else {
		$defstr = sprintf("    default_choice \"%s\"\n", "no$nam");
	    }
	    push(@driveropts,
		 "  option {\n",
		 "    var = \"$varn\"\n",
		 "    desc = \"$com\"\n",
		 $defstr,
		 "    choice \"$nam\" {\n",
		 "      desc = \"$tcom\"\n",
		 "      value = \" -o $nam=True\"\n",
		 "    }\n",
		 "    choice \"no$nam\" {\n",
		 "      desc = \"$fcom\"\n",
		 "      value = \" -o $nam=False\"\n",
		 "    }\n",
		 "  }\n\n");

	} elsif ($arg->{'type'} eq 'string' or $arg->{'type'} eq 'password') {
	    
	    my $nam = $arg->{'name'};
	    my $com = $arg->{'comment'};

	    # Assure that the comment is not empty
	    if (!$com) {
		$com = $nam;
	    }

	    my $def = $arg->{$optionset};
	    my $maxlength = $arg->{'maxlength'};
	    my $proto = $arg->{'proto'};
	    $arg->{'varname'} = "$nam";
	    $arg->{'varname'} =~ s![\-\/\.]!\_!g;
	    my $varn = $arg->{'varname'};

	    my $legal;
            if (defined($maxlength)) {
                $legal .= "Maximum length: $maxlength characters, ";
            }
            $legal .= "Examples/special settings: ";
            for (@{$arg->{'vals'}}) {
                my ($value, $comment, $driverval) = 
		    ($_->{'value'}, $_->{'comment'}, $_->{'driverval'});
		# Retrieve the original string from the prototype
		# and the driverval
		my $string;
		if ($proto) {
		    my $s = index($proto, '%s');
		    my $l = length($driverval) - length($proto) + 2;
		    if (($s < 0) || ($l < 0)) {
			$string = $driverval;
		    } else {
			$string = substr($driverval, $s, $l);
		    }
		} else {
		    $string = $driverval;
		}
		if ($value ne $string) {
		    $legal .= "${value}: \\\"$string\\\"";
		} else {
		    $legal .= "\\\"$value\\\"";
		}
		if ($comment && ($value ne $comment) && 
		    ($string ne $comment) && 
		    (($value ne 'None') || ($comment ne '(None)'))) {
		    $legal .= " ($comment)";
		}
		$legal .= "; ";
	    }
	    $legal =~ s/; $//;

	    $arg->{'legal'} = $legal;
	    
	    my $defstr = "";
	    if ($def) {
		$defstr = sprintf("    def_value \"%s\"\n", $def);
	    }
	    
	    push(@driveropts,
		 "  argument {\n",
		 "    var = \"$varn\"\n",
		 "    desc = \"$com\"\n",
		 $defstr,
		 "    help = \"$legal\"\n",
		 "  }\n\n");
	    
	}
	
    }
    


    ## Define the "docs" option to print the driver documentation page

    push(@driveropts,
	 "  option {\n",
	 "    var = \"DRIVERDOCS\"\n",
	 "    desc = \"Print driver usage information\"\n",
	 "    default_choice \"nodocs\"\n", 
	 "    choice \"docs\" {\n",
	 "      desc = \"Yes\"\n",
	 "      value = \" -o docs\"\n",
	 "    }\n",
	 "    choice \"nodocs\" {\n",
	 "      desc = \"No\"\n",
	 "      value = \"\"\n",
	 "    }\n",
	 "  }\n\n");
    


    ## Build the "foomatic-rip" command line
    my $commandline = "foomatic-rip --pdq";
    if ($printer) {
	$commandline .= " -P $printer";
    } else {
	# Make sure that the PPD file is entered with an absolute path
	if ($ppdfile !~ m!^/!) {	
	    my $pwd = cwd;
	    $ppdfile = "$pwd/$ppdfile";
	}
	$commandline .= " --ppd=$ppdfile";
    }
    for my $arg (@{$dat->{'args'}}) {
	if ($arg->{'varname'}) {
	    $commandline .= "\${$arg->{'varname'}}";
	}
    }
    $commandline .= "\${DRIVERDOCS} \$INPUT > \$OUTPUT";


        
    ## Now we generate code to build the command line snippets for the
    ## numerical options

    my @psfilter;
    for my $arg (@{$dat->{'args'}}) {
		
	# Only numerical and string options need to be treated here
	next if (($arg->{'type'} ne 'int') && 
		 ($arg->{'type'} ne 'float') &&
		 ($arg->{'type'} ne 'string') &&
		 ($arg->{'type'} ne 'password'));

	my $comment = $arg->{'comment'};
	my $name = $arg->{'name'};
	my $varname = $arg->{'varname'};
		    
	# If the option's variable is non-null, put in the
	# argument.  Otherwise this option is the empty
	# string.  Error checking?
		    
	push(@psfilter,
	     "      # $comment\n",
	     (($arg->{'type'} eq 'int') || ($arg->{'type'} eq 'float') ?
	      ("      # We aren't really checking for max/min,\n",
	       "      # this is done by foomatic-rip\n",
	       "      if [ \"x\${$varname}\" != 'x' ]; then\n  ") : ""),
	     #"      $varname=`echo \${$varname} | perl -p -e \"s/'/'\\\\\\\\\\\\\\\\''/g\"`\n",
	     "      $varname=\" -o $name='\${$varname}'\"\n",
	     (($arg->{'type'} eq 'int') || ($arg->{'type'} eq 'float') ?
	      "      fi\n" : ""),
	     "\n");
    }

    # Command execution

    push(@psfilter,
	 "      if ! test -e \$INPUT.ok; then\n",
	 "        sh -c \"$commandline\"\n",
	 "        if ! test -e \$OUTPUT; then \n",
	 "          echo 'Error running foomatic-rip; no output!'\n",
	 "          exit 1\n",
	 "        fi\n",
	 "      else\n",
	 "        ln -s \$INPUT \$OUTPUT\n",
	 "      fi\n\n");
    
    my $version = time();
    my $name = "$model-$version";
    $name =~ s/\W/\-/g;
    $name =~ s/\-+/\-/g;
    
    my $pname = $model;
    
    push (@pdqdriver,
	  "driver \"$name\" {\n\n",
	  "  # This PDQ driver declaration file was generated " .
	  "automatically by\n",
	  "  # foomatic-rip from information in the file $ppdfile.\n",
	  "  # It allows printing with PDQ on the $pname.\n",
	  "\n",
	  "  requires \"foomatic-rip\"\n\n",
	  @driveropts,
	  "  language_driver all {\n",
	  "    # We accept all file types and pass them to foomatic-rip\n",
	  "    # (invoked in \"filter_exec {}\" section) without\n", 
          "    # pre-filtering\n",
	  "    filetype_regx \"\"\n",
	  "    convert_exec {\n",
	  "      ln -s \$INPUT \$OUTPUT\n",
	  "    }\n",
	  "  }\n\n",
	  "  filter_exec {\n",
	  @setcustompagesize,
	  @psfilter,
	  "  }\n",
	  "}\n");
    
    return @pdqdriver;

}



# Emacs tabulator/indentation

### Local Variables:
### tab-width: 8
### perl-indent-level: 4
### End:
