/*   GPDriver2 source gpdriver.c
 *   $Id: gpdriver.c,v 1.9 2007/03/15 15:12:24 wuerthne Exp $
 *
 *   GPDriver2 - Gutenprint based printer spooler for RISC OS
 *   Copyright (C) 2007 Martin Wuerthner
 *
 *   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.
 *
 *   Martin Wuerthner <ro-printing@mw-software.com>
 *   Mannheimer Str. 18, 67655 Kaiserslautern, Germany
 */

/*   The GPDriver2 command-line tool reads in a GemPrint job file and prints it to
 *   the given print job destination stream using libgutenprint. This
 *   implementation is completely independent of the RISC OS printing system.
 *   It only uses OSLib to interface with RISC OS (and the Toolbox).
 *
 *   GPDriver2 prints a GemPrint job, i.e., one GemPrint job file for each page to
 *   be printed. It consults a printer configuration file that holds the advanced
 *   printer-specific settings.
 *
 *   The printer-specific settings are read from Choices:GPDriver2.<printer name>
 *   (where printer name is the short, editable name in the printer
 *   configuration window, so you can have multiple instances of the same
 *   printer with different properties). If the choices file cannot be found,
 *   then the default choices for this printer are used (possibly with poor
 *   results).
 */

/*   usage: gpdriver2 <job prefix> <number of pages> <output path> */
/*          where <job prefix> is an arbitrary string (GemPrint passes a
 *          four-nibble hex string), <number of pages> is a decimal representation
 *          of the number of job files with this prefix (the page sequence number
 *          is encoded as a four-nibble hex number suffix in each file name) and
 *          <output path> is the filename to write the output to
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gutenprint/gutenprint.h"
#include "oslib/osfind.h"
#include "oslib/osfile.h"
#include "oslib/osgbpb.h"
#include "oslib/syslog.h"
#include "oslib/wimp.h"
#include "image.h"
#include "gpd_gui.h"
#include "escchar.h"

#include <unixlib/local.h>

/* we disable UnixLib's suffix swapping to make sure that Gutenprint finds its xml files
   irrespective of any sfix variables set up */
int __riscosify_control = __RISCOSIFY_NO_SUFFIX;

/* the location of the GemPrint job files */
#define JOB_FILE_PATH "<Wimp$ScrapDir>.GemPrint."

/* the location of the printer settings files */
#define SETTINGS_FILE_PATH "Choices:GPDriver2."
#define GPDRIVER_SETTINGS_LEAF_NAME "GPDriver2"   /* not used yet, but if it is ever used, make sure it
                                                     cannot possibly clash with a printer settings file */

#define SETTINGS_HEADER "# GPSetup "
/* the file version of the settings file that we require */
#define CURRENT_SETTINGS_FVER 200

#define FILETYPE_POSCRIPT (0xff5)

/* set to 1 to enable debug messages via Reporter */
#define DEBUG 0
#include "debug.h"

/* Gutenprint settings are a list of key-value pairs */
typedef struct gutenprint_settings_s {
  char* key;
  char value_type;
  char* value;
  struct gutenprint_settings_s* next;
} gutenprint_settings;

os_fw job_file_h = 0, output_file_h = 0;

/* memory state */

/* a structure to hold information about an allocated DA */
struct da_struct {
  os_dynamic_area_no no;    /* dynamic area number */
  byte* next_free;          /* address of the next free byte */
  int physical_free;        /* number of free bytes (physical, i.e., real memory) */
  int logical_free;         /* number of logical free bytes (i.e., related to the maximum size of the DA) */
  int size;                 /* current physical size of DA */
  struct da_struct* next;
  struct da_struct* prev;
};

int use_da = FALSE;                /* do we want to use DAs at all? */
struct da_struct *first_da = NULL; /* pointer to first DA structure (linked list) */
int da_allocated, da_freed;        /* number of blocks currently allocated and freed in the DAs */
int mem_estimated_k;
int print_mem_allocated;
int keep_open = TRUE;              /* do we want to keep the window open after printing has completed? */

static void read_gpdriver_choices(void);
static void do_print(char* job_prefix, int pages, char* output_path);
static void close_files(void);
static int print_image(stp_vars_t* stp_vars, gpd_image_t* img, int* job_started, int copy_no, int copies, int pages_printed);
static int set_page_parameters(stp_vars_t* stp_vars, gpd_image_t* img);
static gutenprint_settings* read_settings(const char* printer, int report_errors);
static void* checked_alloc(size_t size);
static void intercept_stp_memory(int start);
static void remove_das(void);
stp_vars_t* create_vars(gpd_image_t* img, gutenprint_settings* settings);

int main(int argc, char* argv[])
{
  int pages;

  gui_log_msg("GPDriver started", 100);
  if (argc != 4) gui_fatal_error2("GPDriver: invalid command line");

  pages = atoi(argv[2]);
  if (pages <= 0) {
    gui_log_msg("Nothing to print", 100); exit(EXIT_SUCCESS);
  }
  read_gpdriver_choices();

#ifndef GPDRIVER_GUI
  printf("Job number: %s, pages: %s, output to: %s\n", argv[1], argv[2], argv[3]);
#endif

  if (!gui_init()) {
    gui_fatal_error2(gui_lookup("CNIG"));
  }
  atexit(close_files);                /* make sure that open files are closed when the program is aborted */

  do_print(argv[1], pages, argv[3]);
}

