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;
((ITwainStateInternal)this).ChangeState(1, false);
EnforceState = true;
}
TWIdentity _appId;
IntPtr _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(IntPtr appHandle)
{
Debug.WriteLine(string.Format("Thread {0}: OpenManager.", Thread.CurrentThread.ManagedThreadId));
var rc = DGControl.Parent.OpenDsm(appHandle);
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);
if (rc == ReturnCode.Success)
{
_appHandle = IntPtr.Zero;
}
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, IntPtr windowHandle, SynchronizationContext context)
{
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;
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.
/// This should be called on the Thread that originally called the
/// method, if applicable.
///
/// 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
/// and raises the event.
///
protected virtual void OnStateChanged()
{
var hand = StateChanged;
if (hand != null)
{
try
{
hand(this, EventArgs.Empty);
}
catch { }
}
}
///
/// Called when changed
/// and raises the event.
///
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)
/// and raises the event.
///
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 TWAIN logic during xfer work
///
/// 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 without needing a SynchronizationContext.
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;
}
}
///
/// Performs the TWAIN transfer routine at state 6.
///
protected virtual void DoTransferRoutine()
{
var pending = new TWPendingXfers();
var rc = ReturnCode.Success;
do
{
#region build and raise xfer ready
TWAudioInfo audInfo;
if (DGAudio.AudioInfo.Get(out audInfo) != ReturnCode.Success)
{
audInfo = null;
}
TWImageInfo imgInfo;
if (DGImage.ImageInfo.Get(out imgInfo) != ReturnCode.Success)
{
imgInfo = null;
}
// ask consumer for xfer details
var preXferArgs = new TransferReadyEventArgs
{
AudioInfo = audInfo,
PendingImageInfo = imgInfo,
PendingTransferCount = pending.Count,
EndOfJob = pending.EndOfJob == 0
};
OnTransferReady(preXferArgs);
#endregion
#region actually handle xfer
if (preXferArgs.CancelAll)
{
rc = DGControl.PendingXfers.Reset(pending);
}
else if (!preXferArgs.CancelCurrent)
{
DataGroups xferGroup = DataGroups.None;
if (DGControl.XferGroup.Get(ref xferGroup) != ReturnCode.Success)
{
xferGroup = DataGroups.None;
}
if ((xferGroup & DataGroups.Image) == DataGroups.Image)
{
var mech = this.GetCurrentCap(CapabilityId.ICapXferMech);
switch (mech)
{
case XferMech.Native:
DoImageNativeXfer();
break;
case XferMech.Memory:
DoImageMemoryXfer();
break;
case XferMech.File:
DoImageFileXfer();
break;
case XferMech.MemFile:
DoImageMemoryFileXfer();
break;
}
}
if ((xferGroup & DataGroups.Audio) == DataGroups.Audio)
{
var mech = this.GetCurrentCap(CapabilityId.ACapXferMech);
switch (mech)
{
case XferMech.Native:
DoAudioNativeXfer();
break;
case XferMech.File:
DoAudioFileXfer();
break;
}
}
}
rc = DGControl.PendingXfers.EndXfer(pending);
#endregion
} while (rc == ReturnCode.Success && pending.Count != 0);
State = 5;
DisableSource();
}
#region audio xfers
private void DoAudioNativeXfer()
{
IntPtr dataPtr = IntPtr.Zero;
IntPtr lockedPtr = IntPtr.Zero;
try
{
var xrc = DGAudio.AudioNativeXfer.Get(ref dataPtr);
if (xrc == ReturnCode.XferDone)
{
State = 7;
if (dataPtr != IntPtr.Zero)
{
lockedPtr = MemoryManager.Instance.Lock(dataPtr);
}
OnDataTransferred(new DataTransferredEventArgs(lockedPtr, null));
}
}
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;
}
}
}
private void DoAudioFileXfer()
{
string filePath = null;
TWSetupFileXfer setupInfo;
if (DGControl.SetupFileXfer.Get(out setupInfo) == ReturnCode.Success)
{
filePath = setupInfo.FileName;
}
var xrc = DGAudio.AudioFileXfer.Get();
if (xrc == ReturnCode.XferDone)
{
OnDataTransferred(new DataTransferredEventArgs(IntPtr.Zero, filePath));
}
}
#endregion
#region image xfers
private void DoImageNativeXfer()
{
IntPtr dataPtr = IntPtr.Zero;
IntPtr lockedPtr = IntPtr.Zero;
try
{
var xrc = DGImage.ImageNativeXfer.Get(ref dataPtr);
if (xrc == ReturnCode.XferDone)
{
State = 7;
if (dataPtr != IntPtr.Zero)
{
lockedPtr = MemoryManager.Instance.Lock(dataPtr);
}
OnDataTransferred(new DataTransferredEventArgs(lockedPtr, null));
}
}
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;
}
}
}
private void DoImageFileXfer()
{
string filePath = null;
TWSetupFileXfer setupInfo;
if (DGControl.SetupFileXfer.Get(out setupInfo) == ReturnCode.Success)
{
filePath = setupInfo.FileName;
}
var xrc = DGImage.ImageFileXfer.Get();
if (xrc == ReturnCode.XferDone)
{
OnDataTransferred(new DataTransferredEventArgs(IntPtr.Zero, filePath));
}
}
private void DoImageMemoryXfer()
{
throw new NotImplementedException();
}
private void DoImageMemoryFileXfer()
{
// since it's memory-file xfer need info from both (maybe)
TWSetupMemXfer memInfo;
TWSetupFileXfer fileInfo;
if (DGControl.SetupMemXfer.Get(out memInfo) == ReturnCode.Success &&
DGControl.SetupFileXfer.Get(out fileInfo) == ReturnCode.Success)
{
TWImageMemXfer xferInfo = new TWImageMemXfer();
var tempFile = Path.GetTempFileName();
string finalFile = null;
try
{
xferInfo.Memory = new TWMemory
{
Length = memInfo.Preferred,
TheMem = MemoryManager.Instance.Allocate(memInfo.Preferred)
};
var xrc = ReturnCode.Success;
using (var outStream = File.OpenWrite(tempFile))
{
do
{
xrc = DGImage.ImageMemFileXfer.Get(xferInfo);
if (xrc == ReturnCode.Success ||
xrc == ReturnCode.XferDone)
{
byte[] buffer = new byte[(int)xferInfo.BytesWritten];
Marshal.Copy(xferInfo.Memory.TheMem, buffer, 0, buffer.Length);
outStream.Write(buffer, 0, buffer.Length);
}
} while (xrc == ReturnCode.Success);
}
if (xrc == ReturnCode.XferDone)
{
switch (fileInfo.Format)
{
case FileFormat.Bmp:
finalFile = Path.ChangeExtension(tempFile, ".bmp");
break;
case FileFormat.Dejavu:
finalFile = Path.ChangeExtension(tempFile, ".dejavu");
break;
case FileFormat.Exif:
finalFile = Path.ChangeExtension(tempFile, ".exit");
break;
case FileFormat.Fpx:
finalFile = Path.ChangeExtension(tempFile, ".fpx");
break;
case FileFormat.Jfif:
finalFile = Path.ChangeExtension(tempFile, ".jpg");
break;
case FileFormat.Jp2:
finalFile = Path.ChangeExtension(tempFile, ".jp2");
break;
case FileFormat.Jpx:
finalFile = Path.ChangeExtension(tempFile, ".jpx");
break;
case FileFormat.Pdf:
case FileFormat.PdfA:
case FileFormat.PdfA2:
finalFile = Path.ChangeExtension(tempFile, ".pdf");
break;
case FileFormat.Pict:
finalFile = Path.ChangeExtension(tempFile, ".pict");
break;
case FileFormat.Png:
finalFile = Path.ChangeExtension(tempFile, ".png");
break;
case FileFormat.Spiff:
finalFile = Path.ChangeExtension(tempFile, ".spiff");
break;
case FileFormat.Tiff:
case FileFormat.TiffMulti:
finalFile = Path.ChangeExtension(tempFile, ".tif");
break;
case FileFormat.Xbm:
finalFile = Path.ChangeExtension(tempFile, ".xbm");
break;
default:
finalFile = Path.ChangeExtension(tempFile, ".unknown");
break;
}
File.Move(tempFile, finalFile);
}
}
finally
{
if (xferInfo.Memory.TheMem != IntPtr.Zero)
{
MemoryManager.Instance.Free(xferInfo.Memory.TheMem);
}
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
}
if (File.Exists(finalFile))
{
OnDataTransferred(new DataTransferredEventArgs(IntPtr.Zero, finalFile));
}
}
}
#endregion
#endregion
///
/// The MSG structure in Windows for TWAIN use.
///
[StructLayout(LayoutKind.Sequential)]
protected struct MESSAGE
{
///
/// Initializes a new instance of the struct.
///
/// The HWND.
/// The message.
/// The w parameter.
/// The l parameter.
public MESSAGE(IntPtr hwnd, int message, IntPtr wParam, IntPtr lParam)
{
_hwnd = hwnd;
_message = (uint)message;
_wParam = wParam;
_lParam = lParam;
_time = 0;
_x = 0;
_y = 0;
}
IntPtr _hwnd;
uint _message;
IntPtr _wParam;
IntPtr _lParam;
uint _time;
int _x;
int _y;
}
}
}