Added possibility to handle transferred data by app in another thread for high-speed scanner use.

This commit is contained in:
Eugene Wang
2023-04-10 20:42:41 -04:00
parent b89c186987
commit fa88dd4d1f
7 changed files with 154 additions and 79 deletions

View File

@@ -41,6 +41,7 @@
lblState = new System.Windows.Forms.Label(); lblState = new System.Windows.Forms.Label();
label3 = new System.Windows.Forms.Label(); label3 = new System.Windows.Forms.Label();
btnOpenDef = new System.Windows.Forms.Button(); btnOpenDef = new System.Windows.Forms.Button();
ckShowUI = new System.Windows.Forms.CheckBox();
capListView = new System.Windows.Forms.ListView(); capListView = new System.Windows.Forms.ListView();
colCap = new System.Windows.Forms.ColumnHeader(); colCap = new System.Windows.Forms.ColumnHeader();
colType = new System.Windows.Forms.ColumnHeader(); colType = new System.Windows.Forms.ColumnHeader();
@@ -52,7 +53,7 @@
btnStart = new System.Windows.Forms.Button(); btnStart = new System.Windows.Forms.Button();
btnShowSettings = new System.Windows.Forms.Button(); btnShowSettings = new System.Windows.Forms.Button();
btnClose = new System.Windows.Forms.Button(); btnClose = new System.Windows.Forms.Button();
ckShowUI = new System.Windows.Forms.CheckBox(); ckBgImageHandling = new System.Windows.Forms.CheckBox();
((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit(); ((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit();
splitContainer1.Panel1.SuspendLayout(); splitContainer1.Panel1.SuspendLayout();
splitContainer1.Panel2.SuspendLayout(); splitContainer1.Panel2.SuspendLayout();
@@ -167,6 +168,7 @@
// //
// splitContainer1.Panel2 // splitContainer1.Panel2
// //
splitContainer1.Panel2.Controls.Add(ckBgImageHandling);
splitContainer1.Panel2.Controls.Add(ckShowUI); splitContainer1.Panel2.Controls.Add(ckShowUI);
splitContainer1.Panel2.Controls.Add(capListView); splitContainer1.Panel2.Controls.Add(capListView);
splitContainer1.Panel2.Controls.Add(label4); splitContainer1.Panel2.Controls.Add(label4);
@@ -207,6 +209,18 @@
btnOpenDef.UseVisualStyleBackColor = true; btnOpenDef.UseVisualStyleBackColor = true;
btnOpenDef.Click += btnOpenDef_Click; btnOpenDef.Click += btnOpenDef_Click;
// //
// ckShowUI
//
ckShowUI.AutoSize = true;
ckShowUI.Checked = true;
ckShowUI.CheckState = System.Windows.Forms.CheckState.Checked;
ckShowUI.Location = new System.Drawing.Point(437, 35);
ckShowUI.Name = "ckShowUI";
ckShowUI.Size = new System.Drawing.Size(69, 19);
ckShowUI.TabIndex = 9;
ckShowUI.Text = "Show UI";
ckShowUI.UseVisualStyleBackColor = true;
//
// capListView // capListView
// //
capListView.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; capListView.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right;
@@ -289,17 +303,17 @@
btnClose.UseVisualStyleBackColor = true; btnClose.UseVisualStyleBackColor = true;
btnClose.Click += btnClose_Click; btnClose.Click += btnClose_Click;
// //
// ckShowUI // ckBgImageHandling
// //
ckShowUI.AutoSize = true; ckBgImageHandling.AutoSize = true;
ckShowUI.Checked = true; ckBgImageHandling.Checked = true;
ckShowUI.CheckState = System.Windows.Forms.CheckState.Checked; ckBgImageHandling.CheckState = System.Windows.Forms.CheckState.Checked;
ckShowUI.Location = new System.Drawing.Point(437, 35); ckBgImageHandling.Location = new System.Drawing.Point(512, 35);
ckShowUI.Name = "ckShowUI"; ckBgImageHandling.Name = "ckBgImageHandling";
ckShowUI.Size = new System.Drawing.Size(69, 19); ckBgImageHandling.Size = new System.Drawing.Size(220, 19);
ckShowUI.TabIndex = 9; ckBgImageHandling.TabIndex = 10;
ckShowUI.Text = "Show UI"; ckBgImageHandling.Text = "Handle image data in another thread";
ckShowUI.UseVisualStyleBackColor = true; ckBgImageHandling.UseVisualStyleBackColor = true;
// //
// Form1 // Form1
// //
@@ -345,5 +359,6 @@
private System.Windows.Forms.ColumnHeader colSupport; private System.Windows.Forms.ColumnHeader colSupport;
private System.Windows.Forms.ColumnHeader colExtended; private System.Windows.Forms.ColumnHeader colExtended;
private System.Windows.Forms.CheckBox ckShowUI; private System.Windows.Forms.CheckBox ckShowUI;
private System.Windows.Forms.CheckBox ckBgImageHandling;
} }
} }

