/* sane - Scanner Access Now Easy.

   pieusb_specific.c

   Copyright (C) 2012-2015 Jan Vleeshouwers, Michael Rickmann, Klaus Kaempf

   This file is part of the SANE package.

   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, see <https://www.gnu.org/licenses/>.

   As a special exception, the authors of SANE give permission for
   additional uses of the libraries contained in this release of SANE.

   The exception is that, if you link a SANE library with other files
   to produce an executable, this does not by itself cause the
   resulting executable to be covered by the GNU General Public
   License.  Your use of that executable is in no way restricted on
   account of linking the SANE library code into it.

   This exception does not, however, invalidate any other reasons why
   the executable file might be covered by the GNU General Public
   License.

   If you submit changes to SANE to the maintainers to be included in
   a subsequent release, you agree by submitting the changes that
   those changes may be distributed with this exception intact.

   If you write modifications of your own for SANE, it is your choice
   whether to permit this exception to apply to your modifications.
   If you do not wish that, delete this exception notice.  */

/* =========================================================================
 *
 * Various Pieusb backend specific functions
 *
 * Option handling, configuration file handling, post-processing
 *
 * ========================================================================= */

#define DEBUG_DECLARE_ONLY
#include "pieusb.h"

#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include "../include/sane/sane.h"
#include "../include/sane/saneopts.h"
#include "../include/sane/sanei_config.h"

#include <errno.h>
#include <math.h>
#include <time.h>

#include "pieusb_usb.h"
#include "pieusb_scancmd.h"
#include "pieusb_buffer.h"
#include "pieusb_specific.h"

/* Pieusb specific */

/* sub to sanei_pieusb_find_device_callback() */
static SANE_Status pieusb_initialize_device_definition (Pieusb_Device_Definition* dev, Pieusb_Scanner_Properties* inq, const char* devicename, SANE_Word vendor_id, SANE_Word product_id);
static void pieusb_print_inquiry (Pieusb_Device_Definition * dev);

/* sub to sane_start() */
static void pieusb_calculate_shading(struct Pieusb_Scanner *scanner, SANE_Byte* buffer);

/* MR */
/* sub to sanei_pieusb_post() */
static SANE_Status pieusb_write_pnm_file (char *filename, uint16_t *data, int depth, int channels, int pixels_per_line, int lines);

/* Auxiliary */
static size_t max_string_size (SANE_String_Const const strings[]);
static double getGain(int gain);
static int getGainSetting(double gain);
/*
static void updateGain(Pieusb_Scanner *scanner, int color_index);
*/
static void updateGain2(Pieusb_Scanner *scanner, int color_index, double gain_increase);

/* --------------------------------------------------------------------------
 *
 * SPECIFIC PIEUSB
 *
 * --------------------------------------------------------------------------*/

/* Settings for byte order */
#define SCAN_IMG_FMT_OKLINE          0x08
#define SCAN_IMG_FMT_BLK_ONE         0x04
#define SCAN_IMG_FMT_MOTOROLA        0x02
#define SCAN_IMG_FMT_INTEL           0x01

/* Settings for scanner capabilities */
#define SCAN_CAP_PWRSAV              0x80
#define SCAN_CAP_EXT_CAL             0x40
#define SCAN_CAP_FAST_PREVIEW        0x10
#define SCAN_CAP_DISABLE_CAL         0x08
#define SCAN_CAP_SPEEDS              0x07

/* Available scanner options */
#define SCAN_OPT_DEV_MPCL            0x80
#define SCAN_OPT_DEV_TP1             0x04
#define SCAN_OPT_DEV_TP              0x02
#define SCAN_OPT_DEV_ADF             0x01

/* Options */
#define SANE_NAME_EXPOSURE_R         "exposure-time-r"
#define SANE_TITLE_EXPOSURE_R        "Exposure time red"
#define SANE_DESC_EXPOSURE_R         "The time the red color filter of the CCD is exposed"
#define SANE_NAME_EXPOSURE_G         "exposure-time-g"
#define SANE_TITLE_EXPOSURE_G        "Exposure time green"
#define SANE_DESC_EXPOSURE_G         "The time the green color filter of the CCD is exposed"
#define SANE_NAME_EXPOSURE_B         "exposure-time-b"
#define SANE_TITLE_EXPOSURE_B        "Exposure time blue"
#define SANE_DESC_EXPOSURE_B         "The time the blue color filter of the CCD is exposed"
#define SANE_NAME_EXPOSURE_I         "exposure-time-i"
#define SANE_TITLE_EXPOSURE_I        "Exposure time infrared"
#define SANE_DESC_EXPOSURE_I         "The time the infrared color filter of the CCD is exposed"
#define SANE_EXPOSURE_DEFAULT        DEFAULT_EXPOSURE
#if 1
#define SANE_NAME_GAIN_R               "gain-r"
#define SANE_TITLE_GAIN_R              "Gain red"
#define SANE_DESC_GAIN_R               "The gain of the signal processor for red"
#define SANE_NAME_GAIN_G               "gain-g"
#define SANE_TITLE_GAIN_G              "Gain green"
#define SANE_DESC_GAIN_G               "The gain of the signal processor for green"
#define SANE_NAME_GAIN_B               "gain-b"
#define SANE_TITLE_GAIN_B              "Gain blue"
#define SANE_DESC_GAIN_B               "The gain of the signal processor for blue"
#define SANE_NAME_GAIN_I               "gain-i"
#define SANE_TITLE_GAIN_I              "Gain infrared"
#define SANE_DESC_GAIN_I               "The gain of the signal processor for infrared"
#define SANE_GAIN_DEFAULT            DEFAULT_GAIN

#define SANE_NAME_OFFSET_R             "offset-r"
#define SANE_TITLE_OFFSET_R            "Offset red"
#define SANE_DESC_OFFSET_R             "The offset of the signal processor for red"
#define SANE_NAME_OFFSET_G             "offset-g"
#define SANE_TITLE_OFFSET_G            "Offset greed"
#define SANE_DESC_OFFSET_G             "The offset of the signal processor for green"
#define SANE_NAME_OFFSET_B             "offset-b"
#define SANE_TITLE_OFFSET_B            "Offset blue"
#define SANE_DESC_OFFSET_B             "The offset of the signal processor for blue"
#define SANE_NAME_OFFSET_I             "offset-i"
#define SANE_TITLE_OFFSET_I            "Offset infrared"
#define SANE_DESC_OFFSET_I             "The offset of the signal processor for infrared"
#define SANE_OFFSET_DEFAULT          DEFAULT_OFFSET
#else
#define SANE_NAME_GAIN               "gain"
#define SANE_TITLE_GAIN              "Gain"
#define SANE_DESC_GAIN               "The gain of the signal processor for the 4 CCD color filters (R,G,B,I)"
#define SANE_GAIN_DEFAULT            0x13

#define SANE_NAME_OFFSET             "offset"
#define SANE_TITLE_OFFSET            "Offset"
#define SANE_DESC_OFFSET             "The offset of the signal processor for the 4 CCD color filters (R,G,B,I)"
#define SANE_OFFSET_DEFAULT          0
#endif
#define min(a,b) (((a)<(b))?(a):(b))
#define max(a,b) (((a)>(b))?(a):(b))

static const SANE_Range percentage_range_100 = {
  0 << SANE_FIXED_SCALE_SHIFT,	  /* minimum */
  100 << SANE_FIXED_SCALE_SHIFT,  /* maximum */
  0 << SANE_FIXED_SCALE_SHIFT	  /* quantization */
};

/* From the firmware disassembly */
static const SANE_Range gain_range = {
  0,	  /* minimum */
  63,     /* maximum */
  0	  /* quantization */
};

/* From the firmware disassembly */
static const SANE_Range offset_range = {
  0,      /* minimum */
  255,    /* maximum */
  0	  /* quantization */
};

static const double gains[] = {
1.000, 1.075, 1.154, 1.251, 1.362, 1.491, 1.653, /*  0,  5, 10, 15, 20, 25, 30 */
1.858, 2.115, 2.458, 2.935, 3.638, 4.627         /* 35, 40, 45, 50, 55, 60 */
};

/**
 * Callback called whenever a connected USB device reports a supported vendor
 * and product id combination.
 * Used by sane_init() and by sane_open().
 *
 * @param name Device name which has required vendor and product id
 * @return SANE_STATUS_GOOD
 */
SANE_Status
sanei_pieusb_find_device_callback (const char *devicename)
{
    struct Pieusb_Command_Status status;
    SANE_Status r;
    Pieusb_Device_Definition *dev;
    int device_number; /* index in usb devices list maintained by sani_usb */
    Pieusb_Scanner_Properties inq;
    int retry;

    DBG (DBG_info_proc, "sanei_pieusb_find_device_callback: %s\n", devicename);

    /* Check if device is present in the Pieusb device list */
    for (dev = pieusb_definition_list_head; dev; dev = dev->next) {
        if (strcmp (dev->sane.name, devicename) == 0) {
	    return SANE_STATUS_GOOD;
        }
    }

    /* If not, create a new device struct */
    dev = malloc (sizeof (*dev));
    if (!dev) {
        return SANE_STATUS_NO_MEM;
    }

    /* Get device number: index of the device in the sanei_usb devices list */
    r = sanei_usb_open (devicename, &device_number);
    if (r != SANE_STATUS_GOOD) {
        free (dev);
        DBG (DBG_error, "sanei_pieusb_find_device_callback: sanei_usb_open failed for device %s: %s\n",devicename,sane_strstatus(r));
        return r;
    }

    /* Get device properties */

    retry = 2;
    while (retry > 0) {
      retry--;
      /* get inquiry data length */
      sanei_pieusb_cmd_inquiry (device_number, &inq, 5, &status);
      if (status.pieusb_status == PIEUSB_STATUS_GOOD) {
	break;
      }
      else if (status.pieusb_status == PIEUSB_STATUS_IO_ERROR) {
	if (retry > 0) {
	  DBG (DBG_info_proc, "inquiry failed, resetting usb\n");
	  if (sanei_pieusb_usb_reset(device_number) == SANE_STATUS_GOOD) {
	    continue; /* retry after IEEE1284 reset */
	  }
	  if (sanei_usb_reset(device_number) == SANE_STATUS_GOOD) {
	    continue; /* retry after USB reset */
	  }
	}
      }
      free (dev);
      DBG (DBG_error, "sanei_pieusb_find_device_callback: get scanner properties (5 bytes) failed with %d\n", status.pieusb_status);
      sanei_usb_close (device_number);
      return sanei_pieusb_convert_status (status.pieusb_status);
    }
    /* get full inquiry data */
    sanei_pieusb_cmd_inquiry(device_number, &inq, inq.additionalLength+4, &status);
    if (status.pieusb_status != PIEUSB_STATUS_GOOD) {
        free (dev);
        DBG (DBG_error, "sanei_pieusb_find_device_callback: get scanner properties failed\n");
        sanei_usb_close (device_number);
        return sanei_pieusb_convert_status (status.pieusb_status);
    }

    /* Close the device again */
    sanei_usb_close(device_number);

    /* Initialize device definition */
    r = pieusb_initialize_device_definition(dev, &inq, devicename, pieusb_supported_usb_device.vendor, pieusb_supported_usb_device.product);
    if (r != SANE_STATUS_GOOD) {
      return r;
    }

    /* Output */
    pieusb_print_inquiry (dev);

    /* Check model number */
    if (inq.model != pieusb_supported_usb_device.model) {
        free (dev);
        DBG (DBG_error, "sanei_pieusb_find_device_callback: wrong model number %d\n", inq.model);
        return SANE_STATUS_INVAL;
    }

    dev->flags = pieusb_supported_usb_device.flags;

    /* Found a supported scanner, put it in the definitions list*/
    DBG (DBG_info_proc, "sanei_pieusb_find_device_callback: success\n");
    dev->next = pieusb_definition_list_head;
    pieusb_definition_list_head = dev;
    return SANE_STATUS_GOOD;
}

/**
 * Full initialization of a Pieusb_Device structure from INQUIRY data.
 * The function is used in find_device_callback(), so when sane_init() or
 * sane_open() is called.
 *
 * @param dev
 */