static void read_gpdriver_choices(void)
{
#if 0
/*## not implemented yet */
  char filename[256];
  os_fw config_file_h;
  os_error* err;

  sprintf(filename,"%s%s",SETTINGS_FILE_PATH,GPDRIVER_SETTINGS_LEAF_NAME);
  if ((err = xosfind_openinw(osfind_NO_PATH + osfind_ERROR_IF_DIR + osfind_ERROR_IF_ABSENT,
                             filename, NULL, &config_file_h)) == NULL) {
    /* file is present */
    /*## read general choices */
    osfind_closew(config_file_h);
  }
  else {
    /*## use defaults */
  }
#endif
}

static void do_print(char* job_prefix, int pages, char* output_path)
{
  gpd_image_t* img;
  os_error* err;
  int i;
  char filename[256];
  gutenprint_settings* settings = NULL;
  int print_success = TRUE;
  int pages_printed = 0;
  int is_postscript = 0;
  int copies, this_copy;

  stp_vars_t* stp_vars = NULL;
  int job_started = 0;

  gui_set_pages(pages); gui_set_page(0);

  gui_inform(gui_lookup("INIT"), 1); gui_inform("", 2);
  gui_hourglass_on();
  if (stp_init() != 0) {
    gui_fatal_error2(gui_lookup("CNIGP"));
  }
  gui_hourglass_off();

  gui_inform(gui_lookup("OPEN"), 1);
  if ((err = xosfind_openoutw(osfind_NO_PATH + osfind_ERROR_IF_DIR,
                              output_path, NULL, &output_file_h)) != NULL) {
    gui_fatal_error(err);
  }

  for (i = 0; i < pages && !gui_aborted() && print_success; i++) {
    gui_inform_dd(gui_lookup("OPENdd"), i+1, pages, 1);
    sprintf(filename,"%sJ%s%04X",JOB_FILE_PATH,job_prefix,i);
    if ((err = xosfind_openinw(osfind_NO_PATH + osfind_ERROR_IF_DIR + osfind_ERROR_IF_ABSENT,
                             filename, NULL, &job_file_h)) != NULL) {
      gui_fatal_error(err);
    }
    gui_inform_dd(gui_lookup("PRINTdd"), i+1, pages, 1);
    gui_set_page(i+1);

    /* read the job file and create an image structure from it */
    img = gpdi_create_image(job_file_h, gui_progress_row);
    if (!img) {
      gui_fatal_error2(gui_lookup("CNRPJF"));
    }

    if (i == 0) {
      /* first page, so read the settings file for the printer that was used */
      /* in theory, each page could have a different number of copies, but we
         ignore this when forecasting the number of pages - we still read copies
         from each image, so at least the real requested number will be printed */
      const char* long_name = gpdi_get_long_name(img);
      if (strstr(long_name, "PostScript"))
        is_postscript = TRUE;
      REPORT("calling read_settings")
      settings = read_settings(gpdi_get_short_name(img), TRUE);
      REPORT("returned from read_settings")
      if (settings == (gutenprint_settings*)-1) {
        gui_error2(gui_lookup("SPST"));
        settings = read_settings(gpdi_get_short_name(img), TRUE);
      }
      if (!settings || settings == (gutenprint_settings*)-1) {
        /* we did not get any settings so ask the user whether he really wants to
           print using the default settings */
        gui_error2(gui_lookup("DEFS"));
        settings = NULL;
      }
      else {
        REPORT("got settings")
      }

      /* create the Gutenprint vars structure and call start_job */
      stp_vars = create_vars(img, settings);
    }

    /* the gui_error2 above may have caused the job to be aborted already, but in this case, the
       loop is not executed at all */
    copies = gpdi_get_copies(img);
    for (this_copy = 0; this_copy < copies && !gui_aborted() && print_success; this_copy++) {
      REPORT("calling gui_progress_start")
      gui_progress_start();

      /* do the main work */
      REPORT("calling print_image")
      /* may cause gui_aborted() to become true */
      print_success = print_image(stp_vars, img, &job_started, this_copy, copies, pages_printed);
      REPORT("returned from print_image")
      if (!gui_aborted() && print_success) {
        gui_set_pages_printed(++pages_printed);
        if (this_copy + 1 < copies) {
          /* reset the image for the next iteration */
          if (!gpdi_reset_image(img)) print_success = FALSE;
        }
      }
    }
    /* we first clean up before testing gui_aborted() */
    osfind_closew(job_file_h);                     /* close the job file */
    job_file_h = 0;

    /* delete the job file */
    xosfile_delete(filename, NULL, NULL, NULL, NULL, NULL);

    /* we need to call stp_end_job before destroying the image, so we
       do it here in all cases that will make us leave the loop */
    if (i >= pages - 1 || gui_aborted() || !print_success)
    {
      /* last page has just been printed */
      if (job_started) {
        REPORT("calling end_job")
        stp_end_job(stp_vars, gpdi_get_stp_image(img));
        job_started = FALSE;
      }
    }
    /* reclaim some memory */
    gpdi_dispose(img);
  }

  if (stp_vars) stp_vars_destroy(stp_vars);

  /* close the output file */
  osfind_closew(output_file_h);
  output_file_h = 0;
  if (pages && is_postscript) {
    xosfile_set_type(output_path, FILETYPE_POSCRIPT);
  }
  if (gui_aborted() || !print_success) {
    REPORT("GPDriver aborted")
    gui_log_msg("GPDriver aborted",100);
  }
  else {
    REPORT("GPDriver finished")
    gui_log_msg("GPDriver finished",100);
  }
  gui_progress_end(keep_open);      /* does not return */
}

