diff options
author | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2020-02-02 17:13:01 +0100 |
---|---|---|
committer | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2020-02-02 17:13:01 +0100 |
commit | ffa8801644a7d53cc1c785e3450f794c07a14eb0 (patch) | |
tree | 8d72a18a9a08b9151d12badcb1c78ce06a059f62 /backend/genesys/genesys.cpp | |
parent | 1687222e1b9e74c89cafbb5910e72d8ec7bfd40f (diff) |
New upstream version 1.0.29upstream/1.0.29
Diffstat (limited to 'backend/genesys/genesys.cpp')
-rw-r--r-- | backend/genesys/genesys.cpp | 6172 |
1 files changed, 6172 insertions, 0 deletions
diff --git a/backend/genesys/genesys.cpp b/backend/genesys/genesys.cpp new file mode 100644 index 0000000..7c25168 --- /dev/null +++ b/backend/genesys/genesys.cpp @@ -0,0 +1,6172 @@ +/* sane - Scanner Access Now Easy. + + Copyright (C) 2003, 2004 Henning Meier-Geinitz <henning@meier-geinitz.de> + Copyright (C) 2004, 2005 Gerhard Jaeger <gerhard@gjaeger.de> + Copyright (C) 2004-2016 Stéphane Voltz <stef.dev@free.fr> + Copyright (C) 2005-2009 Pierre Willenbrock <pierre@pirsoft.dnsalias.org> + Copyright (C) 2006 Laurent Charpentier <laurent_pubs@yahoo.com> + Copyright (C) 2007 Luke <iceyfor@gmail.com> + Copyright (C) 2010 Chris Berry <s0457957@sms.ed.ac.uk> and Michael Rickmann <mrickma@gwdg.de> + for Plustek Opticbook 3600 support + + Dynamic rasterization code was taken from the epjistsu backend by + m. allan noah <kitno455 at gmail dot com> + + Software processing for deskew, crop and dspeckle are inspired by allan's + noah work in the fujitsu backend + + 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, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, + MA 02111-1307, USA. + + 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. +*/ + +/* + * SANE backend for Genesys Logic GL646/GL841/GL842/GL843/GL846/GL847/GL124 based scanners + */ + +#define DEBUG_NOT_STATIC + +#include "genesys.h" +#include "conv.h" +#include "gl124_registers.h" +#include "gl841_registers.h" +#include "gl843_registers.h" +#include "gl846_registers.h" +#include "gl847_registers.h" +#include "usb_device.h" +#include "utilities.h" +#include "scanner_interface_usb.h" +#include "test_scanner_interface.h" +#include "test_settings.h" +#include "../include/sane/sanei_config.h" +#include "../include/sane/sanei_magic.h" + +#include <array> +#include <cmath> +#include <cstring> +#include <fstream> +#include <iterator> +#include <list> +#include <numeric> +#include <exception> +#include <vector> + +#ifndef SANE_GENESYS_API_LINKAGE +#define SANE_GENESYS_API_LINKAGE extern "C" +#endif + +namespace genesys { + +// Data that we allocate to back SANE_Device objects in s_sane_devices +struct SANE_Device_Data +{ + std::string name; +}; + +namespace { + StaticInit<std::list<Genesys_Scanner>> s_scanners; + StaticInit<std::vector<SANE_Device>> s_sane_devices; + StaticInit<std::vector<SANE_Device_Data>> s_sane_devices_data; + StaticInit<std::vector<SANE_Device*>> s_sane_devices_ptrs; + StaticInit<std::list<Genesys_Device>> s_devices; + + // Maximum time for lamp warm-up + constexpr unsigned WARMUP_TIME = 65; +} // namespace + +static SANE_String_Const mode_list[] = { + SANE_VALUE_SCAN_MODE_COLOR, + SANE_VALUE_SCAN_MODE_GRAY, + /* SANE_TITLE_HALFTONE, currently unused */ + SANE_VALUE_SCAN_MODE_LINEART, + nullptr +}; + +static SANE_String_Const color_filter_list[] = { + SANE_I18N ("Red"), + SANE_I18N ("Green"), + SANE_I18N ("Blue"), + nullptr +}; + +static SANE_String_Const cis_color_filter_list[] = { + SANE_I18N ("Red"), + SANE_I18N ("Green"), + SANE_I18N ("Blue"), + SANE_I18N ("None"), + nullptr +}; + +static SANE_Range swdespeck_range = { + 1, + 9, + 1 +}; + +static SANE_Range time_range = { + 0, /* minimum */ + 60, /* maximum */ + 0 /* quantization */ +}; + +static const SANE_Range u12_range = { + 0, /* minimum */ + 4095, /* maximum */ + 0 /* quantization */ +}; + +static const SANE_Range u14_range = { + 0, /* minimum */ + 16383, /* maximum */ + 0 /* quantization */ +}; + +static const SANE_Range u16_range = { + 0, /* minimum */ + 65535, /* maximum */ + 0 /* quantization */ +}; + +static const SANE_Range percentage_range = { + SANE_FIX (0), /* minimum */ + SANE_FIX (100), /* maximum */ + SANE_FIX (1) /* quantization */ +}; + +static const SANE_Range threshold_curve_range = { + 0, /* minimum */ + 127, /* maximum */ + 1 /* quantization */ +}; + +/** + * range for brightness and contrast + */ +static const SANE_Range enhance_range = { + -100, /* minimum */ + 100, /* maximum */ + 1 /* quantization */ +}; + +/** + * range for expiration time + */ +static const SANE_Range expiration_range = { + -1, /* minimum */ + 30000, /* maximum */ + 1 /* quantization */ +}; + +const Genesys_Sensor& sanei_genesys_find_sensor_any(Genesys_Device* dev) +{ + DBG_HELPER(dbg); + for (const auto& sensor : *s_sensors) { + if (dev->model->sensor_id == sensor.sensor_id) { + return sensor; + } + } + throw std::runtime_error("Given device does not have sensor defined"); +} + +Genesys_Sensor* find_sensor_impl(Genesys_Device* dev, unsigned dpi, unsigned channels, + ScanMethod scan_method) +{ + DBG_HELPER_ARGS(dbg, "dpi: %d, channels: %d, scan_method: %d", dpi, channels, + static_cast<unsigned>(scan_method)); + for (auto& sensor : *s_sensors) { + if (dev->model->sensor_id == sensor.sensor_id && sensor.resolutions.matches(dpi) && + sensor.matches_channel_count(channels) && sensor.method == scan_method) + { + return &sensor; + } + } + return nullptr; +} + +bool sanei_genesys_has_sensor(Genesys_Device* dev, unsigned dpi, unsigned channels, + ScanMethod scan_method) +{ + DBG_HELPER_ARGS(dbg, "dpi: %d, channels: %d, scan_method: %d", dpi, channels, + static_cast<unsigned>(scan_method)); + return find_sensor_impl(dev, dpi, channels, scan_method) != nullptr; +} + +const Genesys_Sensor& sanei_genesys_find_sensor(Genesys_Device* dev, unsigned dpi, unsigned channels, + ScanMethod scan_method) +{ + DBG_HELPER_ARGS(dbg, "dpi: %d, channels: %d, scan_method: %d", dpi, channels, + static_cast<unsigned>(scan_method)); + const auto* sensor = find_sensor_impl(dev, dpi, channels, scan_method); + if (sensor) + return *sensor; + throw std::runtime_error("Given device does not have sensor defined"); +} + +Genesys_Sensor& sanei_genesys_find_sensor_for_write(Genesys_Device* dev, unsigned dpi, + unsigned channels, + ScanMethod scan_method) +{ + DBG_HELPER_ARGS(dbg, "dpi: %d, channels: %d, scan_method: %d", dpi, channels, + static_cast<unsigned>(scan_method)); + auto* sensor = find_sensor_impl(dev, dpi, channels, scan_method); + if (sensor) + return *sensor; + throw std::runtime_error("Given device does not have sensor defined"); +} + + +std::vector<std::reference_wrapper<const Genesys_Sensor>> + sanei_genesys_find_sensors_all(Genesys_Device* dev, ScanMethod scan_method) +{ + DBG_HELPER_ARGS(dbg, "scan_method: %d", static_cast<unsigned>(scan_method)); + std::vector<std::reference_wrapper<const Genesys_Sensor>> ret; + for (const Genesys_Sensor& sensor : sanei_genesys_find_sensors_all_for_write(dev, scan_method)) { + ret.push_back(sensor); + } + return ret; +} + +std::vector<std::reference_wrapper<Genesys_Sensor>> + sanei_genesys_find_sensors_all_for_write(Genesys_Device* dev, ScanMethod scan_method) +{ + DBG_HELPER_ARGS(dbg, "scan_method: %d", static_cast<unsigned>(scan_method)); + std::vector<std::reference_wrapper<Genesys_Sensor>> ret; + for (auto& sensor : *s_sensors) { + if (dev->model->sensor_id == sensor.sensor_id && sensor.method == scan_method) { + ret.push_back(sensor); + } + } + return ret; +} + +void sanei_genesys_init_structs (Genesys_Device * dev) +{ + DBG_HELPER(dbg); + + bool gpo_ok = false; + bool motor_ok = false; + bool fe_ok = false; + + /* initialize the GPO data stuff */ + for (const auto& gpo : *s_gpo) { + if (dev->model->gpio_id == gpo.id) { + dev->gpo = gpo; + gpo_ok = true; + break; + } + } + + // initialize the motor data stuff + for (const auto& motor : *s_motors) { + if (dev->model->motor_id == motor.id) { + dev->motor = motor; + motor_ok = true; + break; + } + } + + for (const auto& frontend : *s_frontends) { + if (dev->model->adc_id == frontend.id) { + dev->frontend_initial = frontend; + dev->frontend = frontend; + fe_ok = true; + break; + } + } + + if (!motor_ok || !gpo_ok || !fe_ok) { + throw SaneException("bad description(s) for fe/gpo/motor=%d/%d/%d\n", + static_cast<unsigned>(dev->model->sensor_id), + static_cast<unsigned>(dev->model->gpio_id), + static_cast<unsigned>(dev->model->motor_id)); + } +} + +/* Generate slope table for motor movement */ +/** + * This function generates a slope table using the slope from the motor struct + * truncated at the given exposure time or step count, whichever comes first. + * The summed time of the acceleration steps is returned, and the + * number of accerelation steps is put into used_steps. + * + * @param dev Device struct + * @param slope_table Table to write to + * @param step_type Generate table for this step_type. 0=>full, 1=>half, + * 2=>quarter + * @param exposure_time Minimum exposure time of a scan line + * @param yres Resolution of a scan line + * @param used_steps Final number of steps is stored here + * @return Motor slope table + * @note all times in pixel time + */ +MotorSlopeTable sanei_genesys_create_slope_table3(AsicType asic_type, const Genesys_Motor& motor, + StepType step_type, int exposure_time, + unsigned yres) +{ + unsigned target_speed_w = (exposure_time * yres) / motor.base_ydpi; + + return create_slope_table(motor.get_slope(step_type), target_speed_w, step_type, 1, 1, + get_slope_table_max_size(asic_type)); +} + +/** @brief computes gamma table + * Generates a gamma table of the given length within 0 and the given + * maximum value + * @param gamma_table gamma table to fill + * @param size size of the table + * @param maximum value allowed for gamma + * @param gamma_max maximum gamma value + * @param gamma gamma to compute values + * @return a gamma table filled with the computed values + * */ +void +sanei_genesys_create_gamma_table (std::vector<uint16_t>& gamma_table, int size, + float maximum, float gamma_max, float gamma) +{ + gamma_table.clear(); + gamma_table.resize(size, 0); + + int i; + float value; + + DBG(DBG_proc, "%s: size = %d, ""maximum = %g, gamma_max = %g, gamma = %g\n", __func__, size, + maximum, gamma_max, gamma); + for (i = 0; i < size; i++) + { + value = static_cast<float>(gamma_max * std::pow(static_cast<double>(i) / size, 1.0 / gamma)); + if (value > maximum) { + value = maximum; + } + gamma_table[i] = static_cast<std::uint16_t>(value); + } + DBG(DBG_proc, "%s: completed\n", __func__); +} + +void sanei_genesys_create_default_gamma_table(Genesys_Device* dev, + std::vector<uint16_t>& gamma_table, float gamma) +{ + int size = 0; + int max = 0; + if (dev->model->asic_type == AsicType::GL646) { + if (dev->model->flags & GENESYS_FLAG_14BIT_GAMMA) { + size = 16384; + } else { + size = 4096; + } + max = size - 1; + } else if (dev->model->asic_type == AsicType::GL124 || + dev->model->asic_type == AsicType::GL846 || + dev->model->asic_type == AsicType::GL847) { + size = 257; + max = 65535; + } else { + size = 256; + max = 65535; + } + sanei_genesys_create_gamma_table(gamma_table, size, max, max, gamma); +} + +/* computes the exposure_time on the basis of the given vertical dpi, + the number of pixels the ccd needs to send, + the step_type and the corresponding maximum speed from the motor struct */ +/* + Currently considers maximum motor speed at given step_type, minimum + line exposure needed for conversion and led exposure time. + + TODO: Should also consider maximum transfer rate: ~6.5MB/s. + Note: The enhance option of the scanners does _not_ help. It only halves + the amount of pixels transfered. + */ +SANE_Int sanei_genesys_exposure_time2(Genesys_Device * dev, float ydpi, + StepType step_type, int endpixel, int exposure_by_led) +{ + int exposure_by_ccd = endpixel + 32; + unsigned max_speed_motor_w = dev->motor.get_slope(step_type).max_speed_w; + int exposure_by_motor = static_cast<int>((max_speed_motor_w * dev->motor.base_ydpi) / ydpi); + + int exposure = exposure_by_ccd; + + if (exposure < exposure_by_motor) + exposure = exposure_by_motor; + + if (exposure < exposure_by_led && dev->model->is_cis) + exposure = exposure_by_led; + + DBG(DBG_info, "%s: ydpi=%d, step=%d, endpixel=%d led=%d => exposure=%d\n", __func__, + static_cast<int>(ydpi), static_cast<unsigned>(step_type), endpixel, + exposure_by_led, exposure); + return exposure; +} + + +/* Sends a block of shading information to the scanner. + The data is placed at address 0x0000 for color mode, gray mode and + unconditionally for the following CCD chips: HP2300, HP2400 and HP5345 + In the other cases (lineart, halftone on ccd chips not mentioned) the + addresses are 0x2a00 for dpihw==0, 0x5500 for dpihw==1 and 0xa800 for + dpihw==2. //Note: why this? + + The data needs to be of size "size", and in little endian byte order. + */ +static void genesys_send_offset_and_shading(Genesys_Device* dev, const Genesys_Sensor& sensor, + uint8_t* data, int size) +{ + DBG_HELPER_ARGS(dbg, "(size = %d)", size); + int dpihw; + int start_address; + + /* ASIC higher than gl843 doesn't have register 2A/2B, so we route to + * a per ASIC shading data loading function if available. + * It is also used for scanners using SHDAREA */ + if (dev->cmd_set->has_send_shading_data()) { + dev->cmd_set->send_shading_data(dev, sensor, data, size); + return; + } + + /* gl646, gl84[123] case */ + dpihw = dev->reg.get8(0x05) >> 6; + + /* TODO invert the test so only the 2 models behaving like that are + * tested instead of adding all the others */ + /* many scanners send coefficient for lineart/gray like in color mode */ + if ((dev->settings.scan_mode == ScanColorMode::LINEART || + dev->settings.scan_mode == ScanColorMode::HALFTONE) + && dev->model->sensor_id != SensorId::CCD_PLUSTEK_OPTICBOOK_3800 + && dev->model->sensor_id != SensorId::CCD_KVSS080 + && dev->model->sensor_id != SensorId::CCD_G4050 + && dev->model->sensor_id != SensorId::CCD_HP_4850C + && dev->model->sensor_id != SensorId::CCD_CANON_4400F + && dev->model->sensor_id != SensorId::CCD_CANON_8400F + && dev->model->sensor_id != SensorId::CCD_CANON_8600F + && dev->model->sensor_id != SensorId::CCD_DSMOBILE600 + && dev->model->sensor_id != SensorId::CCD_XP300 + && dev->model->sensor_id != SensorId::CCD_DP665 + && dev->model->sensor_id != SensorId::CCD_DP685 + && dev->model->sensor_id != SensorId::CIS_CANON_LIDE_80 + && dev->model->sensor_id != SensorId::CCD_ROADWARRIOR + && dev->model->sensor_id != SensorId::CCD_HP2300 + && dev->model->sensor_id != SensorId::CCD_HP2400 + && dev->model->sensor_id != SensorId::CCD_HP3670 + && dev->model->sensor_id != SensorId::CCD_5345) /* lineart, halftone */ + { + if (dpihw == 0) { /* 600 dpi */ + start_address = 0x02a00; + } else if (dpihw == 1) { /* 1200 dpi */ + start_address = 0x05500; + } else if (dpihw == 2) { /* 2400 dpi */ + start_address = 0x0a800; + } else { /* reserved */ + throw SaneException("unknown dpihw"); + } + } + else { // color + start_address = 0x00; + } + + dev->interface->write_buffer(0x3c, start_address, data, size); +} + +// ? +void sanei_genesys_init_shading_data(Genesys_Device* dev, const Genesys_Sensor& sensor, + int pixels_per_line) +{ + DBG_HELPER_ARGS(dbg, "pixels_per_line: %d", pixels_per_line); + + if (dev->model->flags & GENESYS_FLAG_CALIBRATION_HOST_SIDE) { + return; + } + + int channels; + int i; + + if (dev->cmd_set->has_send_shading_data()) { + return; + } + + DBG(DBG_proc, "%s (pixels_per_line = %d)\n", __func__, pixels_per_line); + + // BUG: GRAY shouldn't probably be in the if condition below. Discovered when refactoring + if (dev->settings.scan_mode == ScanColorMode::GRAY || + dev->settings.scan_mode == ScanColorMode::COLOR_SINGLE_PASS) + { + channels = 3; + } else { + channels = 1; + } + + // 16 bit black, 16 bit white + std::vector<uint8_t> shading_data(pixels_per_line * 4 * channels, 0); + + uint8_t* shading_data_ptr = shading_data.data(); + + for (i = 0; i < pixels_per_line * channels; i++) + { + *shading_data_ptr++ = 0x00; /* dark lo */ + *shading_data_ptr++ = 0x00; /* dark hi */ + *shading_data_ptr++ = 0x00; /* white lo */ + *shading_data_ptr++ = 0x40; /* white hi -> 0x4000 */ + } + + genesys_send_offset_and_shading(dev, sensor, shading_data.data(), + pixels_per_line * 4 * channels); +} + + +// Find the position of the reference point: takes gray level 8 bits data and find +// first CCD usable pixel and top of scanning area +void sanei_genesys_search_reference_point(Genesys_Device* dev, Genesys_Sensor& sensor, + const uint8_t* src_data, int start_pixel, int dpi, + int width, int height) +{ + DBG_HELPER(dbg); + int x, y; + int current, left, top = 0; + int size, count; + int level = 80; /* edge threshold level */ + + // sanity check + if ((width < 3) || (height < 3)) { + throw SaneException("invalid width or height"); + } + + /* transformed image data */ + size = width * height; + std::vector<uint8_t> image2(size, 0); + std::vector<uint8_t> image(size, 0); + + /* laplace filter to denoise picture */ + std::memcpy(image2.data(), src_data, size); + std::memcpy(image.data(), src_data, size); // to initialize unprocessed part of the image buffer + + for (y = 1; y < height - 1; y++) { + for (x = 1; x < width - 1; x++) { + image[y * width + x] = + (image2[(y - 1) * width + x + 1] + 2 * image2[(y - 1) * width + x] + + image2[(y - 1) * width + x - 1] + 2 * image2[y * width + x + 1] + + 4 * image2[y * width + x] + 2 * image2[y * width + x - 1] + + image2[(y + 1) * width + x + 1] + 2 * image2[(y + 1) * width + x] + + image2[(y + 1) * width + x - 1]) / 16; + } + } + + image2 = image; + if (DBG_LEVEL >= DBG_data) + sanei_genesys_write_pnm_file("gl_laplace.pnm", image.data(), 8, 1, width, height); + + /* apply X direction sobel filter + -1 0 1 + -2 0 2 + -1 0 1 + and finds threshold level + */ + level = 0; + for (y = 2; y < height - 2; y++) { + for (x = 2; x < width - 2; x++) { + current = image2[(y - 1) * width + x + 1] - image2[(y - 1) * width + x - 1] + + 2 * image2[y * width + x + 1] - 2 * image2[y * width + x - 1] + + image2[(y + 1) * width + x + 1] - image2[(y + 1) * width + x - 1]; + if (current < 0) + current = -current; + if (current > 255) + current = 255; + image[y * width + x] = current; + if (current > level) + level = current; + } + } + if (DBG_LEVEL >= DBG_data) + sanei_genesys_write_pnm_file("gl_xsobel.pnm", image.data(), 8, 1, width, height); + + /* set up detection level */ + level = level / 3; + + /* find left black margin first + todo: search top before left + we average the result of N searches */ + left = 0; + count = 0; + for (y = 2; y < 11; y++) + { + x = 8; + while ((x < width / 2) && (image[y * width + x] < level)) + { + image[y * width + x] = 255; + x++; + } + count++; + left += x; + } + if (DBG_LEVEL >= DBG_data) + sanei_genesys_write_pnm_file("gl_detected-xsobel.pnm", image.data(), 8, 1, width, height); + left = left / count; + + // turn it in CCD pixel at full sensor optical resolution + sensor.ccd_start_xoffset = start_pixel + (left * sensor.optical_res) / dpi; + + /* find top edge by detecting black strip */ + /* apply Y direction sobel filter + -1 -2 -1 + 0 0 0 + 1 2 1 + */ + level = 0; + for (y = 2; y < height - 2; y++) { + for (x = 2; x < width - 2; x++) { + current = -image2[(y - 1) * width + x + 1] - 2 * image2[(y - 1) * width + x] - + image2[(y - 1) * width + x - 1] + image2[(y + 1) * width + x + 1] + + 2 * image2[(y + 1) * width + x] + image2[(y + 1) * width + x - 1]; + if (current < 0) + current = -current; + if (current > 255) + current = 255; + image[y * width + x] = current; + if (current > level) + level = current; + } + } + if (DBG_LEVEL >= DBG_data) + sanei_genesys_write_pnm_file("gl_ysobel.pnm", image.data(), 8, 1, width, height); + + /* set up detection level */ + level = level / 3; + + /* search top of horizontal black stripe : TODO yet another flag */ + if (dev->model->sensor_id == SensorId::CCD_5345 + && dev->model->motor_id == MotorId::MD_5345) + { + top = 0; + count = 0; + for (x = width / 2; x < width - 1; x++) + { + y = 2; + while ((y < height) && (image[x + y * width] < level)) + { + image[y * width + x] = 255; + y++; + } + count++; + top += y; + } + if (DBG_LEVEL >= DBG_data) + sanei_genesys_write_pnm_file("gl_detected-ysobel.pnm", image.data(), 8, 1, width, height); + top = top / count; + + /* bottom of black stripe is of fixed witdh, this hardcoded value + * will be moved into device struct if more such values are needed */ + top += 10; + dev->model->y_offset_calib_white = (top * MM_PER_INCH) / dpi; + DBG(DBG_info, "%s: black stripe y_offset = %f mm \n", __func__, + dev->model->y_offset_calib_white.value()); + } + + /* find white corner in dark area : TODO yet another flag */ + if ((dev->model->sensor_id == SensorId::CCD_HP2300 && dev->model->motor_id == MotorId::HP2300) || + (dev->model->sensor_id == SensorId::CCD_HP2400 && dev->model->motor_id == MotorId::HP2400) || + (dev->model->sensor_id == SensorId::CCD_HP3670 && dev->model->motor_id == MotorId::HP3670)) + { + top = 0; + count = 0; + for (x = 10; x < 60; x++) + { + y = 2; + while ((y < height) && (image[x + y * width] < level)) + y++; + top += y; + count++; + } + top = top / count; + dev->model->y_offset_calib_white = (top * MM_PER_INCH) / dpi; + DBG(DBG_info, "%s: white corner y_offset = %f mm\n", __func__, + dev->model->y_offset_calib_white.value()); + } + + DBG(DBG_proc, "%s: ccd_start_xoffset = %d, left = %d, top = %d\n", __func__, + sensor.ccd_start_xoffset, left, top); +} + +namespace gl843 { + void gl843_park_xpa_lamp(Genesys_Device* dev); + void gl843_set_xpa_motor_power(Genesys_Device* dev, Genesys_Register_Set& regs, bool set); +} // namespace gl843 + +namespace gl124 { + void gl124_setup_scan_gpio(Genesys_Device* dev, int resolution); +} // namespace gl124 + +void scanner_clear_scan_and_feed_counts(Genesys_Device& dev) +{ + switch (dev.model->asic_type) { + case AsicType::GL843: { + dev.interface->write_register(gl843::REG_0x0D, + gl843::REG_0x0D_CLRLNCNT | gl843::REG_0x0D_CLRMCNT); + break; + } + case AsicType::GL845: + case AsicType::GL846: { + dev.interface->write_register(gl846::REG_0x0D, + gl846::REG_0x0D_CLRLNCNT | gl846::REG_0x0D_CLRMCNT); + break; + } + case AsicType::GL847:{ + dev.interface->write_register(gl847::REG_0x0D, + gl847::REG_0x0D_CLRLNCNT | gl847::REG_0x0D_CLRMCNT); + break; + } + case AsicType::GL124:{ + dev.interface->write_register(gl124::REG_0x0D, + gl124::REG_0x0D_CLRLNCNT | gl124::REG_0x0D_CLRMCNT); + break; + } + default: + throw SaneException("Unsupported asic type"); + } +} + +void scanner_clear_scan_and_feed_counts2(Genesys_Device& dev) +{ + // FIXME: switch to scanner_clear_scan_and_feed_counts when updating tests + switch (dev.model->asic_type) { + case AsicType::GL843: { + dev.interface->write_register(gl843::REG_0x0D, gl843::REG_0x0D_CLRLNCNT); + dev.interface->write_register(gl843::REG_0x0D, gl843::REG_0x0D_CLRMCNT); + break; + } + case AsicType::GL845: + case AsicType::GL846: { + dev.interface->write_register(gl846::REG_0x0D, gl846::REG_0x0D_CLRLNCNT); + dev.interface->write_register(gl846::REG_0x0D, gl846::REG_0x0D_CLRMCNT); + break; + } + case AsicType::GL847: { + dev.interface->write_register(gl847::REG_0x0D, gl847::REG_0x0D_CLRLNCNT); + dev.interface->write_register(gl847::REG_0x0D, gl847::REG_0x0D_CLRMCNT); + break; + } + case AsicType::GL124: { + dev.interface->write_register(gl124::REG_0x0D, gl124::REG_0x0D_CLRLNCNT); + dev.interface->write_register(gl124::REG_0x0D, gl124::REG_0x0D_CLRMCNT); + break; + } + default: + throw SaneException("Unsupported asic type"); + } +} + +bool scanner_is_motor_stopped(Genesys_Device& dev) +{ + switch (dev.model->asic_type) { + case AsicType::GL646: { + auto status = scanner_read_status(dev); + return !status.is_motor_enabled && status.is_feeding_finished; + } + case AsicType::GL841: { + auto reg = dev.interface->read_register(gl841::REG_0x40); + + return (!(reg & gl841::REG_0x40_DATAENB) && !(reg & gl841::REG_0x40_MOTMFLG)); + } + case AsicType::GL843: { + auto status = scanner_read_status(dev); + auto reg = dev.interface->read_register(gl843::REG_0x40); + + return (!(reg & gl843::REG_0x40_DATAENB) && !(reg & gl843::REG_0x40_MOTMFLG) && + !status.is_motor_enabled); + } + case AsicType::GL845: + case AsicType::GL846: { + auto status = scanner_read_status(dev); + auto reg = dev.interface->read_register(gl846::REG_0x40); + + return (!(reg & gl846::REG_0x40_DATAENB) && !(reg & gl846::REG_0x40_MOTMFLG) && + !status.is_motor_enabled); + } + case AsicType::GL847: { + auto status = scanner_read_status(dev); + auto reg = dev.interface->read_register(gl847::REG_0x40); + + return (!(reg & gl847::REG_0x40_DATAENB) && !(reg & gl847::REG_0x40_MOTMFLG) && + !status.is_motor_enabled); + } + case AsicType::GL124: { + auto status = scanner_read_status(dev); + auto reg = dev.interface->read_register(gl124::REG_0x100); + + return (!(reg & gl124::REG_0x100_DATAENB) && !(reg & gl124::REG_0x100_MOTMFLG) && + !status.is_motor_enabled); + } + default: + throw SaneException("Unsupported asic type"); + } +} + +void scanner_stop_action(Genesys_Device& dev) +{ + DBG_HELPER(dbg); + + switch (dev.model->asic_type) { + case AsicType::GL843: + case AsicType::GL845: + case AsicType::GL846: + case AsicType::GL847: + case AsicType::GL124: + break; + default: + throw SaneException("Unsupported asic type"); + } + + if (dev.cmd_set->needs_update_home_sensor_gpio()) { + dev.cmd_set->update_home_sensor_gpio(dev); + } + + if (scanner_is_motor_stopped(dev)) { + DBG(DBG_info, "%s: already stopped\n", __func__); + return; + } + + scanner_stop_action_no_move(dev, dev.reg); + + if (is_testing_mode()) { + return; + } + + for (unsigned i = 0; i < 10; ++i) { + if (scanner_is_motor_stopped(dev)) { + return; + } + + dev.interface->sleep_ms(100); + } + + throw SaneException(SANE_STATUS_IO_ERROR, "could not stop motor"); +} + +void scanner_stop_action_no_move(Genesys_Device& dev, genesys::Genesys_Register_Set& regs) +{ + switch (dev.model->asic_type) { + case AsicType::GL646: + case AsicType::GL841: + case AsicType::GL843: + case AsicType::GL845: + case AsicType::GL846: + case AsicType::GL847: + case AsicType::GL124: + break; + default: + throw SaneException("Unsupported asic type"); + } + + regs_set_optical_off(dev.model->asic_type, regs); + // same across all supported ASICs + dev.interface->write_register(0x01, regs.get8(0x01)); + + // looks like certain scanners lock up if we try to scan immediately after stopping previous + // action. + dev.interface->sleep_ms(100); +} + +void scanner_move(Genesys_Device& dev, ScanMethod scan_method, unsigned steps, Direction direction) +{ + DBG_HELPER_ARGS(dbg, "steps=%d direction=%d", steps, static_cast<unsigned>(direction)); + + auto local_reg = dev.reg; + + unsigned resolution = dev.model->get_resolution_settings(scan_method).get_min_resolution_y(); + + const auto& sensor = sanei_genesys_find_sensor(&dev, resolution, 3, scan_method); + + bool uses_secondary_head = (scan_method == ScanMethod::TRANSPARENCY || + scan_method == ScanMethod::TRANSPARENCY_INFRARED); + bool uses_secondary_pos = uses_secondary_head && + dev.model->default_method == ScanMethod::FLATBED; + + if (!dev.is_head_pos_known(ScanHeadId::PRIMARY)) { + throw SaneException("Unknown head position"); + } + if (uses_secondary_pos && !dev.is_head_pos_known(ScanHeadId::SECONDARY)) { + throw SaneException("Unknown head position"); + } + if (direction == Direction::BACKWARD && steps > dev.head_pos(ScanHeadId::PRIMARY)) { + throw SaneException("Trying to feed behind the home position %d %d", + steps, dev.head_pos(ScanHeadId::PRIMARY)); + } + if (uses_secondary_pos && direction == Direction::BACKWARD && + steps > dev.head_pos(ScanHeadId::SECONDARY)) + { + throw SaneException("Trying to feed behind the home position %d %d", + steps, dev.head_pos(ScanHeadId::SECONDARY)); + } + + ScanSession session; + session.params.xres = resolution; + session.params.yres = resolution; + session.params.startx = 0; + session.params.starty = steps; + session.params.pixels = 100; + session.params.lines = 3; + session.params.depth = 8; + session.params.channels = 3; + session.params.scan_method = scan_method; + session.params.scan_mode = ScanColorMode::COLOR_SINGLE_PASS; + if (dev.model->asic_type == AsicType::GL843) { + session.params.color_filter = ColorFilter::RED; + } else { + session.params.color_filter = dev.settings.color_filter; + } + session.params.flags = ScanFlag::DISABLE_SHADING | + ScanFlag::DISABLE_GAMMA | + ScanFlag::FEEDING | + ScanFlag::IGNORE_LINE_DISTANCE; + + if (dev.model->asic_type == AsicType::GL124) { + session.params.flags |= ScanFlag::DISABLE_BUFFER_FULL_MOVE; + } + + if (direction == Direction::BACKWARD) { + session.params.flags |= ScanFlag::REVERSE; + } + + compute_session(&dev, session, sensor); + + dev.cmd_set->init_regs_for_scan_session(&dev, sensor, &local_reg, session); + + if (dev.model->asic_type != AsicType::GL843) { + regs_set_exposure(dev.model->asic_type, local_reg, {0, 0, 0}); + } + scanner_clear_scan_and_feed_counts2(dev); + + dev.interface->write_registers(local_reg); + if (uses_secondary_head) { + gl843::gl843_set_xpa_motor_power(&dev, local_reg, true); + } + + try { + scanner_start_action(dev, true); + } catch (...) { + catch_all_exceptions(__func__, [&]() { + gl843::gl843_set_xpa_motor_power(&dev, local_reg, false); + }); + catch_all_exceptions(__func__, [&]() { scanner_stop_action(dev); }); + // restore original registers + catch_all_exceptions(__func__, [&]() { dev.interface->write_registers(dev.reg); }); + throw; + } + + if (is_testing_mode()) { + dev.interface->test_checkpoint("feed"); + + dev.advance_head_pos_by_steps(ScanHeadId::PRIMARY, direction, steps); + if (uses_secondary_pos) { + dev.advance_head_pos_by_steps(ScanHeadId::SECONDARY, direction, steps); + } + + // FIXME: why don't we stop the scanner like on other ASICs + if (dev.model->asic_type != AsicType::GL843) { + scanner_stop_action(dev); + } + if (uses_secondary_head) { + gl843::gl843_set_xpa_motor_power(&dev, local_reg, false); + } + return; + } + + // wait until feed count reaches the required value + // FIXME: should porbably wait for some timeout + Status status; + for (unsigned i = 0;; ++i) { + status = scanner_read_status(dev); + if (status.is_feeding_finished || ( + direction == Direction::BACKWARD && status.is_at_home)) + { + break; + } + dev.interface->sleep_ms(10); + } + + // FIXME: why don't we stop the scanner like on other ASICs + if (dev.model->asic_type != AsicType::GL843) { + scanner_stop_action(dev); + } + if (uses_secondary_head) { + gl843::gl843_set_xpa_motor_power(&dev, local_reg, false); + } + + dev.advance_head_pos_by_steps(ScanHeadId::PRIMARY, direction, steps); + if (uses_secondary_pos) { + dev.advance_head_pos_by_steps(ScanHeadId::SECONDARY, direction, steps); + } + + // looks like certain scanners lock up if we scan immediately after feeding + dev.interface->sleep_ms(100); +} + +void scanner_move_back_home(Genesys_Device& dev, bool wait_until_home) +{ + DBG_HELPER_ARGS(dbg, "wait_until_home = %d", wait_until_home); + + switch (dev.model->asic_type) { + case AsicType::GL843: + case AsicType::GL845: + case AsicType::GL846: + case AsicType::GL847: + case AsicType::GL124: + break; + default: + throw SaneException("Unsupported asic type"); + } + + // FIXME: also check whether the scanner actually has a secondary head + if (!dev.is_head_pos_known(ScanHeadId::SECONDARY) || + dev.head_pos(ScanHeadId::SECONDARY) > 0 || + dev.settings.scan_method == ScanMethod::TRANSPARENCY || + dev.settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED) + { + scanner_move_back_home_ta(dev); + } + + if (dev.is_head_pos_known(ScanHeadId::PRIMARY) && + dev.head_pos(ScanHeadId::PRIMARY) > 1000) + { + // leave 500 steps for regular slow back home + scanner_move(dev, dev.model->default_method, dev.head_pos(ScanHeadId::PRIMARY) - 500, + Direction::BACKWARD); + } + + if (dev.cmd_set->needs_update_home_sensor_gpio()) { + dev.cmd_set->update_home_sensor_gpio(dev); + } + + auto status = scanner_read_reliable_status(dev); + + if (status.is_at_home) { + dbg.log(DBG_info, "already at home"); + dev.set_head_pos_zero(ScanHeadId::PRIMARY); + return; + } + + if (dev.model->model_id == ModelId::CANON_LIDE_210) { + // move the head back a little first + if (dev.is_head_pos_known(ScanHeadId::PRIMARY) && + dev.head_pos(ScanHeadId::PRIMARY) > 30) + { + scanner_move(dev, dev.model->default_method, 20, Direction::BACKWARD); + } + } + + Genesys_Register_Set local_reg = dev.reg; + unsigned resolution = sanei_genesys_get_lowest_ydpi(&dev); + + const auto& sensor = sanei_genesys_find_sensor(&dev, resolution, 1, dev.model->default_method); + + ScanSession session; + session.params.xres = resolution; + session.params.yres = resolution; + session.params.startx = 100; + if (dev.model->asic_type == AsicType::GL843) { + session.params.starty = 40000; + } else { + session.params.starty = 30000; + } + session.params.pixels = 100; + session.params.lines = 100; + session.params.depth = 8; + session.params.channels = 1; + session.params.scan_method = dev.settings.scan_method; + if (dev.model->asic_type == AsicType::GL843) { + session.params.scan_mode = ScanColorMode::LINEART; + session.params.color_filter = dev.settings.color_filter; + } else { + session.params.scan_mode = ScanColorMode::GRAY; + session.params.color_filter = ColorFilter::RED; + } + session.params.flags = ScanFlag::DISABLE_SHADING | + ScanFlag::DISABLE_GAMMA | + ScanFlag::IGNORE_LINE_DISTANCE | + ScanFlag::REVERSE; + if (dev.model->asic_type == AsicType::GL843) { + session.params.flags |= ScanFlag::DISABLE_BUFFER_FULL_MOVE; + } + + compute_session(&dev, session, sensor); + + dev.cmd_set->init_regs_for_scan_session(&dev, sensor, &local_reg, session); + + scanner_clear_scan_and_feed_counts(dev); + + dev.interface->write_registers(local_reg); + + if (dev.model->asic_type == AsicType::GL124) { + gl124::gl124_setup_scan_gpio(&dev, resolution); + } + + try { + scanner_start_action(dev, true); + } catch (...) { + catch_all_exceptions(__func__, [&]() { scanner_stop_action(dev); }); + // restore original registers + catch_all_exceptions(__func__, [&]() + { + dev.interface->write_registers(dev.reg); + }); + throw; + } + + if (dev.cmd_set->needs_update_home_sensor_gpio()) { + dev.cmd_set->update_home_sensor_gpio(dev); + } + + if (is_testing_mode()) { + dev.interface->test_checkpoint("move_back_home"); + dev.set_head_pos_zero(ScanHeadId::PRIMARY); + return; + } + + if (wait_until_home) { + for (unsigned i = 0; i < 300; ++i) { + auto status = scanner_read_status(dev); + + if (status.is_at_home) { + dbg.log(DBG_info, "reached home position"); + if (dev.model->asic_type == AsicType::GL846 || + dev.model->asic_type == AsicType::GL847) + { + scanner_stop_action(dev); + } + dev.set_head_pos_zero(ScanHeadId::PRIMARY); + return; + } + + dev.interface->sleep_ms(100); + } + + // when we come here then the scanner needed too much time for this, so we better stop + // the motor + catch_all_exceptions(__func__, [&](){ scanner_stop_action(dev); }); + dev.set_head_pos_unknown(); + throw SaneException(SANE_STATUS_IO_ERROR, "timeout while waiting for scanhead to go home"); + } + dbg.log(DBG_info, "scanhead is still moving"); +} + +void scanner_move_back_home_ta(Genesys_Device& dev) +{ + DBG_HELPER(dbg); + + switch (dev.model->asic_type) { + case AsicType::GL843: + break; + default: + throw SaneException("Unsupported asic type"); + } + + Genesys_Register_Set local_reg = dev.reg; + + auto scan_method = ScanMethod::TRANSPARENCY; + unsigned resolution = dev.model->get_resolution_settings(scan_method).get_min_resolution_y(); + + const auto& sensor = sanei_genesys_find_sensor(&dev, resolution, 1, scan_method); + + if (dev.is_head_pos_known(ScanHeadId::SECONDARY) && + dev.head_pos(ScanHeadId::SECONDARY) > 1000) + { + // leave 500 steps for regular slow back home + scanner_move(dev, scan_method, dev.head_pos(ScanHeadId::SECONDARY) - 500, + Direction::BACKWARD); + } + + ScanSession session; + session.params.xres = resolution; + session.params.yres = resolution; + session.params.startx = 100; + session.params.starty = 30000; + session.params.pixels = 100; + session.params.lines = 100; + session.params.depth = 8; + session.params.channels = 1; + session.params.scan_method = scan_method; + session.params.scan_mode = ScanColorMode::GRAY; + session.params.color_filter = ColorFilter::RED; + session.params.flags = ScanFlag::DISABLE_SHADING | + ScanFlag::DISABLE_GAMMA | + ScanFlag::IGNORE_LINE_DISTANCE | + ScanFlag::REVERSE; + + compute_session(&dev, session, sensor); + + dev.cmd_set->init_regs_for_scan_session(&dev, sensor, &local_reg, session); + + scanner_clear_scan_and_feed_counts(dev); + + dev.interface->write_registers(local_reg); + gl843::gl843_set_xpa_motor_power(&dev, local_reg, true); + + try { + scanner_start_action(dev, true); + } catch (...) { + catch_all_exceptions(__func__, [&]() { scanner_stop_action(dev); }); + // restore original registers + catch_all_exceptions(__func__, [&]() { dev.interface->write_registers(dev.reg); }); + throw; + } + + if (is_testing_mode()) { + dev.interface->test_checkpoint("move_back_home_ta"); + + if (dev.is_head_pos_known(ScanHeadId::PRIMARY)) { + if (dev.head_pos(ScanHeadId::PRIMARY) > dev.head_pos(ScanHeadId::SECONDARY)) { + dev.advance_head_pos_by_steps(ScanHeadId::PRIMARY, Direction::BACKWARD, + dev.head_pos(ScanHeadId::SECONDARY)); + } else { + dev.set_head_pos_zero(ScanHeadId::PRIMARY); + } + dev.set_head_pos_zero(ScanHeadId::SECONDARY); + } + + scanner_stop_action(dev); + gl843::gl843_set_xpa_motor_power(&dev, local_reg, false); + return; + } + + for (unsigned i = 0; i < 1200; ++i) { + + auto status = scanner_read_status(dev); + + if (status.is_at_home) { + dbg.log(DBG_info, "TA reached home position"); + + if (dev.is_head_pos_known(ScanHeadId::PRIMARY)) { + if (dev.head_pos(ScanHeadId::PRIMARY) > dev.head_pos(ScanHeadId::SECONDARY)) { + dev.advance_head_pos_by_steps(ScanHeadId::PRIMARY, Direction::BACKWARD, + dev.head_pos(ScanHeadId::SECONDARY)); + } else { + dev.set_head_pos_zero(ScanHeadId::PRIMARY); + } + dev.set_head_pos_zero(ScanHeadId::SECONDARY); + } + + scanner_stop_action(dev); + gl843::gl843_set_xpa_motor_power(&dev, local_reg, false); + return; + } + + dev.interface->sleep_ms(100); + } + + throw SaneException("Timeout waiting for XPA lamp to park"); +} + +void sanei_genesys_calculate_zmod(bool two_table, + uint32_t exposure_time, + const std::vector<uint16_t>& slope_table, + unsigned acceleration_steps, + unsigned move_steps, + unsigned buffer_acceleration_steps, + uint32_t* out_z1, uint32_t* out_z2) +{ + DBG(DBG_info, "%s: two_table=%d\n", __func__, two_table); + + // acceleration total time + unsigned sum = std::accumulate(slope_table.begin(), slope_table.begin() + acceleration_steps, + 0, std::plus<unsigned>()); + + /* Z1MOD: + c = sum(slope_table; reg_stepno) + d = reg_fwdstep * <cruising speed> + Z1MOD = (c+d) % exposure_time + */ + *out_z1 = (sum + buffer_acceleration_steps * slope_table[acceleration_steps - 1]) % exposure_time; + + /* Z2MOD: + a = sum(slope_table; reg_stepno) + b = move_steps or 1 if 2 tables + Z1MOD = (a+b) % exposure_time + */ + if (!two_table) { + sum = sum + (move_steps * slope_table[acceleration_steps - 1]); + } else { + sum = sum + slope_table[acceleration_steps - 1]; + } + *out_z2 = sum % exposure_time; +} + +static uint8_t genesys_adjust_gain(double* applied_multi, double multi, uint8_t gain) +{ + double voltage, original_voltage; + uint8_t new_gain = 0; + + DBG(DBG_proc, "%s: multi=%f, gain=%d\n", __func__, multi, gain); + + voltage = 0.5 + gain * 0.25; + original_voltage = voltage; + + voltage *= multi; + + new_gain = static_cast<std::uint8_t>((voltage - 0.5) * 4); + if (new_gain > 0x0e) + new_gain = 0x0e; + + voltage = 0.5 + (new_gain) * 0.25; + + *applied_multi = voltage / original_voltage; + + DBG(DBG_proc, "%s: orig voltage=%.2f, new voltage=%.2f, *applied_multi=%f, new_gain=%d\n", + __func__, original_voltage, voltage, *applied_multi, new_gain); + + return new_gain; +} + + +// todo: is return status necessary (unchecked?) +static void genesys_average_white(Genesys_Device* dev, Genesys_Sensor& sensor, int channels, + int channel, uint8_t* data, int size, int *max_average) +{ + + DBG_HELPER_ARGS(dbg, "channels=%d, channel=%d, size=%d", channels, channel, size); + int gain_white_ref, sum, range; + int average; + int i; + + range = size / 50; + + if (dev->settings.scan_method == ScanMethod::TRANSPARENCY || + dev->settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED) + { + gain_white_ref = sensor.fau_gain_white_ref * 256; + } else { + gain_white_ref = sensor.gain_white_ref * 256; + } + + if (range < 1) + range = 1; + + size = size / (2 * range * channels); + + data += (channel * 2); + + *max_average = 0; + + while (size--) + { + sum = 0; + for (i = 0; i < range; i++) + { + sum += (*data); + sum += *(data + 1) * 256; + data += (2 * channels); /* byte based */ + } + + average = (sum / range); + if (average > *max_average) + *max_average = average; + } + + DBG(DBG_proc, "%s: max_average=%d, gain_white_ref = %d, finished\n", __func__, *max_average, + gain_white_ref); + + if (*max_average >= gain_white_ref) + throw SaneException(SANE_STATUS_INVAL); +} + +/* todo: understand, values are too high */ +static int +genesys_average_black (Genesys_Device * dev, int channel, + uint8_t * data, int pixels) +{ + int i; + int sum; + int pixel_step; + + DBG(DBG_proc, "%s: channel=%d, pixels=%d\n", __func__, channel, pixels); + + sum = 0; + + if (dev->settings.scan_mode == ScanColorMode::COLOR_SINGLE_PASS) + { + data += (channel * 2); + pixel_step = 3 * 2; + } + else + { + pixel_step = 2; + } + + for (i = 0; i < pixels; i++) + { + sum += *data; + sum += *(data + 1) * 256; + + data += pixel_step; + } + + DBG(DBG_proc, "%s = %d\n", __func__, sum / pixels); + + return sum / pixels; +} + + +// todo: check; it works but the lines 1, 2, and 3 are too dark even with the +// same offset and gain settings? +static void genesys_coarse_calibration(Genesys_Device* dev, Genesys_Sensor& sensor) +{ + DBG_HELPER_ARGS(dbg, "scan_mode = %d", static_cast<unsigned>(dev->settings.scan_mode)); + int black_pixels; + int white_average; + uint8_t offset[4] = { 0xa0, 0x00, 0xa0, 0x40 }; /* first value isn't used */ + uint16_t white[12], dark[12]; + int i, j; + + black_pixels = sensor.black_pixels + * dev->settings.xres / sensor.optical_res; + + unsigned channels = dev->settings.get_channels(); + + DBG(DBG_info, "channels %d y_size %f xres %d\n", channels, dev->model->y_size.value(), + dev->settings.xres); + unsigned size = static_cast<unsigned>(channels * 2 * dev->model->y_size * dev->settings.xres / + MM_PER_INCH); + /* 1 1 mm 1/inch inch/mm */ + + std::vector<uint8_t> calibration_data(size); + std::vector<uint8_t> all_data(size * 4, 1); + + dev->cmd_set->set_fe(dev, sensor, AFE_INIT); + + dev->frontend.set_gain(0, 2); + dev->frontend.set_gain(1, 2); + dev->frontend.set_gain(2, 2); // TODO: ? was 2 + dev->frontend.set_offset(0, offset[0]); + dev->frontend.set_offset(1, offset[0]); + dev->frontend.set_offset(2, offset[0]); + + for (i = 0; i < 4; i++) /* read 4 lines */ + { + if (i < 3) /* first 3 lines */ + { + dev->frontend.set_offset(0, offset[i]); + dev->frontend.set_offset(1, offset[i]); + dev->frontend.set_offset(2, offset[i]); + } + + if (i == 1) /* second line */ + { + double applied_multi; + double gain_white_ref; + + if (dev->settings.scan_method == ScanMethod::TRANSPARENCY || + dev->settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED) + { + gain_white_ref = sensor.fau_gain_white_ref * 256; + } else { + gain_white_ref = sensor.gain_white_ref * 256; + } + + // white and black are defined downwards + + uint8_t gain0 = genesys_adjust_gain(&applied_multi, + gain_white_ref / (white[0] - dark[0]), + dev->frontend.get_gain(0)); + uint8_t gain1 = genesys_adjust_gain(&applied_multi, + gain_white_ref / (white[1] - dark[1]), + dev->frontend.get_gain(1)); + uint8_t gain2 = genesys_adjust_gain(&applied_multi, + gain_white_ref / (white[2] - dark[2]), + dev->frontend.get_gain(2)); + // FIXME: looks like overwritten data. Are the above calculations doing + // anything at all? + dev->frontend.set_gain(0, gain0); + dev->frontend.set_gain(1, gain1); + dev->frontend.set_gain(2, gain2); + dev->frontend.set_gain(0, 2); + dev->frontend.set_gain(1, 2); + dev->frontend.set_gain(2, 2); + + dev->interface->write_fe_register(0x28, dev->frontend.get_gain(0)); + dev->interface->write_fe_register(0x29, dev->frontend.get_gain(1)); + dev->interface->write_fe_register(0x2a, dev->frontend.get_gain(2)); + } + + if (i == 3) /* last line */ + { + double x, y, rate; + + for (j = 0; j < 3; j++) + { + + x = static_cast<double>(dark[(i - 2) * 3 + j] - + dark[(i - 1) * 3 + j]) * 254 / (offset[i - 1] / 2 - + offset[i - 2] / 2); + y = x - x * (offset[i - 1] / 2) / 254 - dark[(i - 1) * 3 + j]; + rate = (x - DARK_VALUE - y) * 254 / x + 0.5; + + uint8_t curr_offset = static_cast<uint8_t>(rate); + + if (curr_offset > 0x7f) { + curr_offset = 0x7f; + } + curr_offset <<= 1; + dev->frontend.set_offset(j, curr_offset); + } + } + dev->interface->write_fe_register(0x20, dev->frontend.get_offset(0)); + dev->interface->write_fe_register(0x21, dev->frontend.get_offset(1)); + dev->interface->write_fe_register(0x22, dev->frontend.get_offset(2)); + + DBG(DBG_info, + "%s: doing scan: gain: %d/%d/%d, offset: %d/%d/%d\n", __func__, + dev->frontend.get_gain(0), + dev->frontend.get_gain(1), + dev->frontend.get_gain(2), + dev->frontend.get_offset(0), + dev->frontend.get_offset(1), + dev->frontend.get_offset(2)); + + + dev->cmd_set->begin_scan(dev, sensor, &dev->calib_reg, false); + + if (is_testing_mode()) { + dev->interface->test_checkpoint("coarse_calibration"); + dev->cmd_set->end_scan(dev, &dev->calib_reg, true); + return; + } + + sanei_genesys_read_data_from_scanner(dev, calibration_data.data(), size); + std::memcpy(all_data.data() + i * size, calibration_data.data(), size); + if (i == 3) /* last line */ + { + std::vector<uint8_t> all_data_8(size * 4 / 2); + unsigned int count; + + for (count = 0; count < static_cast<unsigned>(size * 4 / 2); count++) { + all_data_8[count] = all_data[count * 2 + 1]; + } + sanei_genesys_write_pnm_file("gl_coarse.pnm", all_data_8.data(), 8, channels, size / 6, 4); + } + + dev->cmd_set->end_scan(dev, &dev->calib_reg, true); + + if (dev->settings.scan_mode == ScanColorMode::COLOR_SINGLE_PASS) + { + for (j = 0; j < 3; j++) + { + genesys_average_white(dev, sensor, 3, j, calibration_data.data(), size, &white_average); + white[i * 3 + j] = white_average; + dark[i * 3 + j] = + genesys_average_black (dev, j, calibration_data.data(), + black_pixels); + DBG(DBG_info, "%s: white[%d]=%d, black[%d]=%d\n", __func__, + i * 3 + j, white[i * 3 + j], i * 3 + j, dark[i * 3 + j]); + } + } + else /* one color-component modes */ + { + genesys_average_white(dev, sensor, 1, 0, calibration_data.data(), size, &white_average); + white[i * 3 + 0] = white[i * 3 + 1] = white[i * 3 + 2] = + white_average; + dark[i * 3 + 0] = dark[i * 3 + 1] = dark[i * 3 + 2] = + genesys_average_black (dev, 0, calibration_data.data(), black_pixels); + } + } /* for (i = 0; i < 4; i++) */ + + DBG(DBG_info, "%s: final: gain: %d/%d/%d, offset: %d/%d/%d\n", __func__, + dev->frontend.get_gain(0), + dev->frontend.get_gain(1), + dev->frontend.get_gain(2), + dev->frontend.get_offset(0), + dev->frontend.get_offset(1), + dev->frontend.get_offset(2)); +} + +/** + * scans a white area with motor and lamp off to get the per CCD pixel offset + * that will be used to compute shading coefficient + * @param dev scanner's device + */ +static void genesys_shading_calibration_impl(Genesys_Device* dev, const Genesys_Sensor& sensor, + std::vector<std::uint16_t>& out_average_data, + bool is_dark, const std::string& log_filename_prefix) +{ + DBG_HELPER(dbg); + + debug_dump(DBG_info, dev->calib_session); + + size_t size; + uint32_t pixels_per_line; + uint8_t channels; + + /* end pixel - start pixel */ + pixels_per_line = dev->calib_pixels; + channels = dev->calib_channels; + + uint32_t out_pixels_per_line = pixels_per_line + dev->calib_pixels_offset; + + // FIXME: we set this during both dark and white calibration. A cleaner approach should + // probably be used + dev->average_size = channels * out_pixels_per_line; + + out_average_data.clear(); + out_average_data.resize(dev->average_size); + + if (is_dark && dev->settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED) { + // FIXME: dark shading currently not supported on infrared transparency scans + return; + } + + // FIXME: the current calculation is likely incorrect on non-GL843 implementations, + // but this needs checking + if (dev->calib_total_bytes_to_read > 0) { + size = dev->calib_total_bytes_to_read; + } else if (dev->model->asic_type == AsicType::GL843) { + size = channels * 2 * pixels_per_line * dev->calib_lines; + } else { + size = channels * 2 * pixels_per_line * (dev->calib_lines + 1); + } + + std::vector<uint16_t> calibration_data(size / 2); + + bool motor = true; + if (dev->model->flags & GENESYS_FLAG_SHADING_NO_MOVE) + { + motor = false; + } + + // turn off motor and lamp power for flatbed scanners, but not for sheetfed scanners + // because they have a calibration sheet with a sufficient black strip + if (is_dark && !dev->model->is_sheetfed) { + sanei_genesys_set_lamp_power(dev, sensor, dev->calib_reg, false); + sanei_genesys_set_motor_power(dev->calib_reg, motor); + } else { + sanei_genesys_set_lamp_power(dev, sensor, dev->calib_reg, true); + sanei_genesys_set_motor_power(dev->calib_reg, motor); + } + + dev->interface->write_registers(dev->calib_reg); + + if (is_dark) { + // wait some time to let lamp to get dark + dev->interface->sleep_ms(200); + } else if (dev->model->flags & GENESYS_FLAG_DARK_CALIBRATION) { + // make sure lamp is bright again + // FIXME: what about scanners that take a long time to warm the lamp? + dev->interface->sleep_ms(500); + } + + bool start_motor = !is_dark; + dev->cmd_set->begin_scan(dev, sensor, &dev->calib_reg, start_motor); + + + if (is_testing_mode()) { + dev->interface->test_checkpoint(is_dark ? "dark_shading_calibration" + : "white_shading_calibration"); + dev->cmd_set->end_scan(dev, &dev->calib_reg, true); + return; + } + + sanei_genesys_read_data_from_scanner(dev, reinterpret_cast<std::uint8_t*>(calibration_data.data()), + size); + + dev->cmd_set->end_scan(dev, &dev->calib_reg, true); + + if (dev->model->flags & GENESYS_FLAG_16BIT_DATA_INVERTED) { + for (std::size_t i = 0; i < size / 2; ++i) { + auto value = calibration_data[i]; + value = ((value >> 8) & 0xff) | ((value << 8) & 0xff00); + calibration_data[i] = value; + } + } + + std::fill(out_average_data.begin(), + out_average_data.begin() + dev->calib_pixels_offset * channels, 0); + + compute_array_percentile_approx(out_average_data.data() + dev->calib_pixels_offset * channels, + calibration_data.data(), + dev->calib_lines, pixels_per_line * channels, + 0.5f); + + if (DBG_LEVEL >= DBG_data) { + sanei_genesys_write_pnm_file16((log_filename_prefix + "_shading.pnm").c_str(), + calibration_data.data(), + channels, pixels_per_line, dev->calib_lines); + sanei_genesys_write_pnm_file16((log_filename_prefix + "_average.pnm").c_str(), + out_average_data.data(), + channels, out_pixels_per_line, 1); + } +} + + +static void genesys_dark_shading_calibration(Genesys_Device* dev, const Genesys_Sensor& sensor) +{ + DBG_HELPER(dbg); + genesys_shading_calibration_impl(dev, sensor, dev->dark_average_data, true, "gl_black_"); +} +/* + * this function builds dummy dark calibration data so that we can + * compute shading coefficient in a clean way + * todo: current values are hardcoded, we have to find if they + * can be computed from previous calibration data (when doing offset + * calibration ?) + */ +static void genesys_dummy_dark_shading(Genesys_Device* dev, const Genesys_Sensor& sensor) +{ + DBG_HELPER(dbg); + uint32_t pixels_per_line; + uint8_t channels; + uint32_t skip, xend; + int dummy1, dummy2, dummy3; /* dummy black average per channel */ + + pixels_per_line = dev->calib_pixels; + channels = dev->calib_channels; + + uint32_t out_pixels_per_line = pixels_per_line + dev->calib_pixels_offset; + + dev->average_size = channels * out_pixels_per_line; + dev->dark_average_data.clear(); + dev->dark_average_data.resize(dev->average_size, 0); + + /* we average values on 'the left' where CCD pixels are under casing and + give darkest values. We then use these as dummy dark calibration */ + if (dev->settings.xres <= sensor.optical_res / 2) + { + skip = 4; + xend = 36; + } + else + { + skip = 4; + xend = 68; + } + if (dev->model->sensor_id==SensorId::CCD_G4050 || + dev->model->sensor_id==SensorId::CCD_HP_4850C + || dev->model->sensor_id==SensorId::CCD_CANON_4400F + || dev->model->sensor_id==SensorId::CCD_CANON_8400F + || dev->model->sensor_id==SensorId::CCD_KVSS080) + { + skip = 2; + xend = sensor.black_pixels; + } + + /* average each channels on half left margin */ + dummy1 = 0; + dummy2 = 0; + dummy3 = 0; + + for (unsigned x = skip + 1; x <= xend; x++) { + dummy1 += dev->white_average_data[channels * x]; + if (channels > 1) { + dummy2 += dev->white_average_data[channels * x + 1]; + dummy3 += dev->white_average_data[channels * x + 2]; + } + } + + dummy1 /= (xend - skip); + if (channels > 1) + { + dummy2 /= (xend - skip); + dummy3 /= (xend - skip); + } + DBG(DBG_proc, "%s: dummy1=%d, dummy2=%d, dummy3=%d \n", __func__, dummy1, dummy2, dummy3); + + /* fill dark_average */ + for (unsigned x = 0; x < out_pixels_per_line; x++) { + dev->dark_average_data[channels * x] = dummy1; + if (channels > 1) { + dev->dark_average_data[channels * x + 1] = dummy2; + dev->dark_average_data[channels * x + 2] = dummy3; + } + } +} + + +static void genesys_repark_sensor_before_shading(Genesys_Device* dev) +{ + DBG_HELPER(dbg); + if (dev->model->flags & GENESYS_FLAG_SHADING_REPARK) { + dev->cmd_set->move_back_home(dev, true); + + if (dev->settings.scan_method == ScanMethod::TRANSPARENCY || + dev->settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED) + { + dev->cmd_set->move_to_ta(dev); + } + } +} + +static void genesys_repark_sensor_after_white_shading(Genesys_Device* dev) +{ + DBG_HELPER(dbg); + if (dev->model->flags & GENESYS_FLAG_SHADING_REPARK) { + dev->cmd_set->move_back_home(dev, true); + } +} + +static void genesys_white_shading_calibration(Genesys_Device* dev, const Genesys_Sensor& sensor) +{ + DBG_HELPER(dbg); + genesys_shading_calibration_impl(dev, sensor, dev->white_average_data, false, "gl_white_"); +} + +// This calibration uses a scan over the calibration target, comprising a black and a white strip. +// (So the motor must be on.) +static void genesys_dark_white_shading_calibration(Genesys_Device* dev, + const Genesys_Sensor& sensor) +{ + DBG_HELPER_ARGS(dbg, "lines = %zu", dev->calib_lines); + size_t size; + uint32_t pixels_per_line; + uint8_t channels; + unsigned int x; + uint32_t dark, white, dark_sum, white_sum, dark_count, white_count, col, + dif; + + pixels_per_line = dev->calib_pixels; + channels = dev->calib_channels; + + uint32_t out_pixels_per_line = pixels_per_line + dev->calib_pixels_offset; + + dev->average_size = channels * out_pixels_per_line; + + dev->white_average_data.clear(); + dev->white_average_data.resize(dev->average_size); + + dev->dark_average_data.clear(); + dev->dark_average_data.resize(dev->average_size); + + if (dev->calib_total_bytes_to_read > 0) + size = dev->calib_total_bytes_to_read; + else + size = channels * 2 * pixels_per_line * dev->calib_lines; + + std::vector<uint8_t> calibration_data(size); + + bool motor = true; + if (dev->model->flags & GENESYS_FLAG_SHADING_NO_MOVE) + { + motor = false; + } + + // turn on motor and lamp power + sanei_genesys_set_lamp_power(dev, sensor, dev->calib_reg, true); + sanei_genesys_set_motor_power(dev->calib_reg, motor); + + dev->interface->write_registers(dev->calib_reg); + + dev->cmd_set->begin_scan(dev, sensor, &dev->calib_reg, false); + + if (is_testing_mode()) { + dev->interface->test_checkpoint("dark_white_shading_calibration"); + dev->cmd_set->end_scan(dev, &dev->calib_reg, true); + return; + } + + sanei_genesys_read_data_from_scanner(dev, calibration_data.data(), size); + + dev->cmd_set->end_scan(dev, &dev->calib_reg, true); + + if (DBG_LEVEL >= DBG_data) + { + if (dev->model->is_cis) + { + sanei_genesys_write_pnm_file("gl_black_white_shading.pnm", calibration_data.data(), + 16, 1, pixels_per_line*channels, + dev->calib_lines); + } + else + { + sanei_genesys_write_pnm_file("gl_black_white_shading.pnm", calibration_data.data(), + 16, channels, pixels_per_line, + dev->calib_lines); + } + } + + + std::fill(dev->dark_average_data.begin(), + dev->dark_average_data.begin() + dev->calib_pixels_offset * channels, 0); + std::fill(dev->white_average_data.begin(), + dev->white_average_data.begin() + dev->calib_pixels_offset * channels, 0); + + uint16_t* average_white = dev->white_average_data.data() + dev->calib_pixels_offset * channels; + uint16_t* average_dark = dev->dark_average_data.data() + dev->calib_pixels_offset * channels; + + for (x = 0; x < pixels_per_line * channels; x++) + { + dark = 0xffff; + white = 0; + + for (std::size_t y = 0; y < dev->calib_lines; y++) + { + col = calibration_data[(x + y * pixels_per_line * channels) * 2]; + col |= + calibration_data[(x + y * pixels_per_line * channels) * 2 + + 1] << 8; + + if (col > white) + white = col; + if (col < dark) + dark = col; + } + + dif = white - dark; + + dark = dark + dif / 8; + white = white - dif / 8; + + dark_count = 0; + dark_sum = 0; + + white_count = 0; + white_sum = 0; + + for (std::size_t y = 0; y < dev->calib_lines; y++) + { + col = calibration_data[(x + y * pixels_per_line * channels) * 2]; + col |= + calibration_data[(x + y * pixels_per_line * channels) * 2 + + 1] << 8; + + if (col >= white) + { + white_sum += col; + white_count++; + } + if (col <= dark) + { + dark_sum += col; + dark_count++; + } + + } + + dark_sum /= dark_count; + white_sum /= white_count; + + *average_dark++ = dark_sum; + *average_white++ = white_sum; + } + + if (DBG_LEVEL >= DBG_data) { + sanei_genesys_write_pnm_file16("gl_white_average.pnm", dev->white_average_data.data(), + channels, out_pixels_per_line, 1); + sanei_genesys_write_pnm_file16("gl_dark_average.pnm", dev->dark_average_data.data(), + channels, out_pixels_per_line, 1); + } +} + +/* computes one coefficient given bright-dark value + * @param coeff factor giving 1.00 gain + * @param target desired target code + * @param value brght-dark value + * */ +static unsigned int +compute_coefficient (unsigned int coeff, unsigned int target, unsigned int value) +{ + int result; + + if (value > 0) + { + result = (coeff * target) / value; + if (result >= 65535) + { + result = 65535; + } + } + else + { + result = coeff; + } + return result; +} + +/** @brief compute shading coefficients for LiDE scanners + * The dark/white shading is actually performed _after_ reducing + * resolution via averaging. only dark/white shading data for what would be + * first pixel at full resolution is used. + * + * scanner raw input to output value calculation: + * o=(i-off)*(gain/coeff) + * + * from datasheet: + * off=dark_average + * gain=coeff*bright_target/(bright_average-dark_average) + * works for dark_target==0 + * + * what we want is these: + * bright_target=(bright_average-off)*(gain/coeff) + * dark_target=(dark_average-off)*(gain/coeff) + * leading to + * off = (dark_average*bright_target - bright_average*dark_target)/(bright_target - dark_target) + * gain = (bright_target - dark_target)/(bright_average - dark_average)*coeff + * + * @param dev scanner's device + * @param shading_data memory area where to store the computed shading coefficients + * @param pixels_per_line number of pixels per line + * @param words_per_color memory words per color channel + * @param channels number of color channels (actually 1 or 3) + * @param o shading coefficients left offset + * @param coeff 4000h or 2000h depending on fast scan mode or not (GAIN4 bit) + * @param target_bright value of the white target code + * @param target_dark value of the black target code +*/ +static void +compute_averaged_planar (Genesys_Device * dev, const Genesys_Sensor& sensor, + uint8_t * shading_data, + unsigned int pixels_per_line, + unsigned int words_per_color, + unsigned int channels, + unsigned int o, + unsigned int coeff, + unsigned int target_bright, + unsigned int target_dark) +{ + unsigned int x, i, j, br, dk, res, avgpixels, basepixels, val; + unsigned int fill,factor; + + DBG(DBG_info, "%s: pixels=%d, offset=%d\n", __func__, pixels_per_line, o); + + /* initialize result */ + memset (shading_data, 0xff, words_per_color * 3 * 2); + + /* + strangely i can write 0x20000 bytes beginning at 0x00000 without overwriting + slope tables - which begin at address 0x10000(for 1200dpi hw mode): + memory is organized in words(2 bytes) instead of single bytes. explains + quite some things + */ +/* + another one: the dark/white shading is actually performed _after_ reducing + resolution via averaging. only dark/white shading data for what would be + first pixel at full resolution is used. + */ +/* + scanner raw input to output value calculation: + o=(i-off)*(gain/coeff) + + from datasheet: + off=dark_average + gain=coeff*bright_target/(bright_average-dark_average) + works for dark_target==0 + + what we want is these: + bright_target=(bright_average-off)*(gain/coeff) + dark_target=(dark_average-off)*(gain/coeff) + leading to + off = (dark_average*bright_target - bright_average*dark_target)/(bright_target - dark_target) + gain = (bright_target - dark_target)/(bright_average - dark_average)*coeff + */ + res = dev->settings.xres; + + if (sensor.get_ccd_size_divisor_for_dpi(dev->settings.xres) > 1) + { + res *= 2; + } + + /* this should be evenly dividable */ + basepixels = sensor.optical_res / res; + + /* gl841 supports 1/1 1/2 1/3 1/4 1/5 1/6 1/8 1/10 1/12 1/15 averaging */ + if (basepixels < 1) + avgpixels = 1; + else if (basepixels < 6) + avgpixels = basepixels; + else if (basepixels < 8) + avgpixels = 6; + else if (basepixels < 10) + avgpixels = 8; + else if (basepixels < 12) + avgpixels = 10; + else if (basepixels < 15) + avgpixels = 12; + else + avgpixels = 15; + + /* LiDE80 packs shading data */ + if (dev->model->sensor_id != SensorId::CIS_CANON_LIDE_80) { + factor=1; + fill=avgpixels; + } + else + { + factor=avgpixels; + fill=1; + } + + DBG(DBG_info, "%s: averaging over %d pixels\n", __func__, avgpixels); + DBG(DBG_info, "%s: packing factor is %d\n", __func__, factor); + DBG(DBG_info, "%s: fill length is %d\n", __func__, fill); + + for (x = 0; x <= pixels_per_line - avgpixels; x += avgpixels) + { + if ((x + o) * 2 * 2 + 3 > words_per_color * 2) + break; + + for (j = 0; j < channels; j++) + { + + dk = 0; + br = 0; + for (i = 0; i < avgpixels; i++) + { + // dark data + dk += dev->dark_average_data[(x + i + pixels_per_line * j)]; + // white data + br += dev->white_average_data[(x + i + pixels_per_line * j)]; + } + + br /= avgpixels; + dk /= avgpixels; + + if (br * target_dark > dk * target_bright) + val = 0; + else if (dk * target_bright - br * target_dark > + 65535 * (target_bright - target_dark)) + val = 65535; + else + { + val = (dk * target_bright - br * target_dark) / (target_bright - target_dark); + } + + /*fill all pixels, even if only the last one is relevant*/ + for (i = 0; i < fill; i++) + { + shading_data[(x/factor + o + i) * 2 * 2 + words_per_color * 2 * j] = val & 0xff; + shading_data[(x/factor + o + i) * 2 * 2 + words_per_color * 2 * j + 1] = val >> 8; + } + + val = br - dk; + + if (65535 * val > (target_bright - target_dark) * coeff) + { + val = (coeff * (target_bright - target_dark)) / val; + } + else + { + val = 65535; + } + + /*fill all pixels, even if only the last one is relevant*/ + for (i = 0; i < fill; i++) + { + shading_data[(x/factor + o + i) * 2 * 2 + words_per_color * 2 * j + 2] = val & 0xff; + shading_data[(x/factor + o + i) * 2 * 2 + words_per_color * 2 * j + 3] = val >> 8; + } + } + + /* fill remaining channels */ + for (j = channels; j < 3; j++) + { + for (i = 0; i < fill; i++) + { + shading_data[(x/factor + o + i) * 2 * 2 + words_per_color * 2 * j ] = shading_data[(x/factor + o + i) * 2 * 2 ]; + shading_data[(x/factor + o + i) * 2 * 2 + words_per_color * 2 * j + 1] = shading_data[(x/factor + o + i) * 2 * 2 + 1]; + shading_data[(x/factor + o + i) * 2 * 2 + words_per_color * 2 * j + 2] = shading_data[(x/factor + o + i) * 2 * 2 + 2]; + shading_data[(x/factor + o + i) * 2 * 2 + words_per_color * 2 * j + 3] = shading_data[(x/factor + o + i) * 2 * 2 + 3]; + } + } + } +} + +static std::array<unsigned, 3> color_order_to_cmat(ColorOrder color_order) +{ + switch (color_order) { + case ColorOrder::RGB: return {0, 1, 2}; + case ColorOrder::GBR: return {2, 0, 1}; + default: + throw std::logic_error("Unknown color order"); + } +} + +/** + * Computes shading coefficient using formula in data sheet. 16bit data values + * manipulated here are little endian. For now we assume deletion scanning type + * and that there is always 3 channels. + * @param dev scanner's device + * @param shading_data memory area where to store the computed shading coefficients + * @param pixels_per_line number of pixels per line + * @param channels number of color channels (actually 1 or 3) + * @param cmat color transposition matrix + * @param offset shading coefficients left offset + * @param coeff 4000h or 2000h depending on fast scan mode or not + * @param target value of the target code + */ +static void compute_coefficients(Genesys_Device * dev, + uint8_t * shading_data, + unsigned int pixels_per_line, + unsigned int channels, + ColorOrder color_order, + int offset, + unsigned int coeff, + unsigned int target) +{ + uint8_t *ptr; /* contain 16bit words in little endian */ + unsigned int x, c; + unsigned int val, br, dk; + unsigned int start, end; + + DBG(DBG_io, "%s: pixels_per_line=%d, coeff=0x%04x\n", __func__, pixels_per_line, coeff); + + auto cmat = color_order_to_cmat(color_order); + + /* compute start & end values depending of the offset */ + if (offset < 0) + { + start = -1 * offset; + end = pixels_per_line; + } + else + { + start = 0; + end = pixels_per_line - offset; + } + + for (c = 0; c < channels; c++) + { + for (x = start; x < end; x++) + { + /* TODO if channels=1 , use filter to know the base addr */ + ptr = shading_data + 4 * ((x + offset) * channels + cmat[c]); + + // dark data + dk = dev->dark_average_data[x * channels + c]; + + // white data + br = dev->white_average_data[x * channels + c]; + + /* compute coeff */ + val=compute_coefficient(coeff,target,br-dk); + + /* assign it */ + ptr[0] = dk & 255; + ptr[1] = dk / 256; + ptr[2] = val & 0xff; + ptr[3] = val / 256; + + } + } +} + +/** + * Computes shading coefficient using formula in data sheet. 16bit data values + * manipulated here are little endian. Data is in planar form, ie grouped by + * lines of the same color component. + * @param dev scanner's device + * @param shading_data memory area where to store the computed shading coefficients + * @param factor averaging factor when the calibration scan is done at a higher resolution + * than the final scan + * @param pixels_per_line number of pixels per line + * @param words_per_color total number of shading data words for one color element + * @param channels number of color channels (actually 1 or 3) + * @param cmat transcoding matrix for color channel order + * @param offset shading coefficients left offset + * @param coeff 4000h or 2000h depending on fast scan mode or not + * @param target white target value + */ +static void compute_planar_coefficients(Genesys_Device * dev, + uint8_t * shading_data, + unsigned int factor, + unsigned int pixels_per_line, + unsigned int words_per_color, + unsigned int channels, + ColorOrder color_order, + unsigned int offset, + unsigned int coeff, + unsigned int target) +{ + uint8_t *ptr; /* contains 16bit words in little endian */ + uint32_t x, c, i; + uint32_t val, dk, br; + + auto cmat = color_order_to_cmat(color_order); + + DBG(DBG_io, "%s: factor=%d, pixels_per_line=%d, words=0x%X, coeff=0x%04x\n", __func__, factor, + pixels_per_line, words_per_color, coeff); + for (c = 0; c < channels; c++) + { + /* shading data is larger than pixels_per_line so offset can be neglected */ + for (x = 0; x < pixels_per_line; x+=factor) + { + /* x2 because of 16 bit values, and x2 since one coeff for dark + * and another for white */ + ptr = shading_data + words_per_color * cmat[c] * 2 + (x + offset) * 4; + + dk = 0; + br = 0; + + /* average case */ + for(i=0;i<factor;i++) + { + dk += dev->dark_average_data[((x+i) + pixels_per_line * c)]; + br += dev->white_average_data[((x+i) + pixels_per_line * c)]; + } + dk /= factor; + br /= factor; + + val = compute_coefficient (coeff, target, br - dk); + + /* we duplicate the information to have calibration data at optical resolution */ + for (i = 0; i < factor; i++) + { + ptr[0 + 4 * i] = dk & 255; + ptr[1 + 4 * i] = dk / 256; + ptr[2 + 4 * i] = val & 0xff; + ptr[3 + 4 * i] = val / 256; + } + } + } + /* in case of gray level scan, we duplicate shading information on all + * three color channels */ + if(channels==1) + { + memcpy(shading_data+cmat[1]*2*words_per_color, + shading_data+cmat[0]*2*words_per_color, + words_per_color*2); + memcpy(shading_data+cmat[2]*2*words_per_color, + shading_data+cmat[0]*2*words_per_color, + words_per_color*2); + } +} + +static void +compute_shifted_coefficients (Genesys_Device * dev, + const Genesys_Sensor& sensor, + uint8_t * shading_data, + unsigned int pixels_per_line, + unsigned int channels, + ColorOrder color_order, + int offset, + unsigned int coeff, + unsigned int target_dark, + unsigned int target_bright, + unsigned int patch_size) /* contigous extent */ +{ + unsigned int x, avgpixels, basepixels, i, j, val1, val2; + unsigned int br_tmp [3], dk_tmp [3]; + uint8_t *ptr = shading_data + offset * 3 * 4; /* contain 16bit words in little endian */ + unsigned int patch_cnt = offset * 3; /* at start, offset of first patch */ + + auto cmat = color_order_to_cmat(color_order); + + x = dev->settings.xres; + if (sensor.get_ccd_size_divisor_for_dpi(dev->settings.xres) > 1) + x *= 2; /* scanner is using half-ccd mode */ + basepixels = sensor.optical_res / x; /*this should be evenly dividable */ + + /* gl841 supports 1/1 1/2 1/3 1/4 1/5 1/6 1/8 1/10 1/12 1/15 averaging */ + if (basepixels < 1) + avgpixels = 1; + else if (basepixels < 6) + avgpixels = basepixels; + else if (basepixels < 8) + avgpixels = 6; + else if (basepixels < 10) + avgpixels = 8; + else if (basepixels < 12) + avgpixels = 10; + else if (basepixels < 15) + avgpixels = 12; + else + avgpixels = 15; + DBG(DBG_info, "%s: pixels_per_line=%d, coeff=0x%04x, averaging over %d pixels\n", __func__, + pixels_per_line, coeff, avgpixels); + + for (x = 0; x <= pixels_per_line - avgpixels; x += avgpixels) { + memset (&br_tmp, 0, sizeof(br_tmp)); + memset (&dk_tmp, 0, sizeof(dk_tmp)); + + for (i = 0; i < avgpixels; i++) { + for (j = 0; j < channels; j++) { + br_tmp[j] += dev->white_average_data[((x + i) * channels + j)]; + dk_tmp[i] += dev->dark_average_data[((x + i) * channels + j)]; + } + } + for (j = 0; j < channels; j++) { + br_tmp[j] /= avgpixels; + dk_tmp[j] /= avgpixels; + + if (br_tmp[j] * target_dark > dk_tmp[j] * target_bright) + val1 = 0; + else if (dk_tmp[j] * target_bright - br_tmp[j] * target_dark > 65535 * (target_bright - target_dark)) + val1 = 65535; + else + val1 = (dk_tmp[j] * target_bright - br_tmp[j] * target_dark) / (target_bright - target_dark); + + val2 = br_tmp[j] - dk_tmp[j]; + if (65535 * val2 > (target_bright - target_dark) * coeff) + val2 = (coeff * (target_bright - target_dark)) / val2; + else + val2 = 65535; + + br_tmp[j] = val1; + dk_tmp[j] = val2; + } + for (i = 0; i < avgpixels; i++) { + for (j = 0; j < channels; j++) { + * ptr++ = br_tmp[ cmat[j] ] & 0xff; + * ptr++ = br_tmp[ cmat[j] ] >> 8; + * ptr++ = dk_tmp[ cmat[j] ] & 0xff; + * ptr++ = dk_tmp[ cmat[j] ] >> 8; + patch_cnt++; + if (patch_cnt == patch_size) { + patch_cnt = 0; + val1 = cmat[2]; + cmat[2] = cmat[1]; + cmat[1] = cmat[0]; + cmat[0] = val1; + } + } + } + } +} + +static void genesys_send_shading_coefficient(Genesys_Device* dev, const Genesys_Sensor& sensor) +{ + DBG_HELPER(dbg); + + if (dev->model->flags & GENESYS_FLAG_CALIBRATION_HOST_SIDE) { + return; + } + + uint32_t pixels_per_line; + uint8_t channels; + int o; + unsigned int length; /**> number of shading calibration data words */ + unsigned int factor; + unsigned int coeff, target_code, words_per_color = 0; + + pixels_per_line = dev->calib_pixels + dev->calib_pixels_offset; + channels = dev->calib_channels; + + /* we always build data for three channels, even for gray + * we make the shading data such that each color channel data line is contiguous + * to the next one, which allow to write the 3 channels in 1 write + * during genesys_send_shading_coefficient, some values are words, other bytes + * hence the x2 factor */ + switch (dev->reg.get8(0x05) >> 6) + { + /* 600 dpi */ + case 0: + words_per_color = 0x2a00; + break; + /* 1200 dpi */ + case 1: + words_per_color = 0x5500; + break; + /* 2400 dpi */ + case 2: + words_per_color = 0xa800; + break; + /* 4800 dpi */ + case 3: + words_per_color = 0x15000; + break; + } + + /* special case, memory is aligned on 0x5400, this has yet to be explained */ + /* could be 0xa800 because sensor is truly 2400 dpi, then halved because + * we only set 1200 dpi */ + if(dev->model->sensor_id==SensorId::CIS_CANON_LIDE_80) + { + words_per_color = 0x5400; + } + + length = words_per_color * 3 * 2; + + /* allocate computed size */ + // contains 16bit words in little endian + std::vector<uint8_t> shading_data(length, 0); + + /* TARGET/(Wn-Dn) = white gain -> ~1.xxx then it is multiplied by 0x2000 + or 0x4000 to give an integer + Wn = white average for column n + Dn = dark average for column n + */ + if (get_registers_gain4_bit(dev->model->asic_type, dev->calib_reg)) { + coeff = 0x4000; + } else { + coeff = 0x2000; + } + + /* compute avg factor */ + if(dev->settings.xres>sensor.optical_res) + { + factor=1; + } + else + { + factor=sensor.optical_res/dev->settings.xres; + } + + /* for GL646, shading data is planar if REG_0x01_FASTMOD is set and + * chunky if not. For now we rely on the fact that we know that + * each sensor is used only in one mode. Currently only the CIS_XP200 + * sets REG_0x01_FASTMOD. + */ + + /* TODO setup a struct in genesys_devices that + * will handle these settings instead of having this switch growing up */ + switch (dev->model->sensor_id) + { + case SensorId::CCD_XP300: + case SensorId::CCD_ROADWARRIOR: + case SensorId::CCD_DP665: + case SensorId::CCD_DP685: + case SensorId::CCD_DSMOBILE600: + target_code = 0xdc00; + o = 4; + compute_planar_coefficients (dev, + shading_data.data(), + factor, + pixels_per_line, + words_per_color, + channels, + ColorOrder::RGB, + o, + coeff, + target_code); + break; + case SensorId::CIS_XP200: + target_code = 0xdc00; + o = 2; + compute_planar_coefficients (dev, + shading_data.data(), + 1, + pixels_per_line, + words_per_color, + channels, + ColorOrder::GBR, + o, + coeff, + target_code); + break; + case SensorId::CCD_HP2300: + target_code = 0xdc00; + o = 2; + if(dev->settings.xres<=sensor.optical_res/2) + { + o = o - sensor.dummy_pixel / 2; + } + compute_coefficients (dev, + shading_data.data(), + pixels_per_line, + 3, + ColorOrder::RGB, + o, + coeff, + target_code); + break; + case SensorId::CCD_5345: + target_code = 0xe000; + o = 4; + if(dev->settings.xres<=sensor.optical_res/2) + { + o = o - sensor.dummy_pixel; + } + compute_coefficients (dev, + shading_data.data(), + pixels_per_line, + 3, + ColorOrder::RGB, + o, + coeff, + target_code); + break; + case SensorId::CCD_HP3670: + case SensorId::CCD_HP2400: + target_code = 0xe000; + // offset is dependent on ccd_pixels_per_system_pixel(), but we couldn't use this in + // common code previously. + // FIXME: use sensor.ccd_pixels_per_system_pixel() + if(dev->settings.xres<=300) + { + o = -10; + } + else if(dev->settings.xres<=600) + { + o = -6; + } + else + { + o = +2; + } + compute_coefficients (dev, + shading_data.data(), + pixels_per_line, + 3, + ColorOrder::RGB, + o, + coeff, + target_code); + break; + case SensorId::CCD_KVSS080: + case SensorId::CCD_PLUSTEK_OPTICBOOK_3800: + case SensorId::CCD_G4050: + case SensorId::CCD_HP_4850C: + case SensorId::CCD_CANON_4400F: + case SensorId::CCD_CANON_8400F: + case SensorId::CCD_CANON_8600F: + case SensorId::CCD_PLUSTEK_OPTICFILM_7200I: + case SensorId::CCD_PLUSTEK_OPTICFILM_7300: + case SensorId::CCD_PLUSTEK_OPTICFILM_7500I: + target_code = 0xe000; + o = 0; + compute_coefficients (dev, + shading_data.data(), + pixels_per_line, + 3, + ColorOrder::RGB, + o, + coeff, + target_code); + break; + case SensorId::CIS_CANON_LIDE_700F: + case SensorId::CIS_CANON_LIDE_100: + case SensorId::CIS_CANON_LIDE_200: + case SensorId::CIS_CANON_LIDE_110: + case SensorId::CIS_CANON_LIDE_120: + case SensorId::CIS_CANON_LIDE_210: + case SensorId::CIS_CANON_LIDE_220: + /* TODO store this in a data struct so we avoid + * growing this switch */ + switch(dev->model->sensor_id) + { + case SensorId::CIS_CANON_LIDE_110: + case SensorId::CIS_CANON_LIDE_120: + case SensorId::CIS_CANON_LIDE_210: + case SensorId::CIS_CANON_LIDE_220: + case SensorId::CIS_CANON_LIDE_700F: + target_code = 0xc000; + break; + default: + target_code = 0xdc00; + } + words_per_color=pixels_per_line*2; + length = words_per_color * 3 * 2; + shading_data.clear(); + shading_data.resize(length, 0); + compute_planar_coefficients (dev, + shading_data.data(), + 1, + pixels_per_line, + words_per_color, + channels, + ColorOrder::RGB, + 0, + coeff, + target_code); + break; + case SensorId::CIS_CANON_LIDE_35: + compute_averaged_planar (dev, sensor, + shading_data.data(), + pixels_per_line, + words_per_color, + channels, + 4, + coeff, + 0xe000, + 0x0a00); + break; + case SensorId::CIS_CANON_LIDE_80: + compute_averaged_planar (dev, sensor, + shading_data.data(), + pixels_per_line, + words_per_color, + channels, + 0, + coeff, + 0xe000, + 0x0800); + break; + case SensorId::CCD_PLUSTEK_OPTICPRO_3600: + compute_shifted_coefficients (dev, sensor, + shading_data.data(), + pixels_per_line, + channels, + ColorOrder::RGB, + 12, /* offset */ + coeff, + 0x0001, /* target_dark */ + 0xf900, /* target_bright */ + 256); /* patch_size: contigous extent */ + break; + default: + throw SaneException(SANE_STATUS_UNSUPPORTED, "sensor %d not supported", + static_cast<unsigned>(dev->model->sensor_id)); + break; + } + + // do the actual write of shading calibration data to the scanner + genesys_send_offset_and_shading(dev, sensor, shading_data.data(), length); +} + + +/** + * search calibration cache list for an entry matching required scan. + * If one is found, set device calibration with it + * @param dev scanner's device + * @return false if no matching cache entry has been + * found, true if one has been found and used. + */ +static bool +genesys_restore_calibration(Genesys_Device * dev, Genesys_Sensor& sensor) +{ + DBG_HELPER(dbg); + + // if no cache or no function to evaluate cache entry ther can be no match/ + if (dev->calibration_cache.empty()) { + return false; + } + + auto session = dev->cmd_set->calculate_scan_session(dev, sensor, dev->settings); + + /* we walk the link list of calibration cache in search for a + * matching one */ + for (auto& cache : dev->calibration_cache) + { + if (sanei_genesys_is_compatible_calibration(dev, session, &cache, false)) { + dev->frontend = cache.frontend; + /* we don't restore the gamma fields */ + sensor.exposure = cache.sensor.exposure; + + dev->average_size = cache.average_size; + dev->calib_pixels = cache.calib_pixels; + dev->calib_channels = cache.calib_channels; + + dev->dark_average_data = cache.dark_average_data; + dev->white_average_data = cache.white_average_data; + + if (!dev->cmd_set->has_send_shading_data()) { + genesys_send_shading_coefficient(dev, sensor); + } + + DBG(DBG_proc, "%s: restored\n", __func__); + return true; + } + } + DBG(DBG_proc, "%s: completed(nothing found)\n", __func__); + return false; +} + + +static void genesys_save_calibration(Genesys_Device* dev, const Genesys_Sensor& sensor) +{ + DBG_HELPER(dbg); +#ifdef HAVE_SYS_TIME_H + struct timeval time; +#endif + + auto session = dev->cmd_set->calculate_scan_session(dev, sensor, dev->settings); + + auto found_cache_it = dev->calibration_cache.end(); + for (auto cache_it = dev->calibration_cache.begin(); cache_it != dev->calibration_cache.end(); + cache_it++) + { + if (sanei_genesys_is_compatible_calibration(dev, session, &*cache_it, true)) { + found_cache_it = cache_it; + break; + } + } + + /* if we found on overridable cache, we reuse it */ + if (found_cache_it == dev->calibration_cache.end()) + { + /* create a new cache entry and insert it in the linked list */ + dev->calibration_cache.push_back(Genesys_Calibration_Cache()); + found_cache_it = std::prev(dev->calibration_cache.end()); + } + + found_cache_it->average_size = dev->average_size; + + found_cache_it->dark_average_data = dev->dark_average_data; + found_cache_it->white_average_data = dev->white_average_data; + + found_cache_it->params = session.params; + found_cache_it->frontend = dev->frontend; + found_cache_it->sensor = sensor; + + found_cache_it->calib_pixels = dev->calib_pixels; + found_cache_it->calib_channels = dev->calib_channels; + +#ifdef HAVE_SYS_TIME_H + gettimeofday(&time, nullptr); + found_cache_it->last_calibration = time.tv_sec; +#endif +} + +/** + * does the calibration process for a flatbed scanner + * - offset calibration + * - gain calibration + * - shading calibration + * @param dev device to calibrate + */ +static void genesys_flatbed_calibration(Genesys_Device* dev, Genesys_Sensor& sensor) +{ + DBG_HELPER(dbg); + uint32_t pixels_per_line; + + unsigned coarse_res = sensor.optical_res; + if (dev->settings.yres <= sensor.optical_res / 2) { + coarse_res /= 2; + } + + if (dev->model->model_id == ModelId::CANON_8400F) { + coarse_res = 1600; + } + + if (dev->model->model_id == ModelId::CANON_4400F || + dev->model->model_id == ModelId::CANON_8600F) + { + coarse_res = 1200; + } + + /* do offset calibration if needed */ + if (dev->model->flags & GENESYS_FLAG_OFFSET_CALIBRATION) + { + dev->interface->record_progress_message("offset_calibration"); + dev->cmd_set->offset_calibration(dev, sensor, dev->calib_reg); + + /* since all the registers are set up correctly, just use them */ + dev->interface->record_progress_message("coarse_gain_calibration"); + dev->cmd_set->coarse_gain_calibration(dev, sensor, dev->calib_reg, coarse_res); + } else { + /* since we have 2 gain calibration proc, skip second if first one was + used. */ + dev->interface->record_progress_message("init_regs_for_coarse_calibration"); + dev->cmd_set->init_regs_for_coarse_calibration(dev, sensor, dev->calib_reg); + + dev->interface->record_progress_message("genesys_coarse_calibration"); + genesys_coarse_calibration(dev, sensor); + } + + if (dev->model->is_cis) + { + /* the afe now sends valid data for doing led calibration */ + dev->interface->record_progress_message("led_calibration"); + switch (dev->model->asic_type) { + case AsicType::GL124: + case AsicType::GL845: + case AsicType::GL846: + case AsicType::GL847: { + auto calib_exposure = dev->cmd_set->led_calibration(dev, sensor, dev->calib_reg); + for (auto& sensor_update : + sanei_genesys_find_sensors_all_for_write(dev, sensor.method)) { + sensor_update.get().exposure = calib_exposure; + } + sensor.exposure = calib_exposure; + break; + } + default: { + sensor.exposure = dev->cmd_set->led_calibration(dev, sensor, dev->calib_reg); + } + } + + + /* calibrate afe again to match new exposure */ + if (dev->model->flags & GENESYS_FLAG_OFFSET_CALIBRATION) { + dev->interface->record_progress_message("offset_calibration"); + dev->cmd_set->offset_calibration(dev, sensor, dev->calib_reg); + + // since all the registers are set up correctly, just use them + + dev->interface->record_progress_message("coarse_gain_calibration"); + dev->cmd_set->coarse_gain_calibration(dev, sensor, dev->calib_reg, coarse_res); + } else { + // since we have 2 gain calibration proc, skip second if first one was used + dev->interface->record_progress_message("init_regs_for_coarse_calibration"); + dev->cmd_set->init_regs_for_coarse_calibration(dev, sensor, dev->calib_reg); + + dev->interface->record_progress_message("genesys_coarse_calibration"); + genesys_coarse_calibration(dev, sensor); + } + } + + /* we always use sensor pixel number when the ASIC can't handle multi-segments sensor */ + if (!(dev->model->flags & GENESYS_FLAG_SIS_SENSOR)) + { + pixels_per_line = static_cast<std::uint32_t>((dev->model->x_size * dev->settings.xres) / + MM_PER_INCH); + } + else + { + pixels_per_line = sensor.sensor_pixels; + } + + // send default shading data + dev->interface->record_progress_message("sanei_genesys_init_shading_data"); + sanei_genesys_init_shading_data(dev, sensor, pixels_per_line); + + if (dev->settings.scan_method == ScanMethod::TRANSPARENCY || + dev->settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED) + { + dev->cmd_set->move_to_ta(dev); + } + + // shading calibration + if (dev->model->flags & GENESYS_FLAG_DARK_WHITE_CALIBRATION) { + dev->interface->record_progress_message("init_regs_for_shading"); + dev->cmd_set->init_regs_for_shading(dev, sensor, dev->calib_reg); + + dev->interface->record_progress_message("genesys_dark_white_shading_calibration"); + genesys_dark_white_shading_calibration(dev, sensor); + } else { + DBG(DBG_proc, "%s : genesys_dark_shading_calibration dev->calib_reg ", __func__); + debug_dump(DBG_proc, dev->calib_reg); + + if (dev->model->flags & GENESYS_FLAG_DARK_CALIBRATION) { + dev->interface->record_progress_message("init_regs_for_shading"); + dev->cmd_set->init_regs_for_shading(dev, sensor, dev->calib_reg); + + dev->interface->record_progress_message("genesys_dark_shading_calibration"); + genesys_dark_shading_calibration(dev, sensor); + genesys_repark_sensor_before_shading(dev); + } + + dev->interface->record_progress_message("init_regs_for_shading2"); + dev->cmd_set->init_regs_for_shading(dev, sensor, dev->calib_reg); + + dev->interface->record_progress_message("genesys_white_shading_calibration"); + genesys_white_shading_calibration(dev, sensor); + genesys_repark_sensor_after_white_shading(dev); + + if (!(dev->model->flags & GENESYS_FLAG_DARK_CALIBRATION)) { + genesys_dummy_dark_shading(dev, sensor); + } + } + + if (!dev->cmd_set->has_send_shading_data()) { + dev->interface->record_progress_message("genesys_send_shading_coefficient"); + genesys_send_shading_coefficient(dev, sensor); + } +} + +/** + * Does the calibration process for a sheetfed scanner + * - offset calibration + * - gain calibration + * - shading calibration + * During calibration a predefined calibration sheet with specific black and white + * areas is used. + * @param dev device to calibrate + */ +static void genesys_sheetfed_calibration(Genesys_Device* dev, Genesys_Sensor& sensor) +{ + DBG_HELPER(dbg); + bool forward = true; + + // first step, load document + dev->cmd_set->load_document(dev); + + /* led, offset and gain calibration are influenced by scan + * settings. So we set it to sensor resolution */ + dev->settings.xres = sensor.optical_res; + /* XP200 needs to calibrate a full and half sensor's resolution */ + if (dev->model->sensor_id == SensorId::CIS_XP200 && + dev->settings.xres <= sensor.optical_res / 2) + { + dev->settings.xres /= 2; + } + + /* the afe needs to sends valid data even before calibration */ + + /* go to a white area */ + try { + dev->cmd_set->search_strip(dev, sensor, forward, false); + } catch (...) { + catch_all_exceptions(__func__, [&](){ dev->cmd_set->eject_document(dev); }); + throw; + } + + if (dev->model->is_cis) + { + dev->cmd_set->led_calibration(dev, sensor, dev->calib_reg); + } + + /* calibrate afe */ + if (dev->model->flags & GENESYS_FLAG_OFFSET_CALIBRATION) + { + dev->cmd_set->offset_calibration(dev, sensor, dev->calib_reg); + + /* since all the registers are set up correctly, just use them */ + + dev->cmd_set->coarse_gain_calibration(dev, sensor, dev->calib_reg, sensor.optical_res); + } + else + /* since we have 2 gain calibration proc, skip second if first one was + used. */ + { + dev->cmd_set->init_regs_for_coarse_calibration(dev, sensor, dev->calib_reg); + + genesys_coarse_calibration(dev, sensor); + } + + /* search for a full width black strip and then do a 16 bit scan to + * gather black shading data */ + if (dev->model->flags & GENESYS_FLAG_DARK_CALIBRATION) + { + /* seek black/white reverse/forward */ + try { + dev->cmd_set->search_strip(dev, sensor, forward, true); + } catch (...) { + catch_all_exceptions(__func__, [&](){ dev->cmd_set->eject_document(dev); }); + throw; + } + + dev->cmd_set->init_regs_for_shading(dev, sensor, dev->calib_reg); + + try { + genesys_dark_shading_calibration(dev, sensor); + } catch (...) { + catch_all_exceptions(__func__, [&](){ dev->cmd_set->eject_document(dev); }); + throw; + } + forward = false; + } + + + /* go to a white area */ + try { + dev->cmd_set->search_strip(dev, sensor, forward, false); + } catch (...) { + catch_all_exceptions(__func__, [&](){ dev->cmd_set->eject_document(dev); }); + throw; + } + + genesys_repark_sensor_before_shading(dev); + + dev->cmd_set->init_regs_for_shading(dev, sensor, dev->calib_reg); + + try { + genesys_white_shading_calibration(dev, sensor); + genesys_repark_sensor_after_white_shading(dev); + } catch (...) { + catch_all_exceptions(__func__, [&](){ dev->cmd_set->eject_document(dev); }); + throw; + } + + // in case we haven't black shading data, build it from black pixels of white calibration + // FIXME: shouldn't we use genesys_dummy_dark_shading() ? + if (!(dev->model->flags & GENESYS_FLAG_DARK_CALIBRATION)) { + dev->dark_average_data.clear(); + dev->dark_average_data.resize(dev->average_size, 0x0f0f); + /* XXX STEF XXX + * with black point in white shading, build an average black + * pixel and use it to fill the dark_average + * dev->calib_pixels + (sensor.sensor_pixels * dev->settings.xres) / sensor.optical_res, + dev->calib_lines, + */ + } + + /* send the shading coefficient when doing whole line shading + * but not when using SHDAREA like GL124 */ + if (!dev->cmd_set->has_send_shading_data()) { + genesys_send_shading_coefficient(dev, sensor); + } + + // save the calibration data + genesys_save_calibration(dev, sensor); + + // and finally eject calibration sheet + dev->cmd_set->eject_document(dev); + + // restore settings + dev->settings.xres = sensor.optical_res; +} + +/** + * does the calibration process for a device + * @param dev device to calibrate + */ +static void genesys_scanner_calibration(Genesys_Device* dev, Genesys_Sensor& sensor) +{ + DBG_HELPER(dbg); + if (!dev->model->is_sheetfed) { + genesys_flatbed_calibration(dev, sensor); + return; + } + genesys_sheetfed_calibration(dev, sensor); +} + + +/* ------------------------------------------------------------------------ */ +/* High level (exported) functions */ +/* ------------------------------------------------------------------------ */ + +/* + * wait lamp to be warm enough by scanning the same line until + * differences between two scans are below a threshold + */ +static void genesys_warmup_lamp(Genesys_Device* dev) +{ + DBG_HELPER(dbg); + unsigned seconds = 0; + int pixel; + int channels, total_size; + double first_average = 0; + double second_average = 0; + int difference = 255; + int lines = 3; + + const auto& sensor = sanei_genesys_find_sensor_any(dev); + + dev->cmd_set->init_regs_for_warmup(dev, sensor, &dev->reg, &channels, &total_size); + std::vector<uint8_t> first_line(total_size); + std::vector<uint8_t> second_line(total_size); + + do + { + DBG(DBG_info, "%s: one more loop\n", __func__); + dev->cmd_set->begin_scan(dev, sensor, &dev->reg, false); + + if (is_testing_mode()) { + dev->interface->test_checkpoint("warmup_lamp"); + dev->cmd_set->end_scan(dev, &dev->reg, true); + return; + } + + wait_until_buffer_non_empty(dev); + + try { + sanei_genesys_read_data_from_scanner(dev, first_line.data(), total_size); + } catch (...) { + // FIXME: document why this retry is here + sanei_genesys_read_data_from_scanner(dev, first_line.data(), total_size); + } + + dev->cmd_set->end_scan(dev, &dev->reg, true); + + dev->interface->sleep_ms(1000); + seconds++; + + dev->cmd_set->begin_scan(dev, sensor, &dev->reg, false); + + wait_until_buffer_non_empty(dev); + + sanei_genesys_read_data_from_scanner(dev, second_line.data(), total_size); + dev->cmd_set->end_scan(dev, &dev->reg, true); + + /* compute difference between the two scans */ + for (pixel = 0; pixel < total_size; pixel++) + { + // 16 bit data + if (dev->session.params.depth == 16) { + first_average += (first_line[pixel] + first_line[pixel + 1] * 256); + second_average += (second_line[pixel] + second_line[pixel + 1] * 256); + pixel++; + } + else + { + first_average += first_line[pixel]; + second_average += second_line[pixel]; + } + } + if (dev->session.params.depth == 16) { + first_average /= pixel; + second_average /= pixel; + difference = static_cast<int>(std::fabs(first_average - second_average)); + DBG(DBG_info, "%s: average = %.2f, diff = %.3f\n", __func__, + 100 * ((second_average) / (256 * 256)), + 100 * (difference / second_average)); + + if (second_average > (100 * 256) + && (difference / second_average) < 0.002) + break; + } + else + { + first_average /= pixel; + second_average /= pixel; + if (DBG_LEVEL >= DBG_data) + { + sanei_genesys_write_pnm_file("gl_warmup1.pnm", first_line.data(), 8, channels, + total_size / (lines * channels), lines); + sanei_genesys_write_pnm_file("gl_warmup2.pnm", second_line.data(), 8, channels, + total_size / (lines * channels), lines); + } + DBG(DBG_info, "%s: average 1 = %.2f, average 2 = %.2f\n", __func__, first_average, + second_average); + /* if delta below 15/255 ~= 5.8%, lamp is considred warm enough */ + if (fabs (first_average - second_average) < 15 + && second_average > 55) + break; + } + + /* sleep another second before next loop */ + dev->interface->sleep_ms(1000); + seconds++; + } while (seconds < WARMUP_TIME); + + if (seconds >= WARMUP_TIME) + { + throw SaneException(SANE_STATUS_IO_ERROR, + "warmup timed out after %d seconds. Lamp defective?", seconds); + } + else + { + DBG(DBG_info, "%s: warmup succeeded after %d seconds\n", __func__, seconds); + } +} + + +// High-level start of scanning +static void genesys_start_scan(Genesys_Device* dev, bool lamp_off) +{ + DBG_HELPER(dbg); + unsigned int steps, expected; + + /* since not all scanners are set ot wait for head to park + * we check we are not still parking before starting a new scan */ + if (dev->parking) { + sanei_genesys_wait_for_home(dev); + } + + // disable power saving + dev->cmd_set->save_power(dev, false); + + /* wait for lamp warmup : until a warmup for TRANSPARENCY is designed, skip + * it when scanning from XPA. */ + if (!(dev->model->flags & GENESYS_FLAG_SKIP_WARMUP) + && (dev->settings.scan_method == ScanMethod::FLATBED)) + { + genesys_warmup_lamp(dev); + } + + /* set top left x and y values by scanning the internals if flatbed scanners */ + if (!dev->model->is_sheetfed) { + /* do the geometry detection only once */ + if ((dev->model->flags & GENESYS_FLAG_SEARCH_START) + && (dev->model->y_offset_calib_white == 0)) + { + dev->cmd_set->search_start_position (dev); + + dev->parking = false; + dev->cmd_set->move_back_home(dev, true); + } + else + { + /* Go home */ + /* TODO: check we can drop this since we cannot have the + scanner's head wandering here */ + dev->parking = false; + dev->cmd_set->move_back_home(dev, true); + } + } + + /* move to calibration area for transparency adapter */ + if (dev->settings.scan_method == ScanMethod::TRANSPARENCY || + dev->settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED) + { + dev->cmd_set->move_to_ta(dev); + } + + /* load document if needed (for sheetfed scanner for instance) */ + if (dev->model->is_sheetfed) { + dev->cmd_set->load_document(dev); + } + + auto& sensor = sanei_genesys_find_sensor_for_write(dev, dev->settings.xres, + dev->settings.get_channels(), + dev->settings.scan_method); + + // send gamma tables. They have been set to device or user value + // when setting option value */ + dev->cmd_set->send_gamma_table(dev, sensor); + + /* try to use cached calibration first */ + if (!genesys_restore_calibration (dev, sensor)) + { + /* calibration : sheetfed scanners can't calibrate before each scan */ + /* and also those who have the NO_CALIBRATION flag */ + if (!(dev->model->flags & GENESYS_FLAG_NO_CALIBRATION) && !dev->model->is_sheetfed) { + genesys_scanner_calibration(dev, sensor); + genesys_save_calibration (dev, sensor); + } + else + { + DBG(DBG_warn, "%s: no calibration done\n", __func__); + } + } + + /* build look up table for dynamic lineart */ + if (dev->settings.scan_mode == ScanColorMode::LINEART) { + sanei_genesys_load_lut(dev->lineart_lut, 8, 8, 50, 205, dev->settings.threshold_curve, + dev->settings.threshold-127); + } + + dev->cmd_set->wait_for_motor_stop(dev); + + if (dev->cmd_set->needs_home_before_init_regs_for_scan(dev)) { + dev->cmd_set->move_back_home(dev, true); + } + + if (dev->settings.scan_method == ScanMethod::TRANSPARENCY || + dev->settings.scan_method == ScanMethod::TRANSPARENCY_INFRARED) + { + dev->cmd_set->move_to_ta(dev); + } + + dev->cmd_set->init_regs_for_scan(dev, sensor); + + /* no lamp during scan */ + if (lamp_off) { + sanei_genesys_set_lamp_power(dev, sensor, dev->reg, false); + } + + /* GL124 is using SHDAREA, so we have to wait for scan to be set up before + * sending shading data */ + if (dev->cmd_set->has_send_shading_data() && + !(dev->model->flags & GENESYS_FLAG_NO_CALIBRATION)) + { + genesys_send_shading_coefficient(dev, sensor); + } + + // now send registers for scan + dev->interface->write_registers(dev->reg); + + // start effective scan + dev->cmd_set->begin_scan(dev, sensor, &dev->reg, true); + + if (is_testing_mode()) { + dev->interface->test_checkpoint("start_scan"); + return; + } + + /*do we really need this? the valid data check should be sufficent -- pierre*/ + /* waits for head to reach scanning position */ + expected = dev->reg.get8(0x3d) * 65536 + + dev->reg.get8(0x3e) * 256 + + dev->reg.get8(0x3f); + do + { + // wait some time between each test to avoid overloading USB and CPU + dev->interface->sleep_ms(100); + sanei_genesys_read_feed_steps (dev, &steps); + } + while (steps < expected); + + wait_until_buffer_non_empty(dev); + + // we wait for at least one word of valid scan data + // this is also done in sanei_genesys_read_data_from_scanner -- pierre + if (!dev->model->is_sheetfed) { + do { + dev->interface->sleep_ms(100); + sanei_genesys_read_valid_words(dev, &steps); + } + while (steps < 1); + } +} + +static void genesys_fill_read_buffer(Genesys_Device* dev) +{ + DBG_HELPER(dbg); + + /* for sheetfed scanner, we must check is document is shorter than + * the requested scan */ + if (dev->model->is_sheetfed) { + dev->cmd_set->detect_document_end(dev); + } + + std::size_t size = dev->read_buffer.size() - dev->read_buffer.avail(); + + /* due to sensors and motors, not all data can be directly used. It + * may have to be read from another intermediate buffer and then processed. + * There are currently 3 intermediate stages: + * - handling of odd/even sensors + * - handling of line interpolation for motors that can't have low + * enough dpi + * - handling of multi-segments sensors + * + * This is also the place where full duplex data will be handled. + */ + dev->pipeline_buffer.get_data(size, dev->read_buffer.get_write_pos(size)); + + dev->read_buffer.produce(size); +} + +/* this function does the effective data read in a manner that suits + the scanner. It does data reordering and resizing if need. + It also manages EOF and I/O errors, and line distance correction. + Returns true on success, false on end-of-file. +*/ +static void genesys_read_ordered_data(Genesys_Device* dev, SANE_Byte* destination, size_t* len) +{ + DBG_HELPER(dbg); + size_t bytes = 0; + uint8_t *work_buffer_src; + Genesys_Buffer *src_buffer; + + if (!dev->read_active) { + *len = 0; + throw SaneException("read is not active"); + } + + DBG(DBG_info, "%s: frontend requested %zu bytes\n", __func__, *len); + DBG(DBG_info, "%s: bytes_to_read=%zu, total_bytes_read=%zu\n", __func__, + dev->total_bytes_to_read, dev->total_bytes_read); + + /* is there data left to scan */ + if (dev->total_bytes_read >= dev->total_bytes_to_read) + { + /* issue park command immediatly in case scanner can handle it + * so we save time */ + if (!dev->model->is_sheetfed && !(dev->model->flags & GENESYS_FLAG_MUST_WAIT) && + !dev->parking) + { + dev->cmd_set->move_back_home(dev, false); + dev->parking = true; + } + throw SaneException(SANE_STATUS_EOF, "nothing more to scan: EOF"); + } + +/* convert data */ +/* + 0. fill_read_buffer +-------------- read_buffer ---------------------- + 1a). (opt)uncis (assumes color components to be laid out + planar) + 1b). (opt)reverse_RGB (assumes pixels to be BGR or BBGGRR)) +-------------- lines_buffer ---------------------- + 2a). (opt)line_distance_correction (assumes RGB or RRGGBB) + 2b). (opt)unstagger (assumes pixels to be depth*channels/8 + bytes long, unshrinked) +------------- shrink_buffer --------------------- + 3. (opt)shrink_lines (assumes component separation in pixels) +-------------- out_buffer ----------------------- + 4. memcpy to destination (for lineart with bit reversal) +*/ +/*FIXME: for lineart we need sub byte addressing in buffers, or conversion to + bytes at 0. and back to bits at 4. +Problems with the first approach: + - its not clear how to check if we need to output an incomplete byte + because it is the last one. + */ +/*FIXME: add lineart support for gl646. in the meantime add logic to convert + from gray to lineart at the end? would suffer the above problem, + total_bytes_to_read and total_bytes_read help in that case. + */ + + if (is_testing_mode()) { + if (dev->total_bytes_read + *len > dev->total_bytes_to_read) { + *len = dev->total_bytes_to_read - dev->total_bytes_read; + } + dev->total_bytes_read += *len; + } else { + genesys_fill_read_buffer(dev); + + src_buffer = &(dev->read_buffer); + + /* move data to destination */ + bytes = std::min(src_buffer->avail(), *len); + + work_buffer_src = src_buffer->get_read_pos(); + + std::memcpy(destination, work_buffer_src, bytes); + *len = bytes; + + /* avoid signaling some extra data because we have treated a full block + * on the last block */ + if (dev->total_bytes_read + *len > dev->total_bytes_to_read) { + *len = dev->total_bytes_to_read - dev->total_bytes_read; + } + + /* count bytes sent to frontend */ + dev->total_bytes_read += *len; + + src_buffer->consume(bytes); + } + + /* end scan if all needed data have been read */ + if(dev->total_bytes_read >= dev->total_bytes_to_read) + { + dev->cmd_set->end_scan(dev, &dev->reg, true); + if (dev->model->is_sheetfed) { + dev->cmd_set->eject_document (dev); + } + } + + DBG(DBG_proc, "%s: completed, %zu bytes read\n", __func__, bytes); +} + + + +/* ------------------------------------------------------------------------ */ +/* Start of higher level functions */ +/* ------------------------------------------------------------------------ */ + +static size_t +max_string_size (const SANE_String_Const strings[]) +{ + size_t size, max_size = 0; + SANE_Int i; + + for (i = 0; strings[i]; ++i) + { + size = strlen (strings[i]) + 1; + if (size > max_size) + max_size = size; + } + return max_size; +} + +static std::size_t max_string_size(const std::vector<const char*>& strings) +{ + std::size_t max_size = 0; + for (const auto& s : strings) { + if (!s) { + continue; + } + max_size = std::max(max_size, std::strlen(s)); + } + return max_size; +} + +static unsigned pick_resolution(const std::vector<unsigned>& resolutions, unsigned resolution, + const char* direction) +{ + DBG_HELPER(dbg); + + if (resolutions.empty()) + throw SaneException("Empty resolution list"); + + unsigned best_res = resolutions.front(); + unsigned min_diff = abs_diff(best_res, resolution); + + for (auto it = std::next(resolutions.begin()); it != resolutions.end(); ++it) { + unsigned curr_diff = abs_diff(*it, resolution); + if (curr_diff < min_diff) { + min_diff = curr_diff; + best_res = *it; + } + } + + if (best_res != resolution) { + DBG(DBG_warn, "%s: using resolution %d that is nearest to %d for direction %s\n", + __func__, best_res, resolution, direction); + } + return best_res; +} + +static void calc_parameters(Genesys_Scanner* s) +{ + DBG_HELPER(dbg); + double tl_x = 0, tl_y = 0, br_x = 0, br_y = 0; + + tl_x = SANE_UNFIX(s->pos_top_left_x); + tl_y = SANE_UNFIX(s->pos_top_left_y); + br_x = SANE_UNFIX(s->pos_bottom_right_x); + br_y = SANE_UNFIX(s->pos_bottom_right_y); + + s->params.last_frame = true; /* only single pass scanning supported */ + + if (s->mode == SANE_VALUE_SCAN_MODE_GRAY || s->mode == SANE_VALUE_SCAN_MODE_LINEART) { + s->params.format = SANE_FRAME_GRAY; + } else { + s->params.format = SANE_FRAME_RGB; + } + + if (s->mode == SANE_VALUE_SCAN_MODE_LINEART) { + s->params.depth = 1; + } else { + s->params.depth = s->bit_depth; + } + + s->dev->settings.scan_method = s->scan_method; + const auto& resolutions = s->dev->model->get_resolution_settings(s->dev->settings.scan_method); + + s->dev->settings.depth = s->bit_depth; + + /* interpolation */ + s->dev->settings.disable_interpolation = s->disable_interpolation; + + // FIXME: use correct sensor + const auto& sensor = sanei_genesys_find_sensor_any(s->dev); + + // hardware settings + if (static_cast<unsigned>(s->resolution) > sensor.optical_res && + s->dev->settings.disable_interpolation) + { + s->dev->settings.xres = sensor.optical_res; + } else { + s->dev->settings.xres = s->resolution; + } + s->dev->settings.yres = s->resolution; + + s->dev->settings.xres = pick_resolution(resolutions.resolutions_x, s->dev->settings.xres, "X"); + s->dev->settings.yres = pick_resolution(resolutions.resolutions_y, s->dev->settings.yres, "Y"); + + s->params.lines = static_cast<unsigned>(((br_y - tl_y) * s->dev->settings.yres) / + MM_PER_INCH); + unsigned pixels_per_line = static_cast<unsigned>(((br_x - tl_x) * s->dev->settings.xres) / + MM_PER_INCH); + + /* we need an even pixels number + * TODO invert test logic or generalize behaviour across all ASICs */ + if ((s->dev->model->flags & GENESYS_FLAG_SIS_SENSOR) || + s->dev->model->asic_type == AsicType::GL847 || + s->dev->model->asic_type == AsicType::GL124 || + s->dev->model->asic_type == AsicType::GL845 || + s->dev->model->asic_type == AsicType::GL846 || + s->dev->model->asic_type == AsicType::GL843) + { + if (s->dev->settings.xres <= 1200) { + pixels_per_line = (pixels_per_line / 4) * 4; + } else if (s->dev->settings.xres < s->dev->settings.yres) { + // BUG: this is an artifact of the fact that the resolution was twice as large than + // the actual resolution when scanning above the supported scanner X resolution + pixels_per_line = (pixels_per_line / 8) * 8; + } else { + pixels_per_line = (pixels_per_line / 16) * 16; + } + } + + /* corner case for true lineart for sensor with several segments + * or when xres is doubled to match yres */ + if (s->dev->settings.xres >= 1200 && ( + s->dev->model->asic_type == AsicType::GL124 || + s->dev->model->asic_type == AsicType::GL847 || + s->dev->session.params.xres < s->dev->session.params.yres)) + { + if (s->dev->settings.xres < s->dev->settings.yres) { + // FIXME: this is an artifact of the fact that the resolution was twice as large than + // the actual resolution when scanning above the supported scanner X resolution + pixels_per_line = (pixels_per_line / 8) * 8; + } else { + pixels_per_line = (pixels_per_line / 16) * 16; + } + } + + unsigned xres_factor = s->resolution / s->dev->settings.xres; + + unsigned bytes_per_line = 0; + + if (s->params.depth > 8) + { + s->params.depth = 16; + bytes_per_line = 2 * pixels_per_line; + } + else if (s->params.depth == 1) + { + // round down pixel number. This will is lossy operation, at most 7 pixels will be lost + pixels_per_line = (pixels_per_line / 8) * 8; + bytes_per_line = pixels_per_line / 8; + } else { + bytes_per_line = pixels_per_line; + } + + if (s->params.format == SANE_FRAME_RGB) { + bytes_per_line *= 3; + } + + s->dev->settings.scan_mode = option_string_to_scan_color_mode(s->mode); + + s->dev->settings.lines = s->params.lines; + s->dev->settings.pixels = pixels_per_line; + s->dev->settings.requested_pixels = pixels_per_line * xres_factor; + s->params.pixels_per_line = pixels_per_line * xres_factor; + s->params.bytes_per_line = bytes_per_line * xres_factor; + s->dev->settings.tl_x = tl_x; + s->dev->settings.tl_y = tl_y; + + // threshold setting + s->dev->settings.threshold = static_cast<int>(2.55 * (SANE_UNFIX(s->threshold))); + + // color filter + if (s->color_filter == "Red") { + s->dev->settings.color_filter = ColorFilter::RED; + } else if (s->color_filter == "Green") { + s->dev->settings.color_filter = ColorFilter::GREEN; + } else if (s->color_filter == "Blue") { + s->dev->settings.color_filter = ColorFilter::BLUE; + } else { + s->dev->settings.color_filter = ColorFilter::NONE; + } + + // true gray + if (s->color_filter == "None") { + s->dev->settings.true_gray = 1; + } else { + s->dev->settings.true_gray = 0; + } + + // threshold curve for dynamic rasterization + s->dev->settings.threshold_curve = s->threshold_curve; + + /* some digital processing requires the whole picture to be buffered */ + /* no digital processing takes place when doing preview, or when bit depth is + * higher than 8 bits */ + if ((s->swdespeck || s->swcrop || s->swdeskew || s->swderotate ||(SANE_UNFIX(s->swskip)>0)) + && (!s->preview) + && (s->bit_depth <= 8)) + { + s->dev->buffer_image = true; + } + else + { + s->dev->buffer_image = false; + } + + /* brigthness and contrast only for for 8 bit scans */ + if(s->bit_depth <= 8) + { + s->dev->settings.contrast = (s->contrast * 127) / 100; + s->dev->settings.brightness = (s->brightness * 127) / 100; + } + else + { + s->dev->settings.contrast=0; + s->dev->settings.brightness=0; + } + + /* cache expiration time */ + s->dev->settings.expiration_time = s->expiration_time; +} + + +static void create_bpp_list (Genesys_Scanner * s, const std::vector<unsigned>& bpp) +{ + s->bpp_list[0] = bpp.size(); + std::reverse_copy(bpp.begin(), bpp.end(), s->bpp_list + 1); +} + +/** @brief this function initialize a gamma vector based on the ASIC: + * Set up a default gamma table vector based on device description + * gl646: 12 or 14 bits gamma table depending on GENESYS_FLAG_14BIT_GAMMA + * gl84x: 16 bits + * gl12x: 16 bits + * @param scanner pointer to scanner session to get options + * @param option option number of the gamma table to set + */ +static void +init_gamma_vector_option (Genesys_Scanner * scanner, int option) +{ + /* the option is inactive until the custom gamma control + * is enabled */ + scanner->opt[option].type = SANE_TYPE_INT; + scanner->opt[option].cap |= SANE_CAP_INACTIVE | SANE_CAP_ADVANCED; + scanner->opt[option].unit = SANE_UNIT_NONE; + scanner->opt[option].constraint_type = SANE_CONSTRAINT_RANGE; + if (scanner->dev->model->asic_type == AsicType::GL646) { + if ((scanner->dev->model->flags & GENESYS_FLAG_14BIT_GAMMA) != 0) + { + scanner->opt[option].size = 16384 * sizeof (SANE_Word); + scanner->opt[option].constraint.range = &u14_range; + } + else + { /* 12 bits gamma tables */ + scanner->opt[option].size = 4096 * sizeof (SANE_Word); + scanner->opt[option].constraint.range = &u12_range; + } + } + else + { /* other asics have 16 bits words gamma table */ + scanner->opt[option].size = 256 * sizeof (SANE_Word); + scanner->opt[option].constraint.range = &u16_range; + } +} + +/** + * allocate a geometry range + * @param size maximum size of the range + * @return a pointer to a valid range or nullptr + */ +static SANE_Range create_range(float size) +{ + SANE_Range range; + range.min = SANE_FIX(0.0); + range.max = SANE_FIX(size); + range.quant = SANE_FIX(0.0); + return range; +} + +/** @brief generate calibration cache file nam + * Generates the calibration cache file name to use. + * Tries to store the chache in $HOME/.sane or + * then fallbacks to $TMPDIR or TMP. The filename + * uses the model name if only one scanner is plugged + * else is uses the device name when several identical + * scanners are in use. + * @param currdev current scanner device + * @return an allocated string containing a file name + */ +static std::string calibration_filename(Genesys_Device *currdev) +{ + std::string ret; + ret.resize(PATH_MAX); + + char filename[80]; + unsigned int count; + unsigned int i; + + /* first compute the DIR where we can store cache: + * 1 - home dir + * 2 - $TMPDIR + * 3 - $TMP + * 4 - tmp dir + * 5 - temp dir + * 6 - then resort to current dir + */ + char* ptr = std::getenv("HOME"); + if (ptr == nullptr) { + ptr = std::getenv("USERPROFILE"); + } + if (ptr == nullptr) { + ptr = std::getenv("TMPDIR"); + } + if (ptr == nullptr) { + ptr = std::getenv("TMP"); + } + + /* now choose filename: + * 1 - if only one scanner, name of the model + * 2 - if several scanners of the same model, use device name, + * replacing special chars + */ + count=0; + /* count models of the same names if several scanners attached */ + if(s_devices->size() > 1) { + for (const auto& dev : *s_devices) { + if (dev.model->model_id == currdev->model->model_id) { + count++; + } + } + } + if(count>1) + { + std::snprintf(filename, sizeof(filename), "%s.cal", currdev->file_name.c_str()); + for(i=0;i<strlen(filename);i++) + { + if(filename[i]==':'||filename[i]==PATH_SEP) + { + filename[i]='_'; + } + } + } + else + { + snprintf(filename,sizeof(filename),"%s.cal",currdev->model->name); + } + + /* build final final name : store dir + filename */ + if (ptr == nullptr) { + int size = std::snprintf(&ret.front(), ret.size(), "%s", filename); + ret.resize(size); + } + else + { + int size = 0; +#ifdef HAVE_MKDIR + /* make sure .sane directory exists in existing store dir */ + size = std::snprintf(&ret.front(), ret.size(), "%s%c.sane", ptr, PATH_SEP); + ret.resize(size); + mkdir(ret.c_str(), 0700); + + ret.resize(PATH_MAX); +#endif + size = std::snprintf(&ret.front(), ret.size(), "%s%c.sane%c%s", + ptr, PATH_SEP, PATH_SEP, filename); + ret.resize(size); + } + + DBG(DBG_info, "%s: calibration filename >%s<\n", __func__, ret.c_str()); + + return ret; +} + +static void set_resolution_option_values(Genesys_Scanner& s, bool reset_resolution_value) +{ + auto resolutions = s.dev->model->get_resolutions(s.scan_method); + + s.opt_resolution_values.resize(resolutions.size() + 1, 0); + s.opt_resolution_values[0] = resolutions.size(); + std::copy(resolutions.begin(), resolutions.end(), s.opt_resolution_values.begin() + 1); + + s.opt[OPT_RESOLUTION].constraint.word_list = s.opt_resolution_values.data(); + + if (reset_resolution_value) { + s.resolution = *std::min_element(resolutions.begin(), resolutions.end()); + } +} + +static void set_xy_range_option_values(Genesys_Scanner& s) +{ + if (s.scan_method == ScanMethod::FLATBED) + { + s.opt_x_range = create_range(static_cast<float>(s.dev->model->x_size)); + s.opt_y_range = create_range(static_cast<float>(s.dev->model->y_size)); + } + else + { + s.opt_x_range = create_range(static_cast<float>(s.dev->model->x_size_ta)); + s.opt_y_range = create_range(static_cast<float>(s.dev->model->y_size_ta)); + } + + s.opt[OPT_TL_X].constraint.range = &s.opt_x_range; + s.opt[OPT_TL_Y].constraint.range = &s.opt_y_range; + s.opt[OPT_BR_X].constraint.range = &s.opt_x_range; + s.opt[OPT_BR_Y].constraint.range = &s.opt_y_range; + + s.pos_top_left_x = 0; + s.pos_top_left_y = 0; + s.pos_bottom_right_x = s.opt_x_range.max; + s.pos_bottom_right_y = s.opt_y_range.max; +} + +static void init_options(Genesys_Scanner* s) +{ + DBG_HELPER(dbg); + SANE_Int option; + Genesys_Model *model = s->dev->model; + + memset (s->opt, 0, sizeof (s->opt)); + + for (option = 0; option < NUM_OPTIONS; ++option) + { + s->opt[option].size = sizeof (SANE_Word); + s->opt[option].cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT; + } + s->opt[OPT_NUM_OPTS].name = SANE_NAME_NUM_OPTIONS; + s->opt[OPT_NUM_OPTS].title = SANE_TITLE_NUM_OPTIONS; + s->opt[OPT_NUM_OPTS].desc = SANE_DESC_NUM_OPTIONS; + s->opt[OPT_NUM_OPTS].type = SANE_TYPE_INT; + s->opt[OPT_NUM_OPTS].cap = SANE_CAP_SOFT_DETECT; + + /* "Mode" group: */ + s->opt[OPT_MODE_GROUP].name = "scanmode-group"; + s->opt[OPT_MODE_GROUP].title = SANE_I18N ("Scan Mode"); + s->opt[OPT_MODE_GROUP].desc = ""; + s->opt[OPT_MODE_GROUP].type = SANE_TYPE_GROUP; + s->opt[OPT_MODE_GROUP].size = 0; + s->opt[OPT_MODE_GROUP].cap = 0; + s->opt[OPT_MODE_GROUP].constraint_type = SANE_CONSTRAINT_NONE; + + /* scan mode */ + s->opt[OPT_MODE].name = SANE_NAME_SCAN_MODE; + s->opt[OPT_MODE].title = SANE_TITLE_SCAN_MODE; + s->opt[OPT_MODE].desc = SANE_DESC_SCAN_MODE; + s->opt[OPT_MODE].type = SANE_TYPE_STRING; + s->opt[OPT_MODE].constraint_type = SANE_CONSTRAINT_STRING_LIST; + s->opt[OPT_MODE].size = max_string_size (mode_list); + s->opt[OPT_MODE].constraint.string_list = mode_list; + s->mode = SANE_VALUE_SCAN_MODE_GRAY; + + /* scan source */ + s->opt_source_values.clear(); + for (const auto& resolution_setting : model->resolutions) { + for (auto method : resolution_setting.methods) { + s->opt_source_values.push_back(scan_method_to_option_string(method)); + } + } + s->opt_source_values.push_back(nullptr); + + s->opt[OPT_SOURCE].name = SANE_NAME_SCAN_SOURCE; + s->opt[OPT_SOURCE].title = SANE_TITLE_SCAN_SOURCE; + s->opt[OPT_SOURCE].desc = SANE_DESC_SCAN_SOURCE; + s->opt[OPT_SOURCE].type = SANE_TYPE_STRING; + s->opt[OPT_SOURCE].constraint_type = SANE_CONSTRAINT_STRING_LIST; + s->opt[OPT_SOURCE].size = max_string_size(s->opt_source_values); + s->opt[OPT_SOURCE].constraint.string_list = s->opt_source_values.data(); + if (s->opt_source_values.size() < 2) { + throw SaneException("No scan methods specified for scanner"); + } + s->scan_method = model->default_method; + + /* preview */ + s->opt[OPT_PREVIEW].name = SANE_NAME_PREVIEW; + s->opt[OPT_PREVIEW].title = SANE_TITLE_PREVIEW; + s->opt[OPT_PREVIEW].desc = SANE_DESC_PREVIEW; + s->opt[OPT_PREVIEW].type = SANE_TYPE_BOOL; + s->opt[OPT_PREVIEW].unit = SANE_UNIT_NONE; + s->opt[OPT_PREVIEW].constraint_type = SANE_CONSTRAINT_NONE; + s->preview = false; + + /* bit depth */ + s->opt[OPT_BIT_DEPTH].name = SANE_NAME_BIT_DEPTH; + s->opt[OPT_BIT_DEPTH].title = SANE_TITLE_BIT_DEPTH; + s->opt[OPT_BIT_DEPTH].desc = SANE_DESC_BIT_DEPTH; + s->opt[OPT_BIT_DEPTH].type = SANE_TYPE_INT; + s->opt[OPT_BIT_DEPTH].constraint_type = SANE_CONSTRAINT_WORD_LIST; + s->opt[OPT_BIT_DEPTH].size = sizeof (SANE_Word); + s->opt[OPT_BIT_DEPTH].constraint.word_list = s->bpp_list; + create_bpp_list (s, model->bpp_gray_values); + s->bit_depth = model->bpp_gray_values[0]; + + // resolution + s->opt[OPT_RESOLUTION].name = SANE_NAME_SCAN_RESOLUTION; + s->opt[OPT_RESOLUTION].title = SANE_TITLE_SCAN_RESOLUTION; + s->opt[OPT_RESOLUTION].desc = SANE_DESC_SCAN_RESOLUTION; + s->opt[OPT_RESOLUTION].type = SANE_TYPE_INT; + s->opt[OPT_RESOLUTION].unit = SANE_UNIT_DPI; + s->opt[OPT_RESOLUTION].constraint_type = SANE_CONSTRAINT_WORD_LIST; + set_resolution_option_values(*s, true); + + /* "Geometry" group: */ + s->opt[OPT_GEOMETRY_GROUP].name = SANE_NAME_GEOMETRY; + s->opt[OPT_GEOMETRY_GROUP].title = SANE_I18N ("Geometry"); + s->opt[OPT_GEOMETRY_GROUP].desc = ""; + s->opt[OPT_GEOMETRY_GROUP].type = SANE_TYPE_GROUP; + s->opt[OPT_GEOMETRY_GROUP].cap = SANE_CAP_ADVANCED; + s->opt[OPT_GEOMETRY_GROUP].size = 0; + s->opt[OPT_GEOMETRY_GROUP].constraint_type = SANE_CONSTRAINT_NONE; + + s->opt_x_range = create_range(static_cast<float>(model->x_size)); + s->opt_y_range = create_range(static_cast<float>(model->y_size)); + + // scan area + s->opt[OPT_TL_X].name = SANE_NAME_SCAN_TL_X; + s->opt[OPT_TL_X].title = SANE_TITLE_SCAN_TL_X; + s->opt[OPT_TL_X].desc = SANE_DESC_SCAN_TL_X; + s->opt[OPT_TL_X].type = SANE_TYPE_FIXED; + s->opt[OPT_TL_X].unit = SANE_UNIT_MM; + s->opt[OPT_TL_X].constraint_type = SANE_CONSTRAINT_RANGE; + + s->opt[OPT_TL_Y].name = SANE_NAME_SCAN_TL_Y; + s->opt[OPT_TL_Y].title = SANE_TITLE_SCAN_TL_Y; + s->opt[OPT_TL_Y].desc = SANE_DESC_SCAN_TL_Y; + s->opt[OPT_TL_Y].type = SANE_TYPE_FIXED; + s->opt[OPT_TL_Y].unit = SANE_UNIT_MM; + s->opt[OPT_TL_Y].constraint_type = SANE_CONSTRAINT_RANGE; + + s->opt[OPT_BR_X].name = SANE_NAME_SCAN_BR_X; + s->opt[OPT_BR_X].title = SANE_TITLE_SCAN_BR_X; + s->opt[OPT_BR_X].desc = SANE_DESC_SCAN_BR_X; + s->opt[OPT_BR_X].type = SANE_TYPE_FIXED; + s->opt[OPT_BR_X].unit = SANE_UNIT_MM; + s->opt[OPT_BR_X].constraint_type = SANE_CONSTRAINT_RANGE; + + s->opt[OPT_BR_Y].name = SANE_NAME_SCAN_BR_Y; + s->opt[OPT_BR_Y].title = SANE_TITLE_SCAN_BR_Y; + s->opt[OPT_BR_Y].desc = SANE_DESC_SCAN_BR_Y; + s->opt[OPT_BR_Y].type = SANE_TYPE_FIXED; + s->opt[OPT_BR_Y].unit = SANE_UNIT_MM; + s->opt[OPT_BR_Y].constraint_type = SANE_CONSTRAINT_RANGE; + + set_xy_range_option_values(*s); + + /* "Enhancement" group: */ + s->opt[OPT_ENHANCEMENT_GROUP].name = SANE_NAME_ENHANCEMENT; + s->opt[OPT_ENHANCEMENT_GROUP].title = SANE_I18N ("Enhancement"); + s->opt[OPT_ENHANCEMENT_GROUP].desc = ""; + s->opt[OPT_ENHANCEMENT_GROUP].type = SANE_TYPE_GROUP; + s->opt[OPT_ENHANCEMENT_GROUP].cap = SANE_CAP_ADVANCED; + s->opt[OPT_ENHANCEMENT_GROUP].size = 0; + s->opt[OPT_ENHANCEMENT_GROUP].constraint_type = SANE_CONSTRAINT_NONE; + + /* custom-gamma table */ + s->opt[OPT_CUSTOM_GAMMA].name = SANE_NAME_CUSTOM_GAMMA; + s->opt[OPT_CUSTOM_GAMMA].title = SANE_TITLE_CUSTOM_GAMMA; + s->opt[OPT_CUSTOM_GAMMA].desc = SANE_DESC_CUSTOM_GAMMA; + s->opt[OPT_CUSTOM_GAMMA].type = SANE_TYPE_BOOL; + s->opt[OPT_CUSTOM_GAMMA].cap |= SANE_CAP_ADVANCED; + s->custom_gamma = false; + + /* grayscale gamma vector */ + s->opt[OPT_GAMMA_VECTOR].name = SANE_NAME_GAMMA_VECTOR; + s->opt[OPT_GAMMA_VECTOR].title = SANE_TITLE_GAMMA_VECTOR; + s->opt[OPT_GAMMA_VECTOR].desc = SANE_DESC_GAMMA_VECTOR; + init_gamma_vector_option (s, OPT_GAMMA_VECTOR); + + /* red gamma vector */ + s->opt[OPT_GAMMA_VECTOR_R].name = SANE_NAME_GAMMA_VECTOR_R; + s->opt[OPT_GAMMA_VECTOR_R].title = SANE_TITLE_GAMMA_VECTOR_R; + s->opt[OPT_GAMMA_VECTOR_R].desc = SANE_DESC_GAMMA_VECTOR_R; + init_gamma_vector_option (s, OPT_GAMMA_VECTOR_R); + + /* green gamma vector */ + s->opt[OPT_GAMMA_VECTOR_G].name = SANE_NAME_GAMMA_VECTOR_G; + s->opt[OPT_GAMMA_VECTOR_G].title = SANE_TITLE_GAMMA_VECTOR_G; + s->opt[OPT_GAMMA_VECTOR_G].desc = SANE_DESC_GAMMA_VECTOR_G; + init_gamma_vector_option (s, OPT_GAMMA_VECTOR_G); + + /* blue gamma vector */ + s->opt[OPT_GAMMA_VECTOR_B].name = SANE_NAME_GAMMA_VECTOR_B; + s->opt[OPT_GAMMA_VECTOR_B].title = SANE_TITLE_GAMMA_VECTOR_B; + s->opt[OPT_GAMMA_VECTOR_B].desc = SANE_DESC_GAMMA_VECTOR_B; + init_gamma_vector_option (s, OPT_GAMMA_VECTOR_B); + + /* currently, there are only gamma table options in this group, + * so if the scanner doesn't support gamma table, disable the + * whole group */ + if (!(model->flags & GENESYS_FLAG_CUSTOM_GAMMA)) + { + s->opt[OPT_ENHANCEMENT_GROUP].cap |= SANE_CAP_INACTIVE; + s->opt[OPT_CUSTOM_GAMMA].cap |= SANE_CAP_INACTIVE; + DBG(DBG_info, "%s: custom gamma disabled\n", __func__); + } + + /* software base image enhancements, these are consuming as many + * memory than used by the full scanned image and may fail at high + * resolution + */ + /* software deskew */ + s->opt[OPT_SWDESKEW].name = "swdeskew"; + s->opt[OPT_SWDESKEW].title = "Software deskew"; + s->opt[OPT_SWDESKEW].desc = "Request backend to rotate skewed pages digitally"; + s->opt[OPT_SWDESKEW].type = SANE_TYPE_BOOL; + s->opt[OPT_SWDESKEW].cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT | SANE_CAP_ADVANCED; + s->swdeskew = false; + + /* software deskew */ + s->opt[OPT_SWDESPECK].name = "swdespeck"; + s->opt[OPT_SWDESPECK].title = "Software despeck"; + s->opt[OPT_SWDESPECK].desc = "Request backend to remove lone dots digitally"; + s->opt[OPT_SWDESPECK].type = SANE_TYPE_BOOL; + s->opt[OPT_SWDESPECK].cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT | SANE_CAP_ADVANCED; + s->swdespeck = false; + + /* software despeckle radius */ + s->opt[OPT_DESPECK].name = "despeck"; + s->opt[OPT_DESPECK].title = "Software despeckle diameter"; + s->opt[OPT_DESPECK].desc = "Maximum diameter of lone dots to remove from scan"; + s->opt[OPT_DESPECK].type = SANE_TYPE_INT; + s->opt[OPT_DESPECK].unit = SANE_UNIT_NONE; + s->opt[OPT_DESPECK].constraint_type = SANE_CONSTRAINT_RANGE; + s->opt[OPT_DESPECK].constraint.range = &swdespeck_range; + s->opt[OPT_DESPECK].cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT | SANE_CAP_ADVANCED | SANE_CAP_INACTIVE; + s->despeck = 1; + + /* crop by software */ + s->opt[OPT_SWCROP].name = "swcrop"; + s->opt[OPT_SWCROP].title = SANE_I18N ("Software crop"); + s->opt[OPT_SWCROP].desc = SANE_I18N ("Request backend to remove border from pages digitally"); + s->opt[OPT_SWCROP].type = SANE_TYPE_BOOL; + s->opt[OPT_SWCROP].cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT | SANE_CAP_ADVANCED; + s->opt[OPT_SWCROP].unit = SANE_UNIT_NONE; + s->swcrop = false; + + /* Software blank page skip */ + s->opt[OPT_SWSKIP].name = "swskip"; + s->opt[OPT_SWSKIP].title = SANE_I18N ("Software blank skip percentage"); + s->opt[OPT_SWSKIP].desc = SANE_I18N("Request driver to discard pages with low numbers of dark pixels"); + s->opt[OPT_SWSKIP].type = SANE_TYPE_FIXED; + s->opt[OPT_SWSKIP].unit = SANE_UNIT_PERCENT; + s->opt[OPT_SWSKIP].constraint_type = SANE_CONSTRAINT_RANGE; + s->opt[OPT_SWSKIP].constraint.range = &(percentage_range); + s->opt[OPT_SWSKIP].cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT | SANE_CAP_ADVANCED; + s->swskip = 0; // disable by default + + /* Software Derotate */ + s->opt[OPT_SWDEROTATE].name = "swderotate"; + s->opt[OPT_SWDEROTATE].title = SANE_I18N ("Software derotate"); + s->opt[OPT_SWDEROTATE].desc = SANE_I18N("Request driver to detect and correct 90 degree image rotation"); + s->opt[OPT_SWDEROTATE].type = SANE_TYPE_BOOL; + s->opt[OPT_SWDEROTATE].cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT | SANE_CAP_ADVANCED; + s->opt[OPT_SWDEROTATE].unit = SANE_UNIT_NONE; + s->swderotate = false; + + /* Software brightness */ + s->opt[OPT_BRIGHTNESS].name = SANE_NAME_BRIGHTNESS; + s->opt[OPT_BRIGHTNESS].title = SANE_TITLE_BRIGHTNESS; + s->opt[OPT_BRIGHTNESS].desc = SANE_DESC_BRIGHTNESS; + s->opt[OPT_BRIGHTNESS].type = SANE_TYPE_INT; + s->opt[OPT_BRIGHTNESS].unit = SANE_UNIT_NONE; + s->opt[OPT_BRIGHTNESS].constraint_type = SANE_CONSTRAINT_RANGE; + s->opt[OPT_BRIGHTNESS].constraint.range = &(enhance_range); + s->opt[OPT_BRIGHTNESS].cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT; + s->brightness = 0; // disable by default + + /* Sowftware contrast */ + s->opt[OPT_CONTRAST].name = SANE_NAME_CONTRAST; + s->opt[OPT_CONTRAST].title = SANE_TITLE_CONTRAST; + s->opt[OPT_CONTRAST].desc = SANE_DESC_CONTRAST; + s->opt[OPT_CONTRAST].type = SANE_TYPE_INT; + s->opt[OPT_CONTRAST].unit = SANE_UNIT_NONE; + s->opt[OPT_CONTRAST].constraint_type = SANE_CONSTRAINT_RANGE; + s->opt[OPT_CONTRAST].constraint.range = &(enhance_range); + s->opt[OPT_CONTRAST].cap = SANE_CAP_SOFT_SELECT | SANE_CAP_SOFT_DETECT; + s->contrast = 0; // disable by default + + /* "Extras" group: */ + s->opt[OPT_EXTRAS_GROUP].name = "extras-group"; + s->opt[OPT_EXTRAS_GROUP].title = SANE_I18N ("Extras"); + s->opt[OPT_EXTRAS_GROUP].desc = ""; + s->opt[OPT_EXTRAS_GROUP].type = SANE_TYPE_GROUP; + s->opt[OPT_EXTRAS_GROUP].cap = SANE_CAP_ADVANCED; + s->opt[OPT_EXTRAS_GROUP].size = 0; + s->opt[OPT_EXTRAS_GROUP].constraint_type = SANE_CONSTRAINT_NONE; + + /* BW threshold */ + s->opt[OPT_THRESHOLD].name = SANE_NAME_THRESHOLD; + s->opt[OPT_THRESHOLD].title = SANE_TITLE_THRESHOLD; + s->opt[OPT_THRESHOLD].desc = SANE_DESC_THRESHOLD; + s->opt[OPT_THRESHOLD].type = SANE_TYPE_FIXED; + s->opt[OPT_THRESHOLD].unit = SANE_UNIT_PERCENT; + s->opt[OPT_THRESHOLD].constraint_type = SANE_CONSTRAINT_RANGE; + s->opt[OPT_THRESHOLD].constraint.range = &percentage_range; + s->threshold = SANE_FIX(50); + + /* BW threshold curve */ + s->opt[OPT_THRESHOLD_CURVE].name = "threshold-curve"; + s->opt[OPT_THRESHOLD_CURVE].title = SANE_I18N ("Threshold curve"); + s->opt[OPT_THRESHOLD_CURVE].desc = SANE_I18N ("Dynamic threshold curve, from light to dark, normally 50-65"); + s->opt[OPT_THRESHOLD_CURVE].type = SANE_TYPE_INT; + s->opt[OPT_THRESHOLD_CURVE].unit = SANE_UNIT_NONE; + s->opt[OPT_THRESHOLD_CURVE].constraint_type = SANE_CONSTRAINT_RANGE; + s->opt[OPT_THRESHOLD_CURVE].constraint.range = &threshold_curve_range; + s->threshold_curve = 50; + + /* disable_interpolation */ + s->opt[OPT_DISABLE_INTERPOLATION].name = "disable-interpolation"; + s->opt[OPT_DISABLE_INTERPOLATION].title = + SANE_I18N ("Disable interpolation"); + s->opt[OPT_DISABLE_INTERPOLATION].desc = + SANE_I18N + ("When using high resolutions where the horizontal resolution is smaller " + "than the vertical resolution this disables horizontal interpolation."); + s->opt[OPT_DISABLE_INTERPOLATION].type = SANE_TYPE_BOOL; + s->opt[OPT_DISABLE_INTERPOLATION].unit = SANE_UNIT_NONE; + s->opt[OPT_DISABLE_INTERPOLATION].constraint_type = SANE_CONSTRAINT_NONE; + s->disable_interpolation = false; + + /* color filter */ + s->opt[OPT_COLOR_FILTER].name = "color-filter"; + s->opt[OPT_COLOR_FILTER].title = SANE_I18N ("Color filter"); + s->opt[OPT_COLOR_FILTER].desc = + SANE_I18N + ("When using gray or lineart this option selects the used color."); + s->opt[OPT_COLOR_FILTER].type = SANE_TYPE_STRING; + s->opt[OPT_COLOR_FILTER].constraint_type = SANE_CONSTRAINT_STRING_LIST; + /* true gray not yet supported for GL847 and GL124 scanners */ + if (!model->is_cis || model->asic_type==AsicType::GL847 || model->asic_type==AsicType::GL124) { + s->opt[OPT_COLOR_FILTER].size = max_string_size (color_filter_list); + s->opt[OPT_COLOR_FILTER].constraint.string_list = color_filter_list; + s->color_filter = s->opt[OPT_COLOR_FILTER].constraint.string_list[1]; + } + else + { + s->opt[OPT_COLOR_FILTER].size = max_string_size (cis_color_filter_list); + s->opt[OPT_COLOR_FILTER].constraint.string_list = cis_color_filter_list; + /* default to "None" ie true gray */ + s->color_filter = s->opt[OPT_COLOR_FILTER].constraint.string_list[3]; + } + + // no support for color filter for cis+gl646 scanners + if (model->asic_type == AsicType::GL646 && model->is_cis) { + DISABLE (OPT_COLOR_FILTER); + } + + /* calibration store file name */ + s->opt[OPT_CALIBRATION_FILE].name = "calibration-file"; + s->opt[OPT_CALIBRATION_FILE].title = SANE_I18N ("Calibration file"); + s->opt[OPT_CALIBRATION_FILE].desc = SANE_I18N ("Specify the calibration file to use"); + s->opt[OPT_CALIBRATION_FILE].type = SANE_TYPE_STRING; + s->opt[OPT_CALIBRATION_FILE].unit = SANE_UNIT_NONE; + s->opt[OPT_CALIBRATION_FILE].size = PATH_MAX; + s->opt[OPT_CALIBRATION_FILE].cap = SANE_CAP_SOFT_DETECT | SANE_CAP_SOFT_SELECT | SANE_CAP_ADVANCED; + s->opt[OPT_CALIBRATION_FILE].constraint_type = SANE_CONSTRAINT_NONE; + s->calibration_file.clear(); + /* disable option if ran as root */ +#ifdef HAVE_GETUID + if(geteuid()==0) + { + DISABLE (OPT_CALIBRATION_FILE); + } +#endif + + /* expiration time for calibration cache entries */ + s->opt[OPT_EXPIRATION_TIME].name = "expiration-time"; + s->opt[OPT_EXPIRATION_TIME].title = SANE_I18N ("Calibration cache expiration time"); + s->opt[OPT_EXPIRATION_TIME].desc = SANE_I18N ("Time (in minutes) before a cached calibration expires. " + "A value of 0 means cache is not used. A negative value means cache never expires."); + s->opt[OPT_EXPIRATION_TIME].type = SANE_TYPE_INT; + s->opt[OPT_EXPIRATION_TIME].unit = SANE_UNIT_NONE; + s->opt[OPT_EXPIRATION_TIME].constraint_type = SANE_CONSTRAINT_RANGE; + s->opt[OPT_EXPIRATION_TIME].constraint.range = &expiration_range; + s->expiration_time = 60; // 60 minutes by default + + /* Powersave time (turn lamp off) */ + s->opt[OPT_LAMP_OFF_TIME].name = "lamp-off-time"; + s->opt[OPT_LAMP_OFF_TIME].title = SANE_I18N ("Lamp off time"); + s->opt[OPT_LAMP_OFF_TIME].desc = + SANE_I18N + ("The lamp will be turned off after the given time (in minutes). " + "A value of 0 means, that the lamp won't be turned off."); + s->opt[OPT_LAMP_OFF_TIME].type = SANE_TYPE_INT; + s->opt[OPT_LAMP_OFF_TIME].unit = SANE_UNIT_NONE; + s->opt[OPT_LAMP_OFF_TIME].constraint_type = SANE_CONSTRAINT_RANGE; + s->opt[OPT_LAMP_OFF_TIME].constraint.range = &time_range; + s->lamp_off_time = 15; // 15 minutes + + /* turn lamp off during scan */ + s->opt[OPT_LAMP_OFF].name = "lamp-off-scan"; + s->opt[OPT_LAMP_OFF].title = SANE_I18N ("Lamp off during scan"); + s->opt[OPT_LAMP_OFF].desc = SANE_I18N ("The lamp will be turned off during scan. "); + s->opt[OPT_LAMP_OFF].type = SANE_TYPE_BOOL; + s->opt[OPT_LAMP_OFF].unit = SANE_UNIT_NONE; + s->opt[OPT_LAMP_OFF].constraint_type = SANE_CONSTRAINT_NONE; + s->lamp_off = false; + + s->opt[OPT_SENSOR_GROUP].name = SANE_NAME_SENSORS; + s->opt[OPT_SENSOR_GROUP].title = SANE_TITLE_SENSORS; + s->opt[OPT_SENSOR_GROUP].desc = SANE_DESC_SENSORS; + s->opt[OPT_SENSOR_GROUP].type = SANE_TYPE_GROUP; + s->opt[OPT_SENSOR_GROUP].cap = SANE_CAP_ADVANCED; + s->opt[OPT_SENSOR_GROUP].size = 0; + s->opt[OPT_SENSOR_GROUP].constraint_type = SANE_CONSTRAINT_NONE; + + s->opt[OPT_SCAN_SW].name = SANE_NAME_SCAN; + s->opt[OPT_SCAN_SW].title = SANE_TITLE_SCAN; + s->opt[OPT_SCAN_SW].desc = SANE_DESC_SCAN; + s->opt[OPT_SCAN_SW].type = SANE_TYPE_BOOL; + s->opt[OPT_SCAN_SW].unit = SANE_UNIT_NONE; + if (model->buttons & GENESYS_HAS_SCAN_SW) + s->opt[OPT_SCAN_SW].cap = + SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED; + else + s->opt[OPT_SCAN_SW].cap = SANE_CAP_INACTIVE; + + /* SANE_NAME_FILE is not for buttons */ + s->opt[OPT_FILE_SW].name = "file"; + s->opt[OPT_FILE_SW].title = SANE_I18N ("File button"); + s->opt[OPT_FILE_SW].desc = SANE_I18N ("File button"); + s->opt[OPT_FILE_SW].type = SANE_TYPE_BOOL; + s->opt[OPT_FILE_SW].unit = SANE_UNIT_NONE; + if (model->buttons & GENESYS_HAS_FILE_SW) + s->opt[OPT_FILE_SW].cap = + SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED; + else + s->opt[OPT_FILE_SW].cap = SANE_CAP_INACTIVE; + + s->opt[OPT_EMAIL_SW].name = SANE_NAME_EMAIL; + s->opt[OPT_EMAIL_SW].title = SANE_TITLE_EMAIL; + s->opt[OPT_EMAIL_SW].desc = SANE_DESC_EMAIL; + s->opt[OPT_EMAIL_SW].type = SANE_TYPE_BOOL; + s->opt[OPT_EMAIL_SW].unit = SANE_UNIT_NONE; + if (model->buttons & GENESYS_HAS_EMAIL_SW) + s->opt[OPT_EMAIL_SW].cap = + SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED; + else + s->opt[OPT_EMAIL_SW].cap = SANE_CAP_INACTIVE; + + s->opt[OPT_COPY_SW].name = SANE_NAME_COPY; + s->opt[OPT_COPY_SW].title = SANE_TITLE_COPY; + s->opt[OPT_COPY_SW].desc = SANE_DESC_COPY; + s->opt[OPT_COPY_SW].type = SANE_TYPE_BOOL; + s->opt[OPT_COPY_SW].unit = SANE_UNIT_NONE; + if (model->buttons & GENESYS_HAS_COPY_SW) + s->opt[OPT_COPY_SW].cap = + SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED; + else + s->opt[OPT_COPY_SW].cap = SANE_CAP_INACTIVE; + + s->opt[OPT_PAGE_LOADED_SW].name = SANE_NAME_PAGE_LOADED; + s->opt[OPT_PAGE_LOADED_SW].title = SANE_TITLE_PAGE_LOADED; + s->opt[OPT_PAGE_LOADED_SW].desc = SANE_DESC_PAGE_LOADED; + s->opt[OPT_PAGE_LOADED_SW].type = SANE_TYPE_BOOL; + s->opt[OPT_PAGE_LOADED_SW].unit = SANE_UNIT_NONE; + if (model->buttons & GENESYS_HAS_PAGE_LOADED_SW) + s->opt[OPT_PAGE_LOADED_SW].cap = + SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED; + else + s->opt[OPT_PAGE_LOADED_SW].cap = SANE_CAP_INACTIVE; + + /* OCR button */ + s->opt[OPT_OCR_SW].name = "ocr"; + s->opt[OPT_OCR_SW].title = SANE_I18N ("OCR button"); + s->opt[OPT_OCR_SW].desc = SANE_I18N ("OCR button"); + s->opt[OPT_OCR_SW].type = SANE_TYPE_BOOL; + s->opt[OPT_OCR_SW].unit = SANE_UNIT_NONE; + if (model->buttons & GENESYS_HAS_OCR_SW) + s->opt[OPT_OCR_SW].cap = + SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED; + else + s->opt[OPT_OCR_SW].cap = SANE_CAP_INACTIVE; + + /* power button */ + s->opt[OPT_POWER_SW].name = "power"; + s->opt[OPT_POWER_SW].title = SANE_I18N ("Power button"); + s->opt[OPT_POWER_SW].desc = SANE_I18N ("Power button"); + s->opt[OPT_POWER_SW].type = SANE_TYPE_BOOL; + s->opt[OPT_POWER_SW].unit = SANE_UNIT_NONE; + if (model->buttons & GENESYS_HAS_POWER_SW) + s->opt[OPT_POWER_SW].cap = + SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED; + else + s->opt[OPT_POWER_SW].cap = SANE_CAP_INACTIVE; + + /* extra button */ + s->opt[OPT_EXTRA_SW].name = "extra"; + s->opt[OPT_EXTRA_SW].title = SANE_I18N ("Extra button"); + s->opt[OPT_EXTRA_SW].desc = SANE_I18N ("Extra button"); + s->opt[OPT_EXTRA_SW].type = SANE_TYPE_BOOL; + s->opt[OPT_EXTRA_SW].unit = SANE_UNIT_NONE; + if (model->buttons & GENESYS_HAS_EXTRA_SW) + s->opt[OPT_EXTRA_SW].cap = + SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED; + else + s->opt[OPT_EXTRA_SW].cap = SANE_CAP_INACTIVE; + + /* calibration needed */ + s->opt[OPT_NEED_CALIBRATION_SW].name = "need-calibration"; + s->opt[OPT_NEED_CALIBRATION_SW].title = SANE_I18N ("Needs calibration"); + s->opt[OPT_NEED_CALIBRATION_SW].desc = SANE_I18N ("The scanner needs calibration for the current settings"); + s->opt[OPT_NEED_CALIBRATION_SW].type = SANE_TYPE_BOOL; + s->opt[OPT_NEED_CALIBRATION_SW].unit = SANE_UNIT_NONE; + if (model->buttons & GENESYS_HAS_CALIBRATE) + s->opt[OPT_NEED_CALIBRATION_SW].cap = + SANE_CAP_SOFT_DETECT | SANE_CAP_HARD_SELECT | SANE_CAP_ADVANCED; + else + s->opt[OPT_NEED_CALIBRATION_SW].cap = SANE_CAP_INACTIVE; + + /* button group */ + s->opt[OPT_BUTTON_GROUP].name = "buttons"; + s->opt[OPT_BUTTON_GROUP].title = SANE_I18N ("Buttons"); + s->opt[OPT_BUTTON_GROUP].desc = ""; + s->opt[OPT_BUTTON_GROUP].type = SANE_TYPE_GROUP; + s->opt[OPT_BUTTON_GROUP].cap = SANE_CAP_ADVANCED; + s->opt[OPT_BUTTON_GROUP].size = 0; + s->opt[OPT_BUTTON_GROUP].constraint_type = SANE_CONSTRAINT_NONE; + + /* calibrate button */ + s->opt[OPT_CALIBRATE].name = "calibrate"; + s->opt[OPT_CALIBRATE].title = SANE_I18N ("Calibrate"); + s->opt[OPT_CALIBRATE].desc = + SANE_I18N ("Start calibration using special sheet"); + s->opt[OPT_CALIBRATE].type = SANE_TYPE_BUTTON; + s->opt[OPT_CALIBRATE].unit = SANE_UNIT_NONE; + if (model->buttons & GENESYS_HAS_CALIBRATE) + s->opt[OPT_CALIBRATE].cap = + SANE_CAP_SOFT_DETECT | SANE_CAP_SOFT_SELECT | SANE_CAP_ADVANCED | + SANE_CAP_AUTOMATIC; + else + s->opt[OPT_CALIBRATE].cap = SANE_CAP_INACTIVE; + + /* clear calibration cache button */ + s->opt[OPT_CLEAR_CALIBRATION].name = "clear-calibration"; + s->opt[OPT_CLEAR_CALIBRATION].title = SANE_I18N ("Clear calibration"); + s->opt[OPT_CLEAR_CALIBRATION].desc = SANE_I18N ("Clear calibration cache"); + s->opt[OPT_CLEAR_CALIBRATION].type = SANE_TYPE_BUTTON; + s->opt[OPT_CLEAR_CALIBRATION].unit = SANE_UNIT_NONE; + s->opt[OPT_CLEAR_CALIBRATION].size = 0; + s->opt[OPT_CLEAR_CALIBRATION].constraint_type = SANE_CONSTRAINT_NONE; + s->opt[OPT_CLEAR_CALIBRATION].cap = + SANE_CAP_SOFT_DETECT | SANE_CAP_SOFT_SELECT | SANE_CAP_ADVANCED; + + /* force calibration cache button */ + s->opt[OPT_FORCE_CALIBRATION].name = "force-calibration"; + s->opt[OPT_FORCE_CALIBRATION].title = SANE_I18N("Force calibration"); + s->opt[OPT_FORCE_CALIBRATION].desc = SANE_I18N("Force calibration ignoring all and any calibration caches"); + s->opt[OPT_FORCE_CALIBRATION].type = SANE_TYPE_BUTTON; + s->opt[OPT_FORCE_CALIBRATION].unit = SANE_UNIT_NONE; + s->opt[OPT_FORCE_CALIBRATION].size = 0; + s->opt[OPT_FORCE_CALIBRATION].constraint_type = SANE_CONSTRAINT_NONE; + s->opt[OPT_FORCE_CALIBRATION].cap = + SANE_CAP_SOFT_DETECT | SANE_CAP_SOFT_SELECT | SANE_CAP_ADVANCED; + + // ignore offsets option + s->opt[OPT_IGNORE_OFFSETS].name = "ignore-internal-offsets"; + s->opt[OPT_IGNORE_OFFSETS].title = SANE_I18N("Ignore internal offsets"); + s->opt[OPT_IGNORE_OFFSETS].desc = + SANE_I18N("Acquires the image including the internal calibration areas of the scanner"); + s->opt[OPT_IGNORE_OFFSETS].type = SANE_TYPE_BUTTON; + s->opt[OPT_IGNORE_OFFSETS].unit = SANE_UNIT_NONE; + s->opt[OPT_IGNORE_OFFSETS].size = 0; + s->opt[OPT_IGNORE_OFFSETS].constraint_type = SANE_CONSTRAINT_NONE; + s->opt[OPT_IGNORE_OFFSETS].cap = SANE_CAP_SOFT_DETECT | SANE_CAP_SOFT_SELECT | + SANE_CAP_ADVANCED; + + calc_parameters(s); +} + +static bool present; + +// this function is passed to C API, it must not throw +static SANE_Status +check_present (SANE_String_Const devname) noexcept +{ + DBG_HELPER_ARGS(dbg, "%s detected.", devname); + present = true; + return SANE_STATUS_GOOD; +} + +static Genesys_Device* attach_usb_device(const char* devname, + std::uint16_t vendor_id, std::uint16_t product_id) +{ + Genesys_USB_Device_Entry* found_usb_dev = nullptr; + for (auto& usb_dev : *s_usb_devices) { + if (usb_dev.vendor == vendor_id && + usb_dev.product == product_id) + { + found_usb_dev = &usb_dev; + break; + } + } + + if (found_usb_dev == nullptr) { + throw SaneException("vendor 0x%xd product 0x%xd is not supported by this backend", + vendor_id, product_id); + } + + s_devices->emplace_back(); + Genesys_Device* dev = &s_devices->back(); + dev->file_name = devname; + + dev->model = &found_usb_dev->model; + dev->vendorId = found_usb_dev->vendor; + dev->productId = found_usb_dev->product; + dev->usb_mode = 0; // i.e. unset + dev->already_initialized = false; + return dev; +} + +static Genesys_Device* attach_device_by_name(SANE_String_Const devname, bool may_wait) +{ + DBG_HELPER_ARGS(dbg, " devname: %s, may_wait = %d", devname, may_wait); + + if (!devname) { + throw SaneException("devname must not be nullptr"); + } + + for (auto& dev : *s_devices) { + if (dev.file_name == devname) { + DBG(DBG_info, "%s: device `%s' was already in device list\n", __func__, devname); + return &dev; + } + } + + DBG(DBG_info, "%s: trying to open device `%s'\n", __func__, devname); + + UsbDevice usb_dev; + + usb_dev.open(devname); + DBG(DBG_info, "%s: device `%s' successfully opened\n", __func__, devname); + + int vendor, product; + usb_dev.get_vendor_product(vendor, product); + usb_dev.close(); + + /* KV-SS080 is an auxiliary device which requires a master device to be here */ + if(vendor == 0x04da && product == 0x100f) + { + present = false; + sanei_usb_find_devices (vendor, 0x1006, check_present); + sanei_usb_find_devices (vendor, 0x1007, check_present); + sanei_usb_find_devices (vendor, 0x1010, check_present); + if (present == false) { + throw SaneException("master device not present"); + } + } + + Genesys_Device* dev = attach_usb_device(devname, vendor, product); + + DBG(DBG_info, "%s: found %s flatbed scanner %s at %s\n", __func__, dev->model->vendor, + dev->model->model, dev->file_name.c_str()); + + return dev; +} + +// this function is passed to C API and must not throw +static SANE_Status attach_one_device(SANE_String_Const devname) noexcept +{ + DBG_HELPER(dbg); + return wrap_exceptions_to_status_code(__func__, [=]() + { + attach_device_by_name(devname, false); + }); +} + +/* configuration framework functions */ + +// this function is passed to C API, it must not throw +static SANE_Status +config_attach_genesys(SANEI_Config __sane_unused__ *config, const char *devname) noexcept +{ + /* the devname has been processed and is ready to be used + * directly. Since the backend is an USB only one, we can + * call sanei_usb_attach_matching_devices straight */ + sanei_usb_attach_matching_devices (devname, attach_one_device); + + return SANE_STATUS_GOOD; +} + +/* probes for scanner to attach to the backend */ +static void probe_genesys_devices() +{ + DBG_HELPER(dbg); + if (is_testing_mode()) { + attach_usb_device(get_testing_device_name().c_str(), + get_testing_vendor_id(), get_testing_product_id()); + return; + } + + SANEI_Config config; + + // set configuration options structure : no option for this backend + config.descriptors = nullptr; + config.values = nullptr; + config.count = 0; + + TIE(sanei_configure_attach(GENESYS_CONFIG_FILE, &config, config_attach_genesys)); + + DBG(DBG_info, "%s: %zu devices currently attached\n", __func__, s_devices->size()); +} + +/** + * This should be changed if one of the substructures of + Genesys_Calibration_Cache change, but it must be changed if there are + changes that don't change size -- at least for now, as we store most + of Genesys_Calibration_Cache as is. +*/ +static const char* CALIBRATION_IDENT = "sane_genesys"; +static const int CALIBRATION_VERSION = 21; + +bool read_calibration(std::istream& str, Genesys_Device::Calibration& calibration, + const std::string& path) +{ + DBG_HELPER(dbg); + + std::string ident; + serialize(str, ident); + + if (ident != CALIBRATION_IDENT) { + DBG(DBG_info, "%s: Incorrect calibration file '%s' header\n", __func__, path.c_str()); + return false; + } + + size_t version; + serialize(str, version); + + if (version != CALIBRATION_VERSION) { + DBG(DBG_info, "%s: Incorrect calibration file '%s' version\n", __func__, path.c_str()); + return false; + } + + calibration.clear(); + serialize(str, calibration); + return true; +} + +/** + * reads previously cached calibration data + * from file defined in dev->calib_file + */ +static bool sanei_genesys_read_calibration(Genesys_Device::Calibration& calibration, + const std::string& path) +{ + DBG_HELPER(dbg); + + std::ifstream str; + str.open(path); + if (!str.is_open()) { + DBG(DBG_info, "%s: Cannot open %s\n", __func__, path.c_str()); + return false; + } + + return read_calibration(str, calibration, path); +} + +void write_calibration(std::ostream& str, Genesys_Device::Calibration& calibration) +{ + std::string ident = CALIBRATION_IDENT; + serialize(str, ident); + size_t version = CALIBRATION_VERSION; + serialize(str, version); + serialize_newline(str); + serialize(str, calibration); +} + +static void write_calibration(Genesys_Device::Calibration& calibration, const std::string& path) +{ + DBG_HELPER(dbg); + + std::ofstream str; + str.open(path); + if (!str.is_open()) { + throw SaneException("Cannot open calibration for writing"); + } + write_calibration(str, calibration); +} + +/** @brief buffer scanned picture + * In order to allow digital processing, we must be able to put all the + * scanned picture in a buffer. + */ +static void genesys_buffer_image(Genesys_Scanner *s) +{ + DBG_HELPER(dbg); + size_t maximum; /**> maximum bytes size of the scan */ + size_t len; /**> length of scanned data read */ + size_t total; /**> total of butes read */ + size_t size; /**> size of image buffer */ + size_t read_size; /**> size of reads */ + int lines; /** number of lines of the scan */ + Genesys_Device *dev = s->dev; + + /* compute maximum number of lines for the scan */ + if (s->params.lines > 0) + { + lines = s->params.lines; + } + else + { + lines = static_cast<int>((dev->model->y_size * dev->settings.yres) / MM_PER_INCH); + } + DBG(DBG_info, "%s: buffering %d lines of %d bytes\n", __func__, lines, + s->params.bytes_per_line); + + /* maximum bytes to read */ + maximum = s->params.bytes_per_line * lines; + if (s->dev->settings.scan_mode == ScanColorMode::LINEART) { + maximum *= 8; + } + + /* initial size of the read buffer */ + size = + ((2048 * 2048) / s->params.bytes_per_line) * s->params.bytes_per_line; + + /* read size */ + read_size = size / 2; + + dev->img_buffer.resize(size); + + /* loop reading data until we reach maximum or EOF */ + total = 0; + while (total < maximum) { + len = size - maximum; + if (len > read_size) + { + len = read_size; + } + + try { + genesys_read_ordered_data(dev, dev->img_buffer.data() + total, &len); + } catch (const SaneException& e) { + if (e.status() == SANE_STATUS_EOF) { + // ideally we shouldn't end up here, but because computations are duplicated and + // slightly different everywhere in the genesys backend, we have no other choice + break; + } + throw; + } + total += len; + + // do we need to enlarge read buffer ? + if (total + read_size > size) { + size += read_size; + dev->img_buffer.resize(size); + } + } + + /* since digital processing is going to take place, + * issue head parking command so that the head move while + * computing so we can save time + */ + if (!dev->model->is_sheetfed && !dev->parking) { + dev->cmd_set->move_back_home(dev, dev->model->flags & GENESYS_FLAG_MUST_WAIT); + dev->parking = !(s->dev->model->flags & GENESYS_FLAG_MUST_WAIT); + } + + /* in case of dynamic lineart, we have buffered gray data which + * must be converted to lineart first */ + if (s->dev->settings.scan_mode == ScanColorMode::LINEART) { + total/=8; + std::vector<uint8_t> lineart(total); + + genesys_gray_lineart (dev, + dev->img_buffer.data(), + lineart.data(), + dev->settings.pixels, + (total*8)/dev->settings.pixels, + dev->settings.threshold); + dev->img_buffer = lineart; + } + + /* update counters */ + dev->total_bytes_to_read = total; + dev->total_bytes_read = 0; + + /* update params */ + s->params.lines = total / s->params.bytes_per_line; + if (DBG_LEVEL >= DBG_io2) + { + sanei_genesys_write_pnm_file("gl_unprocessed.pnm", dev->img_buffer.data(), s->params.depth, + s->params.format==SANE_FRAME_RGB ? 3 : 1, + s->params.pixels_per_line, s->params.lines); + } +} + +/* -------------------------- SANE API functions ------------------------- */ + +void sane_init_impl(SANE_Int * version_code, SANE_Auth_Callback authorize) +{ + DBG_INIT (); + DBG_HELPER_ARGS(dbg, "authorize %s null", authorize ? "!=" : "=="); + DBG(DBG_init, "SANE Genesys backend from %s\n", PACKAGE_STRING); + + if (!is_testing_mode()) { +#ifdef HAVE_LIBUSB + DBG(DBG_init, "SANE Genesys backend built with libusb-1.0\n"); +#endif +#ifdef HAVE_LIBUSB_LEGACY + DBG(DBG_init, "SANE Genesys backend built with libusb\n"); +#endif + } + + if (version_code) { + *version_code = SANE_VERSION_CODE(SANE_CURRENT_MAJOR, SANE_CURRENT_MINOR, 0); + } + + if (!is_testing_mode()) { + sanei_usb_init(); + } + + /* init sanei_magic */ + sanei_magic_init(); + + s_scanners.init(); + s_devices.init(); + s_sane_devices.init(); + s_sane_devices_data.init(); + s_sane_devices_ptrs.init(); + genesys_init_sensor_tables(); + genesys_init_frontend_tables(); + genesys_init_gpo_tables(); + genesys_init_motor_tables(); + genesys_init_motor_profile_tables(); + genesys_init_usb_device_tables(); + + + DBG(DBG_info, "%s: %s endian machine\n", __func__, +#ifdef WORDS_BIGENDIAN + "big" +#else + "little" +#endif + ); + + // cold-plug case :detection of allready connected scanners + probe_genesys_devices(); +} + + +SANE_GENESYS_API_LINKAGE +SANE_Status sane_init(SANE_Int * version_code, SANE_Auth_Callback authorize) +{ + return wrap_exceptions_to_status_code(__func__, [=]() + { + sane_init_impl(version_code, authorize); + }); +} + +void +sane_exit_impl(void) +{ + DBG_HELPER(dbg); + + if (!is_testing_mode()) { + sanei_usb_exit(); + } + + run_functions_at_backend_exit(); +} + +SANE_GENESYS_API_LINKAGE +void sane_exit() +{ + catch_all_exceptions(__func__, [](){ sane_exit_impl(); }); +} + +void sane_get_devices_impl(const SANE_Device *** device_list, SANE_Bool local_only) +{ + DBG_HELPER_ARGS(dbg, "local_only = %s", local_only ? "true" : "false"); + + if (!is_testing_mode()) { + // hot-plug case : detection of newly connected scanners */ + sanei_usb_scan_devices(); + } + probe_genesys_devices(); + + s_sane_devices->clear(); + s_sane_devices_data->clear(); + s_sane_devices_ptrs->clear(); + s_sane_devices->reserve(s_devices->size()); + s_sane_devices_data->reserve(s_devices->size()); + s_sane_devices_ptrs->reserve(s_devices->size() + 1); + + for (auto dev_it = s_devices->begin(); dev_it != s_devices->end();) { + + if (is_testing_mode()) { + present = true; + } else { + present = false; + sanei_usb_find_devices(dev_it->vendorId, dev_it->productId, check_present); + } + + if (present) { + s_sane_devices->emplace_back(); + s_sane_devices_data->emplace_back(); + auto& sane_device = s_sane_devices->back(); + auto& sane_device_data = s_sane_devices_data->back(); + sane_device_data.name = dev_it->file_name; + sane_device.name = sane_device_data.name.c_str(); + sane_device.vendor = dev_it->model->vendor; + sane_device.model = dev_it->model->model; + sane_device.type = "flatbed scanner"; + s_sane_devices_ptrs->push_back(&sane_device); + dev_it++; + } else { + dev_it = s_devices->erase(dev_it); + } + } + s_sane_devices_ptrs->push_back(nullptr); + + *const_cast<SANE_Device***>(device_list) = s_sane_devices_ptrs->data(); +} + +SANE_GENESYS_API_LINKAGE +SANE_Status sane_get_devices(const SANE_Device *** device_list, SANE_Bool local_only) +{ + return wrap_exceptions_to_status_code(__func__, [=]() + { + sane_get_devices_impl(device_list, local_only); + }); +} + +static void sane_open_impl(SANE_String_Const devicename, SANE_Handle * handle) +{ + DBG_HELPER_ARGS(dbg, "devicename = %s", devicename); + Genesys_Device* dev = nullptr; + + /* devicename="" or devicename="genesys" are default values that use + * first available device + */ + if (devicename[0] && strcmp ("genesys", devicename) != 0) { + /* search for the given devicename in the device list */ + for (auto& d : *s_devices) { + if (d.file_name == devicename) { + dev = &d; + break; + } + } + + if (dev) { + DBG(DBG_info, "%s: found `%s' in devlist\n", __func__, dev->model->name); + } else if (is_testing_mode()) { + DBG(DBG_info, "%s: couldn't find `%s' in devlist, not attaching", __func__, devicename); + } else { + DBG(DBG_info, "%s: couldn't find `%s' in devlist, trying attach\n", __func__, + devicename); + dbg.status("attach_device_by_name"); + dev = attach_device_by_name(devicename, true); + dbg.clear(); + } + } else { + // empty devicename or "genesys" -> use first device + if (!s_devices->empty()) { + dev = &s_devices->front(); + DBG(DBG_info, "%s: empty devicename, trying `%s'\n", __func__, dev->file_name.c_str()); + } + } + + if (!dev) { + throw SaneException("could not find the device to open: %s", devicename); + } + + if (dev->model->flags & GENESYS_FLAG_UNTESTED) + { + DBG(DBG_error0, "WARNING: Your scanner is not fully supported or at least \n"); + DBG(DBG_error0, " had only limited testing. Please be careful and \n"); + DBG(DBG_error0, " report any failure/success to \n"); + DBG(DBG_error0, " sane-devel@alioth-lists.debian.net. Please provide as many\n"); + DBG(DBG_error0, " details as possible, e.g. the exact name of your\n"); + DBG(DBG_error0, " scanner and what does (not) work.\n"); + } + + dbg.vstatus("open device '%s'", dev->file_name.c_str()); + + if (is_testing_mode()) { + auto interface = std::unique_ptr<TestScannerInterface>{new TestScannerInterface{dev}}; + interface->set_checkpoint_callback(get_testing_checkpoint_callback()); + dev->interface = std::move(interface); + } else { + dev->interface = std::unique_ptr<ScannerInterfaceUsb>{new ScannerInterfaceUsb{dev}}; + } + dev->interface->get_usb_device().open(dev->file_name.c_str()); + dbg.clear(); + + s_scanners->push_back(Genesys_Scanner()); + auto* s = &s_scanners->back(); + + s->dev = dev; + s->scanning = false; + s->dev->parking = false; + s->dev->read_active = false; + s->dev->force_calibration = 0; + s->dev->line_count = 0; + + *handle = s; + + if (!dev->already_initialized) { + sanei_genesys_init_structs (dev); + } + + init_options(s); + + sanei_genesys_init_cmd_set(s->dev); + + // FIXME: we create sensor tables for the sensor, this should happen when we know which sensor + // we will select + dev->cmd_set->init(dev); + + // some hardware capabilities are detected through sensors + s->dev->cmd_set->update_hardware_sensors (s); + + /* here is the place to fetch a stored calibration cache */ + if (s->dev->force_calibration == 0) + { + auto path = calibration_filename(s->dev); + s->calibration_file = path; + s->dev->calib_file = path; + DBG(DBG_info, "%s: Calibration filename set to:\n", __func__); + DBG(DBG_info, "%s: >%s<\n", __func__, s->dev->calib_file.c_str()); + + catch_all_exceptions(__func__, [&]() + { + sanei_genesys_read_calibration(s->dev->calibration_cache, s->dev->calib_file); + }); + } +} + +SANE_GENESYS_API_LINKAGE +SANE_Status sane_open(SANE_String_Const devicename, SANE_Handle* handle) +{ + return wrap_exceptions_to_status_code(__func__, [=]() + { + sane_open_impl(devicename, handle); + }); +} + +void +sane_close_impl(SANE_Handle handle) +{ + DBG_HELPER(dbg); + + /* remove handle from list of open handles: */ + auto it = s_scanners->end(); + for (auto it2 = s_scanners->begin(); it2 != s_scanners->end(); it2++) + { + if (&*it2 == handle) { + it = it2; + break; + } + } + if (it == s_scanners->end()) + { + DBG(DBG_error, "%s: invalid handle %p\n", __func__, handle); + return; /* oops, not a handle we know about */ + } + + Genesys_Scanner* s = &*it; + + /* eject document for sheetfed scanners */ + if (s->dev->model->is_sheetfed) { + catch_all_exceptions(__func__, [&](){ s->dev->cmd_set->eject_document(s->dev); }); + } + else + { + /* in case scanner is parking, wait for the head + * to reach home position */ + if (s->dev->parking) { + sanei_genesys_wait_for_home(s->dev); + } + } + + // enable power saving before leaving + s->dev->cmd_set->save_power(s->dev, true); + + // here is the place to store calibration cache + if (s->dev->force_calibration == 0 && !is_testing_mode()) { + catch_all_exceptions(__func__, [&](){ write_calibration(s->dev->calibration_cache, + s->dev->calib_file); }); + } + + s->dev->already_initialized = false; + + s->dev->clear(); + + // LAMP OFF : same register across all the ASICs */ + s->dev->interface->write_register(0x03, 0x00); + + catch_all_exceptions(__func__, [&](){ s->dev->interface->get_usb_device().clear_halt(); }); + + // we need this to avoid these ASIC getting stuck in bulk writes + catch_all_exceptions(__func__, [&](){ s->dev->interface->get_usb_device().reset(); }); + + // not freeing s->dev because it's in the dev list + catch_all_exceptions(__func__, [&](){ s->dev->interface->get_usb_device().close(); }); + + s_scanners->erase(it); +} + +SANE_GENESYS_API_LINKAGE +void sane_close(SANE_Handle handle) +{ + catch_all_exceptions(__func__, [=]() + { + sane_close_impl(handle); + }); +} + +const SANE_Option_Descriptor * +sane_get_option_descriptor_impl(SANE_Handle handle, SANE_Int option) +{ + DBG_HELPER(dbg); + Genesys_Scanner* s = reinterpret_cast<Genesys_Scanner*>(handle); + + if (static_cast<unsigned>(option) >= NUM_OPTIONS) { + return nullptr; + } + + DBG(DBG_io2, "%s: option = %s (%d)\n", __func__, s->opt[option].name, option); + return s->opt + option; +} + + +SANE_GENESYS_API_LINKAGE +const SANE_Option_Descriptor* sane_get_option_descriptor(SANE_Handle handle, SANE_Int option) +{ + const SANE_Option_Descriptor* ret = nullptr; + catch_all_exceptions(__func__, [&]() + { + ret = sane_get_option_descriptor_impl(handle, option); + }); + return ret; +} + +static void print_option(DebugMessageHelper& dbg, const Genesys_Scanner& s, int option, void* val) +{ + switch (s.opt[option].type) { + case SANE_TYPE_INT: { + dbg.vlog(DBG_proc, "value: %d", *reinterpret_cast<SANE_Word*>(val)); + return; + } + case SANE_TYPE_BOOL: { + dbg.vlog(DBG_proc, "value: %s", *reinterpret_cast<SANE_Bool*>(val) ? "true" : "false"); + return; + } + case SANE_TYPE_FIXED: { + dbg.vlog(DBG_proc, "value: %f", SANE_UNFIX(*reinterpret_cast<SANE_Word*>(val))); + return; + } + case SANE_TYPE_STRING: { + dbg.vlog(DBG_proc, "value: %s", reinterpret_cast<char*>(val)); + return; + } + default: break; + } + dbg.log(DBG_proc, "value: (non-printable)"); +} + +static void get_option_value(Genesys_Scanner* s, int option, void* val) +{ + DBG_HELPER_ARGS(dbg, "option: %s (%d)", s->opt[option].name, option); + unsigned int i; + SANE_Word* table = nullptr; + std::vector<uint16_t> gamma_table; + unsigned option_size = 0; + + const Genesys_Sensor* sensor = nullptr; + if (sanei_genesys_has_sensor(s->dev, s->dev->settings.xres, s->dev->settings.get_channels(), + s->dev->settings.scan_method)) + { + sensor = &sanei_genesys_find_sensor(s->dev, s->dev->settings.xres, + s->dev->settings.get_channels(), + s->dev->settings.scan_method); + } + + switch (option) + { + /* geometry */ + case OPT_TL_X: + *reinterpret_cast<SANE_Word*>(val) = s->pos_top_left_x; + break; + case OPT_TL_Y: + *reinterpret_cast<SANE_Word*>(val) = s->pos_top_left_y; + break; + case OPT_BR_X: + *reinterpret_cast<SANE_Word*>(val) = s->pos_bottom_right_x; + break; + case OPT_BR_Y: + *reinterpret_cast<SANE_Word*>(val) = s->pos_bottom_right_y; + break; + /* word options: */ + case OPT_NUM_OPTS: + *reinterpret_cast<SANE_Word*>(val) = NUM_OPTIONS; + break; + case OPT_RESOLUTION: + *reinterpret_cast<SANE_Word*>(val) = s->resolution; + break; + case OPT_BIT_DEPTH: + *reinterpret_cast<SANE_Word*>(val) = s->bit_depth; + break; + case OPT_PREVIEW: + *reinterpret_cast<SANE_Word*>(val) = s->preview; + break; + case OPT_THRESHOLD: + *reinterpret_cast<SANE_Word*>(val) = s->threshold; + break; + case OPT_THRESHOLD_CURVE: + *reinterpret_cast<SANE_Word*>(val) = s->threshold_curve; + break; + case OPT_DISABLE_INTERPOLATION: + *reinterpret_cast<SANE_Word*>(val) = s->disable_interpolation; + break; + case OPT_LAMP_OFF: + *reinterpret_cast<SANE_Word*>(val) = s->lamp_off; + break; + case OPT_LAMP_OFF_TIME: + *reinterpret_cast<SANE_Word*>(val) = s->lamp_off_time; + break; + case OPT_SWDESKEW: + *reinterpret_cast<SANE_Word*>(val) = s->swdeskew; + break; + case OPT_SWCROP: + *reinterpret_cast<SANE_Word*>(val) = s->swcrop; + break; + case OPT_SWDESPECK: + *reinterpret_cast<SANE_Word*>(val) = s->swdespeck; + break; + case OPT_SWDEROTATE: + *reinterpret_cast<SANE_Word*>(val) = s->swderotate; + break; + case OPT_SWSKIP: + *reinterpret_cast<SANE_Word*>(val) = s->swskip; + break; + case OPT_DESPECK: + *reinterpret_cast<SANE_Word*>(val) = s->despeck; + break; + case OPT_CONTRAST: + *reinterpret_cast<SANE_Word*>(val) = s->contrast; + break; + case OPT_BRIGHTNESS: + *reinterpret_cast<SANE_Word*>(val) = s->brightness; + break; + case OPT_EXPIRATION_TIME: + *reinterpret_cast<SANE_Word*>(val) = s->expiration_time; + break; + case OPT_CUSTOM_GAMMA: + *reinterpret_cast<SANE_Word*>(val) = s->custom_gamma; + break; + + /* string options: */ + case OPT_MODE: + std::strcpy(reinterpret_cast<char*>(val), s->mode.c_str()); + break; + case OPT_COLOR_FILTER: + std::strcpy(reinterpret_cast<char*>(val), s->color_filter.c_str()); + break; + case OPT_CALIBRATION_FILE: + std::strcpy(reinterpret_cast<char*>(val), s->calibration_file.c_str()); + break; + case OPT_SOURCE: + std::strcpy(reinterpret_cast<char*>(val), scan_method_to_option_string(s->scan_method)); + break; + + /* word array options */ + case OPT_GAMMA_VECTOR: + if (!sensor) + throw SaneException("Unsupported scanner mode selected"); + + table = reinterpret_cast<SANE_Word*>(val); + if (s->color_filter == "Red") { + gamma_table = get_gamma_table(s->dev, *sensor, GENESYS_RED); + } else if (s->color_filter == "Blue") { + gamma_table = get_gamma_table(s->dev, *sensor, GENESYS_BLUE); + } else { + gamma_table = get_gamma_table(s->dev, *sensor, GENESYS_GREEN); + } + option_size = s->opt[option].size / sizeof (SANE_Word); + if (gamma_table.size() != option_size) { + throw std::runtime_error("The size of the gamma tables does not match"); + } + for (i = 0; i < option_size; i++) { + table[i] = gamma_table[i]; + } + break; + case OPT_GAMMA_VECTOR_R: + if (!sensor) + throw SaneException("Unsupported scanner mode selected"); + + table = reinterpret_cast<SANE_Word*>(val); + gamma_table = get_gamma_table(s->dev, *sensor, GENESYS_RED); + option_size = s->opt[option].size / sizeof (SANE_Word); + if (gamma_table.size() != option_size) { + throw std::runtime_error("The size of the gamma tables does not match"); + } + for (i = 0; i < option_size; i++) { + table[i] = gamma_table[i]; + } + break; + case OPT_GAMMA_VECTOR_G: + if (!sensor) + throw SaneException("Unsupported scanner mode selected"); + + table = reinterpret_cast<SANE_Word*>(val); + gamma_table = get_gamma_table(s->dev, *sensor, GENESYS_GREEN); + option_size = s->opt[option].size / sizeof (SANE_Word); + if (gamma_table.size() != option_size) { + throw std::runtime_error("The size of the gamma tables does not match"); + } + for (i = 0; i < option_size; i++) { + table[i] = gamma_table[i]; + } + break; + case OPT_GAMMA_VECTOR_B: + if (!sensor) + throw SaneException("Unsupported scanner mode selected"); + + table = reinterpret_cast<SANE_Word*>(val); + gamma_table = get_gamma_table(s->dev, *sensor, GENESYS_BLUE); + option_size = s->opt[option].size / sizeof (SANE_Word); + if (gamma_table.size() != option_size) { + throw std::runtime_error("The size of the gamma tables does not match"); + } + for (i = 0; i < option_size; i++) { + table[i] = gamma_table[i]; + } + break; + /* sensors */ + case OPT_SCAN_SW: + case OPT_FILE_SW: + case OPT_EMAIL_SW: + case OPT_COPY_SW: + case OPT_PAGE_LOADED_SW: + case OPT_OCR_SW: + case OPT_POWER_SW: + case OPT_EXTRA_SW: + s->dev->cmd_set->update_hardware_sensors(s); + *reinterpret_cast<SANE_Bool*>(val) = s->buttons[genesys_option_to_button(option)].read(); + break; + + case OPT_NEED_CALIBRATION_SW: { + if (!sensor) { + throw SaneException("Unsupported scanner mode selected"); + } + + // scanner needs calibration for current mode unless a matching calibration cache is + // found + + bool result = true; + + auto session = s->dev->cmd_set->calculate_scan_session(s->dev, *sensor, + s->dev->settings); + + for (auto& cache : s->dev->calibration_cache) { + if (sanei_genesys_is_compatible_calibration(s->dev, session, &cache, false)) { + *reinterpret_cast<SANE_Bool*>(val) = SANE_FALSE; + } + } + *reinterpret_cast<SANE_Bool*>(val) = result; + break; + } + default: + DBG(DBG_warn, "%s: can't get unknown option %d\n", __func__, option); + } + print_option(dbg, *s, option, val); +} + +/** @brief set calibration file value + * Set calibration file value. Load new cache values from file if it exists, + * else creates the file*/ +static void set_calibration_value(Genesys_Scanner* s, const char* val) +{ + DBG_HELPER(dbg); + + std::string new_calib_path = val; + Genesys_Device::Calibration new_calibration; + + bool is_calib_success = false; + catch_all_exceptions(__func__, [&]() + { + is_calib_success = sanei_genesys_read_calibration(new_calibration, new_calib_path); + }); + + if (!is_calib_success) { + return; + } + + s->dev->calibration_cache = std::move(new_calibration); + s->dev->calib_file = new_calib_path; + s->calibration_file = new_calib_path; + DBG(DBG_info, "%s: Calibration filename set to '%s':\n", __func__, new_calib_path.c_str()); +} + +/* sets an option , called by sane_control_option */ +static void set_option_value(Genesys_Scanner* s, int option, void *val, SANE_Int* myinfo) +{ + DBG_HELPER_ARGS(dbg, "option: %s (%d)", s->opt[option].name, option); + print_option(dbg, *s, option, val); + + SANE_Word *table; + unsigned int i; + unsigned option_size = 0; + + switch (option) + { + case OPT_TL_X: + s->pos_top_left_x = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_TL_Y: + s->pos_top_left_y = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_BR_X: + s->pos_bottom_right_x = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_BR_Y: + s->pos_bottom_right_y = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_RESOLUTION: + s->resolution = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_THRESHOLD: + s->threshold = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_THRESHOLD_CURVE: + s->threshold_curve = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_SWCROP: + s->swcrop = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_SWDESKEW: + s->swdeskew = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_DESPECK: + s->despeck = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_SWDEROTATE: + s->swderotate = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_SWSKIP: + s->swskip = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_DISABLE_INTERPOLATION: + s->disable_interpolation = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_LAMP_OFF: + s->lamp_off = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_PREVIEW: + s->preview = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_BRIGHTNESS: + s->brightness = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_CONTRAST: + s->contrast = *reinterpret_cast<SANE_Word*>(val); + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS; + break; + case OPT_SWDESPECK: + s->swdespeck = *reinterpret_cast<SANE_Word*>(val); + if (s->swdespeck) { + ENABLE(OPT_DESPECK); + } else { + DISABLE(OPT_DESPECK); + } + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS; + break; + /* software enhancement functions only apply to 8 or 1 bits data */ + case OPT_BIT_DEPTH: + s->bit_depth = *reinterpret_cast<SANE_Word*>(val); + if(s->bit_depth>8) + { + DISABLE(OPT_SWDESKEW); + DISABLE(OPT_SWDESPECK); + DISABLE(OPT_SWCROP); + DISABLE(OPT_DESPECK); + DISABLE(OPT_SWDEROTATE); + DISABLE(OPT_SWSKIP); + DISABLE(OPT_CONTRAST); + DISABLE(OPT_BRIGHTNESS); + } + else + { + ENABLE(OPT_SWDESKEW); + ENABLE(OPT_SWDESPECK); + ENABLE(OPT_SWCROP); + ENABLE(OPT_DESPECK); + ENABLE(OPT_SWDEROTATE); + ENABLE(OPT_SWSKIP); + ENABLE(OPT_CONTRAST); + ENABLE(OPT_BRIGHTNESS); + } + calc_parameters(s); + *myinfo |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS; + break; + case OPT_SOURCE: { + auto scan_method = option_string_to_scan_method(reinterpret_cast<const char*>(val)); + if (s->scan_method != scan_method) { + s->scan_method = scan_method; + + set_xy_range_option_values(*s); + set_resolution_option_values(*s, false); + + *myinfo |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS; + } + break; + } + case OPT_MODE: + s->mode = reinterpret_cast<const char*>(val); + + if (s->mode == SANE_VALUE_SCAN_MODE_LINEART) + { + ENABLE (OPT_THRESHOLD); + ENABLE (OPT_THRESHOLD_CURVE); + DISABLE (OPT_BIT_DEPTH); + if (s->dev->model->asic_type != AsicType::GL646 || !s->dev->model->is_cis) { + ENABLE(OPT_COLOR_FILTER); + } + } + else + { + DISABLE (OPT_THRESHOLD); + DISABLE (OPT_THRESHOLD_CURVE); + if (s->mode == SANE_VALUE_SCAN_MODE_GRAY) + { + if (s->dev->model->asic_type != AsicType::GL646 || !s->dev->model->is_cis) { + ENABLE(OPT_COLOR_FILTER); + } + create_bpp_list (s, s->dev->model->bpp_gray_values); + s->bit_depth = s->dev->model->bpp_gray_values[0]; + } + else + { + DISABLE (OPT_COLOR_FILTER); + create_bpp_list (s, s->dev->model->bpp_color_values); + s->bit_depth = s->dev->model->bpp_color_values[0]; + } + } + calc_parameters(s); + + /* if custom gamma, toggle gamma table options according to the mode */ + if (s->custom_gamma) + { + if (s->mode == SANE_VALUE_SCAN_MODE_COLOR) + { + DISABLE (OPT_GAMMA_VECTOR); + ENABLE (OPT_GAMMA_VECTOR_R); + ENABLE (OPT_GAMMA_VECTOR_G); + ENABLE (OPT_GAMMA_VECTOR_B); + } + else + { + ENABLE (OPT_GAMMA_VECTOR); + DISABLE (OPT_GAMMA_VECTOR_R); + DISABLE (OPT_GAMMA_VECTOR_G); + DISABLE (OPT_GAMMA_VECTOR_B); + } + } + + *myinfo |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS; + break; + case OPT_COLOR_FILTER: + s->color_filter = reinterpret_cast<const char*>(val); + calc_parameters(s); + break; + case OPT_CALIBRATION_FILE: + if (s->dev->force_calibration == 0) { + set_calibration_value(s, reinterpret_cast<const char*>(val)); + } + break; + case OPT_LAMP_OFF_TIME: + if (*reinterpret_cast<SANE_Word*>(val) != s->lamp_off_time) { + s->lamp_off_time = *reinterpret_cast<SANE_Word*>(val); + s->dev->cmd_set->set_powersaving(s->dev, s->lamp_off_time); + } + break; + case OPT_EXPIRATION_TIME: + if (*reinterpret_cast<SANE_Word*>(val) != s->expiration_time) { + s->expiration_time = *reinterpret_cast<SANE_Word*>(val); + // BUG: this is most likely not intended behavior, found out during refactor + s->dev->cmd_set->set_powersaving(s->dev, s->expiration_time); + } + break; + + case OPT_CUSTOM_GAMMA: + *myinfo |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS; + s->custom_gamma = *reinterpret_cast<SANE_Bool*>(val); + + if (s->custom_gamma) { + if (s->mode == SANE_VALUE_SCAN_MODE_COLOR) + { + DISABLE (OPT_GAMMA_VECTOR); + ENABLE (OPT_GAMMA_VECTOR_R); + ENABLE (OPT_GAMMA_VECTOR_G); + ENABLE (OPT_GAMMA_VECTOR_B); + } + else + { + ENABLE (OPT_GAMMA_VECTOR); + DISABLE (OPT_GAMMA_VECTOR_R); + DISABLE (OPT_GAMMA_VECTOR_G); + DISABLE (OPT_GAMMA_VECTOR_B); + } + } + else + { + DISABLE (OPT_GAMMA_VECTOR); + DISABLE (OPT_GAMMA_VECTOR_R); + DISABLE (OPT_GAMMA_VECTOR_G); + DISABLE (OPT_GAMMA_VECTOR_B); + for (auto& table : s->dev->gamma_override_tables) { + table.clear(); + } + } + break; + + case OPT_GAMMA_VECTOR: + table = reinterpret_cast<SANE_Word*>(val); + option_size = s->opt[option].size / sizeof (SANE_Word); + + s->dev->gamma_override_tables[GENESYS_RED].resize(option_size); + s->dev->gamma_override_tables[GENESYS_GREEN].resize(option_size); + s->dev->gamma_override_tables[GENESYS_BLUE].resize(option_size); + for (i = 0; i < option_size; i++) { + s->dev->gamma_override_tables[GENESYS_RED][i] = table[i]; + s->dev->gamma_override_tables[GENESYS_GREEN][i] = table[i]; + s->dev->gamma_override_tables[GENESYS_BLUE][i] = table[i]; + } + break; + case OPT_GAMMA_VECTOR_R: + table = reinterpret_cast<SANE_Word*>(val); + option_size = s->opt[option].size / sizeof (SANE_Word); + s->dev->gamma_override_tables[GENESYS_RED].resize(option_size); + for (i = 0; i < option_size; i++) { + s->dev->gamma_override_tables[GENESYS_RED][i] = table[i]; + } + break; + case OPT_GAMMA_VECTOR_G: + table = reinterpret_cast<SANE_Word*>(val); + option_size = s->opt[option].size / sizeof (SANE_Word); + s->dev->gamma_override_tables[GENESYS_GREEN].resize(option_size); + for (i = 0; i < option_size; i++) { + s->dev->gamma_override_tables[GENESYS_GREEN][i] = table[i]; + } + break; + case OPT_GAMMA_VECTOR_B: + table = reinterpret_cast<SANE_Word*>(val); + option_size = s->opt[option].size / sizeof (SANE_Word); + s->dev->gamma_override_tables[GENESYS_BLUE].resize(option_size); + for (i = 0; i < option_size; i++) { + s->dev->gamma_override_tables[GENESYS_BLUE][i] = table[i]; + } + break; + case OPT_CALIBRATE: { + auto& sensor = sanei_genesys_find_sensor_for_write(s->dev, s->dev->settings.xres, + s->dev->settings.get_channels(), + s->dev->settings.scan_method); + catch_all_exceptions(__func__, [&]() + { + s->dev->cmd_set->save_power(s->dev, false); + genesys_scanner_calibration(s->dev, sensor); + }); + catch_all_exceptions(__func__, [&]() + { + s->dev->cmd_set->save_power(s->dev, true); + }); + *myinfo |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS; + break; + } + case OPT_CLEAR_CALIBRATION: + s->dev->calibration_cache.clear(); + + /* remove file */ + unlink(s->dev->calib_file.c_str()); + /* signals that sensors will have to be read again */ + *myinfo |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS; + break; + case OPT_FORCE_CALIBRATION: + s->dev->force_calibration = 1; + s->dev->calibration_cache.clear(); + s->dev->calib_file.clear(); + + /* signals that sensors will have to be read again */ + *myinfo |= SANE_INFO_RELOAD_PARAMS | SANE_INFO_RELOAD_OPTIONS; + break; + + case OPT_IGNORE_OFFSETS: { + s->dev->ignore_offsets = true; + break; + } + default: + DBG(DBG_warn, "%s: can't set unknown option %d\n", __func__, option); + } +} + + +/* sets and gets scanner option values */ +void sane_control_option_impl(SANE_Handle handle, SANE_Int option, + SANE_Action action, void *val, SANE_Int * info) +{ + Genesys_Scanner* s = reinterpret_cast<Genesys_Scanner*>(handle); + auto action_str = (action == SANE_ACTION_GET_VALUE) ? "get" : + (action == SANE_ACTION_SET_VALUE) ? "set" : + (action == SANE_ACTION_SET_AUTO) ? "set_auto" : "unknown"; + DBG_HELPER_ARGS(dbg, "action = %s, option = %s (%d)", action_str, + s->opt[option].name, option); + + SANE_Word cap; + SANE_Int myinfo = 0; + + if (info) { + *info = 0; + } + + if (s->scanning) { + throw SaneException(SANE_STATUS_DEVICE_BUSY, + "don't call this function while scanning (option = %s (%d))", + s->opt[option].name, option); + } + if (option >= NUM_OPTIONS || option < 0) { + throw SaneException("option %d >= NUM_OPTIONS || option < 0", option); + } + + cap = s->opt[option].cap; + + if (!SANE_OPTION_IS_ACTIVE (cap)) { + throw SaneException("option %d is inactive", option); + } + + switch (action) { + case SANE_ACTION_GET_VALUE: + get_option_value(s, option, val); + break; + + case SANE_ACTION_SET_VALUE: + if (!SANE_OPTION_IS_SETTABLE (cap)) { + throw SaneException("option %d is not settable", option); + } + + TIE(sanei_constrain_value(s->opt + option, val, &myinfo)); + + set_option_value(s, option, val, &myinfo); + break; + + case SANE_ACTION_SET_AUTO: + throw SaneException("SANE_ACTION_SET_AUTO unsupported since no option " + "has SANE_CAP_AUTOMATIC"); + default: + throw SaneException("unknown action %d for option %d", action, option); + } + + if (info) + *info = myinfo; +} + +SANE_GENESYS_API_LINKAGE +SANE_Status sane_control_option(SANE_Handle handle, SANE_Int option, + SANE_Action action, void *val, SANE_Int * info) +{ + return wrap_exceptions_to_status_code(__func__, [=]() + { + sane_control_option_impl(handle, option, action, val, info); + }); +} + +void sane_get_parameters_impl(SANE_Handle handle, SANE_Parameters* params) +{ + DBG_HELPER(dbg); + Genesys_Scanner* s = reinterpret_cast<Genesys_Scanner*>(handle); + + /* don't recompute parameters once data reading is active, ie during scan */ + if (!s->dev->read_active) { + calc_parameters(s); + } + if (params) + { + *params = s->params; + + /* in the case of a sheetfed scanner, when full height is specified + * we override the computed line number with -1 to signal that we + * don't know the real document height. + * We don't do that doing buffering image for digital processing + */ + if (s->dev->model->is_sheetfed && !s->dev->buffer_image && + s->pos_bottom_right_y == s->opt[OPT_BR_Y].constraint.range->max) + { + params->lines = -1; + } + } + debug_dump(DBG_proc, *params); +} + +SANE_GENESYS_API_LINKAGE +SANE_Status sane_get_parameters(SANE_Handle handle, SANE_Parameters* params) +{ + return wrap_exceptions_to_status_code(__func__, [=]() + { + sane_get_parameters_impl(handle, params); + }); +} + +void sane_start_impl(SANE_Handle handle) +{ + DBG_HELPER(dbg); + Genesys_Scanner* s = reinterpret_cast<Genesys_Scanner*>(handle); + + if (s->pos_top_left_x >= s->pos_bottom_right_x) { + throw SaneException("top left x >= bottom right x"); + } + if (s->pos_top_left_y >= s->pos_bottom_right_y) { + throw SaneException("top left y >= bottom right y"); + } + + /* First make sure we have a current parameter set. Some of the + parameters will be overwritten below, but that's OK. */ + + calc_parameters(s); + genesys_start_scan(s->dev, s->lamp_off); + + s->scanning = true; + + /* allocate intermediate buffer when doing dynamic lineart */ + if (s->dev->settings.scan_mode == ScanColorMode::LINEART) { + s->dev->binarize_buffer.clear(); + s->dev->binarize_buffer.alloc(s->dev->settings.pixels); + s->dev->local_buffer.clear(); + s->dev->local_buffer.alloc(s->dev->binarize_buffer.size() * 8); + } + + /* if one of the software enhancement option is selected, + * we do the scan internally, process picture then put it an internal + * buffer. Since cropping may change scan parameters, we recompute them + * at the end */ + if (s->dev->buffer_image) + { + genesys_buffer_image(s); + + /* check if we need to skip this page, sheetfed scanners + * can go to next doc while flatbed ones can't */ + if (s->swskip > 0 && IS_ACTIVE(OPT_SWSKIP)) { + auto status = sanei_magic_isBlank(&s->params, + s->dev->img_buffer.data(), + SANE_UNFIX(s->swskip)); + + if (status == SANE_STATUS_NO_DOCS && s->dev->model->is_sheetfed) { + DBG(DBG_info, "%s: blank page, recurse\n", __func__); + sane_start(handle); + return; + } + + if (status != SANE_STATUS_GOOD) { + throw SaneException(status); + } + } + + if (s->swdeskew) { + const auto& sensor = sanei_genesys_find_sensor(s->dev, s->dev->settings.xres, + s->dev->settings.get_channels(), + s->dev->settings.scan_method); + catch_all_exceptions(__func__, [&](){ genesys_deskew(s, sensor); }); + } + + if (s->swdespeck) { + catch_all_exceptions(__func__, [&](){ genesys_despeck(s); }); + } + + if(s->swcrop) { + catch_all_exceptions(__func__, [&](){ genesys_crop(s); }); + } + + if(s->swderotate) { + catch_all_exceptions(__func__, [&](){ genesys_derotate(s); }); + } + } +} + +SANE_GENESYS_API_LINKAGE +SANE_Status sane_start(SANE_Handle handle) +{ + return wrap_exceptions_to_status_code(__func__, [=]() + { + sane_start_impl(handle); + }); +} + +void sane_read_impl(SANE_Handle handle, SANE_Byte * buf, SANE_Int max_len, SANE_Int* len) +{ + DBG_HELPER(dbg); + Genesys_Scanner* s = reinterpret_cast<Genesys_Scanner*>(handle); + Genesys_Device *dev; + size_t local_len; + + if (!s) { + throw SaneException("handle is nullptr"); + } + + dev=s->dev; + if (!dev) { + throw SaneException("dev is nullptr"); + } + + if (!buf) { + throw SaneException("buf is nullptr"); + } + + if (!len) { + throw SaneException("len is nullptr"); + } + + *len = 0; + + if (!s->scanning) { + throw SaneException(SANE_STATUS_CANCELLED, + "scan was cancelled, is over or has not been initiated yet"); + } + + DBG(DBG_proc, "%s: start, %d maximum bytes required\n", __func__, max_len); + DBG(DBG_io2, "%s: bytes_to_read=%zu, total_bytes_read=%zu\n", __func__, + dev->total_bytes_to_read, dev->total_bytes_read); + + if(dev->total_bytes_read>=dev->total_bytes_to_read) + { + DBG(DBG_proc, "%s: nothing more to scan: EOF\n", __func__); + + /* issue park command immediatly in case scanner can handle it + * so we save time */ + if (!dev->model->is_sheetfed && !(dev->model->flags & GENESYS_FLAG_MUST_WAIT) && + !dev->parking) + { + dev->cmd_set->move_back_home(dev, false); + dev->parking = true; + } + throw SaneException(SANE_STATUS_EOF); + } + + local_len = max_len; + + /* in case of image processing, all data has been stored in + * buffer_image. So read data from it if it exists, else from scanner */ + if(!dev->buffer_image) + { + /* dynamic lineart is another kind of digital processing that needs + * another layer of buffering on top of genesys_read_ordered_data */ + if (dev->settings.scan_mode == ScanColorMode::LINEART) { + /* if buffer is empty, fill it with genesys_read_ordered_data */ + if(dev->binarize_buffer.avail() == 0) + { + /* store gray data */ + local_len=dev->local_buffer.size(); + dev->local_buffer.reset(); + genesys_read_ordered_data(dev, dev->local_buffer.get_write_pos(local_len), + &local_len); + dev->local_buffer.produce(local_len); + + dev->binarize_buffer.reset(); + if (!is_testing_mode()) { + genesys_gray_lineart(dev, dev->local_buffer.get_read_pos(), + dev->binarize_buffer.get_write_pos(local_len / 8), + dev->settings.pixels, + local_len / dev->settings.pixels, + dev->settings.threshold); + } + dev->binarize_buffer.produce(local_len / 8); + } + + /* return data from lineart buffer if any, up to the available amount */ + local_len = max_len; + if (static_cast<std::size_t>(max_len) > dev->binarize_buffer.avail()) + { + local_len=dev->binarize_buffer.avail(); + } + if(local_len) + { + memcpy(buf, dev->binarize_buffer.get_read_pos(), local_len); + dev->binarize_buffer.consume(local_len); + } + } + else + { + // most usual case, direct read of data from scanner */ + genesys_read_ordered_data(dev, buf, &local_len); + } + } + else /* read data from buffer */ + { + if(dev->total_bytes_read+local_len>dev->total_bytes_to_read) + { + local_len=dev->total_bytes_to_read-dev->total_bytes_read; + } + memcpy(buf, dev->img_buffer.data() + dev->total_bytes_read, local_len); + dev->total_bytes_read+=local_len; + } + + *len = local_len; + if (local_len > static_cast<std::size_t>(max_len)) { + fprintf (stderr, "[genesys] sane_read: returning incorrect length!!\n"); + } + DBG(DBG_proc, "%s: %d bytes returned\n", __func__, *len); +} + +SANE_GENESYS_API_LINKAGE +SANE_Status sane_read(SANE_Handle handle, SANE_Byte * buf, SANE_Int max_len, SANE_Int* len) +{ + return wrap_exceptions_to_status_code(__func__, [=]() + { + sane_read_impl(handle, buf, max_len, len); + }); +} + +void sane_cancel_impl(SANE_Handle handle) +{ + DBG_HELPER(dbg); + Genesys_Scanner* s = reinterpret_cast<Genesys_Scanner*>(handle); + + s->scanning = false; + s->dev->read_active = false; + s->dev->img_buffer.clear(); + + /* no need to end scan if we are parking the head */ + if (!s->dev->parking) { + s->dev->cmd_set->end_scan(s->dev, &s->dev->reg, true); + } + + /* park head if flatbed scanner */ + if (!s->dev->model->is_sheetfed) { + if (!s->dev->parking) { + s->dev->cmd_set->move_back_home (s->dev, s->dev->model->flags & + GENESYS_FLAG_MUST_WAIT); + + s->dev->parking = !(s->dev->model->flags & GENESYS_FLAG_MUST_WAIT); + } + } + else + { /* in case of sheetfed scanners, we have to eject the document if still present */ + s->dev->cmd_set->eject_document(s->dev); + } + + /* enable power saving mode unless we are parking .... */ + if (!s->dev->parking) { + s->dev->cmd_set->save_power(s->dev, true); + } + + return; +} + +SANE_GENESYS_API_LINKAGE +void sane_cancel(SANE_Handle handle) +{ + catch_all_exceptions(__func__, [=]() { sane_cancel_impl(handle); }); +} + +void sane_set_io_mode_impl(SANE_Handle handle, SANE_Bool non_blocking) +{ + DBG_HELPER_ARGS(dbg, "handle = %p, non_blocking = %s", handle, + non_blocking == SANE_TRUE ? "true" : "false"); + Genesys_Scanner* s = reinterpret_cast<Genesys_Scanner*>(handle); + + if (!s->scanning) { + throw SaneException("not scanning"); + } + if (non_blocking) { + throw SaneException(SANE_STATUS_UNSUPPORTED); + } +} + +SANE_GENESYS_API_LINKAGE +SANE_Status sane_set_io_mode(SANE_Handle handle, SANE_Bool non_blocking) +{ + return wrap_exceptions_to_status_code(__func__, [=]() + { + sane_set_io_mode_impl(handle, non_blocking); + }); +} + +void sane_get_select_fd_impl(SANE_Handle handle, SANE_Int* fd) +{ + DBG_HELPER_ARGS(dbg, "handle = %p, fd = %p", handle, reinterpret_cast<void*>(fd)); + Genesys_Scanner* s = reinterpret_cast<Genesys_Scanner*>(handle); + + if (!s->scanning) { + throw SaneException("not scanning"); + } + throw SaneException(SANE_STATUS_UNSUPPORTED); +} + +SANE_GENESYS_API_LINKAGE +SANE_Status sane_get_select_fd(SANE_Handle handle, SANE_Int* fd) +{ + return wrap_exceptions_to_status_code(__func__, [=]() + { + sane_get_select_fd_impl(handle, fd); + }); +} + +GenesysButtonName genesys_option_to_button(int option) +{ + switch (option) { + case OPT_SCAN_SW: return BUTTON_SCAN_SW; + case OPT_FILE_SW: return BUTTON_FILE_SW; + case OPT_EMAIL_SW: return BUTTON_EMAIL_SW; + case OPT_COPY_SW: return BUTTON_COPY_SW; + case OPT_PAGE_LOADED_SW: return BUTTON_PAGE_LOADED_SW; + case OPT_OCR_SW: return BUTTON_OCR_SW; + case OPT_POWER_SW: return BUTTON_POWER_SW; + case OPT_EXTRA_SW: return BUTTON_EXTRA_SW; + default: throw std::runtime_error("Unknown option to convert to button index"); + } +} + +} // namespace genesys |