static SANE_Status
pieusb_initialize_device_definition (Pieusb_Device_Definition* dev, Pieusb_Scanner_Properties* inq, const char* devicename,
        SANE_Word vendor_id, SANE_Word product_id)
{
    char *pp, *buf;

    /* Initialize device definition */
    dev->next = NULL;
    dev->sane.name = strdup(devicename);

    /* Create 0-terminated string without trailing spaces for vendor */
    buf = malloc(9);
    if (buf == NULL)
      return SANE_STATUS_NO_MEM;
    memcpy(buf, inq->vendor, 8);
    pp = buf + 8;
    *pp-- = '\0';
    while (*pp == ' ') *pp-- = '\0';
    dev->sane.vendor = buf;

    /* Create 0-terminated string without trailing spaces for model */
    buf = malloc(17);
    if (buf == NULL)
      return SANE_STATUS_NO_MEM;
    memcpy(buf, inq->product, 16);
    pp = buf + 16;
    *pp-- = '\0';
    while (*pp == ' ') *pp-- = '\0';
    dev->sane.model = buf;

    dev->sane.type = "film scanner";
    dev->vendorId = vendor_id;
    dev->productId = product_id;

    /* Create 0-terminated strings without trailing spaces for revision */
    buf = malloc(5);
    if (buf == NULL)
      return SANE_STATUS_NO_MEM;
    memcpy(buf, inq->productRevision, 4);
    pp = buf + 4;
    *pp-- = '\0';
    while (*pp == ' ') *pp-- = '\0';
    dev->version = buf;

    dev->model = inq->model;

    /* Maximum resolution values */
    dev->maximum_resolution_x = inq->maxResolutionX;
    dev->maximum_resolution_y = inq->maxResolutionY;
    if (dev->maximum_resolution_y < 256) {
        /* y res is a multiplier */
        dev->maximum_resolution = dev->maximum_resolution_x;
        dev->maximum_resolution_x *= dev->maximum_resolution_y;
        dev->maximum_resolution_y = dev->maximum_resolution_x;
    } else {
      /* y res really is resolution */
      dev->maximum_resolution = min (dev->maximum_resolution_x, dev->maximum_resolution_y);
    }

    /* Geometry */
    dev->scan_bed_width = (double) inq->maxScanWidth / dev->maximum_resolution;
    dev->scan_bed_height = (double) inq->maxScanHeight / dev->maximum_resolution;
    dev->slide_top_left_x = inq->x0;
    dev->slide_top_left_y = inq->y0;
    dev->slide_width = (double) (inq->x1 - inq->x0) / dev->maximum_resolution;
    dev->slide_height = (double) (inq->y1 - inq->y0) / dev->maximum_resolution;

    /* Integer and bit-encoded properties */
    dev->halftone_patterns = inq->halftones & 0x0f;
    dev->color_filters = inq->filters;
    dev->color_depths = inq->colorDepths;
    dev->color_formats = inq->colorFormat;
    dev->image_formats = inq->imageFormat;
    dev->scan_capabilities = inq->scanCapability;
    dev->optional_devices = inq->optionalDevices;
    dev->enhancements = inq->enhancements;
    dev->gamma_bits = inq->gammaBits;
    dev->fast_preview_resolution = inq->previewScanResolution;
    dev->minimum_highlight = inq->minumumHighlight;
    dev->maximum_shadow = inq->maximumShadow;
    dev->calibration_equation = inq->calibrationEquation;
    dev->minimum_exposure = inq->minimumExposure;
    dev->maximum_exposure = inq->maximumExposure*4; /* *4 to solve the strange situation that the default value is out of range */

    dev->x0 = inq->x0;
    dev->y0 = inq->y0;
    dev->x1 = inq->x1;
    dev->y1 = inq->y1;
    dev->production = strndup(inq->production, 4);
    dev->timestamp = strndup(inq->timestamp, 20);
    dev->signature = (char *)strndup((char *)inq->signature, 40);

    /* Ranges for various quantities */
    dev->x_range.min = SANE_FIX (0);
    dev->x_range.quant = SANE_FIX (0);
    dev->x_range.max = SANE_FIX (dev->scan_bed_width * MM_PER_INCH);

    dev->y_range.min = SANE_FIX (0);
    dev->y_range.quant = SANE_FIX (0);
    dev->y_range.max = SANE_FIX (dev->scan_bed_height * MM_PER_INCH);

    dev->dpi_range.min = SANE_FIX (25);
    dev->dpi_range.quant = SANE_FIX (1);
    dev->dpi_range.max = SANE_FIX (max (dev->maximum_resolution_x, dev->maximum_resolution_y));

    dev->shadow_range.min = SANE_FIX (0);
    dev->shadow_range.quant = SANE_FIX (1);
    dev->shadow_range.max = SANE_FIX (dev->maximum_shadow);

    dev->highlight_range.min = SANE_FIX (dev->minimum_highlight);
    dev->highlight_range.quant = SANE_FIX (1);
    dev->highlight_range.max = SANE_FIX (100);

    dev->exposure_range.min = dev->minimum_exposure;
    dev->exposure_range.quant = 1;
    dev->exposure_range.max = dev->maximum_exposure;

    dev->dust_range.min = 0;
    dev->dust_range.quant = 1;
    dev->dust_range.max = 100;

    /* Enumerated ranges vor various quantities */
    /*TODO: create from inq->filters */
    dev->scan_mode_list[0] = SANE_VALUE_SCAN_MODE_LINEART;
    dev->scan_mode_list[1] = SANE_VALUE_SCAN_MODE_HALFTONE;
    dev->scan_mode_list[2] = SANE_VALUE_SCAN_MODE_GRAY;
    dev->scan_mode_list[3] = SANE_VALUE_SCAN_MODE_COLOR;
    dev->scan_mode_list[4] = SANE_VALUE_SCAN_MODE_RGBI;
    dev->scan_mode_list[5] = 0;

    dev->calibration_mode_list[0] = SCAN_CALIBRATION_DEFAULT;
    dev->calibration_mode_list[1] = SCAN_CALIBRATION_AUTO;
    dev->calibration_mode_list[2] = SCAN_CALIBRATION_PREVIEW;
    dev->calibration_mode_list[3] = SCAN_CALIBRATION_OPTIONS;
    dev->calibration_mode_list[4] = 0;

    dev->gain_adjust_list[0] = SCAN_GAIN_ADJUST_03;
    dev->gain_adjust_list[1] = SCAN_GAIN_ADJUST_05;
    dev->gain_adjust_list[2] = SCAN_GAIN_ADJUST_08;
    dev->gain_adjust_list[3] = SCAN_GAIN_ADJUST_10;
    dev->gain_adjust_list[4] = SCAN_GAIN_ADJUST_12;
    dev->gain_adjust_list[5] = SCAN_GAIN_ADJUST_16;
    dev->gain_adjust_list[6] = SCAN_GAIN_ADJUST_19;
    dev->gain_adjust_list[7] = SCAN_GAIN_ADJUST_24;
    dev->gain_adjust_list[8] = SCAN_GAIN_ADJUST_30;
    dev->gain_adjust_list[9] = 0;

    /*TODO: create from inq->colorDepths? Maybe not: didn't experiment with
     * 4 and 12 bit depths. Don;t know how they behave. */
    dev->bpp_list[0] = 3; /* count */
    dev->bpp_list[1] = 1;
    dev->bpp_list[2] = 8;
    dev->bpp_list[3] = 16;

    /* Infrared */
    dev->ir_sw_list[0] = "None";
    dev->ir_sw_list[1] = "Reduce red overlap";
    dev->ir_sw_list[2] = "Remove dirt";
    dev->ir_sw_list[3] = 0;

    dev->grain_sw_list[0] = 4;
    dev->grain_sw_list[1] = 0;
    dev->grain_sw_list[2] = 1;
    dev->grain_sw_list[3] = 2;
    dev->grain_sw_list[4] = 3;
    dev->grain_sw_list[5] = 0;

    dev->crop_sw_list[0] = "None";
    dev->crop_sw_list[1] = "Outside";
    dev->crop_sw_list[2] = "Inside";
    dev->crop_sw_list[3] = 0;

    /* halftone_list */
    dev->halftone_list[0] = "53lpi 45d ROUND"; /* 8x8 pattern */
    dev->halftone_list[1] = "70lpi 45d ROUND"; /* 6x6 pattern */
    dev->halftone_list[2] = "75lpi Hori. Line"; /* 4x4 pattern */
    dev->halftone_list[3] = "4X4 BAYER"; /* 4x4 pattern */
    dev->halftone_list[4] = "4X4 SCROLL"; /* 4x4 pattern */
    dev->halftone_list[5] = "5x5 26 Levels"; /* 5x5 pattern */
    dev->halftone_list[6] = "4x4 SQUARE"; /* 4x4 pattern */
    dev->halftone_list[7] = "5x5 TILE"; /* 5x5 pattern */
    dev->halftone_list[8] = 0;

    return SANE_STATUS_GOOD;
}

/**
 * Output device definition.
 * The function is used in find_device_callback(), so when sane_init() or
 * sane_open() is called.
 *
 * @param dev Device to output
 */
static void
pieusb_print_inquiry (Pieusb_Device_Definition * dev)
{
  DBG (DBG_inquiry, "INQUIRY:\n");
  DBG (DBG_inquiry, "========\n");
  DBG (DBG_inquiry, "\n");
  DBG (DBG_inquiry, "vendor........................: '%s'\n", dev->sane.vendor);
  DBG (DBG_inquiry, "product.......................: '%s'\n", dev->sane.model);
  DBG (DBG_inquiry, "model  .......................: 0x%04x\n", dev->model);
  DBG (DBG_inquiry, "version.......................: '%s'\n", dev->version);

  DBG (DBG_inquiry, "X resolution..................: %d dpi\n",
       dev->maximum_resolution_x);
  DBG (DBG_inquiry, "Y resolution..................: %d dpi\n",
       dev->maximum_resolution_y);
  DBG (DBG_inquiry, "pixel resolution..............: %d dpi\n",
       dev->maximum_resolution);
  DBG (DBG_inquiry, "fb width......................: %f in\n",
       dev->scan_bed_width);
  DBG (DBG_inquiry, "fb length.....................: %f in\n",
       dev->scan_bed_height);

  DBG (DBG_inquiry, "transparency width............: %f in\n",
       dev->slide_width);
  DBG (DBG_inquiry, "transparency length...........: %f in\n",
       dev->slide_height);
  DBG (DBG_inquiry, "transparency offset...........: %d,%d\n",
       dev->slide_top_left_x, dev->slide_top_left_y);

  DBG (DBG_inquiry, "# of halftones................: %d\n",
       dev->halftone_patterns);

  DBG (DBG_inquiry, "One pass color................: %s\n",
       dev->color_filters & SCAN_ONE_PASS_COLOR ? "yes" : "no");

  DBG (DBG_inquiry, "Filters.......................: %s%s%s%s%s (%02x)\n",
       dev->color_filters & SCAN_FILTER_INFRARED ? "Infrared " : "",
       dev->color_filters & SCAN_FILTER_RED ? "Red " : "",
       dev->color_filters & SCAN_FILTER_GREEN ? "Green " : "",
       dev->color_filters & SCAN_FILTER_BLUE ? "Blue " : "",
       dev->color_filters & SCAN_FILTER_NEUTRAL ? "Neutral " : "",
       dev->color_filters);

  DBG (DBG_inquiry, "Color depths..................: %s%s%s%s%s%s (%02x)\n",
       dev->color_depths & SCAN_COLOR_DEPTH_16 ? "16 bit " : "",
       dev->color_depths & SCAN_COLOR_DEPTH_12 ? "12 bit " : "",
       dev->color_depths & SCAN_COLOR_DEPTH_10 ? "10 bit " : "",
       dev->color_depths & SCAN_COLOR_DEPTH_8 ? "8 bit " : "",
       dev->color_depths & SCAN_COLOR_DEPTH_4 ? "4 bit " : "",
       dev->color_depths & SCAN_COLOR_DEPTH_1 ? "1 bit " : "",
       dev->color_depths);

  DBG (DBG_inquiry, "Color Format..................: %s%s%s (%02x)\n",
       dev->color_formats & SCAN_COLOR_FORMAT_INDEX ? "Indexed " : "",
       dev->color_formats & SCAN_COLOR_FORMAT_LINE ? "Line " : "",
       dev->color_formats & SCAN_COLOR_FORMAT_PIXEL ? "Pixel " : "",
       dev->color_formats);

  DBG (DBG_inquiry, "Image Format..................: %s%s%s%s (%02x)\n",
       dev->image_formats & SCAN_IMG_FMT_OKLINE ? "OKLine " : "",
       dev->image_formats & SCAN_IMG_FMT_BLK_ONE ? "BlackOne " : "",
       dev->image_formats & SCAN_IMG_FMT_MOTOROLA ? "Motorola " : "",
       dev->image_formats & SCAN_IMG_FMT_INTEL ? "Intel" : "",
       dev->image_formats);

  DBG (DBG_inquiry,
       "Scan Capability...............: %s%s%s%s%d speeds (%02x)\n",
       dev->scan_capabilities & SCAN_CAP_PWRSAV ? "PowerSave " : "",
       dev->scan_capabilities & SCAN_CAP_EXT_CAL ? "ExtCal " : "",
       dev->scan_capabilities & SCAN_CAP_FAST_PREVIEW ? "FastPreview" :
       "",
       dev->scan_capabilities & SCAN_CAP_DISABLE_CAL ? "DisCal " : "",
       dev->scan_capabilities & SCAN_CAP_SPEEDS,
       dev->scan_capabilities);

  DBG (DBG_inquiry, "Optional Devices..............: %s%s%s%s (%02x)\n",
       dev->optional_devices & SCAN_OPT_DEV_MPCL ? "MultiPageLoad " :
       "",
       dev->optional_devices & SCAN_OPT_DEV_TP1 ? "TransModule1 " : "",
       dev->optional_devices & SCAN_OPT_DEV_TP ? "TransModule " : "",
       dev->optional_devices & SCAN_OPT_DEV_ADF ? "ADF " : "",
       dev->optional_devices);

  DBG (DBG_inquiry, "Enhancement...................: %02x\n",
       dev->enhancements);
  DBG (DBG_inquiry, "Gamma bits....................: %d\n",
       dev->gamma_bits);

  DBG (DBG_inquiry, "Fast Preview Resolution.......: %d\n",
       dev->fast_preview_resolution);
  DBG (DBG_inquiry, "Min Highlight.................: %d\n",
       dev->minimum_highlight);
  DBG (DBG_inquiry, "Max Shadow....................: %d\n",
       dev->maximum_shadow);
  DBG (DBG_inquiry, "Cal Eqn.......................: %d\n",
       dev->calibration_equation);
  DBG (DBG_inquiry, "Min Exposure..................: %d\n",
       dev->minimum_exposure);
  DBG (DBG_inquiry, "Max Exposure..................: %d\n",
       dev->maximum_exposure);

  DBG (DBG_inquiry, "x0,y0 x1,y1...................: %d,%d %d,%d\n",
       dev->x0, dev->y0, dev->x1, dev->y1);
  DBG (DBG_inquiry, "production....................: '%s'\n",
       dev->production);
  DBG (DBG_inquiry, "timestamp.....................: '%s'\n",
       dev->timestamp);
  DBG (DBG_inquiry, "signature.....................: '%s'\n",
       dev->signature);

}

/**
 * Initiaize scanner options from the device definition and from exposure,
 * gain and offset defaults. The function is called by sane_open(), when no
 * optimized settings are available yet. The scanner object is fully
 * initialized in sane_start().
 *
 * @param scanner Scanner to initialize
 * @return SANE_STATUS_GOOD
 */
