在Ubuntu上用sane api实现通用扫描功能

最近由于工作需要,要写一套扫描相关的接口。

在这里记录一下,实现还有有点复杂的。

目录

依赖

主要功能

初始化

获取当前扫描仪列表

打开扫描仪

 sane_open

设置扫描选项

sane_control_option

扫描

关闭设备

结束使用

参考资料


依赖

sudo apt install libsane-dev  sane-utils

主要功能

初始化

我们在操作扫描仪之前需要初始化才能正常使用。

初始化使用的是sane里的sane_init。

void scanner_init()
{
  printf("[%s] Start\n", __FUNCTION__);
  SANE_Int version_code = 0;
  sane_init(&version_code, auth_callback);
  printf("SANE version code: %d\n", version_code);
}

static void
auth_callback(SANE_String_Const resource,
              SANE_Char *username, SANE_Char *password)
{
}

初始化成功则version_code为SANE_STATUS_GOOD(0)。

获取当前扫描仪列表

核心是sane_get_devices函数。

先通过sane_get_devices获取扫描仪列表,然后申请一个二维数组,将扫描仪列表放入二维数组中返回。

const char **scanner_get_available_list()
{
  printf("[%s] Start\n", __FUNCTION__);
  SANE_Status status;
  SANE_Int num_devices = 0;
  const SANE_Device **device_list;

  //获取扫描仪列表
  status = sane_get_devices(&device_list, SANE_FALSE);
  if (status != SANE_STATUS_GOOD)
  {
    printf("Error getting device list: %s\n", sane_strstatus(status));
    return NULL;
  }

  // 获取当前设备数量
  while (device_list[num_devices])
    num_devices++;

  // 如果设备列表为空,返回
  if (num_devices == 0)
  {
    printf("No scanners found.\n");
    sane_exit();
    return NULL;
  }

  // 分配内存
  const char **scanner_list = malloc(sizeof(SANE_Device *) * (num_devices + 1));
  if (!scanner_list)
  {
    printf("Failed to allocate memory.\n");
    sane_exit();
    return NULL;
  }

  // 继续分配内存
  for (int i = 0; i < num_devices; i++)
  {
    scanner_list[i] = strdup(device_list[i]->name);
    if (!scanner_list[i])
    {
      printf("Failed to allocate memory.\n");
      for (int j = 0; j < i; j++)
      {
        free(scanner_list[j]);
      }
      free(scanner_list);
      sane_exit();
      return NULL;
    }
  }
  scanner_list[num_devices] = NULL;

  // 返回设备列表
  return scanner_list;
}

我们再把获取到的设备列表循环打印一下。

    const char **scanner_list = scanner_get_available_list();

    if (scanner_list != NULL)
    {
        for (int num_devices = 0; scanner_list[num_devices]; ++num_devices)
        {
            if (scanner_list[num_devices] != NULL) // 添加一个检查
            {
                printf("Device %d: name=%s \n",
                       num_devices, scanner_list[num_devices]);
            }
        }
    }
    else
    {
        return;
    }

打开扫描仪

 sane_open

这里介绍一下sane_open,我们打开扫描仪主要是用sane中的这个api。

它的第一个参数是扫描仪的名称,第二个参数是一个空的句柄,打开后通过句柄进行后续操作。

extern SANE_Status sane_open (SANE_String_Const devicename,
			      SANE_Handle * handle);

 函数的具体实现如下:

static SANE_Handle sane_handle = NULL; // 扫描仪设备句柄,全局变量

int scanner_open_device(char *scanner_name)
{
  printf("[%s] Start\n", __FUNCTION__);
  SANE_Status sane_status = 0;

  if (sane_status = sane_open(scanner_name, &sane_handle))
  {
    printf("sane_open status: %s\n", sane_strstatus(sane_status));
  }
  if (sane_status != SANE_STATUS_GOOD)
    sane_handle = NULL;

  return sane_status;
}

这里的入参scanner_name是获取扫描仪列表的scanner_list,如果要打开第一个扫描仪的话是入参是scanner_list[0]。

如果函数的返回值不是SANE_STATUS_GOOD,表示打开失败了。


设置扫描选项

sane_control_option

扫描的所有参数都是通过sane_control_option实现的,每个参数的功能详见备注。

