diff --git a/NTwain4.slnx b/NTwain4.slnx
index 489142d..8893119 100644
--- a/NTwain4.slnx
+++ b/NTwain4.slnx
@@ -18,7 +18,11 @@
-
+
+
+
+
+
diff --git a/samples/ConsoleSample/ConsoleSample.csproj b/samples/ConsoleSample/ConsoleSample.csproj
index 7202b70..c351816 100644
--- a/samples/ConsoleSample/ConsoleSample.csproj
+++ b/samples/ConsoleSample/ConsoleSample.csproj
@@ -13,7 +13,7 @@
-
+
diff --git a/samples/ConsoleSample/Program.cs b/samples/ConsoleSample/Program.cs
index ba5e6b1..b555ab5 100644
--- a/samples/ConsoleSample/Program.cs
+++ b/samples/ConsoleSample/Program.cs
@@ -141,6 +141,5 @@ internal class Program
{
Console.WriteLine($"BUMMER! No twain data #{++xferCount} on thread {Environment.CurrentManagedThreadId}.");
}
- e.Dispose();
}
}
diff --git a/samples/KodakTester/KodakTester.csproj b/samples/KodakTester/KodakTester.csproj
index 6dd355a..2fc709e 100644
--- a/samples/KodakTester/KodakTester.csproj
+++ b/samples/KodakTester/KodakTester.csproj
@@ -6,7 +6,7 @@
enable
true
enable
- x86
+ x86;x64
PerMonitorV2
true
diff --git a/samples/KodakTester/MainForm.cs b/samples/KodakTester/MainForm.cs
index 0df0413..45b85a3 100644
--- a/samples/KodakTester/MainForm.cs
+++ b/samples/KodakTester/MainForm.cs
@@ -98,7 +98,6 @@ public partial class MainForm : Form
var path = info.FileName.ToString();
LogIt($"Received {e.ImageInfo.PixelType} {info.Format} in {e.ImageInfo.Compression} compressed file {path}");
}
- e.Dispose();
LogIt("");
}
diff --git a/samples/WinForm32/Form1.cs b/samples/WinForm32/Form1.cs
index eeebfb9..3bbc683 100644
--- a/samples/WinForm32/Form1.cs
+++ b/samples/WinForm32/Form1.cs
@@ -1,11 +1,9 @@
using Microsoft.Win32;
using NTwain;
-using NTwain.Caps;
using NTwain.Data;
using NTwain.Events;
using System;
using System.Collections.Generic;
-using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
@@ -20,7 +18,6 @@ namespace WinFormSample;
public partial class Form1 : Form
{
- bool useDiyPump = true;
TwainAppSession twain;
readonly string saveFolder;
readonly Stopwatch watch = new();
@@ -134,20 +131,22 @@ public partial class Form1 : Form
if (_useThreadForImag)
{
// bad thread example but whatev. should use a dedicated thread of some sort for real
+ var data = e.TakeDataOwnership();
Task.Run(() =>
{
- HandleTransferredData(e);
+ HandleTransferredData(data);
+ data?.Dispose();
});
}
else
{
- HandleTransferredData(e);
+ HandleTransferredData(e.Data);
}
}
- private void HandleTransferredData(TransferredEventArgs e)
+ private void HandleTransferredData(BufferedData? data)
{
- if (e.Data != null)
+ if (data != null)
{
try
{
@@ -156,7 +155,7 @@ public partial class Form1 : Form
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 ||
img.PixelFormat == System.Drawing.Imaging.PixelFormat.Format8bppIndexed)
@@ -177,7 +176,7 @@ public partial class Form1 : Form
}
else
{
- using (var img = new ImageMagick.MagickImage(e.Data.AsSpan()))
+ using (var img = new ImageMagick.MagickImage(data.AsSpan()))
{
var format = ImageMagick.MagickFormat.Png;
if (img.ColorType == ImageMagick.ColorType.Palette)
@@ -201,7 +200,7 @@ public partial class Form1 : Form
catch { }
finally
{
- e.Dispose();
+ data.Dispose();
}
}
}
diff --git a/samples/WinForm32/WinForm32.csproj b/samples/WinForm32/WinForm32.csproj
index 8113a01..063c5c8 100644
--- a/samples/WinForm32/WinForm32.csproj
+++ b/samples/WinForm32/WinForm32.csproj
@@ -12,7 +12,7 @@
-
+
diff --git a/samples/WinForm64/WinForm64.csproj b/samples/WinForm64/WinForm64.csproj
index bfb8b80..1c3706e 100644
--- a/samples/WinForm64/WinForm64.csproj
+++ b/samples/WinForm64/WinForm64.csproj
@@ -16,7 +16,7 @@
-
+
diff --git a/samples/WinUI3/MainWindow.xaml.cs b/samples/WinUI3/MainWindow.xaml.cs
index d3473d6..52fc586 100644
--- a/samples/WinUI3/MainWindow.xaml.cs
+++ b/samples/WinUI3/MainWindow.xaml.cs
@@ -110,10 +110,6 @@ public sealed partial class MainWindow : Window
}
}
catch { }
- finally
- {
- e.Dispose();
- }
}
private void Session_SourceDisabled(TwainAppSession sender, TWIdentityWrapper e)
diff --git a/samples/WinUI3/WinUI3.csproj b/samples/WinUI3/WinUI3.csproj
index 13f5c51..27bcd77 100644
--- a/samples/WinUI3/WinUI3.csproj
+++ b/samples/WinUI3/WinUI3.csproj
@@ -18,9 +18,9 @@
-
+
-
+
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 1661bbc..96007ab 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -2,18 +2,18 @@
4.0.0.0
- beta.1
+ beta.4
4.0.0.0
$(VersionPrefix)
- https://github.com/soukoku/ntwain5
+ https://github.com/soukoku/ntwain
twain scan
MIT
Scanner-Icon.png
- https://github.com/soukoku/ntwain5
+ https://github.com/soukoku/ntwain
GIT
en-US
Eugene Wang 2012-2026
diff --git a/src/NTwain/Data/BufferedData.cs b/src/NTwain/Data/BufferedData.cs
index 12fd072..d10ac99 100644
--- a/src/NTwain/Data/BufferedData.cs
+++ b/src/NTwain/Data/BufferedData.cs
@@ -77,7 +77,7 @@ public class BufferedData : IDisposable
public void Dispose()
{
- if (_fromPool && _disposed)
+ if (_fromPool && !_disposed)
{
MemPool.Return(_buffer);
_disposed = true;
diff --git a/src/NTwain/Events/TransferredEventArgs.cs b/src/NTwain/Events/TransferredEventArgs.cs
index fe9f607..e8cf4b6 100644
--- a/src/NTwain/Events/TransferredEventArgs.cs
+++ b/src/NTwain/Events/TransferredEventArgs.cs
@@ -34,11 +34,14 @@ public class TransferredEventArgs : EventArgs, IDisposable
///
public bool IsImage { get; }
- private readonly BufferedData? _data;
+ private BufferedData? _data;
+ private bool _dataOwnershipTransferred;
+
///
- /// The complete file data if memory was involved in the transfer.
- /// IMPORTANT: Content of this array will not be valid once
- /// this event arg has been disposed.
+ /// Gets the transferred data.
+ /// IMPORTANT: This data is only valid during the event handler execution.
+ /// If you need to keep the data after the event handler returns, call first.
+ /// Otherwise, the data will be automatically disposed when the event completes.
///
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));
}
+ ///
+ /// 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.
+ ///
+ ///
+ /// The buffered data with transferred ownership, or null if no data exists or ownership was already transferred.
+ ///
+ ///
+ ///
+ /// 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
+ /// }
+ /// }
+ /// }
+ ///
+ ///
+ public BufferedData? TakeDataOwnership()
+ {
+ if (_dataOwnershipTransferred || _data == null)
+ return null;
+
+ _dataOwnershipTransferred = true;
+ var data = _data;
+ _data = null;
+ return data;
+ }
+
public void Dispose()
{
- _data?.Dispose();
+ // Only dispose if ownership wasn't transferred
+ if (!_dataOwnershipTransferred && _data != null)
+ {
+ _data.Dispose();
+ _data = null;
+ }
}
}
\ No newline at end of file
diff --git a/src/NTwain/NTwain.csproj b/src/NTwain/NTwain.csproj
index 683e2ba..79eeae6 100644
--- a/src/NTwain/NTwain.csproj
+++ b/src/NTwain/NTwain.csproj
@@ -37,7 +37,7 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/NTwain/Platform/Win32MessagePump.cs b/src/NTwain/Platform/Win32MessagePump.cs
index c6508fb..bbdca8a 100644
--- a/src/NTwain/Platform/Win32MessagePump.cs
+++ b/src/NTwain/Platform/Win32MessagePump.cs
@@ -20,18 +20,17 @@ namespace NTwain.Platform;
#if !NETFRAMEWORK
[SupportedOSPlatform("windows5.1.2600")]
#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 readonly FreeLibrarySafeHandle _hInstance;
+ static readonly FreeLibrarySafeHandle _hInstance = PInvoke.GetModuleHandle((string?)null);
+
private readonly uint _threadId;
private HWND _mainWindow;
- private bool _disposed;
// 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
private readonly Queue _workQueue = new();
@@ -45,11 +44,15 @@ internal sealed class Win32MessagePump : IDisposable
// SynchronizationContext
private Win32SynchronizationContext? _synchronizationContext;
+ private readonly string _windowClassName;
+
public Win32MessagePump(ILogger logger)
{
- _hInstance = PInvoke.GetModuleHandle((string?)null);
_threadId = PInvoke.GetCurrentThreadId();
_logger = logger;
+ _windowClassName = $"MsgPumpParkWindow_{Guid.NewGuid():N}";
+ _wndProc = WindowProc;
+
}
///
@@ -105,33 +108,42 @@ internal sealed class Win32MessagePump : IDisposable
UnregisterWindowClass();
return -1;
}
-
- // Create and install the SynchronizationContext
+
_synchronizationContext = new Win32SynchronizationContext(this);
SynchronizationContext.SetSynchronizationContext(_synchronizationContext);
- int exitCode = RunMessageLoop();
+ int exitCode;
+ try
+ {
+ exitCode = RunMessageLoop();
+ }
+ finally
+ {
+ if (!_mainWindow.IsNull)
+ {
+ PInvoke.DestroyWindow(_mainWindow);
+ _mainWindow = HWND.Null;
+ }
- // Clear the SynchronizationContext
- SynchronizationContext.SetSynchronizationContext(null);
+ UnregisterWindowClass();
- Dispose();
+ SynchronizationContext.SetSynchronizationContext(null);
+ _synchronizationContext = null;
+ }
return exitCode;
}
private bool RegisterWindowClass()
{
- s_wndProc = WindowProc;
-
unsafe
{
- fixed (char* className = WindowClassName)
+ fixed (char* className = _windowClassName)
{
var wc = new WNDCLASSEXW
{
cbSize = (uint)Marshal.SizeOf(),
style = 0,
- lpfnWndProc = s_wndProc,
+ lpfnWndProc = _wndProc,
cbClsExtra = 0,
cbWndExtra = 0,
hInstance = (HINSTANCE)_hInstance.DangerousGetHandle(),
@@ -156,7 +168,7 @@ internal sealed class Win32MessagePump : IDisposable
private void UnregisterWindowClass()
{
- PInvoke.UnregisterClass(WindowClassName, _hInstance);
+ PInvoke.UnregisterClass(_windowClassName, _hInstance);
}
private bool CreateMainWindow()
@@ -165,7 +177,7 @@ internal sealed class Win32MessagePump : IDisposable
{
_mainWindow = PInvoke.CreateWindowEx(
0,
- WindowClassName,
+ _windowClassName,
"MsgPump Window",
0,
0, 0, 0, 0,
@@ -203,8 +215,6 @@ internal sealed class Win32MessagePump : IDisposable
return -1;
}
- ProcessWorkQueue();
-
if (FilterMessage(ref msg))
{
continue;
@@ -257,6 +267,8 @@ internal sealed class Win32MessagePump : IDisposable
///
public void PostToUIThread(Action action)
{
+ if (_mainWindow.IsNull) throw new InvalidOperationException("Message pump main window is not available.");
+
if (InvokeRequired)
{
lock (_workQueueLock)
@@ -280,6 +292,8 @@ internal sealed class Win32MessagePump : IDisposable
///
public void Quit(int exitCode = 0)
{
+ if (_mainWindow.IsNull) throw new InvalidOperationException("Message pump main window is not available.");
+
if (InvokeRequired)
{
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)
{
PInvoke.PostQuitMessage(0);
@@ -340,22 +359,6 @@ internal sealed class Win32MessagePump : IDisposable
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();
- }
}
///
diff --git a/src/NTwain/TransferLoopThread.Logic.cs b/src/NTwain/TransferLoopThread.Logic.cs
index 75ef10b..57d0a88 100644
--- a/src/NTwain/TransferLoopThread.Logic.cs
+++ b/src/NTwain/TransferLoopThread.Logic.cs
@@ -224,10 +224,7 @@ partial class TransferLoopThread
var args = new TransferredEventArgs(info, data);
_twain.RaiseTransferred(args);
}
- catch
- {
- data.Dispose();
- }
+ catch { }
}
}
@@ -241,7 +238,7 @@ partial class TransferLoopThread
}
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);
}
}
@@ -314,10 +311,7 @@ partial class TransferLoopThread
var args = new TransferredEventArgs(_twain, info, null, data);
_twain.RaiseTransferred(args);
}
- catch
- {
- data.Dispose();
- }
+ catch { }
}
@@ -332,7 +326,7 @@ partial class TransferLoopThread
}
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);
}
}
@@ -382,7 +376,14 @@ partial class TransferLoopThread
}
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;
diff --git a/src/NTwain/TwainAppSession.TwainState.cs b/src/NTwain/TwainAppSession.TwainState.cs
index 4e48744..3c64ad3 100644
--- a/src/NTwain/TwainAppSession.TwainState.cs
+++ b/src/NTwain/TwainAppSession.TwainState.cs
@@ -66,7 +66,7 @@ partial class TwainAppSession
internal void RaiseTransferred(TransferredEventArgs args)
{
// 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
/// The type of event arguments.
/// The event handler to invoke.
/// The event arguments.
- protected void RaiseEvent(EventHandler? eventHandler, TEventArgs eventArgs)
+ ///
+ protected void RaiseEvent(EventHandler? eventHandler, TEventArgs eventArgs,
+ Action? completeCallback = null)
{
if (eventHandler == null) return;
if (AppThreadContext != null)
{
- AppThreadContext.Post(_ => eventHandler(this, eventArgs), null);
+ AppThreadContext.Post(_ =>
+ {
+ eventHandler(this, eventArgs);
+ completeCallback?.Invoke(eventArgs);
+ }, null);
}
else
{
eventHandler(this, eventArgs);
+ completeCallback?.Invoke(eventArgs);
}
}
diff --git a/src/NTwain/TwainAppSession.cs b/src/NTwain/TwainAppSession.cs
index 014c591..45662c6 100644
--- a/src/NTwain/TwainAppSession.cs
+++ b/src/NTwain/TwainAppSession.cs
@@ -65,7 +65,7 @@ public partial class TwainAppSession : IDisposable
DllPath.TryUseLocalDsm();
// no need to do another lock call when using marshal alloc
- TW_EVENT _procEvent = default;
+ _procEvent = default;
_procEvent.pEvent = Marshal.AllocHGlobal(Marshal.SizeOf());
Thread pumpThread = new(() =>
@@ -132,7 +132,6 @@ public partial class TwainAppSession : IDisposable
{
_twainPumpForWin.RemoveMessageFilter(this);
_twainPumpForWin.Quit();
- _twainPumpForWin.Dispose();
}
if (_procEvent.pEvent != IntPtr.Zero)