SANE_Status
sanei_pieusb_init_options (Pieusb_Scanner* scanner)
{
    int i;

    DBG (DBG_info_proc, "sanei_pieusb_init_options\n");

    memset (scanner->opt, 0, sizeof (scanner->opt));
    memset (scanner->val, 0, sizeof (scanner->val));

    for (i = 0; i < NUM_OPTIONS; ++i) {
        scanner->opt[i].size = sizeof (SANE_Word);
        scanner->opt[i].cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT;
    }

    /* Number of options (a pseudo-option) */
    scanner->opt[OPT_NUM_OPTS].name = SANE_NAME_NUM_OPTIONS;
    scanner->opt[OPT_NUM_OPTS].title = SANE_TITLE_NUM_OPTIONS;
    scanner->opt[OPT_NUM_OPTS].desc = SANE_DESC_NUM_OPTIONS;
    scanner->opt[OPT_NUM_OPTS].type = SANE_TYPE_INT;
    scanner->opt[OPT_NUM_OPTS].cap = SANE_CAP_SOFT_DETECT;
    scanner->val[OPT_NUM_OPTS].w = NUM_OPTIONS;

    /* "Mode" group: */
    scanner->opt[OPT_MODE_GROUP].title = "Scan Mode";
    scanner->opt[OPT_MODE_GROUP].desc = "";
    scanner->opt[OPT_MODE_GROUP].type = SANE_TYPE_GROUP;
    scanner->opt[OPT_MODE_GROUP].cap = 0;
    scanner->opt[OPT_MODE_GROUP].constraint_type = SANE_CONSTRAINT_NONE;

    /* scan mode */
    scanner->opt[OPT_MODE].name = SANE_NAME_SCAN_MODE;
    scanner->opt[OPT_MODE].title = SANE_TITLE_SCAN_MODE;
    scanner->opt[OPT_MODE].desc = SANE_DESC_SCAN_MODE;
    scanner->opt[OPT_MODE].type = SANE_TYPE_STRING;
    scanner->opt[OPT_MODE].size = max_string_size ((SANE_String_Const const *) scanner->device->scan_mode_list);
    scanner->opt[OPT_MODE].constraint_type = SANE_CONSTRAINT_STRING_LIST;
    scanner->opt[OPT_MODE].constraint.string_list = (SANE_String_Const const *) scanner->device->scan_mode_list;
    scanner->val[OPT_MODE].s = (SANE_Char *) strdup (scanner->device->scan_mode_list[3]); /* default RGB */

    /* bit depth */
    scanner->opt[OPT_BIT_DEPTH].name = SANE_NAME_BIT_DEPTH;
    scanner->opt[OPT_BIT_DEPTH].title = SANE_TITLE_BIT_DEPTH;
    scanner->opt[OPT_BIT_DEPTH].desc = SANE_DESC_BIT_DEPTH;
    scanner->opt[OPT_BIT_DEPTH].type = SANE_TYPE_INT;
    scanner->opt[OPT_BIT_DEPTH].constraint_type = SANE_CONSTRAINT_WORD_LIST;
    scanner->opt[OPT_BIT_DEPTH].size = sizeof (SANE_Word);
    scanner->opt[OPT_BIT_DEPTH].constraint.word_list = scanner->device->bpp_list;
    scanner->val[OPT_BIT_DEPTH].w = scanner->device->bpp_list[2];

    /* resolution */
    scanner->opt[OPT_RESOLUTION].name = SANE_NAME_SCAN_RESOLUTION;
    scanner->opt[OPT_RESOLUTION].title = SANE_TITLE_SCAN_RESOLUTION;
    scanner->opt[OPT_RESOLUTION].desc = SANE_DESC_SCAN_RESOLUTION;
    scanner->opt[OPT_RESOLUTION].type = SANE_TYPE_FIXED;
    scanner->opt[OPT_RESOLUTION].unit = SANE_UNIT_DPI;
    scanner->opt[OPT_RESOLUTION].constraint_type = SANE_CONSTRAINT_RANGE;
    scanner->opt[OPT_RESOLUTION].constraint.range = &scanner->device->dpi_range;
    scanner->val[OPT_RESOLUTION].w = scanner->device->fast_preview_resolution << SANE_FIXED_SCALE_SHIFT;

    /* halftone pattern */
    scanner->opt[OPT_HALFTONE_PATTERN].name = SANE_NAME_HALFTONE_PATTERN;
    scanner->opt[OPT_HALFTONE_PATTERN].title = SANE_TITLE_HALFTONE_PATTERN;
    scanner->opt[OPT_HALFTONE_PATTERN].desc = SANE_DESC_HALFTONE_PATTERN;
    scanner->opt[OPT_HALFTONE_PATTERN].type = SANE_TYPE_STRING;
    scanner->opt[OPT_HALFTONE_PATTERN].size = max_string_size ((SANE_String_Const const *) scanner->device->halftone_list);
    scanner->opt[OPT_HALFTONE_PATTERN].constraint_type = SANE_CONSTRAINT_STRING_LIST;
    scanner->opt[OPT_HALFTONE_PATTERN].constraint.string_list = (SANE_String_Const const *) scanner->device->halftone_list;
    scanner->val[OPT_HALFTONE_PATTERN].s = (SANE_Char *) strdup (scanner->device->halftone_list[6]);
    scanner->opt[OPT_HALFTONE_PATTERN].cap |= SANE_CAP_INACTIVE; /* Not implemented, and only meaningful at depth 1 */

    /* lineart threshold */
    scanner->opt[OPT_THRESHOLD].name = SANE_NAME_THRESHOLD;
    scanner->opt[OPT_THRESHOLD].title = SANE_TITLE_THRESHOLD;
    scanner->opt[OPT_THRESHOLD].desc = SANE_DESC_THRESHOLD;
    scanner->opt[OPT_THRESHOLD].type = SANE_TYPE_FIXED;
    scanner->opt[OPT_THRESHOLD].unit = SANE_UNIT_PERCENT;
    scanner->opt[OPT_THRESHOLD].constraint_type = SANE_CONSTRAINT_RANGE;
    scanner->opt[OPT_THRESHOLD].constraint.range = &percentage_range_100;
    scanner->val[OPT_THRESHOLD].w = SANE_FIX (50);
    /* scanner->opt[OPT_THRESHOLD].cap |= SANE_CAP_INACTIVE; Not implemented, and only meaningful at depth 1 */

    /* create a sharper scan at the cost of scan time */
    scanner->opt[OPT_SHARPEN].name = "sharpen";
    scanner->opt[OPT_SHARPEN].title = "Sharpen scan";
    scanner->opt[OPT_SHARPEN].desc = "Sharpen scan by taking more time to discharge the CCD.";
    scanner->opt[OPT_SHARPEN].type = SANE_TYPE_BOOL;
    scanner->opt[OPT_SHARPEN].unit = SANE_UNIT_NONE;
    scanner->opt[OPT_SHARPEN].constraint_type = SANE_CONSTRAINT_NONE;
    scanner->val[OPT_SHARPEN].b = SANE_FALSE;
    scanner->opt[OPT_SHARPEN].cap |= SANE_CAP_SOFT_SELECT;

    /* skip the auto-calibration phase before the scan */
    scanner->opt[OPT_SHADING_ANALYSIS].name = "shading-analysis";
    scanner->opt[OPT_SHADING_ANALYSIS].title = "Perform shading analysis";
    scanner->opt[OPT_SHADING_ANALYSIS].desc = "Collect shading reference data before scanning the image. If set to 'no', this option may be overridden by the scanner.";
    scanner->opt[OPT_SHADING_ANALYSIS].type = SANE_TYPE_BOOL;
    scanner->opt[OPT_SHADING_ANALYSIS].unit = SANE_UNIT_NONE;
    scanner->opt[OPT_SHADING_ANALYSIS].constraint_type = SANE_CONSTRAINT_NONE;
    scanner->val[OPT_SHADING_ANALYSIS].b = SANE_FALSE;
    scanner->opt[OPT_SHADING_ANALYSIS].cap |= SANE_CAP_SOFT_SELECT;

    /* use auto-calibration settings for scan */
    scanner->opt[OPT_CALIBRATION_MODE].name = "calibration";
    scanner->opt[OPT_CALIBRATION_MODE].title = "Calibration mode";
    scanner->opt[OPT_CALIBRATION_MODE].desc = "How to calibrate the scanner.";
    scanner->opt[OPT_CALIBRATION_MODE].type = SANE_TYPE_STRING;
    scanner->opt[OPT_CALIBRATION_MODE].size = max_string_size ((SANE_String_Const const *) scanner->device->calibration_mode_list);
    scanner->opt[OPT_CALIBRATION_MODE].constraint_type = SANE_CONSTRAINT_STRING_LIST;
    scanner->opt[OPT_CALIBRATION_MODE].constraint.string_list = (SANE_String_Const const *) scanner->device->calibration_mode_list;
    scanner->val[OPT_CALIBRATION_MODE].s = (SANE_Char *) strdup (scanner->device->calibration_mode_list[1]); /* default auto */

    /* OPT_GAIN_ADJUST */
    scanner->opt[OPT_GAIN_ADJUST].name = "gain-adjust";
    scanner->opt[OPT_GAIN_ADJUST].title = "Adjust gain";
    scanner->opt[OPT_GAIN_ADJUST].desc = "Adjust gain determined by calibration procedure.";
    scanner->opt[OPT_GAIN_ADJUST].type = SANE_TYPE_STRING;
    scanner->opt[OPT_GAIN_ADJUST].size = max_string_size ((SANE_String_Const const *) scanner->device->gain_adjust_list);
    scanner->opt[OPT_GAIN_ADJUST].constraint_type = SANE_CONSTRAINT_STRING_LIST;
    scanner->opt[OPT_GAIN_ADJUST].constraint.string_list = (SANE_String_Const const *) scanner->device->gain_adjust_list;
    scanner->val[OPT_GAIN_ADJUST].s = (SANE_Char *) strdup (scanner->device->gain_adjust_list[2]); /* x 1.0 (no change) */

    /* scan infrared channel faster but less accurate */
    scanner->opt[OPT_FAST_INFRARED].name = "fast-infrared";
    scanner->opt[OPT_FAST_INFRARED].title = "Fast infrared scan";
    scanner->opt[OPT_FAST_INFRARED].desc = "Do not reposition scan head before scanning infrared line. Results in an infrared offset which may deteriorate IR dust and scratch removal.";
    scanner->opt[OPT_FAST_INFRARED].type = SANE_TYPE_BOOL;
    scanner->opt[OPT_FAST_INFRARED].unit = SANE_UNIT_NONE;
    scanner->opt[OPT_FAST_INFRARED].constraint_type = SANE_CONSTRAINT_NONE;
    scanner->val[OPT_FAST_INFRARED].b = SANE_FALSE;
    scanner->opt[OPT_FAST_INFRARED].cap |= SANE_CAP_SOFT_SELECT;

    /* automatically advance to next slide after scan */
    scanner->opt[OPT_ADVANCE_SLIDE].name = "advance";
    scanner->opt[OPT_ADVANCE_SLIDE].title = "Advance slide";
    scanner->opt[OPT_ADVANCE_SLIDE].desc = "Automatically advance to next slide after scan";
    scanner->opt[OPT_ADVANCE_SLIDE].type = SANE_TYPE_BOOL;
    scanner->opt[OPT_ADVANCE_SLIDE].unit = SANE_UNIT_NONE;
    scanner->opt[OPT_ADVANCE_SLIDE].constraint_type = SANE_CONSTRAINT_NONE;
    scanner->val[OPT_ADVANCE_SLIDE].w = SANE_TRUE;
    scanner->opt[OPT_ADVANCE_SLIDE].cap |= SANE_CAP_SOFT_SELECT;

    /* "Geometry" group: */
    scanner->opt[OPT_GEOMETRY_GROUP].title = "Geometry";
    scanner->opt[OPT_GEOMETRY_GROUP].desc = "";
    scanner->opt[OPT_GEOMETRY_GROUP].type = SANE_TYPE_GROUP;
    scanner->opt[OPT_GEOMETRY_GROUP].cap = SANE_CAP_ADVANCED;
    scanner->opt[OPT_GEOMETRY_GROUP].constraint_type = SANE_CONSTRAINT_NONE;

    /* top-left x */
    scanner->opt[OPT_TL_X].name = SANE_NAME_SCAN_TL_X;
    scanner->opt[OPT_TL_X].title = SANE_TITLE_SCAN_TL_X;
    scanner->opt[OPT_TL_X].desc = SANE_DESC_SCAN_TL_X;
    scanner->opt[OPT_TL_X].type = SANE_TYPE_FIXED;
    scanner->opt[OPT_TL_X].unit = SANE_UNIT_MM;
    scanner->opt[OPT_TL_X].constraint_type = SANE_CONSTRAINT_RANGE;
    scanner->opt[OPT_TL_X].constraint.range = &(scanner->device->x_range);
    scanner->val[OPT_TL_X].w = 0;

    /* top-left y */
    scanner->opt[OPT_TL_Y].name = SANE_NAME_SCAN_TL_Y;
    scanner->opt[OPT_TL_Y].title = SANE_TITLE_SCAN_TL_Y;
    scanner->opt[OPT_TL_Y].desc = SANE_DESC_SCAN_TL_Y;
    scanner->opt[OPT_TL_Y].type = SANE_TYPE_FIXED;
    scanner->opt[OPT_TL_Y].unit = SANE_UNIT_MM;
    scanner->opt[OPT_TL_Y].constraint_type = SANE_CONSTRAINT_RANGE;
    scanner->opt[OPT_TL_Y].constraint.range = &(scanner->device->y_range);
    scanner->val[OPT_TL_Y].w = 0;

    /* bottom-right x */
    scanner->opt[OPT_BR_X].name = SANE_NAME_SCAN_BR_X;
    scanner->opt[OPT_BR_X].title = SANE_TITLE_SCAN_BR_X;
    scanner->opt[OPT_BR_X].desc = SANE_DESC_SCAN_BR_X;
    scanner->opt[OPT_BR_X].type = SANE_TYPE_FIXED;
    scanner->opt[OPT_BR_X].unit = SANE_UNIT_MM;
    scanner->opt[OPT_BR_X].constraint_type = SANE_CONSTRAINT_RANGE;
    scanner->opt[OPT_BR_X].constraint.range = &(scanner->device->x_range);
    scanner->val[OPT_BR_X].w = scanner->device->x_range.max;

    /* bottom-right y */
    scanner->opt[OPT_BR_Y].name = SANE_NAME_SCAN_BR_Y;
    scanner->opt[OPT_BR_Y].title = SANE_TITLE_SCAN_BR_Y;
    scanner->opt[OPT_BR_Y].desc = SANE_DESC_SCAN_BR_Y;
    scanner->opt[OPT_BR_Y].type = SANE_TYPE_FIXED;
    scanner->opt[OPT_BR_Y].unit = SANE_UNIT_MM;
    scanner->opt[OPT_BR_Y].constraint_type = SANE_CONSTRAINT_RANGE;
    scanner->opt[OPT_BR_Y].constraint.range = &(scanner->device->y_range);
    scanner->val[OPT_BR_Y].w = scanner->device->y_range.max;

    /* "Enhancement" group: */
    scanner->opt[OPT_ENHANCEMENT_GROUP].title = "Enhancement";
    scanner->opt[OPT_ENHANCEMENT_GROUP].desc = "";
    scanner->opt[OPT_ENHANCEMENT_GROUP].type = SANE_TYPE_GROUP;
    scanner->opt[OPT_ENHANCEMENT_GROUP].cap = 0;
    scanner->opt[OPT_ENHANCEMENT_GROUP].constraint_type = SANE_CONSTRAINT_NONE;

    /* correct data for lamp variations (shading) */
    scanner->opt[OPT_CORRECT_SHADING].name = "correct-shading";
    scanner->opt[OPT_CORRECT_SHADING].title = "Correct shading";
    scanner->opt[OPT_CORRECT_SHADING].desc = "Correct data for lamp variations (shading)";
    scanner->opt[OPT_CORRECT_SHADING].type = SANE_TYPE_BOOL;
    scanner->opt[OPT_CORRECT_SHADING].unit = SANE_UNIT_NONE;
    scanner->val[OPT_CORRECT_SHADING].w = SANE_TRUE;

    /* correct infrared for red crosstalk */
    scanner->opt[OPT_CORRECT_INFRARED].name = "correct-infrared";
    scanner->opt[OPT_CORRECT_INFRARED].title = "Correct infrared";
    scanner->opt[OPT_CORRECT_INFRARED].desc = "Correct infrared for red crosstalk";
    scanner->opt[OPT_CORRECT_INFRARED].type = SANE_TYPE_BOOL;
    scanner->opt[OPT_CORRECT_INFRARED].unit = SANE_UNIT_NONE;
    scanner->val[OPT_CORRECT_INFRARED].w = SANE_FALSE;

    /* detect and remove dust and scratch artifacts */
    scanner->opt[OPT_CLEAN_IMAGE].name = "clean-image";
    scanner->opt[OPT_CLEAN_IMAGE].title = "Clean image";
    scanner->opt[OPT_CLEAN_IMAGE].desc = "Detect and remove dust and scratch artifacts";
    scanner->opt[OPT_CLEAN_IMAGE].type = SANE_TYPE_BOOL;
    scanner->opt[OPT_CLEAN_IMAGE].unit = SANE_UNIT_NONE;
    scanner->val[OPT_CLEAN_IMAGE].w = SANE_FALSE;

    /* strength of grain filtering */
    scanner->opt[OPT_SMOOTH_IMAGE].name = "smooth";
    scanner->opt[OPT_SMOOTH_IMAGE].title = "Attenuate film grain";
    scanner->opt[OPT_SMOOTH_IMAGE].desc = "Amount of smoothening";
    scanner->opt[OPT_SMOOTH_IMAGE].type = SANE_TYPE_INT;
    scanner->opt[OPT_SMOOTH_IMAGE].constraint_type = SANE_CONSTRAINT_WORD_LIST;
    scanner->opt[OPT_SMOOTH_IMAGE].size = sizeof (SANE_Word);
    scanner->opt[OPT_SMOOTH_IMAGE].constraint.word_list = scanner->device->grain_sw_list;
    scanner->val[OPT_SMOOTH_IMAGE].w = scanner->device->grain_sw_list[1];
    if (scanner->opt[OPT_SMOOTH_IMAGE].constraint.word_list[0] < 2) {
        scanner->opt[OPT_SMOOTH_IMAGE].cap |= SANE_CAP_INACTIVE;
    }

    /* gamma correction, to make image sRGB like */
    scanner->opt[OPT_TRANSFORM_TO_SRGB].name = "srgb";
    scanner->opt[OPT_TRANSFORM_TO_SRGB].title = "sRGB colors";
    scanner->opt[OPT_TRANSFORM_TO_SRGB].desc = "Transform image to approximate sRGB color space";
    scanner->opt[OPT_TRANSFORM_TO_SRGB].type = SANE_TYPE_BOOL;
    scanner->opt[OPT_TRANSFORM_TO_SRGB].unit = SANE_UNIT_NONE;
    scanner->val[OPT_TRANSFORM_TO_SRGB].w = SANE_FALSE;
    scanner->opt[OPT_TRANSFORM_TO_SRGB].cap |= SANE_CAP_INACTIVE;

    /* color correction for generic negative film */
    scanner->opt[OPT_INVERT_IMAGE].name = "invert";
    scanner->opt[OPT_INVERT_IMAGE].title = "Invert colors";
    scanner->opt[OPT_INVERT_IMAGE].desc = "Correct for generic negative film";
    scanner->opt[OPT_INVERT_IMAGE].type = SANE_TYPE_BOOL;
    scanner->opt[OPT_INVERT_IMAGE].unit = SANE_UNIT_NONE;
    scanner->val[OPT_INVERT_IMAGE].w = SANE_FALSE;
    scanner->opt[OPT_INVERT_IMAGE].cap |= SANE_CAP_INACTIVE;

    /* crop image */
    scanner->opt[OPT_CROP_IMAGE].name = "crop";
    scanner->opt[OPT_CROP_IMAGE].title = "Cropping";
    scanner->opt[OPT_CROP_IMAGE].desc = "How to crop the image";
    scanner->opt[OPT_CROP_IMAGE].type = SANE_TYPE_STRING;
    scanner->opt[OPT_CROP_IMAGE].size = max_string_size ((SANE_String_Const const *)(void*) scanner->device->crop_sw_list);
    scanner->opt[OPT_CROP_IMAGE].constraint_type = SANE_CONSTRAINT_STRING_LIST;
    scanner->opt[OPT_CROP_IMAGE].constraint.string_list = (SANE_String_Const const *)(void*) scanner->device->crop_sw_list;
    scanner->val[OPT_CROP_IMAGE].s = (SANE_Char *) strdup (scanner->device->crop_sw_list[2]);

    /* "Advanced" group: */
    scanner->opt[OPT_ADVANCED_GROUP].title = "Advanced";
    scanner->opt[OPT_ADVANCED_GROUP].desc = "";
    scanner->opt[OPT_ADVANCED_GROUP].type = SANE_TYPE_GROUP;
    scanner->opt[OPT_ADVANCED_GROUP].cap = SANE_CAP_ADVANCED;
    scanner->opt[OPT_ADVANCED_GROUP].constraint_type = SANE_CONSTRAINT_NONE;

    /* preview */
    scanner->opt[OPT_PREVIEW].name = SANE_NAME_PREVIEW;
    scanner->opt[OPT_PREVIEW].title = SANE_TITLE_PREVIEW;
    scanner->opt[OPT_PREVIEW].desc = SANE_DESC_PREVIEW;
    scanner->opt[OPT_PREVIEW].type = SANE_TYPE_BOOL;
    scanner->val[OPT_PREVIEW].w = SANE_FALSE;

    /* save shading data */
    scanner->opt[OPT_SAVE_SHADINGDATA].name = "save-shading-data";
    scanner->opt[OPT_SAVE_SHADINGDATA].title = "Save shading data";
    scanner->opt[OPT_SAVE_SHADINGDATA].desc = "Save shading data in 'pieusb.shading'";
    scanner->opt[OPT_SAVE_SHADINGDATA].type = SANE_TYPE_BOOL;
    scanner->val[OPT_SAVE_SHADINGDATA].w = SANE_FALSE;

    /* save CCD mask */
    scanner->opt[OPT_SAVE_CCDMASK].name = "save-ccdmask";
    scanner->opt[OPT_SAVE_CCDMASK].title = "Save CCD mask";
    scanner->opt[OPT_SAVE_CCDMASK].desc = "Save CCD mask 'pieusb.ccd'";
    scanner->opt[OPT_SAVE_CCDMASK].type = SANE_TYPE_BOOL;
    scanner->val[OPT_SAVE_CCDMASK].w = SANE_FALSE;

    scanner->opt[OPT_LIGHT].name = "light";
    scanner->opt[OPT_LIGHT].title = "Light";
    scanner->opt[OPT_LIGHT].desc = "Light";
    scanner->opt[OPT_LIGHT].type = SANE_TYPE_INT;
    scanner->opt[OPT_LIGHT].unit = SANE_UNIT_MICROSECOND;
    scanner->opt[OPT_LIGHT].cap |= SANE_CAP_SOFT_SELECT;
    scanner->opt[OPT_LIGHT].size = sizeof(SANE_Word);
    scanner->val[OPT_LIGHT].w = DEFAULT_LIGHT;

    scanner->opt[OPT_DOUBLE_TIMES].name = "double-times";
    scanner->opt[OPT_DOUBLE_TIMES].title = "Double times";
    scanner->opt[OPT_DOUBLE_TIMES].desc = "Double times";
    scanner->opt[OPT_DOUBLE_TIMES].type = SANE_TYPE_INT;
    scanner->opt[OPT_DOUBLE_TIMES].unit = SANE_UNIT_MICROSECOND;
    scanner->opt[OPT_DOUBLE_TIMES].cap |= SANE_CAP_SOFT_SELECT;
    scanner->opt[OPT_DOUBLE_TIMES].size = sizeof(SANE_Word);
    scanner->val[OPT_DOUBLE_TIMES].w = DEFAULT_DOUBLE_TIMES;

    /* exposure times for R, G, B and I */
    scanner->opt[OPT_SET_EXPOSURE_R].name = SANE_NAME_EXPOSURE_R;
    scanner->opt[OPT_SET_EXPOSURE_R].title = SANE_TITLE_EXPOSURE_R;
    scanner->opt[OPT_SET_EXPOSURE_R].desc = SANE_DESC_EXPOSURE_R;
    scanner->opt[OPT_SET_EXPOSURE_G].name = SANE_NAME_EXPOSURE_G;
    scanner->opt[OPT_SET_EXPOSURE_G].title = SANE_TITLE_EXPOSURE_G;
    scanner->opt[OPT_SET_EXPOSURE_G].desc = SANE_DESC_EXPOSURE_G;
    scanner->opt[OPT_SET_EXPOSURE_B].name = SANE_NAME_EXPOSURE_B;
    scanner->opt[OPT_SET_EXPOSURE_B].title = SANE_TITLE_EXPOSURE_B;
    scanner->opt[OPT_SET_EXPOSURE_B].desc = SANE_DESC_EXPOSURE_B;
    scanner->opt[OPT_SET_EXPOSURE_I].name = SANE_NAME_EXPOSURE_I;
    scanner->opt[OPT_SET_EXPOSURE_I].title = SANE_TITLE_EXPOSURE_I;
    scanner->opt[OPT_SET_EXPOSURE_I].desc = SANE_DESC_EXPOSURE_I;
    for (i = OPT_SET_EXPOSURE_R; i <= OPT_SET_EXPOSURE_I; ++i) {
    scanner->opt[i].type = SANE_TYPE_INT;
    scanner->opt[i].unit = SANE_UNIT_MICROSECOND;
    scanner->opt[i].cap |= SANE_CAP_SOFT_SELECT;
    scanner->opt[i].constraint_type = SANE_CONSTRAINT_RANGE;
    scanner->opt[i].constraint.range = &(scanner->device->exposure_range);
    scanner->opt[i].size = sizeof(SANE_Word);
    scanner->val[i].w = SANE_EXPOSURE_DEFAULT;
    }

    /* gain for R, G, B and I */
    scanner->opt[OPT_SET_GAIN_R].name = SANE_NAME_GAIN_R;
    scanner->opt[OPT_SET_GAIN_R].title = SANE_TITLE_GAIN_R;
    scanner->opt[OPT_SET_GAIN_R].desc = SANE_DESC_GAIN_R;
    scanner->opt[OPT_SET_GAIN_G].name = SANE_NAME_GAIN_G;
    scanner->opt[OPT_SET_GAIN_G].title = SANE_TITLE_GAIN_G;
    scanner->opt[OPT_SET_GAIN_G].desc = SANE_DESC_GAIN_G;
    scanner->opt[OPT_SET_GAIN_B].name = SANE_NAME_GAIN_B;
    scanner->opt[OPT_SET_GAIN_B].title = SANE_TITLE_GAIN_B;
    scanner->opt[OPT_SET_GAIN_B].desc = SANE_DESC_GAIN_B;
    scanner->opt[OPT_SET_GAIN_I].name = SANE_NAME_GAIN_I;
    scanner->opt[OPT_SET_GAIN_I].title = SANE_TITLE_GAIN_I;
    scanner->opt[OPT_SET_GAIN_I].desc = SANE_DESC_GAIN_I;
    for (i = OPT_SET_GAIN_R; i <= OPT_SET_GAIN_I; ++i) {
      scanner->opt[i].type = SANE_TYPE_INT;
      scanner->opt[i].unit = SANE_UNIT_NONE;
      scanner->opt[i].constraint_type = SANE_CONSTRAINT_RANGE;
      scanner->opt[i].constraint.range = &gain_range;
      scanner->opt[i].size = sizeof(SANE_Word);
      scanner->val[i].w = SANE_GAIN_DEFAULT;
    }
    /* offsets for R, G, B and I */
    scanner->opt[OPT_SET_OFFSET_R].name = SANE_NAME_OFFSET_R;
    scanner->opt[OPT_SET_OFFSET_R].title = SANE_TITLE_OFFSET_R;
    scanner->opt[OPT_SET_OFFSET_R].desc = SANE_DESC_OFFSET_R;
    scanner->opt[OPT_SET_OFFSET_G].name = SANE_NAME_OFFSET_G;
    scanner->opt[OPT_SET_OFFSET_G].title = SANE_TITLE_OFFSET_G;
    scanner->opt[OPT_SET_OFFSET_G].desc = SANE_DESC_OFFSET_G;
    scanner->opt[OPT_SET_OFFSET_B].name = SANE_NAME_OFFSET_B;
    scanner->opt[OPT_SET_OFFSET_B].title = SANE_TITLE_OFFSET_B;
    scanner->opt[OPT_SET_OFFSET_B].desc = SANE_DESC_OFFSET_B;
    scanner->opt[OPT_SET_OFFSET_I].name = SANE_NAME_OFFSET_I;
    scanner->opt[OPT_SET_OFFSET_I].title = SANE_TITLE_OFFSET_I;
    scanner->opt[OPT_SET_OFFSET_I].desc = SANE_DESC_OFFSET_I;
    for (i = OPT_SET_OFFSET_R; i <= OPT_SET_OFFSET_I; ++i) {
      scanner->opt[i].type = SANE_TYPE_INT;
      scanner->opt[i].unit = SANE_UNIT_NONE;
      scanner->opt[i].constraint_type = SANE_CONSTRAINT_RANGE;
      scanner->opt[i].constraint.range = &offset_range;
      scanner->opt[i].size = sizeof(SANE_Word);
      scanner->val[i].w = SANE_OFFSET_DEFAULT;
    }
    return SANE_STATUS_GOOD;
}