extern SANE_Status sane_control_option (SANE_Handle handle,  //sane_open的句柄
                                        SANE_Int option,     //要设置选项的序号,2是颜色,3是分辨率
					                    SANE_Action action,  //操作的类型,给选项赋值或者获取当前值
                                        void *value,         //value的实际值
					                    SANE_Int *info);     //没发现有什么用

 操作的类型一共有以下三种,我们这里只用到第二种。

typedef enum
  {
    SANE_ACTION_GET_VALUE = 0,
    SANE_ACTION_SET_VALUE,
    SANE_ACTION_SET_AUTO
  }
SANE_Action;

我这里设置了颜色,扫描的分辨率和纸张大小,还有很多可以设置的选项,可以自行探索。

static SANE_Handle sane_handle = NULL; // 扫描仪设备句柄,全局变量

// 设置指定扫描仪颜色,通过传入参数val_color进行设置扫描设备的颜色
int scanner_set_color(SANE_String val_color)
{
  printf("[%s] Start!\n", __FUNCTION__);
  SANE_Status status;
  status = sane_control_option(sane_handle, 2,
                               SANE_ACTION_SET_VALUE, val_color, NULL);

  if (status != SANE_STATUS_GOOD)
  {
    printf("Option did not set, desc = %s\n", sane_strstatus(status));

    return status;
  }

  printf("set color option success!\n");
  return status;
}


// 设置指定扫描仪扫描的分辨率(清晰程度,分辨率越大越清晰)
int scanner_set_resolutions(SANE_Int val_resolution)
{
  printf("[%s] Start!\n", __FUNCTION__);
  SANE_Status status;

  status = sane_control_option(sane_handle, 3, SANE_ACTION_SET_VALUE, &val_resolution, NULL);
  if (status != SANE_STATUS_GOOD)
  {
    printf("Option did not set, desc = %s\n", sane_strstatus(status));
    return status;
  }

  printf("set resolution option success!\n");
  return status;
}
设置纸张大小

设置纸张大小有点复杂,因为纸张大小没有对应的option。因此曲线救国,通过设置扫描的纸张的长和宽来实现。 

简单画图解释一下这里是怎么设置的。

假如图中的长方形为纸张,将纸张的左下角当成坐标原点,x轴会有两个坐标x1和x2,同理y1和y2。
sane_control_option可以设置这四个点的值,对应序号分别为7,8,9,10。

实际使用中我们将7,8设置成0。9,10设置成纸张的长和宽即可。

enum sizes_type
{
  A2 = 1,
  A3,
  A4,
  A5,
  A6
};

static double g_saneSizeA4BrY = 297;

int scanner_set_size(SANE_String size)
{
  printf("[%s] Start!\n", __FUNCTION__);
  SANE_Status status = SANE_STATUS_GOOD;
  int type;

  if (!strcmp("A2", size))
  {
    type = A2;
  }
  else if (!strcmp("A3", size))
  {
    type = A3;
  }
  else if (!strcmp("A4", size))
  {
    type = A4;
  }
  else if (!strcmp("A5", size))
  {
    type = A5;
  }
  else if (!strcmp("A6", size))
  {
    type = A6;
  }
  else
  {
    type = 0;
  }

  switch (type)
  {
  case A2:
    status = kdk_scanner_set_size_real(sane_handle, 420, 594);
    break;
  case A3:
    status = kdk_scanner_set_size_real(sane_handle, 297, 420);
    break;
  case A4:
    status = kdk_scanner_set_size_real(sane_handle, 210, g_saneSizeA4BrY);
    break;
  case A5:
    status = kdk_scanner_set_size_real(sane_handle, 148, 210);
    break;
  case A6:
    status = kdk_scanner_set_size_real(sane_handle, 105, 144);
    break;
  default:
    status = SANE_STATUS_UNSUPPORTED;
  }

  return status;
}

/**
 * @brief scanner_set_size_real 统一设置扫描设备尺寸
 *
 * @param sane_handle 扫描句柄
 *
 * @param val_size_br_x 扫描设备右下角的x坐标
 *
 * @param val_size_br_y 扫描设备右下角的y坐标
 *
 * @return 返回扫描设备设置尺寸的情况
 */
