[SC]Работаем со сканером

в 8:51, , рубрики: .net, C#, copy-cat, WIA, Блог компании Тинькофф Банк, обработка изображений, онотоле, онотолей, разработка, сканирование документов, сканирование мозга
[SC]Работаем со сканером - 1

Последние несколько лет мы с коллегами пытаемся сделать так, чтобы в офисе стало меньше бумаги. С цифровыми документами сотрудники работают быстрее и качественнее — да и пыли становится в разы меньше.

Чтобы полностью перейти на цифровые документы сначала надо отсканировать бумажные. Для разработки десктопных приложений сканировщиков мы используем .NET Framework. Из коробки он не предоставляет средств для работы со сканерами. Поскольку .NET дружит с COM, можно использовать компонент WIA (Windows Imaging Architecture).

Для удобной работы с WIA я написал класс, легший в основу многих приложений для сканирования. Я хочу поделиться нашим опытом работы со сканерами на примере класса Scanner.

Он используется в нескольких очень полезных приложениях нашего банка. Например, все заявки и договоры клиентов банка оцифровываются приложениями использующими класс Scanner.

Выглядит это так:

[SC]Работаем со сканером - 2

и так:

[SC]Работаем со сканером - 3

Не все простое гениально

Чтобы начать работать со сканером из разрабатываемого приложения нужно сначала его выбрать, затем настроить, после чего можно сканировать. Поэтому public интерфейс класса можно ограничить двумя методами — Configuration и Scan.

Configuration будет показывать стандартный диалог настройки сканера. Scan будет сканировать документ и возвращать MemoryStream с картинкой.

Теперь в деталях...

Работать со сканером будем через WIA. Для этого подключим к проекту COM компонент Microsoft Windows Image Acquisition Library v2.0, реализация которого находится в файле C:WindowsSystem32wiaaut.dll.

Привычно настроить сканер один раз и при следующей загрузке приложения иметь уже настроенный сканер. Нам понадобятся функции для настройки сканера и для сохранения и восстановления конфигурации. Создадим класс Scanner и в начале файла добавим using WIA. При создании объекта класса Scanner пытаемся загрузить настройки из конфига. Если не получилось, предлагаем настроить сканер вручную.

Обычно одна программа работает с одним сканером, поэтому можно было бы сделать синглетон. В моем случае может быть несколько сканеров одновременно.
Вот как будет выглядеть наш конструктор:

public Scanner() {
    try {
        LoadConfig();
    } catch (Exception) {
        MessageBox.Show("Ошибка конфигурации, требуется ручная настройка сканера");
        Configuration();
    }
}

Что здесь делает MessageBox? В большинстве случаев приложения для сканирования подразумевают GUI. Для вывода информации вполне можно показать сообщение через MessageBox, тем более конфигурировать сканер будем через стандартный WIA диалог.

Начнем с настройки сканера. Настройка сканера будет осуществляться функцией Configuration(), которую, можно вызывать в любой удобный момент, например, по нажатию кнопки «Настройка сканера» в программе.

public void Configuration() {
    try {
        var commonDialog = new CommonDialogClass();
        _scanDevice = commonDialog.ShowSelectDevice(WiaDeviceType.ScannerDeviceType, true);

        if (_scanDevice == null) return;

        var items = commonDialog.ShowSelectItems(_scanDevice);

        if (items.Count < 1) return;

        _scannerItem = items[1];

        SaveProp(_scanDevice.Properties, ref _defaultDeviceProp);

        SaveConfig();
    } catch (Exception e) {
        MessageBox.Show(e.Message, "Интерфейс сканера не доступен");
    }
}

У сканера много параметров, часть из которых должна вычисляться из других, поэтому, чтобы не усложнять, мы использовали стандартный WIA-диалог CommonDialogClass для настройки. Сначала предлагаем пользователю программы выбрать сканер ShowSelectDevice(WiaDeviceType. ScannerDeviceType, true). Диалог вернет объект Device или null, если устройство не выбрано.

[SC]Работаем со сканером - 4

Затем настраиваем полученное устройство ShowSelectItems(_scanDevice). В настройках можно задать DPI, размер бумаги, цветовой режим и другие параметры.

