扫描仪对接(C#)(Twain)

前言

对接扫描仪的几种方式:

TWAIN 此为大多数扫描仪基础协议。是C++语言写的底层dll,对.NET来说通过DLLImport来扩展使用。

此协议是很底层的协议,并没有经过.NET封装。所以要了解其机制才能更好的来开发。

.NET 例子:

http://www.codeproject.com/Articles/1376/NET-TWAIN-image-scanner

http://www.codeproject.com/Articles/171666/Twain-for-WPF-Applications-Look-Ma-No-Handles

WIA .NET已经封装了支持该协议的dll。

相关文章:

https://blog.csdn.net/younghaiqing/article/details/78431454

https://www.likecs.com/show-503057.html

Twain方式1

方式2在部分扫描以上 扫描弹出设置的时候会报错,所以有找的开源的代码:

源代码地址:http://www.codeproject.com/Articles/171666/Twain-for-WPF-Applications-Look-Ma-No-Handles

https://gitee.com/psvmc/scanner_test

TwainDefs.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
using System;
using System.Runtime.InteropServices;

namespace ztwain
{
public class TwProtocol
{ // TWON_PROTOCOL...
public const short Major = 1;
public const short Minor = 9;
}

[Flags]
internal enum TwDg : short
{
// DG_.....
Control = 0x0001,
Image = 0x0002,
Audio = 0x0004
}

internal enum TwDat : short
{
// DAT_....
Null = 0x0000,
Capability = 0x0001,
Event = 0x0002,
Identity = 0x0003,
Parent = 0x0004,
PendingXfers = 0x0005,
SetupMemXfer = 0x0006,
SetupFileXfer = 0x0007,
Status = 0x0008,
UserInterface = 0x0009,
XferGroup = 0x000a,
TwunkIdentity = 0x000b,
CustomDsData = 0x000c,
DeviceEvent = 0x000d,
FileSystem = 0x000e,
PassThru = 0x000f,

ImageInfo = 0x0101,
ImageLayout = 0x0102,
ImageMemXfer = 0x0103,
ImageNativeXfer = 0x0104,
ImageFileXfer = 0x0105,
CieColor = 0x0106,
GrayResponse = 0x0107,
RgbResponse = 0x0108,
JpegCompression = 0x0109,
Palette8 = 0x010a,
ExtImageInfo = 0x010b,

SetupFileXfer2 = 0x0301
}

internal enum TwMsg : short
{
// MSG_.....
Null = 0x0000,
Get = 0x0001,
GetCurrent = 0x0002,
GetDefault = 0x0003,
GetFirst = 0x0004,
GetNext = 0x0005,
Set = 0x0006,
Reset = 0x0007,
QuerySupport = 0x0008,

XFerReady = 0x0101,
CloseDsReq = 0x0102,
CloseDsok = 0x0103,
DeviceEvent = 0x0104,

CheckStatus = 0x0201,

OpenDsm = 0x0301,
CloseDsm = 0x0302,

OpenDs = 0x0401,
CloseDs = 0x0402,
UserSelect = 0x0403,

DisableDs = 0x0501,
EnableDs = 0x0502,
EnableDsuiOnly = 0x0503,

ProcessEvent = 0x0601,

EndXfer = 0x0701,
StopFeeder = 0x0702,

ChangeDirectory = 0x0801,
CreateDirectory = 0x0802,
Delete = 0x0803,
FormatMedia = 0x0804,
GetClose = 0x0805,
GetFirstFile = 0x0806,
GetInfo = 0x0807,
GetNextFile = 0x0808,
Rename = 0x0809,
Copy = 0x080A,
AutoCaptureDir = 0x080B,

PassThru = 0x0901
}

internal enum TwRc : short
{
// TWRC_....
Success = 0x0000,
Failure = 0x0001,
CheckStatus = 0x0002,
Cancel = 0x0003,
DsEvent = 0x0004,
NotDsEvent = 0x0005,
XferDone = 0x0006,
EndOfList = 0x0007,
InfoNotSupported = 0x0008,
DataNotAvailable = 0x0009
}

internal enum TwCc : short
{
// TWCC_....
Success = 0x0000,
Bummer = 0x0001,
LowMemory = 0x0002,
NoDs = 0x0003,
MaxConnections = 0x0004,
OperationError = 0x0005,
BadCap = 0x0006,
BadProtocol = 0x0009,
BadValue = 0x000a,
SeqError = 0x000b,
BadDest = 0x000c,
CapUnsupported = 0x000d,
CapBadOperation = 0x000e,
CapSeqError = 0x000f,
Denied = 0x0010,
FileExists = 0x0011,
FileNotFound = 0x0012,
NotEmpty = 0x0013,
PaperJam = 0x0014,
PaperDoubleFeed = 0x0015,
FileWriteError = 0x0016,
CheckDeviceOnline = 0x0017
}

internal enum TwOn : short
{ // TWON_....
Array = 0x0003,
Enum = 0x0004,
One = 0x0005,
Range = 0x0006,
DontCare = -1
}

internal enum TwType : short
{ // TWTY_....
Int8 = 0x0000,
Int16 = 0x0001,
Int32 = 0x0002,
UInt8 = 0x0003,
UInt16 = 0x0004,
UInt32 = 0x0005,
Bool = 0x0006,
Fix32 = 0x0007,
Frame = 0x0008,
Str32 = 0x0009,
Str64 = 0x000a,
Str128 = 0x000b,
Str255 = 0x000c,
Str1024 = 0x000d,
Str512 = 0x000e
}

internal enum TwCap
{
XferCount = 0x0001, // CAP_XFERCOUNT
Compression = 0x0100, // ICAP_...
PixelType = 0x0101,
Units = 0x0102,
XferMech = 0x0103,
CapDuplexenabled = 0x1013, //单双面 类型TwType.Bool
IcapAutomaticdeskew = 0x1151, //歪斜校正 类型TwType.Bool
CcapCeiBlankskip = 0x8001, //跳过空白页 类型TwType.Int32
CcapCeiCropdetect = 0x8000 + 37 //消除黑框
}

// ------------------- STRUCTS --------------------------------------------

[StructLayout(LayoutKind.Sequential, Pack = 2, CharSet = CharSet.Ansi)]
internal class TwIdentity
{ // TW_IDENTITY
public IntPtr Id;
public TwVersion Version;
public short ProtocolMajor;
public short ProtocolMinor;
public int SupportedGroups;

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 34)]
public string Manufacturer;

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 34)]
public string ProductFamily;

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 34)]
public string ProductName;
}