View File

@@ -6,12 +6,11 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing; using System.Drawing;
using System.Drawing.Imaging;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.Versioning;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
namespace WinFormSample namespace WinFormSample
@@ -20,6 +19,8 @@ namespace WinFormSample
{ {
private TwainAppSession twain; private TwainAppSession twain;
private readonly string saveFolder; private readonly string saveFolder;
readonly Stopwatch watch = new();
private bool _useThreadForImag;
public Form1() public Form1()
{ {
@@ -33,8 +34,9 @@ namespace WinFormSample
twain.StateChanged += Twain_StateChanged; twain.StateChanged += Twain_StateChanged;
twain.DefaultSourceChanged += Twain_DefaultSourceChanged; twain.DefaultSourceChanged += Twain_DefaultSourceChanged;
twain.CurrentSourceChanged += Twain_CurrentSourceChanged; twain.CurrentSourceChanged += Twain_CurrentSourceChanged;
twain.SourceDisabled += Twain_SourceDisabled;
twain.TransferReady += Twain_TransferReady; twain.TransferReady += Twain_TransferReady;
twain.Transferred += Twain_DataTransferred; twain.Transferred += Twain_Transferred;
twain.TransferError += Twain_TransferError; twain.TransferError += Twain_TransferError;
twain.DeviceEvent += Twain_DeviceEvent; twain.DeviceEvent += Twain_DeviceEvent;
@@ -47,6 +49,15 @@ namespace WinFormSample
this.Disposed += Form1_Disposed; this.Disposed += Form1_Disposed;
} }
private void Twain_SourceDisabled(TwainAppSession sender, TW_IDENTITY_LEGACY e)
{
if (watch.IsRunning)
{
watch.Stop();
MessageBox.Show($"Took {watch.Elapsed} to finish that transfer.");
}
}
private void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e) private void SystemEvents_SessionSwitch(object sender, SessionSwitchEventArgs e)
{ {
switch (e.Reason) switch (e.Reason)
@@ -103,28 +114,54 @@ namespace WinFormSample
} }
private void Twain_DataTransferred(TwainAppSession sender, TransferredEventArgs e) private void Twain_Transferred(TwainAppSession sender, TransferredEventArgs e)
{ {
Debug.WriteLine($"[thread {Environment.CurrentManagedThreadId}] data transferred with info {e.ImageInfo}"); Debug.WriteLine($"[thread {Environment.CurrentManagedThreadId}] data transferred with info {e.ImageInfo}");
if (e.Data == null) return; // if using a high-speed scanner, imaging handling could be a bottleneck
// so it's possible to pass the data to another thread while the scanning
// loop happens. Just remember to dispose it after.
// example of using some lib to handle image data if (_useThreadForImag)
var saveFile = Path.Combine(saveFolder, (DateTime.Now.Ticks / 1000).ToString());
using (var img = new ImageMagick.MagickImage(e.Data))
{ {
if (img.ColorType == ImageMagick.ColorType.Palette) // bad thread example but whatev. should use a dedicated thread of some sort for real
Task.Run(() =>
{ {
// bw or gray HandleTransferredData(e);
saveFile += ".png"; });
} }
else else
{
HandleTransferredData(e);
}
}
private void HandleTransferredData(TransferredEventArgs e)
{
try
{
// example of using some lib to handle image data
var saveFile = Path.Combine(saveFolder, (DateTime.Now.Ticks / 1000).ToString());
using (var img = new ImageMagick.MagickImage(e.Data))
{ {
// color if (img.ColorType == ImageMagick.ColorType.Palette)
saveFile += ".jpg"; {
img.Quality = 75; // bw or gray
saveFile += ".png";
}
else
{
// color
saveFile += ".jpg";
img.Quality = 75;
}
img.Write(saveFile);
Debug.WriteLine($"Saved image to {saveFile}");
} }
img.Write(saveFile); }
Debug.WriteLine($"Saved image to {saveFile}"); catch { }
finally
{
e.Dispose();
} }
} }
@@ -362,7 +399,11 @@ namespace WinFormSample
private void btnStart_Click(object sender, EventArgs e) private void btnStart_Click(object sender, EventArgs e)
{ {
twain.EnableSource(ckShowUI.Checked, false); if (twain.EnableSource(ckShowUI.Checked, false).IsSuccess)
{
_useThreadForImag = ckBgImageHandling.Checked;
watch.Restart();
}
} }
} }
} }