/* return -1 in case the settings file cannot be found */
/* return NULL in case the file can be found but contains errors */
static gutenprint_settings* read_settings(const char* printer, int report_errors)
{
  os_error* err;
  char filename[256];
  fileswitch_object_type obj_type;
  int file_size;
  int fver;
  char* file_buffer;
  char* p;
  char *q;
  int line;
  gutenprint_settings* settings = NULL;
  gutenprint_settings* this_setting;
  gutenprint_settings** last_next = &settings;

  strcpy(filename, SETTINGS_FILE_PATH);
  append_adjusted_leaf_name(filename, printer, 256);
  /* find out the file size */
  if ((err = xosfile_read_stamped(filename, &obj_type,NULL,NULL,&file_size,NULL,NULL)) != NULL || obj_type != 1) {
    /* we did not find the settings file, so just return with no settings */
    return (gutenprint_settings*)-1;
  }
  /* we load the complete file in a buffer */
  /* then, we can use the contents directly by using pointers into the buffer */
  file_buffer = checked_alloc(file_size + 2);   /* if the last line does not have a terminator, we will add one */
  err = xosfile_load_stamped_no_path(filename,(byte*)file_buffer,&obj_type, NULL,NULL,NULL,NULL);
  if (err) {
    if (report_errors) gui_wimp_error(err);
    return NULL;
  }

  /* OK, we have got the file */
  /* we terminate the last line in case there is no LF (if there is already an LF we add an empty line, which is
     harmless), finally we add a NUL byte to terminate the file */
  /* doing so will save us a lot of tests below because we know that there is a minimum of one LF-terminated line
     in the buffer and that the buffer is NUL-terminated */
  file_buffer[file_size] = '\n';
  file_buffer[file_size + 1] = '\0';
  p = file_buffer;

  /* check the leading comment */
  if (strncmp(p, SETTINGS_HEADER, strlen(SETTINGS_HEADER)) != 0) {
    /* incorrect format */
    if (report_errors) gui_wimp_error2(gui_lookup("INV"));
    return NULL;
  }
  p += strlen(SETTINGS_HEADER);

  /* we expect the file version number on the same line */
  if (*p < '0' || *p > '9') return NULL;
  q = p;
  while(*++p != 10) {
    if (*p < '0' || *p > '9') {
      if (report_errors) gui_wimp_error2(gui_lookup("INV"));
      return NULL;
    }
  }
  *p++ = '\0';                                  /* replace the LF by a NUL and proceed to start of next line */
  fver = atoi(q);                               /* get the decimal file version number */
  if (fver > CURRENT_SETTINGS_FVER) {
    if (report_errors) gui_wimp_error2(gui_lookup("LATER"));
    return NULL;
  }
  if (fver < CURRENT_SETTINGS_FVER) {
    /* convert the data in some way, not needed at the moment */
    /* after the conversion, adjust fver to the current version */

    /* We do not convert version 1 files - these should never be seen here */
  }
  if (fver != CURRENT_SETTINGS_FVER) {
    if (report_errors) gui_wimp_error2(gui_lookup("EARLY"));
    return NULL;
  }

  /* read the remaining lines */
  line = 2;
  while(*p) {
    if (*p == '#' || *p == 10) {
      /* comment line or empty line, just skip */
      while(*p++ != 10) ;
      ++line;
      continue;
    }
    if (*p == '+') {
      /* a Gutenprint setting */
      q = ++p;
      while(*p && *p != ':') p++;
      if (!*p) {
        /* no colon on this line, so this is an error */
        /* we return the settings we have collected so far and ignore the rest */
        if (report_errors) gui_wimp_error_d(gui_lookup("INC"),line);
        return settings;
      }
      *p++ = '\0'; /* replace the colon by NUL */

      /* now, q points to the key and p points to the type specifier and the value */
      if (!*p || !p[1] || (*p != 'b' && *p != 'i' && *p != 'f' && *p != 's' && *p != 'd') || p[1] != ':') {
        /* invalid type specifier */
        if (report_errors) gui_wimp_error_d(gui_lookup("INC"),line);
        return settings;
      }

      /* allocate a block and link it into the list */
      this_setting = checked_alloc(sizeof(gutenprint_settings));
      this_setting->key = q;
      this_setting->value_type = *p;
      this_setting->value = p + 2;
      this_setting->next = NULL;
      *last_next = this_setting;
      last_next = &this_setting->next;
      while(*p != 10) p++;
      *p++ = '\0'; /* replace the LF by NUL */
      ++line;
    }
    else if (strncmp(p,"KeepOpen:",9)==0) {
      if (strncmp(p+9,"No",2)==0) keep_open = 0;
      while(*p != 10) p++;
      ++line;
    }
    else if (*p == '#' || *p == 10) {
      /* comment line or empty line, just skip */
      while(*p++ != 10) ;
      ++line;
    }
    else {
      /* ignore other lines as well */
      while(*p++ != 10) ;
      ++line;
    }
  }
  return settings;
}

void close_files(void)
{
  if (job_file_h) {
    osfind_closew(job_file_h);
    job_file_h = 0;
  }
  if (output_file_h) {
    osfind_closew(output_file_h);
    output_file_h = 0;
  }
  if (use_da && first_da) {
    remove_das();
    use_da = 0;
  }
}

static int print_error_reported;

void print_error(void *data, const char *buffer, size_t bytes)
{
  char msg[256];
  REPORT("print error")
  strncpy(msg,buffer,256);
  if (bytes < 256) msg[bytes] = '\0'; else msg[255] = '\0';
  gui_error2(msg);
  print_error_reported = 1;
  if (gui_aborted()) gui_progress_end(keep_open);   /* does not return */
}