/**
 * Parse line from config file into a vendor id, product id, model number, and flags
 *
 * @param config_line Text to parse
 * @param vendor_id
 * @param product_id
 * @param model_number
 * @param flags
 * @return SANE_STATUS_GOOD, or SANE_STATUS_INVAL in case of a parse error
 */
SANE_Status
sanei_pieusb_parse_config_line(const char* config_line,
                               SANE_Word* vendor_id,
                               SANE_Word* product_id,
                               SANE_Int* model_number,
                               SANE_Int* flags)
{
    char *vendor_id_string, *product_id_string, *model_number_string, *flags_string;

    if (strncmp (config_line, "usb ", 4) != 0) {
        return SANE_STATUS_INVAL;
    }
    /* Detect vendor-id */
    config_line += 4;
    config_line = sanei_config_skip_whitespace (config_line);
    if (*config_line) {
        config_line = sanei_config_get_string (config_line, &vendor_id_string);
        if (vendor_id_string) {
            *vendor_id = strtol (vendor_id_string, 0, 0);
            free (vendor_id_string);
        } else {
            return SANE_STATUS_INVAL;
        }
        config_line = sanei_config_skip_whitespace (config_line);
    } else {
        return SANE_STATUS_INVAL;
    }
    /* Detect product-id */
    config_line = sanei_config_skip_whitespace (config_line);
    if (*config_line) {
        config_line = sanei_config_get_string (config_line, &product_id_string);
        if (product_id_string) {
            *product_id = strtol (product_id_string, 0, 0);
            free (product_id_string);
        } else {
            return SANE_STATUS_INVAL;
        }
        config_line = sanei_config_skip_whitespace (config_line);
    } else {
        return SANE_STATUS_INVAL;
    }
    /* Detect model number */
    config_line = sanei_config_skip_whitespace (config_line);
    if (*config_line) {
        config_line = sanei_config_get_string (config_line, &model_number_string);
        if (model_number_string) {
            *model_number = (SANE_Int) strtol (model_number_string, 0, 0);
            free (model_number_string);
        } else {
            return SANE_STATUS_INVAL;
        }
        config_line = sanei_config_skip_whitespace (config_line);
    } else {
        return SANE_STATUS_INVAL;
    }
    /* Detect (optional) flags */
    *flags = 0;
    config_line = sanei_config_skip_whitespace (config_line);
    if (*config_line) {
        config_line = sanei_config_get_string (config_line, &flags_string);
        if (flags_string) {
            *flags = (SANE_Int) strtol (flags_string, 0, 0);
            free (flags_string);
        }
    }
    return SANE_STATUS_GOOD;
}

/**
 * Check if current list of supported devices contains the given specifications.
 *
 * @param vendor_id
 * @param product_id
 * @param model_number
 * @param flags
 * @return
 */
SANE_Bool
sanei_pieusb_supported_device_list_contains(SANE_Word vendor_id, SANE_Word product_id, SANE_Int model_number, SANE_Int flags)
{
    int i = 0;
    while (pieusb_supported_usb_device_list[i].vendor != 0) {
        if (pieusb_supported_usb_device_list[i].vendor == vendor_id
              && pieusb_supported_usb_device_list[i].product == product_id
              && pieusb_supported_usb_device_list[i].model == model_number
              && pieusb_supported_usb_device_list[i].flags == flags) {
            return SANE_TRUE;
        }
        i++;
    }
    return SANE_FALSE;
}

/**
 * Add the given specifications to the current list of supported devices
 * @param vendor_id
 * @param product_id
 * @param model_number
 * @param flags
 * @return
 */
SANE_Status
sanei_pieusb_supported_device_list_add(SANE_Word vendor_id, SANE_Word product_id, SANE_Int model_number, SANE_Int flags)
{
    int i = 0, k;
    struct Pieusb_USB_Device_Entry* dl;

    while (pieusb_supported_usb_device_list[i].vendor != 0) {
        i++;
    }
    /* i is index of last entry */
    for (k=0; k<=i; k++) {
        DBG(DBG_info_proc,"sanei_pieusb_supported_device_list_add(): current %03d: %04x %04x %02x %02x\n", i,
            pieusb_supported_usb_device_list[k].vendor,
            pieusb_supported_usb_device_list[k].product,
            pieusb_supported_usb_device_list[k].model,
            pieusb_supported_usb_device_list[k].flags);
    }

    dl = realloc(pieusb_supported_usb_device_list,(i+2)*sizeof(struct Pieusb_USB_Device_Entry)); /* Add one entry to list */
    if (dl == NULL) {
        return SANE_STATUS_INVAL;
    }
    /* Copy values */
    pieusb_supported_usb_device_list = dl;
    pieusb_supported_usb_device_list[i].vendor = vendor_id;
    pieusb_supported_usb_device_list[i].product = product_id;
    pieusb_supported_usb_device_list[i].model = model_number;
    pieusb_supported_usb_device_list[i].flags = flags;
    pieusb_supported_usb_device_list[i+1].vendor = 0;
    pieusb_supported_usb_device_list[i+1].product = 0;
    pieusb_supported_usb_device_list[i+1].model = 0;
    pieusb_supported_usb_device_list[i+1].flags = 0;
    for (k=0; k<=i+1; k++) {
        DBG(DBG_info_proc,"sanei_pieusb_supported_device_list_add() add: %03d: %04x %04x %02x %02x\n", i,
            pieusb_supported_usb_device_list[k].vendor,
            pieusb_supported_usb_device_list[k].product,
            pieusb_supported_usb_device_list[k].model,
            pieusb_supported_usb_device_list[k].flags);
    }
    return SANE_STATUS_GOOD;
}

/**
 * Actions to perform when a cancel request has been received.
 *
 * @param scanner scanner to stop scanning
 * @return SANE_STATUS_CANCELLED
 */
SANE_Status
sanei_pieusb_on_cancel (Pieusb_Scanner * scanner)
{
    struct Pieusb_Command_Status status;

    DBG (DBG_info_proc, "sanei_pieusb_on_cancel()\n");

    sanei_pieusb_cmd_stop_scan (scanner->device_number, &status);
    sanei_pieusb_cmd_set_scan_head (scanner->device_number, 1, 0, &status);
    sanei_pieusb_buffer_delete (&scanner->buffer);
    scanner->scanning = SANE_FALSE;
    return SANE_STATUS_CANCELLED;
}

/**
 * Determine maximum length of a set of strings.
 *
 * @param strings Set of strings
 * @return maximum length
 */
static size_t
max_string_size (SANE_String_Const const strings[])
{
    size_t size, max_size = 0;
    int i;

    for (i = 0; strings[i]; ++i) {
        size = strlen (strings[i]) + 1;
        if (size > max_size) {
            max_size = size;
        }
    }

    return max_size;
}

/* From MR's pie.c */

/* ------------------------- PIEUSB_CORRECT_SHADING -------------------------- */

/**
 * Correct the given buffer for shading using shading data in scanner.
 * If the loop order is width->color->height, a 7200 dpi scan correction takes
 * 45 minutes. If the loop order is color->height->width, this is less than 3
 * minutes. So it is worthwhile to find the used pixels first (array width_to_loc).
 *
 * @param scanner Scanner
 * @param buffer Buffer to correct
 */
