8

I'm trying to interface with a TWAIN-compliant multifunction printer and scanner, a Canon Pixma MG5750, from C# using the NTwain library. I'm writing a program to scan an image into an Image object.

The scanner has to warm up before scanning the image; it displays the following popup while doing so:

enter image description here

Once this process has finished, it begins scanning the document.

While the program does work, the problem is that this warming up process sometimes takes far too long for no apparent reason, up to a few minutes. This problem never occurs when using Canon's own app, IJ Scan Utility, which uses TWAIN and displays the same dialog, but only for a few seconds.

Is there a TWAIN capability I can use to increase the speed of this warm up process? I've tried changing the ICapXResolution and ICapYResolution, but these only increase the speed of the actual scan after the warm up, not affecting the warm up itself.

My program is shown below. Note that it's a Console app, hence the use of ThreadPool.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using NTwain;
using NTwain.Data;
using System.Drawing;
using System.Threading;

namespace TwainExample
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem(o => TwainWork());
            Console.ReadLine();
        }

        static void TwainWork()
        {
            var identity = TWIdentity.CreateFromAssembly(DataGroups.Image, Assembly.GetEntryAssembly());

            var twain = new TwainSession(identity);
            twain.Open();

            twain.DataTransferred += (s, e) =>
            {
                var stream = e.GetNativeImageStream();
                var image = Image.FromStream(stream);
                // Do things with the image...
            };

            var source = twain.First();
            Console.WriteLine($"Scanning from {source.Name}...");
            var openCode = source.Open();
            Console.WriteLine($"Open: {openCode}");
            source.Enable(SourceEnableMode.NoUI, false, IntPtr.Zero);
        }
    }
}

It outputs:

Scanning from Canon MG5700 series Network...
Open: Success
Aaron Christiansen
  • 11,584
  • 5
  • 52
  • 78
  • There seems to be some awareness of the issue in the code, given that Main() has the [STAThread] attribute. So what happens when you don't use a threadpool thread? – Hans Passant Apr 01 '18 at 14:41
  • Did my updated example work for you? – Fred Kleuver Apr 04 '18 at 22:06
  • @FredKleuver Apologies for not replying, I've been away. I couldn't actually get it to compile; there appear to be a few C# errors in there. I will try tweaking it when I'm back, but I currently don't have access to my scanner to actually test it. – Aaron Christiansen Apr 05 '18 at 08:40
  • 1
    Ah yes sorry, I was editing this in the answer rather than in another file in my solution for reasons I can't remember. SO is not a very good type checker :) I updated my answer it should compile now. – Fred Kleuver Apr 07 '18 at 09:31

3 Answers3

3

I built a web service for a scanner a while ago (also with TWAIN) hosted in a console app with OWIN. I suppose by coincidence it never occurred to me to take the same approach as you, because I just built it as a regular web app and when I noticed a similar problem, I found some examples that locked the scanning process itself to a single thread.

Basically, I don't think you need the [STAThread] attribute or the ThreadPool for that matter. Well, you can keep the ThreadPool if you want your console app to remain responsive.

You also need the device ID to fetch the correct data source if I recall correctly. I've adapted some of my code (there were a lot more parts involved related to setting the scan profile, this is now very naively inlined), try this (with or without ThreadPool):

class Program
{
    static void Main(string[] args)
    {
        var scanner = new TwainScanner();
        scanner.Scan("your device id");
        Console.ReadLine();
    }
}

public sealed class CustomTwainSession : TwainSession
{
    private Exception _error;
    private bool _cancel;
    private ReturnCode _returnCode;
    private DataSource _dataSource;
    private Bitmap _image;

    public Exception Error => _error;
    public bool IsSuccess => _error == null && _returnCode == ReturnCode.Success;
    public Bitmap Bitmap => _image;

    static CustomTwainSession()
    {
        PlatformInfo.Current.PreferNewDSM = false;
    }

    public CustomTwainSession(): base(TwainScanner.TwainAppId)
    {
        _cancel = false;

        TransferReady += OnTransferReady;
        DataTransferred += OnDataTransferred;
        TransferError += OnTransferError;
    }

    public void Start(string deviceId)
    {
        try
        {
            _returnCode = Open();
            if (_returnCode == ReturnCode.Success)
            {

                _dataSource = this.FirstOrDefault(x => x.Name == deviceId);
                if (_dataSource != null)
                {
                    _returnCode = _dataSource.Open();
                    if (_returnCode == ReturnCode.Success)
                    {
                        _returnCode = _dataSource.Enable(SourceEnableMode.NoUI, false, IntPtr.Zero);
                    }
                }
                else
                {
                    throw new Exception($"Device {deviceId} not found.");
                }
            }

        }
        catch (Exception ex)
        {
            _error = ex;
        }

        if (_dataSource != null && IsSourceOpen)
        {
            _dataSource.Close();
        }
        if (IsDsmOpen)
        {
            Close();
        }
    }

