ntwain/src/NTwain/TwainSession.cs
2023-03-31 08:10:37 -04:00

565 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using TWAINWorkingGroup;
using static TWAINWorkingGroup.TWAIN;
namespace NTwain
{
/// <summary>
/// A wrapper around the low-level <see cref="TWAIN"/> object
/// that may be easier to use in dotnet with typical scenarios.
/// </summary>
public class TwainSession : IDisposable
{
private TWAIN _twain;
private bool _disposed;
private readonly IThreadMarshaller _threadMarshaller;
private IntPtr _hWnd;
public TwainSession(Assembly applicationInfo,
IThreadMarshaller threadMarshaller, IntPtr hWnd,
TWLG language = TWLG.ENGLISH_USA, TWCY country = TWCY.USA) :
this(FileVersionInfo.GetVersionInfo(applicationInfo.Location),
threadMarshaller, hWnd, language, country)
{ }
public TwainSession(FileVersionInfo applicationInfo,
IThreadMarshaller threadMarshaller, IntPtr hWnd,
TWLG language = TWLG.ENGLISH_USA, TWCY country = TWCY.USA) :
this(applicationInfo.CompanyName, applicationInfo.ProductName, applicationInfo.ProductName,
threadMarshaller, hWnd, language, country)
{ }
public TwainSession(string companyName, string productFamily, string productName,
IThreadMarshaller threadMarshaller, IntPtr hWnd,
TWLG language = TWLG.ENGLISH_USA, TWCY country = TWCY.USA)
{
_twain = new TWAIN(
companyName, productFamily, productName,
(ushort)TWON_PROTOCOL.MAJOR, (ushort)TWON_PROTOCOL.MINOR,
(uint)(DG.APP2 | DG.IMAGE),
country, "", language, 2, 4, false, true,
HandleDeviceEvent,
HandleScanEvent,
HandleUIThreadAction,
hWnd);
_threadMarshaller = threadMarshaller ?? new ThreadPoolMarshaller();
_hWnd = hWnd;
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
if (_twain != null)
{
Close();
_twain.Dispose();
_twain = null;
}
Log.Close();
}
_disposed = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Gets the low-level twain object.
/// Only use it if you know what you're doing.
/// </summary>
public TWAIN TWAIN { get { return _twain; } }
#region event callbacks
/// <summary>
/// Raised when data source has encountered some hardwar event.
/// </summary>
public event EventHandler<TW_DEVICEEVENT> DeviceEvent;
/// <summary>
/// Raised when data source comes down to state 4 from higher.
/// </summary>
public event EventHandler SourceDisabled;
/// <summary>
/// Raised when there's some error during transfer.
/// </summary>
public event EventHandler<TransferErrorEventArgs> TransferError;
/// <summary>
/// Raised when there's a pending transfer. Can be used to cancel transfers.
/// </summary>
public event EventHandler<TransferReadyEventArgs> TransferReady;
private void HandleUIThreadAction(Action action)
{
DebugThreadInfo("begin");
_threadMarshaller.Invoke(action);
}
private STS HandleDeviceEvent()
{
STS sts;
TW_DEVICEEVENT twdeviceevent;
// Drain the event queue...
while (true)
{
DebugThreadInfo("in loop");
// Try to get an event...
twdeviceevent = default;
sts = _twain.DatDeviceevent(DG.CONTROL, MSG.GET, ref twdeviceevent);
if (sts != STS.SUCCESS)
{
break;
}
else
{
try
{
DeviceEvent?.Invoke(this, twdeviceevent);
}
catch { }
}
}
// Return a status, in case we ever need it for anything...
return STS.SUCCESS;
}
private STS HandleScanEvent(bool closing)
{
DebugThreadInfo("begin");
// the scan event needs to return asap since it can come from msg loop
// so fire off the handling work to another thread
_threadMarshaller.BeginInvoke(new Action<bool>(HandleScanEventReal), closing);
return STS.SUCCESS;
}
void HandleScanEventReal(bool closing)
{
DebugThreadInfo("begin");
if (_twain == null || State <= STATE.S4 || closing) return;
if (_twain.IsMsgCloseDsReq() || _twain.IsMsgCloseDsOk())
{
_twain.Rollback(STATE.S4);
return;
}
// all except mem xfer will run this once and raise event.
// mem xfer will run this multiple times until complete image is assembled
if (_twain.IsMsgXferReady())
{
TW_PENDINGXFERS pending = default;
var sts = _twain.DatPendingxfers(DG.CONTROL, MSG.GET, ref pending);
if (sts != STS.SUCCESS)
{
try
{
TransferError?.Invoke(this, new TransferErrorEventArgs(sts));
}
catch { }
return; // do more?
}
var xferMech = Capabilities.ICAP_XFERMECH.GetCurrent();
var readyArgs = new TransferReadyEventArgs(_twain, pending.Count, (TWEJ)pending.EOJ);
try
{
TransferReady?.Invoke(this, readyArgs);
}
catch { }
if (readyArgs.CancelCapture == CancelType.Immediate)
{
sts = _twain.DatPendingxfers(DG.CONTROL, MSG.RESET, ref pending);
}
else
{
if (readyArgs.CancelCapture == CancelType.Graceful) StopCapture();
if (!readyArgs.SkipCurrent)
{
switch (xferMech)
{
case TWSX.NATIVE:
RunImageNativeXfer();
break;
case TWSX.MEMFILE:
RunImageMemFileXfer();
break;
case TWSX.FILE:
RunImageFileXfer();
break;
case TWSX.MEMORY:
RunImageMemoryXfer();
break;
}
}
sts = _twain.DatPendingxfers(DG.CONTROL, MSG.ENDXFER, ref pending);
}
// TODO: may be wrong for now
if (pending.Count == 0 || sts == STS.CANCEL || sts == STS.XFERDONE)
{
_twain.Rollback(STATE.S4);
}
else
{
HandleScanEvent(State <= STATE.S3);
}
}
}
[Conditional("DEBUG")]
private void DebugThreadInfo(string description, [CallerMemberName] string callerName = "")
{
var tid = Thread.CurrentThread.ManagedThreadId;
Debug.WriteLine($"[Thread {tid}] {callerName}() {description}");
}
private void RunImageMemoryXfer()
{
throw new NotImplementedException();
//// Handle DAT_NULL/MSG_XFERREADY...
//if (_twain.IsMsgXferReady() && !_xferReadySent)
//{
// _xferReadySent = true;
// // Get the amount of memory needed...
// TW_SETUPMEMXFER m_twsetupmemxfer = default;
// var sts = _twain.DatSetupmemxfer(DG.CONTROL, MSG.GET, ref m_twsetupmemxfer);
// if ((sts != STS.SUCCESS) || (m_twsetupmemxfer.Preferred == 0))
// {
// _xferReadySent = false;
// if (!_disableDsSent)
// {
// _disableDsSent = true;
// StepDown(STATE.S4);
// }
// }
// // Allocate the transfer memory (with a little extra to protect ourselves)...
// var m_intptrXfer = Marshal.AllocHGlobal((int)m_twsetupmemxfer.Preferred + 65536);
// if (m_intptrXfer == IntPtr.Zero)
// {
// _disableDsSent = true;
// StepDown(STATE.S4);
// }
//}
//// This is where the statemachine runs that transfers and optionally
//// saves the images to disk (it also displays them). It'll go back
//// and forth between states 6 and 7 until an error occurs, or until
//// we run out of images...
//if (_xferReadySent && !_disableDsSent)
//{
// CaptureImages();
//}
}
private void RunImageFileXfer()
{
throw new NotImplementedException();
}
private void RunImageMemFileXfer()
{
throw new NotImplementedException();
}
private void RunImageNativeXfer()
{
}
//protected virtual void OnScanEvent(bool closing) { }
//public event EventHandler<bool> ScanEvent;
#endregion
#region TWAIN operations
/// <summary>
/// Gets the current TWAIN state.
/// </summary>
public STATE State
{
get { return _twain.GetState(); }
}
/// <summary>
/// Opens the TWAIN data source manager.
/// This needs to be done before anything else.
/// </summary>
/// <returns></returns>
public STS Open()
{
var sts = _twain.DatParent(DG.CONTROL, MSG.OPENDSM, ref _hWnd);
return sts;
}
/// <summary>
/// Closes the TWAIN data source manager.
/// This is called when <see cref="Dispose()"/> is invoked.
/// </summary>
public void Close()
{
_twain.Rollback(STATE.S2);
}
/// <summary>
/// Gets list of TWAIN data sources.
/// </summary>
/// <returns></returns>
public IList<TW_IDENTITY> GetDataSources()
{
var list = new List<TW_IDENTITY>();
if (State > STATE.S2)
{
TW_IDENTITY twidentity = default;
STS sts;
for (sts = _twain.DatIdentity(DG.CONTROL, MSG.GETFIRST, ref twidentity);
sts != STS.ENDOFLIST;
sts = _twain.DatIdentity(DG.CONTROL, MSG.GETNEXT, ref twidentity))
{
list.Add(twidentity);
}
}
return list;
}
/// <summary>
/// Gets or sets the default data source.
/// </summary>
public TW_IDENTITY? DefaultDataSource
{
get
{
TW_IDENTITY twidentity = default;
var sts = _twain.DatIdentity(DG.CONTROL, MSG.GETDEFAULT, ref twidentity);
if (sts == STS.SUCCESS) return twidentity;
return null;
}
set
{
// Make it the default, we don't care if this succeeds...
if (value.HasValue)
{
var twidentity = value.Value;
_twain.DatIdentity(DG.CONTROL, MSG.SET, ref twidentity);
}
}
}
/// <summary>
/// Gets or sets the currently open data source.
/// Setting it will try to open it.
/// </summary>
public TW_IDENTITY? CurrentDataSource
{
get
{
if (State > STATE.S3)
{
return _twain.m_twidentityDs;
}
return null;
}
set
{
_twain.Rollback(STATE.S3);
if (value.HasValue)
{
var twidentity = value.Value;
_twain.DatIdentity(DG.CONTROL, MSG.OPENDS, ref twidentity);
}
}
}
private Capabilities _caps;
/// <summary>
/// Get current data source's capabilities. Will be null if no data source is open.
/// </summary>
/// <returns></returns>
public Capabilities Capabilities
{
get
{
if (State >= STATE.S4)
{
return _caps ?? (_caps = new Capabilities(_twain));
}
return null;
}
}
/// <summary>
/// Gets/sets the current source's settings as opaque data.
/// Returns null if not supported.
/// </summary>
public byte[] CustomDsData
{
get
{
TW_CUSTOMDSDATA data = default;
var sts = _twain.DatCustomdsdata(DG.CONTROL, MSG.GET, ref data);
if (sts == STS.SUCCESS)
{
if (data.hData != IntPtr.Zero && data.InfoLength > 0)
{
try
{
var lockedPtr = _twain.DsmMemLock(data.hData);
var bytes = new byte[data.InfoLength];
Marshal.Copy(lockedPtr, bytes, 0, bytes.Length);
}
finally
{
_twain.DsmMemUnlock(data.hData);
_twain.DsmMemFree(ref data.hData);
}
}
return EmptyArray<byte>.Value;
}
return null;
}
set
{
if (value == null || value.Length == 0) return;
TW_CUSTOMDSDATA data = default;
data.InfoLength = (uint)value.Length;
data.hData = _twain.DsmMemAlloc(data.InfoLength);
try
{
var lockedPtr = _twain.DsmMemLock(data.hData);
Marshal.Copy(value, 0, lockedPtr, value.Length);
_twain.DsmMemUnlock(data.hData);
var sts = _twain.DatCustomdsdata(DG.CONTROL, MSG.SET, ref data);
}
finally
{
// should be freed already if no error but just in case
if (data.hData != IntPtr.Zero) _twain.DsmMemFree(ref data.hData);
}
}
}
/// <summary>
/// Attempts to show the current data source's settings dialog if supported.
/// </summary>
/// <returns></returns>
public STS ShowSettings()
{
TW_USERINTERFACE ui = default;
ui.hParent = _hWnd;
ui.ShowUI = 1;
return _twain.DatUserinterface(DG.CONTROL, MSG.ENABLEDSUIONLY, ref ui);
}
/// <summary>
/// Begins the capture process on the current data source.
/// </summary>
/// <param name="showUI">Whether to display settings UI. Not all data sources support this.</param>
/// <returns></returns>
public STS StartCapture(bool showUI)
{
TW_USERINTERFACE ui = default;
ui.hParent = _hWnd;
ui.ShowUI = (ushort)(showUI ? 1 : 0);
return _twain.DatUserinterface(DG.CONTROL, MSG.ENABLEDS, ref ui);
}
/// <summary>
/// Stops the data source's automated feeder
/// if <see cref="Capabilities.CAP_AUTOSCAN"/> is set to true.
/// </summary>
/// <returns></returns>
public STS StopCapture()
{
TW_PENDINGXFERS pending = default;
return _twain.DatPendingxfers(DG.CONTROL, MSG.STOPFEEDER, ref pending);
}
/// <summary>
/// Reads information relating to the last capture run.
/// Only valid on state 4 after a capture.
/// </summary>
public Metrics GetMetrics()
{
TW_METRICS twmetrics = default;
twmetrics.SizeOf = (uint)Marshal.SizeOf(twmetrics);
var sts = _twain.DatMetrics(DG.CONTROL, MSG.GET, ref twmetrics);
if (sts == STS.SUCCESS)
{
return new Metrics
{
ReturnCode = sts,
Images = (int)twmetrics.ImageCount,
Sheets = (int)twmetrics.SheetCount
};
}
return new Metrics { ReturnCode = sts };
}
/// <summary>
/// Sends a TWAIN Direct task from the application to the driver.
/// </summary>
/// <param name="taskJson">The TWAIN Direct task in JSON.</param>
/// <param name="communicationManager">The current system being used to connect the application to the scanner.</param>
/// <returns></returns>
public TwainDirectTaskResult SetTwainDirectTask(string taskJson, ushort communicationManager = 0)
{
var result = new TwainDirectTaskResult { ReturnCode = STS.FAILURE };
TW_TWAINDIRECT task = default;
try
{
task.SizeOf = (uint)Marshal.SizeOf(typeof(TW_TWAINDIRECT));
task.CommunicationManager = communicationManager;
task.Send = ValueWriter.StringToPtrUTF8(_twain, taskJson, out int length);
task.SendSize = (uint)length;
result.ReturnCode = _twain.DatTwaindirect(DG.CONTROL, MSG.SETTASK, ref task);
if (result.ReturnCode == STS.SUCCESS && task.ReceiveSize > 0 && task.Receive != IntPtr.Zero)
{
result.ResponseJson = ValueReader.PtrToStringUTF8(task.Receive, (int)task.ReceiveSize);
}
}
finally
{
if (task.Send != IntPtr.Zero) _twain.DsmMemFree(ref task.Send); // just in case
if (task.Receive != IntPtr.Zero) _twain.DsmMemFree(ref task.Receive);
}
return result;
}
#endregion
}
}