Основной недостаток этого окна настройки в том, что для подтверждения вместо логичного OK, придется нажать на кнопку «Сканировать», хотя в нашем случае сканирование запущено не будет.

[SC]Работаем со сканером - 5

После успешной настройки сохраняем конфигурацию в файл вызовом SaveConfig().

Сохранения/восстановления настроек WIA не предоставляет. Пришлось сделать эту часть самостоятельно. Тут не было ничего сложного, настройки представлены в виде списка IProperty, который есть у Device и у Item.

Для сохранения мы выбрали удобный для чтения текстовый формат, в котором имя параметров, идентификаторы и значения разделены точкой с запятой, а настройки для Device и Item разделены заголовками [device] и [item].

Пример конфига

[device]
DeviceID;{6BDD1FC6-810F-11D0-BEC7-08002BE2092F}003
Item Name;4098;Root
Full Item Name;4099;0003Root
Item Flags;4101;76
Unique Device ID;2;{6BDD1FC6-810F-11D0-BEC7-08002BE2092F}003
Manufacturer;3;FUJITSU
Description;4;fi-6140dj
Type;5;65537
Port;6;\.Usbscan0
Name;7;fi-6140dj #2
Server;8;local
Remote Device ID;9;
UI Class ID;10;{C2A237CB-9CEF-4fd6-B989-E82E1DB0F0C9}
Hardware Configuration;11;0
BaudRate;12;
STI Generic Capabilities;13;51
WIA Version;14;2.0
Driver Version;15;2.1.4.3
PnP ID String;16;\?usb#vid_04c5&pid_114d#8&184ab430&0&2#{6bdd1fc6-810f-11d0-bec7-08002be2092f}
STI Driver Version;17;2
Horizontal Bed Size;3074;8500
Vertical Bed Size;3075;11692
Access Rights;4102;3
Horizontal Optical Resolution;3090;600
Vertical Optical Resolution;3091;600
Firmware Version;1026;0500
Max Scan Time;3095;180000
Preview;3100;0
Show preview control;3103;1
Horizontal Sheet Feed Size;3076;8500
Vertical Sheet Feed Size;3077;14000
Document Handling Capabilities;3086;21
Document Handling Status;3087;5
Document Handling Select;3088;1
Pages;3096;1
Sheet Feeder Registration;3078;1
Horizontal Bed Registration;3079;0
Vertical Bed Registration;3080;0
Document Handling Option;38914;0
[item]
ItemID;0003RootTop
Item Name;4098;Top
Full Item Name;4099;0003RootTop
Item Flags;4101;67
Color Profile Name;4120;sRGB Color Space Profile.icm
Horizontal Resolution;6147;200
Vertical Resolution;6148;200
Horizontal Extent;6151;1653
Vertical Extent;6152;2338
Horizontal Start Position;6149;23
Vertical Start Position;6150;0
Data Type;4103;2
Bits Per Pixel;4104;8
Brightness;6154;0
Contrast;6155;0
Current Intent;6146;0
Pixels Per Line;4112;1653
Number of Lines;4114;2338
Preferred Format;4105;{B96B3CAA-0728-11D3-9D7B-0000F81EF32E}
Item Size;4116;3872792
Threshold;6159;128
Format;4106;{B96B3CAA-0728-11D3-9D7B-0000F81EF32E}
Media Type;4108;128
Channels Per Pixel;4109;1
Bits Per Channel;4110;8
Planar;4111;0
Bytes Per Line;4113;1656
Buffer Size;4118;65535
Access Rights;4102;3
Compression;4107;0
Photometric Interpretation;6153;0
Lamp Warm up Time;6161;180000
3100;3100;0

private void SaveConfig() {
    var settings = new List<string>();
    settings.Add("[device]");
    settings.Add(String.Format("DeviceID;{0}", _scanDevice.DeviceID));

    foreach (IProperty property in _scanDevice.Properties) {
        var propstring = string.Format("{1}{0}{2}{0}{3}", ";", property.Name, property.PropertyID, property.get_Value());
        settings.Add(propstring);
    }

    settings.Add("[item]");
    settings.Add(String.Format("ItemID;{0}", _scannerItem.ItemID));
    
    foreach (IProperty property in _scannerItem.Properties) {
        var propstring = string.Format("{1}{0}{2}{0}{3}", ";", property.Name, property.PropertyID, property.get_Value());
        settings.Add(propstring);
    }

    File.WriteAllLines(Config, settings.ToArray());
}

