diff --git a/samples/WinForm32/Form1.Designer.cs b/samples/WinForm32/Form1.Designer.cs index d242f96..0ee6652 100644 --- a/samples/WinForm32/Form1.Designer.cs +++ b/samples/WinForm32/Form1.Designer.cs @@ -41,6 +41,7 @@ private void InitializeComponent() lblState = new System.Windows.Forms.Label(); label3 = new System.Windows.Forms.Label(); btnOpenDef = new System.Windows.Forms.Button(); + ckShowUI = new System.Windows.Forms.CheckBox(); capListView = new System.Windows.Forms.ListView(); colCap = new System.Windows.Forms.ColumnHeader(); colType = new System.Windows.Forms.ColumnHeader(); @@ -52,7 +53,7 @@ private void InitializeComponent() btnStart = new System.Windows.Forms.Button(); btnShowSettings = 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(); splitContainer1.Panel1.SuspendLayout(); splitContainer1.Panel2.SuspendLayout(); @@ -167,6 +168,7 @@ private void InitializeComponent() // // splitContainer1.Panel2 // + splitContainer1.Panel2.Controls.Add(ckBgImageHandling); splitContainer1.Panel2.Controls.Add(ckShowUI); splitContainer1.Panel2.Controls.Add(capListView); splitContainer1.Panel2.Controls.Add(label4); @@ -207,6 +209,18 @@ private void InitializeComponent() btnOpenDef.UseVisualStyleBackColor = true; 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.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 @@ private void InitializeComponent() btnClose.UseVisualStyleBackColor = true; btnClose.Click += btnClose_Click; // - // ckShowUI + // ckBgImageHandling // - 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; + ckBgImageHandling.AutoSize = true; + ckBgImageHandling.Checked = true; + ckBgImageHandling.CheckState = System.Windows.Forms.CheckState.Checked; + ckBgImageHandling.Location = new System.Drawing.Point(512, 35); + ckBgImageHandling.Name = "ckBgImageHandling"; + ckBgImageHandling.Size = new System.Drawing.Size(220, 19); + ckBgImageHandling.TabIndex = 10; + ckBgImageHandling.Text = "Handle image data in another thread"; + ckBgImageHandling.UseVisualStyleBackColor = true; // // Form1 // @@ -345,5 +359,6 @@ private void InitializeComponent() private System.Windows.Forms.ColumnHeader colSupport; private System.Windows.Forms.ColumnHeader colExtended; private System.Windows.Forms.CheckBox ckShowUI; + private System.Windows.Forms.CheckBox ckBgImageHandling; } } \ No newline at end of file diff --git a/samples/WinForm32/Form1.cs b/samples/WinForm32/Form1.cs index a656e5f..421ee39 100644 --- a/samples/WinForm32/Form1.cs +++ b/samples/WinForm32/Form1.cs @@ -6,12 +6,11 @@ using System.ComponentModel; using System.Diagnostics; using System.Drawing; -using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Reflection; -using System.Runtime.Versioning; using System.Threading; +using System.Threading.Tasks; using System.Windows.Forms; namespace WinFormSample @@ -20,6 +19,8 @@ public partial class Form1 : Form { private TwainAppSession twain; private readonly string saveFolder; + readonly Stopwatch watch = new(); + private bool _useThreadForImag; public Form1() { @@ -33,8 +34,9 @@ public Form1() twain.StateChanged += Twain_StateChanged; twain.DefaultSourceChanged += Twain_DefaultSourceChanged; twain.CurrentSourceChanged += Twain_CurrentSourceChanged; + twain.SourceDisabled += Twain_SourceDisabled; twain.TransferReady += Twain_TransferReady; - twain.Transferred += Twain_DataTransferred; + twain.Transferred += Twain_Transferred; twain.TransferError += Twain_TransferError; twain.DeviceEvent += Twain_DeviceEvent; @@ -47,6 +49,15 @@ public Form1() 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) { switch (e.Reason) @@ -103,28 +114,54 @@ private void Twain_TransferError(TwainAppSession sender, TransferErrorEventArgs } - 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}"); - 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 - var saveFile = Path.Combine(saveFolder, (DateTime.Now.Ticks / 1000).ToString()); - using (var img = new ImageMagick.MagickImage(e.Data)) + if (_useThreadForImag) { - 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 - saveFile += ".png"; - } - else + HandleTransferredData(e); + }); + } + 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 - saveFile += ".jpg"; - img.Quality = 75; + if (img.ColorType == ImageMagick.ColorType.Palette) + { + // 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 @@ private void btnShowSettings_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(); + } } } } \ No newline at end of file diff --git a/src/NTwain/Data/BufferedData.cs b/src/NTwain/Data/BufferedData.cs index 1705c01..3130bb9 100644 --- a/src/NTwain/Data/BufferedData.cs +++ b/src/NTwain/Data/BufferedData.cs @@ -1,22 +1,45 @@ using System; +using System.Buffers; namespace NTwain.Data { /// - /// Simple struct with bytes buffer and the valid data length. + /// Simple thing with shared bytes buffer and the valid data length. /// - 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 MemPool = ArrayPool.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; + /// /// Bytes buffer. This may be bigger than the data size /// and contain invalid data. /// - public byte[]? Buffer; + byte[]? _buffer; + public byte[]? Buffer { get; } /// /// Actual usable data length in the buffer. /// - public int Length; + int _length; /// /// As a span of usable data. @@ -24,8 +47,17 @@ public struct BufferedData /// public ReadOnlySpan AsSpan() { - if (Buffer != null) return Buffer.AsSpan(0, Length); + if (_buffer != null) return _buffer.AsSpan(0, _length); return Span.Empty; } + + public void Dispose() + { + if (_fromPool && _buffer != null) + { + MemPool.Return(_buffer); + _buffer = null; + } + } } } diff --git a/src/NTwain/Native/ImageTools.cs b/src/NTwain/Native/ImageTools.cs index df4a5bf..d987285 100644 --- a/src/NTwain/Native/ImageTools.cs +++ b/src/NTwain/Native/ImageTools.cs @@ -24,7 +24,7 @@ public static bool IsTiff(IntPtr data) return test == 0x4949; } - public static unsafe BufferedData GetBitmapData(System.Buffers.ArrayPool xferMemPool, IntPtr data) + public static unsafe BufferedData? GetBitmapData(IntPtr data) { var infoHeader = Marshal.PtrToStructure(data); if (infoHeader.Validate()) @@ -40,7 +40,7 @@ public static unsafe BufferedData GetBitmapData(System.Buffers.ArrayPool x }; 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 @@ -58,12 +58,12 @@ public static unsafe BufferedData GetBitmapData(System.Buffers.ArrayPool x // write image 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; } - public static BufferedData GetTiffData(System.Buffers.ArrayPool xferMemPool, IntPtr data) + public static BufferedData? GetTiffData(IntPtr data) { // Find the size of the image so we can turn it into a memory stream... var headerSize = Marshal.SizeOf(typeof(TIFFHEADER)); @@ -86,10 +86,9 @@ public static BufferedData GetTiffData(System.Buffers.ArrayPool xferMemPoo if (tiffSize > 0) { - var dataCopy = xferMemPool.Rent(tiffSize);// new byte[tiffSize]; - // is this optimal? + var dataCopy = BufferedData.MemPool.Rent(tiffSize);// new byte[tiffSize]; Marshal.Copy(data, dataCopy, 0, tiffSize); - return new BufferedData { Buffer = dataCopy, Length = tiffSize }; + return new BufferedData(dataCopy, tiffSize, true); } return default; } diff --git a/src/NTwain/TransferReadyEventArgs.cs b/src/NTwain/TransferReadyEventArgs.cs index 8d6a5f7..9b88486 100644 --- a/src/NTwain/TransferReadyEventArgs.cs +++ b/src/NTwain/TransferReadyEventArgs.cs @@ -15,13 +15,6 @@ public TransferReadyEventArgs(int pendingCount, TWEJ endOfJobFlag) EndOfJobFlag = endOfJobFlag; } - - /// - /// Gets or sets whether the current transfer should be skipped - /// and continue next transfer if there are more data. - /// - public bool SkipCurrent { get; set; } - /// /// Gets or sets whether to cancel the capture phase. /// diff --git a/src/NTwain/TransferredEventArgs.cs b/src/NTwain/TransferredEventArgs.cs index 2d6c2bc..cbbc558 100644 --- a/src/NTwain/TransferredEventArgs.cs +++ b/src/NTwain/TransferredEventArgs.cs @@ -3,10 +3,8 @@ 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) { @@ -19,7 +17,7 @@ public TransferredEventArgs(TW_AUDIOINFO info, BufferedData 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; FileInfo = fileInfo; @@ -35,13 +33,13 @@ public TransferredEventArgs(TwainAppSession twain, TW_IMAGEINFO info, TW_SETUPFI /// public bool IsImage { get; } - private readonly BufferedData _data; + private readonly BufferedData? _data; /// /// The complete file data if memory was involved in the transfer. - /// IMPORTANT: Content of this array may not valid once - /// the event handler ends. + /// IMPORTANT: Content of this array will not be valid once + /// this event arg has been disposed. /// - public ReadOnlySpan Data => _data.AsSpan(); + public ReadOnlySpan Data => _data == null ? ReadOnlySpan.Empty : _data.AsSpan(); /// /// The file info if the transfer involved file information. @@ -73,5 +71,9 @@ public STS GetExtendedImageInfo(ref TW_EXTIMAGEINFO container) return _twain.GetExtendedImageInfo(ref container); } + public void Dispose() + { + _data?.Dispose(); + } } } \ No newline at end of file diff --git a/src/NTwain/TwainAppSession.Xfers.cs b/src/NTwain/TwainAppSession.Xfers.cs index 601b4cf..0e195a3 100644 --- a/src/NTwain/TwainAppSession.Xfers.cs +++ b/src/NTwain/TwainAppSession.Xfers.cs @@ -12,11 +12,6 @@ namespace NTwain 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 XferMemPool = ArrayPool.Create(32 * 1024 * 1024, 8); - /// /// Can only be called in state 7, so it's hidden here and /// only exposed in data transferred event. @@ -265,11 +260,11 @@ private STS TransferNativeAudio(ref TW_PENDINGXFERS pending) { State = STATE.S7; lockedPtr = Lock(dataPtr); - BufferedData data = default; + BufferedData? data = default; // TODO: don't know how to read wav/aiff from pointer yet - if (data.Buffer != null) + if (data != null) { try { @@ -277,10 +272,9 @@ private STS TransferNativeAudio(ref TW_PENDINGXFERS pending) var args = new TransferredEventArgs(info, data); Transferred?.Invoke(this, args); } - catch { } - finally + catch { - XferMemPool.Return(data.Buffer); + data.Dispose(); } } } @@ -321,7 +315,7 @@ private STS TransferMemoryImage(ref TW_PENDINGXFERS pending) }; memXferOSX.Memory = memXfer.Memory; - byte[] dotnetBuff = XferMemPool.Rent((int)buffSize); + byte[] dotnetBuff = BufferedData.MemPool.Rent((int)buffSize); try { do @@ -354,7 +348,7 @@ private STS TransferMemoryImage(ref TW_PENDINGXFERS pending) finally { if (memPtr != IntPtr.Zero) Free(memPtr); - XferMemPool.Return(dotnetBuff); + BufferedData.MemPool.Return(dotnetBuff); } @@ -400,7 +394,7 @@ private STS TransferMemoryFileImage(ref TW_PENDINGXFERS pending) // TODO: how to get actual file size before hand? Is it imagelayout? // 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(); try { @@ -431,7 +425,7 @@ private STS TransferMemoryFileImage(ref TW_PENDINGXFERS pending) finally { if (memPtr != IntPtr.Zero) Free(memPtr); - XferMemPool.Return(dotnetBuff); + BufferedData.MemPool.Return(dotnetBuff); } if (rc == TWRC.XFERDONE) @@ -440,7 +434,7 @@ private STS TransferMemoryFileImage(ref TW_PENDINGXFERS pending) { 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 - 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); } catch { } @@ -494,15 +488,15 @@ private STS TransferNativeImage(ref TW_PENDINGXFERS pending) { State = STATE.S7; lockedPtr = Lock(dataPtr); - BufferedData data = default; + BufferedData? data = default; if (ImageTools.IsDib(lockedPtr)) { - data = ImageTools.GetBitmapData(XferMemPool, lockedPtr); + data = ImageTools.GetBitmapData(lockedPtr); } else if (ImageTools.IsTiff(lockedPtr)) { - data = ImageTools.GetTiffData(XferMemPool, lockedPtr); + data = ImageTools.GetTiffData(lockedPtr); } else { @@ -510,7 +504,7 @@ private STS TransferNativeImage(ref TW_PENDINGXFERS pending) // don't support more formats :( } - if (data.Buffer != null) + if (data != null) { try { @@ -519,10 +513,9 @@ private STS TransferNativeImage(ref TW_PENDINGXFERS pending) var args = new TransferredEventArgs(this, info, null, data); Transferred?.Invoke(this, args); } - catch { } - finally + catch { - XferMemPool.Return(data.Buffer); + data.Dispose(); } }