SANE_Status scanner_set_size_real(SANE_Handle sane_handle, SANE_Int val_size_br_x,
                                      SANE_Int val_size_br_y)
{
  printf("[%s] Start!\n", __FUNCTION__);
  SANE_Status status = SANE_STATUS_GOOD;

  SANE_Word x = SANE_FIX(val_size_br_x);
  SANE_Word y = SANE_FIX(val_size_br_y);

  SANE_Word zero = SANE_FIX(0.0);
  // 设置x1
  status = sane_control_option(sane_handle, 7, SANE_ACTION_SET_VALUE, &zero, NULL);
  if (status != SANE_STATUS_GOOD)
  {
    return status;
  }
  // 设置y1
  status = sane_control_option(sane_handle, 8, SANE_ACTION_SET_VALUE, &zero, NULL);
  if (status != SANE_STATUS_GOOD)
  {
    return status;
  }
  // 设置扫描纸张的长度(也就是x2)
  status = sane_control_option(sane_handle, 9,
                               SANE_ACTION_SET_VALUE, &x, NULL);

  if (status != SANE_STATUS_GOOD)
  {
    printf("Option x did not set, desc = %s\n", sane_strstatus(status));
   
    return status;
  }
  // 设置扫描纸张的宽度(也就是y2)
  status = sane_control_option(sane_handle, 10,
                               SANE_ACTION_SET_VALUE, &y, NULL);

  if (status != SANE_STATUS_GOOD)
  {
    printf("Option y did not set, desc = %s\n", sane_strstatus(status));
     
    return status;
  }

  return status;
}

和前面一样,如果函数的返回值不是SANE_STATUS_GOOD,表示失败了。

扫描

扫描我分成两类,分为单页单面扫描和多页双面扫描。

单页和多页也是一种可以设置的扫描属性,单页和多页的主要是这个这个属性的区别,别的部分都差不多。

//设置扫描是单页还是多页
int scanner_set_page_type(SANE_Int type)
{
  printf("[%s] Start!\n", __FUNCTION__);
  SANE_Status status;

  //对应的option序号为4
  status = sane_control_option(sane_handle, 4,
                               SANE_ACTION_SET_VALUE, &type, NULL);

  if (status != SANE_STATUS_GOOD)
  {
    printf("Option did not set, desc = %s\n", sane_strstatus(status));

    return status;
  }

  printf("set page type option success!\n");
  return status;
}

单页单面扫描,就是不管有多少页都只扫描第一页的第一面。

  /**
   * @brief 指定扫描仪进行扫描(统一按照多页,双面处理)
   *
   * @param fileName:保存扫描文件的文件名,比如传test的话,扫描后的文件会是test_1,test_2之类的形式
   * 
   * @param type:扫描类型 0:单面单面扫描,1:多页双面扫描
   *
   * @return 操作的返回值,0或者7为成功,其他为失败
   */
int scanner_start_scan(SANE_String_Const fileName, int type)
{
  printf("[%s] Start\n", __FUNCTION__);
  SANE_Status sane_status = 0;
 
  switch (type)
  {
  case 0:
    return do_scan_one(fileName);

  case 1:
    return do_scan_all(fileName);

  default:
    return do_scan_all(fileName);
  }
}

 

这是扫描单页的接口

// 单页扫描
SANE_Status do_scan_one(const char *fileName)
{
  printf("[%s] Start\n", __FUNCTION__);

  del_old_pic();//扫描之前删掉上一次的内容

  SANE_Status status;
  FILE *ofp = NULL;
  char path[PATH_MAX];
  char part_path[PATH_MAX];
  buffer_size = (32 * 1024);
  buffer = (SANE_Byte *)malloc(buffer_size);
  int i = 1;

  // 设置打印机单页进纸张
  status = kdk_scanner_set_page_type(1);
  if (status != SANE_STATUS_GOOD)
  {
      printf("set page type fail:%s\n", sane_strstatus(status));
      return status;
  }

  do
  {
    // 设置保存路径
    sprintf(path, "/tmp/%s-%d.pnm", fileName, i); // 格式化PNM文件路径
    strcpy(part_path, path);
    strcat(part_path, ".part");

    printf("picture name: %s\n", path);

    // 开始扫描
    status = sane_start(sane_handle);
    if (status != SANE_STATUS_GOOD)
    {
      break;
    }

    if (NULL == (ofp = fopen(part_path, "w")))
    {
      status = SANE_STATUS_ACCESS_DENIED;
      break;
    }

    // 保存扫描数据
    status = scan_it(ofp);

    switch (status)
    {
    case SANE_STATUS_GOOD:
    case SANE_STATUS_EOF:
    {
      status = SANE_STATUS_GOOD;
      if (!ofp || 0 != fclose(ofp))
      {
        status = SANE_STATUS_ACCESS_DENIED;
        break;
      }
      else
      {
        ofp = NULL;
        if (rename(part_path, path))
        {
          status = SANE_STATUS_ACCESS_DENIED;
          break;
        }
      }
    }
    break;
    default:
      break;
    }
  } while (0);

 
  if (ofp)
  {
    fclose(ofp);
    ofp = NULL;
  }
  if (buffer)
  {
    free(buffer);
    buffer = NULL;
  }

  return status;
}