[StructLayout(LayoutKind.Sequential, Pack = 2, CharSet = CharSet.Ansi)]
internal struct TwVersion
{ // TW_VERSION
public short MajorNum;
public short MinorNum;
public short Language;
public short Country;

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 34)]
public string Info;
}

[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal class TwUserInterface
{ // TW_USERINTERFACE
public short ShowUI; // bool is strictly 32 bit, so use short
public short ModalUI;
public IntPtr ParentHand;
}

[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal class TwStatus
{ // TW_STATUS
public short ConditionCode; // TwCC
public short Reserved;
}

[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal struct TwEvent
{ // TW_EVENT
public IntPtr EventPtr;
public short Message;
}

[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal class TwImageInfo
{ // TW_IMAGEINFO
public int XResolution;
public int YResolution;
public int ImageWidth;
public int ImageLength;
public short SamplesPerPixel;

[MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
public short[] BitsPerSample;

public short BitsPerPixel;
public short Planar;
public short PixelType;
public short Compression;
}

[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal class TwPendingXfers
{ // TW_PENDINGXFERS
public short Count;
public int EOJ;
}

[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal struct TwFix32
{ // TW_FIX32
public short Whole;
public ushort Frac;

public float ToFloat()
{
return Whole + (Frac / 65536.0f);
}

public void FromFloat(float f)
{
int i = (int)((f * 65536.0f) + 0.5f);
Whole = (short)(i >> 16);
Frac = (ushort)(i & 0x0000ffff);
}
}

[StructLayout(LayoutKind.Sequential, Pack = 2)]
internal class TwCapability
{ // TW_CAPABILITY
public TwCapability(TwCap cap)
{
Cap = (short)cap;
ConType = -1;
}

public TwCapability(TwCap cap, short sval)
{
Cap = (short)cap;
ConType = (short)TwOn.One;
Handle = TwainWin32.GlobalAlloc(0x42, 6);
IntPtr pv = TwainWin32.GlobalLock(Handle);
Marshal.WriteInt16(pv, 0, (short)TwType.Int16);
Marshal.WriteInt32(pv, 2, sval);
TwainWin32.GlobalUnlock(Handle);
}

~TwCapability()
{
if (Handle != IntPtr.Zero)
TwainWin32.GlobalFree(Handle);
}

public short Cap;
public short ConType;
public IntPtr Handle;
}
}

TwainLib.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace ztwain
{
public enum TwainCommand
{
Not = -1,
Null = 0,
TransferReady = 1,
CloseRequest = 2,
CloseOk = 3,
DeviceEvent = 4
}

public delegate void ProductNameHandler(string productName);

public class Twain
{
private const short CountryUsa = 1;
private const short LanguageUsa = 13;
private readonly ProductNameHandler _productNameHandler;
private bool _isOpenDs;

public Twain(ProductNameHandler handler)
{
_appid = new TwIdentity();
_appid.Id = IntPtr.Zero;
_appid.Version.MajorNum = 1;
_appid.Version.MinorNum = 1;
_appid.Version.Language = LanguageUsa;
_appid.Version.Country = CountryUsa;
_appid.Version.Info = "Hack 1";
_appid.ProtocolMajor = TwProtocol.Major;
_appid.ProtocolMinor = TwProtocol.Minor;
_appid.SupportedGroups = (int)(TwDg.Image | TwDg.Control);
_appid.Manufacturer = "NETMaster";
_appid.ProductFamily = "Freeware";
_appid.ProductName = "Hack";

_srcds = new TwIdentity();
_srcds.Id = IntPtr.Zero;

_evtmsg.EventPtr = Marshal.AllocHGlobal(Marshal.SizeOf(_winmsg));
_productNameHandler = handler;
}

~Twain()
{
Marshal.FreeHGlobal(_evtmsg.EventPtr);
}

public void Init(IntPtr hwndp)
{
Finish();
TwRc rc = DSMparent(_appid, IntPtr.Zero, TwDg.Control, TwDat.Parent, TwMsg.OpenDsm, ref hwndp);
if (rc == TwRc.Success)
{
rc = DSMident(_appid, IntPtr.Zero, TwDg.Control, TwDat.Identity, TwMsg.GetDefault, _srcds);
if (rc == TwRc.Success)
_hwnd = hwndp;
else
DSMparent(_appid, IntPtr.Zero, TwDg.Control, TwDat.Parent, TwMsg.CloseDsm, ref hwndp);
}

if (_productNameHandler != null)
{
_productNameHandler(_srcds.ProductName);
}
}

public void Select()
{
CloseSrc();
if (_appid.Id == IntPtr.Zero)
{
Init(_hwnd);
if (_appid.Id == IntPtr.Zero)
return;
}

DSMident(_appid, IntPtr.Zero, TwDg.Control, TwDat.Identity, TwMsg.UserSelect, _srcds);
if (_productNameHandler != null)
{
_productNameHandler(_srcds.ProductName);
}
}

public void Acquire()
{
Acquire(true, -1);
}

public void Acquire(bool showUi, short num)
{
TwRc rc;
CloseSrc();
if (_appid.Id == IntPtr.Zero)
{
Init(_hwnd);
if (_appid.Id == IntPtr.Zero)
return;
}

rc = DSMident(_appid, IntPtr.Zero, TwDg.Control, TwDat.Identity, TwMsg.OpenDs, _srcds);
if (rc != TwRc.Success)
return;

_isOpenDs = true;

TwCapability cap = new TwCapability(TwCap.XferCount, num);
rc = DScap(_appid, _srcds, TwDg.Control, TwDat.Capability, TwMsg.Set, cap);

//跳过空白页
//TwCapability cap1 = new TwCapability(TwCap.CCAP_CEI_BLANKSKIP, 0); //1为跳过空白页
//rc = DScap(appid, srcds, TwDG.Control, TwDAT.Capability, TwMSG.Set, cap1);

//单面,双面
//TwCapability cap2 = new TwCapability(TwCap.CAP_DUPLEXENABLED, 1); //0单面,1双面
//rc = DScap(appid, srcds, TwDG.Control, TwDAT.Capability, TwMSG.Set, cap2);

//歪斜校正
//TwCapability cap3 = new TwCapability(TwCap.ICAP_AUTOMATICDESKEW, 1);
//rc = DScap(appid, srcds, TwDG.Control, TwDAT.Capability, TwMSG.Set, cap3);

//歪斜校正
//TwCapability cap4 = new TwCapability(TwCap.CCAP_CEI_CROPDETECT, 1);
//rc = DScap(appid, srcds, TwDG.Control, TwDAT.Capability, TwMSG.Set, cap4);

if (rc != TwRc.Success)
{
CloseSrc();
return;
}

TwUserInterface guif = new TwUserInterface();
guif.ShowUI = (short)(showUi ? 1 : 0);
guif.ModalUI = 0;
guif.ParentHand = _hwnd;
rc = DSuserif(_appid, _srcds, TwDg.Control, TwDat.UserInterface, TwMsg.EnableDs, guif);
if (rc != TwRc.Success)
{
CloseSrc();
}
}

public void TransferPictures(Action<IntPtr> picAction)
{
if (_srcds.Id == IntPtr.Zero)
return;

TwRc rc;
IntPtr hbitmap;
TwPendingXfers pxfr = new TwPendingXfers();

do
{
pxfr.Count = 0;
hbitmap = IntPtr.Zero;

TwImageInfo iinf = new TwImageInfo();
rc = DSiinf(_appid, _srcds, TwDg.Image, TwDat.ImageInfo, TwMsg.Get, iinf);
if (rc != TwRc.Success)
{
CloseSrc();
return;
}

rc = DSixfer(_appid, _srcds, TwDg.Image, TwDat.ImageNativeXfer, TwMsg.Get, ref hbitmap);
if (rc != TwRc.XferDone)
{
CloseSrc();
return;
}

rc = DSpxfer(_appid, _srcds, TwDg.Control, TwDat.PendingXfers, TwMsg.EndXfer, pxfr);
if (rc != TwRc.Success)
{
CloseSrc();
return;
}

picAction(hbitmap);
} while (pxfr.Count != 0);

DSpxfer(_appid, _srcds, TwDg.Control, TwDat.PendingXfers, TwMsg.Reset, pxfr);
}

public TwainCommand PassMessage(ref Message m)
{
if (_srcds.Id == IntPtr.Zero)
return TwainCommand.Not;

int pos = TwainWin32.GetMessagePos();

_winmsg.hwnd = m.HWnd;
_winmsg.message = m.Msg;
_winmsg.wParam = m.WParam;
_winmsg.lParam = m.LParam;
_winmsg.time = TwainWin32.GetMessageTime();
_winmsg.x = (short)pos;
_winmsg.y = (short)(pos >> 16);

Marshal.StructureToPtr(_winmsg, _evtmsg.EventPtr, false);
_evtmsg.Message = 0;
TwRc rc = DSevent(_appid, _srcds, TwDg.Control, TwDat.Event, TwMsg.ProcessEvent, ref _evtmsg);
if (rc == TwRc.NotDsEvent)
return TwainCommand.Not;
if (_evtmsg.Message == (short)TwMsg.XFerReady)
return TwainCommand.TransferReady;
if (_evtmsg.Message == (short)TwMsg.CloseDsReq)
return TwainCommand.CloseRequest;
if (_evtmsg.Message == (short)TwMsg.CloseDsok)
return TwainCommand.CloseOk;
if (_evtmsg.Message == (short)TwMsg.DeviceEvent)
return TwainCommand.DeviceEvent;

return TwainCommand.Null;
}

public void CloseSrc()
{
if (_srcds.Id != IntPtr.Zero)
{
TwUserInterface guif = new TwUserInterface();
DSuserif(_appid, _srcds, TwDg.Control, TwDat.UserInterface, TwMsg.DisableDs, guif);
if (_isOpenDs)
{
DSMident(_appid, IntPtr.Zero, TwDg.Control, TwDat.Identity, TwMsg.CloseDs, _srcds);
_isOpenDs = false;
}
}
}

public void Finish()
{
CloseSrc();
if (_appid.Id != IntPtr.Zero)
DSMparent(_appid, IntPtr.Zero, TwDg.Control, TwDat.Parent, TwMsg.CloseDsm, ref _hwnd);
_appid.Id = IntPtr.Zero;
}

private IntPtr _hwnd;
private readonly TwIdentity _appid;
private readonly TwIdentity _srcds;
private TwEvent _evtmsg;
private Winmsg _winmsg;

// ------ DSM entry point DAT_ variants:
[DllImport("twain_32.dll", EntryPoint = "#1")]
private static extern TwRc DSMparent([In, Out] TwIdentity origin, IntPtr zeroptr, TwDg dg, TwDat dat, TwMsg msg,
ref IntPtr refptr);

[DllImport("twain_32.dll", EntryPoint = "#1")]
private static extern TwRc DSMident([In, Out] TwIdentity origin, IntPtr zeroptr, TwDg dg, TwDat dat, TwMsg msg,
[In, Out] TwIdentity idds);

[DllImport("twain_32.dll", EntryPoint = "#1")]
// ReSharper disable once UnusedMember.Local
private static extern TwRc DSMstatus([In, Out] TwIdentity origin, IntPtr zeroptr, TwDg dg, TwDat dat, TwMsg msg,
[In, Out] TwStatus dsmstat);

// ------ DSM entry point DAT_ variants to DS:
[DllImport("twain_32.dll", EntryPoint = "#1")]
private static extern TwRc DSuserif([In, Out] TwIdentity origin, [In, Out] TwIdentity dest, TwDg dg, TwDat dat,
TwMsg msg, TwUserInterface guif);

[DllImport("twain_32.dll", EntryPoint = "#1")]
private static extern TwRc DSevent([In, Out] TwIdentity origin, [In, Out] TwIdentity dest, TwDg dg, TwDat dat,
TwMsg msg, ref TwEvent evt);

[DllImport("twain_32.dll", EntryPoint = "#1")]
// ReSharper disable once UnusedMember.Local
private static extern TwRc DSstatus([In, Out] TwIdentity origin, [In] TwIdentity dest, TwDg dg, TwDat dat,
TwMsg msg, [In, Out] TwStatus dsmstat);

[DllImport("twain_32.dll", EntryPoint = "#1")]
private static extern TwRc DScap([In, Out] TwIdentity origin, [In] TwIdentity dest, TwDg dg, TwDat dat,
TwMsg msg, [In, Out] TwCapability capa);

[DllImport("twain_32.dll", EntryPoint = "#1")]
private static extern TwRc DSiinf([In, Out] TwIdentity origin, [In] TwIdentity dest, TwDg dg, TwDat dat,
TwMsg msg, [In, Out] TwImageInfo imginf);

[DllImport("twain_32.dll", EntryPoint = "#1")]
private static extern TwRc DSixfer([In, Out] TwIdentity origin, [In] TwIdentity dest, TwDg dg, TwDat dat,
TwMsg msg, ref IntPtr hbitmap);

[DllImport("twain_32.dll", EntryPoint = "#1")]
private static extern TwRc DSpxfer([In, Out] TwIdentity origin, [In] TwIdentity dest, TwDg dg, TwDat dat,
TwMsg msg, [In, Out] TwPendingXfers pxfr);

[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal struct Winmsg
{
public IntPtr hwnd;
public int message;
public IntPtr wParam;
public IntPtr lParam;
public int time;
public int x;
public int y;
}
}
}

TwainWin32.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
using System;
using System.Runtime.InteropServices;

namespace ztwain
{
/// <summary>
/// Imports from Windows32
/// </summary>
public static class TwainWin32
{
[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern IntPtr GlobalLock(IntPtr handle);

[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern IntPtr GlobalUnlock(IntPtr handle);

[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern IntPtr GlobalAlloc(int flags, int size);

[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern IntPtr GlobalFree(IntPtr handle);

[DllImport("user32")]
public static extern bool PostMessage(IntPtr hwnd, uint msg, IntPtr wparam, IntPtr lparam);

[DllImport("user32")]
public static extern uint RegisterWindowMessage(string lpString);

[DllImport("user32")]
public static extern bool SendMessage(IntPtr hwnd, uint msg, IntPtr wparam, IntPtr lparam);

[DllImport("user32.dll")]
public static extern int FindWindow(string lpClassName, string lpWindowName);

[DllImport("user32.dll", ExactSpelling = true)]
public static extern int GetMessagePos();

[DllImport("user32.dll", ExactSpelling = true)]
public static extern int GetMessageTime();

[DllImport("gdi32.dll", ExactSpelling = true)]
public static extern int GetDeviceCaps(IntPtr hDc, int nIndex);

[DllImport("gdi32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr CreateDC(string szdriver, string szdevice, string szoutput, IntPtr devmode);

[DllImport("gdi32.dll", ExactSpelling = true)]
public static extern bool DeleteDC(IntPtr hdc);

[StructLayout(LayoutKind.Sequential, Pack = 2)]
public class Bitmapinfoheader
{
public uint biSize;
public int biWidth;
public int biHeight;
public ushort biPlanes;
public ushort biBitCount;
public uint biCompression;
public uint biSizeImage;
public int biXPelsPerMeter;
public int biYPelsPerMeter;
public uint biClrUsed;
public uint biClrImportant;
}
}
}

TwainDibToBitmap.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
using System;
using System.Runtime.InteropServices;
using System.Windows.Media.Imaging;
using System.Windows.Media;

namespace ztwain
{
internal static class TwainDibToBitmap
{
/// <summary>
/// Get managed BitmapSource from a DIB provided as a low level windows hadle
///
/// Notes:
/// Data is copied from the source so the windows handle can be saftely discarded
/// even when the BitmapSource is in use.
///
/// Only a subset of possible DIB forrmats is supported.
///
/// </summary>
/// <param name="dibHandle"></param>
/// <returns>A copy of the image in a managed BitmapSource </returns>
///
public static BitmapSource FormHDib(IntPtr dibHandle)
{
BitmapSource bs = null;
IntPtr bmpPtr = IntPtr.Zero;

try
{
bmpPtr = TwainWin32.GlobalLock(dibHandle);
TwainWin32.Bitmapinfoheader bmi = new TwainWin32.Bitmapinfoheader();
Marshal.PtrToStructure(bmpPtr, bmi);

if (bmi.biSizeImage == 0)
bmi.biSizeImage = (uint)(((((bmi.biWidth * bmi.biBitCount) + 31) & ~31) >> 3) * bmi.biHeight);

int palettSize = 0;

if (bmi.biClrUsed != 0)
throw new NotSupportedException("DibToBitmap: DIB with pallet is not supported");

// pointer to the beginning of the bitmap bits
// ReSharper disable once UselessBinaryOperation
IntPtr pixptr = (IntPtr)((int)bmpPtr + bmi.biSize + palettSize);

// Define parameters used to create the BitmapSource.
PixelFormat pf;
switch (bmi.biBitCount)
{
case 32:
pf = PixelFormats.Bgr32;
break;

case 24:
pf = PixelFormats.Bgr24;
break;

case 8:
pf = PixelFormats.Gray8;
break;

case 1:
pf = PixelFormats.BlackWhite;
break;

default: // not supported
throw new NotSupportedException("DibToBitmap: Can't determine picture format (biBitCount=" + bmi.biBitCount + ")");
// break;
}
int width = bmi.biWidth;
int height = bmi.biHeight;
int stride = (int)(bmi.biSizeImage / height);
byte[] imageBytes = new byte[stride * height];

//Debug: Initialize the image with random data.
//Random value = new Random();
//value.NextBytes(rawImage);
for (int i = 0, j = 0, k = (height - 1) * stride; i < height; i++, j += stride, k -= stride)
Marshal.Copy(((IntPtr)((int)pixptr + j)), imageBytes, k, stride);

int xDpi = (int)Math.Round(bmi.biXPelsPerMeter * 2.54 / 100); // pels per meter to dots per inch
int yDpi = (int)Math.Round(bmi.biYPelsPerMeter * 2.54 / 100);

// Create a BitmapSource.
bs = BitmapSource.Create(width, height, xDpi, yDpi, pf, null, imageBytes, stride);
}
catch (Exception)
{
// ignored
}
finally
{
// cleanup
if (bmpPtr != IntPtr.Zero)
{ // locked sucsessfully
TwainWin32.GlobalUnlock(dibHandle);
}
}
return bs;
}
}
}

TwainWPF.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
using System;
using System.Windows;

using System.Windows.Interop;
using System.Windows.Media.Imaging;

namespace ztwain
{
// A delegate type for hooking up change notifications.
public delegate void TwainEventHandler(TwainWpf sender);

public delegate void TwainProductNameHandler(TwainWpf sender, string productName);

public delegate void TwainTransferReadyHandler(TwainWpf sender, BitmapSource imageSource);

public delegate void TwainTransferFinishHandler(TwainWpf sender);

public class TwainWpf : DependencyObject
{
// events
public event TwainEventHandler TwainCloseRequest;
public event TwainEventHandler TwainCloseOk;
public event TwainEventHandler TwainDeviceEvent;
public event TwainTransferReadyHandler TwainTransferReady;
public event TwainTransferFinishHandler TwainTransferFinish;
public event TwainProductNameHandler TwainProductNameEvent;

private bool _twainMessageProcessing;
private Twain _tw;

private IntPtr _handle = IntPtr.Zero;

public Window TheMainWindow;

public uint WmAppAcquire = TwainWin32.RegisterWindowMessage("IBN_WfpTwain_Acquire");

public IntPtr WindowHandle
{
get
{
if (_handle == IntPtr.Zero)
_handle = (new WindowInteropHelper(TheMainWindow)).Handle;
return _handle;
}
}

public TwainWpf(Window win)
{
TheMainWindow = win;
// hook to events of the main window
if (WindowHandle != IntPtr.Zero)
{
// main windows is initialized and we can hook events and start woking with it
HostWindow_Loaded(this, null);
}
else
{
// hook events etc later, when the main window is loaded.
TheMainWindow.Loaded += HostWindow_Loaded;
}
TheMainWindow.Closing += HostWindow_Closing;
}

~TwainWpf()
{
// by now the interface should already be closed. we call terminate just in case.
TerminateTw();
}

/// <summary>
/// Open the Twain source selection dialog
/// </summary>
/// <returns></returns>
public bool Select()
{
if (_tw != null)
{
_tw.Select();
return true;
}

return false;
}

/// <summary>
/// Activate Twain aquire
///
/// notes:
/// Activation is done using post message to reduce friction between WPF and Windows events.
/// </summary>
/// <param name="showUi"></param>
/// <returns></returns>
public bool Acquire(bool showUi)
{
if (_tw != null)
{
_twainMessageProcessing = true;
bool posted =
TwainWin32.PostMessage(WindowHandle, WmAppAcquire, (IntPtr)(showUi ? 1 : 0), IntPtr.Zero);
return posted;
}
else
return false;
}

private void HostWindow_Loaded(object sender, RoutedEventArgs e)
{
AddMessageHook();
_tw = new Twain(name =>
{
if (TwainProductNameEvent != null)
{
TwainProductNameEvent(this, name);
}
});
_tw.Init(WindowHandle);
}

private void HostWindow_Closing(object sender, EventArgs e)
{
RemoveMessageHook();
}

private void AddMessageHook()
{
HwndSource src = HwndSource.FromHwnd(WindowHandle);
if (src != null) src.AddHook(this.WndProc);
}

private void RemoveMessageHook()
{
HwndSource src = HwndSource.FromHwnd(WindowHandle);
if (src != null) src.AddHook(this.WndProc);
}

private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
System.Windows.Forms.Message m = new System.Windows.Forms.Message();
m.HWnd = hwnd;
m.Msg = msg;
m.WParam = wParam;
m.LParam = lParam;

if (handled)
return IntPtr.Zero;

if (msg == WmAppAcquire)
{
_tw.Acquire(wParam != IntPtr.Zero, -1);
}

//registered with "DSMAPPMESSAGE32"
if (_tw != null)
{
PreFilterMessage(m, ref handled);
}
return IntPtr.Zero;
}

private void TerminateTw()
{
if (_tw != null)
{
_tw.Finish();
_tw = null;
}

_twainMessageProcessing = false;
}

// Twain event callbacks
protected void OnTwainCloseRequest()
{
if (TwainCloseRequest != null)
TwainCloseRequest(this);
_tw.CloseSrc();
}

protected void OnTwainCloseOk()
{
if (TwainCloseOk != null)
TwainCloseOk(this);

//EndingScan();
_tw.CloseSrc();
}

protected void OnTwainDeviceEvent()
{
if (TwainDeviceEvent != null)
TwainDeviceEvent(this);
}

protected void OnTwainTransferReady()
{
if (TwainTransferReady == null)
return;
_tw.TransferPictures(imgHandle =>
{
BitmapSource image = TwainDibToBitmap.FormHDib(imgHandle);
TwainTransferReady(this, image);
TwainWin32.GlobalFree(imgHandle);
});
_tw.CloseSrc();
EndingScan();
if (TwainTransferFinish != null)
{
TwainTransferFinish(this);
}
}

private void EndingScan()
{
if (_twainMessageProcessing)
{
_twainMessageProcessing = false;
}
}

protected void PreFilterMessage(System.Windows.Forms.Message m, ref bool handled)
{
TwainCommand cmd = _tw.PassMessage(ref m);
if (cmd == TwainCommand.Not || cmd == TwainCommand.Null)
return;

switch (cmd)
{
case TwainCommand.CloseRequest:
{
OnTwainCloseRequest();
break;
}
case TwainCommand.CloseOk:
{
OnTwainCloseOk();
break;
}
case TwainCommand.DeviceEvent:
{
OnTwainDeviceEvent();
break;
}
case TwainCommand.TransferReady:
{
OnTwainTransferReady();
break;
}
}

handled = true;
}
}
}

调用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
using System;
using System.Windows;
using System.Windows.Media.Imaging;
using ztwain;

namespace scanner_test
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
protected TwainWpf TwainInterface;

public MainWindow()
{
InitializeComponent();
TwainInterface = new TwainWpf(this);
TwainInterface.TwainTransferReady += TwainInterface_TwainTransferReady;
TwainInterface.TwainTransferFinish += TwainInterface_TwainTransferFinish;
TwainInterface.TwainCloseRequest += TwainInterface_TwainCloseRequest;
TwainInterface.TwainProductNameEvent += TwainInterface_TwainProductNameEvent;

SelectBtn.Click += Select_btn_Click;
ScannerBtn.Click += Scanner_btn_Click;
}


//选中扫描仪后的回调
private void TwainInterface_TwainProductNameEvent(TwainWpf sender, string productName)
{
ScannerName.Text = productName;
}

private void TwainInterface_TwainCloseRequest(TwainWpf sender)
{
}

//单页扫描结果
private void TwainInterface_TwainTransferReady(TwainWpf sender, BitmapSource imageSource)
{
//建议这里保存图片到本地 把图片地址保存在列表中
Console.WriteLine(@"扫描到一张");
}

//一批扫描结束
private void TwainInterface_TwainTransferFinish(TwainWpf sender)
{
//从图片列表中获取图片地址,进行处理
}

private void Select_btn_Click(object sender, RoutedEventArgs e)
{
TwainInterface.Select();
}

private void Scanner_btn_Click(object sender, RoutedEventArgs e)
{
TwainInterface.Acquire(true);
}
}
}

页面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<Window
x:Class="scanner_test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="扫描仪测试"
Width="600"
Height="100"
mc:Ignorable="d">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="100" />
</Grid.ColumnDefinitions>


<Button
Name="SelectBtn"
Width="80"
Height="30"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="选择" />
<TextBlock
Name="ScannerName"
Grid.Column="1"
Width="200"
VerticalAlignment="Center" />
<Button
Name="ScannerBtn"
Grid.Column="2"
Width="80"
Height="30"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="扫描" />

</Grid>
</Window>

保存图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// <summary>
/// 保存图片到文件
/// </summary>
/// <param name="image">图片数据</param>
/// <param name="filePath">保存路径</param>
public static void SaveImageToFile(BitmapSource image, string filePath)
{
BitmapEncoder encoder = GetBitmapEncoder(filePath);
encoder.Frames.Add(BitmapFrame.Create(image));

using (var stream = new FileStream(filePath, FileMode.Create))
{
encoder.Save(stream);
}
}

Twain方式2

添加引用

Nuget 添加依赖 NTwain

image-20220729142158095

Github网址

https://github.com/soukoku/ntwain

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private TwainSession session = null;

//所有的扫描仪
private List<DataSource> sourceList = new List<DataSource>();

//当前使用的扫描仪
private DataSource source_current = null;

private void initScanner()
{
//Allow old Device DSM drives
PlatformInfo.Current.PreferNewDSM = false;
//开始扫描
var appId = TWIdentity.CreateFromAssembly(DataGroups.Image, Assembly.GetExecutingAssembly());
session = new TwainSession(appId);
session.TransferReady += Session_TransferReady;
session.DataTransferred += Session_DataTransferred;
if (session.Open() != ReturnCode.Success)
{
Console.WriteLine("扫描启动失败");
}

var source_list = session.GetSources();
foreach (DataSource source in source_list)
{
sourceList.Add(source);
}
}

private void Session_TransferReady(object sender, TransferReadyEventArgs e)
{
Console.WriteLine("Session_TransferReady");
}

private void Session_DataTransferred(object sender, DataTransferredEventArgs e)
{
//ImageSource img = GenerateThumbnail(e);
Console.WriteLine(GenerateFile(e));
}

DataSource可用以下的属性

  • int Id 如:349 每次重启应用的时候Id会变。
  • string Name 如:Canon DR-M160 TWAIN
  • string Manufacturer 如:Copyright CANON ELECTRONICS INC.
  • string ProductFamily 如:Canon DR-M160 Driver
  • bool IsOpen 如:False

对于双面扫描我们可以通过下面的方式来判断

每一页扫描都会先触发一次Session_TransferReady,之后触发两次Session_DataTransferred,从而来判定一个页面的两面。

扫描

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void Scanner_btn_Click(object sender, System.Windows.RoutedEventArgs e)
{
if (source_current == null)
{
source_current = sourceList[0];
}
if (!source_current.IsOpen)
{
source_current.Open();
}
try
{
session.CurrentSource.Enable(SourceEnableMode.ShowUI, false, new WindowInteropHelper(this).Handle);
}
catch (Exception err)
{
System.Windows.MessageBox.Show(err.ToString());
}
}

其中第一个参数可以传如下的值

  • SourceEnableMode.ShowUI 显示扫描仪配置页面,并且能进行扫描。
  • SourceEnableMode.NoUI 直接扫描不显示配置页面。
  • SourceEnableMode.ShowUIOnly 只显示配置页面,不能扫描,一般和上面的配合使用。

图片处理

Session_DataTransferred回调中我们可以处理图片

获取扫描图片的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private string GenerateFile(DataTransferredEventArgs e)
{
string imagepath = "";

switch (e.TransferType)
{
case XferMech.Native:
using (System.IO.Stream stream = e.GetNativeImageStream())
{
if (stream != null)
{
string docPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
var dirPath = Path.Combine(docPath, "scanner_pic");
DirectoryInfo directoryInfo = new DirectoryInfo(dirPath);
if (!directoryInfo.Exists)
{
System.IO.Directory.CreateDirectory(dirPath);
}
string timestr = DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss-ffff");
imagepath = Path.Combine(dirPath, timestr + ".jpg");
using (var fileStream = File.Create(imagepath))
{
stream.CopyTo(fileStream);
}
}
}
break;

case XferMech.File:
imagepath = e.FileDataPath;
break;
}
return imagepath;
}

获取扫描的缩略图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private ImageSource GenerateThumbnail(DataTransferredEventArgs e)
{
BitmapSource img = null;

switch (e.TransferType)
{
case XferMech.Native:
using (System.IO.Stream stream = e.GetNativeImageStream())
{
if (stream != null)
{
img = stream.ConvertToWpfBitmap(300, 0);
}
}
break;

case XferMech.File:
img = new BitmapImage(new Uri(e.FileDataPath));
if (img.CanFreeze)
{
img.Freeze();
}

break;
}
return img;
}

康佳扫描仪(官方COM)

使用厂家提供的开发COM组件,必须要安装两个东西

  • 扫描仪驱动
  • 开发包(附带COM组件)

添加COM引用

安装完驱动后

项目中添加啊COM引用TechHeroScanProj1

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
private TechHeroScanProj1.TechHeroScan axTechHeroScan1 = null;        

axTechHeroScan1 = new TechHeroScanProj1.TechHeroScan();
//设置参数
axTechHeroScan1.Pixel_Type = 2; // 扫描类型:0黑白 1灰度 2彩色
axTechHeroScan1.Resolution = 200; // 分辨率
axTechHeroScan1.Duplex = true; // 双面扫描:true双面 false单面
axTechHeroScan1.SetAutoDeskew(true);
axTechHeroScan1.PaperSize = -1; // 纸张大小:-1时为自动判断纸张大小 1 A4 2 B5 具体请看help.doc
axTechHeroScan1.Rotation = 0; //旋转角度: 0 90 180 270 为旋转角度 360为根据文字方向自动旋转
axTechHeroScan1.ShowSetupBeforeScan = false; //扫描前显示驱动界面
axTechHeroScan1.ShowIndicators = false; //显示扫描进度条
axTechHeroScan1.SetDetectDoubleFeed(2); //双张检测 0不检测 1长度检测 2超声波检测

选择扫描仪

1
2
3
4
private void Select_btn_Click(object sender, System.Windows.RoutedEventArgs e)
{
axTechHeroScan1.SelectScanner();
}

开始和错误回调

1
2
3
4
5
6
7
8
9
10
11
12
axTechHeroScan1.OnBeforeScan += AxTechHeroScan1_OnBeforeScan;
axTechHeroScan1.OnScanError += AxTechHeroScan1_OnScanError;

private void AxTechHeroScan1_OnBeforeScan()
{
Console.WriteLine("扫描前");
}

private void AxTechHeroScan1_OnScanError(int ErrorCode, int Additional)
{
Console.WriteLine("扫描失败", ErrorCode);
}

扫描

两种扫描方式使用一种即可,如果没有文件命名的需求建议直接使用内部重命名的方式。

内部重命名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
axTechHeroScan1.OnPageDone += AxTechHeroScan1_OnPageDone;
axTechHeroScan1.OnScanDone += AxTechHeroScan1_OnScanDone;

private void AxTechHeroScan1_OnPageDone(string strFileName)
{
Console.WriteLine(strFileName);
}

private void AxTechHeroScan1_OnScanDone()
{
Console.WriteLine("扫描结束");
}

// 按钮事件
private void Scanner_btn_Click(object sender, System.Windows.RoutedEventArgs e)
{
//开始扫描
axTechHeroScan1.ScanImage(0, "D:\\scanner_pic\\", "img_", "jpg");
}

手动重命名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
axTechHeroScan1.OnPageDoneDib += AxTechHeroScan1_OnPageDoneDib;
axTechHeroScan1.OnScanDone += AxTechHeroScan1_OnScanDone;

private void AxTechHeroScan1_OnPageDoneDib(int dib, int Resolution)
{
string timestr = DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss-ffff");
axTechHeroScan1.SaveJpgFile("D:\\scanner_pic\\" + timestr + ".jpg", dib, Resolution, 75);
axTechHeroScan1.FreeDib(dib);
}

private void AxTechHeroScan1_OnScanDone()
{
Console.WriteLine("扫描结束");
}

// 按钮事件
private void Scanner2_btn_Click(object sender, System.Windows.RoutedEventArgs e)
{
//开始扫描
axTechHeroScan1.ScanToDib(0);
}

WIA对接

在COM里导入

image-20220803100347269

一般最近年头的扫描仪都支持这个协议。

错误代码

https://docs.microsoft.com/zh-cn/windows/win32/wia/-wia-error-codes

弹窗设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ImageFile imageFile = null;
CommonDialog cdc = new WIA.CommonDialog();

try
{
imageFile = cdc.ShowAcquireImage(
WIA.WiaDeviceType.ScannerDeviceType,
WIA.WiaImageIntent.TextIntent,
WIA.WiaImageBias.MaximizeQuality,
"{00000000-0000-0000-0000-000000000000}",
true,
true,
false
);
string fileName = Path.GetTempFileName() + ".png";
Console.WriteLine(fileName);
File.Delete(fileName);
imageFile.SaveFile(fileName);//保存temp文件;
}
catch (System.Runtime.InteropServices.COMException)
{
imageFile = null;
}