void
sanei_pieusb_correct_shading(struct Pieusb_Scanner *scanner, struct Pieusb_Read_Buffer *buffer)
{

    int i, j, c, k;
    SANE_Uint val, val_org, *p;
    int *width_to_loc;

    DBG (DBG_info_proc, "sanei_pieusb_correct_shading()\n");

    /* Loop through CCD-mask to find used pixels */
    width_to_loc = calloc(buffer->width,sizeof(int));
    j = 0;
    for (i = 0; i < scanner->ccd_mask_size; i++) {
        if (scanner->ccd_mask[i] == 0) {
            width_to_loc[j++] = i;
        }
    }
    /* Correct complete image */
    for (c = 0; c < buffer->colors; c++) {
        DBG(DBG_info,"sanei_pieusb_correct_shading() correct color %d\n",c);
        for (k = 0; k < buffer->height; k++) {
            /* DBG(DBG_info,"Correct line %d\n",k); */
            p = buffer->data + c * buffer->width * buffer->height + k * buffer->width;
            for (j = 0; j < buffer->width; j++) {
                val_org = *p;
                val = lround((double)scanner->shading_mean[c] / scanner->shading_ref[c][width_to_loc[j]] * val_org);
                /* DBG(DBG_info,"Correct [%d,%d,%d] %d -> %d\n",k,j,c,val_org,val); */
                *p++ = val;
            }
        }
    }
    /* Free memory */
    free(width_to_loc);
}

/* === functions copied from MR's code === */

/**
 *
 * @param scanner
 * @param in_img
 * @param planes
 * @param out_planes
 * @return
 */
SANE_Status
sanei_pieusb_post (Pieusb_Scanner *scanner, uint16_t **in_img, int planes)
{
  uint16_t *cplane[PLANES];    /* R, G, B, I gray scale planes */
  SANE_Parameters parameters;   /* describes the image */
  int winsize_smooth;           /* for adapting replaced pixels */
  char filename[64];
  SANE_Status status;
  int smooth, i;

  memcpy (&parameters, &scanner->scan_parameters, sizeof (SANE_Parameters));
  parameters.format = SANE_FRAME_GRAY;
  parameters.bytes_per_line = parameters.pixels_per_line;
  if (parameters.depth > 8)
    parameters.bytes_per_line *= 2;
  parameters.last_frame = 0;

  DBG (DBG_info, "pie_usb_post: %d ppl, %d lines, %d bits, %d planes, %d dpi\n",
       parameters.pixels_per_line, parameters.lines,
       parameters.depth, planes, scanner->mode.resolution);

  if (planes > PLANES) {
    DBG (DBG_error, "pie_usb_post: too many planes: %d (max %d)\n", planes, PLANES);
    return SANE_STATUS_INVAL;
  }

  for (i = 0; i < planes; i++)
    cplane[i] = in_img[i];

  /* dirt is rather resolution invariant, so
   * setup resolution dependent parameters
   */
  /* film grain reduction */
  smooth = scanner->val[OPT_SMOOTH_IMAGE].w;
  winsize_smooth = (scanner->mode.resolution / 540) | 1;
  /* smoothen whole image or only replaced pixels */
  if (smooth)
    {
      winsize_smooth += 2 * (smooth - 3);       /* even */
      if (winsize_smooth < 3)
        smooth = 0;
    }
  if (winsize_smooth < 3)
    winsize_smooth = 3;
  DBG (DBG_info, "pie_usb_sw_post: winsize_smooth %d\n", winsize_smooth);

  /* RGBI post-processing if selected:
   * 1) remove spectral overlay from ired plane,
   * 2) remove dirt, smoothen if, crop if */
  if (scanner->val[OPT_CORRECT_INFRARED].b) /* (scanner->processing & POST_SW_IRED_MASK) */
    {
      /* remove spectral overlay from ired plane */
      status = sanei_ir_spectral_clean (&parameters, scanner->ln_lut, cplane[0], cplane[3]);
      if (status != SANE_STATUS_GOOD)
        return status;
      if (DBG_LEVEL >= 15)
        {
          snprintf (filename, 63, "/tmp/ir-spectral.pnm");
          pieusb_write_pnm_file (filename, cplane[3],
                                  parameters.depth, 1,
                                  parameters.pixels_per_line, parameters.lines);
        }
      if (scanner->cancel_request)          /* asynchronous cancel ? */
        return SANE_STATUS_CANCELLED;
  } /* scanner-> processing & POST_SW_IRED_MASK */

  /* remove dirt, smoothen if, crop if */
  if (scanner->val[OPT_CLEAN_IMAGE].b) /* (scanner->processing & POST_SW_DIRT) */
    {
      double *norm_histo;
      uint16_t *thresh_data;
      int static_thresh, too_thresh;    /* static thresholds */
      int winsize_filter;               /* primary size of filtering window */
      int size_dilate;                  /* the dirt mask */

      /* size of filter detecting dirt */
      winsize_filter = (int) (5.0 * (double) scanner->mode.resolution / 300.0) | 1;
      if (winsize_filter < 3)
        winsize_filter = 3;
      /* dirt usually has smooth edges which also need correction */
      size_dilate = scanner->mode.resolution / 1000 + 1;

      /* first detect large dirt by a static threshold */
      status = sanei_ir_create_norm_histogram (&parameters, cplane[3], &norm_histo);
      if (status != SANE_STATUS_GOOD)
        {
          DBG (DBG_error, "pie_usb_sw_post: no buffer\n");
          return SANE_STATUS_NO_MEM;
        }
      /* generate a "bimodal" static threshold */
      status = sanei_ir_threshold_yen (&parameters, norm_histo, &static_thresh);
      if (status != SANE_STATUS_GOOD)
        return status;
      /* generate traditional static threshold */
      status = sanei_ir_threshold_otsu (&parameters, norm_histo, &too_thresh);
      if (status != SANE_STATUS_GOOD)
        return status;
      /* choose lower one */
      if (too_thresh < static_thresh)
        static_thresh = too_thresh;
      free (norm_histo);

      /* then generate dirt mask with adaptive thresholding filter
       * and add the dirt from the static threshold */
      /* last two parameters: 10, 50 detects more, 20, 75 less */
      status = sanei_ir_filter_madmean (&parameters, cplane[3], &thresh_data, winsize_filter, 20, 100);
      if (status != SANE_STATUS_GOOD) {
        free (thresh_data);
        return status;
      }
      sanei_ir_add_threshold (&parameters, cplane[3], thresh_data, static_thresh);
      if (DBG_LEVEL >= 15)
        {
          snprintf (filename, 63, "/tmp/ir-threshold.pnm");
          pieusb_write_pnm_file (filename, thresh_data,
                                  8, 1, parameters.pixels_per_line,
                                  parameters.lines);
        }
      if (scanner->cancel_request) {         /* asynchronous cancel ? */
        free (thresh_data);
        return SANE_STATUS_CANCELLED;
      }
      /* replace the dirt and smoothen film grain and crop if possible */
      status = sanei_ir_dilate_mean (&parameters, cplane, thresh_data,
              500, size_dilate, winsize_smooth, smooth,
              0, NULL);
      if (status != SANE_STATUS_GOOD) {
        free (thresh_data);
        return status;
      }
      smooth = 0;
      free (thresh_data);
    }

  if (DBG_LEVEL >= 15)
    {
      pieusb_write_pnm_file ("/tmp/RGBi-img.pnm", scanner->buffer.data,
        scanner->scan_parameters.depth, 3, scanner->scan_parameters.pixels_per_line,
        scanner->scan_parameters.lines);
    }

  return status;
}

/* ------------------------------ PIE_USB_WRITE_PNM_FILE ------------------------------- */
static SANE_Status
pieusb_write_pnm_file (char *filename, SANE_Uint *data, int depth,
                        int channels, int pixels_per_line, int lines)
{
  FILE *out;
  int r, c, ch;
  SANE_Uint val;
  uint8_t b = 0;

  DBG (DBG_info_proc,
       "pie_usb_write_pnm_file: depth=%d, channels=%d, ppl=%d, lines=%d\n",
       depth, channels, pixels_per_line, lines);

  out = fopen (filename, "w");
  if (!out)
    {
      DBG (DBG_error,
           "pie_usb_write_pnm_file: could not open %s for writing: %s\n",
           filename, strerror (errno));
      return SANE_STATUS_INVAL;
    }

  switch (depth) {
      case 1:
          fprintf (out, "P4\n%d\n%d\n", pixels_per_line, lines);
          for (r = 0; r < lines; r++) {
              int i;
              i = 0;
              b = 0;
              for (c = 0; c < pixels_per_line; c++) {
                  val = *(data + r * pixels_per_line + c);
                  if (val > 0) b |= (0x80 >> i);
                  i++;
                  if (i == 7) {
                      fputc(b, out);
                      i = 0;
                      b = 0;
                  }
              }
              if (i != 0) {
                  fputc(b, out);
              }
          }
          break;
      case 8:
          fprintf (out, "P%c\n%d\n%d\n%d\n", channels == 1 ? '5' : '6', pixels_per_line, lines, 255);
          for (r = 0; r < lines; r++) {
              for (c = 0; c < pixels_per_line; c++) {
                  for (ch = 0; ch < channels; ch++) {
                      val = *(data + ch * lines * pixels_per_line + r * pixels_per_line + c);
                      b = val & 0xFF;
                      fputc(b, out);
                  }
              }
          }
          break;
      case 16:
          fprintf (out, "P%c\n%d\n%d\n%d\n", channels == 1 ? '5' : '6', pixels_per_line, lines, 65535);
          for (r = 0; r < lines; r++) {
              for (c = 0; c < pixels_per_line; c++) {
                  for (ch = 0; ch < channels; ch++) {
                      val = *(data + ch * lines * pixels_per_line + r * pixels_per_line + c);
                      b = (val >> 8) & 0xFF;
                      fputc(b, out);
                      b = val & 0xFF;
                      fputc(b, out);
                  }
              }
          }
          break;
      default:
          DBG (DBG_error, "pie_usb_write_pnm_file: depth %d not implemented\n", depth);
  }
  fclose (out);

  DBG (DBG_info, "pie_usb_write_pnm_file: finished\n");
  return SANE_STATUS_GOOD;
}

/**
 * Check option inconsistencies.
 * In most cases an inconsistency can be solved by ignoring an option setting.
 * Message these situations and return 1 to indicate we can work with the
 * current set op options. If the settings are really inconsistent, return 0.
 */
int
sanei_pieusb_analyse_options(struct Pieusb_Scanner *scanner)
{
    /* Checks*/
    if (scanner->val[OPT_TL_X].w > scanner->val[OPT_BR_X].w) {
        DBG (DBG_error, "sane_start: %s (%.1f mm) is bigger than %s (%.1f mm) -- aborting\n",
	   scanner->opt[OPT_TL_X].title,
	   SANE_UNFIX (scanner->val[OPT_TL_X].w),
	   scanner->opt[OPT_BR_X].title,
	   SANE_UNFIX (scanner->val[OPT_BR_X].w));
        return 0;
    }
    if (scanner->val[OPT_TL_Y].w > scanner->val[OPT_BR_Y].w) {
        DBG (DBG_error, "sane_start: %s (%.1f mm) is bigger than %s (%.1f mm) -- aborting\n",
	   scanner->opt[OPT_TL_Y].title,
	   SANE_UNFIX (scanner->val[OPT_TL_Y].w),
	   scanner->opt[OPT_BR_Y].title,
	   SANE_UNFIX (scanner->val[OPT_BR_Y].w));
        return 0;
    }
    /* Modes sometimes limit other choices */
    if (scanner->val[OPT_PREVIEW].b) {
        /* Preview uses its own specific settings */
        if (scanner->val[OPT_RESOLUTION].w != (scanner->device->fast_preview_resolution << SANE_FIXED_SCALE_SHIFT)) {
            DBG (DBG_info_sane, "Option %s = %f ignored during preview\n", scanner->opt[OPT_RESOLUTION].name, SANE_UNFIX(scanner->val[OPT_RESOLUTION].w));
        }
        if (scanner->val[OPT_SHARPEN].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored during preview\n", scanner->opt[OPT_SHARPEN].name, scanner->val[OPT_SHARPEN].b);
        }
        if (!scanner->val[OPT_FAST_INFRARED].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored during preview\n", scanner->opt[OPT_FAST_INFRARED].name, scanner->val[OPT_FAST_INFRARED].b);
        }
        if (scanner->val[OPT_CORRECT_INFRARED].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored during preview\n", scanner->opt[OPT_CORRECT_INFRARED].name, scanner->val[OPT_CORRECT_INFRARED].b);
        }
        if (scanner->val[OPT_CLEAN_IMAGE].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored during preview\n", scanner->opt[OPT_CLEAN_IMAGE].name, scanner->val[OPT_CLEAN_IMAGE].b);
        }
        if (scanner->val[OPT_SMOOTH_IMAGE].w != 0) {
            DBG (DBG_info_sane, "Option %s = %d ignored during preview\n", scanner->opt[OPT_SMOOTH_IMAGE].name, scanner->val[OPT_SMOOTH_IMAGE].w);
        }
        if (strcmp(scanner->val[OPT_CROP_IMAGE].s, scanner->device->crop_sw_list[0]) != 0) {
            DBG (DBG_info_sane, "Option %s = %s ignored during preview\n", scanner->opt[OPT_CROP_IMAGE].name, scanner->val[OPT_CROP_IMAGE].s);
        }
        if (scanner->val[OPT_TRANSFORM_TO_SRGB].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored during preview\n", scanner->opt[OPT_TRANSFORM_TO_SRGB].name, scanner->val[OPT_TRANSFORM_TO_SRGB].w);
        }
        if (scanner->val[OPT_INVERT_IMAGE].w) {
            DBG (DBG_info_sane, "Option %s = %d ignored during preview\n", scanner->opt[OPT_INVERT_IMAGE].name, scanner->val[OPT_INVERT_IMAGE].w);
        }
    } else if (strcmp(scanner->val[OPT_MODE].s,SANE_VALUE_SCAN_MODE_LINEART)==0) {
        /* Can we do any post processing in lineart? Needs testing to see what's possible */
        if (scanner->val[OPT_BIT_DEPTH].w != 1) {
            DBG (DBG_info_sane, "Option %s = %d ignored in lineart mode (will use 1)\n", scanner->opt[OPT_BIT_DEPTH].name, scanner->val[OPT_BIT_DEPTH].w);
        }
        if (!scanner->val[OPT_FAST_INFRARED].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored in lineart mode (irrelevant)\n", scanner->opt[OPT_FAST_INFRARED].name, scanner->val[OPT_FAST_INFRARED].b);
        }
        if (!scanner->val[OPT_CORRECT_SHADING].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored in lineart mode (irrelevant)\n", scanner->opt[OPT_CORRECT_SHADING].name, scanner->val[OPT_CORRECT_SHADING].b);
        }
        if (!scanner->val[OPT_CORRECT_INFRARED].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored in lineart mode (irrelevant)\n", scanner->opt[OPT_CORRECT_INFRARED].name, scanner->val[OPT_CORRECT_INFRARED].b);
        }
        if (scanner->val[OPT_CLEAN_IMAGE].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored in lineart mode (irrelevant)\n", scanner->opt[OPT_CLEAN_IMAGE].name, scanner->val[OPT_CLEAN_IMAGE].b);
        }
        if (scanner->val[OPT_SMOOTH_IMAGE].w != 0) {
            DBG (DBG_info_sane, "Option %s = %d ignored in lineart mode (irrelevant)\n", scanner->opt[OPT_SMOOTH_IMAGE].name, scanner->val[OPT_SMOOTH_IMAGE].w);
        }
        if (strcmp(scanner->val[OPT_CROP_IMAGE].s, scanner->device->crop_sw_list[0]) != 0) {
            DBG (DBG_info_sane, "Option %s = %s ignored in lineart mode (irrelevant)\n", scanner->opt[OPT_CROP_IMAGE].name, scanner->val[OPT_CROP_IMAGE].s);
        }
        if (scanner->val[OPT_TRANSFORM_TO_SRGB].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored in lineart mode (irrelevant)\n", scanner->opt[OPT_TRANSFORM_TO_SRGB].name, scanner->val[OPT_TRANSFORM_TO_SRGB].w);
        }
    } else if (strcmp(scanner->val[OPT_MODE].s,SANE_VALUE_SCAN_MODE_HALFTONE)==0) {
        /* Can we do any post processing in halftone? Needs testing to see what's possible */
        if (scanner->val[OPT_BIT_DEPTH].w != 1) {
            DBG (DBG_info_sane, "Option %s = %d ignored in halftone mode (will use 1)\n", scanner->opt[OPT_BIT_DEPTH].name, scanner->val[OPT_BIT_DEPTH].w);
        }
        if (!scanner->val[OPT_FAST_INFRARED].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored in halftone mode (irrelevant)\n", scanner->opt[OPT_FAST_INFRARED].name, scanner->val[OPT_FAST_INFRARED].b);
        }
        if (!scanner->val[OPT_CORRECT_SHADING].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored in halftone mode (irrelevant)\n", scanner->opt[OPT_CORRECT_SHADING].name, scanner->val[OPT_CORRECT_SHADING].b);
        }
        if (!scanner->val[OPT_CORRECT_INFRARED].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored in halftone mode (irrelevant)\n", scanner->opt[OPT_CORRECT_INFRARED].name, scanner->val[OPT_CORRECT_INFRARED].b);
        }
        if (scanner->val[OPT_CLEAN_IMAGE].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored in halftone mode (irrelevant)\n", scanner->opt[OPT_CLEAN_IMAGE].name, scanner->val[OPT_CLEAN_IMAGE].b);
        }
        if (scanner->val[OPT_SMOOTH_IMAGE].w != 0) {
            DBG (DBG_info_sane, "Option %s = %d ignored in halftone mode (irrelevant)\n", scanner->opt[OPT_SMOOTH_IMAGE].name, scanner->val[OPT_SMOOTH_IMAGE].w);
        }
        if (strcmp(scanner->val[OPT_CROP_IMAGE].s, scanner->device->crop_sw_list[0]) != 0) {
            DBG (DBG_info_sane, "Option %s = %s ignored in halftone mode (irrelevant)\n", scanner->opt[OPT_CROP_IMAGE].name, scanner->val[OPT_CROP_IMAGE].s);
        }
        if (scanner->val[OPT_TRANSFORM_TO_SRGB].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored in halftone mode (irrelevant)\n", scanner->opt[OPT_TRANSFORM_TO_SRGB].name, scanner->val[OPT_TRANSFORM_TO_SRGB].w);
        }
    } else if (strcmp(scanner->val[OPT_MODE].s,SANE_VALUE_SCAN_MODE_GRAY)==0) {
        /* Can we do any post processing in gray mode? */
        /* Can we obtain a single color channel in this mode? How? */
        /* Is this just RGB with luminance trasformation? */
        /* Needs testing to see what's possible */
        /* Only do 8 or 16 bit scans */
        if (scanner->val[OPT_BIT_DEPTH].w == 1) {
            DBG (DBG_info_sane, "Option %s = %d ignored in gray mode (will use 8)\n", scanner->opt[OPT_BIT_DEPTH].name, scanner->val[OPT_BIT_DEPTH].w);
        }
        if (!scanner->val[OPT_FAST_INFRARED].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored in gray mode (irrelevant)\n", scanner->opt[OPT_FAST_INFRARED].name, scanner->val[OPT_FAST_INFRARED].b);
        }
        if (!scanner->val[OPT_CORRECT_INFRARED].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored in gray mode (irrelevant)\n", scanner->opt[OPT_CORRECT_INFRARED].name, scanner->val[OPT_CORRECT_INFRARED].b);
        }
        if (scanner->val[OPT_CLEAN_IMAGE].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored in gray mode (irrelevant)\n", scanner->opt[OPT_CLEAN_IMAGE].name, scanner->val[OPT_CLEAN_IMAGE].b);
        }
        if (scanner->val[OPT_TRANSFORM_TO_SRGB].b) {
            DBG (DBG_info_sane, "Option %s = %d ignored in gray mode (irrelevant)\n", scanner->opt[OPT_TRANSFORM_TO_SRGB].name, scanner->val[OPT_TRANSFORM_TO_SRGB].w);
        }
    } else if (strcmp(scanner->val[OPT_MODE].s,SANE_VALUE_SCAN_MODE_COLOR)==0) {
        /* Some options require infrared data to be obtained, so all infrared options are relevant */
        /* Only do 8 or 16 bit scans */
        if (scanner->val[OPT_BIT_DEPTH].w == 1) {
            DBG (DBG_info_sane, "Option %s = %d ignored in color mode (will use 8)\n", scanner->opt[OPT_BIT_DEPTH].name, scanner->val[OPT_BIT_DEPTH].w);
        }
    } else if (strcmp(scanner->val[OPT_MODE].s,SANE_VALUE_SCAN_MODE_RGBI)==0) {
        /* Only do 8 or 16 bit scans */
        if (scanner->val[OPT_BIT_DEPTH].w == 1) {
            DBG (DBG_info_sane, "Option %s = %d ignored in color mode (will use 8)\n", scanner->opt[OPT_BIT_DEPTH].name, scanner->val[OPT_BIT_DEPTH].w);
        }
    }

    return 1;
}

