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; } } }