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" />
</Folder>
<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/WinForm64/WinForm64.csproj" />
<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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Magick.NET-Q8-AnyCPU" Version="14.10.1" />
<PackageReference Include="Magick.NET-Q8-AnyCPU" Version="14.10.2" />
</ItemGroup>
-1
View File
@@ -141,6 +141,5 @@ internal class Program
{
Console.WriteLine($"BUMMER! No twain data #{++xferCount} on thread {Environment.CurrentManagedThreadId}.");
}
e.Dispose();
}
}
+1 -1
View File
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<PlatformTarget>x86</PlatformTarget>
<Platforms>x86;x64</Platforms>
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
<ApplicationVisualStyles>true</ApplicationVisualStyles>
</PropertyGroup>
-1
View File
@@ -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("");
}
+9 -10
View File
@@ -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();
}
}
}
+1 -1
View File
@@ -12,7 +12,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Magick.NET-Q8-x86" Version="14.10.1" />
<PackageReference Include="Magick.NET-Q8-x86" Version="14.10.2" />
</ItemGroup>
<ItemGroup>
+1 -1
View File
@@ -16,7 +16,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Magick.NET-Q8-x64" Version="14.10.1" />
<PackageReference Include="Magick.NET-Q8-x64" Version="14.10.2" />
</ItemGroup>
<ItemGroup>
-4
View File
@@ -110,10 +110,6 @@ public sealed partial class MainWindow : Window
}
}
catch { }
finally
{
e.Dispose();
}
}
private void Session_SourceDisabled(TwainAppSession sender, TWIdentityWrapper e)
+2 -2
View File
@@ -18,9 +18,9 @@
</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.WindowsAppSDK" Version="1.8.251106002" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.260101001" />
</ItemGroup>
<ItemGroup>
+3 -3
View File
@@ -2,18 +2,18 @@
<PropertyGroup>
<!--change these in each release-->
<VersionPrefix>4.0.0.0</VersionPrefix>
<VersionSuffix>beta.1</VersionSuffix>
<VersionSuffix>beta.4</VersionSuffix>
<!--keep it the same until major # changes-->
<AssemblyVersion>4.0.0.0</AssemblyVersion>
<!--even more unlikely to change-->
<FileVersion>$(VersionPrefix)</FileVersion>
<PackageProjectUrl>https://github.com/soukoku/ntwain5</PackageProjectUrl>
<PackageProjectUrl>https://github.com/soukoku/ntwain</PackageProjectUrl>
<PackageTags>twain scan</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageIcon>Scanner-Icon.png</PackageIcon>
<RepositoryUrl>https://github.com/soukoku/ntwain5</RepositoryUrl>
<RepositoryUrl>https://github.com/soukoku/ntwain</RepositoryUrl>
<RepositoryType>GIT</RepositoryType>
<NeutralLanguage>en-US</NeutralLanguage>
<Copyright>Eugene Wang 2012-2026</Copyright>
+1 -1
View File
@@ -77,7 +77,7 @@ public class BufferedData : IDisposable
public void Dispose()
{
if (_fromPool && _disposed)
if (_fromPool && !_disposed)
{
MemPool.Return(_buffer);
_disposed = true;
+51 -5
View File
@@ -34,11 +34,14 @@ public class TransferredEventArgs : EventArgs, IDisposable
/// </summary>
public bool IsImage { get; }
private readonly BufferedData? _data;
private BufferedData? _data;
private bool _dataOwnershipTransferred;
/// <summary>
/// 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 <see cref="TakeDataOwnership"/> first.
/// Otherwise, the data will be automatically disposed when the event completes.
/// </summary>
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));
}
/// <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()
{
_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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.264">
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.269">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
+39 -36
View File
@@ -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<Action> _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;
}
/// <summary>
@@ -106,32 +109,41 @@ internal sealed class Win32MessagePump : IDisposable
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;
}
UnregisterWindowClass();
// Clear the SynchronizationContext
SynchronizationContext.SetSynchronizationContext(null);
Dispose();
_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<WNDCLASSEXW>(),
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
/// </summary>
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
/// </summary>
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();
}
}
/// <summary>
+12 -11
View File
@@ -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;
+10 -3
View File
@@ -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
/// <typeparam name="TEventArgs">The type of event arguments.</typeparam>
/// <param name="eventHandler">The event handler to invoke.</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 (AppThreadContext != null)
{
AppThreadContext.Post(_ => eventHandler(this, eventArgs), null);
AppThreadContext.Post(_ =>
{
eventHandler(this, eventArgs);
completeCallback?.Invoke(eventArgs);
}, null);
}
else
{
eventHandler(this, eventArgs);
completeCallback?.Invoke(eventArgs);
}
}
+1 -2
View File
@@ -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<WinMSG>());
Thread pumpThread = new(() =>
@@ -132,7 +132,6 @@ public partial class TwainAppSession : IDisposable
{
_twainPumpForWin.RemoveMessageFilter(this);
_twainPumpForWin.Quit();
_twainPumpForWin.Dispose();
}
if (_procEvent.pEvent != IntPtr.Zero)