diff options
author | Didier Raboud <didier@raboud.com> | 2010-05-22 08:38:12 +0200 |
---|---|---|
committer | Didier Raboud <didier@raboud.com> | 2010-05-22 08:38:12 +0200 |
commit | ff7f3eaf811f597b94aa2a93a6bc349d2bec8dfd (patch) | |
tree | a226166a9e7c1d716b7d8e9ee6c3066a0655ad78 /foomatic-rip.in |
Imported Upstream version 3.0.2-20050114upstream/3.0.2-20050114
Diffstat (limited to 'foomatic-rip.in')
-rwxr-xr-x | foomatic-rip.in | 6310 |
1 files changed, 6310 insertions, 0 deletions
diff --git a/foomatic-rip.in b/foomatic-rip.in new file mode 100755 index 0000000..177b372 --- /dev/null +++ b/foomatic-rip.in @@ -0,0 +1,6310 @@ +#!@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.9 $'; +#'# 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 = "@prefix@/bin:/usr/local/bin:/usr/bin:/bin"; + +# CUPS raster drivers are searched here +my $cupsfilterpath = "@prefix@/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 = "@prefix@"; +my $configpath = "@sysconfdir@/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 + close $logh; + 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') { + # 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"; + + + +## 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-/) && + ($line !~ m/EPSF/)) { + # 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*(.*)\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/\'/\'/g; + $str =~ s/\"/\"/g; + $str =~ s/\>/\>/g; + $str =~ s/\</\</g; + $str =~ s/\&/\&/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'}; + $chars =~ s/(?<!\\)((\\\\)*)\//$2\\\//g; + return 0 if $value !~ /^[$chars]*$/; + } + + # Regular expression + if ($arg->{'allowedregexp'}) { + my $regexp = $arg->{'allowedregexp'}; + $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: |