private enum loadMode {undef, device, item};

private void LoadConfig() {
    var settings = File.ReadAllLines(Config);

    var mode  = loadMode.undef;

    foreach (var setting in settings) {
        if (setting.StartsWith("[device]")) {
            mode = loadMode.device;
            continue;
         }
        
        if (setting.StartsWith("[item]")) {
            mode = loadMode.item;
            continue;
        }
                
        if (setting.StartsWith("DeviceID")) {
            var deviceid = setting.Split(';')[1];
            var devMngr = new DeviceManagerClass();

            foreach (IDeviceInfo deviceInfo in devMngr.DeviceInfos) {
                if (deviceInfo.DeviceID == deviceid) {
                    _scanDevice = deviceInfo.Connect();
                    break;
                }
            }

            if (_scanDevice == null) {
                MessageBox.Show("Сканнер из конфигурации не найден");
                return;
            }

            _scannerItem = _scanDevice.Items[1];
            continue;
        }

        if (setting.StartsWith("ItemID")) {
            var itemid = setting.Split(';')[1];
             continue;
        }

        var sett = setting.Split(';');
         
        switch (mode) {
            case loadMode.device:
                SetProp(_scanDevice.Properties, sett[1], sett[2]);
            break;

            case loadMode.item:
                SetProp(_scannerItem.Properties, sett[1], sett[2]);
            break;
        }
    }

    SaveProp(_scanDevice.Properties, ref _defaultDeviceProp);
}

private static void SetProp(IProperties prop, object property, object value) {
    try {
        prop[property].set_Value(value);
    } catch (Exception) { // некоторые свойства доступны только для чтения
        return;
    }
}

Процедура сканирования тривиальна. Она возвращает либо MemoryStream с картинкой, либо null, если получить скан не получилось.

public MemoryStream MemScan() {
    try {
        var result = _scannerItem.Transfer(FormatID.wiaFormatJPEG);
        var wiaImage = (ImageFile)result;
        var imageBytes = (byte[])wiaImage.FileData.get_BinaryData();

        using (var ms = new MemoryStream(imageBytes)) {
            using (var bitmap = Bitmap.FromStream(ms)) {
                bitmap.Save(stream, ImageFormat.Jpeg);
            }
        }
    } catch (Exception) {
        return null;
    }

    return stream;
}

На мой взгляд, при сканировании удобнее рисовать свой прогресс-бар, поэтому скрываем стандартный прогресс. Для сканирования без отображения прогресса используется метод Transfer.

Дуплекс/Симплекс

Многие протяжные сканеры поддерживают режим двухстраничного сканирования, который можно выбрать через диалог настройки.

[SC]Работаем со сканером - 6

Однако в стандартном диалоге можно забыть установить Duplex режим, поэтому полезно иметь возможность форсированно включать режим Duplex и возвращаться к исходным настройкам.

public void SetDuplexMode(bool isDuplex) {
    // WIA property ID constants
    const string wiaDpsDocumentHandlingSelect = "3088";
    const string wiaDpsPages = "3096";

    // WIA_DPS_DOCUMENT_HANDLING_SELECT flags
    const int feeder = 0x001;
    const int duplex = 0x004;

    if (_scanDevice == null) return;

    if (isDuplex) {
        SetProp(_scanDevice.Properties, wiaDpsDocumentHandlingSelect, (duplex | feeder));
        SetProp(_scanDevice.Properties, wiaDpsPages, 1);
    } else {
        try {
            SetProp(_scanDevice.Properties, wiaDpsDocumentHandlingSelect, _defaultDeviceProp[wiaDpsDocumentHandlingSelect]);
            SetProp(_scanDevice.Properties, wiaDpsPages, _defaultDeviceProp[wiaDpsPages]);
        } catch (Exception e) {
            MessageBox.Show(String.Format("Сбой восстановления режима сканирования:{0}{1}", Environment.NewLine, e.Message));
        }
                
    }
}