/**
 * Print options
 *
 * @param scanner
 */
void
sanei_pieusb_print_options(struct Pieusb_Scanner *scanner)
{
    int k;
    /* List current options and values */
    DBG (DBG_info, "Num options = %d\n", scanner->val[OPT_NUM_OPTS].w);
    for (k = 1; k < scanner->val[OPT_NUM_OPTS].w; k++) {
        switch (scanner->opt[k].type) {
            case SANE_TYPE_BOOL:
                DBG(DBG_info,"  Option %d: %s = %d\n", k, scanner->opt[k].name, scanner->val[k].b);
                break;
            case SANE_TYPE_INT:
	        DBG(DBG_info,"  Option %d: %s = %d\n", k, scanner->opt[k].name, scanner->val[k].w);
                break;
            case SANE_TYPE_FIXED:
                DBG(DBG_info,"  Option %d: %s = %f\n", k, scanner->opt[k].name, SANE_UNFIX (scanner->val[k].w));
                break;
            case SANE_TYPE_STRING:
                DBG(DBG_info,"  Option %d: %s = %s\n", k, scanner->opt[k].name, scanner->val[k].s);
                break;
            case SANE_TYPE_GROUP:
                DBG(DBG_info,"  Option %d: %s = %s\n", k, scanner->opt[k].title, scanner->val[k].s);
	        break;
            default:
                DBG(DBG_info,"  Option %d: %s unknown type %d\n", k, scanner->opt[k].name, scanner->opt[k].type);
                break;
        }
    }
}

/**
 * Calculate reference values for each pixel, line means and line maxima.
 * We have got 45 lines for all four colors and for each CCD pixel.
 * The reference value for each pixel is the 45-line average for that
 * pixel, for each color separately.
 *
 * @param scanner
 * @param buffer
 */
static void pieusb_calculate_shading(struct Pieusb_Scanner *scanner, SANE_Byte* buffer)
{
    int k, m;
    SANE_Byte* p;
    SANE_Int ci, val;
    SANE_Int shading_width = scanner->device->shading_parameters[0].pixelsPerLine;
    SANE_Int shading_height = scanner->device->shading_parameters[0].nLines;

    /* Initialize all to 0 */
    for (k = 0; k < SHADING_PARAMETERS_INFO_COUNT; k++) {
        scanner->shading_max[k] = 0;
        scanner->shading_mean[k] = 0;
        memset(scanner->shading_ref[k], 0, shading_width * sizeof (SANE_Int));
    }
    /* Process data from buffer */
    p = buffer;
    switch (scanner->mode.colorFormat) {
        case 0x01: /* Pixel */
            /* Process pixel by pixel */
            for (k = 0; k < shading_height; k++) {
                for (m = 0; m < shading_width; m++) {
                    for (ci = 0; ci < SHADING_PARAMETERS_INFO_COUNT; ci++) {
                        val = *(p) + *(p+1) * 256;
                        scanner->shading_ref[ci][m] += val;
                        scanner->shading_max[ci] = scanner->shading_max[ci] < val ? val : scanner->shading_max[ci];
                        p += 2;
                    }
                }
            }
            break;
        case 0x04: /* Indexed */
            /* Process each line in the sequence found in the buffer */
            for (k = 0; k < shading_height*4; k++) {
                /* Save at right color */
                switch (*p) {
                    case 'R': ci = 0; break;
                    case 'G': ci = 1; break;
                    case 'B': ci = 2; break;
                    case 'I': ci = 3; break;
                    default: ci = -1; break; /* ignore line */
                }
                /* Add scanned data to reference line and keep track of maximum */
                if (ci != -1) {
                    for (m = 0; m < shading_width; m++) {
                        val = *(p+2+2*m) + *(p+2+2*m+1) * 256;
                        scanner->shading_ref[ci][m] += val;
                        scanner->shading_max[ci] = scanner->shading_max[ci] < val ? val : scanner->shading_max[ci];
                        /* DBG(DBG_error,"%02d Shading_ref[%d][%d] = %d\n",k,ci,m,scanner->shading_ref[ci][m]); */
                    }
                }
                /* Next line */
                p += 2*shading_width+2;
            }
            break;
        default:
            DBG (DBG_error,"sane_start(): color format %d not implemented\n",scanner->mode.colorFormat);
            return;
    }
    /* Mean reference value needs division */
    for (k = 0; k < SHADING_PARAMETERS_INFO_COUNT; k++) {
        for (m = 0; m < shading_width; m++) {
            scanner->shading_ref[k][m] = lround((double)scanner->shading_ref[k][m]/shading_height);
            /* DBG(DBG_error,"Shading_ref[%d][%d] = %d\n",k,m,scanner->shading_ref[k][m]); */
        }
    }
    /* Overall means */
    for (k = 0; k < SHADING_PARAMETERS_INFO_COUNT; k++) {
        for (m=0; m<shading_width; m++) {
            scanner->shading_mean[k] += scanner->shading_ref[k][m];
        }
        scanner->shading_mean[k] = lround((double)scanner->shading_mean[k]/shading_width);
        DBG (DBG_error,"Shading_mean[%d] = %d\n",k,scanner->shading_mean[k]);
    }

    /* Set shading data present */
    scanner->shading_data_present = SANE_TRUE;

    /* Export shading data as TIFF */
#ifdef CAN_DO_4_CHANNEL_TIFF
    if (scanner->val[OPT_SAVE_SHADINGDATA].b) {
        struct Pieusb_Read_Buffer shading;
        SANE_Byte* lboff = buffer;
        SANE_Int bpl = shading_width*2;
        SANE_Int n;
        buffer_create(&shading, shading_width, shading_height, 0x0F, 16);
        for (n=0; n<4*shading_height; n++) {
            if (buffer_put_single_color_line(&shading, *lboff, lboff+2, bpl) == 0) {
                break;
            }
            lboff += (bpl + 2);
        }
        FILE* fs = fopen("pieusb.shading", "w");
        /* write_tiff_rgbi_header (fs, shading_width, shading_height, 16, 3600, NULL); */
        fwrite(shading.data, 1, shading.image_size_bytes, fs);
        fclose(fs);
        buffer_delete(&shading);
    }
#endif

}

/*
 * Set frame (from scanner options)
 */

SANE_Status
sanei_pieusb_set_frame_from_options(Pieusb_Scanner * scanner)
{
    double dpmm;
    struct Pieusb_Command_Status status;

    dpmm = (double) scanner->device->maximum_resolution / MM_PER_INCH;
    scanner->frame.x0 = SANE_UNFIX(scanner->val[OPT_TL_X].w) * dpmm;
    scanner->frame.y0 = SANE_UNFIX(scanner->val[OPT_TL_Y].w) * dpmm;
    scanner->frame.x1 = SANE_UNFIX(scanner->val[OPT_BR_X].w) * dpmm;
    scanner->frame.y1 = SANE_UNFIX(scanner->val[OPT_BR_Y].w) * dpmm;
    scanner->frame.index = 0x80; /* 0x80: value from cyberview */
    sanei_pieusb_cmd_set_scan_frame (scanner->device_number, scanner->frame.index, &(scanner->frame), &status);
    DBG (DBG_info_sane, "sanei_pieusb_set_frame_from_options(): sanei_pieusb_cmd_set_scan_frame status %s\n", sane_strstatus (sanei_pieusb_convert_status (status.pieusb_status)));
    return sanei_pieusb_convert_status (status.pieusb_status);
}

/*
 * Set mode (from scanner options)
 */

SANE_Status
sanei_pieusb_set_mode_from_options(Pieusb_Scanner * scanner)
{
    struct Pieusb_Command_Status status;
    const char *mode;
    SANE_Status res;

    mode = scanner->val[OPT_MODE].s;
    if (strcmp (mode, SANE_VALUE_SCAN_MODE_LINEART) == 0) {
        scanner->mode.passes = SCAN_FILTER_GREEN; /* G */
        scanner->mode.colorFormat = SCAN_COLOR_FORMAT_PIXEL;
    } else if(strcmp (mode, SANE_VALUE_SCAN_MODE_HALFTONE) == 0) {
        scanner->mode.passes = SCAN_FILTER_GREEN; /* G */
        scanner->mode.colorFormat = SCAN_COLOR_FORMAT_PIXEL;
    } else if(strcmp (mode, SANE_VALUE_SCAN_MODE_GRAY) == 0) {
        scanner->mode.passes = SCAN_FILTER_GREEN; /* G=gray; unable to get R & B & I to work */
        scanner->mode.colorFormat = SCAN_COLOR_FORMAT_PIXEL;
    } else if(scanner->val[OPT_PREVIEW].b) {
        /* Catch preview here, otherwise next ifs get complicated */
        scanner->mode.passes = SCAN_ONE_PASS_COLOR;
        scanner->mode.colorFormat = SCAN_COLOR_FORMAT_INDEX; /* pixel format might be an alternative */
    } else if(strcmp (mode, SANE_VALUE_SCAN_MODE_RGBI) == 0) {
        scanner->mode.passes = SCAN_ONE_PASS_RGBI;
        scanner->mode.colorFormat = SCAN_COLOR_FORMAT_INDEX;
    } else if(strcmp (mode, SANE_VALUE_SCAN_MODE_COLOR) == 0 && scanner->val[OPT_CLEAN_IMAGE].b) {
        scanner->mode.passes = SCAN_ONE_PASS_RGBI; /* Need infrared for cleaning */
        scanner->mode.colorFormat = SCAN_COLOR_FORMAT_INDEX;
    } else { /* SANE_VALUE_SCAN_MODE_COLOR */
        scanner->mode.passes = SCAN_ONE_PASS_COLOR;
        scanner->mode.colorFormat = SCAN_COLOR_FORMAT_INDEX; /* pixel format might be an alternative */
    }
    /* Resolution */
    if (scanner->val[OPT_PREVIEW].b) {
        scanner->mode.resolution = scanner->device->fast_preview_resolution;
        DBG (DBG_info_sane, "sanei_pieusb_set_mode_from_options(): resolution fast preview (%d)\n", scanner->mode.resolution);
    } else {
        scanner->mode.resolution = SANE_UNFIX (scanner->val[OPT_RESOLUTION].w);
        DBG (DBG_info_sane, "sanei_pieusb_set_mode_from_options(): resolution from option setting (%d)\n", scanner->mode.resolution);
    }
    /* Bit depth: exit on untested values */
    switch (scanner->val[OPT_BIT_DEPTH].w) {
        case 1: scanner->mode.colorDepth = SCAN_COLOR_DEPTH_1; break;
        case 8: scanner->mode.colorDepth = SCAN_COLOR_DEPTH_8; break;
        case 16: scanner->mode.colorDepth = SCAN_COLOR_DEPTH_16; break;
        default: /* 4, 10 & 12 */
            DBG (DBG_error, "sanei_pieusb_set_mode_from_options(): sanei_pieusb_cmd_set_scan_frame untested bit depth %d\n", scanner->val[OPT_BIT_DEPTH].w);
            return SANE_STATUS_INVAL;
    }
    scanner->mode.byteOrder = 0x01; /* 0x01 = Intel; only bit 0 used */
    scanner->mode.sharpen = scanner->val[OPT_SHARPEN].b && !scanner->val[OPT_PREVIEW].b;
    scanner->mode.skipShadingAnalysis = !scanner->val[OPT_SHADING_ANALYSIS].b;
    scanner->mode.fastInfrared = scanner->val[OPT_FAST_INFRARED].b && !scanner->val[OPT_PREVIEW].b;
    if (strcmp (scanner->val[OPT_HALFTONE_PATTERN].s, "53lpi 45d ROUND") == 0) {
        scanner->mode.halftonePattern = 0;
    } else { /*TODO: the others */
        scanner->mode.halftonePattern = 0;
    }
    scanner->mode.lineThreshold = SANE_UNFIX (scanner->val[OPT_THRESHOLD].w) / 100 * 0xFF; /* 0xFF = 100% */
    sanei_pieusb_cmd_set_mode (scanner->device_number, &(scanner->mode), &status);
    res = sanei_pieusb_convert_status(status.pieusb_status);
    if (res == SANE_STATUS_GOOD) {
      res = sanei_pieusb_wait_ready (scanner, 0);
    }
    DBG (DBG_info_sane, "sanei_pieusb_set_mode_from_options(): sanei_pieusb_cmd_set_mode status %s\n", sane_strstatus(res));
    return res;
}