static int data_dumped;

void print_output(void *data, const char *buffer, size_t bytes)
{
  const byte* buff = (const byte*)buffer;
  int unwritten = bytes;
  /* printf("Dumping %d bytes of data.\n", bytes); */
  /* REPORT_D("Dumping %d bytes", bytes) */
  while(unwritten) {
    os_error *err;
    if ((err = xosgbpb_writew(output_file_h,buff,unwritten,&unwritten)) != NULL) {
      gui_fatal_error(err); return;
    }
    buff += unwritten;
  }
  data_dumped += bytes;
}

extern void *(*stp_malloc_func)(size_t size);
extern void *(*stpi_realloc_func)(void *ptr, size_t size);
extern void (*stpi_free_func)(void *ptr);

void register_memory_funcs(void*(*cmalloc)(size_t),void(*cfree)(void*),void*(*crealloc)(void*, size_t))
{
  if (cmalloc != NULL && cfree != NULL && crealloc != NULL)
  {
    stp_malloc_func = cmalloc;
    stpi_free_func = cfree;
    stpi_realloc_func = crealloc;
  }
  else
  {
    stp_malloc_func = malloc;
    stpi_free_func = free;
    stpi_realloc_func = realloc;
  }
}

extern void
stp_register_memory_inform_func(void (*mfunc)(size_t));

void memory_inform(size_t mem)
{
  int current_slot, free_size;
  /* we read the current slot size and add to it the additional amount we require */
  xwimp_slot_size(-1,-1,&current_slot, NULL, &free_size);
  REPORT_D("current slot = %dk", current_slot >> 10);
  mem_estimated_k = (current_slot + mem) >> 10;
  mem_estimated_k = 100 * (mem_estimated_k / 100) + 100;
  REPORT_D("mem_estimated_k = %d",mem_estimated_k);
}

void report_memory(size_t size)
{
  /* let us find out the additional amount of memory we need */
  int more_mem_k = mem_estimated_k - (print_mem_allocated >> 10);
  if (more_mem_k <= 0) more_mem_k = size >> 10;
  if (more_mem_k == 0) more_mem_k = 1;
  gui_error2_d(gui_lookup("NEEDMEMd"), more_mem_k);
  if (gui_aborted()) gui_progress_end(keep_open);   /* does not return */
}

/* use DAs with a max size of 32MB */
#define DA_SIZE (32*1024*1024)
/* increase size in steps of 256k */
#define DA_DELTA (256*1024)

/* our strategy is to divert all memory claims to the dynamic area(s)
   and then throw the area(s) away after the page has been printed */
void* alloc_mem_da(size_t size)
{
  void* memptr = NULL;
  struct da_struct* this_da = first_da;
  struct da_struct* prev = NULL;
  struct da_struct** last_next = &first_da;
  os_error* err;
  size_t raw_size;
  size = (size + 3) &~3;    /* whole number of words! */
  raw_size = size;
  REPORT_D("Allocate %d",raw_size);
  while (this_da) {
    if (this_da->logical_free >= raw_size) {
      /* we can fit the block in here */
      /* assuming we can extend the DA accordingly */
      REPORT_DD("Use existing DA %d with %d free", this_da->no, this_da->logical_free);
      break;
    }
    last_next = &(this_da->next);
    prev = this_da;
    this_da = this_da->next;
  }
  if (!this_da) {
    /* we could not fit the block in one of our existing DAs, so allocate a new one */
    /* and add it to the end of the list */
    os_dynamic_area_no no;
    byte* base;
    int init_size = DA_DELTA;
    int max_size;
    REPORT("Allocate new DA");
    if (raw_size > init_size) init_size = raw_size + DA_DELTA;
    REPORT_D("Allocate physical size %dk",init_size >> 10);
    if ((err=xosdynamicarea_create(osdynamicarea_ALLOCATE_AREA, init_size, osdynamicarea_ALLOCATE_BASE,
                                   os_AREA_ACCESS_READ_WRITE | os_AREA_NO_USER_DRAG,
                                   DA_SIZE, NULL, NULL, "GPDriver workspace", &no, &base, &max_size)) != NULL) {
      /* allocation failed, so try exact size */
      init_size = raw_size;
      while ((err=xosdynamicarea_create(osdynamicarea_ALLOCATE_AREA, init_size, osdynamicarea_ALLOCATE_BASE,
                                        os_AREA_ACCESS_READ_WRITE | os_AREA_NO_USER_DRAG,
                                        DA_SIZE, NULL, NULL, "GPDriver workspace", &no, &base, &max_size)) != NULL) {
        /* allocation still failed, so notify the user */
        REPORT_D("DA creation with size %dk failed", init_size >> 10)
        report_memory(raw_size);
      }
    }
    /* REPORT_D("Allocated DA %d",no); */
    /* REPORT_D("DA allocated, max size = %dk",max_size >> 10); */

    /* we need to read back the actual initial size allocated */
    if ((err=xosdynamicarea_read(no, &init_size, NULL,NULL, &max_size, NULL,NULL,NULL)) != NULL) {
      /* allocation failed! */
      gui_fatal_error(err);
      return NULL;
    }

    this_da = malloc(sizeof(struct da_struct));
    *last_next = this_da;
    this_da->no = no;
    this_da->prev = prev;
    this_da->next = NULL;
    this_da->size = init_size;
    this_da->physical_free = init_size;
    this_da->logical_free = max_size;
    this_da->next_free = base;
    REPORT_DD("DA %d created, logical_free = %d",no,max_size)
  }
  /* do we need to extend the DA? */
  if (this_da->physical_free < raw_size) {
    /* extend it by DA_DELTA, but at least by raw_size - physical_free */
    int delta = DA_DELTA;
    int real_delta;
    REPORT("Extending DA");
    if (delta < raw_size - this_da->physical_free) {
      delta = raw_size - this_da->physical_free;
    }
    /* we may not always be able to get the full DA_DELTA - the area might be almost full,
       but with just enough free space for raw_size, but not enough for DA_DELTA! */
    /* NB logical_free >= physical_free but we cannot allocate logical_free because it
       includes physical_free, which has already been allocated, we can only allocate
       the difference! */
    if (delta > this_da->logical_free - this_da->physical_free) {
      delta = this_da->logical_free - this_da->physical_free;
    }
    while ((err = xos_change_dynamic_area(this_da->no, delta, &real_delta)) != NULL) {
      REPORT_DD("extension by %d [%dk] failed", delta, delta >> 10);
      report_memory(raw_size);
    }
    REPORT_D("Extended by %d",real_delta);
    this_da->physical_free += real_delta;
    this_da->size += real_delta;
  }

  /* finally, there is enough space in our DA, so we can allocate the block */
  this_da->physical_free -= raw_size;
  this_da->logical_free -= raw_size;
  REPORT_D("logical_free after alloc = %d", this_da->logical_free)
  memptr = (void*)(this_da->next_free);
  this_da->next_free += raw_size;
  da_allocated++;
  print_mem_allocated += raw_size;
  REPORT_DD("Block at %08x allocated, bytes remaining %d",(int)memptr,this_da->physical_free);
  return memptr;
}

