using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NTwain.Triplets;
using NTwain.Data;
using NTwain.Values;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Windows.Interop;
using System.Diagnostics;
using System.Security.Permissions;
using System.IO;
using System.ComponentModel;
using System.Threading;
namespace NTwain
{
///
/// Provides a session for working with TWAIN api in an application.
///
public class TwainSession : ITwainStateInternal, IMessageFilter, INotifyPropertyChanged
{
///
/// Initializes a new instance of the class.
///
/// The app id.
///
public TwainSession(TWIdentity appId)
{
if (appId == null) { throw new ArgumentNullException("appId"); }
_appId = appId;
State = 1;
EnforceState = true;
}
#region properties
object _callbackObj;
SynchronizationContext _syncer;
TWIdentity _appId;
///
/// Gets the app id used for the session.
///
/// The app id.
TWIdentity ITwainStateInternal.GetAppId() { return _appId; }
///
/// Gets the source id used for the session.
///
/// The source id.
public TWIdentity SourceId { get; private set; }
///
/// Gets the current state number as defined by the TWAIN spec.
///
/// The state.
public int State { get; private set; }
///
/// Gets or sets a value indicating whether callback is used parts of source communication
/// if supported. May be required if things don't work. This does not take effect if
/// the source is already open.
///
///
/// true to disable callback; otherwise, false.
///
public bool DisableCallback { get; set; }
///
/// Gets or sets a value indicating whether calls to triplets will verify the current twain session state.
///
///
/// true if state value is enforced; otherwise, false.
///
public bool EnforceState { get; set; }
DGAudio _dgAudio;
///
/// Gets the triplet operations defined for audio data group.
///
/// The DG audio.
public DGAudio DGAudio
{
get
{
if (_dgAudio == null) { _dgAudio = new DGAudio(this); }
return _dgAudio;
}
}
DGControl _dgControl;
///
/// Gets the triplet operations defined for control data group.
///
/// The DG control.
public DGControl DGControl
{
get
{
if (_dgControl == null) { _dgControl = new DGControl(this); }
return _dgControl;
}
}
DGImage _dgImage;
///
/// Gets the triplet operations defined for image data group.
///
/// The DG image.
public DGImage DGImage
{
get
{
if (_dgImage == null) { _dgImage = new DGImage(this); }
return _dgImage;
}
}
private IList _supportedCaps;
///
/// Gets the supported caps for the current source.
///
///
/// The supported caps.
///
public IList SupportedCaps
{
get
{
if (_supportedCaps == null && State > 3)
{
_supportedCaps = this.GetCapabilities();
}
return _supportedCaps ?? new CapabilityId[0];
}
private set
{
_supportedCaps = value;
RaisePropertyChanged("SupportedCaps");
}
}
#endregion
#region state transition calls
void ITwainStateInternal.ChangeState(int newState, bool notifyChange)
{
Debug.WriteLine("TWAIN State = " + newState);
State = newState;
if (notifyChange) { RaisePropertyChanged("State"); }
}
ICommitable ITwainStateInternal.GetPendingStateChanger(int newState)
{
return new TentativeStateCommitable(this, newState);
}
void ITwainStateInternal.ChangeSourceId(TWIdentity sourceId)
{
SourceId = sourceId;
RaisePropertyChanged("SourceId");
}
HandleRef _parentHandle;
///
/// Opens the data source manager.
///
/// The handle. On Windows = points to the window handle (hWnd) that will act as the Source’s
/// "parent". On Macintosh = should be a NULL value.
///
public ReturnCode OpenManager(HandleRef handle)
{
Debug.WriteLine(string.Format("Thread {0}: OpenManager.", Thread.CurrentThread.ManagedThreadId));
_parentHandle = handle;
var rc = DGControl.Parent.OpenDsm(handle.Handle);
if (rc == ReturnCode.Success)
{
// if twain2 then get mem management stuff
if ((_appId.DataFunctionalities & DataFunctionalities.Dsm2) == DataFunctionalities.Dsm2)
{
TWEntryPoint entry;
rc = DGControl.EntryPoint.Get(out entry);
if (rc == ReturnCode.Success)
{
MemoryManager.Instance.UpdateEntryPoint(entry);
Debug.WriteLine("Using TWAIN2 memory functions.");
}
else
{
CloseManager();
}
}
}
return rc;
}
///
/// Closes the data source manager.
///
///
public ReturnCode CloseManager()
{
Debug.WriteLine(string.Format("Thread {0}: CloseManager.", Thread.CurrentThread.ManagedThreadId));
var rc = DGControl.Parent.CloseDsm(_parentHandle.Handle);
if (rc == ReturnCode.Success)
{
_parentHandle = default(HandleRef);
MemoryManager.Instance.UpdateEntryPoint(null);
}
return rc;
}
///
/// Loads the specified source into main memory and causes its initialization.
///
/// Name of the source.
///
public ReturnCode OpenSource(string sourceProductName)
{
var source = new TWIdentity();
source.ProductName = sourceProductName;
return OpenSource(source);
}
///
/// Loads the specified Source into main memory and causes its initialization.
///
/// The source id.
///
public ReturnCode OpenSource(TWIdentity sourceId)
{
if (sourceId == null) { throw new ArgumentNullException("sourceId"); }
Debug.WriteLine(string.Format("Thread {0}: OpenSource.", Thread.CurrentThread.ManagedThreadId));
var rc = DGControl.Identity.OpenDS(sourceId);
if (rc == ReturnCode.Success)
{
SupportedCaps = this.GetCapabilities();
// TODO: does it work?
_syncer = SynchronizationContext.Current ?? new SynchronizationContext();
if (!DisableCallback)
{
// app v2.2 or higher uses callback2
if (_appId.ProtocolMajor >= 2 && _appId.ProtocolMinor >= 2)
{
var cb = new TWCallback2(CallbackHandler);
var rc2 = DGControl.Callback2.RegisterCallback(cb);
if (rc2 == ReturnCode.Success)
{
Debug.WriteLine("Registered callback2.");
_callbackObj = cb;
}
}
else
{
var cb = new TWCallback(CallbackHandler);
var rc2 = DGControl.Callback.RegisterCallback(cb);
if (rc2 == ReturnCode.Success)
{
Debug.WriteLine("Registered callback.");
_callbackObj = cb;
}
}
}
}
return rc;
}
ReturnCode CallbackHandler(TWIdentity origin, TWIdentity dest,
DataGroups dg, DataArgumentType dat, Values.Message msg, IntPtr data)
{
if (origin != null && SourceId != null && origin.Id == SourceId.Id)
{
Debug.WriteLine(string.Format("Thread {0}: GOT TWAIN callback for msg {1}.", Thread.CurrentThread.ManagedThreadId, msg));
// spec says should handle this on the thread that enabled the DS,
// but it's already the same and doesn't work (failure + seqError) w/o jumping to another thread and back.
// My guess is the DS needs to see the Success first before letting transfer happen
// so this is an artificial delay to make it happen.
// TODO: find a better method.
ThreadPool.QueueUserWorkItem(o =>
{
_syncer.Send(blah =>
{
HandleSourceMsg(origin, dest, dg, dat, msg, data);
}, null);
}, null);
return ReturnCode.Success;
}
return ReturnCode.Failure;
}
///
/// When an application is finished with a Source, it must formally close the session between them
/// using this operation. This is necessary in case the Source only supports connection with a single
/// application (many desktop scanners will behave this way). A Source such as this cannot be
/// accessed by other applications until its current session is terminated
///
///
public ReturnCode CloseSource()
{
Debug.WriteLine(string.Format("Thread {0}: CloseSource.", Thread.CurrentThread.ManagedThreadId));
var rc = DGControl.Identity.CloseDS();
if (rc == ReturnCode.Success)
{
_callbackObj = null;
SupportedCaps = null;
}
return rc;
}
TWUserInterface _twui;
///
/// Enables the source for data acquisition.
///
/// The mode.
/// if set to true any driver UI will display as modal.
/// The window handle if modal.
///
public ReturnCode EnableSource(SourceEnableMode mode, bool modal, HandleRef windowHandle)
{
Debug.WriteLine(string.Format("Thread {0}: EnableSource.", Thread.CurrentThread.ManagedThreadId));
_twui = new TWUserInterface();
_twui.ShowUI = mode == SourceEnableMode.ShowUI;
_twui.ModalUI = modal;
_twui.hParent = windowHandle.Handle;
if (mode == SourceEnableMode.ShowUIOnly)
{
return DGControl.UserInterface.EnableDSUIOnly(_twui);
}
else
{
return DGControl.UserInterface.EnableDS(_twui);
}
}
///
/// Disables the source to end data acquisition.
///
///
ReturnCode DisableSource()
{
Debug.WriteLine(string.Format("Thread {0}: DisableSource.", Thread.CurrentThread.ManagedThreadId));
var rc = DGControl.UserInterface.DisableDS(_twui);
if (rc == ReturnCode.Success)
{
var hand = SourceDisabled;
if (hand != null)
{
try
{
hand(this, EventArgs.Empty);
}
catch { }
}
}
return rc;
}
#endregion
#region consumer to handle
///
/// Occurs when source has been disabled (back to state 4).
///
public event EventHandler SourceDisabled;
///
/// Occurs when a data transfer is ready.
///
public event EventHandler TransferReady;
///
/// Occurs when the source has generated an event.
///
public event EventHandler DeviceEvent;
///
/// Occurs when data has been transferred.
///
public event EventHandler DataTransferred;
private void DoTransferRoutine()
{
TWPendingXfers pending = new TWPendingXfers();
var rc = ReturnCode.Success;
do
{
IList formats = Enumerable.Empty().ToList();
IList compressions = Enumerable.Empty().ToList();
bool canDoFileXfer = this.CapGetImageXferMech().Contains(XferMech.File);
var curFormat = this.GetCurrentCap(CapabilityId.ICapImageFileFormat);
var curComp = this.GetCurrentCap(CapabilityId.ICapCompression);
TWImageInfo imgInfo;
bool skip = false;
if (DGImage.ImageInfo.Get(out imgInfo) != ReturnCode.Success)
{
// bad!
skip = true;
}
try
{
formats = this.CapGetImageFileFormat();
}
catch { }
try
{
compressions = this.CapGetCompression();
}
catch { }
// ask consumer for cancel in case of non-ui multi-page transfers
TransferReadyEventArgs args = new TransferReadyEventArgs(pending, formats, curFormat, compressions,
curComp, canDoFileXfer, imgInfo);
args.CancelCurrent = skip;
var hand = TransferReady;
if (hand != null)
{
try
{
hand(this, args);
}
catch { }
}
if (!args.CancelAll && !args.CancelCurrent)
{
Values.XferMech mech = this.GetCurrentCap(CapabilityId.ICapXferMech);
if (args.CanDoFileXfer && !string.IsNullOrEmpty(args.OutputFile))
{
var setXferRC = DGControl.SetupFileXfer.Set(new TWSetupFileXfer
{
FileName = args.OutputFile,
Format = args.ImageFormat
});
if (setXferRC == ReturnCode.Success)
{
mech = XferMech.File;
}
}
// I don't know how this is supposed to work so it probably doesn't
//this.CapSetImageFormat(args.ImageFormat);
//this.CapSetImageCompression(args.ImageCompression);
#region do xfer
// TODO: expose all swallowed exceptions somehow later
IntPtr dataPtr = IntPtr.Zero;
IntPtr lockedPtr = IntPtr.Zero;
string file = null;
try
{
ReturnCode xrc = ReturnCode.Cancel;
switch (mech)
{
case Values.XferMech.Native:
xrc = DGImage.ImageNativeXfer.Get(ref dataPtr);
break;
case Values.XferMech.File:
xrc = DGImage.ImageFileXfer.Get();
if (File.Exists(args.OutputFile))
{
file = args.OutputFile;
}
break;
case Values.XferMech.MemFile:
// not supported yet
//TWImageMemXfer memxfer = new TWImageMemXfer();
//xrc = DGImage.ImageMemXfer.Get(memxfer);
break;
}
if (xrc == ReturnCode.XferDone)
{
State = 7;
try
{
var dtHand = DataTransferred;
if (dtHand != null)
{
if (dataPtr != IntPtr.Zero)
{
lockedPtr = MemoryManager.Instance.Lock(dataPtr);
}
dtHand(this, new DataTransferredEventArgs(lockedPtr, file));
}
}
catch { }
}
//}
//else if (group == DataGroups.Audio)
//{
// var xrc = DGAudio.AudioNativeXfer.Get(ref dataPtr);
// if (xrc == ReturnCode.XferDone)
// {
// State = 7;
// try
// {
// var dtHand = DataTransferred;
// if (dtHand != null)
// {
// lockedPtr = MemoryManager.Instance.MemLock(dataPtr);
// dtHand(this, new DataTransferredEventArgs(lockedPtr));
// }
// }
// catch { }
// }
//}
}
finally
{
State = 6;
// data here is allocated by source so needs to use shared mem calls
if (lockedPtr != IntPtr.Zero)
{
MemoryManager.Instance.Unlock(lockedPtr);
lockedPtr = IntPtr.Zero;
}
if (dataPtr != IntPtr.Zero)
{
MemoryManager.Instance.Free(dataPtr);
dataPtr = IntPtr.Zero;
}
}
#endregion
}
if (args.CancelAll)
{
rc = DGControl.PendingXfers.Reset(pending);
if (rc == ReturnCode.Success)
{
// if audio exit here
//if (group == DataGroups.Audio)
//{
// //???
// return;
//}
}
}
else
{
rc = DGControl.PendingXfers.EndXfer(pending);
}
} while (rc == ReturnCode.Success && pending.Count != 0);
State = 5;
DisableSource();
}
#endregion
#region messaging use
ReturnCode HandleSourceMsg(TWIdentity origin, TWIdentity destination,
DataGroups dg, DataArgumentType dat, NTwain.Values.Message msg, IntPtr data)
{
Debug.WriteLine(string.Format("Thread {0}: HandleSourceMsg at state {1} with DG={2} DAT={3} MSG={4}.", Thread.CurrentThread.ManagedThreadId, State, dg, dat, msg));
ReturnCode rc = ReturnCode.Success;
switch (msg)
{
case Values.Message.XferReady:
if (State < 6)
State = 6;
// this is the meat of all twain stuff
DoTransferRoutine();
break;
case Values.Message.DeviceEvent:
TWDeviceEvent de;
rc = DGControl.DeviceEvent.Get(out de);
if (rc == ReturnCode.Success)
{
var hand = this.DeviceEvent;
if (hand != null)
{
try
{
hand(this, new DeviceEventArgs(de));
}
catch { }
}
}
break;
case Values.Message.CloseDSReq:
case Values.Message.CloseDSOK:
// even though it says closeDS it's really disable.
// dsok is sent if source is enabled with uionly
// some sources send this at other states so do a step down
if (State > 5)
{
ForceStepDown(4);
}
else if (State == 5)
{
// needs this state check since some source sends this more than once
DisableSource();
}
break;
}
return rc;
}
///
/// Forces the stepping down of an opened source ignoring return values.
/// Used when session state and source state become out of sync.
///
/// State of the target.
public void ForceStepDown(int targetState)
{
Debug.WriteLine(string.Format("Thread {0}: ForceStepDown.", Thread.CurrentThread.ManagedThreadId));
bool origFlag = EnforceState;
EnforceState = false;
// From the twain spec
// Stepping Back Down the States
// DG_CONTROL / DAT_PENDINGXFERS / MSG_ENDXFER → state 7 to 6
// DG_CONTROL / DAT_PENDINGXFERS / MSG_RESET → state 6 to 5
// DG_CONTROL / DAT_USERINTERFACE / MSG_DISABLEDS → state 5 to 4
// DG_CONTROL / DAT_IDENTITY / MSG_CLOSEDS → state 4 to 3
// Ignore the status returns from the calls prior to the one yielding the desired state. For instance, if a
// call during scanning returns TWCC_SEQERROR and the desire is to return to state 5, then use the
// following commands.
// DG_CONTROL / DAT_PENDINGXFERS / MSG_ENDXFER → state 7 to 6
// DG_CONTROL / DAT_PENDINGXFERS / MSG_RESET → state 6 to 5
// Being sure to confirm that DG_CONTROL / DAT_PENDINGXFERS / MSG_RESET returned
// success, the return status from DG_CONTROL / DAT_PENDINGXFERS / MSG_ENDXFER may
// be ignored.
if (targetState < 7)
{
DGControl.PendingXfers.EndXfer(new TWPendingXfers());
}
if (targetState < 6)
{
DGControl.PendingXfers.Reset(new TWPendingXfers());
}
if (targetState < 5)
{
DisableSource();
}
if (targetState < 4)
{
CloseSource();
}
if (targetState < 3)
{
CloseManager();
}
EnforceState = origFlag;
}
///
/// Handles the message from a message loop.
///
/// Pointer to message structure.
/// True if handled by TWAIN.
bool HandleLoopMsgEvent(ref IntPtr msgPtr)
{
TWEvent evt = new TWEvent();
evt.pEvent = msgPtr;
var rc = DGControl.Event.ProcessEvent(evt);
HandleSourceMsg(null, null, DataGroups.Control, DataArgumentType.Null, evt.TWMessage, IntPtr.Zero);
return rc == ReturnCode.DSEvent;
}
///
/// Message loop processor for winform.
/// Use this by adding the as an .
///
/// The message to be dispatched. You cannot modify this message.
///
/// true to filter the message and stop it from being dispatched; false to allow the message to continue to the next filter or control.
///
//[EnvironmentPermissionAttribute(SecurityAction.LinkDemand)]
[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
bool IMessageFilter.PreFilterMessage(ref System.Windows.Forms.Message m)
{
if (State > 3)
{
MSG winmsg = default(MSG);
winmsg.hwnd = m.HWnd;
winmsg.lParam = m.LParam;
winmsg.message = m.Msg;
winmsg.wParam = m.WParam;
IntPtr msgPtr = IntPtr.Zero;
try
{
// no need to lock for marshal alloc
msgPtr = MemoryManager.Instance.Allocate((uint)Marshal.SizeOf(winmsg));
Marshal.StructureToPtr(winmsg, msgPtr, false);
return HandleLoopMsgEvent(ref msgPtr);
}
finally
{
if (msgPtr != IntPtr.Zero)
MemoryManager.Instance.Free(msgPtr);
}
}
return false;
}
///
/// Message loop processor for wpf.
/// Use this as the target of delegate.
///
/// The window handle.
/// The message ID.
/// The message's wParam value.
/// The message's lParam value.
/// A value that indicates whether the message was handled. Set the value to true if the message was handled; otherwise, false.
///
[EnvironmentPermissionAttribute(SecurityAction.LinkDemand)]
public IntPtr PreFilterMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
// always pass message since it works whether there's a callback or not?
if (State > 3)// && _callbackObj == null)
{
MSG winmsg = default(MSG);
winmsg.hwnd = hwnd;
winmsg.lParam = lParam;
winmsg.message = msg;
winmsg.wParam = wParam;
IntPtr msgPtr = IntPtr.Zero;
try
{
// no need to lock for marshal alloc
msgPtr = MemoryManager.Instance.Allocate((uint)Marshal.SizeOf(winmsg));
Marshal.StructureToPtr(winmsg, msgPtr, false);
handled = HandleLoopMsgEvent(ref msgPtr);
}
finally
{
if (msgPtr != IntPtr.Zero)
MemoryManager.Instance.Free(msgPtr);
}
}
return IntPtr.Zero;
}
#endregion
#region INotifyPropertyChanged Members
///
/// Occurs when a property value changes.
///
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged(string property)
{
var hand = PropertyChanged;
if (hand != null) { hand(this, new PropertyChangedEventArgs(property)); }
}
#endregion
}
}