/**
 * Set gains, exposure and offset, to:
 * - values default (pieusb_set_default_gain_offset)
 * - values set by options
 * - values set by auto-calibration procedure
 * - values determined from preceding preview
 *
 * @param scanner
 * @return
 */
SANE_Status
sanei_pieusb_set_gain_offset(Pieusb_Scanner * scanner, const char *calibration_mode)
{
    struct Pieusb_Command_Status status;
    SANE_Status ret;
    double gain;

    DBG (DBG_info,"sanei_pieusb_set_gain_offset(): mode = %s\n", calibration_mode);

    if (strcmp (calibration_mode, SCAN_CALIBRATION_DEFAULT) == 0) {
        /* Default values */
        DBG(DBG_info_sane,"sanei_pieusb_set_gain_offset(): get calibration data from defaults\n");
        scanner->settings.exposureTime[0] = DEFAULT_EXPOSURE;
        scanner->settings.exposureTime[1] = DEFAULT_EXPOSURE;
        scanner->settings.exposureTime[2] = DEFAULT_EXPOSURE;
        scanner->settings.exposureTime[3] = DEFAULT_EXPOSURE;
        scanner->settings.offset[0] = DEFAULT_OFFSET;
        scanner->settings.offset[1] = DEFAULT_OFFSET;
        scanner->settings.offset[2] = DEFAULT_OFFSET;
        scanner->settings.offset[3] = DEFAULT_OFFSET;
        scanner->settings.gain[0] = DEFAULT_GAIN;
        scanner->settings.gain[1] = DEFAULT_GAIN;
        scanner->settings.gain[2] = DEFAULT_GAIN;
        scanner->settings.gain[3] = DEFAULT_GAIN;
        scanner->settings.light = DEFAULT_LIGHT;
        scanner->settings.extraEntries = DEFAULT_ADDITIONAL_ENTRIES;
        scanner->settings.doubleTimes = DEFAULT_DOUBLE_TIMES;
        status.pieusb_status = PIEUSB_STATUS_GOOD;
    } else if ((strcmp(calibration_mode, SCAN_CALIBRATION_PREVIEW) == 0)
	       && scanner->preview_done) {
        /* If no preview data available, do the auto-calibration. */
        double dg, dgi;
        DBG (DBG_info, "sanei_pieusb_set_gain_offset(): get calibration data from preview. scanner->mode.passes %d\n", scanner->mode.passes);
        switch (scanner->mode.passes) {
            case SCAN_ONE_PASS_RGBI:
                dg = 3.00;
                dgi = ((double)scanner->settings.saturationLevel[0] / 65536) / ((double)scanner->preview_upper_bound[0] / HISTOGRAM_SIZE);
                if (dgi < dg) dg = dgi;
                dgi = ((double)scanner->settings.saturationLevel[1] / 65536) / ((double)scanner->preview_upper_bound[1] / HISTOGRAM_SIZE);
                if (dgi < dg) dg = dgi;
                dgi = ((double)scanner->settings.saturationLevel[2] / 65536) / ((double)scanner->preview_upper_bound[2] / HISTOGRAM_SIZE);
                if (dgi < dg) dg = dgi;
                updateGain2(scanner, 0, dg);
                updateGain2(scanner, 1, dg);
                updateGain2(scanner, 2, dg);
	    break;
            case SCAN_ONE_PASS_COLOR:
                dg = 3.00;
                dgi = ((double)scanner->settings.saturationLevel[0] / 65536) / ((double)scanner->preview_upper_bound[0] / HISTOGRAM_SIZE);
                if (dgi < dg) dg = dgi;
                dgi = ((double)scanner->settings.saturationLevel[1] / 65536) / ((double)scanner->preview_upper_bound[1] / HISTOGRAM_SIZE);
                if (dgi < dg) dg = dgi;
                dgi = ((double)scanner->settings.saturationLevel[2] / 65536) / ((double)scanner->preview_upper_bound[2] / HISTOGRAM_SIZE);
                if (dgi < dg) dg = dgi;
                updateGain2(scanner, 0, dg);
                updateGain2(scanner, 1, dg);
                updateGain2(scanner, 2, dg);
                break;
            case SCAN_FILTER_BLUE:
                dg = 3.00;
                dgi = ((double)scanner->settings.saturationLevel[2] / 65536) / ((double)scanner->preview_upper_bound[2] / HISTOGRAM_SIZE);
                if (dgi < dg) dg = dgi;
                updateGain2(scanner, 2, dg);
                break;
            case SCAN_FILTER_GREEN:
                dg = 3.00;
                dgi = ((double)scanner->settings.saturationLevel[1] / 65536) / ((double)scanner->preview_upper_bound[1] / HISTOGRAM_SIZE);
                if (dgi < dg) dg = dgi;
                updateGain2(scanner, 1, dg);
                break;
            case SCAN_FILTER_RED:
                dg = 3.00;
                dgi = ((double)scanner->settings.saturationLevel[0] / 65536) / ((double)scanner->preview_upper_bound[0] / HISTOGRAM_SIZE);
                if (dgi < dg) dg = dgi;
                updateGain2(scanner, 0, dg);
                break;
            case SCAN_FILTER_NEUTRAL:
                break;
        }
        status.pieusb_status = PIEUSB_STATUS_GOOD;
    } else if (strcmp (calibration_mode, SCAN_CALIBRATION_OPTIONS) == 0) {
        DBG (DBG_info_sane, "sanei_pieusb_set_gain_offset(): get calibration data from options\n");
        /* Exposure times */
        scanner->settings.exposureTime[0] = scanner->val[OPT_SET_EXPOSURE_R].w;
        scanner->settings.exposureTime[1] = scanner->val[OPT_SET_EXPOSURE_G].w;
        scanner->settings.exposureTime[2] = scanner->val[OPT_SET_EXPOSURE_B].w;
        scanner->settings.exposureTime[3] = scanner->val[OPT_SET_EXPOSURE_I].w; /* Infrared */
        /* Offsets */
        scanner->settings.offset[0] = scanner->val[OPT_SET_OFFSET_R].w;
        scanner->settings.offset[1] = scanner->val[OPT_SET_OFFSET_G].w;
        scanner->settings.offset[2] = scanner->val[OPT_SET_OFFSET_B].w;
        scanner->settings.offset[3] = scanner->val[OPT_SET_OFFSET_I].w; /* Infrared */
        /* Gains */
        scanner->settings.gain[0] = scanner->val[OPT_SET_GAIN_R].w;
        scanner->settings.gain[1] = scanner->val[OPT_SET_GAIN_G].w;
        scanner->settings.gain[2] = scanner->val[OPT_SET_GAIN_B].w;
        scanner->settings.gain[3] = scanner->val[OPT_SET_GAIN_I].w; /* Infrared */
        /* Light, extra entries and doubling */
        scanner->settings.light = scanner->val[OPT_LIGHT].w;
        scanner->settings.extraEntries = DEFAULT_ADDITIONAL_ENTRIES;
        scanner->settings.doubleTimes = scanner->val[OPT_DOUBLE_TIMES].w;
        status.pieusb_status = PIEUSB_STATUS_GOOD;
    } else { /* SCAN_CALIBRATION_AUTO */
        DBG (DBG_info_sane, "sanei_pieusb_set_gain_offset(): get calibration data from scanner\n");
        sanei_pieusb_cmd_get_gain_offset (scanner->device_number, &scanner->settings, &status);
    }
    /* Check status */
    if (status.pieusb_status == PIEUSB_STATUS_DEVICE_BUSY) {
      ret = sanei_pieusb_wait_ready (scanner, 0);
      if (ret != SANE_STATUS_GOOD) {
	DBG (DBG_error,"sanei_pieusb_set_gain_offset(): not ready after sanei_pieusb_cmd_get_gain_offset(): %d\n", ret);
	return ret;
      }
    }
    else if (status.pieusb_status != PIEUSB_STATUS_GOOD) {
        return SANE_STATUS_INVAL;
    }
    /* Adjust gain */
    gain = 1.0;
    if (strcmp (scanner->val[OPT_GAIN_ADJUST].s, SCAN_GAIN_ADJUST_03) == 0) {
        gain = 0.3;
    } else if (strcmp (scanner->val[OPT_GAIN_ADJUST].s, SCAN_GAIN_ADJUST_05) == 0) {
        gain = 0.5;
    } else if (strcmp (scanner->val[OPT_GAIN_ADJUST].s, SCAN_GAIN_ADJUST_08) == 0) {
        gain = 0.8;
    } else if (strcmp (scanner->val[OPT_GAIN_ADJUST].s, SCAN_GAIN_ADJUST_10) == 0) {
        gain = 1.0;
    } else if (strcmp (scanner->val[OPT_GAIN_ADJUST].s, SCAN_GAIN_ADJUST_12) == 0) {
        gain = 1.2;
    } else if (strcmp (scanner->val[OPT_GAIN_ADJUST].s, SCAN_GAIN_ADJUST_16) == 0) {
        gain = 1.6;
    } else if (strcmp (scanner->val[OPT_GAIN_ADJUST].s, SCAN_GAIN_ADJUST_19) == 0) {
        gain = 1.9;
    } else if (strcmp (scanner->val[OPT_GAIN_ADJUST].s, SCAN_GAIN_ADJUST_24) == 0) {
        gain = 2.4;
    } else if (strcmp (scanner->val[OPT_GAIN_ADJUST].s, SCAN_GAIN_ADJUST_30) == 0) {
        gain = 3.0;
    }
    switch (scanner->mode.passes) {
        case SCAN_ONE_PASS_RGBI:
        case SCAN_ONE_PASS_COLOR:
            updateGain2 (scanner, 0, gain);
            updateGain2 (scanner, 1, gain);
            updateGain2 (scanner, 2, gain);
            /* Don't correct IR, hampers cleaning process... */
            break;
        case SCAN_FILTER_INFRARED:
            updateGain2 (scanner, 3, gain);
            break;
        case SCAN_FILTER_BLUE:
            updateGain2 (scanner, 2, gain);
            break;
        case SCAN_FILTER_GREEN:
            updateGain2 (scanner, 1, gain);
            break;
        case SCAN_FILTER_RED:
            updateGain2 (scanner, 0, gain);
            break;
        case SCAN_FILTER_NEUTRAL:
            break;
    }
    /* Now set values for gain, offset and exposure */
    sanei_pieusb_cmd_set_gain_offset (scanner->device_number, &(scanner->settings), &status);
    ret = sanei_pieusb_convert_status (status.pieusb_status);
    DBG (DBG_info_sane, "sanei_pieusb_set_gain_offset(): status %s\n", sane_strstatus (ret));
    return ret;
}

/*
 * get shading data
 * must be called immediately after sanei_pieusb_set_gain_offset
 */

SANE_Status
sanei_pieusb_get_shading_data(Pieusb_Scanner * scanner)
{
    struct Pieusb_Command_Status status;
    SANE_Int shading_width;
    SANE_Int shading_height;
    SANE_Byte* buffer;
    SANE_Int lines;
    SANE_Int cols;
    SANE_Int size;
    SANE_Status res = SANE_STATUS_GOOD;

    DBG (DBG_info_sane, "sanei_pieusb_get_shading_data()\n");
    shading_width = scanner->device->shading_parameters[0].pixelsPerLine;
    shading_height = scanner->device->shading_parameters[0].nLines;
    if (shading_height < 1) {
        DBG (DBG_error, "shading_height < 1\n");
	return SANE_STATUS_INVAL;
    }
    switch (scanner->mode.colorFormat) {
        case SCAN_COLOR_FORMAT_PIXEL: /* Pixel */
            lines = shading_height * 4;
            cols = 2 * shading_width;
            break;
        case SCAN_COLOR_FORMAT_INDEX: /* Indexed */
            lines = shading_height * 4;
            cols = (2 * shading_width + 2);
            break;
        default:
            DBG (DBG_error, "sanei_pieusb_get_shading_data(): color format %d not implemented\n", scanner->mode.colorFormat);
            return SANE_STATUS_INVAL;
    }

    size = cols * lines;
    buffer = malloc (size);
    if (buffer == NULL) {
        return SANE_STATUS_NO_MEM;
    }
    sanei_pieusb_cmd_get_scanned_lines (scanner->device_number, buffer, 4, cols * 4, &status);
    if (status.pieusb_status == PIEUSB_STATUS_GOOD) {
        res = sanei_pieusb_wait_ready (scanner, 0);
        if (res == SANE_STATUS_GOOD) {
            sanei_pieusb_cmd_get_scanned_lines (scanner->device_number, buffer + cols*4, lines - 4, (lines - 4) * cols, &status);
	    if (status.pieusb_status == PIEUSB_STATUS_GOOD) {
	        pieusb_calculate_shading (scanner, buffer);
	    }
	    res = sanei_pieusb_convert_status (status.pieusb_status);
	}
    }
    else {
        res = sanei_pieusb_convert_status (status.pieusb_status);
    }
    free (buffer);
    return res;
}

/*
 *
 */

SANE_Status
sanei_pieusb_get_ccd_mask(Pieusb_Scanner * scanner)
{
    struct Pieusb_Command_Status status;

    DBG(DBG_info_proc, "sanei_pieusb_get_ccd_mask()\n");

    sanei_pieusb_cmd_get_ccd_mask(scanner->device_number, scanner->ccd_mask, scanner->ccd_mask_size, &status);
    if (status.pieusb_status == PIEUSB_STATUS_GOOD) {
      /* Save CCD mask */
      if (scanner->val[OPT_SAVE_CCDMASK].b) {
        FILE* fs = fopen ("pieusb.ccd", "w");
        fwrite (scanner->ccd_mask, 1, scanner->ccd_mask_size, fs);
        fclose (fs);
      }
    }
  return sanei_pieusb_convert_status(status.pieusb_status);

}

/**
 * Read parameters from scanner
 * and initialize SANE parameters
 *
 * @param scanner
 * @return parameter_bytes for use in get_scan_data()
 */
SANE_Status
sanei_pieusb_get_parameters(Pieusb_Scanner * scanner, SANE_Int *parameter_bytes)
{
    struct Pieusb_Command_Status status;
    struct Pieusb_Scan_Parameters parameters;
    const char *mode;

    DBG (DBG_info_proc, "sanei_pieusb_get_parameters()\n");

    sanei_pieusb_cmd_get_parameters (scanner->device_number, &parameters, &status);
    if (status.pieusb_status != PIEUSB_STATUS_GOOD) {
        return sanei_pieusb_convert_status (status.pieusb_status);
    }
    *parameter_bytes = parameters.bytes;
    /* Use response from sanei_pieusb_cmd_get_parameters() for initialization of SANE parameters.
     * Note the weird values of the bytes-field: this is because of the colorFormat
     * setting in sanei_pieusb_cmd_set_mode(). The single-color modes all use the pixel format,
     * which makes sanei_pieusb_cmd_get_parameters() return a full color line although just
     * one color actually contains data. For the index format, the bytes field
     * gives the size of a single color line. */
    mode = scanner->val[OPT_MODE].s;
    if (strcmp (mode, SANE_VALUE_SCAN_MODE_LINEART) == 0) {
        scanner->scan_parameters.format = SANE_FRAME_GRAY;
        scanner->scan_parameters.depth = 1;
        scanner->scan_parameters.bytes_per_line = parameters.bytes/3;
    } else if (strcmp (mode, SANE_VALUE_SCAN_MODE_HALFTONE) == 0) {
        scanner->scan_parameters.format = SANE_FRAME_GRAY;
        scanner->scan_parameters.depth = 1;
        scanner->scan_parameters.bytes_per_line = parameters.bytes/3;
    } else if (strcmp (mode, SANE_VALUE_SCAN_MODE_GRAY) == 0) {
        scanner->scan_parameters.format = SANE_FRAME_GRAY;
        scanner->scan_parameters.depth = scanner->val[OPT_BIT_DEPTH].w;
        scanner->scan_parameters.bytes_per_line = parameters.bytes/3;
    } else if (strcmp (mode, SANE_VALUE_SCAN_MODE_RGBI) == 0) {
        scanner->scan_parameters.format = SANE_FRAME_RGB; /* was: SANE_FRAME_RGBI */
        scanner->scan_parameters.depth = scanner->val[OPT_BIT_DEPTH].w;
        scanner->scan_parameters.bytes_per_line = 4*parameters.bytes;
    } else { /* SANE_VALUE_SCAN_MODE_COLOR, with and without option clean image set */
        scanner->scan_parameters.format = SANE_FRAME_RGB;
        scanner->scan_parameters.depth = scanner->val[OPT_BIT_DEPTH].w;
        scanner->scan_parameters.bytes_per_line = 3*parameters.bytes;
    }
    scanner->scan_parameters.lines = parameters.lines;
    scanner->scan_parameters.pixels_per_line = parameters.width;
    scanner->scan_parameters.last_frame = SANE_TRUE;

    DBG (DBG_info_sane,"sanei_pieusb_get_parameters(): mode '%s'\n", mode);
    DBG (DBG_info_sane," format = %d\n", scanner->scan_parameters.format);
    DBG (DBG_info_sane," depth = %d\n", scanner->scan_parameters.depth);
    DBG (DBG_info_sane," bytes_per_line = %d\n", scanner->scan_parameters.bytes_per_line);
    DBG (DBG_info_sane," lines = %d\n", scanner->scan_parameters.lines);
    DBG (DBG_info_sane," pixels_per_line = %d\n", scanner->scan_parameters.pixels_per_line);
    DBG (DBG_info_sane," last_frame = %d\n", scanner->scan_parameters.last_frame);

    return SANE_STATUS_GOOD;
}