View File

@@ -1,22 +1,45 @@
using System; using System;
using System.Buffers;
namespace NTwain.Data namespace NTwain.Data
{ {
/// <summary> /// <summary>
/// Simple struct with bytes buffer and the valid data length. /// Simple thing with shared bytes buffer and the valid data length.
/// </summary> /// </summary>
public struct BufferedData public class BufferedData : IDisposable
{ {
// experiment using array pool for things transferred in memory.
// this can pool up to a "normal" max of legal size paper in 24 bit at 300 dpi (~31MB)
// so the array max is made with 32 MB. Typical usage should be a lot less.
internal static readonly ArrayPool<byte> MemPool = ArrayPool<byte>.Create(32 * 1024 * 1024, 8);
public BufferedData(int size)
{
_buffer = MemPool.Rent(size);
_length = size;
_fromPool = true;
}
internal BufferedData(byte[] data, int size, bool fromPool)
{
_buffer = data;
_length = size;
_fromPool = fromPool;
}
bool _fromPool;
/// <summary> /// <summary>
/// Bytes buffer. This may be bigger than the data size /// Bytes buffer. This may be bigger than the data size
/// and contain invalid data. /// and contain invalid data.
/// </summary> /// </summary>
public byte[]? Buffer; byte[]? _buffer;
public byte[]? Buffer { get; }
/// <summary> /// <summary>
/// Actual usable data length in the buffer. /// Actual usable data length in the buffer.
/// </summary> /// </summary>
public int Length; int _length;
/// <summary> /// <summary>
/// As a span of usable data. /// As a span of usable data.
@@ -24,8 +47,17 @@ namespace NTwain.Data
/// <returns></returns> /// <returns></returns>
public ReadOnlySpan<byte> AsSpan() public ReadOnlySpan<byte> AsSpan()
{ {
if (Buffer != null) return Buffer.AsSpan(0, Length); if (_buffer != null) return _buffer.AsSpan(0, _length);
return Span<byte>.Empty; return Span<byte>.Empty;
} }
public void Dispose()
{
if (_fromPool && _buffer != null)
{
MemPool.Return(_buffer);
_buffer = null;
}
}
} }
} }

View File

@@ -24,7 +24,7 @@ namespace NTwain.Native
return test == 0x4949; return test == 0x4949;
} }
public static unsafe BufferedData GetBitmapData(System.Buffers.ArrayPool<byte> xferMemPool, IntPtr data) public static unsafe BufferedData? GetBitmapData(IntPtr data)
{ {
var infoHeader = Marshal.PtrToStructure<BITMAPINFOHEADER>(data); var infoHeader = Marshal.PtrToStructure<BITMAPINFOHEADER>(data);
if (infoHeader.Validate()) if (infoHeader.Validate())
@@ -40,7 +40,7 @@ namespace NTwain.Native
}; };
fileHeader.bfSize = fileHeader.bfOffBits + infoHeader.biSizeImage; fileHeader.bfSize = fileHeader.bfOffBits + infoHeader.biSizeImage;
var dataCopy = xferMemPool.Rent((int)fileHeader.bfSize); // new byte[fileHeader.bfSize]; var dataCopy = BufferedData.MemPool.Rent((int)fileHeader.bfSize); // new byte[fileHeader.bfSize];
// TODO: run benchmark on which one is faster // TODO: run benchmark on which one is faster
@@ -58,12 +58,12 @@ namespace NTwain.Native
// write image // write image
Marshal.Copy(data, dataCopy, fileHeaderSize, (int)fileHeader.bfSize - fileHeaderSize); Marshal.Copy(data, dataCopy, fileHeaderSize, (int)fileHeader.bfSize - fileHeaderSize);
return new BufferedData { Buffer = dataCopy, Length = (int)fileHeader.bfSize }; return new BufferedData(dataCopy, (int)fileHeader.bfSize, true);
} }
return default; return default;
} }
public static BufferedData GetTiffData(System.Buffers.ArrayPool<byte> xferMemPool, IntPtr data) public static BufferedData? GetTiffData(IntPtr data)
{ {
// Find the size of the image so we can turn it into a memory stream... // Find the size of the image so we can turn it into a memory stream...
var headerSize = Marshal.SizeOf(typeof(TIFFHEADER)); var headerSize = Marshal.SizeOf(typeof(TIFFHEADER));
@@ -86,10 +86,9 @@ namespace NTwain.Native
if (tiffSize > 0) if (tiffSize > 0)
{ {
var dataCopy = xferMemPool.Rent(tiffSize);// new byte[tiffSize]; var dataCopy = BufferedData.MemPool.Rent(tiffSize);// new byte[tiffSize];
// is this optimal?
Marshal.Copy(data, dataCopy, 0, tiffSize); Marshal.Copy(data, dataCopy, 0, tiffSize);
return new BufferedData { Buffer = dataCopy, Length = tiffSize }; return new BufferedData(dataCopy, tiffSize, true);
} }
return default; return default;
} }