void* alloc_mem_heap(size_t size)
{
  void* memptr;
  while((memptr = malloc(size)) == NULL) {
    report_memory(size);
  }
  print_mem_allocated += size;
  return memptr;
}

void free_mem_da(void* ptr)
{
  /* we do nothing */
  da_freed++;
}

void free_mem_heap(void* ptr)
{
  free(ptr);
}

stp_vars_t* create_vars(gpd_image_t* img, gutenprint_settings* settings)
{
  const stp_printer_t* printer;
  stp_vars_t* stp_vars;
  int is_borderless = 0;
  int gamma_set = 0;
  stp_parameter_t desc;

  REPORT_D("Found %d printers", stp_printer_model_count())
  printer = stp_get_printer_by_long_name(gpdi_get_long_name(img));
  if (printer == NULL) {
    REPORT("printer not known")
    gui_error_c(gui_lookup("PNK"), gpdi_get_long_name(img)); return 0;
  }

  /* allocate a structure and set the defaults for this printer */
  REPORT("allocate vars")
  stp_vars = stp_vars_create();
  REPORT("set defaults")
  stp_set_printer_defaults(stp_vars, printer);

  stp_set_errdata(stp_vars, NULL);
  stp_set_errfunc(stp_vars, &print_error);

  /* set the resolution if there is a resolution parameter (there might not be one if
     this is a generic PostScript driver) */
  stp_describe_parameter(stp_vars, "Resolution", &desc);
  if (desc.p_type != STP_PARAMETER_TYPE_INVALID) {
    if (desc.p_type == STP_PARAMETER_TYPE_STRING_LIST) {
      REPORT("set resolution")
      stp_set_string_parameter(stp_vars,"Resolution", gpdi_get_resolution_name(img));
    }
    stp_parameter_description_destroy(&desc);
  }

  /* we do not set the printing mode, OUTPUT_COLOR is the default anyway */

  /* set up the printer from the settings structure */
  while(settings) {
    REPORT_C("setting '%s'",settings->key)
    switch(settings->value_type) {
      case 'b':
        stp_set_boolean_parameter(stp_vars, settings->key, atoi(settings->value));
        if (strcmp(settings->key, "FullBleed") == 0)
          is_borderless = atoi(settings->value);
        break;
      case 'i':
        stp_set_int_parameter(stp_vars, settings->key, atoi(settings->value));
        break;
      case 'f':
        stp_set_float_parameter(stp_vars, settings->key, atof(settings->value));
        if (strcmp(settings->key, "Gamma") == 0)
          gamma_set = 1;
        break;
      case 's':
        stp_set_string_parameter(stp_vars, settings->key, settings->value);
        break;
      case 'd':
        stp_set_dimension_parameter(stp_vars, settings->key, atof(settings->value));
        break;
      default:
        gui_error_c(gui_lookup("UNKS"), settings->key);
        break;
    }
    settings = settings->next;
  }

  /* standard settings */
  stp_set_string_parameter(stp_vars,"InputImageType", "RGB");
  /* we do not set the output colour model - we take the default
     (should be CMY anyway - we could not do better than setting
     CMY output model) */

  /* canon end job command is only sent in JOB MODE, so we choose that */
  stp_set_string_parameter(stp_vars, "JobMode", "Job");
  return stp_vars;
}

/* return 1 for success, 0 for failure */
/* *job_started: indicates whether the job has already been started
                 if not, this routine should start it and update the flag
   copy_no: number of copies of this page printed so far
   copies:  overall number of copies to print
   pages_printed: number of pages printed so far */
