using NTwain.Data; using NTwain.Internals; using NTwain.Properties; using NTwain.Triplets; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Threading; namespace NTwain { /// /// Basic class for interfacing with TWAIN. You should only have one of this per application process. /// public class TwainSession : ITwainSessionInternal { /// /// Initializes a new instance of the class. /// /// The supported groups. public TwainSession(DataGroups supportedGroups) : this(TWIdentity.CreateFromAssembly(supportedGroups, Assembly.GetEntryAssembly())) { } /// /// Initializes a new instance of the class. /// /// The app id that represents calling application. /// public TwainSession(TWIdentity appId) { if (appId == null) { throw new ArgumentNullException("appId"); } _appId = appId; ((ITwainSessionInternal)this).ChangeState(1, false); EnforceState = true; MessageLoop.Instance.EnsureStarted(HandleWndProcMessage); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")] object _callbackObj; // kept around so it doesn't get gc'ed TWIdentity _appId; TWUserInterface _twui; static readonly Dictionary __ownedSources = new Dictionary(); internal static TwainSource GetSourceInstance(ITwainSessionInternal session, TWIdentity sourceId) { var key = string.Format(CultureInfo.InvariantCulture, "{0}|{1}|{2}", sourceId.Manufacturer, sourceId.ProductFamily, sourceId.ProductName); if (__ownedSources.ContainsKey(key)) { return __ownedSources[key]; } return __ownedSources[key] = new TwainSource(session, sourceId); } /// /// Gets or sets the optional synchronization context. /// This allows events to be raised on the thread /// associated with the context. /// /// /// The synchronization context. /// public SynchronizationContext SynchronizationContext { get; set; } #region ITwainSessionInternal Members /// /// Gets the app id used for the session. /// /// The app id. TWIdentity ITwainSessionInternal.AppId { get { 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 ITwainSessionInternal.ChangeState(int newState, bool notifyChange) { _state = newState; if (notifyChange) { OnPropertyChanged("State"); SafeAsyncSyncableRaiseOnEvent(OnStateChanged, StateChanged); } } ICommittable ITwainSessionInternal.GetPendingStateChanger(int newState) { return new TentativeStateCommitable(this, newState); } void ITwainSessionInternal.ChangeSourceId(TwainSource source) { CurrentSource = source; OnPropertyChanged("SourceId"); SafeAsyncSyncableRaiseOnEvent(OnSourceChanged, SourceChanged); } void ITwainSessionInternal.SafeSyncableRaiseEvent(DataTransferredEventArgs e) { SafeSyncableRaiseOnEvent(OnDataTransferred, DataTransferred, e); } void ITwainSessionInternal.SafeSyncableRaiseEvent(TransferErrorEventArgs e) { SafeSyncableRaiseOnEvent(OnTransferError, TransferError, e); } void ITwainSessionInternal.SafeSyncableRaiseEvent(TransferReadyEventArgs e) { SafeSyncableRaiseOnEvent(OnTransferReady, TransferReady, e); } #endregion #region ITwainSession Members /// /// Gets the currently open source. /// /// /// The current source. /// public TwainSource CurrentSource { get; private set; } /// /// Gets or sets the default source for this application. /// While this can be get as long as the session is open, /// it can only be set at State 3. /// /// /// The default source. /// public TwainSource DefaultSource { get { TWIdentity id; if (((ITwainSessionInternal)this).DGControl.Identity.GetDefault(out id) == ReturnCode.Success) { return GetSourceInstance(this, id); } return null; } set { if (value != null) { ((ITwainSessionInternal)this).DGControl.Identity.Set(value.Identity); } } } /// /// Try to show the built-in source selector dialog and return the selected source. /// This is not recommended and is only included for completeness. /// /// public TwainSource ShowSourceSelector() { TWIdentity id; if (((ITwainSessionInternal)this).DGControl.Identity.UserSelect(out id) == ReturnCode.Success) { return GetSourceInstance(this, id); } return null; } int _state; /// /// Gets the current state number as defined by the TWAIN spec. /// /// /// The state. /// public int State { get { return _state; } private set { if (value > 0 && value < 8) { _state = value; OnPropertyChanged("State"); SafeAsyncSyncableRaiseOnEvent(OnStateChanged, StateChanged); } } } #endregion #region ITwainOperation Members DGAudio _dgAudio; /// /// Gets the triplet operations defined for audio data group. /// DGAudio ITwainSessionInternal.DGAudio { get { if (_dgAudio == null) { _dgAudio = new DGAudio(this); } return _dgAudio; } } DGControl _dgControl; DGControl ITwainSessionInternal.DGControl { get { if (_dgControl == null) { _dgControl = new DGControl(this); } return _dgControl; } } DGImage _dgImage; DGImage ITwainSessionInternal.DGImage { get { if (_dgImage == null) { _dgImage = new DGImage(this); } return _dgImage; } } DGCustom _dgCustom; DGCustom ITwainSessionInternal.DGCustom { get { if (_dgCustom == null) { _dgCustom = new DGCustom(this); } return _dgCustom; } } /// /// 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 with a TWAIN session. /// /// public ReturnCode Open() { var rc = ReturnCode.Failure; MessageLoop.Instance.Invoke(() => { Debug.WriteLine(string.Format(CultureInfo.InvariantCulture, "Thread {0}: OpenManager.", Thread.CurrentThread.ManagedThreadId)); rc = ((ITwainSessionInternal)this).DGControl.Parent.OpenDsm(MessageLoop.Instance.LoopHandle); if (rc == ReturnCode.Success) { // if twain2 then get memory management functions if ((_appId.DataFunctionalities & DataFunctionalities.Dsm2) == DataFunctionalities.Dsm2) { TWEntryPoint entry; rc = ((ITwainSessionInternal)this).DGControl.EntryPoint.Get(out entry); if (rc == ReturnCode.Success) { Platform.MemoryManager = entry; Debug.WriteLine("Using TWAIN2 memory functions."); } else { Close(); } } } }); return rc; } /// /// Closes the data source manager. /// /// public ReturnCode Close() { var rc = ReturnCode.Failure; MessageLoop.Instance.Invoke(() => { Debug.WriteLine(string.Format(CultureInfo.InvariantCulture, "Thread {0}: CloseManager.", Thread.CurrentThread.ManagedThreadId)); rc = ((ITwainSessionInternal)this).DGControl.Parent.CloseDsm(MessageLoop.Instance.LoopHandle); if (rc == ReturnCode.Success) { Platform.MemoryManager = null; } }); return rc; } /// /// Gets list of sources available in the system. /// Only call this at state 2 or higher. /// /// public IEnumerable GetSources() { TWIdentity srcId; var rc = ((ITwainSessionInternal)this).DGControl.Identity.GetFirst(out srcId); while (rc == ReturnCode.Success) { yield return GetSourceInstance(this, srcId); rc = ((ITwainSessionInternal)this).DGControl.Identity.GetNext(out srcId); } } /// /// Gets the manager status. Only call this at state 2 or higher. /// /// public TWStatus GetStatus() { TWStatus stat; ((ITwainSessionInternal)this).DGControl.Status.GetManager(out stat); return stat; } /// /// Gets the manager status. Only call this at state 3 or higher. /// /// public TWStatusUtf8 GetStatusUtf8() { TWStatusUtf8 stat; ((ITwainSessionInternal)this).DGControl.StatusUtf8.GetManager(out stat); return stat; } #endregion #region INotifyPropertyChanged Members /// /// Occurs when a property value changes. /// public event PropertyChangedEventHandler PropertyChanged; /// /// Raises the event. /// /// Name of the property. protected void OnPropertyChanged(string propertyName) { var syncer = SynchronizationContext; if (syncer == null) { try { var hand = PropertyChanged; if (hand != null) { hand(this, new PropertyChangedEventArgs(propertyName)); } } catch { } } else { syncer.Post(o => { try { var hand = PropertyChanged; if (hand != null) { hand(this, new PropertyChangedEventArgs(propertyName)); } } catch { } }, null); } } #endregion #region privileged calls that causes state change in TWAIN /// /// Enables the source to start transferring. /// /// The mode. /// if set to true any driver UI will display as modal. /// The window handle if modal. /// ReturnCode ITwainSessionInternal.EnableSource(SourceEnableMode mode, bool modal, IntPtr windowHandle) { var rc = ReturnCode.Failure; MessageLoop.Instance.Invoke(() => { Debug.WriteLine(string.Format(CultureInfo.InvariantCulture, "Thread {0}: EnableSource.", Thread.CurrentThread.ManagedThreadId)); // app v2.2 or higher uses callback2 if (_appId.ProtocolMajor >= 2 && _appId.ProtocolMinor >= 2) { var cb = new TWCallback2(HandleCallback); var rc2 = ((ITwainSessionInternal)this).DGControl.Callback2.RegisterCallback(cb); if (rc2 == ReturnCode.Success) { Debug.WriteLine("Registered callback2 OK."); _callbackObj = cb; } } else { var cb = new TWCallback(HandleCallback); var rc2 = ((ITwainSessionInternal)this).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) { rc = ((ITwainSessionInternal)this).DGControl.UserInterface.EnableDSUIOnly(_twui); } else { rc = ((ITwainSessionInternal)this).DGControl.UserInterface.EnableDS(_twui); } if (rc != ReturnCode.Success) { _callbackObj = null; } }); return rc; } ReturnCode ITwainSessionInternal.DisableSource() { var rc = ReturnCode.Failure; MessageLoop.Instance.Invoke(() => { Debug.WriteLine(string.Format(CultureInfo.InvariantCulture, "Thread {0}: DisableSource.", Thread.CurrentThread.ManagedThreadId)); rc = ((ITwainSessionInternal)this).DGControl.UserInterface.DisableDS(_twui); if (rc == ReturnCode.Success) { _callbackObj = null; SafeAsyncSyncableRaiseOnEvent(OnSourceDisabled, SourceDisabled); } }); 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(CultureInfo.InvariantCulture, "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. MessageLoop.Instance.Invoke(() => { if (targetState < 7) { ((ITwainSessionInternal)this).DGControl.PendingXfers.EndXfer(new TWPendingXfers()); } if (targetState < 6) { ((ITwainSessionInternal)this).DGControl.PendingXfers.Reset(new TWPendingXfers()); } if (targetState < 5) { ((ITwainSessionInternal)this).DisableSource(); } if (targetState < 4 && CurrentSource != null) { CurrentSource.Close(); } if (targetState < 3) { Close(); } }); 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; /// /// Occurs when an error has been encountered during transfer. /// public event EventHandler TransferError; /// /// Raises event and if applicable marshal it asynchronously to the thread /// without exceptions. /// /// The on event function. /// The handler. void SafeAsyncSyncableRaiseOnEvent(Action onEventFunc, EventHandler handler) { var syncer = SynchronizationContext; if (syncer == null) { try { onEventFunc(); if (handler != null) { handler(this, EventArgs.Empty); } } catch { } } else { syncer.Post(o => { try { onEventFunc(); if (handler != null) { handler(this, EventArgs.Empty); } } catch { } }, null); } } /// /// Raises event and if applicable marshal it synchronously to the thread /// without exceptions. /// /// The type of the event arguments. /// The on event function. /// The handler. /// The TEventArgs instance containing the event data. void SafeSyncableRaiseOnEvent(Action onEventFunc, EventHandler handler, TEventArgs e) where TEventArgs : EventArgs { var syncer = SynchronizationContext; if (syncer == null) { Debug.WriteLine(string.Format(CultureInfo.InvariantCulture, "Trying to raise event {0} on thread {1} without sync.", e.GetType().Name, Thread.CurrentThread.ManagedThreadId)); try { onEventFunc(e); if (handler != null) { handler(this, e); } } catch { } } else { Debug.WriteLine(string.Format(CultureInfo.InvariantCulture, "Trying to raise event {0} on thread {1} with sync.", e.GetType().Name, Thread.CurrentThread.ManagedThreadId)); // on some consumer desktop scanner with poor drivers this can frequently hang. there's nothing I can do here. syncer.Send(o => { try { onEventFunc(e); if (handler != null) { handler(this, e); } } catch { } }, null); } } /// /// Called when changed. /// protected virtual void OnStateChanged() { } /// /// Called when changed. /// protected virtual void OnSourceChanged() { } /// /// Called when source has been disabled (back to state 4). /// protected virtual void OnSourceDisabled() { } /// /// Called when the source has generated an event. /// /// The instance containing the event data. protected virtual void OnDeviceEvent(DeviceEventArgs e) { } /// /// Called when a data transfer is ready. /// /// The instance containing the event data. protected virtual void OnTransferReady(TransferReadyEventArgs e) { } /// /// Called when data has been transferred. /// /// The instance containing the event data. protected virtual void OnDataTransferred(DataTransferredEventArgs e) { } /// /// Called when an error has been encountered during transfer. /// /// The instance containing the event data. protected virtual void OnTransferError(TransferErrorEventArgs e) { } #endregion #region handle twain ds message void HandleWndProcMessage(ref WindowsHook.MESSAGE winMsg, ref bool handled) { // this handles the message from a typical WndProc message loop and check if it's from the TWAIN source. if (_state >= 5) { // 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(winMsg)); Marshal.StructureToPtr(winMsg, msgPtr, false); var evt = new TWEvent(); evt.pEvent = msgPtr; if (handled = (((ITwainSessionInternal)this).DGControl.Event.ProcessEvent(evt) == ReturnCode.DSEvent)) { Debug.WriteLine(string.Format(CultureInfo.InvariantCulture, "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); } } } } ReturnCode HandleCallback(TWIdentity origin, TWIdentity destination, DataGroups dg, DataArgumentType dat, Message msg, IntPtr data) { if (origin != null && CurrentSource != null && origin.Id == CurrentSource.Identity.Id && _state >= 5) { Debug.WriteLine(string.Format(CultureInfo.InvariantCulture, "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. // by using the internal dispatcher this will be the case. MessageLoop.Instance.BeginInvoke(() => { HandleSourceMsg(msg); }); return ReturnCode.Success; } return ReturnCode.Failure; } // final 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; } TransferLogic.DoTransferRoutine(this); break; case Message.DeviceEvent: TWDeviceEvent de; var rc = ((ITwainSessionInternal)this).DGControl.DeviceEvent.Get(out de); if (rc == ReturnCode.Success) { SafeSyncableRaiseOnEvent(OnDeviceEvent, DeviceEvent, 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 ((ITwainSessionInternal)this).DisableSource(); } break; } } #endregion } }