View File

@@ -15,13 +15,6 @@ namespace NTwain
EndOfJobFlag = endOfJobFlag; EndOfJobFlag = endOfJobFlag;
} }
/// <summary>
/// Gets or sets whether the current transfer should be skipped
/// and continue next transfer if there are more data.
/// </summary>
public bool SkipCurrent { get; set; }
/// <summary> /// <summary>
/// Gets or sets whether to cancel the capture phase. /// Gets or sets whether to cancel the capture phase.
/// </summary> /// </summary>

View File

@@ -3,10 +3,8 @@ using System;
namespace NTwain namespace NTwain
{ {
// TODO: maybe a 2-level "dispose" with end of event being 1
// and manual dispose 2 for perf if this is not good enough.
public class TransferredEventArgs : EventArgs public class TransferredEventArgs : EventArgs, IDisposable
{ {
public TransferredEventArgs(TW_AUDIOINFO info, TW_SETUPFILEXFER fileInfo) public TransferredEventArgs(TW_AUDIOINFO info, TW_SETUPFILEXFER fileInfo)
{ {
@@ -19,7 +17,7 @@ namespace NTwain
_data = data; _data = data;
} }
public TransferredEventArgs(TwainAppSession twain, TW_IMAGEINFO info, TW_SETUPFILEXFER? fileInfo, BufferedData data) public TransferredEventArgs(TwainAppSession twain, TW_IMAGEINFO info, TW_SETUPFILEXFER? fileInfo, BufferedData? data)
{ {
ImageInfo = info; ImageInfo = info;
FileInfo = fileInfo; FileInfo = fileInfo;
@@ -35,13 +33,13 @@ namespace NTwain
/// </summary> /// </summary>
public bool IsImage { get; } public bool IsImage { get; }
private readonly BufferedData _data; private readonly BufferedData? _data;
/// <summary> /// <summary>
/// The complete file data if memory was involved in the transfer. /// The complete file data if memory was involved in the transfer.
/// IMPORTANT: Content of this array may not valid once /// IMPORTANT: Content of this array will not be valid once
/// the event handler ends. /// this event arg has been disposed.
/// </summary> /// </summary>
public ReadOnlySpan<byte> Data => _data.AsSpan(); public ReadOnlySpan<byte> Data => _data == null ? ReadOnlySpan<byte>.Empty : _data.AsSpan();
/// <summary> /// <summary>
/// The file info if the transfer involved file information. /// The file info if the transfer involved file information.
@@ -73,5 +71,9 @@ namespace NTwain
return _twain.GetExtendedImageInfo(ref container); return _twain.GetExtendedImageInfo(ref container);
} }
public void Dispose()
{
_data?.Dispose();
}
} }
} }

View File

