This commit is contained in:
Eugene Wang
2026-02-21 05:56:04 -05:00
18 changed files with 139 additions and 86 deletions
+5 -1
View File
@@ -18,7 +18,11 @@
<File Path="twain-doc\twain2.5.h" /> <File Path="twain-doc\twain2.5.h" />
</Folder> </Folder>
<Folder Name="/Samples/"> <Folder Name="/Samples/">
<Project Path="samples/KodakTester/KodakTester.csproj" /> <Project Path="samples/KodakTester/KodakTester.csproj">
<Platform Solution="*|ARM64" Project="x64" />
<Platform Solution="*|x64" Project="x64" />
<Platform Solution="*|x86" Project="x86" />
</Project>
<Project Path="samples/WinForm32/WinForm32.csproj" /> <Project Path="samples/WinForm32/WinForm32.csproj" />
<Project Path="samples/WinForm64/WinForm64.csproj" /> <Project Path="samples/WinForm64/WinForm64.csproj" />
<Project Path="samples/WinUI3/WinUI3.csproj" Id="9876f110-22e9-4813-8f5d-b317c152885f"> <Project Path="samples/WinUI3/WinUI3.csproj" Id="9876f110-22e9-4813-8f5d-b317c152885f">
+1 -1
View File
@@ -13,7 +13,7 @@
<ProjectReference Include="..\..\src\NTwain\NTwain.csproj" /> <ProjectReference Include="..\..\src\NTwain\NTwain.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Magick.NET-Q8-AnyCPU" Version="14.10.1" /> <PackageReference Include="Magick.NET-Q8-AnyCPU" Version="14.10.2" />
</ItemGroup> </ItemGroup>
-1
View File
@@ -141,6 +141,5 @@ internal class Program
{ {
Console.WriteLine($"BUMMER! No twain data #{++xferCount} on thread {Environment.CurrentManagedThreadId}."); Console.WriteLine($"BUMMER! No twain data #{++xferCount} on thread {Environment.CurrentManagedThreadId}.");
} }
e.Dispose();
} }
} }
+1 -1
View File
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<PlatformTarget>x86</PlatformTarget> <Platforms>x86;x64</Platforms>
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode> <ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
<ApplicationVisualStyles>true</ApplicationVisualStyles> <ApplicationVisualStyles>true</ApplicationVisualStyles>
</PropertyGroup> </PropertyGroup>
-1
View File
@@ -98,7 +98,6 @@ public partial class MainForm : Form
var path = info.FileName.ToString(); var path = info.FileName.ToString();
LogIt($"Received {e.ImageInfo.PixelType} {info.Format} in {e.ImageInfo.Compression} compressed file {path}"); LogIt($"Received {e.ImageInfo.PixelType} {info.Format} in {e.ImageInfo.Compression} compressed file {path}");
} }
e.Dispose();
LogIt(""); LogIt("");
} }
+9 -10
View File
@@ -1,11 +1,9 @@
using Microsoft.Win32; using Microsoft.Win32;
using NTwain; using NTwain;
using NTwain.Caps;
using NTwain.Data; using NTwain.Data;
using NTwain.Events; using NTwain.Events;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing; using System.Drawing;
using System.Drawing.Imaging; using System.Drawing.Imaging;
@@ -20,7 +18,6 @@ namespace WinFormSample;
public partial class Form1 : Form public partial class Form1 : Form
{ {
bool useDiyPump = true;
TwainAppSession twain; TwainAppSession twain;
readonly string saveFolder; readonly string saveFolder;
readonly Stopwatch watch = new(); readonly Stopwatch watch = new();
@@ -134,20 +131,22 @@ public partial class Form1 : Form
if (_useThreadForImag) if (_useThreadForImag)
{ {
// bad thread example but whatev. should use a dedicated thread of some sort for real // bad thread example but whatev. should use a dedicated thread of some sort for real
var data = e.TakeDataOwnership();
Task.Run(() => Task.Run(() =>
{ {
HandleTransferredData(e); HandleTransferredData(data);
data?.Dispose();
}); });
} }
else else
{ {
HandleTransferredData(e); HandleTransferredData(e.Data);
} }
} }
private void HandleTransferredData(TransferredEventArgs e) private void HandleTransferredData(BufferedData? data)
{ {
if (e.Data != null) if (data != null)
{ {
try try
{ {
@@ -156,7 +155,7 @@ public partial class Form1 : Form
if (_useSystemDrawing) if (_useSystemDrawing)
{ {
using (var img = Image.FromStream(e.Data.AsStream())) using (var img = Image.FromStream(data.AsStream()))
{ {
if (img.PixelFormat == System.Drawing.Imaging.PixelFormat.Format1bppIndexed || if (img.PixelFormat == System.Drawing.Imaging.PixelFormat.Format1bppIndexed ||
img.PixelFormat == System.Drawing.Imaging.PixelFormat.Format8bppIndexed) img.PixelFormat == System.Drawing.Imaging.PixelFormat.Format8bppIndexed)
@@ -177,7 +176,7 @@ public partial class Form1 : Form
} }
else else
{ {
using (var img = new ImageMagick.MagickImage(e.Data.AsSpan())) using (var img = new ImageMagick.MagickImage(data.AsSpan()))
{ {
var format = ImageMagick.MagickFormat.Png; var format = ImageMagick.MagickFormat.Png;
if (img.ColorType == ImageMagick.ColorType.Palette) if (img.ColorType == ImageMagick.ColorType.Palette)
@@ -201,7 +200,7 @@ public partial class Form1 : Form
catch { } catch { }
finally finally
{ {
e.Dispose(); data.Dispose();
} }
} }
} }
+1 -1
View File
@@ -12,7 +12,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Magick.NET-Q8-x86" Version="14.10.1" /> <PackageReference Include="Magick.NET-Q8-x86" Version="14.10.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+1 -1
View File
@@ -16,7 +16,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Magick.NET-Q8-x64" Version="14.10.1" /> <PackageReference Include="Magick.NET-Q8-x64" Version="14.10.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
-4
View File
@@ -110,10 +110,6 @@ public sealed partial class MainWindow : Window
} }
} }
catch { } catch { }
finally
{
e.Dispose();
}
} }
private void Session_SourceDisabled(TwainAppSession sender, TWIdentityWrapper e) private void Session_SourceDisabled(TwainAppSession sender, TWIdentityWrapper e)
+2 -2
View File
@@ -18,9 +18,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Magick.NET-Q8-x86" Version="14.10.1" /> <PackageReference Include="Magick.NET-Q8-AnyCPU" Version="14.10.2" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" /> <PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7463" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" /> <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+3 -3
View File
@@ -2,18 +2,18 @@
<PropertyGroup> <PropertyGroup>
<!--change these in each release--> <!--change these in each release-->
<VersionPrefix>4.0.0.0</VersionPrefix> <VersionPrefix>4.0.0.0</VersionPrefix>
<VersionSuffix>beta.1</VersionSuffix> <VersionSuffix>beta.4</VersionSuffix>
<!--keep it the same until major # changes--> <!--keep it the same until major # changes-->
<AssemblyVersion>4.0.0.0</AssemblyVersion> <AssemblyVersion>4.0.0.0</AssemblyVersion>
<!--even more unlikely to change--> <!--even more unlikely to change-->
<FileVersion>$(VersionPrefix)</FileVersion> <FileVersion>$(VersionPrefix)</FileVersion>
<PackageProjectUrl>https://github.com/soukoku/ntwain5</PackageProjectUrl> <PackageProjectUrl>https://github.com/soukoku/ntwain</PackageProjectUrl>
<PackageTags>twain scan</PackageTags> <PackageTags>twain scan</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Scanner-Icon.png</PackageIcon> <PackageIcon>Scanner-Icon.png</PackageIcon>
<RepositoryUrl>https://github.com/soukoku/ntwain5</RepositoryUrl> <RepositoryUrl>https://github.com/soukoku/ntwain</RepositoryUrl>
<RepositoryType>GIT</RepositoryType> <RepositoryType>GIT</RepositoryType>
<NeutralLanguage>en-US</NeutralLanguage> <NeutralLanguage>en-US</NeutralLanguage>
<Copyright>Eugene Wang 2012-2026</Copyright> <Copyright>Eugene Wang 2012-2026</Copyright>
+1 -1
View File
@@ -77,7 +77,7 @@ public class BufferedData : IDisposable
public void Dispose() public void Dispose()
{ {
if (_fromPool && _disposed) if (_fromPool && !_disposed)
{ {
MemPool.Return(_buffer); MemPool.Return(_buffer);
_disposed = true; _disposed = true;
+51 -5
View File
@@ -34,11 +34,14 @@ public class TransferredEventArgs : EventArgs, IDisposable
/// </summary> /// </summary>
public bool IsImage { get; } public bool IsImage { get; }
private readonly BufferedData? _data; private BufferedData? _data;
private bool _dataOwnershipTransferred;
/// <summary> /// <summary>
/// The complete file data if memory was involved in the transfer. /// Gets the transferred data.
/// IMPORTANT: Content of this array will not be valid once /// IMPORTANT: This data is only valid during the event handler execution.
/// this event arg has been disposed. /// If you need to keep the data after the event handler returns, call <see cref="TakeDataOwnership"/> first.
/// Otherwise, the data will be automatically disposed when the event completes.
/// </summary> /// </summary>
public BufferedData? Data => _data; public BufferedData? Data => _data;
@@ -73,8 +76,51 @@ public class TransferredEventArgs : EventArgs, IDisposable
return _twain.WrapInSTS(DGImage.ExtImageInfo.Get(_twain.AppIdentity, _twain.CurrentSource, ref container)); return _twain.WrapInSTS(DGImage.ExtImageInfo.Get(_twain.AppIdentity, _twain.CurrentSource, ref container));
} }
/// <summary>
/// Transfers ownership of the in-memory data to the caller, preventing automatic disposal.
/// Use this when you need to process the data asynchronously or keep it beyond the event handler scope.
/// After calling this, you MUST manually dispose the returned BufferedData when finished.
/// </summary>
/// <returns>
/// The buffered data with transferred ownership, or null if no data exists or ownership was already transferred.
/// </returns>
/// <example>
/// <code>
/// private async void OnTransferred(TwainAppSession sender, TransferredEventArgs e)
/// {
/// var data = e.TakeDataOwnership(); // Take ownership
/// if (data != null)
/// {
/// try
/// {
/// await ProcessDataAsync(data); // Can use beyond event handler
/// }
/// finally
/// {
/// data.Dispose(); // Must dispose when done
/// }
/// }
/// }
/// </code>
/// </example>
public BufferedData? TakeDataOwnership()
{
if (_dataOwnershipTransferred || _data == null)
return null;
_dataOwnershipTransferred = true;
var data = _data;
_data = null;
return data;
}
public void Dispose() public void Dispose()
{ {
_data?.Dispose(); // Only dispose if ownership wasn't transferred
if (!_dataOwnershipTransferred && _data != null)
{
_data.Dispose();
_data = null;
}
} }
} }
+1 -1
View File
@@ -37,7 +37,7 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.264"> <PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.269">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
+40 -37
View File
@@ -20,18 +20,17 @@ namespace NTwain.Platform;
#if !NETFRAMEWORK #if !NETFRAMEWORK
[SupportedOSPlatform("windows5.1.2600")] [SupportedOSPlatform("windows5.1.2600")]
#endif #endif
internal sealed class Win32MessagePump : IDisposable internal sealed class Win32MessagePump
{ {
private const string WindowClassName = "MsgPumpParkWindow";
private const uint WM_APP_INVOKE = PInvoke.WM_APP + 1; private const uint WM_APP_INVOKE = PInvoke.WM_APP + 1;
private readonly FreeLibrarySafeHandle _hInstance; static readonly FreeLibrarySafeHandle _hInstance = PInvoke.GetModuleHandle((string?)null);
private readonly uint _threadId; private readonly uint _threadId;
private HWND _mainWindow; private HWND _mainWindow;
private bool _disposed;
// Store the delegate to prevent garbage collection // Store the delegate to prevent garbage collection
private static WNDPROC? s_wndProc; private WNDPROC _wndProc; // Instance field, not static
// Queue for work items posted to the UI thread // Queue for work items posted to the UI thread
private readonly Queue<Action> _workQueue = new(); private readonly Queue<Action> _workQueue = new();
@@ -45,11 +44,15 @@ internal sealed class Win32MessagePump : IDisposable
// SynchronizationContext // SynchronizationContext
private Win32SynchronizationContext? _synchronizationContext; private Win32SynchronizationContext? _synchronizationContext;
private readonly string _windowClassName;
public Win32MessagePump(ILogger logger) public Win32MessagePump(ILogger logger)
{ {
_hInstance = PInvoke.GetModuleHandle((string?)null);
_threadId = PInvoke.GetCurrentThreadId(); _threadId = PInvoke.GetCurrentThreadId();
_logger = logger; _logger = logger;
_windowClassName = $"MsgPumpParkWindow_{Guid.NewGuid():N}";
_wndProc = WindowProc;
} }
/// <summary> /// <summary>
@@ -105,33 +108,42 @@ internal sealed class Win32MessagePump : IDisposable
UnregisterWindowClass(); UnregisterWindowClass();
return -1; return -1;
} }
// Create and install the SynchronizationContext
_synchronizationContext = new Win32SynchronizationContext(this); _synchronizationContext = new Win32SynchronizationContext(this);
SynchronizationContext.SetSynchronizationContext(_synchronizationContext); SynchronizationContext.SetSynchronizationContext(_synchronizationContext);
int exitCode = RunMessageLoop(); int exitCode;
try
{
exitCode = RunMessageLoop();
}
finally
{
if (!_mainWindow.IsNull)
{
PInvoke.DestroyWindow(_mainWindow);
_mainWindow = HWND.Null;
}
// Clear the SynchronizationContext UnregisterWindowClass();
SynchronizationContext.SetSynchronizationContext(null);
Dispose(); SynchronizationContext.SetSynchronizationContext(null);
_synchronizationContext = null;
}
return exitCode; return exitCode;
} }
private bool RegisterWindowClass() private bool RegisterWindowClass()
{ {
s_wndProc = WindowProc;
unsafe unsafe
{ {
fixed (char* className = WindowClassName) fixed (char* className = _windowClassName)
{ {
var wc = new WNDCLASSEXW var wc = new WNDCLASSEXW
{ {
cbSize = (uint)Marshal.SizeOf<WNDCLASSEXW>(), cbSize = (uint)Marshal.SizeOf<WNDCLASSEXW>(),
style = 0, style = 0,
lpfnWndProc = s_wndProc, lpfnWndProc = _wndProc,
cbClsExtra = 0, cbClsExtra = 0,
cbWndExtra = 0, cbWndExtra = 0,
hInstance = (HINSTANCE)_hInstance.DangerousGetHandle(), hInstance = (HINSTANCE)_hInstance.DangerousGetHandle(),
@@ -156,7 +168,7 @@ internal sealed class Win32MessagePump : IDisposable
private void UnregisterWindowClass() private void UnregisterWindowClass()
{ {
PInvoke.UnregisterClass(WindowClassName, _hInstance); PInvoke.UnregisterClass(_windowClassName, _hInstance);
} }
private bool CreateMainWindow() private bool CreateMainWindow()
@@ -165,7 +177,7 @@ internal sealed class Win32MessagePump : IDisposable
{ {
_mainWindow = PInvoke.CreateWindowEx( _mainWindow = PInvoke.CreateWindowEx(
0, 0,
WindowClassName, _windowClassName,
"MsgPump Window", "MsgPump Window",
0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
@@ -203,8 +215,6 @@ internal sealed class Win32MessagePump : IDisposable
return -1; return -1;
} }
ProcessWorkQueue();
if (FilterMessage(ref msg)) if (FilterMessage(ref msg))
{ {
continue; continue;
@@ -257,6 +267,8 @@ internal sealed class Win32MessagePump : IDisposable
/// </summary> /// </summary>
public void PostToUIThread(Action action) public void PostToUIThread(Action action)
{ {
if (_mainWindow.IsNull) throw new InvalidOperationException("Message pump main window is not available.");
if (InvokeRequired) if (InvokeRequired)
{ {
lock (_workQueueLock) lock (_workQueueLock)
@@ -280,6 +292,8 @@ internal sealed class Win32MessagePump : IDisposable
/// </summary> /// </summary>
public void Quit(int exitCode = 0) public void Quit(int exitCode = 0)
{ {
if (_mainWindow.IsNull) throw new InvalidOperationException("Message pump main window is not available.");
if (InvokeRequired) if (InvokeRequired)
{ {
PostToUIThread(() => PostToUIThread(() =>
@@ -330,8 +344,13 @@ internal sealed class Win32MessagePump : IDisposable
} }
} }
private static LRESULT WindowProc(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam) private LRESULT WindowProc(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam)
{ {
if (msg == WM_APP_INVOKE)
{
ProcessWorkQueue();
return new LRESULT(0);
}
if (msg == PInvoke.WM_DESTROY) if (msg == PInvoke.WM_DESTROY)
{ {
PInvoke.PostQuitMessage(0); PInvoke.PostQuitMessage(0);
@@ -340,22 +359,6 @@ internal sealed class Win32MessagePump : IDisposable
return PInvoke.DefWindowProc(hwnd, msg, wParam, lParam); return PInvoke.DefWindowProc(hwnd, msg, wParam, lParam);
} }
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
if (!_mainWindow.IsNull)
{
PInvoke.DestroyWindow(_mainWindow);
_mainWindow = HWND.Null;
}
UnregisterWindowClass();
}
} }
/// <summary> /// <summary>
+12 -11
View File
@@ -224,10 +224,7 @@ partial class TransferLoopThread
var args = new TransferredEventArgs(info, data); var args = new TransferredEventArgs(info, data);
_twain.RaiseTransferred(args); _twain.RaiseTransferred(args);
} }
catch catch { }
{
data.Dispose();
}
} }
} }
@@ -241,7 +238,7 @@ partial class TransferLoopThread
} }
finally finally
{ {
if (lockedPtr != IntPtr.Zero) _twain.MemoryManager.Unlock(lockedPtr); if (lockedPtr != IntPtr.Zero) _twain.MemoryManager.Unlock(dataPtr);
if (dataPtr != IntPtr.Zero) _twain.MemoryManager.Free(dataPtr); if (dataPtr != IntPtr.Zero) _twain.MemoryManager.Free(dataPtr);
} }
} }
@@ -314,10 +311,7 @@ partial class TransferLoopThread
var args = new TransferredEventArgs(_twain, info, null, data); var args = new TransferredEventArgs(_twain, info, null, data);
_twain.RaiseTransferred(args); _twain.RaiseTransferred(args);
} }
catch catch { }
{
data.Dispose();
}
} }
@@ -332,7 +326,7 @@ partial class TransferLoopThread
} }
finally finally
{ {
if (lockedPtr != IntPtr.Zero) _twain.MemoryManager.Unlock(lockedPtr); if (lockedPtr != IntPtr.Zero) _twain.MemoryManager.Unlock(dataPtr);
if (dataPtr != IntPtr.Zero) _twain.MemoryManager.Free(dataPtr); if (dataPtr != IntPtr.Zero) _twain.MemoryManager.Free(dataPtr);
} }
} }
@@ -382,7 +376,14 @@ partial class TransferLoopThread
} }
else else
{ {
Debugger.Break(); if (Debugger.IsAttached) Debugger.Break();
_twain.Logger.LogWarning(
"TransferFileImage failed unexpectedly: RC={RC}, CC={CC}",
sts.RC, sts.ConditionCode);
// Or raise error event for user to handle
_twain.RaiseTransferError(new TransferErrorEventArgs(sts, "TransferFileImage"));
} }
} }
return sts; return sts;
+10 -3
View File
@@ -66,7 +66,7 @@ partial class TwainAppSession
internal void RaiseTransferred(TransferredEventArgs args) internal void RaiseTransferred(TransferredEventArgs args)
{ {
// no wait here, app can handle at its leisure // no wait here, app can handle at its leisure
RaiseEvent(Transferred, args); RaiseEvent(Transferred, args, static e => e.Dispose());
} }
@@ -139,17 +139,24 @@ partial class TwainAppSession
/// <typeparam name="TEventArgs">The type of event arguments.</typeparam> /// <typeparam name="TEventArgs">The type of event arguments.</typeparam>
/// <param name="eventHandler">The event handler to invoke.</param> /// <param name="eventHandler">The event handler to invoke.</param>
/// <param name="eventArgs">The event arguments.</param> /// <param name="eventArgs">The event arguments.</param>
protected void RaiseEvent<TEventArgs>(EventHandler<TwainAppSession, TEventArgs>? eventHandler, TEventArgs eventArgs) /// <param name="completeCallback"></param>
protected void RaiseEvent<TEventArgs>(EventHandler<TwainAppSession, TEventArgs>? eventHandler, TEventArgs eventArgs,
Action<TEventArgs>? completeCallback = null)
{ {
if (eventHandler == null) return; if (eventHandler == null) return;
if (AppThreadContext != null) if (AppThreadContext != null)
{ {
AppThreadContext.Post(_ => eventHandler(this, eventArgs), null); AppThreadContext.Post(_ =>
{
eventHandler(this, eventArgs);
completeCallback?.Invoke(eventArgs);
}, null);
} }
else else
{ {
eventHandler(this, eventArgs); eventHandler(this, eventArgs);
completeCallback?.Invoke(eventArgs);
} }
} }
+1 -2
View File
@@ -65,7 +65,7 @@ public partial class TwainAppSession : IDisposable
DllPath.TryUseLocalDsm(); DllPath.TryUseLocalDsm();
// no need to do another lock call when using marshal alloc // no need to do another lock call when using marshal alloc
TW_EVENT _procEvent = default; _procEvent = default;
_procEvent.pEvent = Marshal.AllocHGlobal(Marshal.SizeOf<WinMSG>()); _procEvent.pEvent = Marshal.AllocHGlobal(Marshal.SizeOf<WinMSG>());
Thread pumpThread = new(() => Thread pumpThread = new(() =>
@@ -132,7 +132,6 @@ public partial class TwainAppSession : IDisposable
{ {
_twainPumpForWin.RemoveMessageFilter(this); _twainPumpForWin.RemoveMessageFilter(this);
_twainPumpForWin.Quit(); _twainPumpForWin.Quit();
_twainPumpForWin.Dispose();
} }
if (_procEvent.pEvent != IntPtr.Zero) if (_procEvent.pEvent != IntPtr.Zero)