// 删除上一次扫描的文件
void del_old_pic()
{
  DIR *dir;
  struct dirent *entry;
  char path[] = "/tmp/";
  char ext[] = ".pnm";

  dir = opendir(path);
  if (dir == NULL)
  {
    perror("Unable to open directory");
    exit(EXIT_FAILURE);
  }

  while ((entry = readdir(dir)) != NULL)
  {
    // Check if the entry is a file and ends with .pnm
    if (entry->d_type == DT_REG &&
        strstr(entry->d_name, ext) != NULL &&
        strcmp(entry->d_name + strlen(entry->d_name) - strlen(ext), ext) == 0)
    {
      char full_path[512];
      sprintf(full_path, "%s%s", path, entry->d_name);

      if (remove(full_path) == 0)
      {
        printf("Deleted %s\n", full_path);
      }
      else
      {
        perror("Unable to delete file");
      }
    }
  }

  closedir(dir);
}

// sane 设置扫描方式
int kdk_scanner_set_page_type(SANE_Int type)
{
  printf("[%s] Start!\n", __FUNCTION__);
  SANE_Status status;
  status = sane_control_option(sane_handle, 4,
                               SANE_ACTION_SET_VALUE, &type, NULL);

  if (status != SANE_STATUS_GOOD)
  {
    printf("Option did not set, desc = %s\n", sane_strstatus(status));

    return status;
  }

  printf("set page type option success!\n");
  return status;
}

保存图片,这一部分细节很多,我也没仔细研究,直接用就行。