Для сканирования в двустраничном режиме нужно вызвать MemScan() для первой и второй страниц. Первый вызов отсканирует лист и вернет изображение первой страницы, второй вызов вернет скан второй страницы. При работе с протяжными(feeder)сканерами удобно сканировать в цикле пока MemScan() возвращает != null — это часто означает, что в сканере закончилась бумага. Можно не думать, в каком режиме работает сканер- документы будут отсканированы в любом случае. Если запустить сканирование в цикле для сканера хлопушки, процесс сканирования не остановится пока сканер подключен к розетке, этот нюанс следует учитывать при разработке ПО.

Полный исходный код класса для работы со сканером

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Windows.Forms;
using WIA;

namespace Scanning {
    public class Scanner {

        public const string Config = "scanner.cfg";
        private Device _scanDevice;
        private Item _scannerItem;
        private Random _rnd = new Random();

        private Dictionary<string, object> _defaultDeviceProp;

        public bool IsVirtual;

        public Scanner() {
                try {
                    LoadConfig();
                } catch (Exception) {
                    MessageBox.Show("Ошибка конфигурации, требуется ручная настройка сканера");
                    Configuration();
                }
        }

        public void Configuration() {
            try {
                var commonDialog = new CommonDialogClass();
                _scanDevice = commonDialog.ShowSelectDevice(WiaDeviceType.ScannerDeviceType, true);

                if (_scanDevice == null)
                    return;

                var items = commonDialog.ShowSelectItems(_scanDevice);

                if (items.Count < 1)
                    return;

                _scannerItem = items[1];

                SaveProp(_scanDevice.Properties, ref _defaultDeviceProp);

                SaveConfig();
            } catch (Exception e) {
                MessageBox.Show(e.Message, "Интерфейс сканера не доступен");
            }
        }

        private void SaveProp(WIA.Properties props, ref Dictionary<string, object> dic) {
            if (dic == null) dic = new Dictionary<string, object>();

            foreach (Property property in props) {
                var propId = property.PropertyID.ToString();
                var propValue = property.get_Value();

                dic[propId] = propValue;
            }
        }

        public void SetDuplexMode(bool isDuplex) {
            // WIA property ID constants
            const string wiaDpsDocumentHandlingSelect = "3088";
            const string wiaDpsPages = "3096";

            // WIA_DPS_DOCUMENT_HANDLING_SELECT flags
            const int feeder = 0x001;
            const int duplex = 0x004;

            if (_scanDevice == null) return;

            if (isDuplex) {
                SetProp(_scanDevice.Properties, wiaDpsDocumentHandlingSelect, (duplex | feeder));
                SetProp(_scanDevice.Properties, wiaDpsPages, 1);
            } else {
                try {
                    SetProp(_scanDevice.Properties, wiaDpsDocumentHandlingSelect, _defaultDeviceProp[wiaDpsDocumentHandlingSelect]);
                    SetProp(_scanDevice.Properties, wiaDpsPages, _defaultDeviceProp[wiaDpsPages]);
                } catch (Exception e) {
                    MessageBox.Show(String.Format("Сбой восстановления режима сканирования:{0}{1}", Environment.NewLine, e.Message));
                }
            }
        }

        public MemoryStream MemScan() {
            if ((_scannerItem == null) && (!IsVirtual)) {
                MessageBox.Show("Сканер не настроен, активировано виртуальное устройство!", "Info");
                //return null;
                IsVirtual = true;
            }

            var stream = new MemoryStream();

            if (IsVirtual) {
                if (_rnd.Next(3) == 0) {
                    return null;
                }

                var btm = GetVirtualScan();
                btm.Save(stream, ImageFormat.Jpeg);
                return stream;
            }

            try {
                var result = _scannerItem.Transfer(FormatID.wiaFormatJPEG);
                var wiaImage = (ImageFile)result;
                var imageBytes = (byte[])wiaImage.FileData.get_BinaryData();

                using (var ms = new MemoryStream(imageBytes)) {
                    using (var bitmap = Bitmap.FromStream(ms)) {
                        bitmap.Save(stream, ImageFormat.Jpeg);
                    }
                }

            } catch (Exception) {
                return null;
            }

            return stream;
        }