int print_image(stp_vars_t* stp_vars, gpd_image_t* img, int* job_started,
                int copy_no, int copies, int pages_printed)
{
  stp_set_int_parameter(stp_vars, "PageNumber", pages_printed);

  /* let Gutenprint compute the parameters it needs */
  if (!set_page_parameters(stp_vars, img)) return 0;

  /* Now print using libgutenprint */

  /* register the function to output the printer codes */
  stp_set_outdata(stp_vars, NULL);
  stp_set_outfunc(stp_vars, &print_output);

  gui_inform(gui_lookup("VER"),2);
  if (!stp_verify(stp_vars)) {
    gui_fatal_error2(gui_lookup("CNV")); return 0;
  }

  if (copies > 1) gui_inform_dd(gui_lookup("PRINTCdd"), copy_no + 1, copies, 2);
  else gui_inform("", 2);

  if (!*job_started) {
    REPORT("calling start_job")
    stp_start_job(stp_vars, gpdi_get_stp_image(img));
    *job_started = TRUE;
  }

  /* start intercepting memory allocation */
  mem_estimated_k = 0;
  intercept_stp_memory(1);
  data_dumped = 0; print_error_reported = 0;

  /* CALL THE MAIN PRINT FUNCTION */
  /* there is a significant delay before Gutenprint asks for the first row,
     so there should be an hourglass - the GUI will switch it off when
     first calling Wimp_Poll */
  gui_hourglass_on();
  REPORT("calling print")
  stp_print(stp_vars, gpdi_get_stp_image(img));

  /* just in case there was no Wimp_Poll inbetween, otherwise the hourglass
     has been turned off already */
  gui_hourglass_off();

  /* stop intercepting memory allocation */
  intercept_stp_memory(0);
  if (print_error_reported) return 0;

  /* if we do arrive here, we have finished successfully */
  gui_inform(gui_lookup("FIN"),2);
  gui_inform_d(gui_lookup("DUMP"),data_dumped,2);
  return 1;
}

/* start (start=1) or stop (start=0) intercepting Gutenprint's memory allocation */
static void intercept_stp_memory(int start)
{
  if (start) {
    /* we need to check whether we need to use dynamic areas */
    int current_slot, max_size, free_size;
    REPORT("Start intercept memory");
    use_da = 0;
    /* read the maximum app slot size */
    xos_read_dynamic_area(os_DYNAMIC_AREA_APPLICATION_SPACE, NULL, NULL, &max_size);
    REPORT_D("app slot max size = %dk", max_size >> 10);
    xwimp_slot_size(-1,-1,&current_slot, NULL, &free_size);
    REPORT_D("current slot = %dk", current_slot >> 10);
    REPORT_D("free memory = %dk", free_size >> 10);
    /* if we can fit all the free memory in the app slot, then we will not need a DA */
    if (current_slot + free_size > max_size) {
      /* we cannot make use of all the free memory within the wimp slot, so
         intercept Gutenprint's memory management */
      REPORT("Use DA");
      use_da = TRUE;
      register_memory_funcs(&alloc_mem_da, &free_mem_da, &realloc);
      da_allocated = 0; da_freed = 0;
    }
    else {
      /* use the heap */
      register_memory_funcs(&alloc_mem_heap, &free_mem_heap, &realloc);
    }
    print_mem_allocated = 0;
    stp_register_memory_inform_func(&memory_inform);
  }
  else {
    REPORT("End intercept memory");
    register_memory_funcs(NULL, NULL, NULL);
    if (use_da) {
      /* stop intercepting Gutenprint's memory management */
      remove_das();
      use_da = 0;
    }
    stp_register_memory_inform_func(NULL);
  }
}

static void remove_das(void)
{
  struct da_struct* this_da = first_da;
  /* remove all DAs in reverse order of allocation */
  /* first, find the last da in list */
  while(this_da && this_da->next) {
    this_da = this_da->next;
  }
  /* then delete the items travelling backwards in the list */
  REPORT("Remove DAs");
  while(this_da) {
    struct da_struct* prev = this_da->prev;
    xosdynamicarea_delete(this_da->no);
    free(this_da); this_da = prev;
  }
  first_da = NULL;
}