static SANE_Status scan_it(FILE *ofp)
{
  int i, len, first_frame = 1, offset = 0, must_buffer = 0, hundred_percent;
  SANE_Byte min = 0xff, max = 0;
  SANE_Parameters parm;
  SANE_Status status;
  Image image = {0, 0, 0, 0, 0};
  static const char *format_name[] = {"gray", "RGB", "red", "green", "blue"};
  SANE_Word total_bytes = 0, expected_bytes;
  SANE_Int hang_over = -1;

  do
  {

    if (!first_frame)
    {
      status = sane_start(sane_handle);
      if (status != SANE_STATUS_GOOD)
      {
        goto cleanup;
      }
    }

    status = sane_get_parameters(sane_handle, &parm);
    if (status != SANE_STATUS_GOOD)
    {
      goto cleanup;
    }

    if (first_frame)
    {
      switch (parm.format)
      {
      case SANE_FRAME_RED:
      case SANE_FRAME_GREEN:
      case SANE_FRAME_BLUE:
        assert(parm.depth == 8);
        must_buffer = 1;
        offset = parm.format - SANE_FRAME_RED;
        break;
      case SANE_FRAME_RGB:
        assert((parm.depth == 8) || (parm.depth == 16));
      case SANE_FRAME_GRAY:
        assert((parm.depth == 1) || (parm.depth == 8) || (parm.depth == 16));
        if (parm.lines < 0)
        {
          must_buffer = 1;
          offset = 0;
        }
        else
        {
          write_pnm_header(parm.format, parm.pixels_per_line, parm.lines, parm.depth, ofp);
        }
        break;
      default:
        break;
      }

      if (must_buffer)
      {
        image.width = parm.bytes_per_line;
        if (parm.lines >= 0)
          image.height = parm.lines - STRIP_HEIGHT + 1;
        else
          image.height = 0;

        image.x = image.width - 1;
        image.y = -1;
        if (!advance(&image))
        {
          status = SANE_STATUS_NO_MEM;
          goto cleanup;
        }
      }
    }
    else
    {
      assert(parm.format >= SANE_FRAME_RED && parm.format <= SANE_FRAME_BLUE);
      offset = parm.format - SANE_FRAME_RED;
      image.x = image.y = 0;
    }

    hundred_percent = parm.bytes_per_line * parm.lines * ((parm.format == SANE_FRAME_RGB || parm.format == SANE_FRAME_GRAY) ? 1 : 3);

    // 这段是写图片数据
    while (1)
    {
      double progr;
      status = sane_read(sane_handle, buffer, buffer_size, &len);
      total_bytes += (SANE_Word)len;
      progr = ((total_bytes * 100.) / (double)hundred_percent);
      if (progr > 100.)
        progr = 100.;

      if (status != SANE_STATUS_GOOD)
      {
        if (status != SANE_STATUS_EOF)
        {
          return status;
        }
        break;
      }

      if (must_buffer)
      {
        switch (parm.format)
        {
        case SANE_FRAME_RED:
        case SANE_FRAME_GREEN:
        case SANE_FRAME_BLUE:
          for (i = 0; i < len; ++i)
          {
            image.data[offset + 3 * i] = buffer[i];
            if (!advance(&image))
            {
              status = SANE_STATUS_NO_MEM;
              goto cleanup;
            }
          }
          offset += 3 * len;
          break;
        case SANE_FRAME_RGB:
          for (i = 0; i < len; ++i)
          {
            image.data[offset + i] = buffer[i];
            if (!advance(&image))
            {
              status = SANE_STATUS_NO_MEM;
              goto cleanup;
            }
          }
          offset += len;
          break;
        case SANE_FRAME_GRAY:
          for (i = 0; i < len; ++i)
          {
            image.data[offset + i] = buffer[i];
            if (!advance(&image))
            {
              status = SANE_STATUS_NO_MEM;
              goto cleanup;
            }
          }
          offset += len;
          break;
        default:
          break;
        }
      }
      else /* ! must_buffer */
      {
        if ((parm.depth != 16))
          fwrite(buffer, 1, len, ofp);
        else
        {
#if !defined(WORDS_BIGENDIAN)
          int i, start = 0;
          /* check if we have saved one byte from the last sane_read */
          if (hang_over > -1)
          {
            if (len > 0)
            {
              fwrite(buffer, 1, 1, ofp);
              buffer[0] = (SANE_Byte)hang_over;
              hang_over = -1;
              start = 1;
            }
          }
          /* now do the byte-swapping */
          for (i = start; i < (len - 1); i += 2)
          {
            unsigned char LSB;
            LSB = buffer[i];
            buffer[i] = buffer[i + 1];
            buffer[i + 1] = LSB;
          }
          /* check if we have an odd number of bytes */
          if (((len - start) % 2) != 0)
          {
            hang_over = buffer[len - 1];
            len--;
          }
#endif
          fwrite(buffer, 1, len, ofp);
        }
      }

      if (verbose && parm.depth == 8)
      {
        for (i = 0; i < len; ++i)
          if (buffer[i] >= max)
            max = buffer[i];
          else if (buffer[i] < min)
            min = buffer[i];
      }
    }
    first_frame = 0;
  } while (!parm.last_frame);

  if (must_buffer)
  {
    image.height = image.y;
    write_pnm_header(parm.format, parm.pixels_per_line, image.height, parm.depth, ofp);

#if !defined(WORDS_BIGENDIAN)
    if (parm.depth == 16)
    {
      int i;
      for (i = 0; i < image.height * image.width; i += 2)
      {
        unsigned char LSB;
        LSB = image.data[i];
        image.data[i] = image.data[i + 1];
        image.data[i + 1] = LSB;
      }
    }
#endif
    fwrite(image.data, 1, image.height * image.width, ofp);
  }

  fflush(ofp);

cleanup:
  if (image.data)
    free(image.data);

  return status;
}

void write_pnm_header(SANE_Frame format, int width, int height, int depth, FILE *ofp)
{
  printf("[%s] Start\n", __FUNCTION__);
  switch (format)
  {
  case SANE_FRAME_RED:
  case SANE_FRAME_GREEN:
  case SANE_FRAME_BLUE:
  case SANE_FRAME_RGB:
    fprintf(ofp, "P6\n# SANE data follows\n%d %d\n%d\n", width, height, (depth <= 8) ? 255 : 65535);
    break;
  default:
    if (depth == 1)
      fprintf(ofp, "P4\n# SANE data follows\n%d %d\n", width, height);
    else
      fprintf(ofp, "P5\n# SANE data follows\n%d %d\n%d\n", width, height, (depth <= 8) ? 255 : 65535);
    break;
  }
}