@@ -12,11 +12,6 @@ namespace NTwain
partial class TwainAppSession partial class TwainAppSession
{ {
// experiment using array pool for things transferred in memory.
// this can pool up to a "normal" max of legal size paper in 24 bit at 300 dpi (~31MB)
// so the array max is made with 32 MB. Typical usage should be a lot less.
static readonly ArrayPool<byte> XferMemPool = ArrayPool<byte>.Create(32 * 1024 * 1024, 8);
/// <summary> /// <summary>
/// Can only be called in state 7, so it's hidden here and /// Can only be called in state 7, so it's hidden here and
/// only exposed in data transferred event. /// only exposed in data transferred event.
@@ -265,11 +260,11 @@ namespace NTwain
{ {
State = STATE.S7; State = STATE.S7;
lockedPtr = Lock(dataPtr); lockedPtr = Lock(dataPtr);
BufferedData data = default; BufferedData? data = default;
// TODO: don't know how to read wav/aiff from pointer yet // TODO: don't know how to read wav/aiff from pointer yet
if (data.Buffer != null) if (data != null)
{ {
try try
{ {
@@ -277,10 +272,9 @@ namespace NTwain
var args = new TransferredEventArgs(info, data); var args = new TransferredEventArgs(info, data);
Transferred?.Invoke(this, args); Transferred?.Invoke(this, args);
} }
catch { } catch
finally
{ {
XferMemPool.Return(data.Buffer); data.Dispose();
} }
} }
} }
@@ -321,7 +315,7 @@ namespace NTwain
}; };
memXferOSX.Memory = memXfer.Memory; memXferOSX.Memory = memXfer.Memory;
byte[] dotnetBuff = XferMemPool.Rent((int)buffSize); byte[] dotnetBuff = BufferedData.MemPool.Rent((int)buffSize);
try try
{ {
do do
@@ -354,7 +348,7 @@ namespace NTwain
finally finally
{ {
if (memPtr != IntPtr.Zero) Free(memPtr); if (memPtr != IntPtr.Zero) Free(memPtr);
XferMemPool.Return(dotnetBuff); BufferedData.MemPool.Return(dotnetBuff);
} }
@@ -400,7 +394,7 @@ namespace NTwain
// TODO: how to get actual file size before hand? Is it imagelayout? // TODO: how to get actual file size before hand? Is it imagelayout?
// otherwise will just write to stream with lots of copies // otherwise will just write to stream with lots of copies
byte[] dotnetBuff = XferMemPool.Rent((int)buffSize); byte[] dotnetBuff = BufferedData.MemPool.Rent((int)buffSize);
using var outStream = new MemoryStream(); using var outStream = new MemoryStream();
try try
{ {
@@ -431,7 +425,7 @@ namespace NTwain
finally finally
{ {
if (memPtr != IntPtr.Zero) Free(memPtr); if (memPtr != IntPtr.Zero) Free(memPtr);
XferMemPool.Return(dotnetBuff); BufferedData.MemPool.Return(dotnetBuff);
} }
if (rc == TWRC.XFERDONE) if (rc == TWRC.XFERDONE)
@@ -440,7 +434,7 @@ namespace NTwain
{ {
DGImage.ImageInfo.Get(ref _appIdentity, ref _currentDS, out TW_IMAGEINFO info); DGImage.ImageInfo.Get(ref _appIdentity, ref _currentDS, out TW_IMAGEINFO info);
// ToArray bypasses the XferMemPool but I guess this will have to do for now // ToArray bypasses the XferMemPool but I guess this will have to do for now
var args = new TransferredEventArgs(this, info, fileSetup, new BufferedData { Buffer = outStream.ToArray(), Length = (int)outStream.Length }); var args = new TransferredEventArgs(this, info, fileSetup, new BufferedData(outStream.ToArray(), (int)outStream.Length, false));
Transferred?.Invoke(this, args); Transferred?.Invoke(this, args);
} }
catch { } catch { }
@@ -494,15 +488,15 @@ namespace NTwain
{ {
State = STATE.S7; State = STATE.S7;
lockedPtr = Lock(dataPtr); lockedPtr = Lock(dataPtr);
BufferedData data = default; BufferedData? data = default;
if (ImageTools.IsDib(lockedPtr)) if (ImageTools.IsDib(lockedPtr))
{ {
data = ImageTools.GetBitmapData(XferMemPool, lockedPtr); data = ImageTools.GetBitmapData(lockedPtr);
} }
else if (ImageTools.IsTiff(lockedPtr)) else if (ImageTools.IsTiff(lockedPtr))
{ {
data = ImageTools.GetTiffData(XferMemPool, lockedPtr); data = ImageTools.GetTiffData(lockedPtr);
} }
else else
{ {
@@ -510,7 +504,7 @@ namespace NTwain
// don't support more formats :( // don't support more formats :(
} }
if (data.Buffer != null) if (data != null)
{ {
try try
{ {
@@ -519,10 +513,9 @@ namespace NTwain
var args = new TransferredEventArgs(this, info, null, data); var args = new TransferredEventArgs(this, info, null, data);
Transferred?.Invoke(this, args); Transferred?.Invoke(this, args);
} }
catch { } catch
finally
{ {
XferMemPool.Return(data.Buffer); data.Dispose();
} }
} }