using NTwain.Data;
using NTwain.Triplets;
using NTwain.Values;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
namespace NTwain
{
///
/// Basic class for interfacing with TWAIN.
///
public class TwainSession : ITwainStateInternal, ITwainOperation
{
///
/// 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;
}
TWIdentity _appId;
HandleRef _appHandle;
SynchronizationContext _syncer;
object _callbackObj; // kept around so it doesn't get gc'ed
TWUserInterface _twui;
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");
}
}
#region ITwainStateInternal Members
///
/// Gets the app id used for the session.
///
/// The app id.
TWIdentity ITwainStateInternal.GetAppId() { return _appId; }
///
/// 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; }
void ITwainStateInternal.ChangeState(int newState, bool notifyChange)
{
_state = newState;
if (notifyChange)
{
RaisePropertyChanged("State");
OnStateChanged();
}
}
ICommitable ITwainStateInternal.GetPendingStateChanger(int newState)
{
return new TentativeStateCommitable(this, newState);
}
void ITwainStateInternal.ChangeSourceId(TWIdentity sourceId)
{
SourceId = sourceId;
RaisePropertyChanged("SourceId");
OnSourceChanged();
}
#endregion
#region ITwainState Members
///
/// Gets the source id used for the session.
///
///
/// The source id.
///
public TWIdentity SourceId { get; private set; }
int _state;
///
/// Gets the current state number as defined by the TWAIN spec.
///
///
/// The state.
///
public int State
{
get { return _state; }
protected set
{
if (value > 0 && value < 8)
{
_state = value;
RaisePropertyChanged("State");
OnStateChanged();
}
}
}
#endregion
#region ITwainOperation Members
DGAudio _dgAudio;
///
/// Gets the triplet operations defined for audio data group.
///
public DGAudio DGAudio
{
get
{
if (_dgAudio == null) { _dgAudio = new DGAudio(this); }
return _dgAudio;
}
}
DGControl _dgControl;
///
/// Gets the triplet operations defined for control data group.
///
public DGControl DGControl
{
get
{
if (_dgControl == null) { _dgControl = new DGControl(this); }
return _dgControl;
}
}
DGImage _dgImage;
///
/// Gets the triplet operations defined for image data group.
///
public DGImage DGImage
{
get
{
if (_dgImage == null) { _dgImage = new DGImage(this); }
return _dgImage;
}
}
#endregion
#region INotifyPropertyChanged Members
///
/// Occurs when a property value changes.
///
public event PropertyChangedEventHandler PropertyChanged;
///
/// Raises the event.
///
/// Name of the property.
protected void RaisePropertyChanged(string propertyName)
{
var hand = PropertyChanged;
if (hand != null) { hand(this, new PropertyChangedEventArgs(propertyName)); }
}
#endregion
#region privileged calls that causes state change in TWAIN
///
/// Opens the data source manager. This must be the first method used
/// before using other TWAIN functions. Calls to this must be followed by when done.
///
/// 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 appHandle)
{
Debug.WriteLine(string.Format("Thread {0}: OpenManager.", Thread.CurrentThread.ManagedThreadId));
var rc = DGControl.Parent.OpenDsm(appHandle.Handle);
if (rc == ReturnCode.Success)
{
_appHandle = appHandle;
// if twain2 then get memory management functions
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(_appHandle.Handle);
if (rc == ReturnCode.Success)
{
_appHandle = default(HandleRef);
}
return rc;
}
///
/// Loads the specified source into main memory and causes its initialization.
/// Calls to this must be followed by
/// when done.
///
/// Name of the source.
///
/// Source name is required.;sourceProductName
public ReturnCode OpenSource(string sourceProductName)
{
if (string.IsNullOrEmpty(sourceProductName)) { throw new ArgumentException("Source name is required.", "sourceProductName"); }
Debug.WriteLine(string.Format("Thread {0}: OpenSource.", Thread.CurrentThread.ManagedThreadId));
var source = new TWIdentity();
source.ProductName = sourceProductName;
var rc = DGControl.Identity.OpenDS(source);
if (rc == ReturnCode.Success)
{
}
return rc;
}
///
/// 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;
}
return rc;
}
///
/// Enables the source.
///
/// The mode.
/// if set to true any driver UI will display as modal.
/// The window handle if modal.
/// The
/// Windows only.
/// that is required for certain operations.
/// It is recommended you call this method in an UI thread and pass in
///
/// if you do not have a custom one setup.
///
/// context
public ReturnCode EnableSource(SourceEnableMode mode, bool modal, HandleRef windowHandle, SynchronizationContext context)
{
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
{
if (context == null) { throw new ArgumentNullException("context"); }
}
Debug.WriteLine(string.Format("Thread {0}: EnableSource.", Thread.CurrentThread.ManagedThreadId));
_syncer = context;
// app v2.2 or higher uses callback2
if (_appId.ProtocolMajor >= 2 && _appId.ProtocolMinor >= 2)
{
var cb = new TWCallback2(HandleCallback);
var rc2 = DGControl.Callback2.RegisterCallback(cb);
if (rc2 == ReturnCode.Success)
{
Debug.WriteLine("Registered callback2 OK.");
_callbackObj = cb;
}
}
else
{
var cb = new TWCallback(HandleCallback);
var rc2 = DGControl.Callback.RegisterCallback(cb);
if (rc2 == ReturnCode.Success)
{
Debug.WriteLine("Registered callback OK.");
_callbackObj = cb;
}
}
_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.
///
///
protected ReturnCode DisableSource()
{
Debug.WriteLine(string.Format("Thread {0}: DisableSource.", Thread.CurrentThread.ManagedThreadId));
var rc = DGControl.UserInterface.DisableDS(_twui);
if (rc == ReturnCode.Success)
{
OnSourceDisabled();
}
return rc;
}
///
/// Forces the stepping down of an opened source when things gets out of control.
/// 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;
}
#endregion
#region custom events and overridables
///
/// Occurs when has changed.
///
public event EventHandler StateChanged;
///
/// Occurs when has changed.
///
public event EventHandler SourceChanged;
///
/// Occurs when source has been disabled (back to state 4).
///
public event EventHandler SourceDisabled;
///
/// Occurs when the source has generated an event.
///
public event EventHandler DeviceEvent;
///
/// Occurs when a data transfer is ready.
///
public event EventHandler TransferReady;
///
/// Occurs when data has been transferred.
///
public event EventHandler DataTransferred;
///
/// Called when changed.
///
protected virtual void OnStateChanged()
{
var hand = StateChanged;
if (hand != null)
{
try
{
hand(this, EventArgs.Empty);
}
catch { }
}
}
///
/// Called when changed.
///
protected virtual void OnSourceChanged()
{
var hand = SourceChanged;
if (hand != null)
{
try
{
hand(this, EventArgs.Empty);
}
catch { }
}
}
///
/// Called when source has been disabled (back to state 4).
///
protected virtual void OnSourceDisabled()
{
var hand = SourceDisabled;
if (hand != null)
{
try
{
hand(this, EventArgs.Empty);
}
catch { }
}
}
///
/// Raises the event.
///
/// The instance containing the event data.
protected virtual void OnDeviceEvent(DeviceEventArgs e)
{
var hand = DeviceEvent;
if (hand != null)
{
try
{
hand(this, e);
}
catch { }
}
}
///
/// Raises the event.
///
/// The instance containing the event data.
protected virtual void OnTransferReady(TransferReadyEventArgs e)
{
var hand = TransferReady;
if (hand != null)
{
try
{
hand(this, e);
}
catch { }
}
}
///
/// Raises the event.
///
/// The instance containing the event data.
protected virtual void OnDataTransferred(DataTransferredEventArgs e)
{
var hand = DataTransferred;
if (hand != null)
{
try
{
hand(this, e);
}
catch { }
}
}
#endregion
#region real TWAIN logic during xfer
///
/// Handles the message from a typical WndProc message loop and check if it's from the TWAIN source.
///
/// The message.
/// True if handled by TWAIN.
protected bool HandleWndProcMessage(ref MESSAGE message)
{
var handled = false;
if (State >= 4) // technically we should only handle on state >= 5 but there might be missed msgs if we wait until state changes after enabling ds
{
// transform it into a pointer for twain
IntPtr msgPtr = IntPtr.Zero;
try
{
// no need to do another lock call when using marshal alloc
msgPtr = Marshal.AllocHGlobal(Marshal.SizeOf(message));
Marshal.StructureToPtr(message, msgPtr, false);
TWEvent evt = new TWEvent();
evt.pEvent = msgPtr;
if (handled = DGControl.Event.ProcessEvent(evt) == ReturnCode.DSEvent)
{
Debug.WriteLine(string.Format("Thread {0}: HandleWndProcMessage at state {1} with MSG={2}.", Thread.CurrentThread.ManagedThreadId, State, evt.TWMessage));
HandleSourceMsg(evt.TWMessage);
}
}
finally
{
if (msgPtr != IntPtr.Zero) { Marshal.FreeHGlobal(msgPtr); }
}
}
return handled;
}
ReturnCode HandleCallback(TWIdentity origin, TWIdentity destination, DataGroups dg, DataArgumentType dat, Message msg, IntPtr data)
{
if (origin != null && SourceId != null && origin.Id == SourceId.Id)
{
Debug.WriteLine(string.Format("Thread {0}: CallbackHandler at state {1} with MSG={2}.", Thread.CurrentThread.ManagedThreadId, State, msg));
// spec says we must handle this on the thread that enabled the DS,
// but it's usually already the same thread and doesn't work (failure + seqError) w/o jumping to another thread and back.
// My guess is the DS needs to see the Success return code first before letting transfer happen
// so this is an hack to make it happen.
// TODO: find a better method.
ThreadPool.QueueUserWorkItem(o =>
{
var ctx = o as SynchronizationContext;
if (ctx != null)
{
_syncer.Post(blah =>
{
HandleSourceMsg(msg);
}, null);
}
else
{
// no context? better hope for the best!
HandleSourceMsg(msg);
}
}, _syncer);
return ReturnCode.Success;
}
return ReturnCode.Failure;
}
// method that handles msg from the source, whether it's from wndproc or callbacks
void HandleSourceMsg(Message msg)
{
switch (msg)
{
case Message.XferReady:
if (State < 6)
{
State = 6;
}
DoTransferRoutine();
break;
case Message.DeviceEvent:
TWDeviceEvent de;
var rc = DGControl.DeviceEvent.Get(out de);
if (rc == ReturnCode.Success)
{
OnDeviceEvent(new DeviceEventArgs(de));
}
break;
case Message.CloseDSReq:
case 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;
}
}
///
/// Does the TWAIN transfer routine at state 6.
///
protected virtual 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;
OnTransferReady(args);
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;
if (dataPtr != IntPtr.Zero)
{
lockedPtr = MemoryManager.Instance.Lock(dataPtr);
}
OnDataTransferred(new DataTransferredEventArgs(lockedPtr, file));
}
//}
//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
///
/// The MSG structure in Windows for TWAIN use.
///
[StructLayout(LayoutKind.Sequential)]
protected struct MESSAGE
{
public IntPtr hwnd;
public uint message;
public IntPtr wParam;
public IntPtr lParam;
public uint time;
public int x;
public int y;
}
}
}