SANE_Status
sanei_pieusb_get_scan_data(Pieusb_Scanner * scanner, SANE_Int parameter_bytes)
{
    struct Pieusb_Command_Status status;
    SANE_Parameters *parameters = &scanner->scan_parameters;
    SANE_Int lines_to_read, lines_remaining;
    SANE_Int ppl, bpl;
    SANE_Byte *linebuf, *lboff;
    SANE_Bool compress;
    int n, k, i;

    switch (scanner->mode.colorFormat) {
        case SCAN_COLOR_FORMAT_PIXEL: /* Pixel */
            lines_to_read = scanner->buffer.height;
            break;
        case SCAN_COLOR_FORMAT_INDEX: /* Indexed */
            lines_to_read = scanner->buffer.colors * scanner->buffer.height;
            break;
        default:
            DBG(DBG_error, "sanei_pieusb_get_scan_data(): color format %d not implemented\n",scanner->mode.colorFormat);
            return SANE_STATUS_INVAL;
    }
    lines_remaining = lines_to_read;
    DBG (DBG_info_proc, "sanei_pieusb_get_scan_data(colorFormat %d), lines_to_read %d, bytes %d\n", scanner->mode.colorFormat, lines_to_read, parameter_bytes);

  /*
    fdraw = open("/tmp/pieusb.raw", O_WRONLY | O_CREAT | O_TRUNC, (mode_t)0600);
    if (fdraw == -1) {
         perror("error opening raw image buffer file");
    }
*/
    while (lines_remaining > 0) {
        SANE_Int lines;
        /* Read lines */
        /* The amount of bytes_per_line varies with color format setting; only 'pixel' and 'index' implemented */
        ppl = parameters->pixels_per_line;
        switch (scanner->mode.colorFormat) {
	    case SCAN_COLOR_FORMAT_PIXEL: /* Pixel */
	        bpl = parameter_bytes;
	        break;
            case SCAN_COLOR_FORMAT_INDEX: /* Indexed */
	        bpl = parameter_bytes + 2; /* Index bytes! */
	        break;
            default:
	        DBG(DBG_error, "sanei_pieusb_get_scan_data(): color format %d not implemented\n", scanner->mode.colorFormat);
	        return SANE_STATUS_INVAL;
        }
        lines = (lines_remaining < 256) ? lines_remaining : 255;
        DBG(DBG_info_sane, "sanei_pieusb_get_scan_data(): reading lines: now %d, bytes per line = %d\n", lines, bpl);
        linebuf = malloc(lines * bpl);
        sanei_pieusb_cmd_get_scanned_lines(scanner->device_number, linebuf, lines, lines * bpl, &status);
        if (status.pieusb_status != PIEUSB_STATUS_GOOD ) {
	    /* Error, return */
	    free(linebuf);
	    return SANE_STATUS_INVAL;
	}
        /* Save raw data */
/*
        if (fdraw != -1) {
            wcnt = write(fdraw,linebuf,parameters.lines*bpl);
            DBG(DBG_info_sane,"Raw written %d\n",wcnt);
        }
*/
        /* Copy into official buffer
	 * Sometimes the scanner returns too many lines. Take care not to
	 * overflow the buffer. */
        lboff = linebuf;
        switch (scanner->mode.colorFormat) {
	    case SCAN_COLOR_FORMAT_PIXEL:
	        /* The scanner may return lines with 3 colors even though only
		 * one color is actually scanned. Detect this situation and
		 * eliminate the excess samples from the line buffer before
		 * handing it to buffer_put_full_color_line(). */
	        compress = SANE_FALSE;
	        if (scanner->buffer.colors == 1
		    && (bpl * scanner->buffer.packing_density / ppl) == (3 * scanner->buffer.packet_size_bytes)) {
		    compress = SANE_TRUE;
	        }
	        for (n = 0; n < lines; n++) {
		     if (compress) {
		         /* Move samples to fill up all unused locations */
		         int ps = scanner->buffer.packet_size_bytes;
		         for (k = 0; k < scanner->buffer.line_size_packets; k++) {
			     for (i = 0; i < ps; i++) {
			         lboff[ps*k+i] = lboff[3*ps*k+i];
			     }
			 }
		     }
		     if (sanei_pieusb_buffer_put_full_color_line(&scanner->buffer, lboff, bpl/3) == 0) {
		         /* Error, return */
		         return SANE_STATUS_INVAL;
		     }
		     lboff += bpl;
	        }
	        break;
	    case SCAN_COLOR_FORMAT_INDEX:
	        /* Indexed data */
	        for (n = 0; n < lines; n++) {
		    if (sanei_pieusb_buffer_put_single_color_line(&scanner->buffer, *lboff, lboff+2, bpl-2) == 0) {
		        /* Error, return */
		        return SANE_STATUS_INVAL;
		    }
		    lboff += bpl;
	        }
	        break;
	    default:
	        DBG(DBG_error, "sanei_pieusb_get_scan_data(): store color format %d not implemented\n", scanner->mode.colorFormat);
	        free(linebuf);
	        return SANE_STATUS_INVAL;
        }
        free(linebuf);
        lines_remaining -= lines; /* Note: excess discarded */
        DBG(DBG_info_sane, "sanei_pieusb_get_scan_data(): reading lines: remaining %d\n", lines_remaining);
    }
/*
    if (fdraw != -1) close(fdraw);
*/
    return SANE_STATUS_GOOD;
}

/**
 * Wait for scanner to get ready
 *
 * loop of test_ready/read_state
 *
 * @param scanner
 * @param device_number, used if scanner == NULL
 * @return SANE_Status
 */

SANE_Status
sanei_pieusb_wait_ready(Pieusb_Scanner * scanner, SANE_Int device_number)
{
  struct Pieusb_Command_Status status;
  struct Pieusb_Scanner_State state;
  time_t start, elapsed;

  DBG (DBG_info_proc, "sanei_pieusb_wait_ready()\n");
  start = time(NULL);
  if (scanner)
    device_number = scanner->device_number;

  for(;;) {
    sanei_pieusb_cmd_test_unit_ready(device_number, &status);
    DBG (DBG_info_proc, "-> sanei_pieusb_cmd_test_unit_ready: %d\n", status.pieusb_status);
    if (status.pieusb_status == PIEUSB_STATUS_GOOD)
      break;
    if (status.pieusb_status == PIEUSB_STATUS_IO_ERROR)
      break;
    sanei_pieusb_cmd_read_state(device_number, &state, &status);
    DBG (DBG_info_proc, "-> sanei_pieusb_cmd_read_state: %d\n", status.pieusb_status);
    if (status.pieusb_status != PIEUSB_STATUS_DEVICE_BUSY)
      break;
    sleep(2);
    elapsed = time(NULL) - start;
    if (elapsed > 120) { /* 2 minute overall timeout */
      DBG (DBG_error, "scanner not ready after 2 minutes\n");
      break;
    }
    if (elapsed % 2) {
      DBG (DBG_info, "still waiting for scanner to get ready\n");
    }
  }
  return sanei_pieusb_convert_status(status.pieusb_status);
}


SANE_Status sanei_pieusb_analyze_preview(Pieusb_Scanner * scanner)
{
    int k, n;
    SANE_Parameters params;
    SANE_Int N;
    double *norm_histo;
    double level;

    DBG(DBG_info, "sanei_pieusb_analyze_preview(): saving preview data\n");

    /* Settings */
    scanner->preview_done = SANE_TRUE;
    for (k = 0; k < 4; k++) {
        scanner->preview_exposure[k] = scanner->settings.exposureTime[k];
        scanner->preview_gain[k] = scanner->settings.gain[k];
        scanner->preview_offset[k] = scanner->settings.offset[k];
    }
    /* Analyze color planes */
    N = scanner->buffer.width * scanner->buffer.height;
    params.format = SANE_FRAME_GRAY;
    params.depth = scanner->buffer.depth;
    params.pixels_per_line = scanner->buffer.width;
    params.lines = scanner->buffer.height;
    for (k = 0; k < scanner->buffer.colors; k++) {
        /* Create histogram for color k */
        sanei_ir_create_norm_histogram (&params, scanner->buffer.data + k * N, &norm_histo);
        /* Find 1% and 99% limits */
        level = 0;
        for (n =0; n < HISTOGRAM_SIZE; n++) {

            level += norm_histo[n];
            if (level < 0.01) {
                scanner->preview_lower_bound[k] = n;
            }
            if (level < 0.99) {
                scanner->preview_upper_bound[k] = n;
            }
        }
        DBG(DBG_info,"sanei_pieusb_analyze_preview(): 1%%-99%% levels for color %d: %d - %d\n", k, scanner->preview_lower_bound[k], scanner->preview_upper_bound[k]);
    }
    /* Disable remaining color planes */
    for (k = scanner->buffer.colors; k < 4; k++) {
        scanner->preview_lower_bound[k] = 0;
        scanner->preview_upper_bound[k] = 0;
    }
    return SANE_STATUS_GOOD;
}


/**
 * Return actual gain at given gain setting
 *
 * @param gain Gain setting (0 - 63)
 * @return
 */
static double getGain(int gain)
{
    int k;

    /* Actually an error, but don't be picky */
    if (gain <= 0) {
        return gains[0];
    }
    /* A gain > 63 is also an error, but don't be picky */
    if (gain >= 60) {
        return (gain-55)*(gains[12]-gains[11])/5 + gains[11];
    }
    /* Interpolate other values */
    k = gain/5; /* index of array value just below given gain */
    return (gain-5*k)*(gains[k+1]-gains[k])/5 + gains[k];
}

static int getGainSetting(double gain)
{
    int k, m;

    /* Out of bounds */
    if (gain < 1.0) {
        return 0;
    }
    if (gain >= gains[12]) {
        m = 60 + lround((gain-gains[11])/(gains[12]-gains[11])*5);
        if (m > 63) m = 63;
        return m;
    }
    /* Interpolate the rest */
    m = 0;
    for (k = 0; k <= 11; k++) {
        if (gains[k] <= gain && gain < gains[k+1]) {
            m = 5*k + lround((gain-gains[k])/(gains[k+1]-gains[k])*5);
        }
    }
    return m;
}

/**
 * Modify gain and exposure times in order to make maximal use of the scan depth.
 * Each color treated separately, infrared excluded.
 *
 * This may be too aggressive => leads to a noisy whitish border instead of the orange.
 * In a couuple of tries, gain was set to values of 60 and above, which introduces
 * the noise?
 * The whitish border is logical since the brightest parts of the negative, the
 * unexposed borders, are amplified to values near CCD saturation, which is white.
 * Maybe a uniform gain increase for each color is more appropriate? Somewhere
 * between 2.5 and 3 seems worthwhile trying, see updateGain2().
 *
        switch (scanner->mode.passes) {
            case SCAN_ONE_PASS_RGBI:
                updateGain(scanner,0);
                updateGain(scanner,1);
                updateGain(scanner,2);
                updateGain(scanner,3);
                break;
            case SCAN_ONE_PASS_COLOR:
                updateGain(scanner,0);
                updateGain(scanner,1);
                updateGain(scanner,2);
                break;
            case SCAN_FILTER_INFRARED:
                updateGain(scanner,3);
                break;
            case SCAN_FILTER_BLUE:
                updateGain(scanner,2);
                break;
            case SCAN_FILTER_GREEN:
                updateGain(scanner,1);
                break;
            case SCAN_FILTER_RED:
                updateGain(scanner,0);
                break;
            case SCAN_FILTER_NEUTRAL:
                break;
        }
 * @param scanner
 */
/*
static void updateGain(Pieusb_Scanner *scanner, int color_index)
{
    double g, dg;

    DBG(DBG_info_sane,"updateGain(): color %d preview used G=%d Exp=%d\n", color_index, scanner->preview_gain[color_index], scanner->preview_exposure[color_index]);
    // Additional gain to obtain
    dg = ((double)scanner->settings.saturationLevel[color_index] / 65536) / ((double)scanner->preview_upper_bound[color_index] / HISTOGRAM_SIZE);
    DBG(DBG_info_sane,"updateGain(): additional gain %f\n", dg);
    // Achieve this by modifying gain and exposure
    // Gain used for preview
    g = getGain(scanner->preview_gain[color_index]);
    DBG(DBG_info_sane,"updateGain(): preview had gain %d => %f\n",scanner->preview_gain[color_index],g);
    // Look up new gain setting g*sqrt(dg)
    DBG(DBG_info_sane,"updateGain(): optimized gain * %f = %f\n",sqrt(dg),sqrt(dg)*g);
    scanner->settings.gain[color_index] = getGainSetting(g*sqrt(dg));
    DBG(DBG_info_sane,"updateGain(): optimized gain setting %d => %f\n",scanner->settings.gain[color_index],getGain(scanner->settings.gain[color_index]));
    // Exposure change is straightforward
    DBG(DBG_info_sane,"updateGain(): remains for exposure %f\n",dg/(getGain(scanner->settings.gain[color_index])/g));
    scanner->settings.exposureTime[color_index] = lround( g / getGain(scanner->settings.gain[color_index]) * dg * scanner->preview_exposure[color_index] );
    DBG(DBG_info_sane,"updateGain(): new setting G=%d Exp=%d\n", scanner->settings.gain[color_index], scanner->settings.exposureTime[color_index]);
}
*/

static void updateGain2(Pieusb_Scanner *scanner, int color_index, double gain_increase)
{
    double g;

    DBG(DBG_info,"updateGain2(): color %d preview used G=%d Exp=%d\n", color_index, scanner->settings.gain[color_index], scanner->settings.exposureTime[color_index]);
    /* Additional gain to obtain */
    DBG(DBG_info,"updateGain2(): additional gain %f\n", gain_increase);
    /* Achieve this by modifying gain and exposure */
    /* Gain used for preview */
    g = getGain(scanner->settings.gain[color_index]);
    DBG(DBG_info,"updateGain2(): preview had gain %d => %f\n", scanner->settings.gain[color_index], g);
    /* Look up new gain setting g*sqrt(dg) */
    DBG(DBG_info,"updateGain2(): optimized gain * %f = %f\n", sqrt(gain_increase), sqrt(gain_increase) * g);
    scanner->settings.gain[color_index] = getGainSetting(g * sqrt(gain_increase));
    DBG(DBG_info,"updateGain2(): optimized gain setting %d => %f\n", scanner->settings.gain[color_index], getGain(scanner->settings.gain[color_index]));
    /* Exposure change is straightforward */
    DBG(DBG_info,"updateGain2(): remains for exposure %f\n", gain_increase / (getGain(scanner->settings.gain[color_index]) / g));
    scanner->settings.exposureTime[color_index] = lround( g / getGain(scanner->settings.gain[color_index]) * gain_increase * scanner->settings.exposureTime[color_index] );
    DBG(DBG_info,"updateGain2(): new setting G=%d Exp=%d\n", scanner->settings.gain[color_index], scanner->settings.exposureTime[color_index]);
}
