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)