//
// PDF-to-PostScript filter function for libppd.
//
// Copyright 2011-2020 by Till Kamppeter
// Copyright 2007-2011 by Apple Inc.
// Copyright 1997-2006 by Easy Software Products.
//
// Licensed under Apache License v2.0.  See the file "LICENSE" for more
// information.
//
// Contents:
//
//   parsePDFTOPDFComment() - Check whether we are executed after pdftopdf
//   remove_options()       - Remove unwished entries from an option list
//   log_command_line()     - Log the command line of a program which we call
//   ppdFilterPDFToPS()     - pdftops filter function
//

//
// Include necessary headers...
//

#include <config.h>
#include <cups/cups.h>
#include <cups/file.h>
#include <signal.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <ctype.h>
#include <cupsfilters/filter.h>
#include <cupsfilters/raster.h>
#include <cupsfilters/ipp.h>
#include <cupsfilters/pdf.h>
#include <cupsfilters/image.h>
#include "ppd.h"
#include "ppd-filter.h"

#define MAX_CHECK_COMMENT_LINES	20

//
// Type definitions
//

typedef unsigned renderer_t;
enum renderer_e
{
  GS         = 0,
  PDFTOPS    = 1,
  ACROREAD   = 2,
  PDFTOCAIRO = 3,
  MUPDF      = 4,
  HYBRID     = 5
};


//
// When calling the "pstops" filter we exclude the following options from its
// command line as we have applied these options already to the PDF input,
// either on the "pdftops"/Ghostscript call in this filter or by use of the
// "pdftopdf" filter before this filter.
//

const char *pstops_exclude_general[] =
{
  "crop-to-fit",
  "fill",
  "fitplot",
  "fit-to-page",
  "landscape",
  "orientation-requested",
  NULL
};

const char *pstops_exclude_page_management[] =
{
  "brightness",
  "Collate",
  "even-duplex",
  "gamma",
  "hue",
  "ipp-attribute-fidelity",
  "MirrorPrint",
  "mirror",
  "multiple-document-handling",
  "natural-scaling",
  "number-up",
  "number-up-layout",
  "OutputOrder",
  "page-border",
  "page-bottom",
  "page-label",
  "page-left",
  "input-page-ranges",
  "page-ranges",
  "page-right",
  "page-set",
  "page-top",
  "position",
  "saturation",
  "scaling",
  NULL
};

//
// Check whether we were called after the "pdftopdf" filter and extract
// parameters passed over by "pdftopdf" in the header comments of the PDF
// file
//

static void
parse_pdftopdf_comment(char *filename,		// I - Input file
		       int *pdftopdfapplied,	// O - Does the input
						//     data come from
						//     pdftopdf filter?
		       char *deviceCopies,	// O - Number of copies
						//     (hardware)
		       int *deviceCollate,	// O - Hardware collate
		       cf_logfunc_t log,	// I - Log function
		       void *ld)		// I - Aux. data for
						//     log function
{
  char buf[4096];
  int i;
  FILE *fp;

  if ((fp = fopen(filename, "rb")) == NULL)
  {
    if (log) log(ld, CF_LOGLEVEL_ERROR,
		 "ppdFilterPDFToPS: Cannot open input file \"%s\"",
		 filename);
    return;
  }

  // skip until PDF start header
  while (fgets(buf, sizeof(buf), fp) != 0)
  {
    if (strncmp(buf, "%PDF", 4) == 0)
      break;
  }
  for (i = 0; i < MAX_CHECK_COMMENT_LINES; i ++)
  {
    if (fgets(buf, sizeof(buf), fp) == 0)
      break;
    if (strncmp(buf, "%%PDFTOPDFNumCopies", 19) == 0)
    {
      char *p;

      p = strchr(buf + 19, ':') + 1;
      while (*p == ' ' || *p == '\t')
	p ++;
      strncpy(deviceCopies, p, 31);
      deviceCopies[31] = '\0';
      p = deviceCopies + strlen(deviceCopies) - 1;
      while (*p == ' ' || *p == '\t'  || *p == '\r'  || *p == '\n')
	p --;
      *(p + 1) = '\0';
      *pdftopdfapplied = 1;
    }
    else if (strncmp(buf, "%%PDFTOPDFCollate", 17) == 0)
    {
      char *p;

      p = strchr(buf + 17, ':') + 1;
      while (*p == ' ' || *p == '\t')
	p ++;
      if (strncasecmp(p, "true", 4) == 0)
	*deviceCollate = 1;
      else
	*deviceCollate = 0;
      *pdftopdfapplied = 1;
    }
    else if (strcmp(buf, "% This file was generated by pdftopdf") == 0)
      *pdftopdfapplied = 1;
  }

  fclose(fp);
}


//
// Check whether given file is empty
//

static int                     // O - Result: 1: Empty; 0: Contains pages
is_empty(char *filename,       // I - Input file
	 cf_logfunc_t log,     // I - Log function
	 void *ld)             // I - Auxiliary data for log function
{
  FILE *fp = NULL;
  fp = fopen(filename, "rb");
  if (fp == NULL)
  {
    if (log) log(ld, CF_LOGLEVEL_ERROR,
		 "ppdFilterPDFToPS: Cannot open input file \"%s\"",
		 filename);
    return (1);
  }
  else
  {
    char buf[1];
    rewind(fp);
    if (fread(buf, 1, 1, fp) == 0)
    {
      fclose(fp);
      if (log) log(ld, CF_LOGLEVEL_DEBUG,
		   "ppdFilterPDFToPS: Input is empty, outputting empty file.");
      return (1);
    }
    fclose(fp);
    int pages = cfPDFPages(filename);
    if (pages == 0)
    {
      if (log) log(ld, CF_LOGLEVEL_DEBUG,
		   "ppdFilterPDFToPS: No pages left, outputting empty file.");
      return (1);
    }
    if (pages > 0)
      return (0);
    return (1);
  }
}


//
// Before calling any command line utility, log its command line in CUPS'
// debug mode
//

void
log_command_line(const char* file,     // I - Program to be executed
		 char *const argv[],   // I - Argument list
		 cf_logfunc_t log,     // I - Log function
		 void *ld)             // I - Auxiliary data for log function
{
  int i;
  char *apos;
  char buf[32768];

  if (log == NULL)
    return;

  // Debug output: Full command line of program to be called
  snprintf(buf, sizeof(buf) - 1,
	   "ppdFilterPDFToPS: Running command line for %s:",
	   (file ? file : argv[0]));
  if (file)
    snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf) - 1,
	     " %s", file);
  for (i = (file ? 1 : 0); argv[i]; i ++)
  {
    if ((strchr(argv[i],' ')) || (strchr(argv[i],'\t')))
      apos = "'";
    else
      apos = "";
    snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf) - 1,
	     " %s%s%s", apos, argv[i], apos);
  }
  buf[sizeof(buf) - 1] = '\0';

  log(ld, CF_LOGLEVEL_DEBUG, "%s", buf);
}


//
// 'ppdFilterPDFToPS()' - Filter function to convert PDF input into
//                        PostScript to be printed on PostScript printers
//