/* work out media size from paper size info in job file */
/* return 1 for success */
int set_page_parameters(stp_vars_t* stp_vars, gpd_image_t* img)
{
  const stp_papersize_t* paper_size;
  int pwidth_mp, pheight_mp, pwidth, pheight, left, top, n_paper_sizes, this_paper;
  int i_left, i_right, i_top_from_top, i_bottom_from_top;   /* imageable area as reported from Gutenprint */
  int m_width, m_height;                  /* media size as reported from Gutenprint */
  int cropped_rows = 0, cropped_columns = 0;
  int cropped_bottom_rows = 0, cropped_right_columns = 0;
  stp_parameter_t desc;
  const char* our_paper;

  pwidth_mp = gpdi_get_pwidth_mp(img);
  pheight_mp = gpdi_get_pheight_mp(img);
  pwidth = pwidth_mp / 1000;
  pheight = pheight_mp / 1000;

  int printer_has_page_size_parameter = 0;
  stp_describe_parameter(stp_vars, "PageSize", &desc);
  if (desc.p_type == STP_PARAMETER_TYPE_STRING_LIST)
    printer_has_page_size_parameter = 1;

  if (printer_has_page_size_parameter) {
#ifndef GPDRIVER_GUI
    printf("Finding paper size (%d,%d)\n",pheight, pwidth);
#endif
    REPORT_DD("Finding paper size (%d,%d)", pheight, pwidth);

    /* try to find an appropriate media size in Gutenprint */
    paper_size = stp_get_papersize_by_size(pheight, pwidth);

    /* we set the media size to a named paper size in Gutenprint */
    /* this may or may not be overridden by set_page_width/height below */
    if (!paper_size) {
      /* we did not find the media size */
#ifndef GPDRIVER_GUI
      printf("Could not find paper size, using Custom");
#endif
      REPORT("Could not find paper size, trying Custom")
      stp_set_string_parameter(stp_vars, "PageSize", "Custom");

       /* we set the physical page width to the user's chosen page size */
      stp_set_page_width(stp_vars, pwidth);
      stp_set_page_height(stp_vars, pheight);
    }
    else {
#ifndef GPDRIVER_GUI
      printf("Found paper size %s\n", paper_size->name);
#endif
      REPORT_C("Found paper size %s", paper_size->name)
      stp_set_string_parameter(stp_vars, "PageSize", paper_size->name);
    }

    /* now we check whether that page size is supported by the given printer! */
    REPORT("checking availability of paper size")
    stp_string_list_t* paper_sizes;
    our_paper = stp_get_string_parameter(stp_vars, "PageSize");

    paper_sizes = desc.bounds.str;
    n_paper_sizes = stp_string_list_count(paper_sizes);
    for (this_paper = 0; this_paper < n_paper_sizes; ++this_paper) {
      stp_param_string_t* str = stp_string_list_param(paper_sizes, this_paper);
      if (strcmp(our_paper, str->name) == 0) break;
    }
    if (this_paper == n_paper_sizes) {
      /* we did not find our paper in the list! */
      /* we choose the smallest page that is bigger than the one we need */
      char msg2[256];
      int this_size = -1;
      int best_paper = -1;    /* no suitable paper found yet */
      REPORT("Paper size not supported by printer")
      for (this_paper = 0; this_paper < n_paper_sizes; ++this_paper) {
        stp_param_string_t* str = stp_string_list_param(paper_sizes, this_paper);
        const char* this_page_name = str->name;
        const stp_papersize_t* this_paper_size = stp_get_papersize_by_name(this_page_name);
        int width = this_paper_size->width;
        int height = this_paper_size->height;
        if (width >= pwidth && height >= pheight) {
          REPORT_C("found suitable paper size %s", this_page_name)
          if (best_paper == -1 || (width * height < this_size)) {
            /* no suitable paper found yet or this one is smaller than the best match so far */
            REPORT("best candidate")
            best_paper = this_paper;
            this_size = width * height;
          }
        }
      }
      if (best_paper == -1) {
        REPORT("Did not find any suitable paper size")
        gui_fatal_error2(gui_lookup("PAGEnf2"));
        return 0;
      }
      stp_param_string_t* best_paper_par = stp_string_list_param(paper_sizes, best_paper);
      sprintf(msg2,gui_lookup("PAGEnvc"), best_paper_par->text);
      gui_error2(msg2);
      if (gui_aborted()) return 0;
      stp_set_string_parameter(stp_vars, "PageSize", best_paper_par->name);
    }
  }
  else {
    stp_set_page_width(stp_vars, pwidth);
    stp_set_page_height(stp_vars, pheight);
  }

  if (desc.p_type != STP_PARAMETER_TYPE_INVALID)
    stp_parameter_description_destroy(&desc);

  /* read the imagable area */
  stp_get_imageable_area(stp_vars, &i_left, &i_right, &i_bottom_from_top, &i_top_from_top);
  REPORT_D("Imageable left %dpt",i_left);
  REPORT_D("Imageable right %dpt",i_right);
  REPORT_D("Imageable top %dpt",i_top_from_top);
  REPORT_D("Imageable bottom %dpt",i_bottom_from_top);
  REPORT_D("Imageable top %d mm/10", 2540 * i_top_from_top / 720);

  /* Find out about the top and left margins
   * These are at least the paper margins as defined in !Printers and stored in the job
   * file, but there are additional factors (in the job file as well):
   * - FIRSTR/FIRSTP
   * - TOPM/LEFTM
   * FIRSTR/FIRSTP are offsets from PDumperGP when finding out that some top rows or left
   * columns are completely empty, in which case the resulting bitmap does not contain
   * them. TOPM/LEFTM are offsets from PDriverDP ("additional blank rows to print", "left
   * margin") when the printed area as passed by the application is smaller than the
   * printable area of the paper.
   * We ignore the paper borders completely, they have been dealt with by PDriverDP and
   * are now subsumed in TOPM/LEFTM.
   */
  /* left = left paper margin + FIRSTP + LEFTM, all converted to pt */
  REPORT_D("LEFTM %dpt",gpdi_get_leftm(img) * 72 / gpdi_get_xres(img))
  REPORT_D("LEFTM %dmm/10",2540 * gpdi_get_leftm(img) / gpdi_get_xres(img))
  REPORT_D("FIRSTP %dpt",gpdi_get_firstp(img) * 72 / gpdi_get_xres(img))
  REPORT_D("FIRSTP %dmm/10",2540 * gpdi_get_firstp(img) / gpdi_get_xres(img))
  REPORT_D("Page X0 %dpt",gpdi_get_page_x0_mp(img) / 1000);
  REPORT_D("Page X0 %dmm/10",254 * gpdi_get_page_x0_mp(img) / 7200);

  left = 72 * (gpdi_get_firstp(img) + gpdi_get_leftm(img)) / gpdi_get_xres(img);
#ifndef GPDRIVER_GUI
  printf("Left = %dpt (%d mm)\n",left,254 * left / 720);
  printf("Page Y1 = %dpt (%dmm)\n",gpdi_get_page_y1_mp(img) / 1000,(gpdi_get_page_y1_mp(img) / 1000) * 254 / 720);
  printf("FirstR = %d, YRes = %d\n",gpdi_get_firstr(img), gpdi_get_yres(img));
#endif
  REPORT_D("Left = %d pt", left);
  REPORT_D("Left = %d mm/10",2540 * left / 720);
  /* top = top paper margin + FIRSTR + TOPM, all converted to pt */
  top = 72 * (gpdi_get_firstr(img) + gpdi_get_topm(img))/ gpdi_get_yres(img);
#ifndef GPDRIVER_GUI
  printf("Top = %dpt (%d mm)\n",top,254 * top / 720);
#endif
  REPORT_D("Top = %d pt", top);
  REPORT_D("Top = %d mm/10",2540 * top / 720);

  /* these are relative to the page border which Gutenprint appears to want
     (in contrast to Gimp-Print, which wanted them relative to the imageable area) */
  /* in contrast to GPDriver 1, we no longer move the bitmap if it is outside the
     imageable area, instead we crop it properly */
  if (i_top_from_top > top) {
    /* we crop some rows - to compute this precisely, we compute the position of
       the imageable border in bitmap row coordinates */
    int top_border_in_yres_rows = i_top_from_top * gpdi_get_yres(img) / 72;
    cropped_rows = top_border_in_yres_rows - (gpdi_get_firstr(img) + gpdi_get_topm(img));
    if (cropped_rows < 0) cropped_rows = 0;         /* might happen due to rounding? */
    top = i_top_from_top;
    REPORT_D("cropped rows = %d", cropped_rows);
  }
  /* when computing the height it is important to round up by adding gpdi_get_yres(img) / 72 - 1
     because we need to cover the complete height in the original resolution */
  int image_height = (72 * (gpdi_get_height(img) - cropped_rows) + gpdi_get_yres(img) - 1) / gpdi_get_yres(img);
  if (top + image_height > i_bottom_from_top) {
    /* we need to crop at the bottom, too */
    /* calculate how many bitmap rows we must cut off */
    int size_in_pt_to_cut_off = top + image_height - i_bottom_from_top;
    cropped_bottom_rows = size_in_pt_to_cut_off * gpdi_get_yres(img) / 72;
    image_height -= size_in_pt_to_cut_off;
    REPORT_D("cropped bottom rows = %d", cropped_bottom_rows);
  }
  /* since we can only specify the height of the bitmap in full pt and we rounded up about we might now
     stretch the image slightly, which can be a problem if it was less than a pt high to begin with, e.g.,
     just a horizontal 0.25pt line would be vertically stretched to become a 1pt line! */
  /* therefore, we want to make sure that our virtual bitmap is a full number of pt high to avoid such
     aliasing problems */
  /* so, we might have to "invent" a few extra empty rows at the bottom - NB: the extra rows only
     extend to the next pt boundary so they do not influence image_height */
  int invented_rows =
    (image_height * gpdi_get_yres(img) - 72 * (gpdi_get_height(img) - cropped_rows - cropped_bottom_rows)) / 72;
  /* might happen due to rounding */
  if (invented_rows < 0) invented_rows = 0;

  if (i_left > left) {
    int left_border_in_xres_columns = i_left * gpdi_get_xres(img) / 72;
    cropped_columns = left_border_in_xres_columns - (gpdi_get_firstp(img) + gpdi_get_leftm(img));
    if (cropped_columns < 0) cropped_columns = 0;   /* might happen due to rounding? */
    left = i_left;
    REPORT_D("cropped columns = %d", cropped_columns);
  }
  /* as above, round up when computing the width */
  int image_width = (72 * (gpdi_get_width(img) - cropped_columns) + gpdi_get_xres(img) - 1) / gpdi_get_xres(img);
  if (left + image_width > i_right) {
    /* we need to crop at the right, too */
    /* calculate how many columns we must cut off */
    int size_in_pt_to_cut_off = left + image_width - i_right;
    cropped_right_columns = size_in_pt_to_cut_off * gpdi_get_xres(img) / 72;
    image_width -= size_in_pt_to_cut_off;
    REPORT_D("cropped right columns = %d", cropped_right_columns);
  }
  /* as above, we need to make the width a full number of pt */
  int invented_columns =
    (image_width * gpdi_get_xres(img) - 72 * (gpdi_get_width(img) - cropped_columns - cropped_right_columns)) / 72;
  /* might happen due to rounding */
  if (invented_columns < 0) invented_columns = 0;
  gpdi_set_cropping(img, cropped_rows, cropped_columns, cropped_bottom_rows, cropped_right_columns);
  REPORT_DD("extending image by %d rows and %d columns", invented_rows, invented_columns);
  gpdi_extend_image(img, invented_rows, invented_columns);

  /* we might have cropped everything, in which case we still print a 1 by 1 bitmap */
  /* but we need to make sure that it is within the margins! */
  if (gpdi_get_height(img) - cropped_rows - cropped_bottom_rows <= 0
      || gpdi_get_width(img) - cropped_columns - cropped_right_columns <= 0) {
    top = i_top_from_top;
    left = i_left;
    image_width = 1;
    image_height = 1;
  }

  /* set the position */
  REPORT_D("setting left %d", left);
  stp_set_left(stp_vars, left);
  REPORT_D("setting top %d", top);
  stp_set_top(stp_vars, top);

  /* set the image width and height */
  REPORT_D("setting image width %d", image_width);
  stp_set_width(stp_vars, image_width);
  REPORT_D("setting image height %d", image_height);
  stp_set_height(stp_vars, image_height);
  return 1;
}

/* does not return if malloc failed */
static void* checked_alloc(size_t size)
{
  void* memptr = malloc(size);
  if (!memptr) {
    gui_fatal_error2(gui_lookup("NOMEM"));
  }
  return memptr;
}