    private void OnTransferReady(object sender, TransferReadyEventArgs e)
    {
        _dataSource.Capabilities.CapFeederEnabled.SetValue(BoolType.False);
        _dataSource.Capabilities.CapDuplexEnabled.SetValue(BoolType.False);
        _dataSource.Capabilities.ICapPixelType.SetValue(PixelType.RGB);
        _dataSource.Capabilities.ICapUnits.SetValue(Unit.Inches);
        TWImageLayout imageLayout;
        _dataSource.DGImage.ImageLayout.Get(out imageLayout);
        imageLayout.Frame = new TWFrame
        {
            Left = 0,
            Right = 210 * 0.393701f,
            Top = 0,
            Bottom = 297 * 0.393701f
        };
        _dataSource.DGImage.ImageLayout.Set(imageLayout);
        _dataSource.Capabilities.ICapXResolution.SetValue(150);
        _dataSource.Capabilities.ICapYResolution.SetValue(150);

        if (_cancel)
        {
            e.CancelAll = true;
        }
    }

    private void OnDataTransferred(object sender, DataTransferredEventArgs e)
    {
        using (var output = Image.FromStream(e.GetNativeImageStream()))
        {
            _image = new Bitmap(output);
        }
    }

    private void OnTransferError(object sender, TransferErrorEventArgs e)
    {
        _error = e.Exception;
        _cancel = true;
    }

    public void Dispose()
    {
        _image.Dispose();
    }
}

public sealed class TwainScanner
{
    public static TWIdentity TwainAppId { get; }
    private static CustomTwainSession Session { get; set; }
    static volatile object _locker = new object();

    static TwainScanner()
    {
        TwainAppId = TWIdentity.CreateFromAssembly(DataGroups.Image | DataGroups.Control, Assembly.GetEntryAssembly());
    }

    public Bitmap Scan(string deviceId)
    {
        bool lockWasTaken = false;
        try
        {
            if (Monitor.TryEnter(_locker))
            {
                lockWasTaken = true;

                if (Session != null)
                {
                    Session.Dispose();
                    Session = null;
                }

                Session = new CustomTwainSession();

                Session.Start(deviceId);

                return Session.Bitmap;
            }
            else
            {
                return null;
            }
        }
        finally
        {
            if (lockWasTaken)
            {
                Monitor.Exit(_locker);
            }
        }
    }

}

As I may have eluded, this was some years ago and I did not know all that much about threading at the time. I would probably do it differently but right now I cannot test it with an actual scanner. This is just the locking mechanism I had around my scanning logic and I know it solved the unresponsiveness issue.

Others feel free to shoot holes in this particular approach; I know it's not ideal :)

Fred Kleuver
  • 7,797
  • 2
  • 27
  • 38
  • Unfortunately this didn't work; thanks for answering though. – Aaron Christiansen Apr 01 '18 at 17:26
  • I just realized as I was looking through my old code, you've got some things missing I think. For one, you need the device ID. I've updated my answer. – Fred Kleuver Apr 01 '18 at 19:22
  • This currently crashes with a C++ exception: `The program '[3332] TwainExample.exe' has exited with code -529697949 (0xe06d7363) 'Microsoft C++ Exception'.`. It never invokes `OnTransferReady`, `OnDataTransferred` or `OnTransferError`. I'm beginning to think that this is partially due to Canon's drivers being terrible! I appreciate your persistance with this issue though. – Aaron Christiansen Apr 07 '18 at 13:40
  • Even though this didn't fully fix the problem, I'm still going to award the bounty to it because it gave me a great foundation; turns out the fix was easier than I thought! I'll post a separate answer. – Aaron Christiansen Apr 07 '18 at 13:46
  • 1
    Ah yes! I recall a very similar struggle with an Epson scanner. Took me a while to figure out where to find the Device IDs and then which one to use, in combination with which settings. One thing I learned is that almost nobody knows this stuff (and almost nobody needs to), so of course I stuck around! The bounty is nice, but seeing a pesky issue like this being solved is nicer :) – Fred Kleuver Apr 07 '18 at 14:39
  • Thanks again. May I ask what your project was which used TWAIN, and whether it's open source? You seem to know your stuff about scanners! – Aaron Christiansen Apr 07 '18 at 16:01
  • 1
    The idea was to make a web interface to fully automate certain administration tasks such as doing taxes. The scanner would be controlled via that interface, and text recognition + rules engine would automatically process the scanned documents. The scanning part and rules engine worked beautifully, it was the text recognition that ended up being too unreliable and time-consuming to get to work so I put the project in the fridge. It's in a rough state, I might at some point try to salvage the useful bits and open source it :) – Fred Kleuver Apr 07 '18 at 16:08
2

This was much simpler to fix than I thought it would be!

My Canon multifunction exposes two devices to Windows. On my machine, they are:

  • Canon MG5700 series Network
  • WIA-MG5700 series _C6CA27000000

Use the WIA device, not the Canon one. The WIA device warms up almost instantly!

The code posted by Fred Kleuver works beautifully for scanning, so long as you use the WIA device; it appears to crash when using the Canon one.

Aaron Christiansen
  • 11,584
  • 5
  • 52
  • 78
0

A few things I'd suggest

  1. Check if TwainSession implements IDisposable, if it does, surround with "using".

  2. A handler is being added to twain.DataTransferred but not being removed, extract the lambda expression into it's own method then add a line to remove the handler.

Goose
  • 546
  • 3
  • 7