static void *
advance(Image *image)
{
  if (++image->x >= image->width)
  {
    image->x = 0;
    if (++image->y >= image->height || !image->data)
    {
      size_t old_size = 0, new_size;

      if (image->data)
        old_size = image->height * image->width;

      image->height += STRIP_HEIGHT;
      new_size = image->height * image->width;

      if (image->data)
        image->data = realloc(image->data, new_size);
      else
        image->data = malloc(new_size);
      if (image->data)
        memset(image->data + old_size, 0, new_size - old_size);
    }
  }
  if (!image->data)
    fprintf(stderr, "can't allocate image buffer (%dx%d)\n",
            image->width, image->height);
  return image->data;
}

双页扫描,用do_scan_all替换do_scan_one,其他的函数都一样。

// 双面扫描全部文件+保存为PNM图像格式
SANE_Status do_scan_all(const char *fileName)
{
  printf("[%s] Start\n", __FUNCTION__);

  SANE_Status status;           // 返回状态
  FILE *ofp = NULL;             // 输出文件指针
  char path[PATH_MAX];          // PNM文件路径
  char part_path[PATH_MAX];     // 临时PNN文件路径
  buffer_size = (32 * 1024);    // 缓冲区大小
  buffer = malloc(buffer_size); // 动态分配缓冲区
  int i = 1;

  del_old_pic();

  //设置打印机多页进纸张
  status = kdk_scanner_set_page_type(0);
  if (status != SANE_STATUS_GOOD)
  {
      printf("set page type fail:%s\n", sane_strstatus(status));
      return status;
  }

  do
  {
    sprintf(path, "/tmp/%s-%d.pnm", fileName, i); // 格式化PNM文件路径
    strcpy(part_path, path);                      // 复制PNM文件路径到临时文件路径
    strcat(part_path, ".part");                   // 在临时文件路径后添加扩展名".part"

    // 启动扫描过程
    status = sane_start(sane_handle);
    if (status != SANE_STATUS_GOOD)
    {
      break;
    }

    // 创建临时文件
    if (NULL == (ofp = fopen(part_path, "w")))
    {
      status = SANE_STATUS_ACCESS_DENIED;
      break;
    }

    // 进行扫描,并将结果写入到临时文件中
    status = scan_it(ofp);

    switch (status)
    {
    case SANE_STATUS_GOOD:
    case SANE_STATUS_EOF:
    {
      // 扫描成功或结束
      status = SANE_STATUS_GOOD;

      // 关闭临时文件,并检查是否成功关闭
      if (!ofp || 0 != fclose(ofp))
      {
        status = SANE_STATUS_ACCESS_DENIED;
        break;
      }
      else
      {
        ofp = NULL; // 将文件指针设置为NULL,避免重复关闭
        // 将临时文件重命名为正式的PNM文件
        if (rename(part_path, path))
        {
          status = SANE_STATUS_ACCESS_DENIED;
          break;
        }
      }
    }
    break;
    default:
      break;
    }
    i++;
  } while (status == SANE_STATUS_GOOD);

  // 如果出现错误,则取消扫描进程
  if (SANE_STATUS_GOOD != status)
  {
    sane_cancel(sane_handle);
  }

  // 关闭输出文件
  if (ofp)
  {
    fclose(ofp);
    ofp = NULL;
  }

  // 释放缓冲区内存
  if (buffer)
  {
    free(buffer);
    buffer = NULL;
  }

  if ((status == SANE_STATUS_NO_DOCS) && (i > 1))
    status = SANE_STATUS_GOOD;

  return status; // 返回状态
}

扫描完成会会在tmp下生成扫描文件。

关闭设备

void scanner_close_device()
{
  printf("[%s] Start\n", __FUNCTION__);
  if (sane_handle != NULL)
  {
    sane_close(sane_handle);
  }
  sane_handle = NULL;
}

结束使用

void scanner_exit()
{
  printf("[%s] Start\n", __FUNCTION__);
  sane_exit();
}

参考资料

Linux下通用扫描仪API——SANE( Scanner Access Now Easy)_linux sane-CSDN博客

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值