        private Bitmap GetVirtualScan() {
            const int imgSize = 777;
            var defBtm = new Bitmap(imgSize, imgSize);
            var g = Graphics.FromImage(defBtm);

            var r = new Random();

            g.FillRectangle(new SolidBrush(Color.FromArgb(r.Next(0, 50), r.Next(0, 50), r.Next(0, 50))), 0, 0, imgSize, imgSize); // bg

            for (int i = 0; i < r.Next(1000, 3000); i++) {
                var den = r.Next(200, 255);
                var br = new SolidBrush(Color.FromArgb(den, den, den));

                den -= 100;

                var pr = new Pen(Color.FromArgb(den, den, den), 1);

                var size = r.Next(1, 8);
                var x = r.Next(0, imgSize - size);
                var y = r.Next(0, imgSize - size);
                g.FillEllipse(br, x, y, size, size);
                g.DrawEllipse(pr, x, y, size, size);
            }

            g.DrawString("Виртуальный сканер", new Font(FontFamily.GenericSerif, 25), Brushes.Aqua, new RectangleF(0, 0, imgSize, imgSize));

            g.Flush();

            return defBtm;
        }

        private void SaveConfig() {
            var settings = new List<string>();
            settings.Add("[device]");
            settings.Add(String.Format("DeviceID;{0}", _scanDevice.DeviceID));

            foreach (IProperty property in _scanDevice.Properties) {
                var propstring = string.Format("{1}{0}{2}{0}{3}", ";", property.Name, property.PropertyID, property.get_Value());
                settings.Add(propstring);
            }

            settings.Add("[item]");
            settings.Add(String.Format("ItemID;{0}", _scannerItem.ItemID));
            foreach (IProperty property in _scannerItem.Properties) {
                var propstring = string.Format("{1}{0}{2}{0}{3}", ";", property.Name, property.PropertyID, property.get_Value());
                settings.Add(propstring);
            }

            File.WriteAllLines(Config, settings.ToArray());
        }

        private enum loadMode {undef, device, item};

        private void LoadConfig() {
            var settings = File.ReadAllLines(Config);

            var mode  = loadMode.undef;

            foreach (var setting in settings) {
                if (setting.StartsWith("[device]")) {
                    mode = loadMode.device;
                    continue;
                }

                if (setting.StartsWith("[item]")) {
                    mode = loadMode.item;
                    continue;
                }
                
                if (setting.StartsWith("DeviceID")) {
                    var deviceid = setting.Split(';')[1];
                    var devMngr = new DeviceManagerClass();

                    foreach (IDeviceInfo deviceInfo in devMngr.DeviceInfos) {
                        if (deviceInfo.DeviceID == deviceid) {
                            _scanDevice = deviceInfo.Connect();
                            break;
                        }
                    }

                    if (_scanDevice == null) {
                        MessageBox.Show("Сканнер из конигурации не найден");
                        return;
                    }

                    _scannerItem = _scanDevice.Items[1];
                    continue;
                }

                if (setting.StartsWith("ItemID")) {
                    var itemid = setting.Split(';')[1];
                    continue;
                }

                var sett = setting.Split(';');
                switch (mode) {
                    case loadMode.device:
                        SetProp(_scanDevice.Properties, sett[1], sett[2]);
                    break;

                    case loadMode.item:
                        SetProp(_scannerItem.Properties, sett[1], sett[2]);
                    break;
                }
            }
            SaveProp(_scanDevice.Properties, ref _defaultDeviceProp);
        }

        private static void SetProp(IProperties prop, object property, object value) {
            try {
                prop[property].set_Value(value);
            } catch (Exception) {
                return;
            }
        }
    }
}

Конечно, Scanner нельзя назвать полноценным инструментом для работы со сканером, но это готовое решение для работы с различными типами сканеров. Этот пример иллюстрирует принцип работы с WIA из-под. NET, и может быть основой для построения программ сканирования.

А так мы сканируем распоряжения HR отдела;)

[SC]Работаем со сканером - 7

Спасибо за внимание, до новых встреч на Хабре!

Автор: Тинькофф Банк

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js