int					// O - Error status
ppdFilterPDFToPS(int inputfd,		// I - File descriptor input stream
		 int outputfd,		// I - File descriptor output stream
		 int inputseekable,	// I - Is input stream seekable?
					//     (unused)
		 cf_filter_data_t *data,// I - Job and printer data
		 void *parameters)	// I - Filter-specific parameters
					//     (unused)
{
  ppd_filter_data_ext_t *filter_data_ext =
    (ppd_filter_data_ext_t *)cfFilterDataGetExt(data,
						PPD_FILTER_DATA_EXT);
  renderer_t    renderer = CUPS_PDFTOPS_RENDERER; // Renderer: gs or pdftops
						  // or acroread or pdftocairo
						  // or hybrid
  FILE          *inputfp;		// Input file pointer
  int		fd = 0;			// Copy file descriptor
  int           i, j;
  int		pdftopdfapplied = 0;    // Input data from pdftopdf filter?
  char		deviceCopies[32] = "1"; // Hardware copies
  int		deviceCollate = 0;      // Hardware collate
  char          make_model[128] = "";   // Printer make and model (for quirks)
  char		*filename,		// PDF file to convert
		tempfile[1024];		// Temporary file
  char		buffer[8192];		// Copy buffer
  int		bytes;			// Bytes copied
  int		num_options = 0,	// Number of options
                num_pstops_options;	// Number of options for pstops
  cups_option_t	*options = NULL,	// Options
                *pstops_options,	// Options for pstops filter function
                *option;
  const char    *exclude;
  cf_filter_data_t pstops_filter_data;
  int           ret;
  const char	*val;			// Option value
  ppd_file_t	*ppd = NULL;		// PPD file
  char		resolution[128] = "";   // Output resolution
  int           xres = 0, yres = 0,     // resolution values
                mres, res,
                maxres = CUPS_PDFTOPS_MAX_RESOLUTION,
                                        // Maximum image rendering resolution
                numvalues = 0;          // Number of values actually read
  ppd_choice_t  *choice;
  ppd_attr_t    *attr;
  cups_page_header2_t header;
  cups_file_t	*fp;			// Post-processing input file
  int		pdf_pid,		// Process ID for pdftops/gs
		pdf_argc = 0,		// Number of args for pdftops/gs
		pstops_pid = 0,		// Process ID of pstops filter
		pstops_pipe[2],		// Pipe to pstops filter
		need_post_proc = 0,     // Post-processing needed?
		post_proc_pid = 0,	// Process ID of post-processing
		post_proc_pipe[2],	// Pipe to post-processing
		wait_children,		// Number of child processes left
		wait_pid,		// Process ID from wait()
		wait_status,		// Status from child
		exit_status = 0;	// Exit status
  int		gray_output = 0;	// Checking for monochrome/grayscale
					// PostScript output
  char		*pdf_argv[100],		// Arguments for pdftops/gs
		*ptr;			// Pointer into value
  int		duplex, tumble;         // Duplex settings for PPD-less
					// printing
  cups_cspace_t cspace = (cups_cspace_t)(-1);
  cf_logfunc_t	log = data->logfunc;
  void		*ld = data->logdata;
  cf_filter_iscanceledfunc_t iscanceled = data->iscanceledfunc;
  void		*icd = data->iscanceleddata;
  ipp_t		*printer_attrs = data->printer_attrs;
  ipp_t		*job_attrs = data->job_attrs;
  ipp_attribute_t *ipp;


  (void)inputseekable;
  (void)parameters;

  //
  // Ignore broken pipe signals...
  //

  signal(SIGPIPE, SIG_IGN);

  //
  // Open the input data stream specified by the inputfd...
  //

  if ((inputfp = fdopen(inputfd, "r")) == NULL)
  {
    if (!iscanceled || !iscanceled(icd))
    {
      if (log) log(ld, CF_LOGLEVEL_DEBUG,
		   "ppdFilterPDFToPS: Unable to open input data stream.");
    }

    return (1);
  }

  //
  // Copy input into temporary file ...
  //

  if ((fd = cupsTempFd(tempfile, sizeof(tempfile))) < 0)
  {
    if (log) log(ld, CF_LOGLEVEL_ERROR,
		 "ppdFilterPDFToPS: Unable to copy PDF file: %s", strerror(errno));

    fclose(inputfp);
    return (1);
  }

  if (log) log(ld, CF_LOGLEVEL_DEBUG,
	       "ppdFilterPDFToPS: Copying input to temp file \"%s\"",
	       tempfile);

  while ((bytes = fread(buffer, 1, sizeof(buffer), inputfp)) > 0)
    bytes = write(fd, buffer, bytes);

  if (inputfd)
  {
    fclose(inputfp);
    close(inputfd);
  }
  close(fd);

  filename = tempfile;

  //
  // Stop on empty or zero-pages files without error, perhaps we eliminated
  // all pages via the "page-ranges" option and a previous filter
  //

  if (is_empty(filename, log, ld))
  {
    unlink(tempfile);
    return 0;
  }

  //
  // Read out copy counts and collate setting passed over by pdftopdf
  //

  parse_pdftopdf_comment(filename, &pdftopdfapplied, deviceCopies,
			 &deviceCollate, log, ld);

  //
  // CUPS option list
  //

  num_options = cfJoinJobOptionsAndAttrs(data, num_options, &options);
  

  if (filter_data_ext)
    ppd = filter_data_ext->ppd;

  //
  // Process job options...
  //

  if ((val = cupsGetOption("make-and-model", num_options, options)) != NULL)
  {
    strncpy(make_model, val, sizeof(make_model) - 1);
    if (strlen(val) > 127)
      make_model[127] = '\0';
    for (ptr = make_model; *ptr; ptr ++)
      if (*ptr == '-') *ptr = ' ';
  }
  else if(printer_attrs)
  {
    if ((ipp = ippFindAttribute(printer_attrs, "printer-make-and-model",
				IPP_TAG_ZERO)) != NULL)
    {
      char make[56];
      char* model;
      ippAttributeString(ipp, make, sizeof(make));
      if (!strncasecmp(make, "Hewlett Packard ", 16) ||
	  !strncasecmp(make, "Hewlett-Packard ", 16))
      {
	model = make + 16;
	strncpy(make, "HP", sizeof(make));
      }
      else if ((model = strchr(make, ' ')) != NULL)
        *model++ = '\0';
      else
        model = make;
      snprintf(make_model, sizeof(make_model), "%s %s", make, model);
    }
  }
  else if (ppd)
  {
    snprintf(make_model, sizeof(make_model), "%s %s", ppd->manufacturer,
	     ppd->product + 1);
    make_model[strlen(make_model) - 1] = '\0';
  }
  if (log) log(ld, CF_LOGLEVEL_DEBUG,
	       "ppdFilterPDFToPS: Printer make and model: %s", make_model);

  //
  // Select the PDF renderer: Ghostscript (gs), Poppler (pdftops),
  // Adobe Reader (arcoread), Poppler with Cairo (pdftocairo), or
  // Hybrid (hybrid, Poppler for Brother, Minolta, Konica Minolta, Dell, and
  // old HP LaserJets and Ghostscript otherwise)
  //

  if ((val = cupsGetOption("pdftops-renderer", num_options, options)) != NULL)
  {
    if (strcasecmp(val, "gs") == 0)
      renderer = GS;
    else if (strcasecmp(val, "pdftops") == 0)
      renderer = PDFTOPS;
    else if (strcasecmp(val, "acroread") == 0)
      renderer = ACROREAD;
    else if (strcasecmp(val, "pdftocairo") == 0)
      renderer = PDFTOCAIRO;
    else if (strcasecmp(val, "mupdf") == 0)
      renderer = MUPDF;
    else if (strcasecmp(val, "hybrid") == 0)
      renderer = HYBRID;
    else
      if (log) log(ld, CF_LOGLEVEL_WARN,
		   "ppdFilterPDFToPS: Invalid value for \"pdftops-renderer\": \"%s\"",
		   val);
  }

  if (renderer == HYBRID)
  {
    if (make_model[0] &&
	(!strncasecmp(make_model, "Brother", 7) ||
	 !strncasecmp(make_model, "Dell", 4) ||
	 strcasestr(make_model, "Minolta") ||
	 (!strncasecmp(make_model, "Apple", 5) &&
	  (ptr = strcasestr(make_model, "LaserWriter")))))
    {
      if (log) log(ld, CF_LOGLEVEL_DEBUG,
		   "ppdFilterPDFToPS: Switching to Poppler's pdftops instead of "
		   "Ghostscript for Brother, Minolta, Konica Minolta, Dell, "
		   "and Apple LaserWriter printers to work around bugs in the "
		   "printer's PS interpreters");
      renderer = PDFTOPS;
    }
    else
      renderer = GS;

    //
    // Use Poppler instead of Ghostscript for old HP LaserJet printers due to
    // a bug in their PS interpreters. They are very slow with Ghostscript.
    // A LaserJet is considered old if its model number does not have a letter
    // in the beginning, like LaserJet 3 or LaserJet 4000, not LaserJet P2015.
    // See https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=742765
    //

    if (make_model[0] &&
	((!strncasecmp(make_model, "HP", 2) ||
	  !strncasecmp(make_model, "Hewlett-Packard", 15) ||
	  !strncasecmp(make_model, "Hewlett Packard", 15)) &&
	 (ptr = strcasestr(make_model, "LaserJet"))))
    {
      for (ptr += 8; *ptr; ptr ++)
      {
	if (isspace(*ptr)) continue;
	if (isdigit(*ptr))
	{
	  while (*ptr && isalnum(*ptr))
	    ptr ++;
	  if (!*ptr) // End of string, no further word
	  {
	    if (log) log(ld, CF_LOGLEVEL_DEBUG,
			 "ppdFilterPDFToPS: Switching to Poppler's pdftops instead of "
			 "Ghostscript for old HP LaserJet (\"LaserJet "
			 "<number>\", no letters before <number>, no "
			 "additional words after <number>) printers to "
			 "work around bugs in the printer's PS interpreters");
	    renderer = PDFTOPS;
	  }
	}
	break;
      }
    }
  }

  //
  // Create option list for pstops filter function, stripping options which
  // "pstops" does not need to apply any more
  //

  num_pstops_options = 0;
  pstops_options = NULL;
  for (i = num_options, option = options; i > 0; i --, option ++)
  {
    for (j = 0, exclude = pstops_exclude_general[j]; exclude;
	 j ++, exclude = pstops_exclude_general[j])
      if (!strcasecmp(option->name, exclude))
	break;
    if (exclude)
      continue;
    if (pdftopdfapplied)
    {
      for (j = 0, exclude = pstops_exclude_page_management[j]; exclude;
	   j++, exclude = pstops_exclude_page_management[j])
	if (!strcasecmp(option->name, exclude))
	  break;
      if (exclude)
	continue;
    }
    num_pstops_options = cupsAddOption(option->name,
				       option->value,
				       num_pstops_options, &pstops_options);
  }

  if (pdftopdfapplied && deviceCollate)
  {
    //
    // Add collate option to the pstops call if pdftopdf has found out that the
    // printer does hardware collate.
    //

    num_pstops_options = cupsAddOption("Collate",
				       "True",
				       num_pstops_options, &pstops_options);
  }

  //
  // Create data record to call the pstops filter function
  //

  pstops_filter_data.job_id = data->job_id;
  pstops_filter_data.job_user = data->job_user;
  pstops_filter_data.job_title = data->job_title;
  if (pdftopdfapplied)
    pstops_filter_data.copies = atoi(deviceCopies);
  else
    pstops_filter_data.copies = data->copies;
  pstops_filter_data.job_attrs = NULL;
  pstops_filter_data.printer_attrs = NULL;
  pstops_filter_data.num_options = num_pstops_options;
  pstops_filter_data.options = pstops_options;
  pstops_filter_data.extension = data->extension;
  pstops_filter_data.logfunc = log;
  pstops_filter_data.logdata = ld;
  pstops_filter_data.iscanceledfunc = iscanceled;
  pstops_filter_data.iscanceleddata = icd;

  //
  // Force monochrome/grayscale PostScript output 
  // if job is to be printed in monochrome/grayscale
  //

  if (ppd && ppd->color_device == 0)  // Monochrome printer
    gray_output = 1;
  else  //Color Printer - user option for Grayscale
  {
    if ((val = cupsGetOption("pwg-raster-document-type", num_options,
			     options)) != NULL ||
	(val = cupsGetOption("PwgRasterDocumentType", num_options,
			     options)) != NULL ||
	(val = cupsGetOption("print-color-mode", num_options,
			     options)) != NULL ||
	(val = cupsGetOption("PrintColorMode", num_options,
			     options)) != NULL ||
	(val = cupsGetOption("color-space", num_options,
			     options)) != NULL ||
	(val = cupsGetOption("ColorSpace", num_options,
			     options)) != NULL ||
	(val = cupsGetOption("color-model", num_options,
			     options)) != NULL ||
	(val = cupsGetOption("ColorModel", num_options,
			     options)) != NULL ||
	(val = cupsGetOption("output-mode", num_options,
			     options)) != NULL ||
	(val = cupsGetOption("OutputMode", num_options,
			     options)) != NULL)
    {
      if (strcasestr(val, "Black") ||
	  strcasestr(val, "Gray") ||
	  strcasestr(val, "Mono"))
	gray_output = 1;
    }
    else
    {
      if(job_attrs != NULL)
      {
	if ((ipp = ippFindAttribute(job_attrs, "pwg-raster-document-type",
				    IPP_TAG_ZERO)) != NULL ||
	    (ipp = ippFindAttribute(job_attrs, "color-space",
				    IPP_TAG_ZERO)) != NULL ||
	    (ipp = ippFindAttribute(job_attrs, "color-model",
				    IPP_TAG_ZERO)) != NULL ||
	    (ipp = ippFindAttribute(job_attrs, "print-color-mode",
				    IPP_TAG_ZERO)) != NULL ||
	    (ipp = ippFindAttribute(job_attrs, "output-mode",
				    IPP_TAG_ZERO)) != NULL)
	{
	  ippAttributeString(ipp, buffer, sizeof(buffer));
	  val = buffer;
	  if(strcasestr(val, "Black") ||
	     (strcasestr(val, "Gray")) ||
	     (strcasestr(val, "Mono")))
	    gray_output = 1;
	}
      }
    }
  }

  //
  // Build the command-line for the ppdFilterPDFToPS, gs, mutool, pdftocairo, or
  // acroread filter...
  //

  if (renderer == PDFTOPS)
  {
    pdf_argv[0] = (char *)"pdftops";
    pdf_argc    = 1;
  }
  else if (renderer == GS)
  {
    pdf_argv[0] = (char *)"gs";
    pdf_argv[1] = (char *)"-q";
    pdf_argv[2] = (char *)"-dNOPAUSE";
    pdf_argv[3] = (char *)"-dBATCH";
    pdf_argv[4] = (char *)"-dSAFER";
    pdf_argv[5] = (char *)"-dNOMEDIAATTRS";
    pdf_argv[6] = (char *)"-sstdout=%stderr";
#    ifdef HAVE_GHOSTSCRIPT_PS2WRITE
    pdf_argv[7] = (char *)"-sDEVICE=ps2write";
#    else
    pdf_argv[7] = (char *)"-sDEVICE=pswrite";
#    endif // HAVE_GHOSTSCRIPT_PS2WRITE
    pdf_argv[8] = (char *)"-dShowAcroForm";
    pdf_argv[9] = (char *)"-sOUTPUTFILE=%stdout";
    if (gray_output == 1) // Checking for monochrome/grayscale PostScript
                          // output
    {
      pdf_argv[10] = (char *)"-sProcessColorModel=DeviceGray";
      pdf_argv[11] = (char *)"-sColorConversionStrategy=Gray";
      pdf_argc = 12;
    }
    else
      pdf_argc = 10;
  }
  else if (renderer == MUPDF)
  {
    pdf_argv[0] = (char *)"mutool";
    pdf_argv[1] = (char *)"draw";
    pdf_argv[2] = (char *)"-L";
    pdf_argv[3] = (char *)"-smtf";
    pdf_argv[4] = (char *)"-Fps";
    pdf_argv[5] = (char *)"-o-";
    if (gray_output == 1) // Checking for monochrome/grayscale PostScript
                          // output
      pdf_argv[6] = (char *)"-cgray";
    else
      pdf_argv[6] = (char *)"-crgb";
    pdf_argc = 7;
  }
  else if (renderer == PDFTOCAIRO)
  {
    pdf_argv[0] = (char *)"pdftocairo";
    pdf_argv[1] = (char *)"-ps";
    pdf_argc    = 2;
  }
  else if (renderer == ACROREAD)
  {
    pdf_argv[0] = (char *)"acroread";
    pdf_argv[1] = (char *)"-toPostScript";
    pdf_argc    = 2;
  }

  //
  // Set language level and TrueType font handling...
  //

  if (ppd)
  {
    if (ppd->language_level == 1)
    {
      if (renderer == PDFTOPS)
      {
	pdf_argv[pdf_argc++] = (char *)"-level1";
	pdf_argv[pdf_argc++] = (char *)"-noembtt";
      }
      else if (renderer == GS)
	pdf_argv[pdf_argc++] = (char *)"-dLanguageLevel=1";
      else if (renderer == PDFTOCAIRO)
      {
	if (log) log(ld, CF_LOGLEVEL_WARN,
		     "ppdFilterPDFToPS: Level 1 PostScript not supported by "
		     "pdftocairo.");
      }
      else if (renderer == ACROREAD)
      {
	if (log) log(ld, CF_LOGLEVEL_WARN,
		     "ppdFilterPDFToPS: Level 1 PostScript not supported by acroread.");
      }
      else if (renderer == MUPDF)
      {
	if (log) log(ld, CF_LOGLEVEL_WARN,
		     "ppdFilterPDFToPS: Level 1 PostScript not supported by mutool.");
      }
    }
    else if (ppd->language_level == 2)
    {
      if (renderer == PDFTOPS)
      {
	pdf_argv[pdf_argc++] = (char *)"-level2";
	if (!ppd->ttrasterizer)
	  pdf_argv[pdf_argc++] = (char *)"-noembtt";
      }
      else if (renderer == GS)
	pdf_argv[pdf_argc++] = (char *)"-dLanguageLevel=2";
      else if (renderer != MUPDF) // MuPDF is PS level 2 only
	// PDFTOCAIRO, ACROREAD
        pdf_argv[pdf_argc++] = (char *)"-level2";
    }
    else
    {
      if (renderer == PDFTOPS)
      {
        // Do not emit PS Level 3 with Poppler on Brother and HP PostScript
	// laser printers as some do not like it.
	// See https://bugs.launchpad.net/bugs/277404 and
	// https://bugs.launchpad.net/bugs/1306849 comment #42.
	if (!make_model[0] ||
	    !strncasecmp(make_model, "Brother", 7))
	  pdf_argv[pdf_argc++] = (char *)"-level2";
	else
	  pdf_argv[pdf_argc++] = (char *)"-level3";
      }
      else if (renderer == GS)
      {
        // Do not emit PS Level 3 with Ghostscript on Brother PostScript
	// laser printers as some do not like it.
	// See https://bugs.launchpad.net/bugs/1306849 comment #42.
	if (!make_model[0] ||
	    !strncasecmp(make_model, "Brother", 7))
	  pdf_argv[pdf_argc++] = (char *)"-dLanguageLevel=2";
	else
	  pdf_argv[pdf_argc++] = (char *)"-dLanguageLevel=3";
      }
      else if (renderer == MUPDF)
      {
	if (log) log(ld, CF_LOGLEVEL_WARN,
		     "ppdFilterPDFToPS: Level 3 PostScript not supported by mutool.");
      }
      else // PDFTOCAIRO || ACROREAD
        pdf_argv[pdf_argc++] = (char *)"-level3";
    }
  }
  else
  {
    if (renderer == PDFTOPS)
    {
      // Do not emit PS Level 3 with Poppler on HP PostScript laser printers
      // as some do not like it. See https://bugs.launchpad.net/bugs/277404.
      if (!make_model[0] ||
	  ((!strncasecmp(make_model, "HP", 2) ||
	    !strncasecmp(make_model, "Hewlett-Packard", 15) ||
	    !strncasecmp(make_model, "Hewlett Packard", 15)) &&
	   (strcasestr(make_model, "LaserJet"))))
	pdf_argv[pdf_argc++] = (char *)"-level2";
      else
	pdf_argv[pdf_argc++] = (char *)"-level3";
      pdf_argv[pdf_argc++] = (char *)"-noembtt";
    }
    else if (renderer == GS)
      pdf_argv[pdf_argc++] = (char *)"-dLanguageLevel=3";
    else if (renderer != MUPDF) // MuPDF is PS level 2 only
      // PDFTOCAIRO || ACROREAD
      pdf_argv[pdf_argc++] = (char *)"-level3";
  }

#ifdef HAVE_POPPLER_PDFTOPS_WITH_ORIGPAGESIZES
  if ((renderer == PDFTOPS) || (renderer == PDFTOCAIRO))
  {
    //
    // Use the page sizes of the original PDF document, this way documents
    // which contain pages of different sizes can be printed correctly
    //

    pdf_argv[pdf_argc++] = (char *)"-origpagesizes";
    pdf_argv[pdf_argc++] = (char *)"-nocenter";
  }
  else
#endif // HAVE_POPPLER_PDFTOPS_WITH_ORIGPAGESIZES
  if (renderer == ACROREAD)
  {
    //
    // Use the page sizes of the original PDF document, this way documents
    // which contain pages of different sizes can be printed correctly
    //

    pdf_argv[pdf_argc++] = (char *)"-choosePaperByPDFPageSize";
  }

  //
  // Set output resolution ...
  //

  if (ppd)
  {
    // Ignore error exits of ppdRasterInterpretPPD(), if it found a resolution
    // setting before erroring it is OK for us
    ppdRasterInterpretPPD(&header, ppd, num_options, options, NULL);
    // 100 dpi is default, this means that if we have 100 dpi here this
    // method failed to find the printing resolution
    resolution[0] = '\0';
    if (header.HWResolution[0] != 100 || header.HWResolution[1] != 100)
    {
      xres = header.HWResolution[0];
      yres = header.HWResolution[1];
    }
    else if ((choice = ppdFindMarkedChoice(ppd, "Resolution")) != NULL)
      strncpy(resolution, choice->choice, sizeof(resolution) - 1);
    else if ((attr = ppdFindAttr(ppd,"DefaultResolution",NULL)) != NULL)
      strncpy(resolution, attr->value, sizeof(resolution) - 1);
    resolution[sizeof(resolution) - 1] = '\0';
    if (((xres == 100 && yres == 100) || xres <= 0 || yres <= 0))
    {
      if (resolution[0] &&
	  (numvalues = sscanf(resolution, "%dx%d", &xres, &yres)) == 1)
	yres = xres;
      if (numvalues <= 0 && log)
	log(ld, CF_LOGLEVEL_DEBUG,
	    "ppdFilterPDFToPS: No resolution information found in the PPD file.");
    }
  }
  else
  {
    cfRasterPrepareHeader(&header, data, CF_FILTER_OUT_FORMAT_CUPS_RASTER,
			  CF_FILTER_OUT_FORMAT_CUPS_RASTER, 0, &cspace);
    if (header.HWResolution[0] > 100 && header.HWResolution[1] > 100)
    {
      xres = header.HWResolution[0];
      yres = header.HWResolution[1];
    }
    else if ((ipp = ippFindAttribute(printer_attrs,
				     "printer-resolution-default",
				     IPP_TAG_ZERO)) != NULL)
    {
      ippAttributeString(ipp, buffer, sizeof(buffer));
      const char *p = buffer;
      xres = atoi(p);
      if ((p = strchr(p, 'x')) != NULL)
        yres = atoi(p + 1);
      else
	yres = xres;
    }
    else if ((ipp = ippFindAttribute(printer_attrs,
				     "printer-resolution-supported",
				     IPP_TAG_ZERO))!=NULL){
      ippAttributeString(ipp, buffer, sizeof(buffer));
      for (i = 0; buffer[i] != '\0'; i ++)
      {
        if(buffer[i] == ' ' ||
	   buffer[i] == ',')
	{
	  buffer[i] = '\0';
	  break;
	}
      }
      const char *p = buffer;
      xres = atoi(p);
      if((p = strchr(p, 'x')) != NULL)
        yres = atoi(p + 1);
      else
	yres = xres;
    }
  }
  if ((xres == 0) && (yres == 0))
  {
    if ((val = cupsGetOption("printer-resolution", num_options,
			     options)) != NULL ||
	(val = cupsGetOption("Resolution", num_options, options)) != NULL)
    {
      xres = yres = strtol(val, (char **)&ptr, 10);
      if (ptr > val && xres > 0)
      {
	if (*ptr == 'x')
	  yres = strtol(ptr + 1, (char **)&ptr, 10);
      }

      if (ptr <= val || xres <= 0 || yres <= 0 || !ptr ||
	  (*ptr != '\0' &&
	   strcasecmp(ptr, "dpi") &&
	   strcasecmp(ptr, "dpc") &&
	   strcasecmp(ptr, "dpcm")))
      {
	if (log) log(ld, CF_LOGLEVEL_DEBUG,
		     "ppdFilterPDFToPS: Bad resolution value \"%s\".", val);
      }
      else
      {
	if (!strcasecmp(ptr, "dpc") ||
	    !strcasecmp(ptr, "dpcm"))
	{
	  xres = xres * 254 / 100;
	  yres = yres * 254 / 100;
	}
      }
    }
  }
  if ((xres > 0) || (yres > 0))
  {
    if (yres == 0) yres = xres;
    if (xres == 0) xres = yres;
    if (xres > yres)
      res = yres;
    else
      res = xres;
  }
  else
    res = 300;

  //
  // Get the ceiling for the image rendering resolution
  //

  if ((val = cupsGetOption("pdftops-max-image-resolution",
			   num_options, options)) != NULL)
  {
    if ((numvalues = sscanf(val, "%d", &mres)) > 0)
      maxres = mres;
    else
      if (log) log(ld, CF_LOGLEVEL_WARN,
		   "ppdFilterPDFToPS: Invalid value for "
		   "\"pdftops-max-image-resolution\": \"%s\"",
		   val);
  }

  //
  // Reduce the image rendering resolution to not exceed a given maximum
  // to make processing of jobs by the PDF->PS converter and the printer faster
  //
  // maxres = 0 means no limit
  //

  if (maxres)
    while (res > maxres)
      res = res / 2;

  if ((renderer == PDFTOPS) || (renderer == PDFTOCAIRO))
  {
#ifdef HAVE_POPPLER_PDFTOPS_WITH_RESOLUTION
    //
    // Set resolution to avoid slow processing by the printer when the
    // resolution of embedded images does not match the printer's resolution
    //

    pdf_argv[pdf_argc++] = (char *)"-r";
    snprintf(resolution, sizeof(resolution), "%d", res);
    pdf_argv[pdf_argc++] = resolution;
    if (log) log(ld, CF_LOGLEVEL_DEBUG,
		 "ppdFilterPDFToPS: Using image rendering resolution %d dpi",
		 res);
#endif // HAVE_POPPLER_PDFTOPS_WITH_RESOLUTION
    if (gray_output == 1) // Checking for monochrome/grayscale PostScript
                          // output
    {
      //
      // Poppler does not explicitly support turning colored PDF files into
      // grayscale PostScript. As a workaround, one could let the "pdftops"
      // command line utility generate PostScript level 1 output which is
      // always grayscale, but output files get huge and printing too 
      // slow.
      // Recommended solution is to not use Poppler as PDF renderer for
      // printing, especially if one uses a color PostScript printer and
      // wants to have the possibility to print jobs also in grayscale.
      // See the use of the "pdftops-renderer" option in the README.md file.
      // Example code for PostScript level1 workaround:
      // pdf_argv[1] = (char *)"-level1";
      // pdf_argv[pdf_argc++] = (char *)"-optimizecolorspace";
      //

      // Issue a warning message when printing a grayscale job with Poppler
      if (log) log(ld, CF_LOGLEVEL_WARN,
		   "ppdFilterPDFToPS: Grayscale/monochrome printing requested for this "
		   "job but Poppler is not able to convert to "
		   "grayscale/monochrome PostScript.");
      if (log) log(ld, CF_LOGLEVEL_WARN,
		   "ppdFilterPDFToPS: Use \"pdftops-renderer\" option (see "
		   "cups-filters README.md file) to use Ghostscript or MuPDF for "
		   "the PDF -> PostScript conversion.");
    }
    pdf_argv[pdf_argc++] = filename;
    pdf_argv[pdf_argc++] = (char *)"-";
  }
  else if (renderer == GS)
  {
    //
    // Set resolution to avoid slow processing by the printer when the
    // resolution of embedded images does not match the printer's resolution
    //

    snprintf(resolution, 127, "-r%d", res);
    pdf_argv[pdf_argc++] = resolution;
    if (log) log(ld, CF_LOGLEVEL_DEBUG,
		 "ppdFilterPDFToPS: Using image rendering resolution %d dpi",
		 res);
    //
    // PostScript debug mode: If you send a job with "lpr -o psdebug"
    // Ghostscript will not compress the pages, so that the PostScript
    // code can get analysed. This is especially important if a
    // PostScript printer errors or misbehaves on Ghostscript's
    // output. On Kyocera and Utax (uses Kyocera hard- and software)
    // printers we always suppress page compression, to avoid slow
    // processing of raster images.
    //

    val = cupsGetOption("psdebug", num_options, options);
    if ((val && strcasecmp(val, "no") && strcasecmp(val, "off") &&
	 strcasecmp(val, "false")) ||
	(make_model[0] &&
	 (!strncasecmp(make_model, "Kyocera", 7) ||
	  !strncasecmp(make_model, "Utax", 4))))
    {
      if (log) log(ld, CF_LOGLEVEL_DEBUG,
		   "ppdFilterPDFToPS: Deactivated compression of pages in "
		   "Ghostscript's PostScript output (\"psdebug\" debug mode "
		   "or Kyocera/Utax printer)");
      pdf_argv[pdf_argc++] = (char *)"-dCompressPages=false";
    }

    //
    // The PostScript interpreters on many printers have bugs which make
    // the interpreter crash, error out, or otherwise misbehave on too
    // heavily compressed input files, especially if code with compressed
    // elements is compressed again. Therefore we reduce compression here.
    //

    pdf_argv[pdf_argc++] = (char *)"-dCompressFonts=false";
    pdf_argv[pdf_argc++] = (char *)"-dNoT3CCITT";
    if (make_model[0] &&
	!strncasecmp(make_model, "Brother", 7))
    {
      if (log) log(ld, CF_LOGLEVEL_DEBUG,
		   "ppdFilterPDFToPS: Deactivation of Ghostscript's image compression "
		   "for Brother printers to workarounmd PS interpreter bug");
      pdf_argv[pdf_argc++] = (char *)"-dEncodeMonoImages=false";
      pdf_argv[pdf_argc++] = (char *)"-dEncodeColorImages=false";
    }

    //
    // Toshiba's PS interpreters have an issue with how we handle
    // TrueType/Type42 fonts, therefore we add command line options to turn
    // the TTF outlines into bitmaps, usually Type 3 PostScript fonts, only
    // large glyphs into normal image data.
    // See https://bugs.launchpad.net/bugs/978120
    //

    if (make_model[0] &&
	!strncasecmp(make_model, "Toshiba", 7))
    {
      if (log) log(ld, CF_LOGLEVEL_DEBUG,
		   "ppdFilterPDFToPS: To work around a bug in Toshiba's PS "
		   "interpreters turn TTF font glyphs into bitmaps, usually "
		   "Type 3 PS fonts, or images for large characters");
      pdf_argv[pdf_argc++] = (char *)"-dHaveTrueTypes=false";
    }
    pdf_argv[pdf_argc++] = (char *)"-dNOINTERPOLATE";
    pdf_argv[pdf_argc++] = (char *)"-c";
    if (make_model[0] &&
	!strncasecmp(make_model, "Toshiba", 7))
      pdf_argv[pdf_argc++] = (char *)"<< /MaxFontItem 500000 >> setuserparams";
    pdf_argv[pdf_argc++] = (char *)"save pop";
    pdf_argv[pdf_argc++] = (char *)"-f";
    pdf_argv[pdf_argc++] = filename;
  }
  else if (renderer == MUPDF)
  {
    //
    // Add Resolution option to avoid slow processing by the printer when the
    // resolution of embedded images does not match the printer's resolution
    //
   
    snprintf(resolution, 127, "-r%dx%d", res, res);
    pdf_argv[pdf_argc++] = resolution;

    //
    // Add input file name
    //
   
    pdf_argv[pdf_argc++] = filename;
  }

  pdf_argv[pdf_argc] = NULL;

  log_command_line(NULL, pdf_argv, log, ld);

  //
  // Do we need post-processing of the PostScript output to work around bugs
  // of the printer's PostScript interpreter?
  //

  if ((renderer == PDFTOPS) || (renderer == PDFTOCAIRO) ||
      (renderer == MUPDF))
    need_post_proc = 0;
  else if (renderer == GS)
    need_post_proc =
      (make_model[0] &&
       (!strncasecmp(make_model, "Kyocera", 7) ||
	!strncasecmp(make_model, "Utax", 4) ||
	!strncasecmp(make_model, "Brother", 7)) ? 1 : 0);
  else
    need_post_proc = 1;

  //
  // Do we need post-processing of the PostScript output to apply option
  // settings when doing PPD-less printing?
  //

  if (!ppd)
    need_post_proc = 1;

  //
  // Execute "pdftops/gs/mutool [ | PS post-processing ] [ | pstops ]"...
  //


  //
  // Create a pipe for each pair of subsequent processes. The variables
  // are named by the receiving process.
  //

  if (ppd)
  {
    if (pipe(pstops_pipe))
    {
      if (log) log(ld, CF_LOGLEVEL_ERROR,
		   "ppdFilterPDFToPS: Unable to create pipe for ppdFilterPSToPS: %s",
		   strerror(errno));

      exit_status = 1;
      goto error;
    }
  }

  if (need_post_proc)
  {
    if (pipe(post_proc_pipe))
    {
      if (log) log(ld, CF_LOGLEVEL_ERROR,
		   "ppdFilterPDFToPS: Unable to create pipe for post-processing: %s",
		   strerror(errno));

      exit_status = 1;
      goto error;
    }
  }

  if ((pdf_pid = fork()) == 0)
  {
    //
    // Child comes here...
    //

    if (need_post_proc)
    {
      dup2(post_proc_pipe[1], 1);
      close(post_proc_pipe[0]);
      close(post_proc_pipe[1]);
    }
    else if (ppd)
    {
      dup2(pstops_pipe[1], 1);
      close(pstops_pipe[0]);
      close(pstops_pipe[1]);
    }
    else
      dup2(outputfd, 1);

    if (renderer == PDFTOPS)
    {
      execvp(CUPS_POPPLER_PDFTOPS, pdf_argv);
      if (log) log(ld, CF_LOGLEVEL_ERROR,
		   "ppdFilterPDFToPS: Unable to execute pdftops program: %s",
		   strerror(errno));
    }
    else if (renderer == GS)
    {
      execvp(CUPS_GHOSTSCRIPT, pdf_argv);
      if (log) log(ld, CF_LOGLEVEL_ERROR,
		   "ppdFilterPDFToPS: Unable to execute gs program: %s",
		   strerror(errno));
    }
    else if (renderer == PDFTOCAIRO)
    {
      execvp(CUPS_POPPLER_PDFTOCAIRO, pdf_argv);
      if (log) log(ld, CF_LOGLEVEL_ERROR,
		   "ppdFilterPDFToPS: Unable to execute pdftocairo program: %s",
		   strerror(errno));
    }
    else if (renderer == ACROREAD)
    {
      //
      // use filename as stdin for acroread to force output to stdout
      //

      if ((fd = open(filename, O_RDONLY)))
      {
        dup2(fd, 0);
        close(fd);
      }
     
      execvp(CUPS_ACROREAD, pdf_argv);
      if (log) log(ld, CF_LOGLEVEL_ERROR,
		   "ppdFilterPDFToPS: Unable to execute acroread program: %s",
		   strerror(errno));
    }
    else if (renderer == MUPDF)
    {
      execvp(CUPS_MUTOOL, pdf_argv);
      if (log) log(ld, CF_LOGLEVEL_ERROR,
		   "ppdFilterPDFToPS: Unable to execute mutool program: %s",
		   strerror(errno));
    }

    exit(1);
  }
  else if (pdf_pid < 0)
  {
    //
    // Unable to fork!
    //

    if (log)
    {
      if (renderer == PDFTOPS)
	log(ld, CF_LOGLEVEL_ERROR,
	    "ppdFilterPDFToPS: Unable to execute pdftops program: %s",
	    strerror(errno));
      else if (renderer == GS)
	log(ld, CF_LOGLEVEL_ERROR,
	    "ppdFilterPDFToPS: Unable to execute gs program: %s",
	    strerror(errno));
      else if (renderer == PDFTOCAIRO)
	log(ld, CF_LOGLEVEL_ERROR,
	    "ppdFilterPDFToPS: Unable to execute pdftocairo program: %s",
	    strerror(errno));
      else if (renderer == ACROREAD)
	log(ld, CF_LOGLEVEL_ERROR,
	    "ppdFilterPDFToPS: Unable to execute acroread program: %s",
	    strerror(errno));
      else if (renderer == MUPDF)
	log(ld, CF_LOGLEVEL_ERROR,
	    "ppdFilterPDFToPS: Unable to execute mutool program: %s",
	    strerror(errno));
    }

    exit_status = 1;
    goto error;
  }

  if (log) log(ld, CF_LOGLEVEL_DEBUG,
	       "ppdFilterPDFToPS: Started filter %s (PID %d)",
	       pdf_argv[0], pdf_pid);

  if (need_post_proc)
  {
    if ((post_proc_pid = fork()) == 0)
    {
      //
      // Child comes here...
      //

      close(post_proc_pipe[1]);
      if (ppd)
      {
	dup2(pstops_pipe[1], 1);
	close(pstops_pipe[0]);
	close(pstops_pipe[1]);
      }
      else
	dup2(outputfd, 1);

      fp = cupsFileOpenFd(post_proc_pipe[0], "r");

      if (renderer == ACROREAD)
      {
	//
        // Set %Title and %For from filter arguments since acroread inserts
        // garbage for these when using -toPostScript
	//

        while ((bytes = cupsFileGetLine(fp, buffer, sizeof(buffer))) > 0 &&
               strncmp(buffer, "%%BeginProlog", 13))
        {
          if (strncmp(buffer, "%%Title", 7) == 0)
            printf("%%%%Title: %s\n", data->job_title);
          else if (strncmp(buffer, "%%For", 5) == 0)
            printf("%%%%For: %s\n", data->job_user);
          else
            printf("%s", buffer);
        }

	//
	// Copy the rest of the file
	//

	while ((bytes = cupsFileRead(fp, buffer, sizeof(buffer))) > 0)
	  fwrite(buffer, 1, bytes, stdout);
      }
      else
      {
	//
	// Copy everything until after initial comments (Prolog section)
	//

	while ((bytes = cupsFileGetLine(fp, buffer, sizeof(buffer))) > 0 &&
	       strncmp(buffer, "%%BeginProlog", 13) &&
	       strncmp(buffer, "%%EndProlog", 11) &&
	       strncmp(buffer, "%%BeginSetup", 12) &&
	       strncmp(buffer, "%%Page:", 7))
	  printf("%s", buffer);

	if (bytes > 0)
	{
	  //
	  // Insert PostScript interpreter bug fix code in the beginning of
	  // the Prolog section (before the first active PostScript code)
	  //

	  if (strncmp(buffer, "%%BeginProlog", 13))
	  {
	    // No Prolog section, create one
	    if (log) log(ld, CF_LOGLEVEL_DEBUG,
			 "ppdFilterPDFToPS: Adding Prolog section for workaround "
			 "PostScript code");
	    puts("%%BeginProlog");
	  }
	  else
	    printf("%s", buffer);

	  if (renderer == GS && make_model[0])
	  {
	    //
	    // Kyocera (and Utax) printers have a bug in their PostScript
	    // interpreter making them crashing on PostScript input data
	    // generated by Ghostscript's "ps2write" output device.
	    //
	    // The problem can be simply worked around by preceding the
	    // PostScript code with some extra bits.
	    //
	    // See https://bugs.launchpad.net/bugs/951627
	    //
	    // In addition, at least some of Kyocera's PostScript printers are
	    // very slow on rendering images which request interpolation. So we
	    // also add some code to eliminate interpolation requests.
	    //
	    // See https://bugs.launchpad.net/bugs/1026974
	    //

	    if (!strncasecmp(make_model, "Kyocera", 7) ||
		!strncasecmp(make_model, "Utax", 4))
	    {
	      if (log) log(ld, CF_LOGLEVEL_DEBUG,
			   "ppdFilterPDFToPS: Inserted workaround PostScript code for "
			   "Kyocera and Utax printers");
	      puts("% ===== Workaround insertion by pdftops CUPS filter =====");
	      puts("% Kyocera's/Utax's PostScript interpreter crashes on "
		   "early name binding,");
	      puts("% so eliminate all \"bind\"s by redefining \"bind\" to "
		   "no-op");
	      puts("/bind {} bind def");
	      puts("% Some Kyocera and Utax printers have an unacceptably "
		   "slow implementation");
	      puts("% of image interpolation.");
	      puts("/image");
	      puts("{");
	      puts("  dup /Interpolate known");
	      puts("  {");
	      puts("    dup /Interpolate undef");
	      puts("  } if");
	      puts("  systemdict /image get exec");
	      puts("} def");
	      puts("% =====");
	    }

	    //
	    // Brother printers have a bug in their PostScript interpreter
	    // making them printing one blank page if PostScript input data
	    // generated by Ghostscript's "ps2write" output device is used.
	    //
	    // The problem can be simply worked around by preceding the
	    // PostScript code with some extra bits.
	    //
	    // See https://bugs.launchpad.net/bugs/950713
	    //

	    else if (!strncasecmp(make_model, "Brother", 7))
	    {
	      if (log) log(ld, CF_LOGLEVEL_DEBUG,
			   "ppdFilterPDFToPS: Inserted workaround PostScript code for "
			   "Brother printers");
	      puts("% ===== Workaround insertion by pdftops CUPS filter =====");
	      puts("% Brother's PostScript interpreter spits out the current "
		   "page");
	      puts("% and aborts the job on the \"currenthalftone\" operator, "
		   "so redefine");
	      puts("% it to null");
	      puts("/currenthalftone {//null} bind def");
	      puts("/orig.sethalftone systemdict /sethalftone get def");
	      puts("/sethalftone {dup //null eq not {//orig.sethalftone}{pop} "
		   "ifelse} bind def");
	      puts("% =====");
	    }
	  }

	  if (strncmp(buffer, "%%BeginProlog", 13))
	  {
	    // Close newly created Prolog section
	    if (strncmp(buffer, "%%EndProlog", 11))
	      puts("%%EndProlog");
	    printf("%s", buffer);
	  }

	  if (!ppd)
	  {
	    //
	    // Copy everything until the setup section
	    //

	    while (bytes > 0 &&
		   strncmp(buffer, "%%BeginSetup", 12) &&
		   strncmp(buffer, "%%EndSetup", 10) &&
		   strncmp(buffer, "%%Page:", 7))
	    {
	      bytes = cupsFileGetLine(fp, buffer, sizeof(buffer));
	      if (strncmp(buffer, "%%Page:", 7) &&
		  strncmp(buffer, "%%EndSetup", 10))
		printf("%s", buffer);
	    }
	  
	    if (bytes > 0)
	    {
	      //
	      // Insert option PostScript code in Setup section
	      //

	      if (strncmp(buffer, "%%BeginSetup", 12))
	      {
		// No Setup section, create one
		if (log) log(ld, CF_LOGLEVEL_DEBUG,
			     "ppdFilterPDFToPS: Adding Setup section for option "
			     "PostScript code");
		puts("%%BeginSetup");
	      }

	      //
	      // Duplex
	      //

	      duplex = 0;
	      tumble = 0;
	      if ((val = cupsGetOption("sides",
				       num_options, options)) != NULL ||
		  (val = cupsGetOption("Duplex", num_options, options)) != NULL)
	      {
		if (!strcasecmp(val, "On") ||
			 !strcasecmp(val, "True") || !strcasecmp(val, "Yes") ||
			 !strncasecmp(val, "two-sided", 9) ||
			 !strncasecmp(val, "TwoSided", 8) ||
			 !strncasecmp(val, "Duplex", 6))
		{
		  duplex = 1;
		  if (!strncasecmp(val, "DuplexTumble", 12))
		    tumble = 1;
		}
	      }

	      if ((val = cupsGetOption("sides",
				       num_options, options)) != NULL ||
		  (val = cupsGetOption("Tumble", num_options, options)) != NULL)
	      {
		if (!strcasecmp(val, "None") || !strcasecmp(val, "Off") ||
		    !strcasecmp(val, "False") || !strcasecmp(val, "No") ||
		    !strcasecmp(val, "one-sided") ||
		    !strcasecmp(val, "OneSided") ||
		    !strcasecmp(val, "two-sided-long-edge") ||
		    !strcasecmp(val, "TwoSidedLongEdge") ||
		    !strcasecmp(val, "DuplexNoTumble"))
		  tumble = 0;
		else if (!strcasecmp(val, "On") ||
			 !strcasecmp(val, "True") || !strcasecmp(val, "Yes") ||
			 !strcasecmp(val, "two-sided-short-edge") ||
			 !strcasecmp(val, "TwoSidedShortEdge") ||
			 !strcasecmp(val, "DuplexTumble"))
		  tumble = 1;
	      }

	      if (duplex)
	      {
		if (tumble)
		  puts("<</Duplex true /Tumble true>> setpagedevice");
		else
		  puts("<</Duplex true /Tumble false>> setpagedevice");
	      }
	      else
		puts("<</Duplex false>> setpagedevice");

	      //
	      // Resolution
	      //

	      if ((xres > 0) && (yres > 0))
		printf("<</HWResolution[%d %d]>> setpagedevice\n", xres, yres);

	      //
	      // InputSlot/MediaSource
	      //

	      if ((val = cupsGetOption("media-position", num_options,
				       options)) != NULL ||
		  (val = cupsGetOption("MediaPosition", num_options,
				       options)) != NULL ||
		  (val = cupsGetOption("media-source", num_options,
				       options)) != NULL ||
		  (val = cupsGetOption("MediaSource", num_options,
				       options)) != NULL ||
		  (val = cupsGetOption("InputSlot", num_options,
				       options)) != NULL)
	      {
		if (!strncasecmp(val, "Auto", 4) ||
		    !strncasecmp(val, "Default", 7))
		  puts("<</ManualFeed false /MediaPosition 7>> setpagedevice");
		else if (!strcasecmp(val, "Main"))
		  puts("<</MediaPosition 0 /ManualFeed false>> setpagedevice");
		else if (!strcasecmp(val, "Alternate"))
		  puts("<</MediaPosition 1 /ManualFeed false>> setpagedevice");
		else if (!strcasecmp(val, "Manual"))
		  puts("<</MediaPosition 3 /ManualFeed true>> setpagedevice");
		else if (!strcasecmp(val, "Top"))
		  puts("<</MediaPosition 0 /ManualFeed false>> setpagedevice");
		else if (!strcasecmp(val, "Bottom"))
		  puts("<</MediaPosition 1 /ManualFeed false>> setpagedevice");
		else if (!strcasecmp(val, "ByPassTray"))
		  puts("<</MediaPosition 3 /ManualFeed false>> setpagedevice");
		else if (!strcasecmp(val, "Tray1"))
		  puts("<</MediaPosition 3 /ManualFeed false>> setpagedevice");
		else if (!strcasecmp(val, "Tray2"))
		  puts("<</MediaPosition 0 /ManualFeed false>> setpagedevice");
		else if (!strcasecmp(val, "Tray3"))
		  puts("<</MediaPosition 1 /ManualFeed false>> setpagedevice");
	      }

	      //
	      // ColorModel
	      //

	      if ((val = cupsGetOption("pwg-raster-document-type", num_options,
				       options)) != NULL ||
		  (val = cupsGetOption("PwgRasterDocumentType", num_options,
				       options)) != NULL ||
		  (val = cupsGetOption("print-color-mode", num_options,
				       options)) != NULL ||
		  (val = cupsGetOption("PrintColorMode", num_options,
				       options)) != NULL ||
		  (val = cupsGetOption("color-space", num_options,
				       options)) != NULL ||
		  (val = cupsGetOption("ColorSpace", num_options,
				       options)) != NULL ||
		  (val = cupsGetOption("color-model", num_options,
				       options)) != NULL ||
		  (val = cupsGetOption("ColorModel", num_options,
				       options)) != NULL)
	      {
		if (!strncasecmp(val, "Black", 5))
		  puts("<</ProcessColorModel /DeviceGray>> setpagedevice");
		else if (!strncasecmp(val, "Cmyk", 4))
		  puts("<</ProcessColorModel /DeviceCMYK>> setpagedevice");
		else if (!strncasecmp(val, "Cmy", 3))
		  puts("<</ProcessColorModel /DeviceCMY>> setpagedevice");
		else if (!strncasecmp(val, "Rgb", 3))
		  puts("<</ProcessColorModel /DeviceRGB>> setpagedevice");
		else if (!strncasecmp(val, "Gray", 4))
		  puts("<</ProcessColorModel /DeviceGray>> setpagedevice");
		else if (!strncasecmp(val, "Color", 5))
		  puts("<</ProcessColorModel /DeviceRGB>> setpagedevice");
	      }

	      if (strncmp(buffer, "%%BeginSetup", 12))
	      {
		// Close newly created Setup section
		if (strncmp(buffer, "%%EndSetup", 10))
		  puts("%%EndSetup");
		printf("%s", buffer);
	      }
	    }
	  }

	  //
	  // Copy the rest of the file
	  //

	  while ((bytes = cupsFileRead(fp, buffer, sizeof(buffer))) > 0)
	    fwrite(buffer, 1, bytes, stdout);
	}
      }
      close(post_proc_pipe[0]);

      exit(0);
    }
    else if (post_proc_pid < 0)
    {
      //
      // Unable to fork!
      //

      if (log) log(ld, CF_LOGLEVEL_ERROR,
		   "ppdFilterPDFToPS: Unable to execute post-processing process: %s",
		   strerror(errno));

      exit_status = 1;
      goto error;
    }

    if (log) log(ld, CF_LOGLEVEL_DEBUG,
		 "ppdFilterPDFToPS: Started post-processing (PID %d)",
		 post_proc_pid);
  }

  if (ppd)
  {
    if ((pstops_pid = fork()) == 0)
    {
      //
      // Child comes here...
      //

      close(pstops_pipe[1]);
      if (need_post_proc)
      {
	close(post_proc_pipe[0]);
	close(post_proc_pipe[1]);
      }

      ret = ppdFilterPSToPS(pstops_pipe[0], outputfd, 0, &pstops_filter_data,
			    NULL);
      close(pstops_pipe[0]);

      if (ret && log) log(ld, CF_LOGLEVEL_ERROR,
			  "ppdFilterPDFToPS: pstops filter function failed.");

      close(outputfd);
      exit(ret);
    }
    else if (pstops_pid < 0)
    {
      //
      // Unable to fork!
      //

      if (log) log(ld, CF_LOGLEVEL_ERROR,
		   "ppdFilterPDFToPS: Unable to execute pstops program: %s",
		   strerror(errno));

      exit_status = 1;
      goto error;
    }

    if (log) log(ld, CF_LOGLEVEL_DEBUG,
		 "ppdFilterPDFToPS: Started filter pstops (PID %d)",
		 pstops_pid);

    close(pstops_pipe[0]);
    close(pstops_pipe[1]);
  }

  if (need_post_proc)
  {
    close(post_proc_pipe[0]);
    close(post_proc_pipe[1]);
  }

  //
  // Wait for the child processes to exit...
  //

  wait_children = 1 + need_post_proc + (ppd ? 1 : 0);

  while (wait_children > 0)
  {
    //
    // Wait until we get a valid process ID or the job is canceled...
    //

    while ((wait_pid = wait(&wait_status)) < 0 && errno == EINTR)
    {
      if (iscanceled && iscanceled(icd))
      {
	kill(pdf_pid, SIGTERM);
	if (need_post_proc)
	  kill(post_proc_pid, SIGTERM);
	kill(pstops_pid, SIGTERM);
      }
    }

    if (wait_pid < 0)
      break;

    wait_children --;

    //
    // Report child status...
    //

    if (wait_status)
    {
      if (WIFEXITED(wait_status))
      {
	exit_status = WEXITSTATUS(wait_status);

	if (log) log(ld, CF_LOGLEVEL_DEBUG,
		     "ppdFilterPDFToPS: PID %d (%s) stopped with status %d!",
		     wait_pid,
		     wait_pid == pdf_pid ?
		     (renderer == PDFTOPS ? "pdftops" :
		      (renderer == PDFTOCAIRO ? "pdftocairo" :
		       (renderer == GS ? "gs" :
			(renderer == ACROREAD ? "acroread" :
			 (renderer == MUPDF ? "mutool" :
			  "Unknown renderer"))))) :
		     (wait_pid == pstops_pid ? "pstops" :
		      (wait_pid == post_proc_pid ? "Post-processing" :
		       "Unknown process")),
		     exit_status);
      }
      else if (WTERMSIG(wait_status) == SIGTERM)
      {
	if (log) log(ld, CF_LOGLEVEL_DEBUG,
		     "ppdFilterPDFToPS: PID %d (%s) was terminated normally with "
		     "signal %d!",
		     wait_pid,
		     wait_pid == pdf_pid ?
		     (renderer == PDFTOPS ? "pdftops" :
		      (renderer == PDFTOCAIRO ? "pdftocairo" :
		       (renderer == GS ? "gs" :
			(renderer == ACROREAD ? "acroread" :
			 (renderer == MUPDF ? "mutool" :
			  "Unknown renderer"))))) :
		     (wait_pid == pstops_pid ? "pstops" :
		      (wait_pid == post_proc_pid ? "Post-processing" :
		       "Unknown process")),
		     exit_status);
      }
      else
      {
	exit_status = WTERMSIG(wait_status);

	if (log) log(ld, CF_LOGLEVEL_ERROR,
		     "ppdFilterPDFToPS: PID %d (%s) crashed on signal %d!",
		     wait_pid,
		     wait_pid == pdf_pid ?
		     (renderer == PDFTOPS ? "pdftops" :
		      (renderer == PDFTOCAIRO ? "pdftocairo" :
		       (renderer == GS ? "gs" :
			(renderer == ACROREAD ? "acroread" :
			 (renderer == MUPDF ? "mutool" :
			  "Unknown renderer"))))) :
		     (wait_pid == pstops_pid ? "pstops" :
		      (wait_pid == post_proc_pid ? "Post-processing" :
		       "Unknown process")),
		     exit_status);
      }
    }
    else
    {
      if (log) log(ld, CF_LOGLEVEL_DEBUG,
		   "ppdFilterPDFToPS: PID %d (%s) exited with no errors.",
		   wait_pid,
		   wait_pid == pdf_pid ?
		   (renderer == PDFTOPS ? "pdftops" :
		    (renderer == PDFTOCAIRO ? "pdftocairo" :
		     (renderer == GS ? "gs" :
		      (renderer == ACROREAD ? "acroread" :
		       (renderer == MUPDF ? "mutool" :
			"Unknown renderer"))))) :
		   (wait_pid == pstops_pid ? "pstops" :
		    (wait_pid == post_proc_pid ? "Post-processing" :
		     "Unknown process")));
    }
  }

  //
  // Cleanup and exit...
  //

 error:

  if (log) log(ld, CF_LOGLEVEL_DEBUG,
	       "ppdFilterPDFToPS: Closing files ...");

  close(outputfd);

  unlink(tempfile);

  return (exit_status);
}
