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.Linq; 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 partial class TwainSession { /// /// 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; _ownedSources = new Dictionary(); if (PlatformInfo.Current.IsSupported) { ((ITwainSessionInternal)this).ChangeState(2, false); } #if DEBUG // defaults to false on release since it's only useful during dev EnforceState = true; #endif } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")] object _callbackObj; // kept around so it doesn't get gc'ed TWIdentity _appId; TWUserInterface _twui; // cache generated twain sources so if you get same source from one session it'll return the same object readonly Dictionary _ownedSources; DataSource GetSourceInstance(ITwainSessionInternal session, TWIdentity sourceId) { DataSource source = null; PlatformInfo.Current.Log.Debug("Source id = {0}", sourceId.Id); var key = string.Format(CultureInfo.InvariantCulture, "{0}|{1}|{2}|{3}", sourceId.Id, sourceId.Manufacturer, sourceId.ProductFamily, sourceId.ProductName); if (_ownedSources.ContainsKey(key)) { source = _ownedSources[key]; } else { _ownedSources[key] = source = new DataSource(session, sourceId); } return source; } #region ITwainSession Members /// /// 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; } /// /// [Experimental] Gets or sets the optional synchronization context when not specifying a on . /// This allows events to be raised on the thread associated with the context. This is experimental is not recommended for use. /// /// /// The synchronization context. /// public SynchronizationContext SynchronizationContext { get; set; } /// /// Gets the currently open source. /// /// /// The current source. /// public DataSource 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 DataSource 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 DataSource ShowSourceSelector() { TWIdentity id; if (((ITwainSessionInternal)this).DGControl.Identity.UserSelect(out id) == ReturnCode.Success) { return GetSourceInstance(this, id); } return null; } int _state = 1; /// /// 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); } } } /// /// Gets the named state value as defined by the TWAIN spec. /// /// /// The state. /// public State StateEx { get { return (State)_state; } } /// /// Quick flag to check if the DSM has been opened. /// public bool IsDsmOpen { get { return State > 2; } } /// /// Quick flag to check if a source has been opened. /// public bool IsSourceOpen { get { return State > 3; } } /// /// Quick flag to check if a source has been enabled. /// public bool IsSourceEnabled { get { return State > 4; } } /// /// Quick flag to check if a source is in the transferring state. /// public bool IsTransferring { get { return State > 5; } } /// /// Whether to stop the transfer process when transfer error is encountered. /// May be required on some sources. /// /// /// true to stop on transfer error; otherwise, false. /// public bool StopOnTransferError { get; set; } /// /// Gets the reason a source was disabled (dropped from state 5) if it's due to user action. /// Mostly only or . /// /// /// The dialog result. /// public Message DisableReason { get; private set; } /// /// 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() { return Open(new InternalMessageLoopHook()); } /// /// 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. /// /// The message loop hook. /// /// messageLoopHook public ReturnCode Open(MessageLoopHook messageLoopHook) { if (messageLoopHook == null) { throw new ArgumentNullException("messageLoopHook"); } _msgLoopHook = messageLoopHook; _msgLoopHook.Start(this); var rc = ReturnCode.Failure; _msgLoopHook.Invoke(() => { PlatformInfo.Current.Log.Debug("Thread {0}: OpenManager.", Thread.CurrentThread.ManagedThreadId); rc = ((ITwainSessionInternal)this).DGControl.Parent.OpenDsm(_msgLoopHook.Handle); 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) { PlatformInfo.InternalCurrent.MemoryManager = entry; PlatformInfo.Current.Log.Debug("Using TWAIN2 memory functions."); } else { Close(); } } } }); return rc; } /// /// Closes the data source manager. /// /// public ReturnCode Close() { var rc = ReturnCode.Failure; _msgLoopHook?.Invoke(() => { PlatformInfo.Current.Log.Debug("Thread {0}: CloseManager.", Thread.CurrentThread.ManagedThreadId); rc = ((ITwainSessionInternal)this).DGControl.Parent.CloseDsm(_msgLoopHook.Handle); if (rc == ReturnCode.Success) { PlatformInfo.InternalCurrent.MemoryManager = null; _msgLoopHook.Stop(); _msgLoopHook = null; } }); return rc; } /// /// Gets list of sources available in the system. /// Only call this at state 2 or higher. /// /// public IEnumerable GetSources() { return this; } /// /// Quick shortcut to open a source. /// /// Name of the source. /// public ReturnCode OpenSource(string sourceName) { var curSrc = CurrentSource; if (curSrc != null) { // TODO: close any open sources first } var hit = this.Where(s => string.Equals(s.Name, sourceName)).FirstOrDefault(); if (hit != null) { return hit.Open(); } return ReturnCode.Failure; } /// /// Quick shortcut to open a source. /// /// Id of the source. /// public ReturnCode OpenSource(int sourceId) { var curSrc = CurrentSource; if (curSrc != null) { // TODO: close any open sources first } var hit = this.Where(s => s.Id == sourceId).FirstOrDefault(); if (hit != null) { return hit.Open(); } return ReturnCode.Failure; } /// /// 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; } /// /// 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) { PlatformInfo.Current.Log.Debug("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. _msgLoopHook?.Invoke(() => { if (targetState < 7 && CurrentSource != null) { // not sure if really necessary but can't hurt to pin it var pending = new TWPendingXfers(); var handle = GCHandle.Alloc(pending, GCHandleType.Pinned); try { ((ITwainSessionInternal)this).DGControl.PendingXfers.EndXfer(pending); } finally { handle.Free(); } } if (targetState < 6 && CurrentSource != null) { var pending = new TWPendingXfers(); var handle = GCHandle.Alloc(pending, GCHandleType.Pinned); try { ((ITwainSessionInternal)this).DGControl.PendingXfers.Reset(pending); } finally { handle.Free(); } } if (targetState < 5 && CurrentSource != null) { ((ITwainSessionInternal)this).DisableSource(); } if (targetState < 4 && CurrentSource != null) { CurrentSource.Close(); } if (targetState < 3) { Close(); } }); EnforceState = origFlag; } /// /// 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; #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 (Exception ex) { PlatformInfo.Current.Log.Error("PropertyChanged event error.", ex); } } else { syncer.Post(o => { try { var hand = PropertyChanged; if (hand != null) { hand(this, new PropertyChangedEventArgs(propertyName)); } } catch (Exception ex) { PlatformInfo.Current.Log.Error("PropertyChanged event error.", ex); } }, null); } } #endregion #region IEnumerable Members /// /// Gets the enumerator. /// /// public IEnumerator GetEnumerator() { 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); } } #endregion #region IEnumerable Members System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion #region events overridables /// /// 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 (Exception ex) { PlatformInfo.Current.Log.Error("{0} event error.", ex, handler.Method.Name); } } else { syncer.Post(o => { try { onEventFunc(); if (handler != null) { handler(this, EventArgs.Empty); } } catch (Exception ex) { PlatformInfo.Current.Log.Error("{0} event error.", ex, handler.Method.Name); } }, 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) { PlatformInfo.Current.Log.Debug("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 (Exception ex) { PlatformInfo.Current.Log.Error("{0} event error.", ex, handler.Method.Name); } } else { PlatformInfo.Current.Log.Debug("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 (Exception ex) { PlatformInfo.Current.Log.Error("{0} event error.", ex, handler.Method.Name); } }, 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 } }